1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10:
11: class Horde_SyncMl_Sync
12: {
13: 14: 15:
16: const STATE_INIT = 0;
17: const STATE_SYNC = 1;
18: const STATE_MAP = 2;
19: const STATE_COMPLETED = 3;
20:
21: 22: 23: 24: 25:
26: protected $_targetLocURI;
27:
28: 29: 30: 31: 32:
33: protected $_sourceLocURI;
34:
35: 36: 37: 38: 39:
40: protected $_syncType;
41:
42: 43: 44: 45: 46:
47: protected $_syncsSent = 0;
48:
49: 50: 51: 52: 53:
54: protected $_syncsReceived = 0;
55:
56: 57: 58: 59: 60:
61: protected $_expectingMapData = false;
62:
63: 64: 65: 66: 67: 68: 69: 70: 71:
72: protected $_state = Horde_SyncMl_Sync::STATE_INIT;
73:
74: 75: 76: 77: 78:
79: protected $_clientAnchorNext;
80:
81: protected $_serverAnchorLast;
82: protected $_serverAnchorNext;
83:
84: 85: 86: 87: 88:
89: protected $_client_add_count = 0;
90:
91: 92: 93: 94: 95:
96: protected $_client_replace_count = 0;
97:
98: 99: 100: 101: 102:
103: protected $_client_delete_count = 0;
104:
105: 106: 107: 108: 109: 110:
111: protected $_client_addreplaces = 0;
112:
113: 114: 115: 116: 117:
118: protected $_server_add_count = 0;
119:
120: 121: 122: 123: 124:
125: protected $_server_replace_count = 0;
126:
127: 128: 129: 130: 131:
132: protected $_server_delete_count = 0;
133:
134: 135: 136: 137: 138:
139: protected $_errors = 0;
140:
141: 142: 143: 144: 145: 146: 147:
148: protected $_server_adds;
149:
150: 151: 152: 153: 154: 155: 156:
157: protected $_server_replaces;
158:
159: 160: 161: 162: 163: 164: 165:
166: protected $_server_deletes;
167:
168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179:
180: protected $_server_task_adds;
181:
182: 183: 184: 185: 186: 187: 188: 189: 190:
191: public function __construct($syncType, $serverURI, $clientURI, $serverAnchorLast,
192: $serverAnchorNext, $clientAnchorNext)
193: {
194: $this->_syncType = $syncType;
195: $this->_targetLocURI = $serverURI;
196: $this->_sourceLocURI = $clientURI;
197: $this->_clientAnchorNext = $clientAnchorNext;
198: $this->_serverAnchorLast = $serverAnchorLast;
199: $this->_serverAnchorNext = $serverAnchorNext;
200: }
201:
202: 203: 204: 205: 206: 207: 208: 209: 210: 211:
212: public function handleClientSyncItem(&$output, &$item)
213: {
214: global $backend;
215:
216: $backend->logMessage(
217: 'Handling <' . $item->elementType . '> sent from client', 'DEBUG');
218:
219:
220: if ($item->size > 0) {
221: if (strlen($item->content) != $item->size &&
222: 223: 224: 225:
226: strlen($item->content) + 1 != $item->size) {
227: $item->responseCode = Horde_SyncMl::RESPONSE_SIZE_MISMATCH;
228: $backend->logMessage(
229: 'Item size mismatch. Size reported as ' . $item->size
230: . ' but actual size is ' . strlen($item->content), 'ERR');
231: $this->_errors++;
232: return false;
233: }
234: }
235:
236: $device = $GLOBALS['backend']->state->getDevice();
237: $hordedatabase = $database = $this->_targetLocURI;
238: $content = $item->content;
239: if ($item->contentFormat == 'b64') {
240: $content = base64_decode($content);
241: }
242:
243: if (($item->contentType == 'text/calendar' ||
244: $item->contentType == 'text/x-vcalendar') &&
245: $backend->normalize($database) == 'calendar' &&
246: $device->handleTasksInCalendar()) {
247: $tasksincalendar = true;
248:
249: if (preg_match('/(\r\n|\r|\n)BEGIN[^:]*:VTODO/',
250: "\n" . $content)) {
251: $hordedatabase = $this->_taskToCalendar($backend->normalize($database));
252: }
253: } else {
254: $tasksincalendar = false;
255: }
256:
257:
258: $contentType = $item->contentType;
259:
260:
261: if (!$contentType) {
262: $contentType = $device->getPreferredContentType($hordedatabase);
263: }
264:
265: if ($item->elementType != 'Delete') {
266: list($content, $contentType) = $device->convertClient2Server($content, $contentType);
267: }
268:
269: $cuid = $item->cuid;
270: $suid = false;
271:
272: if ($item->elementType == 'Add') {
273: 274: 275: 276:
277: $suid = $backend->addEntry($hordedatabase, $content, $contentType, $cuid);
278: if (!is_a($suid, 'PEAR_Error')) {
279: $this->_client_add_count++;
280: $item->responseCode = Horde_SyncMl::RESPONSE_ITEM_ADDED;
281: $backend->logMessage('Added client entry as ' . $suid, 'DEBUG');
282: } else {
283: $this->_errors++;
284:
285: $item->responseCode = Horde_SyncMl::RESPONSE_NO_EXECUTED;
286: $backend->logMessage('Error in adding client entry: ' . $suid->message, 'ERR');
287: }
288: } elseif ($item->elementType == 'Delete') {
289:
290: $ok = $backend->deleteEntry($database, $cuid);
291: if (!$ok && $tasksincalendar) {
292: $backend->logMessage(
293: 'Task ' . $cuid . ' deletion sent with calendar request', 'DEBUG');
294: $ok = $backend->deleteEntry($this->_taskToCalendar($backend->normalize($database)), $cuid);
295: }
296:
297: if ($ok) {
298: $this->_client_delete_count++;
299: $item->responseCode = Horde_SyncMl::RESPONSE_OK;
300: $backend->logMessage('Deleted entry ' . $suid . ' due to client request', 'DEBUG');
301: } else {
302: $this->_errors++;
303: $item->responseCode = Horde_SyncMl::RESPONSE_ITEM_NO_DELETED;
304: $backend->logMessage('Failure deleting client entry, maybe already disappeared from server', 'DEBUG');
305: }
306:
307: } elseif ($item->elementType == 'Replace') {
308:
309: $suid = $backend->replaceEntry($hordedatabase, $content,
310: $contentType, $cuid);
311:
312: if (!is_a($suid, 'PEAR_Error')) {
313: $this->_client_replace_count++;
314: $item->responseCode = Horde_SyncMl::RESPONSE_OK;
315: $backend->logMessage('Replaced entry ' . $suid . ' due to client request', 'DEBUG');
316: } else {
317: $backend->logMessage($suid->message, 'DEBUG');
318:
319:
320: $suid = $backend->addEntry($hordedatabase, $content,
321: $contentType, $cuid);
322: if (!is_a($suid, 'PEAR_Error')) {
323: $this->_client_addreplaces++;
324: $item->responseCode = Horde_SyncMl::RESPONSE_ITEM_ADDED;
325: $backend->logMessage(
326: 'Added instead of replaced entry ' . $suid, 'DEBUG');
327: } else {
328: $this->_errors++;
329:
330: $item->responseCode = Horde_SyncMl::RESPONSE_NO_EXECUTED;
331: $backend->logMessage(
332: 'Error in adding client entry due to replace request: '
333: . $suid->message, 'ERR');
334: }
335: }
336: } else {
337: $backend->logMessage(
338: 'Unexpected elementType: ' . $item->elementType, 'ERR');
339: }
340:
341: return $suid;
342: }
343:
344: 345: 346: 347: 348:
349: public function createSyncOutput(&$output)
350: {
351: global $backend, $messageFull;
352:
353: $backend->logMessage(
354: 'Creating <Sync> output for server changes in database '
355: . $this->_targetLocURI, 'DEBUG');
356:
357:
358: if($this->_syncType == Horde_SyncMl::ALERT_ONE_WAY_FROM_CLIENT ||
359: $this->_syncType == Horde_SyncMl::ALERT_REFRESH_FROM_CLIENT) {
360: return;
361: }
362:
363:
364: if ($this->_syncsSent > 0 && !$this->hasPendingElements()) {
365: return;
366: }
367:
368: 369: 370:
371: $messageFull = false;
372:
373: $state = $GLOBALS['backend']->state;
374: $device = $state->getDevice();
375: $contentType = $device->getPreferredContentTypeClient(
376: $this->_targetLocURI, $this->_sourceLocURI);
377: $contentTypeTasks = $device->getPreferredContentTypeClient(
378: 'tasks', $this->_sourceLocURI);
379: if ($state->deviceInfo && $state->deviceInfo->CTCaps) {
380: $fields = array($contentType => isset($state->deviceInfo->CTCaps[$contentType]) ? $state->deviceInfo->CTCaps[$contentType] : null,
381: $contentTypeTasks => isset($state->deviceInfo->CTCaps[$contentTypeTasks]) ? $state->deviceInfo->CTCaps[$contentTypeTasks] : null);
382: } else {
383: $fields = array($contentType => null, $contentTypeTasks => null);
384: }
385:
386: 387:
388: if (!is_array($this->_server_adds)) {
389: $backend->logMessage(
390: 'Compiling server changes from '
391: . date('Y-m-d H:i:s', $this->_serverAnchorLast)
392: . ' to ' . date('Y-m-d H:i:s', $this->_serverAnchorNext), 'DEBUG');
393:
394: $result = $this->_retrieveChanges($this->_targetLocURI,
395: $this->_server_adds,
396: $this->_server_replaces,
397: $this->_server_deletes);
398: if (is_a($result, 'PEAR_Error')) {
399: return;
400: }
401:
402: 403:
404: if ($backend->normalize($this->_targetLocURI) == 'calendar' &&
405: $device->handleTasksInCalendar()) {
406: $this->_server_task_adds = $deletes2 = $replaces2 = array();
407: $result = $this->_retrieveChanges('tasks',
408: $this->_server_task_adds,
409: $replaces2,
410: $deletes2);
411: if (is_a($result, 'PEAR_Error')) {
412: return;
413: }
414: $this->_server_adds = array_merge($this->_server_adds,
415: $this->_server_task_adds);
416: $this->_server_replaces = array_merge($this->_server_replaces,
417: $replaces2);
418: $this->_server_deletes = array_merge($this->_server_deletes,
419: $deletes2);
420: }
421:
422: $numChanges = count($this->_server_adds)
423: + count($this->_server_replaces)
424: + count($this->_server_deletes);
425: $backend->logMessage(
426: 'Sending ' . $numChanges . ' server changes ' . 'for client URI '
427: . $this->_targetLocURI, 'DEBUG');
428:
429: 430:
431: $di = $state->deviceInfo;
432: if ($di->SupportNumberOfChanges) {
433: $output->outputSyncStart($this->_sourceLocURI,
434: $this->_targetLocURI,
435: $numChanges);
436: } else {
437: $output->outputSyncStart($this->_sourceLocURI,
438: $this->_targetLocURI);
439: }
440: } else {
441:
442: $output->outputSyncStart($this->_sourceLocURI,
443: $this->_targetLocURI);
444: }
445:
446: 447:
448: $GLOBALS['message_expectresponse'] = true;
449:
450:
451: $deletes = $this->_server_deletes;
452: foreach ($deletes as $suid => $cuid) {
453:
454: if ($state->maxMsgSize - $output->getOutputSize() < Horde_SyncMl::MSG_TRAILER_LEN) {
455: $backend->logMessage(
456: 'Maximum message size ' . $state->maxMsgSize
457: . ' approached during delete; current size: '
458: . $output->getOutputSize(), 'DEBUG');
459: $messageFull = true;
460: $output->outputSyncEnd();
461: $this->_syncsSent += 1;
462: return;
463: }
464: $backend->logMessage(
465: "Sending delete from server: client id $cuid, server id $suid", 'DEBUG');
466:
467: $cmdId = $output->outputSyncCommand('Delete', null, null, null, $cuid, null);
468: unset($this->_server_deletes[$suid]);
469: $state->serverChanges[$state->messageID][$this->_targetLocURI][$cmdId] = array($suid, $cuid);
470: $this->_server_delete_count++;
471: }
472:
473:
474: $adds = $this->_server_adds;
475: foreach ($adds as $suid => $cuid) {
476: $backend->logMessage("Sending add from server: $suid", 'DEBUG');
477:
478: $syncDB = isset($this->_server_task_adds[$suid]) ? 'tasks' : $this->_targetLocURI;
479: $ct = isset($this->_server_task_adds[$suid]) ? $contentTypeTasks : $contentType;
480:
481: $c = $backend->retrieveEntry($syncDB, $suid, $ct, $fields[$ct]);
482: 483:
484: if (is_a($c, 'PEAR_Error')) {
485: $backend->logMessage(
486: 'API export call for ' . $suid . ' failed: '
487: . $c->getMessage(), 'ERR');
488: } else {
489: list($clientContent, $clientContentType, $clientEncodingType) =
490: $device->convertServer2Client($c, $contentType, $syncDB);
491:
492: if (($state->maxMsgSize - $output->getOutputSize() - strlen($clientContent)) < Horde_SyncMl::MSG_TRAILER_LEN) {
493: $backend->logMessage(
494: 'Maximum message size ' . $state->maxMsgSize
495: . ' approached during add; current size: '
496: . $output->getOutputSize(), 'DEBUG');
497: if (strlen($clientContent) + Horde_SyncMl::MSG_DEFAULT_LEN > $state->maxMsgSize) {
498: $backend->logMessage(
499: 'Data item won\'t fit into a single message. Partial sending not implemented yet. Item will not be sent!', 'WARN');
500: 501:
502: unset($this->_server_adds[$suid]);
503: continue;
504: }
505: $messageFull = true;
506: $output->outputSyncEnd();
507: $this->_syncsSent += 1;
508: return;
509: }
510:
511:
512:
513: $cmdId = $output->outputSyncCommand('Add', $clientContent,
514: $clientContentType,
515: $clientEncodingType,
516: null, $suid);
517: $this->_server_add_count++;
518: $state->serverChanges[$state->messageID][$this->_targetLocURI][$cmdId] = array($suid, 0);
519: }
520: unset($this->_server_adds[$suid]);
521: }
522:
523: if ($this->_server_add_count) {
524: $this->_expectingMapData = true;
525: }
526:
527:
528: $replaces = $this->_server_replaces;
529: foreach ($replaces as $suid => $cuid) {
530: $syncDB = isset($replaces2[$suid]) ? 'tasks' : $this->_targetLocURI;
531: $ct = isset($replaces2[$suid]) ? $contentTypeTasks : $contentType;
532: $c = $backend->retrieveEntry($syncDB, $suid, $ct, $fields[$ct]);
533: if (is_a($c, 'PEAR_Error')) {
534:
535: unset($this->_server_replaces[$suid]);
536: continue;
537: }
538:
539: $backend->logMessage(
540: "Sending replace from server: $suid", 'DEBUG');
541: list($clientContent, $clientContentType, $clientEncodingType) =
542: $device->convertServer2Client($c, $contentType, $syncDB);
543:
544: if (($state->maxMsgSize - $output->getOutputSize() - strlen($clientContent)) < Horde_SyncMl::MSG_TRAILER_LEN) {
545: $backend->logMessage(
546: 'Maximum message size ' . $state->maxMsgSize
547: . ' approached during replace; current size: '
548: . $output->getOutputSize(), 'DEBUG');
549: if (strlen($clientContent) + Horde_SyncMl::MSG_DEFAULT_LEN > $state->maxMsgSize) {
550: $backend->logMessage(
551: 'Data item won\'t fit into a single message. Partial sending not implemented yet. Item will not be sent!', 'WARNING');
552: 553:
554: unset($this->_server_replaces[$suid]);
555: continue;
556: }
557: $messageFull = true;
558: $output->outputSyncEnd();
559: $this->_syncsSent += 1;
560: return;
561: }
562: $cmdId = $output->outputSyncCommand('Replace', $clientContent,
563: $clientContentType,
564: $clientEncodingType,
565: $cuid, null);
566: $this->_server_replace_count++;
567: unset($this->_server_replaces[$suid]);
568: $state->serverChanges[$state->messageID][$this->_targetLocURI][$cmdId] = array($suid, $cuid);
569: }
570:
571:
572: $output->outputSyncEnd();
573: $this->_syncsSent += 1;
574: }
575:
576: 577: 578: 579: 580: 581: 582: 583: 584: 585: 586: 587:
588: protected function _retrieveChanges($syncDB, &$adds, &$replaces, &$deletes)
589: {
590: $adds = $replaces = $deletes = array();
591: if ($syncDB == 'configuration') {
592: return;
593: }
594: $result = $GLOBALS['backend']->getServerChanges($syncDB,
595: $this->_serverAnchorLast,
596: $this->_serverAnchorNext,
597: $adds, $replaces, $deletes);
598: if (is_a($result, 'PEAR_Error')) {
599: $GLOBALS['backend']->logMessage($result, 'ERR');
600: return $result;
601: }
602: }
603:
604: 605: 606: 607: 608: 609: 610: 611: 612: 613:
614: public function handleFinal(&$output, $debug = false)
615: {
616: switch ($this->_state) {
617: case Horde_SyncMl_Sync::STATE_INIT:
618: $state = 'Init';
619: break;
620: case Horde_SyncMl_Sync::STATE_SYNC:
621: $state = 'Sync';
622: break;
623: case Horde_SyncMl_Sync::STATE_MAP:
624: $state = 'Map';
625: break;
626: case Horde_SyncMl_Sync::STATE_COMPLETED:
627: $state = 'Completed';
628: break;
629: }
630:
631: $GLOBALS['backend']->logMessage('Handle <Final> for state ' . $state, 'DEBUG');
632:
633: switch ($this->_state) {
634: case Horde_SyncMl_Sync::STATE_INIT:
635: $this->_state = Horde_SyncMl_Sync::STATE_SYNC;
636: break;
637: case Horde_SyncMl_Sync::STATE_SYNC:
638: 639:
640: if (!$debug) {
641: $this->createSyncOutput($output);
642: }
643:
644:
645: if ($this->_syncType == Horde_SyncMl::ALERT_ONE_WAY_FROM_CLIENT ||
646: $this->_syncType == Horde_SyncMl::ALERT_REFRESH_FROM_CLIENT ||
647: !$this->_expectingMapData) {
648: $this->_state = Horde_SyncMl_Sync::STATE_COMPLETED;
649: } else {
650: $this->_state = Horde_SyncMl_Sync::STATE_MAP;
651: }
652: break;
653: case Horde_SyncMl_Sync::STATE_MAP:
654: $this->_state = Horde_SyncMl_Sync::STATE_COMPLETED;
655: break;
656: }
657: }
658:
659: 660: 661: 662: 663: 664: 665:
666: public function hasPendingElements()
667: {
668: if (!is_array($this->_server_adds)) {
669:
670: return false;
671: }
672:
673: return (count($this->_server_adds) + count($this->_server_replaces) + count($this->_server_deletes)) > 0;
674: }
675:
676: public function addSyncReceived()
677: {
678: $this->_syncsReceived++;
679: }
680:
681:
682: public function getSyncsReceived()
683: {
684: return $this->_syncsReceived;
685: }
686:
687: public function isComplete()
688: {
689: return $this->_state == Horde_SyncMl_Sync::STATE_COMPLETED;
690: }
691:
692: 693: 694: 695:
696: public function closeSync()
697: {
698: $GLOBALS['backend']->writeSyncAnchors($this->_targetLocURI,
699: $this->_clientAnchorNext,
700: $this->_serverAnchorNext);
701:
702: $s = sprintf(
703: 'Finished sync of database %s. Failures: %d; '
704: . 'changes from client (Add, Replace, Delete, AddReplaces): %d, %d, %d, %d; '
705: . 'changes from server (Add, Replace, Delete): %d, %d, %d',
706: $this->_targetLocURI,
707: $this->_errors,
708: $this->_client_add_count,
709: $this->_client_replace_count,
710: $this->_client_delete_count,
711: $this->_client_addreplaces,
712: $this->_server_add_count,
713: $this->_server_replace_count,
714: $this->_server_delete_count);
715: $GLOBALS['backend']->logMessage($s, 'INFO');
716: }
717:
718: public function getServerLocURI()
719: {
720: return $this->_targetLocURI;
721: }
722:
723: public function getClientLocURI()
724: {
725: return $this->_sourceLocURI;
726: }
727:
728: public function getClientAnchorNext()
729: {
730: return $this->_clientAnchorNext;
731: }
732:
733: public function getServerAnchorNext()
734: {
735: return $this->_serverAnchorNext;
736: }
737:
738: public function getServerAnchorLast()
739: {
740: return $this->_serverAnchorLast;
741: }
742:
743: public function createUidMap($databaseURI, $cuid, $suid)
744: {
745: $device = $GLOBALS['backend']->state->getDevice();
746:
747: if ($GLOBALS['backend']->normalize($databaseURI) == 'calendar' &&
748: $device->handleTasksInCalendar() &&
749: isset($this->_server_task_adds[$suid])) {
750: $db = $this->_taskToCalendar($GLOBALS['backend']->normalize($databaseURI));
751: } else {
752: $db = $databaseURI;
753: }
754:
755: $GLOBALS['backend']->createUidMap($db, $cuid, $suid);
756: $GLOBALS['backend']->logMessage(
757: 'Created map for client id ' . $cuid . ' and server id ' . $suid
758: . ' in database ' . $db, 'DEBUG');
759: }
760:
761: 762: 763: 764: 765: 766: 767: 768: 769:
770: public function getServerChange($change, $id)
771: {
772: $property = '_server_' . $change . 's';
773: return isset($this->$property[$id]) ? $this->$property[$id] : null;
774: }
775:
776: 777: 778: 779: 780: 781: 782: 783:
784: public function setServerChange($change, $sid, $cid)
785: {
786: $property = '_server_' . $change . 's';
787: $this->$property[$sid] = $cid;
788: }
789:
790: 791: 792: 793: 794: 795: 796:
797: public function unsetServerChange($change, $id)
798: {
799: $property = '_server_' . $change . 's';
800: unset($this->$property[$id]);
801: }
802:
803: 804: 805: 806:
807: protected function _taskToCalendar($databaseURI)
808: {
809: return str_replace('calendar', 'tasks', $databaseURI);
810: }
811: }
812: