mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-12-22 14:41:29 +01:00
468 lines
15 KiB
PHP
468 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* eGroupWare - SyncML based on Horde 3
|
|
*
|
|
*
|
|
* Using the PEAR Log class (which need to be installed!)
|
|
*
|
|
* @link http://www.egroupware.org
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @package api
|
|
* @subpackage horde
|
|
* @author Anthony Mills <amills@pyramid6.com>
|
|
* @author Joerg Lehrke <jlehrke@noc.de>
|
|
* @copyright (c) The Horde Project (http://www.horde.org/)
|
|
* @version $Id$
|
|
*/
|
|
|
|
class Horde_SyncML_Sync {
|
|
|
|
/**
|
|
* Target, either contacts, notes, events,
|
|
*/
|
|
var $_targetLocURI;
|
|
|
|
var $_sourceLocURI;
|
|
|
|
var $_locName;
|
|
|
|
/**
|
|
* The synchronization method, one of the ALERT_* constants.
|
|
*
|
|
* @var integer
|
|
*/
|
|
var $_syncType;
|
|
|
|
/**
|
|
* Return if all commands success.
|
|
*/
|
|
var $globalSuccess;
|
|
|
|
/**
|
|
* This is the content type to use to export data.
|
|
*/
|
|
var $preferedContentType;
|
|
|
|
/**
|
|
* Optional filter expression for this content.
|
|
*
|
|
* @var string
|
|
*/
|
|
var $_filterExpression = '';
|
|
|
|
|
|
|
|
/**
|
|
* Do have the sync data loaded from the database already?
|
|
*/
|
|
var $syncDataLoaded;
|
|
|
|
function &factory($alert) {
|
|
Horde::logMessage('SyncML: new sync for alerttype ' . $alert, __FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
switch ($alert) {
|
|
case ALERT_TWO_WAY:
|
|
include_once 'Horde/SyncML/Sync/TwoWaySync.php';
|
|
return $sync = new Horde_SyncML_Sync_TwoWaySync();
|
|
|
|
case ALERT_SLOW_SYNC:
|
|
include_once 'Horde/SyncML/Sync/SlowSync.php';
|
|
return $sync = new Horde_SyncML_Sync_SlowSync();
|
|
|
|
case ALERT_ONE_WAY_FROM_CLIENT:
|
|
include_once 'Horde/SyncML/Sync/OneWayFromClientSync.php';
|
|
return $sync = new Horde_SyncML_Sync_OneWayFromClientSync();
|
|
|
|
case ALERT_REFRESH_FROM_CLIENT:
|
|
include_once 'Horde/SyncML/Sync/RefreshFromClientSync.php';
|
|
return $sync = new Horde_SyncML_Sync_RefreshFromClientSync();
|
|
|
|
case ALERT_ONE_WAY_FROM_SERVER:
|
|
include_once 'Horde/SyncML/Sync/OneWayFromServerSync.php';
|
|
return $sync = new Horde_SyncML_Sync_OneWayFromServerSync();
|
|
|
|
case ALERT_REFRESH_FROM_SERVER:
|
|
include_once 'Horde/SyncML/Sync/RefreshFromServerSync.php';
|
|
return $sync = new Horde_SyncML_Sync_RefreshFromServerSync();
|
|
}
|
|
|
|
require_once 'PEAR.php';
|
|
return PEAR::raiseError('Alert ' . $alert . ' not found.');
|
|
}
|
|
|
|
function nextSyncCommand($currentCmdID, &$syncCommand, &$output) {
|
|
$this->runSyncCommand($syncCommand);
|
|
if ($syncCommand->hasMoreData()) {
|
|
Horde::logMessage('SyncML: moreData: TRUE', __FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
$syncCommand->setStatus(RESPONSE_CHUNKED_ITEM_ACCEPTED_AND_BUFFERED);
|
|
}
|
|
return $syncCommand->output($currentCmdID, $output);
|
|
}
|
|
|
|
function startSync($currentCmdID, &$output) {
|
|
return $currentCmdID;
|
|
}
|
|
|
|
function endSync($currentCmdID, &$output) {
|
|
return $currentCmdID;
|
|
}
|
|
|
|
/**
|
|
* Setter for property sourceURI.
|
|
*
|
|
* @param string $sourceURI New value of property sourceLocURI.
|
|
*/
|
|
function setSourceLocURI($sourceURI) {
|
|
$this->_sourceLocURI = $sourceURI;
|
|
}
|
|
|
|
/**
|
|
* Get sourceURI.
|
|
*
|
|
* @return string $sourceURI or false on error.
|
|
*/
|
|
function getSourceLocURI() {
|
|
if (empty($this->_sourceLocURI)) {
|
|
return false;
|
|
}
|
|
return $this->_sourceLocURI;
|
|
}
|
|
|
|
/**
|
|
* Setter for property targetURI.
|
|
*
|
|
* @param string $targetURI New value of property targetLocURI.
|
|
*/
|
|
function setTargetLocURI($targetURI) {
|
|
$this->_targetLocURI = $targetURI;
|
|
}
|
|
|
|
/**
|
|
* Setter for property syncType.
|
|
*
|
|
* @param integer $syncType New value of property syncType.
|
|
*/
|
|
function setSyncType($syncType) {
|
|
$this->_syncType = $syncType;
|
|
}
|
|
|
|
/**
|
|
* Setter for property locName.
|
|
*
|
|
* @param string $locName New value of property locName.
|
|
*/
|
|
function setLocName($locName) {
|
|
$this->_locName = $locName;
|
|
}
|
|
|
|
/**
|
|
* Setter for property filterExpression.
|
|
*
|
|
* @param string $expression New value of property filterExpression.
|
|
*/
|
|
function setFilterExpression($expression) {
|
|
$this->_filterExpression = $expression;
|
|
}
|
|
|
|
/**
|
|
* Here's where the actual processing of a client-sent Sync
|
|
* Command takes place. Entries are added, deleted or replaced
|
|
* from the server database by using Horde API (Registry) calls.
|
|
*/
|
|
function runSyncCommand(&$command) {
|
|
global $registry;
|
|
$history = $GLOBALS['egw']->contenthistory;
|
|
$state = &$_SESSION['SyncML.state'];
|
|
|
|
|
|
$type = $this->_targetLocURI;
|
|
|
|
$syncml_prefs = $GLOBALS['egw_info']['user']['preferences']['syncml'];
|
|
if (isset($syncml_prefs[$type])) {
|
|
$sync_conflicts = $syncml_prefs[$type];
|
|
} else {
|
|
$sync_conflicts = CONFLICT_SERVER_WINNING;
|
|
}
|
|
|
|
$state->setLocName($this->_locName);
|
|
$locName = $state->getLocName();
|
|
$sourceURI = $state->getSourceURI();
|
|
$hordeType = $state->getHordeType($type);
|
|
$serverAnchorLast = $state->getServerAnchorLast($type);
|
|
$changes = array();
|
|
foreach($state->getChangedItems($type) as $change) {
|
|
// now we have to remove the ones
|
|
// that came from the last sync with this client
|
|
$guid_ts = $state->getSyncTSforAction($change, 'modify');
|
|
$sync_ts = $state->getChangeTS($type, $change);
|
|
if ($sync_ts && $sync_ts == $guid_ts) {
|
|
// Change was done by us upon request of client.
|
|
Horde::logMessage("SyncML: change: $change ignored, " .
|
|
"came from client", __FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
continue;
|
|
}
|
|
$changes[] = $change;
|
|
}
|
|
|
|
Horde::logMessage('SyncML: runSyncCommand found ' . count($changes) .
|
|
" possible conflicts for $type", __FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
|
|
if(!$contentType = $command->getContentType()) {
|
|
$contentType = $state->getPreferedContentType($type);
|
|
}
|
|
|
|
if (($contentType == 'text/x-vcalendar' || $contentType == 'text/calendar')
|
|
&& strpos($command->getContent(), 'BEGIN:VTODO') !== false) {
|
|
$hordeType = 'tasks';
|
|
}
|
|
|
|
$syncElementItems = $command->getSyncElementItems();
|
|
|
|
foreach($syncElementItems as $syncItem) {
|
|
|
|
$guid = false;
|
|
$locURI = $syncItem->getLocURI();
|
|
|
|
if (is_a($command, 'Horde_SyncML_Command_Sync_Add')) {
|
|
if ($sync_conflicts > CONFLICT_RESOLVED_WITH_DUPLICATE) {
|
|
// We enforce the client not to change anything
|
|
if ($sync_conflicts > CONFLICT_CLIENT_CHANGES_IGNORED) {
|
|
// delete this item from client
|
|
Horde::logMessage("SyncML: Server RO! REMOVE $locURI from client",
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$state->addConflictItem($type, $locURI);
|
|
} else {
|
|
Horde::logMessage('SyncML: Server RO! '
|
|
. 'REJECT all client changes',
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$state->log('Client-AddReplaceIgnored');
|
|
}
|
|
continue;
|
|
}
|
|
|
|
$guid = $registry->call($hordeType . '/import',
|
|
array($state->convertClient2Server($syncItem->getContent(), $contentType), $contentType));
|
|
|
|
if (!is_a($guid, 'PEAR_Error') && $guid != false) {
|
|
$ts = $state->getSyncTSforAction($guid, 'add');
|
|
$state->setUID($type, $locURI, $guid, $ts);
|
|
$state->log('Client-Add');
|
|
Horde::logMessage("SyncML: added client entry $locURI as $guid",
|
|
__FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
} else {
|
|
$state->log('Client-AddFailure');
|
|
Horde::logMessage('SyncML: Error in adding client entry ' . $locURI . ': '
|
|
. $guid->message, __FILE__, __LINE__, PEAR_LOG_ERR);
|
|
}
|
|
} elseif (is_a($command, 'Horde_SyncML_Command_Sync_Delete')) {
|
|
$guid = $state->removeUID($type, $locURI);
|
|
if (!$guid) {
|
|
// the entry is no longer on the server
|
|
$state->log('Client-DeleteFailure');
|
|
Horde::logMessage('SyncML: Failure deleting client entry ' . $locURI
|
|
. ', gone on server already!',
|
|
__FILE__, __LINE__, PEAR_LOG_ERR);
|
|
continue;
|
|
}
|
|
if ($sync_conflicts > CONFLICT_RESOLVED_WITH_DUPLICATE) {
|
|
// We enforce the client not to change anything
|
|
if ($sync_conflicts > CONFLICT_CLIENT_CHANGES_IGNORED) {
|
|
Horde::logMessage('SyncML: Server RO! ADD '
|
|
. $guid . ' to client again',
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$state->pushAddedItem($type, $guid);
|
|
} else {
|
|
Horde::logMessage('SyncML: '.
|
|
'Server RO! REJECT all client changes',
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
}
|
|
$state->log('Client-DeleteIgnored');
|
|
continue;
|
|
}
|
|
elseif ($sync_conflicts == CONFLICT_RESOLVED_WITH_DUPLICATE &&
|
|
in_array($guid, $changes))
|
|
{
|
|
Horde::logMessage('SyncML: '.
|
|
'Server has updated version to keep',
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$state->log('Client-DeleteIgnored');
|
|
continue;
|
|
}
|
|
elseif ($sync_conflicts == CONFLICT_MERGE_DATA)
|
|
{
|
|
Horde::logMessage('SyncML: Server Merge Only: ADD '
|
|
. $guid . ' to client again',
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$state->pushAddedItem($type, $guid);
|
|
$state->log('Client-DeleteIgnored');
|
|
continue;
|
|
}
|
|
|
|
Horde::logMessage('SyncML: about to delete entry '
|
|
. $type .' / '. $guid . ' due to client request '
|
|
. $syncItem->getLocURI(), __FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
|
|
if (!is_a($guid, 'PEAR_Error') && $guid != false) {
|
|
$registry->call($hordeType . '/delete', array($guid));
|
|
$ts = $state->getSyncTSforAction($guid, 'delete');
|
|
$state->setUID($type, $locURI, $guid, $ts, 1);
|
|
$state->log('Client-Delete');
|
|
Horde::logMessage('SyncML: deleted entry '
|
|
. $guid . ' due to client request',
|
|
__FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
} else {
|
|
$state->log('Client-DeleteFailure');
|
|
Horde::logMessage('SyncML: Failure deleting client entry ' . $locURI
|
|
. ', maybe gone on server already: '
|
|
. $guid->message, __FILE__, __LINE__, PEAR_LOG_ERR);
|
|
}
|
|
} elseif (is_a($command, 'Horde_SyncML_Command_Sync_Replace')) {
|
|
$guid = $state->getGlobalUID($type, $locURI);
|
|
$replace = true;
|
|
$ok = false;
|
|
$merge = false;
|
|
if ($hordeType == 'configuration')
|
|
{
|
|
$command->setStatus(RESPONSE_ALREADY_EXISITS);
|
|
}
|
|
if ($guid)
|
|
{
|
|
Horde::logMessage('SyncML: locuri '. $locURI . ' guid ' . $guid , __FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
if (($sync_conflicts > CONFLICT_RESOLVED_WITH_DUPLICATE) || in_array($guid, $changes))
|
|
{
|
|
Horde::logMessage('SyncML: CONFLICT for locuri ' . $locURI . ' guid ' . $guid , __FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
switch ($sync_conflicts)
|
|
{
|
|
case CONFLICT_CLIENT_WINNING:
|
|
$command->setStatus(RESPONSE_CONFLICT_RESOLVED_WITH_CLIENT_WINS);
|
|
break;
|
|
case CONFLICT_SERVER_WINNING:
|
|
Horde::logMessage('SyncML: REJECT client change for locuri ' .
|
|
$locURI . ' guid ' . $guid ,
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$command->setStatus(RESPONSE_CONFLICT_RESOLVED_WITH_SERVER_WINS);
|
|
$ok = true;
|
|
$replace = false;
|
|
$state->log('Client-AddReplaceIgnored');
|
|
break;
|
|
case CONFLICT_MERGE_DATA:
|
|
Horde::logMessage('SyncML: Merge server and client data for locuri ' .
|
|
$locURI . ' guid ' . $guid ,
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$command->setStatus(RESPONSE_CONFLICT_RESOLVED_WITH_MERGE);
|
|
$merge = true;
|
|
break;
|
|
case CONFLICT_RESOLVED_WITH_DUPLICATE:
|
|
$replace = false;
|
|
break;
|
|
case CONFLICT_CLIENT_CHANGES_IGNORED:
|
|
Horde::logMessage('SyncML: Server RO! REJECT client change for locuri ' .
|
|
$locURI . ' guid ' . $guid ,
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$command->setStatus(RESPONSE_PERMISSION_DENIED);
|
|
$ok = true;
|
|
$replace = false;
|
|
$ts = $state->getSyncTSforAction($guid, 'modify');
|
|
$state->setUID($type, $locURI, $guid, $ts);
|
|
$state->log('Client-AddReplaceIgnored');
|
|
break;
|
|
default: // We enforce our data on client
|
|
Horde::logMessage('SyncML: Server RO! UNDO client change for locuri ' .
|
|
$locURI . ' guid ' . $guid ,
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$command->setStatus(RESPONSE_PERMISSION_DENIED);
|
|
$state->pushChangedItem($type, $guid);
|
|
$ok = true;
|
|
$replace = false;
|
|
}
|
|
}
|
|
elseif ($sync_conflicts == CONFLICT_MERGE_DATA)
|
|
{
|
|
Horde::logMessage('SyncML: Merge server and client data for locuri ' .
|
|
$locURI . ' guid ' . $guid ,
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$merge = true;
|
|
}
|
|
|
|
if ($replace)
|
|
{
|
|
// Entry exists: replace/merge with current one.
|
|
$ok = $registry->call($hordeType . '/replace',
|
|
array($guid, $state->convertClient2Server($syncItem->getContent(),
|
|
$contentType), $contentType, $type, $merge));
|
|
if (!is_a($ok, 'PEAR_Error') && $ok != false)
|
|
{
|
|
$ts = $state->getSyncTSforAction($guid, 'modify');
|
|
$state->setUID($type, $locURI, $guid, $ts);
|
|
if ($merge)
|
|
{
|
|
Horde::logMessage('SyncML: Merged entry ' . $locURI . ' due to client request guid: ' .
|
|
$guid . ' ts: ' . $ts, __FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
}
|
|
else
|
|
{
|
|
Horde::logMessage('SyncML: replaced entry ' . $locURI . ' due to client request guid: ' .
|
|
$guid . ' ts: ' . $ts, __FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
}
|
|
$state->log('Client-Replace');
|
|
$ok = true;
|
|
}
|
|
else
|
|
{
|
|
// Entry may have been deleted; try adding it.
|
|
$ok = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$ok) {
|
|
// Entry does either not exist in map or database, or should be added due to a conflict
|
|
|
|
if ($sync_conflicts > CONFLICT_RESOLVED_WITH_DUPLICATE) {
|
|
// We enforce the client not to change anything
|
|
if ($sync_conflicts > CONFLICT_CLIENT_CHANGES_IGNORED) {
|
|
// delete this item from client
|
|
Horde::logMessage('SyncML: Server RO! REMOVE ' . $locURI . ' from client',
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
$state->addConflictItem($type, $syncItem->getLocURI());
|
|
} else {
|
|
Horde::logMessage('SyncML: Server RO! REJECT all client changes',
|
|
__FILE__, __LINE__, PEAR_LOG_WARNING);
|
|
}
|
|
continue;
|
|
}
|
|
Horde::logMessage('SyncML: try to add contentype '
|
|
. $contentType . ' for locuri ' . $locURI,
|
|
__FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
//continue;
|
|
$oguid = $guid;
|
|
$guid = $registry->call($hordeType . '/import',
|
|
array($state->convertClient2Server($syncItem->getContent(), $contentType), $contentType, $guid));
|
|
if (!is_a($guid, 'PEAR_Error')) {
|
|
if ($oguid != $guid) {
|
|
// We add a new entry
|
|
$ts = $state->getSyncTSforAction($guid, 'add');
|
|
Horde::logMessage('SyncML: added entry ' . $locURI . ' guid '
|
|
. $guid . ' from client',
|
|
__FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
$state->log('Client-Add');
|
|
} else {
|
|
// We replaced an entry
|
|
$ts = $state->getSyncTSforAction($guid, 'modify');
|
|
Horde::logMessage('SyncML: replaced entry ' . $locURI . ' guid '
|
|
. $guid . ' from client',
|
|
__FILE__, __LINE__, PEAR_LOG_DEBUG);
|
|
$state->log('Client-Replace');
|
|
}
|
|
$state->setUID($type, $locURI, $guid, $ts);
|
|
} else {
|
|
Horde::logMessage('SyncML: Error in replacing/'
|
|
. 'add client entry ' . $locURI . ' : ' . $guid->message,
|
|
__FILE__, __LINE__, PEAR_LOG_ERR);
|
|
$state->log('Client-AddFailure');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|