1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10:
11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49:
50: class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
51: {
52: 53: 54: 55: 56:
57: private $_stateDir;
58:
59: 60: 61: 62: 63:
64: private $_haveStateDirectory;
65:
66: 67: 68: 69: 70: 71: 72:
73: public function __construct($params = array())
74: {
75: parent::__construct($params);
76:
77: if (empty($this->_params['directory'])) {
78: throw new InvalidArgumentException('Missing required "stateDir" parameter.');
79: }
80:
81: $this->_stateDir = $this->_params['directory'];
82: }
83:
84: 85: 86: 87: 88: 89: 90: 91: 92:
93: public function loadState($syncKey, $type = null, $id = '')
94: {
95:
96: $this->_ensureUserDirectory();
97:
98:
99: if (empty($syncKey)) {
100: $this->_stateCache = array();
101: return;
102: }
103:
104:
105: if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) {
106: throw new Horde_ActiveSync_Exception('Invalid sync key');
107: }
108:
109:
110: $this->_gc($syncKey);
111:
112:
113: $filename = $this->_stateDir . '/' . $this->_backend->getUser() . '/' . $syncKey;
114: if (!file_exists($filename)) {
115: throw new Horde_ActiveSync_Exception('Sync state not found');
116: }
117: $this->_stateCache = unserialize(file_get_contents($filename));
118: $this->_syncKey = $syncKey;
119: }
120:
121: 122: 123: 124: 125: 126: 127:
128: public function isConflict($stat, $type)
129: {
130: foreach ($this->_stateCache as $state) {
131: if ($state['id'] == $stat['id']) {
132: $oldstat = $state;
133: break;
134: }
135: }
136:
137:
138:
139: if (!isset($oldstat)) {
140: return false;
141: }
142:
143: if ($stat['mod'] != $oldstat['mod']) {
144:
145: if ($type == 'delete' || $type == 'change') {
146:
147:
148: return true;
149: } else {
150:
151: return false;
152: }
153: }
154: }
155:
156: 157: 158: 159: 160: 161: 162:
163: public function save()
164: {
165: return file_put_contents(
166: $this->_stateDir . '/' . $this->_backend->getUser() . '/' . $this->_syncKey,
167: !empty($this->_stateCache) ? serialize($this->_stateCache) : '');
168: }
169:
170: 171: 172: 173: 174: 175: 176: 177:
178: public function updateState($type, array $change, $origin = Horde_ActiveSync::CHANGE_ORIGIN_NA, $user = null)
179: {
180: if (empty($this->_stateCache)) {
181: $this->_stateCache = array();
182: }
183:
184:
185: if ($type == 'change') {
186: 187: 188: 189: 190:
191: if (!isset($change['mod'])) {
192: $change = $this->_backend->statMessage($this->_collection['id'], $change['id']);
193: }
194: for($i = 0; $i < count($this->_stateCache); $i++) {
195: if($this->_stateCache[$i]['id'] == $change['id']) {
196: $this->_stateCache[$i] = $change;
197:
198: if (!empty($this->_pingState['collections'])) {
199: $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache;
200: }
201: return;
202: }
203: }
204:
205: $this->_stateCache[] = $change;
206: } else {
207: for ($i = 0; $i < count($this->_stateCache); $i++) {
208:
209: if ($this->_stateCache[$i]['id'] == $change['id']) {
210: if ($type == 'flags') {
211:
212: $this->_stateCache[$i]['flags'] = $change['flags'];
213: } elseif ($type == 'delete') {
214:
215: array_splice($this->_stateCache, $i, 1);
216: }
217: break;
218: }
219: }
220: }
221:
222:
223: if (!empty($this->_pingState['collections'])) {
224: $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache;
225: }
226: }
227:
228: 229: 230: 231: 232: 233: 234: 235: 236: 237:
238: public function setFolderData($devId, $folders)
239: {
240: if (!is_array($folders) || empty ($folders)) {
241: return false;
242: }
243:
244: $unique_folders = array ();
245: foreach ($folders as $folder) {
246:
247: if ($folder->type == Horde_ActiveSync::FOLDER_TYPE_INBOX) {
248: continue;
249: }
250:
251:
252: if (!array_key_exists($folder->type, $unique_folders) || $folder->parentid == 0) {
253: $unique_folders[$folder->type] = $folder->serverid;
254: }
255: }
256:
257:
258:
259: if (!array_key_exists(Horde_ActiveSync::FOLDER_TYPE_APPOINTMENT, $unique_folders)) {
260: $unique_folders[Horde_ActiveSync::FOLDER_TYPE_APPOINTMENT] = Horde_ActiveSync::FOLDER_TYPE_DUMMY;
261: }
262: if (!array_key_exists(Horde_ActiveSync::FOLDER_TYPE_CONTACT, $unique_folders)) {
263: $unique_folders[Horde_ActiveSync::FOLDER_TYPE_CONTACT] = Horde_ActiveSync::FOLDER_TYPE_DUMMY;
264:
265: }
266: if (!file_put_contents($this->_stateDir . '/' . $this->_backend->getUser() . '/compat-' . $devId, serialize($unique_folders))) {
267: $this->_logger->err('_saveFolderData: Data could not be saved!');
268: throw new Horde_ActiveSync_Exception('Folder data could not be saved');
269: }
270: }
271:
272: 273: 274: 275: 276: 277: 278: 279: 280:
281: public function getFolderData($devId, $class)
282: {
283: $filename = $this->_stateDir . '/' . $this->_backend->getUser() . '/compat-' . $devId;
284: if (file_exists($filename)) {
285: $arr = unserialize(file_get_contents($filename));
286: if ($class == "Calendar") {
287: return $arr[Horde_ActiveSync::FOLDER_TYPE_APPOINTMENT];
288: }
289: if ($class == "Contacts") {
290: return $arr[Horde_ActiveSync::FOLDER_TYPE_CONTACT];
291: }
292: }
293:
294: return false;
295: }
296:
297: 298: 299: 300: 301: 302: 303:
304: public function getKnownFolders()
305: {
306: if (!isset($this->_stateCache)) {
307: throw new Horde_ActiveSync_Exception('Sync state not loaded');
308: }
309: $folders = array();
310: foreach ($this->_stateCache as $folder) {
311: $folders[] = $folder['id'];
312: }
313: return $folders;
314: }
315:
316: 317: 318: 319: 320: 321: 322: 323:
324: public function initPingState($devId)
325: {
326: $this->_devId = $devId;
327: $file = $this->_stateDir . '/' . $this->_backend->getUser() . '/' . $devId;
328: if (file_exists($file)) {
329: $this->_pingState = unserialize(file_get_contents($file));
330: } else {
331: $this->resetPingState();
332: }
333:
334: return $this->_pingState['collections'];
335: }
336:
337: 338: 339: 340: 341: 342: 343: 344: 345:
346: public function loadDeviceInfo($devId, $user)
347: {
348: $this->_devId = $devId;
349: $file = $this->_stateDir . '/' . $user . '/info-' . $devId;
350: if (file_exists($file)) {
351: return unserialize(file_get_contents($file));
352: } else {
353: throw new Horde_ActiveSync_Exception('Device not found.');
354: }
355: }
356:
357: 358: 359: 360: 361: 362: 363: 364:
365: public function setDeviceInfo($data)
366: {
367: $this->_ensureUserDirectory();
368: $this->_devId = $data->id;
369: $file = $this->_stateDir . '/' . $this->_backend->getUser() . '/info-' . $this->_devId;
370: return file_put_contents($file, serialize($data));
371: }
372:
373: 374: 375: 376: 377: 378: 379: 380: 381:
382: public function deviceExists($devId, $user = null)
383: {
384: if (empty($user)) {
385: return count(glob($this->_stateDir . '/*/info-' . $devId)) > 0;
386: }
387: return file_exists($this->_stateDir . '/' . $user . '/info-' . $devId);
388: }
389:
390: 391: 392: 393: 394: 395: 396: 397: 398:
399: public function loadPingCollectionState($pingCollection)
400: {
401: if (empty($this->_pingState)) {
402: throw new Horde_ActiveSync_Exception('PING state not initialized');
403: }
404:
405: $haveState = false;
406:
407:
408:
409:
410: if (!empty($this->_pingState['collections'][$pingCollection['class']])) {
411: $this->_collection = $this->_pingState['collections'][$pingCollection['class']];
412: $this->_collection['synckey'] = $this->_devId;
413: $this->_stateCache = $this->_collection['state'];
414: $haveState = true;
415: }
416:
417:
418: if (!$haveState) {
419: $this->_logger->info('[' . $this->_devId . '] Empty state for '. $pingCollection['class']);
420:
421:
422: $this->_syncKey = $this->_devId;
423: $this->_collection = $pingCollection;
424: $this->_collection['synckey'] = $this->_devId;
425: $this->_collection['state'] = array();
426:
427:
428: $this->_pingState['collections'][$this->_collection['class']] = $this->_collection;
429:
430:
431: $this->_stateCache = array();
432:
433: $changes = $this->getChanges();
434: foreach ($changes as $change) {
435: switch ($change['type']) {
436: case 'change':
437: $stat = $this->_backend->statMessage($this->_collection['id'], $change['id']);
438: if (!$message = $this->_backend->getMessage($this->_collection['id'], $change['id'], 0)) {
439: continue;
440: }
441: if ($stat && $message) {
442: $this->updateState('change', $stat);
443: }
444: break;
445:
446: default:
447: throw new Horde_ActiveSync_Exception('Unexpected change type in loadPingState');
448: }
449: }
450:
451: $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache;
452: $this->savePingState();
453: }
454: }
455:
456: 457: 458: 459: 460: 461:
462: public function savePingState()
463: {
464: if (empty($this->_pingState)) {
465: throw new Horde_ActiveSync_Exception('PING state not initialized');
466: }
467: $this->_ensureUserDirectory();
468: $state = serialize(array('lifetime' => $this->_pingState['lifetime'],
469: 'collections' => $this->_pingState['collections']));
470:
471: $this->_logger->info('[' . $this->_devId . '] Saving new PING state.');
472: return file_put_contents($this->_stateDir . '/' . $this->_backend->getUser() . '/' . $this->_devId, $state);
473: }
474:
475: 476: 477: 478: 479: 480:
481: public function getHeartbeatInterval()
482: {
483: if (empty($this->_pingState)) {
484: throw new Horde_ActiveSync_Exception('PING state not initialized');
485: }
486:
487: return (!$this->_pingState) ? 0 : $this->_pingState['lifetime'];
488: }
489:
490: 491: 492: 493: 494:
495: public function setHeartbeatInterval($lifetime)
496: {
497: $this->_pingState['lifetime'] = $lifetime;
498: }
499:
500: 501: 502: 503: 504: 505:
506: public function setPolicyKey($devId, $key)
507: {
508: $info = $this->loadDeviceInfo($devId);
509: $info->policykey = $key;
510: $this->setDeviceInfo($info);
511: $this->_logger->info('[' . $devId . '] New policykey saved: ' . $key);
512: }
513:
514: 515: 516: 517: 518: 519: 520: 521:
522: public function resetAllPolicyKeys()
523: {
524: throw new Horde_ActiveSync_Exception('Not Implemented');
525: }
526:
527: 528: 529: 530: 531: 532: 533: 534:
535: public function setDeviceRWStatus($devId, $status)
536: {
537: $info = $this->loadDeviceInfo($devId);
538: $info->rwstatus = $status;
539: $this->setDeviceInfo($info);
540: $this->_logger->info('[' . $devId . '] Setting DeviceRWStatus: ' . $status);
541: }
542:
543: 544: 545: 546: 547: 548: 549:
550: public function getChanges($flags = 0)
551: {
552: $syncState = empty($this->_stateCache) ? array() : $this->_stateCache;
553: $cutoffdate = self::_getCutOffDate(!empty($this->_collection['filtertype']) ? $this->_collection['filtertype'] : 0);
554:
555: if (!empty($this->_collection['id'])) {
556: $folderId = $this->_collection['id'];
557: $this->_logger->info('[' . $this->_devId . '] Initializing message diff engine.');
558: if (!$syncState) {
559: $syncState = array();
560: }
561: $this->_logger->debug('[' . $this->_devId . ']' . count($syncState) . ' messages in state.');
562:
563:
564: if ($folderId != Horde_ActiveSync::FOLDER_TYPE_DUMMY) {
565:
566: if ($this->_collection['class'] === false && $flags == Horde_ActiveSync::BACKEND_DISCARD_DATA && $this->_backend->alterPing()) {
567:
568: $this->_changes = $this->_backend->alterPingChanges($folderId, $syncState);
569: } else {
570:
571: $msglist = $this->_backend->getMessageList($this->_collection['id'], $cutoffdate);
572: if ($msglist === false) {
573: return false;
574: }
575: $this->_changes = $this->_getDiff($syncState, $msglist);
576: }
577: }
578: $this->_logger->info('[' . $this->_devId . '] Found ' . count($this->_changes) . ' message changes.');
579:
580: } else {
581:
582: $this->_logger->info('[' . $this->_devId . '] Initializing folder diff engine.');
583: $folderlist = $this->_backend->getFolderList();
584: if ($folderlist === false) {
585: return false;
586: }
587:
588: $this->_changes = $this->_getDiff($syncState, $folderlist);
589: $this->_logger->info('[' . $this->_devId . '] Found ' . count($this->_changes) . ' folder changes.');
590: }
591:
592: return $this->_changes;
593: }
594:
595: 596: 597: 598: 599: 600:
601: public function removeState($syncKey = null, $devId = null)
602: {
603: if ($devId) {
604: throw new Horde_ActiveSync_Exception('Not implemented.');
605: }
606: $this->_gc($syncKey, true);
607: }
608:
609: public function listDevices()
610: {
611: throw new Horde_ActiveSync_Exception('Not Implemented');
612: }
613:
614: 615: 616: 617: 618: 619: 620: 621:
622: public function getLastSyncTimestamp()
623: {
624: throw new Horde_ActiveSync_Exception('Not Implemented');
625: }
626:
627: 628: 629: 630: 631: 632: 633: 634: 635:
636: private function _gc($syncKey, $all = false)
637: {
638: if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) {
639: return false;
640: }
641: $guid = $matches[1];
642: $n = $matches[2];
643:
644: $dir = @opendir($this->_stateDir . '/' . $this->_backend->getUser());
645: if (!$dir) {
646: return false;
647: }
648: while ($entry = readdir($dir)) {
649: if (preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $entry, $matches)) {
650: if ($matches[1] == $guid && ((!$all && $matches[2] < $n) || $all)) {
651: unlink($this->_stateDir . '/' . $this->_backend->getUser() . '/' . $entry);
652: }
653: }
654: }
655:
656: return true;
657: }
658:
659: 660: 661: 662: 663:
664: private function _ensureUserDirectory()
665: {
666:
667: if ($this->_haveStateDirectory) {
668: return true;
669: }
670:
671: $dir = $this->_stateDir . '/' . $this->_backend->getUser();
672: if (!file_exists($dir)) {
673: if (!mkdir($dir)) {
674: throw new Horde_ActiveSync_Exception('Failed to create user state storage');
675: }
676: }
677:
678: $this->_haveStateDirectory = true;
679: }
680:
681: }
682: