Overview

Packages

  • None
  • SyncMl

Classes

  • Horde_SyncMl_Backend_Sql
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * Generic SQL based Horde_SyncMl Backend.
   4:  *
   5:  * This can be used as a starting point for a custom backend implementation.
   6:  *
   7:  * Copyright 2006-2012 Horde LLC (http://www.horde.org/)
   8:  *
   9:  * See the enclosed file COPYING for license information (LGPL). If you
  10:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  11:  *
  12:  * @author  Karsten Fourmont <karsten@horde.org>
  13:  * @package SyncMl
  14:  */
  15: 
  16: /*
  17:  * The SQL Database must contain five tables as created by the following SQL
  18:  * script:
  19:  *
  20:  * CREATE DATABASE syncml;
  21:  *
  22:  * USE syncml;
  23:  *
  24:  * CREATE TABLE syncml_data(
  25:  *     syncml_id            VARCHAR(255),
  26:  *     syncml_db            VARCHAR(255),
  27:  *     syncml_uid           VARCHAR(255),
  28:  *     syncml_data          TEXT,
  29:  *     syncml_contenttype   VARCHAR(255),
  30:  *     syncml_created_ts    INTEGER,
  31:  *     syncml_modified_ts   INTEGER
  32:  * );
  33:  *
  34:  * CREATE TABLE syncml_map(
  35:  *     syncml_syncpartner VARCHAR(255),
  36:  *     syncml_db          VARCHAR(255),
  37:  *     syncml_uid         VARCHAR(255),
  38:  *     syncml_cuid        VARCHAR(255),
  39:  *     syncml_suid        VARCHAR(255),
  40:  *     syncml_timestamp   INTEGER
  41:  * );
  42:  *
  43:  * CREATE INDEX syncml_syncpartner_idx ON syncml_map (syncml_syncpartner);
  44:  * CREATE INDEX syncml_db_idx ON syncml_map (syncml_db);
  45:  * CREATE INDEX syncml_uid_idx ON syncml_map (syncml_uid);
  46:  * CREATE INDEX syncml_cuid_idx ON syncml_map (syncml_cuid);
  47:  * CREATE INDEX syncml_suid_idx ON syncml_map (syncml_suid);
  48:  *
  49:  * CREATE TABLE syncml_anchors(
  50:  *     syncml_syncpartner   VARCHAR(255),
  51:  *     syncml_db            VARCHAR(255),
  52:  *     syncml_uid           VARCHAR(255),
  53:  *     syncml_clientanchor  VARCHAR(255),
  54:  *     syncml_serveranchor  VARCHAR(255)
  55:  * );
  56:  *
  57:  * CREATE TABLE syncml_suidlist(
  58:  *     syncml_syncpartner    VARCHAR(255),
  59:  *     syncml_db             VARCHAR(255),
  60:  *     syncml_uid            VARCHAR(255),
  61:  *     syncml_suid           VARCHAR(255)
  62:  * );
  63:  *
  64:  * CREATE TABLE syncml_uids(
  65:  *     syncml_uid      VARCHAR(255),
  66:  *     syncml_password VARCHAR(255)
  67:  * );
  68:  */
  69: 
  70: /**
  71:  */
  72: class Horde_SyncMl_Backend_Sql extends Horde_SyncMl_Backend
  73: {
  74:     /**
  75:      * A PEAR MDB2 instance.
  76:      *
  77:      * @var MDB2
  78:      */
  79:     protected $_db;
  80: 
  81:     /**
  82:      * Constructor.
  83:      *
  84:      * @param array $params  A hash with parameters. In addition to those
  85:      *                       supported by the Horde_SyncMl_Backend class one more
  86:      *                       parameter is required for the database connection:
  87:      *                       'dsn' => connection DSN.
  88:      */
  89:     public function __construct($params)
  90:     {
  91:         parent::__construct($params);
  92: 
  93:         $this->_db = &MDB2::connect($params['dsn']);
  94:         if (is_a($this->_db, 'PEAR_Error')) {
  95:             $this->logMessage($this->_db, 'ERR');
  96:         }
  97:     }
  98: 
  99:     /**
 100:      * Returns whether a database URI is valid to be synced with this backend.
 101:      *
 102:      * @param string $databaseURI  URI of a database. Like calendar, tasks,
 103:      *                             contacts or notes. May include optional
 104:      *                             parameters:
 105:      *                             tasks?options=ignorecompleted.
 106:      *
 107:      * @return boolean  True if a valid URI.
 108:      */
 109:     public function isValidDatabaseURI($databaseURI)
 110:     {
 111:         $database = $this->normalize($databaseURI);
 112: 
 113:         switch($database) {
 114:         case 'tasks';
 115:         case 'calendar';
 116:         case 'notes';
 117:         case 'contacts';
 118:         case 'events':
 119:         case 'memo':
 120:             return true;
 121: 
 122:         default:
 123:             $this->logMessage('Invalid database ' . $database
 124:                               . '. Try tasks, calendar, notes or contacts.', 'ERR');
 125:             return false;
 126:         }
 127:     }
 128: 
 129:     /**
 130:      * Returns entries that have been modified in the server database.
 131:      *
 132:      * @param string $databaseURI  URI of Database to sync. Like calendar,
 133:      *                             tasks, contacts or notes. May include
 134:      *                             optional parameters:
 135:      *                             tasks?options=ignorecompleted.
 136:      * @param integer $from_ts     Start timestamp.
 137:      * @param integer $to_ts       Exclusive end timestamp. Not yet
 138:      *                             implemented.
 139:      * @param array &$adds         Output array: hash of adds suid => 0
 140:      * @param array &$mods         Output array: hash of modifications
 141:      *                             suid => cuid
 142:      * @param array &$dels         Output array: hash of deletions suid => cuid
 143:      *
 144:      * @return mixed  True on success or a PEAR_Error object.
 145:      */
 146:     public function getServerChanges($databaseURI, $from_ts, $to_ts, &$adds, &$mods,
 147:                               &$dels)
 148:     {
 149:         $database = $this->normalize($databaseURI);
 150:         $adds = $mods = $dels = array();
 151: 
 152:         // Handle additions:
 153:         $data = $this->_db->queryAll(
 154:             'SELECT syncml_id, syncml_created_ts from syncml_data '
 155:             . 'WHERE syncml_db = '
 156:             . $this->_db->quote($database, 'text')
 157:             . ' AND syncml_uid = '
 158:             . $this->_db->quote($this->_user, 'text')
 159:             . ' AND syncml_created_ts >= '
 160:             . $this->_db->quote($from_ts, 'integer')
 161:             . ' AND syncml_created_ts < '
 162:             . $this->_db->quote($to_ts, 'integer'));
 163:         if ($this->_checkForError($data)) {
 164:             return $data;
 165:         }
 166: 
 167:         foreach ($data as $d) {
 168:             $suid = $d[0];
 169:             $suid_ts = $d[1];
 170:             $sync_ts = $this->_getChangeTS($databaseURI, $suid);
 171:             if ($sync_ts && $sync_ts >= $suid_ts) {
 172:                 // Change was done by us upon request of client, don't mirror
 173:                 // that back to the client.
 174:                 $this->logMessage("Added to server from client: $suid ignored", 'DEBUG');
 175:                 continue;
 176:             }
 177:             $adds[$suid] = 0;
 178:         }
 179: 
 180:         // Only compile changes on delta sync:
 181:         if ($from_ts > 0) {
 182:             // Handle replaces. We might get IDs that are already in the adds
 183:             // array but that's ok: The calling code takes care to ignore
 184:             // these.
 185:             $data = $this->_db->queryAll(
 186:                 'SELECT syncml_id, syncml_modified_ts from syncml_data '
 187:                 .'WHERE syncml_db = '
 188:                 . $this->_db->quote($database, 'text')
 189:                 . ' AND syncml_uid = '
 190:                 . $this->_db->quote($this->_user, 'text')
 191:                 . ' AND syncml_modified_ts >= '
 192:                 . $this->_db->quote($from_ts, 'integer')
 193:                 . ' AND syncml_modified_ts < '
 194:                 . $this->_db->quote($to_ts, 'integer'));
 195:             if ($this->_checkForError($data)) {
 196:                 return $data;
 197:             }
 198: 
 199:             foreach($data as $d) {
 200:                 // Only the server needs to check the change timestamp do
 201:                 // identify client-sent changes.
 202:                 if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
 203:                     $suid = $d[0];
 204:                     $suid_ts = $d[1];
 205:                     $sync_ts = $this->_getChangeTS($databaseURI, $suid);
 206:                     if ($sync_ts && $sync_ts >= $suid_ts) {
 207:                         // Change was done by us upon request of client, don't
 208:                         // mirror that back to the client.
 209:                         $this->logMessage(
 210:                             "Changed on server after sent from client: $suid ignored", 'DEBUG');
 211:                         continue;
 212:                     }
 213:                     $mods[$suid] = $this->_getCuid($databaseURI, $suid);
 214:                 } else {
 215:                     $mods[$d[0]] = $d[0];
 216:                 }
 217:             }
 218:         }
 219: 
 220:         // Handle deletions:
 221:         // We assume stupid a backend datastore (syncml_data) where deleted
 222:         // items are simply "gone" from the datastore. So we need to do our
 223:         // own bookkeeping to identify entries that have been deleted since
 224:         // the last sync run.
 225:         // This is done by the _trackDeless() helper function: we feed it with
 226:         // a current list of all suids and get the ones missing (and thus
 227:         // deleted) in return.
 228:         $data = $this->_db->queryCol(
 229:             'SELECT syncml_id from syncml_data WHERE syncml_db = '
 230:             . $this->_db->quote($database, 'text')
 231:             . ' AND syncml_uid = '
 232:             . $this->_db->quote($this->_user, 'text'));
 233:         if ($this->_checkForError($data)) {
 234:             return $data;
 235:         }
 236: 
 237:         // Get deleted items and store current items:
 238:         // Only use the deleted information on delta sync. On initial slowsync
 239:         // we just need to call _trackDeletes() once to init the list.
 240:         $data = $this->_trackDeletes($databaseURI, $data);
 241:         if ($this->_checkForError($data)) {
 242:             return $data;
 243:         }
 244: 
 245:         if ($from_ts > 0) {
 246:             foreach($data as $suid) {
 247:                 // Only the server needs to handle the cuid suid map:
 248:                 if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
 249:                     $dels[$suid] = $this->_getCuid($databaseURI, $suid);
 250:                 } else {
 251:                     $dels[$suid] = $suid;
 252:                 }
 253:             }
 254:         }
 255:     }
 256: 
 257:     /**
 258:      * Retrieves an entry from the backend.
 259:      *
 260:      * @param string $databaseURI  URI of Database to sync. Like calendar,
 261:      *                             tasks, contacts or notes. May include
 262:      *                             optional parameters:
 263:      *                             tasks?options=ignorecompleted.
 264:      * @param string $suid         Server unique id of the entry: for horde
 265:      *                             this is the guid.
 266:      * @param string $contentType  Content-Type: the MIME type in which the
 267:      *                             public function should return the data.
 268:      * @param array $fields        Hash of field names and Horde_SyncMl_Property
 269:      *                             properties with the requested fields.
 270:      *
 271:      * @return mixed  A string with the data entry or a PEAR_Error object.
 272:      */
 273:     public function retrieveEntry($databaseURI, $suid, $contentType, $fields)
 274:     {
 275:         $database = $this->normalize($databaseURI);
 276: 
 277:         return $this->_db->queryOne(
 278:             'SELECT syncml_data from syncml_data '
 279:             . 'WHERE syncml_db = '
 280:             . $this->_db->quote($database, 'text')
 281:             . ' AND syncml_uid = '
 282:             . $this->_db->quote($this->_user, 'text')
 283:             . ' AND syncml_id = '
 284:             . $this->_db->quote($suid, 'text'));
 285:     }
 286: 
 287:     /**
 288:      * Adds an entry into the server database.
 289:      *
 290:      * @param string $databaseURI  URI of Database to sync. Like calendar,
 291:      *                             tasks, contacts or notes. May include
 292:      *                             optional parameters:
 293:      *                             tasks?options=ignorecompleted.
 294:      * @param string $content      The actual data.
 295:      * @param string $contentType  MIME type of the content.
 296:      * @param string $cuid         Client ID of this entry.
 297:      *
 298:      * @return array  PEAR_Error or suid (Horde guid) of new entry
 299:      */
 300:     public function addEntry($databaseURI, $content, $contentType, $cuid = null)
 301:     {
 302:         $database = $this->normalize($databaseURI);
 303: 
 304:         // Generate an id (suid). It's also possible to use a database
 305:         // generated primary key here.
 306:         $suid = strval(new Horde_Support_Uuid());
 307:         $created_ts = $this->getCurrentTimeStamp();
 308: 
 309:         $r = $this->_db->exec(
 310:             'INSERT INTO syncml_data (syncml_id, syncml_db, syncml_uid, '
 311:             . 'syncml_data, syncml_contenttype,  syncml_created_ts, '
 312:             . 'syncml_modified_ts) VALUES ('
 313:             . $this->_db->quote($suid, 'text') . ','
 314:             . $this->_db->quote($database, 'text') . ','
 315:             . $this->_db->quote($this->_user, 'text') . ','
 316:             . $this->_db->quote($content, 'text') . ','
 317:             . $this->_db->quote($contentType, 'text') . ','
 318:             . $this->_db->quote($created_ts, 'integer') . ','
 319:             . $this->_db->quote($created_ts, 'integer')
 320:             . ')');
 321:         if ($this->_checkForError($r)) {
 322:             return $r;
 323:         }
 324: 
 325:         // Only the server needs to handle the cuid suid map:
 326:         if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
 327:            $this->createUidMap($databaseURI, $cuid, $suid, $created_ts);
 328:         }
 329:     }
 330: 
 331:     /**
 332:      * Replaces an entry in the server database.
 333:      *
 334:      * @param string $databaseURI  URI of Database to sync. Like calendar,
 335:      *                             tasks, contacts or notes. May include
 336:      *                             optional parameters:
 337:      *                             tasks?options=ignorecompleted.
 338:      * @param string $content      The actual data.
 339:      * @param string $contentType  MIME type of the content.
 340:      * @param string $cuid         Client ID of this entry.
 341:      *
 342:      * @return string  PEAR_Error or server ID (Horde GUID) of modified entry.
 343:      */
 344:     public function replaceEntry($databaseURI, $content, $contentType, $cuid)
 345:     {
 346:         $database = $this->normalize($databaseURI);
 347: 
 348:         if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
 349:             $suid = $this->_getSuid($databaseURI, $cuid);
 350:         } else {
 351:             $suid = $cuid;
 352:         }
 353: 
 354:         if ($suid) {
 355:             // Entry exists: replace current one.
 356:             $modified_ts = $this->getCurrentTimeStamp();
 357:             $r = $this->_db->exec(
 358:                 'UPDATE syncml_data '
 359:                 . 'SET syncml_modified_ts = '
 360:                 . $this->_db->quote($modified_ts, 'integer')
 361:                 . ', syncml_data = '
 362:                 . $this->_db->quote($content, 'text')
 363:                 . ', syncml_contenttype = '
 364:                 . $this->_db->quote($contentType, 'text')
 365:                 . 'WHERE syncml_db = '
 366:                 . $this->_db->quote($database, 'text')
 367:                 . ' AND syncml_uid = '
 368:                 . $this->_db->quote($this->_user, 'text')
 369:                 . ' AND syncml_id = '
 370:                 . $this->_db->quote($suid, 'text'));
 371:             if ($this->_checkForError($r)) {
 372:                 return $r;
 373:             }
 374: 
 375:             // Only the server needs to keep the map:
 376:             if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
 377:                 $this->createUidMap($databaseURI, $cuid, $suid, $modified_ts);
 378:             }
 379:         } else {
 380:             return PEAR::raiseError("No map entry found for client id $cuid replacing on server");
 381:         }
 382: 
 383:         return $suid;
 384:     }
 385: 
 386:     /**
 387:      * Deletes an entry from the server database.
 388:      *
 389:      * @param string $databaseURI  URI of Database to sync. Like calendar,
 390:      *                             tasks, contacts or notes. May include
 391:      *                             optional parameters:
 392:      *                             tasks?options=ignorecompleted.
 393:      * @param string $cuid         Client ID of the entry.
 394:      *
 395:      * @return boolean  True on success or false on failed (item not found).
 396:      */
 397:     public function deleteEntry($databaseURI, $cuid)
 398:     {
 399:         $database = $this->normalize($databaseURI);
 400: 
 401:         // Find ID for this entry:
 402:         if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
 403:             $suid = $this->_getSuid($databaseURI, $cuid);
 404:         } else {
 405:             $suid = $cuid;
 406:         }
 407: 
 408:         if (!is_a($suid, 'PEAR_Error')) {
 409:             // A clever backend datastore would store some information about a
 410:             // deletion so this information can be extracted from the history.
 411:             // However we do a "stupid" datastore here where deleted items are
 412:             // simply gone. This allows us to illustrate the _trackDeletes()
 413:             // bookkeeping mechanism.
 414:             $r = $this->_db->queryOne(
 415:                 'DELETE FROM syncml_data '
 416:                 . ' WHERE syncml_db = '
 417:                 . $this->_db->quote($database, 'text')
 418:                 . ' AND syncml_uid = '
 419:                 . $this->_db->quote($this->_user, 'text')
 420:                 . ' AND syncml_id = '
 421:                 . $this->_db->quote($suid, 'text'));
 422:             if ($this->_checkForError($r)) {
 423:                 return $r;
 424:             }
 425: 
 426:             // Deleted bookkeeping is required for server and client, but not
 427:             // for test mode:
 428:             if ($this->_backendMode != Horde_SyncMl_Backend::MODE_TEST) {
 429:                 $this->_removeFromSuidList($databaseURI, $suid);
 430:             }
 431: 
 432:             // @todo: delete from map!
 433:         } else {
 434:             return false;
 435:         }
 436: 
 437:         if (is_a($r, 'PEAR_Error')) {
 438:             return false;
 439:         }
 440: 
 441:         return true;
 442:     }
 443: 
 444:     /**
 445:      * Authenticates the user at the backend.
 446:      *
 447:      * @param string $username    A user name.
 448:      * @param string $password    A password.
 449:      *
 450:      * @return boolean|string  The user name if authentication succeeded, false
 451:      *                         otherwise.
 452:      */
 453:     protected function _checkAuthentication($username, $password)
 454:     {
 455:             // Empty passwords result in errors for some authentication
 456:             // backends, don't call the backend in this case.
 457:             if ($pwd === '') {
 458:                 return false;
 459:             }
 460:             $r = $this->_db->queryOne(
 461:                 'SELECT syncml_uid FROM syncml_uids'
 462:                 . ' WHERE syncml_uid = '
 463:                 . $this->_db->quote($username, 'text')
 464:                 . ' AND syncml_password = '
 465:                 . $this->_db->quote($pwd, 'text'));
 466:             $this->_checkForError($r);
 467: 
 468:             if ($r === $username) {
 469:                 return $username;
 470:             }
 471:             return false;
 472:     }
 473: 
 474:     /**
 475:      * Sets a user as being authenticated at the backend.
 476:      *
 477:      * @abstract
 478:      *
 479:      * @param string $username    A user name.
 480:      * @param string $credData    Authentication data provided by <Cred><Data>
 481:      *                            in the <SyncHdr>.
 482:      *
 483:      * @return string  The user name.
 484:      */
 485:     protected function _setAuthenticated($username, $credData)
 486:     {
 487:         return $username;
 488:     }
 489: 
 490:     /**
 491:      * Stores Sync anchors after a successful synchronization to allow two-way
 492:      * synchronization next time.
 493:      *
 494:      * The backend has to store the parameters in its persistence engine
 495:      * where user, syncDeviceID and database are the keys while client and
 496:      * server anchor ar the payload. See readSyncAnchors() for retrieval.
 497:      *
 498:      * @param string $databaseURI       URI of database to sync. Like calendar,
 499:      *                                  tasks, contacts or notes. May include
 500:      *                                  optional parameters:
 501:      *                                  tasks?options=ignorecompleted.
 502:      * @param string $clientAnchorNext  The client anchor as sent by the
 503:      *                                  client.
 504:      * @param string $serverAnchorNext  The anchor as used internally by the
 505:      *                                  server.
 506:      */
 507:     public function writeSyncAnchors($databaseURI, $clientAnchorNext,
 508:                               $serverAnchorNext)
 509:     {
 510:         $database = $this->normalize($databaseURI);
 511: 
 512:         // Check if entry exists. If not insert, otherwise update.
 513:         if (!$this->readSyncAnchors($databaseURI)) {
 514:             $r = $this->_db->exec(
 515:                 'INSERT INTO syncml_anchors (syncml_syncpartner, '
 516:                 . 'syncml_db,syncml_uid, syncml_clientanchor, '
 517:                 . 'syncml_serveranchor) VALUES ('
 518:                 . $this->_db->quote($this->_syncDeviceID, 'text') . ', '
 519:                 . $this->_db->quote($database, 'text') . ', '
 520:                 . $this->_db->quote($this->_user, 'text') . ', '
 521:                 . $this->_db->quote($clientAnchorNext, 'text') . ', '
 522:                 . $this->_db->quote($serverAnchorNext, 'text')
 523:                 . ')');
 524:         } else {
 525:             $r = $this->_db->exec(
 526:                 'UPDATE syncml_anchors '
 527:                 . ' SET syncml_clientanchor = '
 528:                 . $this->_db->quote($clientAnchorNext, 'text')
 529:                 . ', syncml_serveranchor = '
 530:                 . $this->_db->quote($serverAnchorNext, 'text')
 531:                 . ' WHERE syncml_syncpartner = '
 532:                 . $this->_db->quote($this->_syncDeviceID, 'text')
 533:                 . ' AND syncml_db = '
 534:                 . $this->_db->quote($database, 'text')
 535:                 . ' AND syncml_uid = '
 536:                 . $this->_db->quote($this->_user, 'text'));
 537:         }
 538:         if ($this->_checkForError($r)) {
 539:             return $r;
 540:         }
 541: 
 542:         return true;
 543:     }
 544: 
 545:     /**
 546:      * Reads the previously written sync anchors from the database.
 547:      *
 548:      * @param string $databaseURI  URI of database to sync. Like calendar,
 549:      *                             tasks, contacts or notes. May include
 550:      *                             optional parameters:
 551:      *                             tasks?options=ignorecompleted.
 552:      *
 553:      * @return mixed  Two-element array with client anchor and server anchor as
 554:      *                stored in previous writeSyncAnchor() calls. False if no
 555:      *                data found.
 556:      */
 557:     public function readSyncAnchors($databaseURI)
 558:     {
 559:         $database = $this->normalize($databaseURI);
 560: 
 561:         $r = $this->_db->queryRow(
 562:             'SELECT syncml_clientanchor, syncml_serveranchor '
 563:             . 'FROM syncml_anchors WHERE syncml_syncpartner = '
 564:             . $this->_db->quote($this->_syncDeviceID, 'text')
 565:             . ' AND syncml_db = '
 566:             . $this->_db->quote($database, 'text')
 567:             . ' AND syncml_uid = '
 568:             . $this->_db->quote($this->_user, 'text'));
 569:         $this->_checkForError($r);
 570: 
 571:         if (!is_array($r)) {
 572:             return false;
 573:         }
 574: 
 575:         return array($r[0], $r[1]);
 576:     }
 577: 
 578:     /**
 579:      * Creates a map entry to map between server and client IDs.
 580:      *
 581:      * If an entry already exists, it is overwritten.
 582:      *
 583:      * @param string $databaseURI  URI of database to sync. Like calendar,
 584:      *                             tasks, contacts or notes. May include
 585:      *                             optional parameters:
 586:      *                             tasks?options=ignorecompleted.
 587:      * @param string $cuid         Client ID of the entry.
 588:      * @param string $suid         Server ID of the entry.
 589:      * @param integer $timestamp   Optional timestamp. This can be used to
 590:      *                             'tag' changes made in the backend during the
 591:      *                             sync process. This allows to identify these,
 592:      *                             and ensure that these changes are not
 593:      *                             replicated back to the client (and thus
 594:      *                             duplicated). See key concept "Changes and
 595:      *                             timestamps".
 596:      */
 597:     public function createUidMap($databaseURI, $cuid, $suid, $timestamp = 0)
 598:     {
 599:         $database = $this->normalize($databaseURI);
 600: 
 601:         // Check if entry exists. If not insert, otherwise update.
 602:         if (!$this->_getSuid($databaseURI, $cuid)) {
 603:             $r = $this->_db->exec(
 604:                 'INSERT INTO syncml_map (syncml_syncpartner, '
 605:                 . 'syncml_db, syncml_uid, syncml_cuid, syncml_suid, '
 606:                 . 'syncml_timestamp) VALUES ('
 607:                 . $this->_db->quote($this->_syncDeviceID, 'text') . ', '
 608:                 . $this->_db->quote($database, 'text') . ', '
 609:                 . $this->_db->quote($this->_user, 'text') . ', '
 610:                 . $this->_db->quote($cuid, 'text') . ', '
 611:                 . $this->_db->quote($suid, 'text') . ', '
 612:                 . $this->_db->quote($timestamp, 'integer')
 613:                 . ')');
 614:         } else {
 615:             $r = $this->_db->exec(
 616:                 'UPDATE syncml_map SET syncml_suid = '
 617:                 . $this->_db->quote($suid, 'text')
 618:                 . ', syncml_timestamp = '
 619:                 . $this->_db->quote($timestamp, 'text')
 620:                 . ' WHERE syncml_syncpartner = '
 621:                 . $this->_db->quote($this->_syncDeviceID, 'text')
 622:                 . ' AND syncml_db = '
 623:                 . $this->_db->quote($database, 'text')
 624:                 . ' AND syncml_uid = '
 625:                 . $this->_db->quote($this->_user, 'text')
 626:                 . ' AND syncml_cuid = '
 627:                 . $this->_db->quote($cuid, 'text'));
 628:         }
 629:         if ($this->_checkForError($r)) {
 630:             return $r;
 631:         }
 632: 
 633:         return true;
 634:     }
 635: 
 636:     /**
 637:      * Retrieves the Server ID for a given Client ID from the map.
 638:      *
 639:      * @param string $databaseURI  URI of database to sync. Like calendar,
 640:      *                             tasks, contacts or notes. May include
 641:      *                             optional parameters:
 642:      *                             tasks?options=ignorecompleted.
 643:      * @param string $cuid         The client ID.
 644:      *
 645:      * @return mixed  The server ID string or false if no entry is found.
 646:      */
 647:     protected function _getSuid($databaseURI, $cuid)
 648:     {
 649:         $database = $this->normalize($databaseURI);
 650: 
 651:         $r = $this->_db->queryOne(
 652:             'SELECT syncml_suid FROM syncml_map '
 653:             . ' WHERE syncml_syncpartner = '
 654:             . $this->_db->quote($this->_syncDeviceID, 'text')
 655:             . ' AND syncml_db = '
 656:             . $this->_db->quote($database, 'text')
 657:             . ' AND syncml_uid = '
 658:             . $this->_db->quote($this->_user, 'text')
 659:             . ' AND syncml_cuid = '
 660:             . $this->_db->quote($cuid, 'text'));
 661:         $this->_checkForError($r);
 662: 
 663:         if (!empty($r)) {
 664:             return $r;
 665:         }
 666: 
 667:         return false;
 668:     }
 669: 
 670:     /**
 671:      * Retrieves the Client ID for a given Server ID from the map.
 672:      *
 673:      * @param string $databaseURI  URI of database to sync. Like calendar,
 674:      *                             tasks, contacts or notes. May include
 675:      *                             optional parameters:
 676:      *                             tasks?options=ignorecompleted.
 677:      * @param string $suid         The server ID.
 678:      *
 679:      * @return mixed  The client ID string or false if no entry is found.
 680:      */
 681:     protected function _getCuid($databaseURI, $suid)
 682:     {
 683:         $database = $this->normalize($databaseURI);
 684: 
 685:         $r = $this->_db->queryOne(
 686:             'SELECT syncml_cuid FROM syncml_map '
 687:             . ' WHERE syncml_syncpartner = '
 688:             . $this->_db->quote($this->_syncDeviceID, 'text')
 689:             . ' AND syncml_db = '
 690:             . $this->_db->quote($database, 'text')
 691:             . ' AND syncml_uid = '
 692:             . $this->_db->quote($this->_user, 'text')
 693:             . ' AND syncml_suid = '
 694:             . $this->_db->quote($suid, 'text'));
 695: 
 696:         $this->_checkForError($r);
 697: 
 698:         if (!empty($r)) {
 699:             return $r;
 700:         }
 701: 
 702:         return false;
 703:     }
 704: 
 705:     /**
 706:      * Returns a timestamp stored in the map for a given Server ID.
 707:      *
 708:      * The timestamp is the timestamp of the last change to this server ID
 709:      * that was done inside a sync session (as a result of a change received
 710:      * by the server). It's important to distinguish changes in the backend a)
 711:      * made by the user during normal operation and b) changes made by SyncML
 712:      * to reflect client updates.  When the server is sending its changes it
 713:      * is only allowed to send type a). However the history feature in the
 714:      * backend my not know if a change is of type a) or type b). So the
 715:      * timestamp is used to differentiate between the two.
 716:      *
 717:      * @param string $databaseURI  URI of database to sync. Like calendar,
 718:      *                             tasks, contacts or notes. May include
 719:      *                             optional parameters:
 720:      *                             tasks?options=ignorecompleted.
 721:      * @param string $suid         The server ID.
 722:      *
 723:      * @return mixed  The previously stored timestamp or false if no entry is
 724:      *                found.
 725:      */
 726:     protected function _getChangeTS($databaseURI, $suid)
 727:     {
 728:         $database = $this->normalize($databaseURI);
 729: 
 730:         $r = $this->_db->queryOne(
 731:             'SELECT syncml_timestamp FROM syncml_map '
 732:             . ' WHERE syncml_syncpartner = '
 733:             . $this->_db->quote($this->_syncDeviceID, 'text')
 734:             . ' AND syncml_db = '
 735:             . $this->_db->quote($database, 'text')
 736:             . ' AND syncml_uid = '
 737:             . $this->_db->quote($this->_user, 'text')
 738:             . ' AND syncml_suid = '
 739:             . $this->_db->quote($suid, 'text'));
 740:         $this->_checkForError($r);
 741: 
 742:         if (!empty($r)) {
 743:             return $r;
 744:         }
 745: 
 746:         return false;
 747:     }
 748: 
 749:     /**
 750:      * Erases all mapping entries for one combination of user, device ID.
 751:      *
 752:      * This is used during SlowSync so that we really sync everything properly
 753:      * and no old mapping entries remain.
 754:      *
 755:      * @param string $databaseURI  URI of database to sync. Like calendar,
 756:      *                             tasks, contacts or notes. May include
 757:      *                             optional parameters:
 758:      *                             tasks?options=ignorecompleted.
 759:      */
 760:     public function eraseMap($databaseURI)
 761:     {
 762:         $database = $this->normalize($databaseURI);
 763: 
 764:         $r = $this->_db->exec(
 765:             'DELETE FROM syncml_map '
 766:             . ' WHERE syncml_syncpartner = '
 767:             . $this->_db->quote($this->_syncDeviceID, 'text')
 768:             . ' AND syncml_db = '
 769:             . $this->_db->quote($database, 'text')
 770:             . ' AND syncml_uid = '
 771:             . $this->_db->quote($this->_user, 'text'));
 772:         if ($this->_checkForError($r)) {
 773:             return $r;
 774:         }
 775: 
 776:         $r = $this->_db->exec(
 777:             'DELETE FROM syncml_suidlist '
 778:             . ' WHERE syncml_syncpartner = '
 779:             . $this->_db->quote($this->_syncDeviceID, 'text')
 780:             . ' AND syncml_db = '
 781:             . $this->_db->quote($database, 'text')
 782:             . ' AND syncml_uid = '
 783:             . $this->_db->quote($this->_user, 'text'));
 784:         if ($this->_checkForError($r)) {
 785:             return $r;
 786:         }
 787: 
 788:         return true;
 789:     }
 790: 
 791:     /**
 792:      * Cleanup public function called after all message processing is finished.
 793:      *
 794:      * Allows for things like closing databases or flushing logs.  When
 795:      * running in test mode, tearDown() must be called rather than close.
 796:      */
 797:     public function close()
 798:     {
 799:         parent::close();
 800:         $this->_db->disconnect();
 801:     }
 802: 
 803:     /**
 804:      * Checks if the parameter is a PEAR_Error object and if so logs the
 805:      * error.
 806:      *
 807:      * @param mixed $o  An object or value to check.
 808:      *
 809:      * @return mixed  The error object if an error has been passed or false if
 810:      *                no error has been passed.
 811:      */
 812:     protected function _checkForError($o)
 813:     {
 814:         if (is_a($o, 'PEAR_Error')) {
 815:             $this->logMessage($o);
 816:             return $o;
 817:         }
 818:         return false;
 819:     }
 820: 
 821:     /**
 822:      * Returns a list of item IDs that have been deleted since the last sync
 823:      * run and stores a complete list of IDs for next sync run.
 824:      *
 825:      * Some backend datastores don't keep information about deleted entries.
 826:      * So we have to create a workaround that finds out what entries have been
 827:      * deleted since the last sync run. This method provides this
 828:      * functionality: it is called with a list of all IDs currently in the
 829:      * database. It then compares this list with its own previously stored
 830:      * list of IDs to identify those missing (and thus deleted). The passed
 831:      * list is then stored for the next invocation.
 832:      *
 833:      * @param string $databaseURI  URI of database to sync. Like calendar,
 834:      *                             tasks, contacts or notes. May include
 835:      *                             optional parameters:
 836:      *                             tasks?options=ignorecompleted.
 837:      * @param array $currentSuids  Array of all SUIDs (primary keys) currently
 838:      *                             in the server datastore.
 839:      *
 840:      * @return array  Array of all entries that have been deleted since the
 841:      *                last call.
 842:      */
 843:     protected function _trackDeletes($databaseURI, $currentSuids)
 844:     {
 845:         $database = $this->normalize($databaseURI);
 846:         if (!is_array($currentSuids)) {
 847:             $currentSuids = array();
 848:         }
 849: 
 850:         $this->logMessage('_trackDeletes() with ' . count($currentSuids)
 851:                           . ' current ids', 'DEBUG');
 852: 
 853:         $r = $this->_db->queryCol(
 854:             'SELECT syncml_suid FROM syncml_suidlist '
 855:             . ' WHERE syncml_syncpartner = '
 856:             . $this->_db->quote($this->_syncDeviceID, 'text')
 857:             . ' AND syncml_db = '
 858:             . $this->_db->quote($database, 'text')
 859:             . ' AND syncml_uid = '
 860:             . $this->_db->quote($this->_user, 'text'));
 861:         if ($this->_checkForError($r)) {
 862:             return $r;
 863:         }
 864: 
 865:         $this->logMessage('_trackDeletes() found ' . count($r)
 866:                           . ' items in prevlist', 'DEBUG');
 867: 
 868:         // Convert to hash with suid as key.
 869:         if (is_array($r)) {
 870:             $prevSuids = array_flip($r);
 871:         } else {
 872:             $prevSuids = array();
 873:         }
 874: 
 875:         foreach ($currentSuids as $suid) {
 876:             if (isset($prevSuids[$suid])) {
 877:                 // Entry is there now and in $prevSuids. Unset in $prevSuids
 878:                 // array so we end up with only those in $prevSuids that are
 879:                 // no longer there now.
 880:                 unset($prevSuids[$suid]);
 881:             } else {
 882:                 // Entry is there now but not in $prevSuids. New entry, store
 883:                 // in syncml_suidlist
 884:                 $r = $this->_db->exec(
 885:                     'INSERT INTO syncml_suidlist '
 886:                     . ' (syncml_syncpartner, syncml_db, syncml_uid, '
 887:                     . 'syncml_suid) VALUES ('
 888:                     . $this->_db->quote($this->_syncDeviceID, 'text') . ', '
 889:                     . $this->_db->quote($database, 'text') . ', '
 890:                     . $this->_db->quote($this->_user, 'text') . ', '
 891:                     . $this->_db->quote($suid, 'text')
 892:                     . ')');
 893:                 if ($this->_checkForError($r)) {
 894:                     return $r;
 895:                 }
 896:             }
 897:         }
 898: 
 899:         // $prevSuids now contains the deleted suids. Remove those from
 900:         // syncml_suidlist so we have a current list of all existing suids.
 901:         foreach ($prevSuids as $suid => $cuid) {
 902:             $r = $this->_removeFromSuidList($databaseURI, $suid);
 903:         }
 904: 
 905:         $this->logMessage('_trackDeletes() with ' . count($prevSuids)
 906:                           . ' deleted items', 'DEBUG');
 907: 
 908:         return array_keys($prevSuids);
 909:     }
 910: 
 911:     /**
 912:      * Removes a suid from the suidlist.
 913:      *
 914:      * Called by _trackDeletes() when updating the suidlist and deleteEntry()
 915:      * when removing an entry due to a client request.
 916:      *
 917:      * @param string $databaseURI  URI of database to sync. Like calendar,
 918:      *                             tasks, contacts or notes. May include
 919:      *                             optional parameters:
 920:      *                             tasks?options=ignorecompleted.
 921:      * @param array $suid          The suid to remove from the list.
 922:      */
 923:     protected function _removeFromSuidList($databaseURI, $suid)
 924:     {
 925:         $database = $this->normalize($databaseURI);
 926: 
 927:         $this->logMessage('_removeFromSuidList(): item ' . $suid, 'DEBUG');
 928:         $r = $this->_db->queryCol(
 929:             'DELETE FROM syncml_suidlist '
 930:             . 'WHERE syncml_syncpartner = '
 931:             . $this->_db->quote($this->_syncDeviceID, 'text')
 932:             . ' AND syncml_db = '
 933:             . $this->_db->quote($database, 'text')
 934:             . ' AND syncml_uid = '
 935:             . $this->_db->quote($this->_user, 'text')
 936:             . ' AND syncml_suid = '
 937:             . $this->_db->quote($suid, 'text'));
 938:         if ($this->_checkForError($r)) {
 939:             return $r;
 940:         }
 941: 
 942:         $this->logMessage('_removeFromSuidList(): result ' . implode('!', $r), 'DEBUG');
 943: 
 944:         return true;
 945:     }
 946: 
 947:     /**
 948:      * Creates a clean test environment in the backend.
 949:      *
 950:      * Ensures there's a user with the given credentials and an empty data
 951:      * store.
 952:      *
 953:      * @param string $user This user accout has to be created in the backend.
 954:      * @param string $pwd  The password for user $user.
 955:      */
 956:     public function testSetup($user, $pwd)
 957:     {
 958:         $this->_user = $user;
 959:         $this->_cleanUser($user);
 960:         $this->_backend->_user = $user;
 961: 
 962:         $r = $this->_db->exec(
 963:             'INSERT INTO syncml_uids (syncml_uid, syncml_password)'
 964:             . ' VALUES ('
 965:             . $this->_db->quote($user, 'text') . ', '
 966:             . $this->_db->quote($pwd, 'text') . ')');
 967:         $this->_checkForError($r);
 968:     }
 969: 
 970:     /**
 971:      * Prepares the test start.
 972:      *
 973:      * @param string $user This user accout has to be created in the backend.
 974:      */
 975:     public function testStart($user)
 976:     {
 977:         $this->_user = $user;
 978:         $this->_backend->_user = $user;
 979:     }
 980: 
 981:     /**
 982:      * Tears down the test environment after the test is run.
 983:      *
 984:      * Should remove the testuser created during testSetup and all its data.
 985:      */
 986:     public function testTearDown()
 987:     {
 988:         $this->_cleanUser($this->_user);
 989:         $this->_db->disconnect();
 990:     }
 991: 
 992:     /* Database access functions. The following methods are not part of the
 993:      * backend API. They are here to illustrate how a backend application
 994:      * (like a web calendar) has to modify the data with respect to the
 995:      * history. There are three functions:
 996:      * addEntry_backend(), replaceEntry_backend(), deleteEntry_backend().
 997:      * They are very similar to the API methods above, but don't use cuids or
 998:      * syncDeviceIDs as these are only relevant for syncing. */
 999: 
1000:     /**
1001:      * Adds an entry into the server database.
1002:      *
1003:      * @param string $user         The username to use. Not strictly necessery
1004:      *                             to store this, but it helps for the test
1005:      *                             environment to clean up all entries for a
1006:      *                             test user.
1007:      * @param string $databaseURI  URI of Database to sync. Like calendar,
1008:      *                             tasks, contacts or notes. May include
1009:      *                             optional parameters:
1010:      *                             tasks?options=ignorecompleted.
1011:      * @param string $content      The actual data.
1012:      * @param string $contentType  MIME type of the content.
1013:      *
1014:      * @return array  PEAR_Error or suid of new entry.
1015:      */
1016:     public function addEntry_backend($user, $databaseURI, $content, $contentType)
1017:     {
1018:         $database = $this->normalize($databaseURI);
1019: 
1020:         // Generate an id (suid). It's also possible to use a database
1021:         // generated primary key here. */
1022:         $suid = strval(new Horde_Support_Uuid());
1023: 
1024:         $created_ts = $this->getCurrentTimeStamp();
1025:         $r = $this->_db->exec(
1026:             'INSERT INTO syncml_data (syncml_id, syncml_db, syncml_uid, '
1027:             . 'syncml_data, syncml_contenttype, syncml_created_ts, '
1028:             . 'syncml_modified_ts) VALUES ('
1029:             . $this->_db->quote($suid, 'text') . ', '
1030:             . $this->_db->quote($database, 'text') . ', '
1031:             . $this->_db->quote($user, 'text') . ', '
1032:             . $this->_db->quote($content, 'text') . ', '
1033:             . $this->_db->quote($contentType, 'text') . ', '
1034:             . $this->_db->quote($created_ts, 'integer') . ', '
1035:             . $this->_db->quote($created_ts, 'integer')
1036:             . ')');
1037:         if ($this->_checkForError($r)) {
1038:             return $r;
1039:         }
1040: 
1041:         return $suid;
1042:     }
1043: 
1044:     /**
1045:      * Replaces an entry in the server database.
1046:      *
1047:      * @param string $user         The username to use. Not strictly necessery
1048:      *                             to store this but, it helps for the test
1049:      *                             environment to clean up all entries for a
1050:      *                             test user.
1051:      * @param string $databaseURI  URI of Database to sync. Like calendar,
1052:      *                             tasks, contacts or notes. May include
1053:      *                             optional parameters:
1054:      *                             tasks?options=ignorecompleted.
1055:      * @param string $content      The actual data.
1056:      * @param string $contentType  MIME type of the content.
1057:      * @param string $suid         Server ID of this entry.
1058:      *
1059:      * @return string  PEAR_Error or suid of modified entry.
1060:      */
1061:     public function replaceEntry_backend($user, $databaseURI, $content, $contentType,
1062:                                   $suid)
1063:     {
1064:         $database = $this->normalize($databaseURI);
1065:         $modified_ts = $this->getCurrentTimeStamp();
1066: 
1067:         // Entry exists: replace current one.
1068:         $r = $this->_db->exec(
1069:             'UPDATE syncml_data '
1070:             . 'SET syncml_modified_ts = '
1071:             . $this->_db->quote($modified_ts, 'integer')
1072:             . ',syncml_data = '
1073:             . $this->_db->quote($content, 'text')
1074:             . ',syncml_contenttype = '
1075:             . $this->_db->quote($contentType, 'text')
1076:             . 'WHERE syncml_db = '
1077:             . $this->_db->quote($database, 'text')
1078:             . ' AND syncml_uid = '
1079:             . $this->_db->quote($user, 'text')
1080:             . ' AND syncml_id = '
1081:             . $this->_db->quote($suid, 'text'));
1082:         if ($this->_checkForError($r)) {
1083:             return $r;
1084:         }
1085: 
1086:         return $suid;
1087:     }
1088: 
1089:     /**
1090:      * Deletes an entry from the server database.
1091:      *
1092:      * @param string $user         The username to use. Not strictly necessery
1093:      *                             to store this, but it helps for the test
1094:      *                             environment to clean up all entries for a
1095:      *                             test user.
1096:      * @param string $databaseURI  URI of Database to sync. Like calendar,
1097:      *                             tasks, contacts or notes. May include
1098:      *                             optional parameters:
1099:      *                             tasks?options=ignorecompleted.
1100:      * @param string $suid         Server ID of the entry.
1101:      *
1102:      * @return boolean  True on success or false on failed (item not found).
1103:      */
1104:     public function deleteEntry_backend($user, $databaseURI, $suid)
1105:     {
1106:         $database = $this->normalize($databaseURI);
1107: 
1108:         $r = $this->_db->queryOne(
1109:             'DELETE FROM syncml_data '
1110:             . 'WHERE syncml_db = '
1111:             . $this->_db->quote($database, 'text')
1112:             . ' AND syncml_uid = '
1113:             . $this->_db->quote($user, 'text')
1114:             . ' AND syncml_id = '
1115:             . $this->_db->quote($suid, 'text'));
1116:         if ($this->_checkForError($r)) {
1117:             return false;
1118:         }
1119: 
1120:         return true;
1121:     }
1122: 
1123:     protected function _cleanUser($user)
1124:     {
1125:         $r = $this->_db->exec('DELETE FROM syncml_data WHERE syncml_uid = '
1126:                               . $this->_db->quote($user, 'text'));
1127:         $this->_checkForError($r);
1128: 
1129:         $r = $this->_db->exec('DELETE FROM syncml_map WHERE syncml_uid = '
1130:                               . $this->_db->quote($user, 'text'));
1131:         $this->_checkForError($r);
1132: 
1133:         $r = $this->_db->exec('DELETE FROM syncml_anchors WHERE syncml_uid = '
1134:                               . $this->_db->quote($user, 'text'));
1135:         $this->_checkForError($r);
1136: 
1137:         $r = $this->_db->exec('DELETE FROM syncml_uids WHERE syncml_uid = '
1138:                               . $this->_db->quote($user, 'text'));
1139:         $this->_checkForError($r);
1140: 
1141:         $r = $this->_db->exec('DELETE FROM syncml_suidlist WHERE syncml_uid = '
1142:                               . $this->_db->quote($user, 'text'));
1143:         $this->_checkForError($r);
1144:     }
1145: }
1146: 
API documentation generated by ApiGen