1055 lines
39 KiB
PHP
1055 lines
39 KiB
PHP
<?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;
|
|
}
|
|
}
|