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: