Overview

Packages

  • None
  • SyncMl

Classes

  • Horde_SyncMl
  • Horde_SyncMl_Backend
  • Horde_SyncMl_Backend_Horde
  • Horde_SyncMl_Command
  • Horde_SyncMl_Command_Alert
  • Horde_SyncMl_Command_Final
  • Horde_SyncMl_Command_Get
  • Horde_SyncMl_Command_Map
  • Horde_SyncMl_Command_Put
  • Horde_SyncMl_Command_Replace
  • Horde_SyncMl_Command_Results
  • Horde_SyncMl_Command_Status
  • Horde_SyncMl_Command_Sync
  • Horde_SyncMl_Command_SyncHdr
  • Horde_SyncMl_ContentHandler
  • Horde_SyncMl_DataStore
  • Horde_SyncMl_Device
  • Horde_SyncMl_Device_Nokia
  • Horde_SyncMl_Device_P800
  • Horde_SyncMl_Device_sync4j
  • Horde_SyncMl_Device_Sync4JMozilla
  • Horde_SyncMl_Device_Synthesis
  • Horde_SyncMl_DeviceInfo
  • Horde_SyncMl_Property
  • Horde_SyncMl_PropertyParameter
  • Horde_SyncMl_State
  • Horde_SyncMl_Sync
  • Horde_SyncMl_SyncElement
  • Horde_SyncMl_Translation
  • Horde_SyncMl_XmlOutput
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Copyright 2003-2012 Horde LLC (http://www.horde.org/)
  4:  *
  5:  * See the enclosed file COPYING for license information (LGPL). If you
  6:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  7:  *
  8:  * @author  Anthony Mills <amills@pyramid6.com>
  9:  * @package SyncMl
 10:  */
 11: class Horde_SyncMl_Sync
 12: {
 13:     /**
 14:      * @see Horde_SyncMl_Sync::_state
 15:      */
 16:     const STATE_INIT =      0;
 17:     const STATE_SYNC =      1;
 18:     const STATE_MAP =       2;
 19:     const STATE_COMPLETED = 3;
 20: 
 21:     /**
 22:      * Target (client) URI (database).
 23:      *
 24:      * @var string
 25:      */
 26:     protected $_targetLocURI;
 27: 
 28:     /**
 29:      * Source (server) URI (database).
 30:      *
 31:      * @var string
 32:      */
 33:     protected $_sourceLocURI;
 34: 
 35:     /**
 36:      * The synchronization method, one of the Horde_SyncMl::ALERT_* constants.
 37:      *
 38:      * @var integer
 39:      */
 40:     protected $_syncType;
 41: 
 42:     /**
 43:      * Counts the <Sync>s sent by the server.
 44:      *
 45:      * @var integer
 46:      */
 47:     protected $_syncsSent = 0;
 48: 
 49:     /**
 50:      * Counts the <Sync>s received by the server. Currently unused.
 51:      *
 52:      * @var integer
 53:      */
 54:     protected $_syncsReceived = 0;
 55: 
 56:     /**
 57:      * Map data is expected whenever an add is sent to the client.
 58:      *
 59:      * @var boolean
 60:      */
 61:     protected $_expectingMapData = false;
 62: 
 63:     /**
 64:      * State of the current sync.
 65:      *
 66:      * A sync starts in Horde_SyncMl_Sync::STATE_INIT and moves on to the next state with every
 67:      * <Final> received from the client: Horde_SyncMl_Sync::STATE_INIT, Horde_SyncMl_Sync::STATE_SYNC, Horde_SyncMl_Sync::STATE_MAP,
 68:      * Horde_SyncMl_Sync::STATE_COMPLETED.  Horde_SyncMl_Sync::STATE_MAP doesn't occur for _FROM_CLIENT syncs.
 69:      *
 70:      * @var constant
 71:      */
 72:     protected $_state = Horde_SyncMl_Sync::STATE_INIT;
 73: 
 74:     /**
 75:      * Sync Anchors determine the interval from which changes are retrieved.
 76:      *
 77:      * @var integer
 78:      */
 79:     protected $_clientAnchorNext;
 80: 
 81:     protected $_serverAnchorLast;
 82:     protected $_serverAnchorNext;
 83: 
 84:     /**
 85:      * Number of objects that have been sent to the server for adding.
 86:      *
 87:      * @var integer
 88:      */
 89:     protected $_client_add_count = 0;
 90: 
 91:     /**
 92:      * Number of objects that have been sent to the server for replacement.
 93:      *
 94:      * @var integer
 95:      */
 96:     protected $_client_replace_count = 0;
 97: 
 98:     /**
 99:      * Number of objects that have been sent to the server for deletion.
100:      *
101:      * @var integer
102:      */
103:     protected $_client_delete_count = 0;
104: 
105:     /**
106:      * Add due to client replace request when map entry is not found. Happens
107:      * during SlowSync.
108:      *
109:      * @var integer
110:      */
111:     protected $_client_addreplaces = 0;
112: 
113:     /**
114:      * Number of objects that have been sent to the client for adding.
115:      *
116:      * @var integer
117:      */
118:     protected $_server_add_count = 0;
119: 
120:     /**
121:      * Number of objects that have been sent to the client for replacement.
122:      *
123:      * @var integer
124:      */
125:     protected $_server_replace_count = 0;
126: 
127:     /**
128:      * Number of objects that have been sent to the client for deletion.
129:      *
130:      * @var integer
131:      */
132:     protected $_server_delete_count = 0;
133: 
134:     /**
135:      * Number of failed actions, for logging purposes only.
136:      *
137:      * @var integer
138:      */
139:     protected $_errors = 0;
140: 
141:     /**
142:      * List of object UIDs (in the keys) that have been added on the server
143:      * since the last synchronization and are supposed to be sent to the
144:      * client.
145:      *
146:      * @var array
147:      */
148:     protected $_server_adds;
149: 
150:     /**
151:      * List of object UIDs (in the keys) that have been changed on the server
152:      * since the last synchronization and are supposed to be sent to the
153:      * client.
154:      *
155:      * @var array
156:      */
157:     protected $_server_replaces;
158: 
159:     /**
160:      * List of object UIDs (in the keys) that have been deleted on the server
161:      * since the last synchronization and are supposed to be sent to the
162:      * client.
163:      *
164:      * @var array
165:      */
166:     protected $_server_deletes;
167: 
168:     /**
169:      * List of task UIDs (in the keys) that have been added on the server
170:      * since the last synchronization and are supposed to be sent to the
171:      * client.
172:      *
173:      * This is only used for clients handling tasks and events in one
174:      * database. We need to seperately store the server tasks adds, so when we
175:      * get a Map command from the client, we know whether to put this in tasks
176:      * or calendar.
177:      *
178:      * @var array
179:      */
180:     protected $_server_task_adds;
181: 
182:     /**
183:      *
184:      * @param string $syncType
185:      * @param string $serverURI
186:      * @param string $clientURI
187:      * @param integer $serverAnchorLast
188:      * @param integer $serverAnchorNext
189:      * @param string $clientAnchorNext
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:      * Here's where the actual processing of a client-sent Sync Item takes
204:      * place. Entries are added, deleted or replaced from the server database
205:      * by using backend API calls.
206:      *
207:      * @todo maybe this should be moved to SyncItem
208:      *
209:      * @param $output
210:      * @param Horde_SyncMl_SyncElement $item
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:         /* if size of item is set: check it first */
220:         if ($item->size > 0) {
221:             if (strlen($item->content) != $item->size &&
222:                 /* For some strange reason the SyncML conformance test suite
223:                  * sends an item with length n and size tag=n+1 and expects us
224:                  * the accept it. Happens in test 1301.  So ignore this to be
225:                  * conformant (and wrong). */
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:             /* Check if the client sends us a vtodo in a calendar sync. */
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:         /* Use contentType explicitly specified in this sync command. */
258:         $contentType = $item->contentType;
259: 
260:         /* If not provided, use default from device info. */
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:             /* Handle client add requests.
274:              *
275:              * @todo: check if this $cuid is already present and then maybe do
276:              * an replace instead? */
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:                 /* @todo: better response code. */
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:             /* Handle client delete requests. */
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:             /* Handle client replace requests. */
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:                 /* Entry may have been deleted; try adding it. */
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:                     /* @todo: better response code. */
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:      * Creates a <Sync> output containing the server changes.
346:      *
347:      * @todo Check for Mem/FreeMem and Mem/FreeID when checking MaxObjSize
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:         /* If sync data from client only, nothing to be done here. */
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:         /* If one sync has been sent an no pending data: bail out. */
364:         if ($this->_syncsSent > 0 && !$this->hasPendingElements()) {
365:             return;
366:         }
367: 
368:         /* $messageFull will be set to true to indicate that there's no room
369:          * for other data in this message. If it's false (empty) and there are
370:          * pending Sync data, the final command will sent the pending data. */
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:         /* If server modifications are not retrieved yet (first Sync element),
387:          * do it now. */
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:             /* If tasks are handled inside calendar, do the same again for
403:              * tasks. Merge resulting arrays. */
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:             /* Now we know the number of Changes and can send them to the
430:              * client. */
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:             /* Package continued. Sync in subsequent message. */
442:             $output->outputSyncStart($this->_sourceLocURI,
443:                                      $this->_targetLocURI);
444:         }
445: 
446:         /* We sent a Sync. So at least we espect a status response and thus
447:          * another message from the client. */
448:         $GLOBALS['message_expectresponse'] = true;
449: 
450:         /* Handle deletions. */
451:         $deletes = $this->_server_deletes;
452:         foreach ($deletes as $suid => $cuid) {
453:             /* Check if we have space left in the message. */
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:             /* Create a Delete request for client. */
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:         /* Handle additions. */
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:             /* Item in history but not in database. Strange, but can
483:              * happen. */
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:                 /* Check if we have space left in the message. */
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:                         /* @todo: implement partial sending instead of
501:                          * dropping item! */
502:                         unset($this->_server_adds[$suid]);
503:                         continue;
504:                     }
505:                     $messageFull = true;
506:                     $output->outputSyncEnd();
507:                     $this->_syncsSent += 1;
508:                     return;
509:                 }
510: 
511:                 /* @todo: on SlowSync send Replace instead! */
512:                 // $output->outputSyncCommand($refts == 0 ? 'Replace' : 'Add',
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:         /* Handle Replaces. */
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:                 /* Item in history but not in database. */
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:             /* Check if we have space left in the message. */
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:                     /* @todo: implement partial sending instead of
553:                      * dropping item! */
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:         /* Finished! Send closing </Sync>. */
572:         $output->outputSyncEnd();
573:         $this->_syncsSent += 1;
574:     }
575: 
576:     /**
577:      * Retrieves and condenses the changes on the server side since the last
578:      * synchronization.
579:      *
580:      * @param string $syncDB   The database being synchronized.
581:      * @param array $adds      Will be set with the server-client-uid mappings
582:      *                         of added objects.
583:      * @param array $replaces  Will be set with the server-client-uid mappings
584:      *                         of changed objects.
585:      * @param array $deletes   Will be set with the server-client-uid mappings
586:      *                         of deleted objects.
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:      * Notifies the sync that a final has been received by the client.
606:      *
607:      * Depending on the current state of the sync this can mean various
608:      * things:
609:      * a) Init phase (Alerts) done. Next package contaings actual syncs.
610:      * b) Sync sending from client done. Next package are maps (or finish
611:      *    or finish if ONE_WAY_FROM_CLIENT sync
612:      * c) Maps finished, completly done.
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:             /* Received all client Sync data, now we are allowed to send
639:              * server sync data. */
640:             if (!$debug) {
641:                 $this->createSyncOutput($output);
642:             }
643: 
644:             // FROM_CLIENT_SYNC doeesn't require a MAP package:
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:      * Returns true if there are still outstanding server sync items to
661:      * be sent to the client.
662:      *
663:      * This is the case if the MaxMsgSize has been reached and the pending
664:      * elements are to be sent in another message.
665:      */
666:     public function hasPendingElements()
667:     {
668:         if (!is_array($this->_server_adds)) {
669:             /* Changes not compiled yet: not pending: */
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:     /* Currently unused */
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:      * Completes a sync once everything is done: store the sync anchors so the
694:      * next sync can be a delta sync and produce some debug info.
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:      * Returns the client ID of server change identified by the change type
763:      * and server ID.
764:      *
765:      * @param string $change  The change type (add, replace, delete).
766:      * @param string $id      The object's server UID.
767:      *
768:      * @return string  The matching client ID or null if none found.
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:      * Sets the client ID of server change identified by the change type and
778:      * server ID.
779:      *
780:      * @param string $change  The change type (add, replace, delete).
781:      * @param string $sid     The object's server UID.
782:      * @param string $cid     The object's client UID.
783:      */
784:     public function setServerChange($change, $sid, $cid)
785:     {
786:         $property = '_server_' . $change . 's';
787:         $this->$property[$sid] = $cid;
788:     }
789: 
790:     /**
791:      * Unsets the server-client-map of server change identified by the change
792:      * type and server ID.
793:      *
794:      * @param string $change  The change type (add, replace, delete).
795:      * @param string $id      The object's server UID.
796:      */
797:     public function unsetServerChange($change, $id)
798:     {
799:         $property = '_server_' . $change . 's';
800:         unset($this->$property[$id]);
801:     }
802: 
803:     /**
804:      * Converts a calendar databaseURI to a tasks databaseURI for devices with
805:      * handleTasksInCalendar.
806:      */
807:     protected function _taskToCalendar($databaseURI)
808:     {
809:         return str_replace('calendar', 'tasks', $databaseURI);
810:     }
811: }
812: 
API documentation generated by ApiGen