1: <?php
2: /**
3: * SyncML Backend for the Horde Application framework.
4: *
5: * Copyright 2005-2012 Horde LLC (http://www.horde.org/)
6: *
7: * See the enclosed file COPYING for license information (LGPL). If you did not
8: * receive this file, see http://www.horde.org/licenses/lgpl21.
9: *
10: * @author Karsten Fourmont <karsten@horde.org>
11: * @package SyncMl
12: */
13: class Horde_SyncMl_Backend_Horde extends Horde_SyncMl_Backend
14: {
15: /**
16: * A database instance.
17: *
18: * @var Horde_Db_Adapter_Base
19: */
20: protected $_db;
21:
22: /**
23: * The session ID used in the Horde session.
24: *
25: * @var string
26: */
27: protected $_sessionId;
28:
29: /**
30: * Constructor.
31: *
32: * Initializes the logger.
33: *
34: * @param array $params Any parameters the backend might need.
35: */
36: public function __construct($params)
37: {
38: parent::__construct($params);
39:
40: $this->_db = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Db')->create();
41: }
42:
43: /**
44: * Sets the user used for this session.
45: *
46: * @param string $user A user name.
47: */
48: public function setUser($user)
49: {
50: parent::setUser($user);
51:
52: if ($this->_backendMode == Horde_SyncMl_Backend::MODE_TEST) {
53: /* After a session the user gets automatically logged out, so we
54: * have to login again. */
55: $GLOBALS['registry']->setAuth($this->_user, array());
56: }
57: }
58:
59: /**
60: * Starts a PHP session.
61: *
62: * @param string $syncDeviceID The device ID.
63: * @param string $session_id The session ID to use.
64: * @param integer $backendMode The backend mode, one of the
65: * Horde_SyncMl_Backend::MODE_* constants.
66: */
67: public function sessionStart($syncDeviceID, $sessionId,
68: $backendMode = Horde_SyncMl_Backend::MODE_SERVER)
69: {
70: $this->_backendMode = $backendMode;
71: $this->_syncDeviceID = $syncDeviceID;
72: $this->_sessionId = md5($syncDeviceID . $sessionId);
73:
74: /* Only the server needs to start a session. */
75: if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
76: /* Reload the Horde SessionHandler if necessary. */
77: $GLOBALS['session']->setup(true, null, $this->_sessionId);
78: $this->state = $GLOBALS['session']->get('horde', 'syncml');
79: }
80: }
81:
82: public function close()
83: {
84: if ($this->state) {
85: $GLOBALS['session']->set('horde', 'syncml', $this->state);
86: }
87: parent::close();
88: }
89:
90: protected function _fastsync($databaseURI, $from_ts, $to_ts)
91: {
92: global $registry;
93:
94: $results = array(
95: 'adds' => array(),
96: 'mods' => array(),
97: 'dels' => array());
98:
99: $map = array(
100: 'adds' => 'add',
101: 'dels' => 'delete',
102: 'mods' => 'modify');
103:
104: $database = $this->normalize($databaseURI);
105:
106: // Get ALL server changes from backend
107: try {
108: $changes = $registry->{$database}->getChanges($from_ts, $to_ts);
109: } catch (Horde_Exception $e) {
110: $this->logMessage(
111: sprintf(
112: ' %s getChanges() failed during _fastSync: %s', $database, $e->getMessage()),
113: 'ERR');
114: }
115:
116: $add_ts = array();
117: foreach (array_keys($results) as $type) {
118: foreach ($changes[$map[$type]] as $suid) {
119: // Only server needs to check for client sent entries:
120: if ($this->_backendMode != Horde_SyncMl_Backend::MODE_SERVER) {
121: switch ($type) {
122: case 'adds':
123: $id = 0;
124: break;
125: case 'mods':
126: case 'dels':
127: $id = $suid;
128: }
129: $results[$type][$suid] = $id;
130: continue;
131: }
132:
133: try {
134: $change_ts = $registry->{$database}->getActionTimestamp(
135: $suid, $map[$type], Horde_SyncMl_Backend::getParameter($databaseURI, 'source'));
136: } catch (Horde_Exception $e) {
137: $this->logMessage($e->getMessage(), 'ERR');
138: return;
139: }
140: // If added, then deleted all since last sync, don't bother
141: // sending change
142: if ($type == 'adds') {
143: $add_ts[$suid] = $change_ts;
144: } elseif ($type == 'dels') {
145: if (isset($results['adds'][$suid]) && $add_ts[$suid] < $change_ts) {
146: unset($results['adds'][$suid]);
147: continue;
148: }
149: if (isset($results['mods'][$suid])) {
150: unset($results['mods'][$suid]);
151: }
152: }
153:
154: $sync_ts = $this->_getChangeTS($database, $suid);
155: if ($sync_ts && $sync_ts >= $change_ts) {
156: // Change was done by us upon request of client. Don't
157: // mirror that back to the client.
158: $this->logMessage(
159: "Added to server from client: $suid ignored", 'DEBUG');
160: continue;
161: }
162:
163: // Sanity check and prepare list of changes
164: if ($type != 'adds') {
165: $cuid = $this->_getCuid($database, $suid);
166: if (empty($cuid) && $type == 'mods') {
167: $this->logMessage(
168: "Unable to create change for server id $suid: client id not found in map, adding instead.", 'WARN');
169: $results['adds'][$suid] = 0;
170: continue;
171: } elseif (empty($cuid) && $type == 'dels') {
172: $this->logMessage(
173: "Unable to create delete for server id $suid: client id not found in map", 'WARN');
174: continue;
175: } else {
176: $id = $cuid;
177: }
178: } else {
179: $id = 0;
180: }
181: $results[$type][$suid] = $id;
182: }
183: }
184:
185: return $results;
186: }
187:
188: protected function _slowsync($databaseURI, $from_ts, $to_ts)
189: {
190: global $registry;
191:
192: $results = array(
193: 'adds' => array(),
194: 'dels' => array(),
195: 'mods' => array());
196:
197: $database = $this->normalize($databaseURI);
198:
199: // Return all db entries directly rather than bother history. But
200: // first check if we only want to sync data from a given start
201: // date:
202: $start = trim(Horde_SyncMl_Backend::getParameter($databaseURI, 'start'));
203: try {
204: if (!empty($start)) {
205: if (strlen($start) == 4) {
206: $start .= '0101000000';
207: } elseif (strlen($start) == 6) {
208: $start .= '01000000';
209: } elseif (strlen($start) == 8) {
210: $start .= '000000';
211: }
212: $start = new Horde_Date($start);
213: $this->logMessage('Slow-syncing all events starting from ' . (string)$start, 'DEBUG');
214: $data = $registry->{$database}->listUids(
215: Horde_SyncMl_Backend::getParameter($databaseURI, 'source'), $start);
216: } else {
217: $data = $registry->{$database}->listUids(
218: Horde_SyncMl_Backend::getParameter($databaseURI, 'source'));
219: }
220: } catch (Horde_Exception $e) {
221: $this->logMessage(
222: "$database/list or $database/listBy failed while retrieving server additions:"
223: . $e->getMessage(), 'ERR');
224: return;
225: }
226:
227: foreach ($data as $suid) {
228: // Only server needs to check for client sent entries:
229: if ($this->_backendMode != Horde_SyncMl_Backend::MODE_SERVER) {
230: $results['adds'][$suid] = 0;
231: continue;
232: }
233:
234: // Ignore if a map entry is present
235: $cuid = $this->_getCuid($database, $suid);
236: if ($cuid) {
237: $this->logMessage(
238: "Added to server from client during SlowSync: $suid ignored", 'DEBUG');
239: continue;
240: }
241:
242: try {
243: $add_ts = $registry->{$database}->getActionTimestamp(
244: $suid,
245: 'add',
246: Horde_SyncMl_Backend::getParameter($databaseURI, 'source'));
247: } catch (Horde_Exception $e) {
248: $this->logMessage($e->getMessage(), 'ERR');
249: return;
250: }
251:
252: $sync_ts = $this->_getChangeTS($database, $suid);
253: if ($sync_ts && $sync_ts >= $add_ts) {
254: // Change was done by us upon request of client. Don't mirror
255: // that back to the client.
256: $this->logMessage("Added to server from client: $suid ignored", 'DEBUG');
257: continue;
258: }
259: $this->logMessage(
260: "Adding to client from db $database, server id $suid", 'DEBUG');
261:
262: $results['adds'][$suid] = 0;
263: }
264:
265: return $results;
266: }
267:
268: /**
269: * Returns entries that have been modified in the server database.
270: *
271: * @param string $databaseURI URI of Database to sync. Like calendar,
272: * tasks, contacts or notes. May include
273: * optional parameters:
274: * tasks?options=ignorecompleted.
275: * @param integer $from_ts Start timestamp.
276: * @param integer $to_ts Exclusive end timestamp. Not yet
277: * implemented.
278: * @param array &$adds Output array: hash of adds suid => 0
279: * @param array &$mods Output array: hash of modifications
280: * suid => cuid
281: * @param array &$dels Output array: hash of deletions suid => cuid
282: *
283: * @return boolean true
284: */
285: public function getServerChanges($databaseURI, $from_ts, $to_ts, &$adds, &$mods,
286: &$dels)
287: {
288: global $registry;
289:
290: $slowsync = $from_ts == 0;
291:
292: if ($slowsync) {
293: $results = $this->_slowsync($databaseURI, $from_ts, $to_ts);
294: } else {
295: $results = $this->_fastSync($databaseURI, $from_ts, $to_ts);
296: }
297:
298: $adds = $results['adds'];
299: $mods = $results['mods'];
300: $dels = $results['dels'];
301:
302: // @TODO: No need to return true, since errors are now thrown. H5 should
303: // remove this.
304: return true;
305: }
306:
307: /**
308: * Retrieves an entry from the backend.
309: *
310: * @param string $databaseURI URI of Database to sync. Like calendar,
311: * tasks, contacts or notes. May include
312: * optional parameters:
313: * tasks?options=ignorecompleted.
314: * @param string $suid Server unique id of the entry: for horde
315: * this is the guid.
316: * @param string $contentType Content-Type: the MIME type in which the
317: * public function should return the data.
318: * @param array $fields Hash of field names and Horde_SyncMl_Property
319: * properties with the requested fields.
320: *
321: * @return mixed A string with the data entry or a PEAR_Error object.
322: */
323: public function retrieveEntry($databaseURI, $suid, $contentType, $fields)
324: {
325: try {
326: return $GLOBALS['registry']->call(
327: $this->normalize($databaseURI) . '/export',
328: array('guid' => $suid, 'contentType' => $contentType, 'dummy' => null, 'fields' => $fields));
329: } catch (Horde_Exception $e) {
330: return PEAR::raiseError($e->getMessage());
331: }
332: }
333:
334: /**
335: * Adds an entry into the server database.
336: *
337: * @param string $databaseURI URI of Database to sync. Like calendar,
338: * tasks, contacts or notes. May include
339: * optional parameters:
340: * tasks?options=ignorecompleted.
341: * @param string $content The actual data.
342: * @param string $contentType MIME type of the content.
343: * @param string $cuid Client ID of this entry.
344: *
345: * @return array PEAR_Error or suid (Horde guid) of new entry
346: */
347: public function addEntry($databaseURI, $content, $contentType, $cuid = null)
348: {
349: global $registry;
350:
351: $database = $this->normalize($databaseURI);
352:
353: try {
354: $suid = $registry->call(
355: $database . '/import',
356: array($content,
357: $contentType,
358: Horde_SyncMl_Backend::getParameter($databaseURI, 'source')));
359:
360: $this->logMessage(
361: "Added to server db $database client id $cuid -> server id $suid", 'DEBUG');
362: $ts = $registry->call(
363: $database . '/getActionTimestamp',
364: array($suid,
365: 'add',
366: Horde_SyncMl_Backend::getParameter($databaseURI, 'source')));
367: if (!$ts) {
368: $this->logMessage(
369: "Unable to find addition timestamp for server id $suid at $ts", 'ERR');
370: }
371: // Only server needs to do a cuid<->suid map
372: if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
373: $this->createUidMap($database, $cuid, $suid, $ts);
374: }
375: } catch (Horde_Exception $e) {
376: // Failed import. Maybe the entry is already there. Check if a
377: // guid is returned:
378: /* Not working with exceptions
379: if ($suid->getDebugInfo()) {
380: $suid = $suid->getDebugInfo();
381: $this->logMessage(
382: 'Adding client entry to server: already exists with server id ' . $suid, 'NOTICE');
383: if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
384: $this->createUidMap($database, $cuid, $suid, 0);
385: }
386: }
387: */
388:
389: }
390:
391: return $suid;
392: }
393:
394: /**
395: * Replaces an entry in the server database.
396: *
397: * @param string $databaseURI URI of Database to sync. Like calendar,
398: * tasks, contacts or notes. May include
399: * optional parameters:
400: * tasks?options=ignorecompleted.
401: * @param string $content The actual data.
402: * @param string $contentType MIME type of the content.
403: * @param string $cuid Client ID of this entry.
404: *
405: * @return string PEAR_Error or server ID (Horde GUID) of modified entry.
406: */
407: public function replaceEntry($databaseURI, $content, $contentType, $cuid)
408: {
409: global $registry;
410:
411: $database = $this->normalize($databaseURI);
412:
413: // Only server needs to do a cuid<->suid map
414: if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
415: $suid = $this->_getSuid($database, $cuid);
416: } else {
417: $suid = $cuid;
418: }
419:
420: if (!$suid) {
421: return PEAR::raiseError("No map entry found for client id $cuid replacing on server");
422: }
423:
424: // Entry exists: replace current one.
425: try {
426: $ok = $registry->call($database . '/replace',
427: array($suid, $content, $contentType));
428: } catch (Horde_Exception $e) {
429: return PEAR::raiseError($e->getMessage());
430: }
431: $this->logMessage(
432: "Replaced in server db $database client id $cuid -> server id $suid", 'DEBUG');
433: $ts = $registry->call(
434: $database . '/getActionTimestamp',
435: array($suid,
436: 'modify',
437: Horde_SyncMl_Backend::getParameter($databaseURI,'source')));
438: // Only server needs to do a cuid<->suid map
439: if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
440: $this->createUidMap($database, $cuid, $suid, $ts);
441: }
442:
443: return $suid;
444: }
445:
446: /**
447: * Deletes an entry from the server database.
448: *
449: * @param string $databaseURI URI of Database to sync. Like calendar,
450: * tasks, contacts or notes. May include
451: * optional parameters:
452: * tasks?options=ignorecompleted.
453: * @param string $cuid Client ID of the entry.
454: *
455: * @return boolean True on success or false on failed (item not found).
456: */
457: public function deleteEntry($databaseURI, $cuid)
458: {
459: global $registry;
460:
461: $database = $this->normalize($databaseURI);
462: // Find server ID for this entry:
463: // Only server needs to do a cuid<->suid map
464: if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
465: try {
466: $suid = $this->_getSuid($database, $cuid);
467: } catch (Horde_Exception $e) {
468: return false;
469: }
470: } else {
471: $suid = $cuid;
472: }
473: if (empty($suid) || is_a($suid, 'PEAR_Error')) {
474: return false;
475: }
476:
477: try {
478: $registry->call($database. '/delete', array($suid));
479: } catch (Horde_Exception $e) {
480: return false;
481: }
482:
483: $this->logMessage(
484: "Deleted in server db $database client id $cuid -> server id $suid", 'DEBUG');
485: $ts = $registry->call($database . '/getActionTimestamp',
486: array($suid, 'delete'));
487: // We can't remove the mapping entry as we need to keep the timestamp
488: // information.
489: // Only server needs to do a cuid<->suid map
490: if ($this->_backendMode == Horde_SyncMl_Backend::MODE_SERVER) {
491: $this->createUidMap($database, $cuid, $suid, $ts);
492: }
493:
494: return true;
495: }
496:
497: /**
498: * Authenticates the user at the backend.
499: *
500: * @param string $username A user name.
501: * @param string $password A password.
502: *
503: * @return boolean|string The user name if authentication succeeded, false
504: * otherwise.
505: */
506: protected function _checkAuthentication($username, $password)
507: {
508: $auth = $GLOBALS['injector']
509: ->getInstance('Horde_Core_Factory_Auth')
510: ->create()
511: ->authenticate($username, array('password' => $password))
512: ? $GLOBALS['registry']->getAuth()
513: : false;
514:
515: /* Horde is regenerating the session id at login, but we need to keep
516: * our own, predictable session to not lose state. */
517: session_id($this->_sessionId);
518:
519: return $auth;
520: }
521:
522: /**
523: * Sets a user as being authenticated at the backend.
524: *
525: * @abstract
526: *
527: * @param string $username A user name.
528: * @param string $credData Authentication data provided by <Cred><Data>
529: * in the <SyncHdr>.
530: *
531: * @return string The user name.
532: */
533: protected function _setAuthenticated($username, $credData)
534: {
535: global $registry;
536:
537: $registry->setAuth($username, $credData);
538: return $registry->getAuth();
539: }
540:
541: /**
542: * Stores Sync anchors after a successful synchronization to allow two-way
543: * synchronization next time.
544: *
545: * The backend has to store the parameters in its persistence engine
546: * where user, syncDeviceID and database are the keys while client and
547: * server anchor ar the payload. See readSyncAnchors() for retrieval.
548: *
549: * @param string $databaseURI URI of database to sync. Like calendar,
550: * tasks, contacts or notes. May include
551: * optional parameters:
552: * tasks?options=ignorecompleted.
553: * @param string $clientAnchorNext The client anchor as sent by the
554: * client.
555: * @param string $serverAnchorNext The anchor as used internally by the
556: * server.
557: */
558: public function writeSyncAnchors($databaseURI, $clientAnchorNext,
559: $serverAnchorNext)
560: {
561: $database = $this->normalize($databaseURI);
562:
563: $values = array($clientAnchorNext, $serverAnchorNext,
564: $this->_syncDeviceID, $database, $this->_user);
565: if (!$this->readSyncAnchors($databaseURI)) {
566: $query = 'INSERT INTO horde_syncml_anchors '
567: . '(syncml_clientanchor, syncml_serveranchor, '
568: . 'syncml_syncpartner, syncml_db, syncml_uid) '
569: . 'VALUES (?, ?, ?, ?, ?)';
570: $this->_db->insert($query, $values);
571: } else {
572: $query = 'UPDATE horde_syncml_anchors '
573: . 'SET syncml_clientanchor = ?, syncml_serveranchor = ? '
574: . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
575: . 'syncml_uid = ?';
576: $this->_db->update($query, $values);
577: }
578: }
579:
580: /**
581: * Reads the previously written sync anchors from the database.
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: *
588: * @return mixed Two-element array with client anchor and server anchor as
589: * stored in previous writeSyncAnchor() calls. False if no
590: * data found.
591: */
592: public function readSyncAnchors($databaseURI)
593: {
594: $database = $this->normalize($databaseURI);
595: $query = 'SELECT syncml_clientanchor, syncml_serveranchor '
596: . 'FROM horde_syncml_anchors '
597: . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
598: . 'syncml_uid = ?';
599: $values = array($this->_syncDeviceID, $database, $this->_user);
600: try {
601: if ($res = $this->_db->selectOne($query, $values)) {
602: return array(
603: $res['syncml_clientanchor'],
604: $res['syncml_serveranchor']
605: );
606: }
607: } catch (Horde_Db_Exception $e) {}
608:
609: return false;
610: }
611:
612: /**
613: * Returns all previously written sync anchors for a user.
614: *
615: * @param string $user A user name.
616: *
617: * @return array A hash tree with all devices, databases and sync anchors
618: * from the specified user.
619: */
620: public function getUserAnchors($user)
621: {
622: $query = 'SELECT syncml_syncpartner, syncml_db, syncml_clientanchor, '
623: . 'syncml_serveranchor FROM horde_syncml_anchors '
624: . 'WHERE syncml_uid = ?';
625: $values = array($user);
626: return $this->_db->selectAll($query, $values);
627: }
628:
629: /**
630: * Deletes previously written sync anchors for a user.
631: *
632: * If no device or database are specified, anchors for all devices and/or
633: * databases will be deleted.
634: *
635: * @param string $user A user name.
636: * @param string $device The ID of the client device.
637: * @param string $database Normalized URI of database to delete. Like
638: * calendar, tasks, contacts or notes.
639: *
640: * @return array
641: */
642: public function removeAnchor($user, $device = null, $database = null)
643: {
644: $query = 'DELETE FROM horde_syncml_anchors WHERE syncml_uid = ?';
645: $values = array($user);
646: if (strlen($device)) {
647: $query .= ' AND syncml_syncpartner = ?';
648: $values[] = $device;
649: }
650: if (strlen($database)) {
651: $query .= ' AND syncml_db = ?';
652: $values[] = $database;
653: }
654:
655: $this->_db->delete($query, $values);
656: }
657:
658: /**
659: * Deletes previously written sync maps for a user.
660: *
661: * If no device or database are specified, maps for all devices and/or
662: * databases will be deleted.
663: *
664: * @param string $user A user name.
665: * @param string $device The ID of the client device.
666: * @param string $database Normalized URI of database to delete. Like
667: * calendar, tasks, contacts or notes.
668: *
669: * @return array
670: */
671: public function removeMaps($user, $device = null, $database = null)
672: {
673: $query = 'DELETE FROM horde_syncml_map WHERE syncml_uid = ?';
674: $values = array($user);
675: if (strlen($device)) {
676: $query .= ' AND syncml_syncpartner = ?';
677: $values[] = $device;
678: }
679: if (strlen($database)) {
680: $query .= ' AND syncml_db = ?';
681: $values[] = $database;
682: }
683:
684: $this->_db->delete($query, $values);
685: }
686:
687: /**
688: * Creates a map entry to map between server and client IDs.
689: *
690: * If an entry already exists, it is overwritten.
691: *
692: * @param string $databaseURI URI of database to sync. Like calendar,
693: * tasks, contacts or notes. May include
694: * optional parameters:
695: * tasks?options=ignorecompleted.
696: * @param string $cuid Client ID of the entry.
697: * @param string $suid Server ID of the entry.
698: * @param integer $timestamp Optional timestamp. This can be used to
699: * 'tag' changes made in the backend during the
700: * sync process. This allows to identify these,
701: * and ensure that these changes are not
702: * replicated back to the client (and thus
703: * duplicated). See key concept "Changes and
704: * timestamps".
705: */
706: public function createUidMap($databaseURI, $cuid, $suid, $timestamp = 0)
707: {
708: $database = $this->normalize($databaseURI);
709:
710: $values = array($suid, (int)$timestamp, $this->_syncDeviceID,
711: $database, $this->_user, $cuid);
712: // Check if entry exists. If not insert, otherwise update.
713: if (!$this->_getSuid($databaseURI, $cuid)) {
714: $query = 'INSERT INTO horde_syncml_map '
715: . '(syncml_suid, syncml_timestamp, syncml_syncpartner, '
716: . 'syncml_db, syncml_uid, syncml_cuid) '
717: . 'VALUES (?, ?, ?, ?, ?, ?)';
718: $this->_db->insert($query, $values);
719: } else {
720: $query = 'UPDATE horde_syncml_map '
721: . 'SET syncml_suid = ?, syncml_timestamp = ? '
722: . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
723: . 'syncml_uid = ? AND syncml_cuid = ?';
724: $this->_db->update($query, $values);
725: }
726: }
727:
728: /**
729: * Retrieves the Server ID for a given Client ID from the map.
730: *
731: * @param string $databaseURI URI of database to sync. Like calendar,
732: * tasks, contacts or notes. May include
733: * optional parameters:
734: * tasks?options=ignorecompleted.
735: * @param string $cuid The client ID.
736: *
737: * @return mixed The server ID string or false if no entry is found.
738: */
739: protected function _getSuid($databaseURI, $cuid)
740: {
741: $database = $this->normalize($databaseURI);
742: $query = 'SELECT syncml_suid FROM horde_syncml_map '
743: . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
744: . 'syncml_uid = ? AND syncml_cuid = ?';
745: $values = array($this->_syncDeviceID, $database, $this->_user, $cuid);
746: return $this->_db->selectValue($query, $values);
747: }
748:
749: /**
750: * Retrieves the Client ID for a given Server ID from the map.
751: *
752: * @param string $databaseURI URI of database to sync. Like calendar,
753: * tasks, contacts or notes. May include
754: * optional parameters:
755: * tasks?options=ignorecompleted.
756: * @param string $suid The server ID.
757: *
758: * @return mixed The client ID string or false if no entry is found.
759: */
760: protected function _getCuid($databaseURI, $suid)
761: {
762: $database = $this->normalize($databaseURI);
763:
764: $query = 'SELECT syncml_cuid FROM horde_syncml_map '
765: . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
766: . 'syncml_uid = ? AND syncml_suid = ?';
767: $values = array($this->_syncDeviceID, $database, $this->_user, $suid);
768: return $this->_db->selectValue($query, $values);
769: }
770:
771: /**
772: * Returns a timestamp stored in the map for a given Server ID.
773: *
774: * The timestamp is the timestamp of the last change to this server ID
775: * that was done inside a sync session (as a result of a change received
776: * by the server). It's important to distinguish changes in the backend a)
777: * made by the user during normal operation and b) changes made by SyncML
778: * to reflect client updates. When the server is sending its changes it
779: * is only allowed to send type a). However the history feature in the
780: * backend my not know if a change is of type a) or type b). So the
781: * timestamp is used to differentiate between the two.
782: *
783: * @param string $databaseURI URI of database to sync. Like calendar,
784: * tasks, contacts or notes. May include
785: * optional parameters:
786: * tasks?options=ignorecompleted.
787: * @param string $suid The server ID.
788: *
789: * @return mixed The previously stored timestamp or false if no entry is
790: * found.
791: */
792: protected function _getChangeTS($databaseURI, $suid)
793: {
794: $database = $this->normalize($databaseURI);
795: $query = 'SELECT syncml_timestamp FROM horde_syncml_map '
796: . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
797: . 'syncml_uid = ? AND syncml_suid = ?';
798: $values = array($this->_syncDeviceID, $database, $this->_user, $suid);
799: return $this->_db->selectValue($query, $values);
800: }
801:
802: /**
803: * Erases all mapping entries for one combination of user, device ID.
804: *
805: * This is used during SlowSync so that we really sync everything properly
806: * and no old mapping entries remain.
807: *
808: * @param string $databaseURI URI of database to sync. Like calendar,
809: * tasks, contacts or notes. May include
810: * optional parameters:
811: * tasks?options=ignorecompleted.
812: */
813: public function eraseMap($databaseURI)
814: {
815: $database = $this->normalize($databaseURI);
816: $query = 'DELETE FROM horde_syncml_map '
817: . 'WHERE syncml_syncpartner = ? AND syncml_db = ? AND '
818: . 'syncml_uid = ?';
819: $values = array($this->_syncDeviceID, $database, $this->_user);
820: $this->_db->delete($query, $values);
821: }
822:
823: /**
824: * Logs a message in the backend.
825: *
826: * @param mixed $message Either a string or a PEAR_Error object.
827: * @param string $priority The priority of the message. One of:
828: * - EMERG
829: * - ALERT
830: * - CRIT
831: * - ERR
832: * - WARN
833: * - NOTICE
834: * - INFO
835: * - DEBUG
836: */
837: public function logMessage($message, $priority = 'INFO')
838: {
839: $trace = debug_backtrace();
840: $trace = $trace[1];
841:
842: // Internal logging to $this->_logtext.
843: parent::logMessage($message, $priority);
844:
845: // Logging to Horde log:
846: Horde::logMessage($message, $priority, array('file' => $trace['file'], 'line' => $trace['line']));
847: }
848:
849: /**
850: * Creates a clean test environment in the backend.
851: *
852: * Ensures there's a user with the given credentials and an empty data
853: * store.
854: *
855: * @param string $user This user accout has to be created in the backend.
856: * @param string $pwd The password for user $user.
857: *
858: * @throws Horde_Exception
859: */
860: public function testSetup($user, $pwd)
861: {
862: $this->_user = $user;
863: if (empty($pwd)) {
864: $pwd = rand() . rand();
865: }
866:
867: /* Get an Auth object. */
868: $auth = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create();
869:
870: /* Make this user an admin for the time beeing to allow deletion of
871: * user data. */
872: $GLOBALS['conf']['auth']['admins'][] = $user;
873:
874: /* Always remove test user first. */
875: if ($auth->exists($user)) {
876: $GLOBALS['registry']->removeUser($user);
877: }
878:
879: $auth->addUser($user, array('password' => $pwd));
880: }
881:
882: /**
883: * Prepares the test start.
884: *
885: * @param string $user This user accout has to be created in the backend.
886: */
887: public function testStart($user)
888: {
889: $this->_user = $user;
890:
891: /* Make this user an admin for the time beeing to allow deletion of
892: * user data. */
893: $GLOBALS['conf']['auth']['admins'][] = $user;
894:
895: $GLOBALS['registry']->setAuth($user, array());
896: }
897:
898: /**
899: * Tears down the test environment after the test is run.
900: *
901: * Should remove the testuser created during testSetup and all its data.
902: */
903: public function testTearDown()
904: {
905: /* Get an Auth object. */
906: try {
907: $auth = $GLOBALS['injector']->getInstance('Horde_Core_Factory_Auth')->create();
908: } catch (Horde_Exception $e) {
909: // TODO
910: }
911:
912: /* We need to be logged in to call removeUserData, otherwise we run
913: * into permission issues. */
914: $GLOBALS['registry']->setAuth($this->_user, array());
915:
916: print "\nCleaning up: removing test user data and test user...";
917: $registry->removeUser($this->_user);
918:
919: print "OK\n";
920: }
921: }
922: