<?php
/**
 * A SyncML Backend provides the interface between the SyncML protocol and an
 * actual calendar or address book application. This "actual application" is
 * called the "data store" in this description.
 *
 * The backend provides the following groups of functions:
 *
 * 1) Access to the datastore
 *    Reading, adding, replacing and deleting of entries.  Also retrieve
 *    information about changes in data store.  This is done via the
 *    retrieveEntry(), addEntry(), replaceEntry(), deleteEntry() and
 *    getServerChanges() methods.
 *
 * 2) User management functions
 *    This is the checkAuthentication() method to verify that a given user
 *    password combination is allowed to access the backend data store, and
 *    the setUser() method which does a "login" to the backend data store if
 *    required by the type of backend data store. Please note that the
 *    password is only transferred once in a sync session, so when handling
 *    the subsequent packets messages, the user may need to be "logged in"
 *    without a password. (Or the session management keeps the user "logged
 *    in").
 *
 * 3) Maintainig the client ID <-> server ID map
 *    The SyncML protocol does not require clients and servers to use the same
 *    primary keys for the data entries. So a map has to be in place to
 *    convert between client primary keys (called cuid's here) and server
 *    primary keys (called suid's). It's up to the server to maintain this
 *    map.  Method for this is createUidMap().
 *
 * 4) Sync anchor handling
 *    After a successful initial sync, the client and server sync timestamps
 *    are stored. This allows to perform subsequent syncs as delta syncs,
 *    where only new changes are replicated. Servers as well as clients need
 *    to be able to store two sync anchors (the client's and the server's) for
 *    a sync. Methods for this are readSyncAnchors() and writeSyncAnchors().
 *
 * 5) Test supporting functions
 *    The SyncML module comes with its own testing framework. All you need to
 *    do is implement the two methods testSetup() and testTearDown() and you
 *    are able to test your backend with all the test cases that are part of
 *    the module.
 *
 * 6) Miscellaneous functions
 *    This involves session handling (sessionStart() and sessionClose()),
 *    logging (logMessage() and logFile()), timestamp creation
 *    (getCurrentTimeStamp()), charset handling (getCharset(), setCharset())
 *    and database identification (isValidDatabaseURI()). For all of these
 *    functions, a default implementation is provided in Horde_SyncMl_Backend.
 *
 * If you want to create a backend for your own appliction, you can either
 * derive from Horde_SyncMl_Backend and implement everything in groups 1 to 5
 * or you derive from Horde_SyncMl_Backend_Sql which implements an example
 * backend based on direct database access using the PEAR MDB2 package. In this
 * case you only need to implement groups 1 to 3 and can use the implementation
 * from Horde_SyncMl_Backend_Sql as a guideline for these functions.
 *
 * Key Concepts
 * ------------
 * In order to successfully create a backend, some understanding of a few key
 * concepts in SyncML and the Horde_SyncMl package are certainly helpful.  So
 * here's some stuff that should make some issues clear (or at lest less
 * obfuscated):
 *
 * 1) DatabaseURIs and Databases
 *    The SyncML protocol itself is completly independant from the data that
 *    is replicated. Normally the data are calendar or address book entries
 *    but it may really be anything from browser bookmarks to comeplete
 *    database tables. An ID (string name) of the database you want to
 *    actually replicate has to be configured in the client. Typically that's
 *    something like 'calendar' or 'tasks'. Client and server must agree on
 *    these names.  In addition this string may be used to provide additional
 *    arguments.  These are provided in a HTTP GET query style: like
 *    tasks?ignorecompletedtasks to replicate only pending tasks. Such a "sync
 *    identifier" is called a DatabaseURI and is really a database name plus
 *    some additional options.
 *    The Horde_SyncMl package completly ignores these options and simply passes
 *    them on to the backend. It's up to the backend to decide what to do with
 *    them. However when dealing with the internal maps (cuid<->suid and sync
 *    anchors), it's most likely to use the database name only rather than the
 *    full databaseURI. The map information saying that server entry
 *    20070101203040xxa@mypc.org has id 768 in the client device is valid for
 *    the database "tasks", not for "tasks?somesillyoptions". So what you
 *    normally do is calling some kind of <code>$database =
 *    $this->normalize($databaseURI)</cod> in every backend method that deals
 *    with databaseURIs and use $database afterwards. However actual usage of
 *    options is up to the backend implementation. SyncML works fine without.
 *
 * 2) Suid and Guid mapping
 *    This is the mapping of client IDs to server IDs and vice versa.  Please
 *    note that this map is per user and per client device: the server entry
 *    20070101203040xxa@mypc.org may have ID 720 in your PDA and AA10FC3A in
 *    your mobile phone.
 *
 * 3) Sync Anchors
 *    @todo describe sync anchors
 *    Have a look at the SyncML spec
 *    http://www.openmobilealliance.org/tech/affiliates/syncml/syncmlindex.html
 *    to find out more.
 *
 * 4) Changes and Timestamps
 *    @todo description of Changes and Timestamps, "mirroring effect"
 *    This is real tricky stuff.
 *    First it's important to know, that the SyncML protocol requires the
 *    ending timestamp of the sync timeframe to be exchanged _before_ the
 *    actual syncing starts. So all changes made during a sync have timestamps
 *    that are in the timeframe for the next upcoming sync.  Data exchange in
 *    a sync session works in two steps: 1st) the clients sends its changes to
 *    the server, 2nd) the server sends its changes to the client.
 *    So when in step 2, the backend datastore API is called with a request
 *    like "give me all changes in the server since the last sync".  Thus you
 *    also get the changes induced by the client in step 1 as well.  You have
 *    to somehow "tag" them to avoid echoing (and thus duplicating) them back
 *    to the client. Simply storing the guids in the session is not
 *    sufficient: the changes are made _after_ the end timestamp (see 1) of
 *    the current sync so you'll dupe them in the next sync.
 *    The current implementation deals with this as follows: whenever a client
 *    induced change is done in the backend, the timestamp for this change is
 *    stored in the cuid<->suid map in an additional field. That's the perfect
 *    place as the tagging needs to be done "per client device": when an add
 *    is received from the PDA it must not be sent back as an add to this
 *    device, but to mobile phone it must be sent.
 *    This is sorted out during the getServerChanges() process: if a server
 *    change has a timestamp that's the same as in the guid<->suid map, it
 *    came from the client and must not be added to the list of changes to be
 *    sent to this client.
 *    See the description of Horde_SyncMl_Backend_Sql::_getChangeTS() for some
 *    more information.
 *
 * 5) Messages and Packages
 *    A message is a single HTTP Request. A package is single "logical
 *    message", a sync step. Normally the two coincide. However due to message
 *    size restrictions one package may be transferred in multiple messages
 *    (HTTP requests).
 *
 * 7) Server mode, client mode and test mode
 *    Per default, a backend is used for an SyncML server. Regarding the
 *    SyncML protocol, the working of client and server is similar, except
 *    that
 *    a) the client initiates the sync requests and the server respons to them,
 *       and
 *    b) the server must maintain the client id<->server id map.
 *
 *    Currently the Horde_SyncMl package is designed to create servers. But
 *    is's an obvious (and straightforward) extension to do it for clients as
 *    well.  And as a client has actually less work to do than a server, the
 *    backend should work for servers _and_ clients. During the sessionStart(),
 *    the backend gets a parameter to let it know whether it's in client or
 *    server mode (or test, see below). When in client mode, it should behave
 *    slightly different:
 *    a) the client doesn't do suid<->cuid mapping, so all invokations to the
 *       map creation method createUidMap().
 *    b) the client has only client ids, no server ids. So all arguments are
 *       considered cuids even when named suid. See the Horde_SyncMl_Backend_Sql
 *       implementation, it's actually not that difficult.
 *
 *    Finally there's the test mode. The test cases consist of replaying
 *    pre-recorded sessions. For that to work, the test script must "simulate"
 *    user entries in the server data store. To do so, it creates a backend in
 *    test mode. This behaves similar to a client: when an server entry is
 *    created (modified) using addEntry() (replaceEntry()), no map entry must
 *    be done.
 *    The test backend uses also the two methods testSetup() and testTearDown()
 *    to create a clean (empty) enviroment for the test user "syncmltest".  See
 *    the Horde_SyncMl_Backend_Sql implementation for details.
 *
 * Copyright 2005-2016 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @author  Karsten Fourmont <karsten@horde.org>
 * @package SyncMl
 */

class Horde_SyncMl_Backend
{
    /** Types of logfiles. See logFile() method. */
    const LOGFILE_CLIENTMESSAGE = 1;
    const LOGFILE_SERVERMESSAGE = 2;
    const LOGFILE_DEVINF =        3;
    const LOGFILE_DATA =          4;

    /** Backend modes. */
    const MODE_SERVER = 1;
    const MODE_CLIENT = 2;
    const MODE_TEST =   3;

    /**
     * The State object.
     *
     * @var Horde_SyncMl_State
     */
    public $state;

    /**
     * The concatenated log messages.
     *
     * @var string
     */
    protected $_logtext = '';

    /**
     * The directory where debugging information is stored.
     *
     * @see Horde_SyncMl_Backend()
     * @var string
     */
    protected $_debugDir;

    /**
     * Whether to save SyncML messages in the debug directory.
     *
     * @see Horde_SyncMl_Backend()
     * @var boolean
     */
    protected $_debugFiles;

    /**
     * The log level.
     *
     * @see Horde_SyncMl_Backend()
     * @var string
     */
    protected $_logLevel = 'INFO';

    /**
     * The charset used in the SyncML messages.
     *
     * @var string
     */
    protected $_charset;

    /**
     * The current user.
     *
     * @var string
     */
    protected $_user;

    /**
     * The ID of the client device.
     *
     * This is used for all data access as an ID to allow to distinguish
     * between syncs with different devices.  $this->_user together with
     * $this->_syncDeviceID is used as an additional key for all persistence
     * operations.
     *
     * @var string
     */
    protected $_syncDeviceID;

    /**
     * The backend mode. One of the Horde_SyncMl_Backend::MODE_* constants.
     *
     * @var integer
     */
    protected $_backendMode;

    /**
     * Constructor.
     *
     * Sets up the default logging mechanism.
     *
     * @param array $params  A hash with parameters. The following are
     *                       supported by the default implementation.
     *                       Individual backends may support other parameters.
     *                       - debug_dir:   A directory to write debug output
     *                                      to. Must be writeable by the web
     *                                      server.
     *                       - debug_files: If true, log all incoming and
     *                                      outgoing packets and data
     *                                      conversions and devinf log in
     *                                      debug_dir.
     *                       - log_level:   Only log entries with at least
     *                                      this level. Defaults to 'INFO'.
     */
    public function __construct($params)
    {
        if (!empty($params['debug_dir']) && is_dir($params['debug_dir'])) {
            $this->_debugDir = $params['debug_dir'];
        }
        $this->_debugFiles = !empty($params['debug_files']);
        if (isset($params['log_level'])) {
            $this->_logLevel = $params['log_level'];
        }

        $this->logMessage('Backend of class ' . get_class($this) . ' created', 'DEBUG');
     }

    /**
     * Attempts to return a concrete Horde_SyncMl_Backend instance based on $driver.
     *
     * @param string $driver The type of concrete Backend subclass to return.
     *                       The code is dynamically included from
     *                       Backend/$driver.php if no path is given or
     *                       directly with "include_once $driver . '.php'"
     *                       if a path is included. So make sure this parameter
     *                       is "safe" and not directly taken from web input.
     *                       The class in the file must be named
     *                       'Horde_SyncMl_Backend_' . basename($driver) and extend
     *                       Horde_SyncMl_Backend.
     * @param array $params  A hash containing any additional configuration or
     *                       connection parameters a subclass might need.
     *
     * @return Horde_SyncMl_Backend  The newly created concrete Horde_SyncMl_Backend
     *                         instance, or false on an error.
     */
    public function factory($driver, $params = null)
    {
        if (empty($driver) || ($driver == 'none')) {
            return false;
        }

        $driver = basename($driver);
        $class = 'Horde_SyncMl_Backend_' . $driver;
        if (class_exists($class)) {
            $backend = new $class($params);
        } else {
            return false;
        }

        return $backend;
    }

    /**
     * Sets the charset.
     *
     * All data passed to the backend uses this charset and data returned from
     * the backend must use this charset, too.
     *
     * @param string $charset  A valid charset.
     */
    public function setCharset($charset)
    {
        $this->_charset = $charset;
    }

    /**
     * Returns the charset.
     *
     * @return string  The charset used when talking to the backend.
     */
    public function getCharset()
    {
        return $this->_charset;
    }

    /**
     * Returns the current device's ID.
     *
     * @return string  The device ID.
     */
    public function getSyncDeviceID()
    {
        return $this->_syncDeviceID;
    }

    /**
     * Sets the user used for this session.
     *
     * This method is called by SyncML right after sessionStart() when either
     * authentication is accepted via checkAuthentication() or a valid user
     * has been retrieved from the state.  $this->_user together with
     * $this->_syncDeviceID is used as an additional key for all persistence
     * operations.
     * This method may have to force a "login", when the backend doesn't keep
     * auth state within a session or when in test mode.
     *
     * @param string $user  A user name.
     */
    public function setUser($user)
    {
        $this->_user = $user;
    }

    /**
     * Returns the current user.
     *
     * @return string  The current user.
     */
    public function getUser()
    {
        return $this->_user;
    }

    /**
     * Is called after the Horde_SyncMl_State object has been set up, either
     * restored from the session, or freshly created.
     */
    public function setupState()
    {
    }

    /**
     * Starts a PHP session.
     *
     * @param string $syncDeviceID  The device ID.
     * @param string $session_id    The session ID to use.
     * @param integer $backendMode  The backend mode, one of the
     *                              Horde_SyncMl_Backend::MODE_* constants.
     */
    public function sessionStart($syncDeviceID, $sessionId,
                          $backendMode = Horde_SyncMl_Backend::MODE_SERVER)
    {
        $this->_syncDeviceID = $syncDeviceID;
        $this->_backendMode = $backendMode;

        // Only the server needs to start a session:
        if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
            $sid = md5($syncDeviceID . $sessionId);
            session_id($sid);
            @session_start();
        }
    }

    /**
     * Closes the PHP session.
     */
    public function sessionClose()
    {
        // Only the server needs to start a session:
        if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
            session_unset();
            session_destroy();
        }
    }

    /**
     * Returns whether a database URI is valid to be synced with this backend.
     *
     * This default implementation accepts "tasks", "calendar", "notes" and
     * "contacts".  However individual backends may offer replication of
     * different or completly other databases (like browser bookmarks or
     * cooking recipes).
     *
     * @param string $databaseURI  URI of a database. Like calendar, tasks,
     *                             contacts or notes. May include optional
     *                             parameters:
     *                             tasks?options=ignorecompleted.
     *
     * @return boolean  True if a valid URI.
     */
    public function isValidDatabaseURI($databaseURI)
    {
        $database = $this->normalize($databaseURI);

        switch($database) {
        case 'tasks':
        case 'calendar':
        case 'notes':
        case 'contacts':
        case 'configuration':
            return true;

        default:
            $this->logMessage('Invalid database "' . $database
                              . '". Try tasks, calendar, notes or contacts.', 'ERR');
            return false;
        }
    }

    /**
     * Returns entries that have been modified in the server database.
     *
     * @abstract
     *
     * @param string $databaseURI  URI of Database to sync. Like calendar,
     *                             tasks, contacts or notes. May include
     *                             optional parameters:
     *                             tasks?options=ignorecompleted.
     * @param integer $from_ts     Start timestamp.
     * @param integer $to_ts       Exclusive end timestamp. Not yet
     *                             implemented.
     * @param array &$adds         Output array: hash of adds suid => 0
     * @param array &$mods         Output array: hash of modifications
     *                             suid => cuid
     * @param array &$dels         Output array: hash of deletions suid => cuid
     *
     * @return mixed  True on success or a PEAR_Error object.
     */
    public function getServerChanges($databaseURI, $from_ts, $to_ts, &$adds, &$mods,
                              &$dels)
    {
        die('getServerChanges() not implemented!');
    }

    /**
     * Retrieves an entry from the backend.
     *
     * @abstract
     *
     * @param string $databaseURI  URI of Database to sync. Like calendar,
     *                             tasks, contacts or notes. May include
     *                             optional parameters:
     *                             tasks?options=ignorecompleted.
     * @param string $suid         Server unique id of the entry: for horde
     *                             this is the guid.
     * @param string $contentType  Content-Type: the MIME type in which the
     *                             public function should return the data.
     * @param array $fields        Hash of field names and Horde_SyncMl_Property
     *                             properties with the requested fields.
     *
     * @return mixed  A string with the data entry or a PEAR_Error object.
     */
    public function retrieveEntry($databaseURI, $suid, $contentType, $fields)
    {
        die('retrieveEntry() not implemented!');
    }

    /**
     * Adds an entry into the server database.
     *
     * @abstract
     *
     * @param string $databaseURI  URI of Database to sync. Like calendar,
     *                             tasks, contacts or notes. May include
     *                             optional parameters:
     *                             tasks?options=ignorecompleted.
     * @param string $content      The actual data.
     * @param string $contentType  MIME type of the content.
     * @param string $cuid         Client ID of this entry.
     *
     * @return array  PEAR_Error or suid (Horde guid) of new entry
     */
    public function addEntry($databaseURI, $content, $contentType, $cuid)
    {
        die('addEntry() not implemented!');
    }

    /**
     * Replaces an entry in the server database.
     *
     * @abstract
     *
     * @param string $databaseURI  URI of Database to sync. Like calendar,
     *                             tasks, contacts or notes. May include
     *                             optional parameters:
     *                             tasks?options=ignorecompleted.
     * @param string $content      The actual data.
     * @param string $contentType  MIME type of the content.
     * @param string $cuid         Client ID of this entry.
     *
     * @return string  PEAR_Error or server ID (Horde GUID) of modified entry.
     */
    public function replaceEntry($databaseURI, $content, $contentType, $cuid)
    {
        die('replaceEntry() not implemented!');
    }

    /**
     * Deletes an entry from the server database.
     *
     * @abstract
     *
     * @param string $databaseURI  URI of Database to sync. Like calendar,
     *                             tasks, contacts or notes. May include
     *                             optional parameters:
     *                             tasks?options=ignorecompleted.
     * @param string $cuid         Client ID of the entry.
     *
     * @return boolean  True on success or false on failed (item not found).
     */
    public function deleteEntry($databaseURI, $cuid)
    {
        die('deleteEntry() not implemented!');
    }

    /**
     * Authenticates the user at the backend.
     *
     * For some types of authentications (notably auth:basic) the username
     * gets extracted from the authentication data and is then stored in
     * username.  For security reasons the caller must ensure that this is the
     * username that is used for the session, overriding any username
     * specified in <LocName>.
     *
     * @param string $username    Username as provided in the <SyncHdr>.
     *                            May be overwritten by $credData.
     * @param string $credData    Authentication data provided by <Cred><Data>
     *                            in the <SyncHdr>.
     * @param string $credFormat  Format of data as <Cread><Meta><Format> in
     *                            the <SyncHdr>. Typically 'b64'.
     * @param string $credType    Auth type as provided by <Cred><Meta><Type>
     *                            in the <SyncHdr>. Typically
     *                            'syncml:auth-basic'.
     *
     * @return boolean|string  The user name if authentication succeeded, false
     *                         otherwise.
     */
    public function checkAuthentication(&$username, $credData, $credFormat, $credType)
    {
        if (empty($credData) || empty($credType)) {
            return false;
        }

        switch ($credType) {
        case 'syncml:auth-basic':
            list($username, $pwd) = explode(':', base64_decode($credData), 2);
            $this->logMessage('Checking authentication for user ' . $username, 'DEBUG');
            return $this->_checkAuthentication($username, $pwd);

        case 'syncml:auth-md5':
            /* syncml:auth-md5 only transfers hash values of passwords.
             * Currently the syncml:auth-md5 hash scheme is not supported
             * by the authentication backend. So we can't use Horde to do
             * authentication. Instead here is a very crude direct manual hook:
             * To allow authentication for a user 'dummy' with password 'sync',
             * run
             * php -r 'print base64_encode(pack("H*",md5("dummy:sync")));'
             * from the command line. Then create an entry like
             *  'dummy' => 'ZD1ZeisPeQs0qipHc9tEsw==' in the users array below,
             * where the value is the command line output.
             * This user/password combination is then accepted for md5-auth.
             */
            $users = array(
                  // example for user dummy with pass pass:
                  // 'dummy' => 'ZD1ZeisPeQs0qipHc9tEsw=='
                          );
            if (empty($users[$username])) {
                return false;
            }

            // @todo: nonce may be specified by client. Use it then.
            $nonce = '';
            if (base64_encode(pack('H*', md5($users[$username] . ':' . $nonce))) === $credData) {
                return $this->_setAuthenticated($username, $credData);
            }
            return false;

        default:
            $this->logMessage('Unsupported authentication type ' . $credType, 'ERR');
            return false;
        }
    }

    /**
     * Authenticates the user at the backend.
     *
     * @abstract
     *
     * @param string $username    A user name.
     * @param string $password    A password.
     *
     * @return boolean|string  The user name if authentication succeeded, false
     *                         otherwise.
     */
    protected function _checkAuthentication($username, $password)
    {
        die('_checkAuthentication() not implemented!');
    }

    /**
     * Sets a user as being authenticated at the backend.
     *
     * @abstract
     *
     * @param string $username    A user name.
     * @param string $credData    Authentication data provided by <Cred><Data>
     *                            in the <SyncHdr>.
     *
     * @return string  The user name.
     */
    protected function _setAuthenticated($username, $credData)
    {
        die('setAuthenticated() not implemented!');
    }

    /**
     * Stores Sync anchors after a successful synchronization to allow two-way
     * synchronization next time.
     *
     * The backend has to store the parameters in its persistence engine
     * where user, syncDeviceID and database are the keys while client and
     * server anchor ar the payload. See readSyncAnchors() for retrieval.
     *
     * @abstract
     *
     * @param string $databaseURI       URI of database to sync. Like calendar,
     *                                  tasks, contacts or notes. May include
     *                                  optional parameters:
     *                                  tasks?options=ignorecompleted.
     * @param string $clientAnchorNext  The client anchor as sent by the
     *                                  client.
     * @param string $serverAnchorNext  The anchor as used internally by the
     *                                  server.
     */
    public function writeSyncAnchors($databaseURI, $clientAnchorNext,
                              $serverAnchorNext)
    {
    }

    /**
     * Reads the previously written sync anchors from the database.
     *
     * @abstract
     *
     * @param string $databaseURI  URI of database to sync. Like calendar,
     *                             tasks, contacts or notes. May include
     *                             optional parameters:
     *                             tasks?options=ignorecompleted.
     *
     * @return mixed  Two-element array with client anchor and server anchor as
     *                stored in previous writeSyncAnchor() calls. False if no
     *                data found.
     */
    public function readSyncAnchors($databaseURI)
    {
    }

    /**
     * Creates a map entry to map between server and client IDs.
     *
     * If an entry already exists, it is overwritten.
     *
     * @abstract
     *
     * @param string $databaseURI  URI of database to sync. Like calendar,
     *                             tasks, contacts or notes. May include
     *                             optional parameters:
     *                             tasks?options=ignorecompleted.
     * @param string $cuid         Client ID of the entry.
     * @param string $suid         Server ID of the entry.
     * @param integer $timestamp   Optional timestamp. This can be used to
     *                             'tag' changes made in the backend during the
     *                             sync process. This allows to identify these,
     *                             and ensure that these changes are not
     *                             replicated back to the client (and thus
     *                             duplicated). See key concept "Changes and
     *                             timestamps".
     */
    public function createUidMap($databaseURI, $cuid, $suid, $timestamp = 0)
    {
    }

    /**
     * Erases all mapping entries for one combination of user, device ID.
     *
     * This is used during SlowSync so that we really sync everything properly
     * and no old mapping entries remain.
     *
     * @abstract
     *
     * @param string $databaseURI  URI of database to sync. Like calendar,
     *                             tasks, contacts or notes. May include
     *                             optional parameters:
     *                             tasks?options=ignorecompleted.
     */
    public function eraseMap($databaseURI)
    {
    }

    /**
     * Logs a message in the backend.
     *
     * TODO: This should be done via Horde_Log or the equivalent.
     *
     * @param mixed $message     Either a string or a PEAR_Error object.
     * @param string $file       What file was the log public function called from
     *                           (e.g. __FILE__)?
     * @param integer $line      What line was the log public function called from
     *                           (e.g. __LINE__)?
     * @param integer $priority  The priority of the message. One of:
     *                           - EMERG
     *                           - ALERT
     *                           - CRIT
     *                           - ERR
     *                           - WARN
     *                           - NOTICE
     *                           - INFO
     *                           - DEBUG
     */
    public function logMessage($message, $priority = 'INFO')
    {
        if (is_string($priority)) {
            $priority = defined('Horde_Log::' . $priority)
                ? constant('Horde_Log::' . $priority)
                : Horde_Log::INFO;
        }

        if (is_string($this->_logLevel)) {
            $loglevel = defined('Horde_Log::' . $this->_logLevel)
                ? constant('Horde_Log::' . $this->_logLevel)
                : Horde_Log::INFO;
        } else {
            $loglevel = $this->_logLevel;
        }

        if ($priority > $loglevel) {
            return;
        }

        // Internal logging to logtext
        if (is_string($this->_logtext)) {
            switch ($priority) {
            case Horde_Log::EMERG:
                $this->_logtext .= 'EMERG:  ';
                break;
            case Horde_Log::ALERT:
                $this->_logtext .= 'ALERT:  ';
                break;
            case Horde_Log::CRIT:
                $this->_logtext .= 'CRIT:   ';
                break;
            case Horde_Log::ERR:
                $this->_logtext .= 'ERR:    ';
                break;
            case Horde_Log::WARN:
                $this->_logtext .= 'WARNING:';
                break;
            case Horde_Log::NOTICE:
                $this->_logtext .= 'NOTICE: ';
                break;
            case Horde_Log::INFO:
                $this->_logtext .= 'INFO:   ';
                break;
            case Horde_Log::DEBUG:
                $this->_logtext .= 'DEBUG:  ';
                break;
            default:
                $this->_logtext .= 'UNKNOWN:';
            }
            if (is_string($message)) {
                $this->_logtext .= $message;
            } elseif (is_a($message, 'PEAR_Error')) {
                $this->_logtext .= $message->getMessage();
            }
            $this->_logtext .= "\n";
        }
    }

    /**
     * Logs data to a file in the debug directory.
     *
     * @param integer $type          The data type. One of the Horde_SyncMl_Backend::LOGFILE_*
     *                               constants.
     * @param string $content        The data content.
     * @param boolean $wbxml         Whether the data is wbxml encoded.
     * @param boolean $sessionClose  Whether this is the last SyncML message
     *                               in a session. Bump the file number.
     */
    public function logFile($type, $content, $wbxml = false, $sessionClose = false)
    {
        if (empty($this->_debugDir) || !$this->_debugFiles) {
            return;
        }

        switch ($type) {
        case Horde_SyncMl_Backend::LOGFILE_CLIENTMESSAGE:
            $filename = 'client_';
            $mode = 'wb';
            break;
        case Horde_SyncMl_Backend::LOGFILE_SERVERMESSAGE:
            $filename = 'server_';
            $mode = 'wb';
            break;
        case Horde_SyncMl_Backend::LOGFILE_DEVINF:
            $filename = 'devinf.txt';
            $mode = 'wb';
            break;
        case Horde_SyncMl_Backend::LOGFILE_DATA:
            $filename = 'data.txt';
            $mode = 'a';
            break;
        default:
            // Unkown type. Use $type as filename:
            $filename = $type;
            $mode = 'a';
            break;
        }

        if ($type === Horde_SyncMl_Backend::LOGFILE_CLIENTMESSAGE ||
            $type === Horde_SyncMl_Backend::LOGFILE_SERVERMESSAGE) {
            $packetNum = @intval(file_get_contents($this->_debugDir
                                                   . '/packetnum.txt'));
            if (empty($packetNum)) {
                $packetNum = 10;
            }
            if ($wbxml) {
                $filename .= $packetNum . '.wbxml';
            } else {
                $filename .= $packetNum . '.xml';
            }
        }

        /* Write file */
        $fp = @fopen($this->_debugDir . '/' . $filename, $mode);
        if ($fp) {
            @fwrite($fp, $content);
            @fclose($fp);
        }

        if ($type === Horde_SyncMl_Backend::LOGFILE_CLIENTMESSAGE) {
            $this->logMessage('Started at ' . date('Y-m-d H:i:s')
                              . '. Packet logged in '
                              . $this->_debugDir . '/' . $filename, 'DEBUG');
        }

        /* Increase packet number. */
        if ($type === Horde_SyncMl_Backend::LOGFILE_SERVERMESSAGE) {
            $this->logMessage('Finished at ' . date('Y-m-d H:i:s')
                              . '. Packet logged in '
                              . $this->_debugDir . '/' . $filename, 'DEBUG');

            $fp = @fopen($this->_debugDir . '/packetnum.txt', 'w');
            if ($fp) {
                /* When one complete session is finished: go to next 10th. */
                if ($sessionClose) {
                    $packetNum += 10 - $packetNum % 10;
                } else {
                    $packetNum += 1;
                }
                fwrite($fp, $packetNum);
                fclose($fp);
            }
        }
    }

    /**
     * Cleanup public function called after all message processing is finished.
     *
     * Allows for things like closing databases or flushing logs.  When
     * running in test mode, tearDown() must be called rather than close.
     */
    public function close()
    {
        if (!empty($this->_debugDir)) {
            $f = @fopen($this->_debugDir . '/log.txt', 'a');
            if ($f) {
                fwrite($f, $this->_logtext . "\n");
                fclose($f);
            }
        }
        session_write_close();
    }

    /**
     * Returns the current timestamp in the same format as used by
     * getServerChanges().
     *
     * Backends can use their own way to represent timestamps, like unix epoch
     * integers or UTC Datetime strings.
     *
     * @return mixed  A timestamp of the current time.
     */
    public function getCurrentTimeStamp()
    {
        /* Use unix epoch as default method for timestamps. */
        return time();
    }

    /**
     * Creates a clean test environment in the backend.
     *
     * Ensures there's a user with the given credentials and an empty data
     * store.
     *
     * @abstract
     *
     * @param string $user This user accout has to be created in the backend.
     * @param string $pwd  The password for user $user.
     */
    public function testSetup($user, $pwd)
    {
        die('testSetup() not implemented!');
    }

    /**
     * Prepares the test start.
     *
     * @param string $user This user accout has to be created in the backend.
     */
    public function testStart($user)
    {
        die('testStart() not implemented!');
    }

    /**
     * Tears down the test environment after the test is run.
     *
     * @abstract
     *
     * Should remove the testuser created during testSetup and all its data.
     */
    public function testTearDown()
    {
        die('testTearDown() not implemented!');
    }

    /**
     * Normalizes a databaseURI to a database name, so that
     * _normalize('tasks?ignorecompleted') should return just 'tasks'.
     *
     * @param string $databaseURI  URI of a database. Like calendar, tasks,
     *                             contacts or notes. May include optional
     *                             parameters:
     *                             tasks?options=ignorecompleted.
     *
     * @return string  The normalized database name.
     */
    public function normalize($databaseURI)
    {
        $database = Horde_String::lower(
            basename(preg_replace('|\?.*$|', '', $databaseURI)));

        /* Convert some commonly encountered types to a fixed set of known
         * service names: */
        switch($database) {
        case 'contacts':
        case 'contact':
        case 'card':
        case 'scard':
            return 'contacts';
        case 'calendar':
        case 'event':
        case 'events':
        case 'cal':
        case 'scal':
            return 'calendar';
        case 'notes':
        case 'memo':
        case 'note':
        case 'snote':
            return 'notes';
        case 'tasks':
        case 'task':
        case 'stask':
            return 'tasks';
        default:
            return $database;
        }
    }

    /**
     * Extracts an HTTP GET like parameter from an URL.
     *
     * Example: <code>getParameter('test?q=1', 'q') == 1</code>
     *
     * @static
     *
     * @param string $url        The complete URL.
     * @param string $parameter  The parameter name to extract.
     * @param string $default    A default value to return if none has been
     *                           provided in the URL.
     */
    public function getParameter($url, $parameter, $default = null)
    {
        if (preg_match('|[&\?]' . $parameter . '=([^&]*)|', $url, $m)) {
            return $m[1];
        }
        return $default;
    }
}
