Overview

Packages

  • None
  • SyncMl

Classes

  • Horde_SyncMl
  • Horde_SyncMl_Backend
  • Horde_SyncMl_Backend_Horde
  • Horde_SyncMl_Command
  • Horde_SyncMl_Command_Alert
  • Horde_SyncMl_Command_Final
  • Horde_SyncMl_Command_Get
  • Horde_SyncMl_Command_Map
  • Horde_SyncMl_Command_Put
  • Horde_SyncMl_Command_Replace
  • Horde_SyncMl_Command_Results
  • Horde_SyncMl_Command_Status
  • Horde_SyncMl_Command_Sync
  • Horde_SyncMl_Command_SyncHdr
  • Horde_SyncMl_ContentHandler
  • Horde_SyncMl_DataStore
  • Horde_SyncMl_Device
  • Horde_SyncMl_Device_Nokia
  • Horde_SyncMl_Device_P800
  • Horde_SyncMl_Device_sync4j
  • Horde_SyncMl_Device_Sync4JMozilla
  • Horde_SyncMl_Device_Synthesis
  • Horde_SyncMl_DeviceInfo
  • Horde_SyncMl_Property
  • Horde_SyncMl_PropertyParameter
  • Horde_SyncMl_State
  • Horde_SyncMl_Sync
  • Horde_SyncMl_SyncElement
  • Horde_SyncMl_Translation
  • Horde_SyncMl_XmlOutput
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * A SyncML Backend provides the interface between the SyncML protocol and an
   4:  * actual calendar or address book application. This "actual application" is
   5:  * called the "data store" in this description.
   6:  *
   7:  * The backend provides the following groups of functions:
   8:  *
   9:  * 1) Access to the datastore
  10:  *    Reading, adding, replacing and deleting of entries.  Also retrieve
  11:  *    information about changes in data store.  This is done via the
  12:  *    retrieveEntry(), addEntry(), replaceEntry(), deleteEntry() and
  13:  *    getServerChanges() methods.
  14:  *
  15:  * 2) User management functions
  16:  *    This is the checkAuthentication() method to verify that a given user
  17:  *    password combination is allowed to access the backend data store, and
  18:  *    the setUser() method which does a "login" to the backend data store if
  19:  *    required by the type of backend data store. Please note that the
  20:  *    password is only transferred once in a sync session, so when handling
  21:  *    the subsequent packets messages, the user may need to be "logged in"
  22:  *    without a password. (Or the session management keeps the user "logged
  23:  *    in").
  24:  *
  25:  * 3) Maintainig the client ID <-> server ID map
  26:  *    The SyncML protocol does not require clients and servers to use the same
  27:  *    primary keys for the data entries. So a map has to be in place to
  28:  *    convert between client primary keys (called cuid's here) and server
  29:  *    primary keys (called suid's). It's up to the server to maintain this
  30:  *    map.  Method for this is createUidMap().
  31:  *
  32:  * 4) Sync anchor handling
  33:  *    After a successful initial sync, the client and server sync timestamps
  34:  *    are stored. This allows to perform subsequent syncs as delta syncs,
  35:  *    where only new changes are replicated. Servers as well as clients need
  36:  *    to be able to store two sync anchors (the client's and the server's) for
  37:  *    a sync. Methods for this are readSyncAnchors() and writeSyncAnchors().
  38:  *
  39:  * 5) Test supporting functions
  40:  *    The SyncML module comes with its own testing framework. All you need to
  41:  *    do is implement the two methods testSetup() and testTearDown() and you
  42:  *    are able to test your backend with all the test cases that are part of
  43:  *    the module.
  44:  *
  45:  * 6) Miscellaneous functions
  46:  *    This involves session handling (sessionStart() and sessionClose()),
  47:  *    logging (logMessage() and logFile()), timestamp creation
  48:  *    (getCurrentTimeStamp()), charset handling (getCharset(), setCharset())
  49:  *    and database identification (isValidDatabaseURI()). For all of these
  50:  *    functions, a default implementation is provided in Horde_SyncMl_Backend.
  51:  *
  52:  * If you want to create a backend for your own appliction, you can either
  53:  * derive from Horde_SyncMl_Backend and implement everything in groups 1 to 5
  54:  * or you derive from Horde_SyncMl_Backend_Sql which implements an example
  55:  * backend based on direct database access using the PEAR MDB2 package. In this
  56:  * case you only need to implement groups 1 to 3 and can use the implementation
  57:  * from Horde_SyncMl_Backend_Sql as a guideline for these functions.
  58:  *
  59:  * Key Concepts
  60:  * ------------
  61:  * In order to successfully create a backend, some understanding of a few key
  62:  * concepts in SyncML and the Horde_SyncMl package are certainly helpful.  So
  63:  * here's some stuff that should make some issues clear (or at lest less
  64:  * obfuscated):
  65:  *
  66:  * 1) DatabaseURIs and Databases
  67:  *    The SyncML protocol itself is completly independant from the data that
  68:  *    is replicated. Normally the data are calendar or address book entries
  69:  *    but it may really be anything from browser bookmarks to comeplete
  70:  *    database tables. An ID (string name) of the database you want to
  71:  *    actually replicate has to be configured in the client. Typically that's
  72:  *    something like 'calendar' or 'tasks'. Client and server must agree on
  73:  *    these names.  In addition this string may be used to provide additional
  74:  *    arguments.  These are provided in a HTTP GET query style: like
  75:  *    tasks?ignorecompletedtasks to replicate only pending tasks. Such a "sync
  76:  *    identifier" is called a DatabaseURI and is really a database name plus
  77:  *    some additional options.
  78:  *    The Horde_SyncMl package completly ignores these options and simply passes
  79:  *    them on to the backend. It's up to the backend to decide what to do with
  80:  *    them. However when dealing with the internal maps (cuid<->suid and sync
  81:  *    anchors), it's most likely to use the database name only rather than the
  82:  *    full databaseURI. The map information saying that server entry
  83:  *    20070101203040xxa@mypc.org has id 768 in the client device is valid for
  84:  *    the database "tasks", not for "tasks?somesillyoptions". So what you
  85:  *    normally do is calling some kind of <code>$database =
  86:  *    $this->normalize($databaseURI)</cod> in every backend method that deals
  87:  *    with databaseURIs and use $database afterwards. However actual usage of
  88:  *    options is up to the backend implementation. SyncML works fine without.
  89:  *
  90:  * 2) Suid and Guid mapping
  91:  *    This is the mapping of client IDs to server IDs and vice versa.  Please
  92:  *    note that this map is per user and per client device: the server entry
  93:  *    20070101203040xxa@mypc.org may have ID 720 in your PDA and AA10FC3A in
  94:  *    your mobile phone.
  95:  *
  96:  * 3) Sync Anchors
  97:  *    @todo describe sync anchors
  98:  *    Have a look at the SyncML spec
  99:  *    http://www.openmobilealliance.org/tech/affiliates/syncml/syncmlindex.html
 100:  *    to find out more.
 101:  *
 102:  * 4) Changes and Timestamps
 103:  *    @todo description of Changes and Timestamps, "mirroring effect"
 104:  *    This is real tricky stuff.
 105:  *    First it's important to know, that the SyncML protocol requires the
 106:  *    ending timestamp of the sync timeframe to be exchanged _before_ the
 107:  *    actual syncing starts. So all changes made during a sync have timestamps
 108:  *    that are in the timeframe for the next upcoming sync.  Data exchange in
 109:  *    a sync session works in two steps: 1st) the clients sends its changes to
 110:  *    the server, 2nd) the server sends its changes to the client.
 111:  *    So when in step 2, the backend datastore API is called with a request
 112:  *    like "give me all changes in the server since the last sync".  Thus you
 113:  *    also get the changes induced by the client in step 1 as well.  You have
 114:  *    to somehow "tag" them to avoid echoing (and thus duplicating) them back
 115:  *    to the client. Simply storing the guids in the session is not
 116:  *    sufficient: the changes are made _after_ the end timestamp (see 1) of
 117:  *    the current sync so you'll dupe them in the next sync.
 118:  *    The current implementation deals with this as follows: whenever a client
 119:  *    induced change is done in the backend, the timestamp for this change is
 120:  *    stored in the cuid<->suid map in an additional field. That's the perfect
 121:  *    place as the tagging needs to be done "per client device": when an add
 122:  *    is received from the PDA it must not be sent back as an add to this
 123:  *    device, but to mobile phone it must be sent.
 124:  *    This is sorted out during the getServerChanges() process: if a server
 125:  *    change has a timestamp that's the same as in the guid<->suid map, it
 126:  *    came from the client and must not be added to the list of changes to be
 127:  *    sent to this client.
 128:  *    See the description of Horde_SyncMl_Backend_Sql::_getChangeTS() for some
 129:  *    more information.
 130:  *
 131:  * 5) Messages and Packages
 132:  *    A message is a single HTTP Request. A package is single "logical
 133:  *    message", a sync step. Normally the two coincide. However due to message
 134:  *    size restrictions one package may be transferred in multiple messages
 135:  *    (HTTP requests).
 136:  *
 137:  * 7) Server mode, client mode and test mode
 138:  *    Per default, a backend is used for an SyncML server. Regarding the
 139:  *    SyncML protocol, the working of client and server is similar, except
 140:  *    that
 141:  *    a) the client initiates the sync requests and the server respons to them,
 142:  *       and
 143:  *    b) the server must maintain the client id<->server id map.
 144:  *
 145:  *    Currently the Horde_SyncMl package is designed to create servers. But
 146:  *    is's an obvious (and straightforward) extension to do it for clients as
 147:  *    well.  And as a client has actually less work to do than a server, the
 148:  *    backend should work for servers _and_ clients. During the sessionStart(),
 149:  *    the backend gets a parameter to let it know whether it's in client or
 150:  *    server mode (or test, see below). When in client mode, it should behave
 151:  *    slightly different:
 152:  *    a) the client doesn't do suid<->cuid mapping, so all invokations to the
 153:  *       map creation method createUidMap().
 154:  *    b) the client has only client ids, no server ids. So all arguments are
 155:  *       considered cuids even when named suid. See the Horde_SyncMl_Backend_Sql
 156:  *       implementation, it's actually not that difficult.
 157:  *
 158:  *    Finally there's the test mode. The test cases consist of replaying
 159:  *    pre-recorded sessions. For that to work, the test script must "simulate"
 160:  *    user entries in the server data store. To do so, it creates a backend in
 161:  *    test mode. This behaves similar to a client: when an server entry is
 162:  *    created (modified) using addEntry() (replaceEntry()), no map entry must
 163:  *    be done.
 164:  *    The test backend uses also the two methods testSetup() and testTearDown()
 165:  *    to create a clean (empty) enviroment for the test user "syncmltest".  See
 166:  *    the Horde_SyncMl_Backend_Sql implementation for details.
 167:  *
 168:  * Copyright 2005-2012 Horde LLC (http://www.horde.org/)
 169:  *
 170:  * See the enclosed file COPYING for license information (LGPL). If you
 171:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 172:  *
 173:  * @author  Karsten Fourmont <karsten@horde.org>
 174:  * @package SyncMl
 175:  */
 176: 
 177: class Horde_SyncMl_Backend
 178: {
 179:     /** Types of logfiles. See logFile() method. */
 180:     const LOGFILE_CLIENTMESSAGE = 1;
 181:     const LOGFILE_SERVERMESSAGE = 2;
 182:     const LOGFILE_DEVINF =        3;
 183:     const LOGFILE_DATA =          4;
 184: 
 185:     /** Backend modes. */
 186:     const MODE_SERVER = 1;
 187:     const MODE_CLIENT = 2;
 188:     const MODE_TEST =   3;
 189: 
 190:     /**
 191:      * The State object.
 192:      *
 193:      * @var Horde_SyncMl_State
 194:      */
 195:     public $state;
 196: 
 197:     /**
 198:      * The concatenated log messages.
 199:      *
 200:      * @var string
 201:      */
 202:     protected $_logtext = '';
 203: 
 204:     /**
 205:      * The directory where debugging information is stored.
 206:      *
 207:      * @see Horde_SyncMl_Backend()
 208:      * @var string
 209:      */
 210:     protected $_debugDir;
 211: 
 212:     /**
 213:      * Whether to save SyncML messages in the debug directory.
 214:      *
 215:      * @see Horde_SyncMl_Backend()
 216:      * @var boolean
 217:      */
 218:     protected $_debugFiles;
 219: 
 220:     /**
 221:      * The log level.
 222:      *
 223:      * @see Horde_SyncMl_Backend()
 224:      * @var string
 225:      */
 226:     protected $_logLevel = 'INFO';
 227: 
 228:     /**
 229:      * The charset used in the SyncML messages.
 230:      *
 231:      * @var string
 232:      */
 233:     protected $_charset;
 234: 
 235:     /**
 236:      * The current user.
 237:      *
 238:      * @var string
 239:      */
 240:     protected $_user;
 241: 
 242:     /**
 243:      * The ID of the client device.
 244:      *
 245:      * This is used for all data access as an ID to allow to distinguish
 246:      * between syncs with different devices.  $this->_user together with
 247:      * $this->_syncDeviceID is used as an additional key for all persistence
 248:      * operations.
 249:      *
 250:      * @var string
 251:      */
 252:     protected $_syncDeviceID;
 253: 
 254:     /**
 255:      * The backend mode. One of the Horde_SyncMl_Backend::MODE_* constants.
 256:      *
 257:      * @var integer
 258:      */
 259:     protected $_backendMode;
 260: 
 261:     /**
 262:      * Constructor.
 263:      *
 264:      * Sets up the default logging mechanism.
 265:      *
 266:      * @param array $params  A hash with parameters. The following are
 267:      *                       supported by the default implementation.
 268:      *                       Individual backends may support other parameters.
 269:      *                       - debug_dir:   A directory to write debug output
 270:      *                                      to. Must be writeable by the web
 271:      *                                      server.
 272:      *                       - debug_files: If true, log all incoming and
 273:      *                                      outgoing packets and data
 274:      *                                      conversions and devinf log in
 275:      *                                      debug_dir.
 276:      *                       - log_level:   Only log entries with at least
 277:      *                                      this level. Defaults to 'INFO'.
 278:      */
 279:     public function __construct($params)
 280:     {
 281:         if (!empty($params['debug_dir']) && is_dir($params['debug_dir'])) {
 282:             $this->_debugDir = $params['debug_dir'];
 283:         }
 284:         $this->_debugFiles = !empty($params['debug_files']);
 285:         if (isset($params['log_level'])) {
 286:             $this->_logLevel = $params['log_level'];
 287:         }
 288: 
 289:         $this->logMessage('Backend of class ' . get_class($this) . ' created', 'DEBUG');
 290:      }
 291: 
 292:     /**
 293:      * Attempts to return a concrete Horde_SyncMl_Backend instance based on $driver.
 294:      *
 295:      * @param string $driver The type of concrete Backend subclass to return.
 296:      *                       The code is dynamically included from
 297:      *                       Backend/$driver.php if no path is given or
 298:      *                       directly with "include_once $driver . '.php'"
 299:      *                       if a path is included. So make sure this parameter
 300:      *                       is "safe" and not directly taken from web input.
 301:      *                       The class in the file must be named
 302:      *                       'Horde_SyncMl_Backend_' . basename($driver) and extend
 303:      *                       Horde_SyncMl_Backend.
 304:      * @param array $params  A hash containing any additional configuration or
 305:      *                       connection parameters a subclass might need.
 306:      *
 307:      * @return Horde_SyncMl_Backend  The newly created concrete Horde_SyncMl_Backend
 308:      *                         instance, or false on an error.
 309:      */
 310:     public function factory($driver, $params = null)
 311:     {
 312:         if (empty($driver) || ($driver == 'none')) {
 313:             return false;
 314:         }
 315: 
 316:         $driver = basename($driver);
 317:         $class = 'Horde_SyncMl_Backend_' . $driver;
 318:         if (class_exists($class)) {
 319:             $backend = new $class($params);
 320:         } else {
 321:             return false;
 322:         }
 323: 
 324:         return $backend;
 325:     }
 326: 
 327:     /**
 328:      * Sets the charset.
 329:      *
 330:      * All data passed to the backend uses this charset and data returned from
 331:      * the backend must use this charset, too.
 332:      *
 333:      * @param string $charset  A valid charset.
 334:      */
 335:     public function setCharset($charset)
 336:     {
 337:         $this->_charset = $charset;
 338:     }
 339: 
 340:     /**
 341:      * Returns the charset.
 342:      *
 343:      * @return string  The charset used when talking to the backend.
 344:      */
 345:     public function getCharset()
 346:     {
 347:         return $this->_charset;
 348:     }
 349: 
 350:     /**
 351:      * Returns the current device's ID.
 352:      *
 353:      * @return string  The device ID.
 354:      */
 355:     public function getSyncDeviceID()
 356:     {
 357:         return $this->_syncDeviceID;
 358:     }
 359: 
 360:     /**
 361:      * Sets the user used for this session.
 362:      *
 363:      * This method is called by SyncML right after sessionStart() when either
 364:      * authentication is accepted via checkAuthentication() or a valid user
 365:      * has been retrieved from the state.  $this->_user together with
 366:      * $this->_syncDeviceID is used as an additional key for all persistence
 367:      * operations.
 368:      * This method may have to force a "login", when the backend doesn't keep
 369:      * auth state within a session or when in test mode.
 370:      *
 371:      * @param string $user  A user name.
 372:      */
 373:     public function setUser($user)
 374:     {
 375:         $this->_user = $user;
 376:     }
 377: 
 378:     /**
 379:      * Returns the current user.
 380:      *
 381:      * @return string  The current user.
 382:      */
 383:     public function getUser()
 384:     {
 385:         return $this->_user;
 386:     }
 387: 
 388:     /**
 389:      * Is called after the Horde_SyncMl_State object has been set up, either
 390:      * restored from the session, or freshly created.
 391:      */
 392:     public function setupState()
 393:     {
 394:     }
 395: 
 396:     /**
 397:      * Starts a PHP session.
 398:      *
 399:      * @param string $syncDeviceID  The device ID.
 400:      * @param string $session_id    The session ID to use.
 401:      * @param integer $backendMode  The backend mode, one of the
 402:      *                              Horde_SyncMl_Backend::MODE_* constants.
 403:      */
 404:     public function sessionStart($syncDeviceID, $sessionId,
 405:                           $backendMode = Horde_SyncMl_Backend::MODE_SERVER)
 406:     {
 407:         $this->_syncDeviceID = $syncDeviceID;
 408:         $this->_backendMode = $backendMode;
 409: 
 410:         // Only the server needs to start a session:
 411:         if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
 412:             $sid = md5($syncDeviceID . $sessionId);
 413:             session_id($sid);
 414:             @session_start();
 415:         }
 416:     }
 417: 
 418:     /**
 419:      * Closes the PHP session.
 420:      */
 421:     public function sessionClose()
 422:     {
 423:         // Only the server needs to start a session:
 424:         if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
 425:             session_unset();
 426:             session_destroy();
 427:         }
 428:     }
 429: 
 430:     /**
 431:      * Returns whether a database URI is valid to be synced with this backend.
 432:      *
 433:      * This default implementation accepts "tasks", "calendar", "notes" and
 434:      * "contacts".  However individual backends may offer replication of
 435:      * different or completly other databases (like browser bookmarks or
 436:      * cooking recipes).
 437:      *
 438:      * @param string $databaseURI  URI of a database. Like calendar, tasks,
 439:      *                             contacts or notes. May include optional
 440:      *                             parameters:
 441:      *                             tasks?options=ignorecompleted.
 442:      *
 443:      * @return boolean  True if a valid URI.
 444:      */
 445:     public function isValidDatabaseURI($databaseURI)
 446:     {
 447:         $database = $this->normalize($databaseURI);
 448: 
 449:         switch($database) {
 450:         case 'tasks':
 451:         case 'calendar':
 452:         case 'notes':
 453:         case 'contacts':
 454:         case 'configuration':
 455:             return true;
 456: 
 457:         default:
 458:             $this->logMessage('Invalid database "' . $database
 459:                               . '". Try tasks, calendar, notes or contacts.', 'ERR');
 460:             return false;
 461:         }
 462:     }
 463: 
 464:     /**
 465:      * Returns entries that have been modified in the server database.
 466:      *
 467:      * @abstract
 468:      *
 469:      * @param string $databaseURI  URI of Database to sync. Like calendar,
 470:      *                             tasks, contacts or notes. May include
 471:      *                             optional parameters:
 472:      *                             tasks?options=ignorecompleted.
 473:      * @param integer $from_ts     Start timestamp.
 474:      * @param integer $to_ts       Exclusive end timestamp. Not yet
 475:      *                             implemented.
 476:      * @param array &$adds         Output array: hash of adds suid => 0
 477:      * @param array &$mods         Output array: hash of modifications
 478:      *                             suid => cuid
 479:      * @param array &$dels         Output array: hash of deletions suid => cuid
 480:      *
 481:      * @return mixed  True on success or a PEAR_Error object.
 482:      */
 483:     public function getServerChanges($databaseURI, $from_ts, $to_ts, &$adds, &$mods,
 484:                               &$dels)
 485:     {
 486:         die('getServerChanges() not implemented!');
 487:     }
 488: 
 489:     /**
 490:      * Retrieves an entry from the backend.
 491:      *
 492:      * @abstract
 493:      *
 494:      * @param string $databaseURI  URI of Database to sync. Like calendar,
 495:      *                             tasks, contacts or notes. May include
 496:      *                             optional parameters:
 497:      *                             tasks?options=ignorecompleted.
 498:      * @param string $suid         Server unique id of the entry: for horde
 499:      *                             this is the guid.
 500:      * @param string $contentType  Content-Type: the MIME type in which the
 501:      *                             public function should return the data.
 502:      * @param array $fields        Hash of field names and Horde_SyncMl_Property
 503:      *                             properties with the requested fields.
 504:      *
 505:      * @return mixed  A string with the data entry or a PEAR_Error object.
 506:      */
 507:     public function retrieveEntry($databaseURI, $suid, $contentType, $fields)
 508:     {
 509:         die('retrieveEntry() not implemented!');
 510:     }
 511: 
 512:     /**
 513:      * Adds an entry into the server database.
 514:      *
 515:      * @abstract
 516:      *
 517:      * @param string $databaseURI  URI of Database to sync. Like calendar,
 518:      *                             tasks, contacts or notes. May include
 519:      *                             optional parameters:
 520:      *                             tasks?options=ignorecompleted.
 521:      * @param string $content      The actual data.
 522:      * @param string $contentType  MIME type of the content.
 523:      * @param string $cuid         Client ID of this entry.
 524:      *
 525:      * @return array  PEAR_Error or suid (Horde guid) of new entry
 526:      */
 527:     public function addEntry($databaseURI, $content, $contentType, $cuid)
 528:     {
 529:         die('addEntry() not implemented!');
 530:     }
 531: 
 532:     /**
 533:      * Replaces an entry in the server database.
 534:      *
 535:      * @abstract
 536:      *
 537:      * @param string $databaseURI  URI of Database to sync. Like calendar,
 538:      *                             tasks, contacts or notes. May include
 539:      *                             optional parameters:
 540:      *                             tasks?options=ignorecompleted.
 541:      * @param string $content      The actual data.
 542:      * @param string $contentType  MIME type of the content.
 543:      * @param string $cuid         Client ID of this entry.
 544:      *
 545:      * @return string  PEAR_Error or server ID (Horde GUID) of modified entry.
 546:      */
 547:     public function replaceEntry($databaseURI, $content, $contentType, $cuid)
 548:     {
 549:         die('replaceEntry() not implemented!');
 550:     }
 551: 
 552:     /**
 553:      * Deletes an entry from the server database.
 554:      *
 555:      * @abstract
 556:      *
 557:      * @param string $databaseURI  URI of Database to sync. Like calendar,
 558:      *                             tasks, contacts or notes. May include
 559:      *                             optional parameters:
 560:      *                             tasks?options=ignorecompleted.
 561:      * @param string $cuid         Client ID of the entry.
 562:      *
 563:      * @return boolean  True on success or false on failed (item not found).
 564:      */
 565:     public function deleteEntry($databaseURI, $cuid)
 566:     {
 567:         die('deleteEntry() not implemented!');
 568:     }
 569: 
 570:     /**
 571:      * Authenticates the user at the backend.
 572:      *
 573:      * For some types of authentications (notably auth:basic) the username
 574:      * gets extracted from the authentication data and is then stored in
 575:      * username.  For security reasons the caller must ensure that this is the
 576:      * username that is used for the session, overriding any username
 577:      * specified in <LocName>.
 578:      *
 579:      * @param string $username    Username as provided in the <SyncHdr>.
 580:      *                            May be overwritten by $credData.
 581:      * @param string $credData    Authentication data provided by <Cred><Data>
 582:      *                            in the <SyncHdr>.
 583:      * @param string $credFormat  Format of data as <Cread><Meta><Format> in
 584:      *                            the <SyncHdr>. Typically 'b64'.
 585:      * @param string $credType    Auth type as provided by <Cred><Meta><Type>
 586:      *                            in the <SyncHdr>. Typically
 587:      *                            'syncml:auth-basic'.
 588:      *
 589:      * @return boolean|string  The user name if authentication succeeded, false
 590:      *                         otherwise.
 591:      */
 592:     public function checkAuthentication(&$username, $credData, $credFormat, $credType)
 593:     {
 594:         if (empty($credData) || empty($credType)) {
 595:             return false;
 596:         }
 597: 
 598:         switch ($credType) {
 599:         case 'syncml:auth-basic':
 600:             list($username, $pwd) = explode(':', base64_decode($credData), 2);
 601:             $this->logMessage('Checking authentication for user ' . $username, 'DEBUG');
 602:             return $this->_checkAuthentication($username, $pwd);
 603: 
 604:         case 'syncml:auth-md5':
 605:             /* syncml:auth-md5 only transfers hash values of passwords.
 606:              * Currently the syncml:auth-md5 hash scheme is not supported
 607:              * by the authentication backend. So we can't use Horde to do
 608:              * authentication. Instead here is a very crude direct manual hook:
 609:              * To allow authentication for a user 'dummy' with password 'sync',
 610:              * run
 611:              * php -r 'print base64_encode(pack("H*",md5("dummy:sync")));'
 612:              * from the command line. Then create an entry like
 613:              *  'dummy' => 'ZD1ZeisPeQs0qipHc9tEsw==' in the users array below,
 614:              * where the value is the command line output.
 615:              * This user/password combination is then accepted for md5-auth.
 616:              */
 617:             $users = array(
 618:                   // example for user dummy with pass pass:
 619:                   // 'dummy' => 'ZD1ZeisPeQs0qipHc9tEsw=='
 620:                           );
 621:             if (empty($users[$username])) {
 622:                 return false;
 623:             }
 624: 
 625:             // @todo: nonce may be specified by client. Use it then.
 626:             $nonce = '';
 627:             if (base64_encode(pack('H*', md5($users[$username] . ':' . $nonce))) === $credData) {
 628:                 return $this->_setAuthenticated($username, $credData);
 629:             }
 630:             return false;
 631: 
 632:         default:
 633:             $this->logMessage('Unsupported authentication type ' . $credType, 'ERR');
 634:             return false;
 635:         }
 636:     }
 637: 
 638:     /**
 639:      * Authenticates the user at the backend.
 640:      *
 641:      * @abstract
 642:      *
 643:      * @param string $username    A user name.
 644:      * @param string $password    A password.
 645:      *
 646:      * @return boolean|string  The user name if authentication succeeded, false
 647:      *                         otherwise.
 648:      */
 649:     protected function _checkAuthentication($username, $password)
 650:     {
 651:         die('_checkAuthentication() not implemented!');
 652:     }
 653: 
 654:     /**
 655:      * Sets a user as being authenticated at the backend.
 656:      *
 657:      * @abstract
 658:      *
 659:      * @param string $username    A user name.
 660:      * @param string $credData    Authentication data provided by <Cred><Data>
 661:      *                            in the <SyncHdr>.
 662:      *
 663:      * @return string  The user name.
 664:      */
 665:     protected function _setAuthenticated($username, $credData)
 666:     {
 667:         die('setAuthenticated() not implemented!');
 668:     }
 669: 
 670:     /**
 671:      * Stores Sync anchors after a successful synchronization to allow two-way
 672:      * synchronization next time.
 673:      *
 674:      * The backend has to store the parameters in its persistence engine
 675:      * where user, syncDeviceID and database are the keys while client and
 676:      * server anchor ar the payload. See readSyncAnchors() for retrieval.
 677:      *
 678:      * @abstract
 679:      *
 680:      * @param string $databaseURI       URI of database to sync. Like calendar,
 681:      *                                  tasks, contacts or notes. May include
 682:      *                                  optional parameters:
 683:      *                                  tasks?options=ignorecompleted.
 684:      * @param string $clientAnchorNext  The client anchor as sent by the
 685:      *                                  client.
 686:      * @param string $serverAnchorNext  The anchor as used internally by the
 687:      *                                  server.
 688:      */
 689:     public function writeSyncAnchors($databaseURI, $clientAnchorNext,
 690:                               $serverAnchorNext)
 691:     {
 692:     }
 693: 
 694:     /**
 695:      * Reads the previously written sync anchors from the database.
 696:      *
 697:      * @abstract
 698:      *
 699:      * @param string $databaseURI  URI of database to sync. Like calendar,
 700:      *                             tasks, contacts or notes. May include
 701:      *                             optional parameters:
 702:      *                             tasks?options=ignorecompleted.
 703:      *
 704:      * @return mixed  Two-element array with client anchor and server anchor as
 705:      *                stored in previous writeSyncAnchor() calls. False if no
 706:      *                data found.
 707:      */
 708:     public function readSyncAnchors($databaseURI)
 709:     {
 710:     }
 711: 
 712:     /**
 713:      * Creates a map entry to map between server and client IDs.
 714:      *
 715:      * If an entry already exists, it is overwritten.
 716:      *
 717:      * @abstract
 718:      *
 719:      * @param string $databaseURI  URI of database to sync. Like calendar,
 720:      *                             tasks, contacts or notes. May include
 721:      *                             optional parameters:
 722:      *                             tasks?options=ignorecompleted.
 723:      * @param string $cuid         Client ID of the entry.
 724:      * @param string $suid         Server ID of the entry.
 725:      * @param integer $timestamp   Optional timestamp. This can be used to
 726:      *                             'tag' changes made in the backend during the
 727:      *                             sync process. This allows to identify these,
 728:      *                             and ensure that these changes are not
 729:      *                             replicated back to the client (and thus
 730:      *                             duplicated). See key concept "Changes and
 731:      *                             timestamps".
 732:      */
 733:     public function createUidMap($databaseURI, $cuid, $suid, $timestamp = 0)
 734:     {
 735:     }
 736: 
 737:     /**
 738:      * Erases all mapping entries for one combination of user, device ID.
 739:      *
 740:      * This is used during SlowSync so that we really sync everything properly
 741:      * and no old mapping entries remain.
 742:      *
 743:      * @abstract
 744:      *
 745:      * @param string $databaseURI  URI of database to sync. Like calendar,
 746:      *                             tasks, contacts or notes. May include
 747:      *                             optional parameters:
 748:      *                             tasks?options=ignorecompleted.
 749:      */
 750:     public function eraseMap($databaseURI)
 751:     {
 752:     }
 753: 
 754:     /**
 755:      * Logs a message in the backend.
 756:      *
 757:      * TODO: This should be done via Horde_Log or the equivalent.
 758:      *
 759:      * @param mixed $message     Either a string or a PEAR_Error object.
 760:      * @param string $file       What file was the log public function called from
 761:      *                           (e.g. __FILE__)?
 762:      * @param integer $line      What line was the log public function called from
 763:      *                           (e.g. __LINE__)?
 764:      * @param integer $priority  The priority of the message. One of:
 765:      *                           - EMERG
 766:      *                           - ALERT
 767:      *                           - CRIT
 768:      *                           - ERR
 769:      *                           - WARN
 770:      *                           - NOTICE
 771:      *                           - INFO
 772:      *                           - DEBUG
 773:      */
 774:     public function logMessage($message, $priority = 'INFO')
 775:     {
 776:         if (is_string($priority)) {
 777:             $priority = defined('Horde_Log::' . $priority)
 778:                 ? constant('Horde_Log::' . $priority)
 779:                 : Horde_Log::INFO;
 780:         }
 781: 
 782:         if (is_string($this->_logLevel)) {
 783:             $loglevel = defined('Horde_Log::' . $this->_logLevel)
 784:                 ? constant('Horde_Log::' . $this->_logLevel)
 785:                 : Horde_Log::INFO;
 786:         } else {
 787:             $loglevel = $this->_logLevel;
 788:         }
 789: 
 790:         if ($priority > $loglevel) {
 791:             return;
 792:         }
 793: 
 794:         // Internal logging to logtext
 795:         if (is_string($this->_logtext)) {
 796:             switch ($priority) {
 797:             case Horde_Log::EMERG:
 798:                 $this->_logtext .= 'EMERG:  ';
 799:                 break;
 800:             case Horde_Log::ALERT:
 801:                 $this->_logtext .= 'ALERT:  ';
 802:                 break;
 803:             case Horde_Log::CRIT:
 804:                 $this->_logtext .= 'CRIT:   ';
 805:                 break;
 806:             case Horde_Log::ERR:
 807:                 $this->_logtext .= 'ERR:    ';
 808:                 break;
 809:             case Horde_Log::WARN:
 810:                 $this->_logtext .= 'WARNING:';
 811:                 break;
 812:             case Horde_Log::NOTICE:
 813:                 $this->_logtext .= 'NOTICE: ';
 814:                 break;
 815:             case Horde_Log::INFO:
 816:                 $this->_logtext .= 'INFO:   ';
 817:                 break;
 818:             case Horde_Log::DEBUG:
 819:                 $this->_logtext .= 'DEBUG:  ';
 820:                 break;
 821:             default:
 822:                 $this->_logtext .= 'UNKNOWN:';
 823:             }
 824:             if (is_string($message)) {
 825:                 $this->_logtext .= $message;
 826:             } elseif (is_a($message, 'PEAR_Error')) {
 827:                 $this->_logtext .= $message->getMessage();
 828:             }
 829:             $this->_logtext .= "\n";
 830:         }
 831:     }
 832: 
 833:     /**
 834:      * Logs data to a file in the debug directory.
 835:      *
 836:      * @param integer $type          The data type. One of the Horde_SyncMl_Backend::LOGFILE_*
 837:      *                               constants.
 838:      * @param string $content        The data content.
 839:      * @param boolean $wbxml         Whether the data is wbxml encoded.
 840:      * @param boolean $sessionClose  Whether this is the last SyncML message
 841:      *                               in a session. Bump the file number.
 842:      */
 843:     public function logFile($type, $content, $wbxml = false, $sessionClose = false)
 844:     {
 845:         if (empty($this->_debugDir) || !$this->_debugFiles) {
 846:             return;
 847:         }
 848: 
 849:         switch ($type) {
 850:         case Horde_SyncMl_Backend::LOGFILE_CLIENTMESSAGE:
 851:             $filename = 'client_';
 852:             $mode = 'wb';
 853:             break;
 854:         case Horde_SyncMl_Backend::LOGFILE_SERVERMESSAGE:
 855:             $filename = 'server_';
 856:             $mode = 'wb';
 857:             break;
 858:         case Horde_SyncMl_Backend::LOGFILE_DEVINF:
 859:             $filename = 'devinf.txt';
 860:             $mode = 'wb';
 861:             break;
 862:         case Horde_SyncMl_Backend::LOGFILE_DATA:
 863:             $filename = 'data.txt';
 864:             $mode = 'a';
 865:             break;
 866:         default:
 867:             // Unkown type. Use $type as filename:
 868:             $filename = $type;
 869:             $mode = 'a';
 870:             break;
 871:         }
 872: 
 873:         if ($type === Horde_SyncMl_Backend::LOGFILE_CLIENTMESSAGE ||
 874:             $type === Horde_SyncMl_Backend::LOGFILE_SERVERMESSAGE) {
 875:             $packetNum = @intval(file_get_contents($this->_debugDir
 876:                                                    . '/packetnum.txt'));
 877:             if (empty($packetNum)) {
 878:                 $packetNum = 10;
 879:             }
 880:             if ($wbxml) {
 881:                 $filename .= $packetNum . '.wbxml';
 882:             } else {
 883:                 $filename .= $packetNum . '.xml';
 884:             }
 885:         }
 886: 
 887:         /* Write file */
 888:         $fp = @fopen($this->_debugDir . '/' . $filename, $mode);
 889:         if ($fp) {
 890:             @fwrite($fp, $content);
 891:             @fclose($fp);
 892:         }
 893: 
 894:         if ($type === Horde_SyncMl_Backend::LOGFILE_CLIENTMESSAGE) {
 895:             $this->logMessage('Started at ' . date('Y-m-d H:i:s')
 896:                               . '. Packet logged in '
 897:                               . $this->_debugDir . '/' . $filename, 'DEBUG');
 898:         }
 899: 
 900:         /* Increase packet number. */
 901:         if ($type === Horde_SyncMl_Backend::LOGFILE_SERVERMESSAGE) {
 902:             $this->logMessage('Finished at ' . date('Y-m-d H:i:s')
 903:                               . '. Packet logged in '
 904:                               . $this->_debugDir . '/' . $filename, 'DEBUG');
 905: 
 906:             $fp = @fopen($this->_debugDir . '/packetnum.txt', 'w');
 907:             if ($fp) {
 908:                 /* When one complete session is finished: go to next 10th. */
 909:                 if ($sessionClose) {
 910:                     $packetNum += 10 - $packetNum % 10;
 911:                 } else {
 912:                     $packetNum += 1;
 913:                 }
 914:                 fwrite($fp, $packetNum);
 915:                 fclose($fp);
 916:             }
 917:         }
 918:     }
 919: 
 920:     /**
 921:      * Cleanup public function called after all message processing is finished.
 922:      *
 923:      * Allows for things like closing databases or flushing logs.  When
 924:      * running in test mode, tearDown() must be called rather than close.
 925:      */
 926:     public function close()
 927:     {
 928:         if (!empty($this->_debugDir)) {
 929:             $f = @fopen($this->_debugDir . '/log.txt', 'a');
 930:             if ($f) {
 931:                 fwrite($f, $this->_logtext . "\n");
 932:                 fclose($f);
 933:             }
 934:         }
 935:         session_write_close();
 936:     }
 937: 
 938:     /**
 939:      * Returns the current timestamp in the same format as used by
 940:      * getServerChanges().
 941:      *
 942:      * Backends can use their own way to represent timestamps, like unix epoch
 943:      * integers or UTC Datetime strings.
 944:      *
 945:      * @return mixed  A timestamp of the current time.
 946:      */
 947:     public function getCurrentTimeStamp()
 948:     {
 949:         /* Use unix epoch as default method for timestamps. */
 950:         return time();
 951:     }
 952: 
 953:     /**
 954:      * Creates a clean test environment in the backend.
 955:      *
 956:      * Ensures there's a user with the given credentials and an empty data
 957:      * store.
 958:      *
 959:      * @abstract
 960:      *
 961:      * @param string $user This user accout has to be created in the backend.
 962:      * @param string $pwd  The password for user $user.
 963:      */
 964:     public function testSetup($user, $pwd)
 965:     {
 966:         die('testSetup() not implemented!');
 967:     }
 968: 
 969:     /**
 970:      * Prepares the test start.
 971:      *
 972:      * @param string $user This user accout has to be created in the backend.
 973:      */
 974:     public function testStart($user)
 975:     {
 976:         die('testStart() not implemented!');
 977:     }
 978: 
 979:     /**
 980:      * Tears down the test environment after the test is run.
 981:      *
 982:      * @abstract
 983:      *
 984:      * Should remove the testuser created during testSetup and all its data.
 985:      */
 986:     public function testTearDown()
 987:     {
 988:         die('testTearDown() not implemented!');
 989:     }
 990: 
 991:     /**
 992:      * Normalizes a databaseURI to a database name, so that
 993:      * _normalize('tasks?ignorecompleted') should return just 'tasks'.
 994:      *
 995:      * @param string $databaseURI  URI of a database. Like calendar, tasks,
 996:      *                             contacts or notes. May include optional
 997:      *                             parameters:
 998:      *                             tasks?options=ignorecompleted.
 999:      *
1000:      * @return string  The normalized database name.
1001:      */
1002:     public function normalize($databaseURI)
1003:     {
1004:         $database = Horde_String::lower(
1005:             basename(preg_replace('|\?.*$|', '', $databaseURI)));
1006: 
1007:         /* Convert some commonly encountered types to a fixed set of known
1008:          * service names: */
1009:         switch($database) {
1010:         case 'contacts':
1011:         case 'contact':
1012:         case 'card':
1013:         case 'scard':
1014:             return 'contacts';
1015:         case 'calendar':
1016:         case 'event':
1017:         case 'events':
1018:         case 'cal':
1019:         case 'scal':
1020:             return 'calendar';
1021:         case 'notes':
1022:         case 'memo':
1023:         case 'note':
1024:         case 'snote':
1025:             return 'notes';
1026:         case 'tasks':
1027:         case 'task':
1028:         case 'stask':
1029:             return 'tasks';
1030:         default:
1031:             return $database;
1032:         }
1033:     }
1034: 
1035:     /**
1036:      * Extracts an HTTP GET like parameter from an URL.
1037:      *
1038:      * Example: <code>getParameter('test?q=1', 'q') == 1</code>
1039:      *
1040:      * @static
1041:      *
1042:      * @param string $url        The complete URL.
1043:      * @param string $parameter  The parameter name to extract.
1044:      * @param string $default    A default value to return if none has been
1045:      *                           provided in the URL.
1046:      */
1047:     public function getParameter($url, $parameter, $default = null)
1048:     {
1049:         if (preg_match('|[&\?]' . $parameter . '=([^&]*)|', $url, $m)) {
1050:             return $m[1];
1051:         }
1052:         return $default;
1053:     }
1054: }
1055: 
API documentation generated by ApiGen