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