1: <?php
2: /**
3: * Base class for managing everything related to state:
4: *
5: * Persistence of state data
6: * Generating delta between server and PIM
7: * Caching PING related state (hearbeat interval, folder list etc...)
8: *
9: * Copyright 2010-2012 Horde LLC (http://www.horde.org/)
10: *
11: * @author Michael J. Rubinsky <mrubinsk@horde.org>
12: * @package ActiveSync
13: */
14: abstract class Horde_ActiveSync_State_Base
15: {
16: /**
17: * Filtertype constants
18: */
19: const FILTERTYPE_ALL = 0;
20: const FILTERTYPE_1DAY = 1;
21: const FILTERTYPE_3DAYS = 2;
22: const FILTERTYPE_1WEEK = 3;
23: const FILTERTYPE_2WEEKS = 4;
24: const FILTERTYPE_1MONTH = 5;
25: const FILTERTYPE_3MONTHS = 6;
26: const FILTERTYPE_6MONTHS = 7;
27:
28: /**
29: * Configuration parameters
30: *
31: * @var array
32: */
33: protected $_params;
34:
35: /**
36: * Caches the current state(s) in memory
37: *
38: * @var array
39: */
40: protected $_stateCache;
41:
42: /**
43: * The syncKey for the current request.
44: *
45: * @var string
46: */
47: protected $_syncKey;
48:
49: /**
50: * The backend driver
51: *
52: * @param Horde_ActiveSync_Driver_Base
53: */
54: protected $_backend;
55:
56: /**
57: * Cache for ping state
58: *
59: * @var array
60: */
61: protected $_pingState;
62:
63: /**
64: * The collection array for the collection we are currently syncing.
65: * Keys include:
66: * 'class' - The collection class Contacts, Calendar etc...
67: * 'synckey' - The current synckey
68: * 'newsynckey' - The new synckey sent back to the PIM
69: * 'id' - Server folder id
70: * 'filtertype' - Filter
71: * 'conflict' - Conflicts
72: * 'truncation' - Truncation
73: *
74: *
75: * @var array
76: */
77: protected $_collection;
78:
79: /**
80: * Logger instance
81: *
82: * @var Horde_Log_Logger
83: */
84: protected $_logger;
85:
86: /**
87: * The PIM device id. Needed for PING requests
88: *
89: * @var string
90: */
91: protected $_devId;
92:
93: /**
94: * Device info cache
95: *
96: * @var object
97: */
98: protected $_deviceInfo;
99:
100: /**
101: * Local cache for changes to *send* to PIM
102: * (Will remain null until getChanges() is called)
103: *
104: * @var
105: */
106: protected $_changes;
107:
108: /**
109: * The type of request we are handling (if important).
110: *
111: * @var string
112: */
113: protected $_type;
114:
115: /**
116: * Const'r
117: *
118: * @param array $collection A collection array
119: * @param array $params All configuration parameters, requirements.
120: *
121: * @return Horde_ActiveSync_State_Base
122: */
123: public function __construct($params = array())
124: {
125: $this->_params = $params;
126: if (empty($params['logger'])) {
127: $this->_logger = new Horde_Support_Stub();
128: }
129: }
130:
131: public function __destruct()
132: {
133: unset ($this->_backend);
134: }
135:
136: /**
137: * Update the $oldKey syncState to $newKey.
138: *
139: * @param string $newKey
140: *
141: * @return void
142: */
143: public function setNewSyncKey($newKey)
144: {
145: $this->_syncKey = $newKey;
146: }
147:
148: /**
149: * Get the current synckey
150: *
151: * @return string The synkey we last retrieved state for
152: */
153: public function getCurrentSyncKey()
154: {
155: return $this->_syncKey;
156: }
157:
158: /**
159: * Generate a random 10 digit policy key
160: *
161: * @return unknown
162: */
163: public function generatePolicyKey()
164: {
165: return mt_rand(1000000000, 9999999999);
166: }
167:
168: /**
169: * Obtain the current policy key, if it exists.
170: *
171: * @param string $devId The device id to obtain policy key for.
172: *
173: * @return integer The current policy key for this device, or 0 if none
174: * exists.
175: */
176: public function getPolicyKey($devId)
177: {
178: //@TODO - combine _devId and _deviceInfo
179: /* See if we have it already */
180: if (empty($this->_deviceInfo) || $this->_devId != $devId) {
181: throw new Horde_ActiveSync_Exception('Device not loaded.');
182: }
183:
184: return $this->_deviceInfo->policykey;
185: }
186:
187: /**
188: * Return a device wipe status
189: *
190: * @param string $devId
191: *
192: * @return integer
193: */
194: public function getDeviceRWStatus($devId)
195: {
196: //@TODO - combine _devId and _deviceInfo
197: /* See if we have it already */
198: if (empty($this->_deviceInfo) || $this->_devId != $devId) {
199: throw new Horde_ActiveSync_Exception('Device not loaded.');
200: }
201:
202: return $this->_deviceInfo->rwstatus;
203: }
204:
205: /**
206: * Set the backend driver
207: * (should really only be called by a backend object when passing this
208: * object to client code)
209: *
210: * @param Horde_ActiveSync_Driver_Base $backend The backend driver
211: *
212: * @return void
213: */
214: public function setBackend(Horde_ActiveSync_Driver_Base $backend)
215: {
216: $this->_backend = $backend;
217: }
218:
219: /**
220: * Initialize the state object
221: *
222: * @param array $collection The collection array
223: *
224: * @return void
225: */
226: public function init($collection = array())
227: {
228: $this->_collection = $collection;
229: }
230:
231: /**
232: * Set the logger instance for this object.
233: *
234: * @param Horde_Log_Logger $logger
235: */
236: public function setLogger($logger)
237: {
238: $this->_logger = $logger;
239: }
240:
241: /**
242: * Reset the device's PING state.
243: *
244: * @return void
245: */
246: public function resetPingState()
247: {
248: $this->_logger->debug('Resetting PING state');
249: $this->_pingState = array(
250: 'lifetime' => 0,
251: 'collections' => array());
252: }
253:
254: /**
255: * Get the number of server changes.
256: *
257: * @return integer
258: */
259: public function getChangeCount()
260: {
261: if (!isset($this->_changes)) {
262: $this->getChanges();
263: }
264:
265: return count($this->_changes);
266: }
267:
268: /**
269: * Gets the new sync key for a specified sync key. You must save the new
270: * sync state under this sync key when done sync'ing by calling
271: * setNewSyncKey(), then save().
272: *
273: * @param string $syncKey The old syncKey
274: *
275: * @return string The new synckey
276: * @throws Horde_ActiveSync_Exception
277: */
278: static public function getNewSyncKey($syncKey)
279: {
280: if (empty($syncKey)) {
281: return '{' . new Horde_Support_Uuid() . '}' . '1';
282: } else {
283: if (preg_match('/^s{0,1}\{([a-fA-F0-9-]+)\}([0-9]+)$/', $syncKey, $matches)) {
284: $n = $matches[2];
285: $n++;
286:
287: return '{' . $matches[1] . '}' . $n;
288: }
289: throw new Horde_ActiveSync_Exception('Invalid SyncKey format passed to getNewSyncKey()');
290: }
291: }
292:
293: /**
294: * Return the counter for the specified syncKey.
295: *
296: * @return mixed integer|boolean
297: */
298: static public function getSyncKeyCounter($syncKey)
299: {
300: if (preg_match('/^s{0,1}\{([a-fA-F0-9-]+)\}([0-9]+)$/', $syncKey, $matches)) {
301: $n = $matches[2];
302: return $n;
303: }
304:
305: return false;
306: }
307:
308: /**
309: * Returns the timestamp of the earliest modification time to consider
310: *
311: * @param integer $restrict The time period to restrict to
312: *
313: * @return integer
314: */
315: static protected function _getCutOffDate($restrict)
316: {
317: switch($restrict) {
318: case self::FILTERTYPE_1DAY:
319: $back = 60 * 60 * 24;
320: break;
321: case self::FILTERTYPE_3DAYS:
322: $back = 60 * 60 * 24 * 3;
323: break;
324: case self::FILTERTYPE_1WEEK:
325: $back = 60 * 60 * 24 * 7;
326: break;
327: case self::FILTERTYPE_2WEEKS:
328: $back = 60 * 60 * 24 * 14;
329: break;
330: case self::FILTERTYPE_1MONTH:
331: $back = 60 * 60 * 24 * 31;
332: break;
333: case self::FILTERTYPE_3MONTHS:
334: $back = 60 * 60 * 24 * 31 * 3;
335: break;
336: case self::FILTERTYPE_6MONTHS:
337: $back = 60 * 60 * 24 * 31 * 6;
338: break;
339: default:
340: break;
341: }
342:
343: if (isset($back))
344: {
345: $date = time() - $back;
346: return $date;
347: } else {
348: return 0; // unlimited
349: }
350: }
351:
352: /**
353: * Helper function that performs the actual diff between PIM state and
354: * server state arrays.
355: *
356: * @param array $old The PIM state
357: * @param array $new The current server state
358: *
359: * @return unknown_type
360: */
361: protected function _getDiff($old, $new)
362: {
363: $changes = array();
364:
365: // Sort both arrays in the same way by ID
366: usort($old, array(__CLASS__, 'RowCmp'));
367: usort($new, array(__CLASS__, 'RowCmp'));
368:
369: $inew = 0;
370: $iold = 0;
371:
372: // Get changes by comparing our list of messages with
373: // our previous state
374: while (1) {
375: $change = array();
376:
377: if ($iold >= count($old) || $inew >= count($new)) {
378: break;
379: }
380:
381: if ($old[$iold]['id'] == $new[$inew]['id']) {
382: // Both messages are still available, compare flags and mod
383: if (isset($old[$iold]['flags']) && isset($new[$inew]['flags']) && $old[$iold]['flags'] != $new[$inew]['flags']) {
384: // Flags changed
385: $change['type'] = 'flags';
386: $change['id'] = $new[$inew]['id'];
387: $change['flags'] = $new[$inew]['flags'];
388: $changes[] = $change;
389: }
390:
391: if ($old[$iold]['mod'] != $new[$inew]['mod']) {
392: $change['type'] = 'change';
393: $change['id'] = $new[$inew]['id'];
394: $changes[] = $change;
395: }
396:
397: $inew++;
398: $iold++;
399: } else {
400: if ($old[$iold]['id'] > $new[$inew]['id']) {
401: // Message in state seems to have disappeared (delete)
402: $change['type'] = 'delete';
403: $change['id'] = $old[$iold]['id'];
404: $changes[] = $change;
405: $iold++;
406: } else {
407: // Message in new seems to be new (add)
408: $change['type'] = 'change';
409: $change['flags'] = Horde_ActiveSync::FLAG_NEWMESSAGE;
410: $change['id'] = $new[$inew]['id'];
411: $changes[] = $change;
412: $inew++;
413: }
414: }
415: }
416:
417: while ($iold < count($old)) {
418: // All data left in _syncstate have been deleted
419: $change['type'] = 'delete';
420: $change['id'] = $old[$iold]['id'];
421: $changes[] = $change;
422: $iold++;
423: }
424:
425: while ($inew < count($new)) {
426: // All data left in new have been added
427: $change['type'] = 'change';
428: $change['flags'] = Horde_ActiveSync::FLAG_NEWMESSAGE;
429: $change['id'] = $new[$inew]['id'];
430: $changes[] = $change;
431: $inew++;
432: }
433:
434: return $changes;
435: }
436:
437: /**
438: * Helper function for the _diff method
439: *
440: * @param $a
441: * @param $b
442: * @return unknown_type
443: */
444: static public function RowCmp($a, $b)
445: {
446: return $a['id'] < $b['id'] ? 1 : -1;
447: }
448:
449: /**
450: * Loads the initial state from storage for the specified syncKey and
451: * intializes the stateMachine for use.
452: *
453: * @param string $syncKey The key for the state to load.
454: * @param string $type Treat the loaded state data as this type of state.
455: * @param string $id The collection id this represents
456: *
457: * @return array The state array
458: */
459: abstract public function loadState($syncKey, $type = null, $id = '');
460:
461: /**
462: * Load/initialize the ping state for the specified device.
463: *
464: * @param object $device
465: */
466: abstract public function initPingState($device);
467:
468: /**
469: * Load the ping state for the given device id
470: *
471: * @param string $devid The device id.
472: */
473: abstract public function loadPingCollectionState($devid);
474:
475: /**
476: * Get the list of known folders for the specified syncState
477: *
478: * @return array An array of server folder ids
479: */
480: abstract public function getKnownFolders();
481:
482: /**
483: * Save the current syncstate to storage
484: */
485: abstract public function save();
486:
487: /**
488: * Update the state to reflect changes
489: *
490: * @param string $type The type of change (change, delete, flags)
491: * @param array $change A stat/change hash describing the change
492: * @param integer $origin Flag to indicate the origin of the change.
493: * @param string $user The current synch user
494: *
495: * @return void
496: */
497: abstract public function updateState($type, array $change,
498: $origin = Horde_ActiveSync::CHANGE_ORIGIN_NA,
499: $user = null);
500:
501: /**
502: * Save folder data for a specific device. This is needed for BC with older
503: * activesync versions that use GETHIERARCHY requests to get the folder info
504: * instead of maintaining the folder state with FOLDERSYNC requests.
505: *
506: * @param object $device The device object
507: * @param array $folders The folder data
508: *
509: * @return boolean
510: * @throws Horde_ActiveSync_Exception
511: */
512: abstract public function setFolderData($device, $folders);
513:
514: /**
515: * Get the folder data for a specific device
516: *
517: * @param object $device The device object
518: * @param string $class The folder class to fetch (Calendar, Contacts etc.)
519: *
520: * @return mixed Either an array of folder data || false
521: */
522: abstract public function getFolderData($device, $class);
523:
524: /**
525: * Get all items that have changed since the last sync time
526: *
527: * @param integer $flags
528: *
529: * @return array
530: */
531: abstract public function getChanges($flags = 0);
532:
533: /**
534: * Determines if the server version of the message represented by $stat
535: * conflicts with the PIM version of the message according to the current
536: * state.
537: *
538: * @param array $stat A message stat array
539: * @param string $type The type of change (change, delete, add)
540: *
541: * @return boolean
542: */
543: abstract public function isConflict($stat, $type);
544:
545: /**
546: * Save a new device policy key to storage.
547: *
548: * @param string $devId The device id
549: * @param integer $key The new policy key
550: */
551: abstract public function setPolicyKey($devId, $key);
552:
553: /**
554: * Reset ALL device policy keys. Used when server policies have changed
555: * and you want to force ALL devices to pick up the changes. This will
556: * cause all devices that support provisioning to be reprovisioned.
557: *
558: * @throws Horde_ActiveSync_Exception
559: *
560: */
561: abstract public function resetAllPolicyKeys();
562:
563: /**
564: * Set a new remotewipe status for the device
565: *
566: * @param string $devid
567: * @param string $status
568: *
569: * @return boolean
570: */
571: abstract public function setDeviceRWStatus($devid, $status);
572:
573: /**
574: * Obtain the device object.
575: *
576: * @param object $device
577: * @param string $user
578: *
579: * @return StdClass
580: */
581: abstract public function loadDeviceInfo($device, $user);
582:
583: /**
584: * Check that a given device id is known to the server. This is regardless
585: * of Provisioning status.
586: *
587: * @param string $devId The device id to check
588: * @param string $user The device should be owned by this user.
589: *
590: * @return boolean
591: */
592: abstract public function deviceExists($devId, $user = null);
593:
594: /**
595: * Set new device info
596: *
597: * @param object $device The device information
598: *
599: * @return boolean
600: */
601: abstract public function setDeviceInfo($data);
602:
603: /**
604: * Explicitly remove a state from storage.
605: *
606: * @param string $synckey The specific state to remove
607: * @param string $devId Remove all state for this device (ignores synckey)
608: *
609: * @throws Horde_ActiveSyncException
610: */
611: abstract public function removeState($synckey = null, $devId = null);
612:
613: /**
614: * Return the heartbeat interval, or zero if we have no existing state
615: *
616: * @return integer The hearbeat interval, or zero if not found.
617: * @throws Horde_ActiveSync_Exception
618: */
619: abstract public function getHeartbeatInterval();
620:
621: /**
622: * Set the device's heartbeat interval
623: *
624: * @param integer $heartbeat The interval (in seconds).
625: */
626: abstract public function setHeartbeatInterval($heartbeat);
627:
628: /**
629: * List all devices that we know about.
630: *
631: * @return array An array of device hashes
632: * @throws Horde_ActiveSync_Exception
633: */
634: abstract public function listDevices();
635:
636: /**
637: * Get the last time the currently loaded device issued a SYNC request.
638: *
639: * @return integer The timestamp of the last sync, regardless of collection
640: * @throws Horde_ActiveSync_Exception
641: */
642: abstract public function getLastSyncTimestamp();
643:
644: }