* * *

diff -r b614f6a2eb9f framework/SyncML/SyncML/Backend.php
--- a/framework/SyncML/SyncML/Backend.php	Wed May 07 12:12:00 2008 +0200
+++ b/framework/SyncML/SyncML/Backend.php	Wed May 07 12:41:58 2008 +0200
@@ -574,6 +574,24 @@ class SyncML_Backend {
     }
 
     /**
+     * Checks if the entry specified by $cuid has been modified on the server.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like
+     *                             calendar, tasks, contacts or notes.
+     *                             May include optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string  $cuid        Client ID of this entry
+     * @param integer $from_ts     Start timestamp.
+     *
+     * @return boolean   True if the client entry has been modified on
+     *                   the server. False otherwise.
+     */
+    function unchanged($databaseURI, $cuid, $from_ts)
+    {
+        die ("Not implemented!");
+    }
+
+    /**
      * Authenticates the user at the backend.
      *
      * For some types of authentications (notably auth:basic) the username
diff -r b614f6a2eb9f framework/SyncML/SyncML/Backend/Horde.php
--- a/framework/SyncML/SyncML/Backend/Horde.php	Wed May 07 12:12:00 2008 +0200
+++ b/framework/SyncML/SyncML/Backend/Horde.php	Wed May 07 12:41:58 2008 +0200
@@ -493,6 +493,71 @@ class SyncML_Backend_Horde extends SyncM
             $this->createUidMap($database, $cuid, $suid, $ts);
         }
 
+        return true;
+    }
+
+    /**
+     * Checks if the entry specified by $cuid has been modified on the server.
+     *
+     * @param string $databaseURI  URI of Database to sync. Like
+     *                             calendar, tasks, contacts or notes.
+     *                             May include optional parameters:
+     *                             tasks?options=ignorecompleted.
+     * @param string $cuid         Client ID of this entry
+     * @param integer $from_ts     Last server sync time.
+     *
+     * @return boolean   True if the client entry has been modified on
+     *                   the server. False otherwise.
+     */
+    function getConflict($databaseURI, $cuid, $server_ts)
+    {
+        global $registry;
+
+        $database = $this->_normalize($databaseURI);
+
+        // Only server needs to do a cuid<->suid map
+        if ($this->_backendMode == SYNCML_BACKENDMODE_SERVER) {
+            $suid = $this->_getSuid($database, $cuid);
+        } else {
+            $suid = $cuid;
+        }
+        $this->logMessage("checking modification date of entry suid $suid in $database", __FILE__, __LINE__, PEAR_LOG_DEBUG);
+
+        if ($suid) {
+            $mod_ts = $registry->call($database . '/getActionTimestamp', array($suid, 'modify', SyncML_Backend::getParameter($databaseURI,'source')));
+            $client_ts = $this->_getChangeTS($database, $suid);
+            // Check that the last server anchor is smaller than the
+            // last modification stamp (this means that a change
+            // occurred after the last sync started). This might still
+            // be a client change so check that the last client update
+            // also happened before the last modification)
+            if ($server_ts < $mod_ts && $client_ts < $mod_ts) {
+                // Duplicate the server entry
+                $this->logMessage("Conflict detected. Returning conflicting server entry $suid.", __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                $device = &$_SESSION['SyncML.state']->getDevice();
+                $contentType = $device->getPreferredContentType($databaseURI);
+                return $this->retrieveEntry($databaseURI, $suid, $contentType);
+            }
+        } 
+        return false;
+    }
+
+    function duplicateConflict($databaseURI, $content)
+    {
+        global $registry;
+
+        $database = $this->_normalize($databaseURI);
+        $device = &$_SESSION['SyncML.state']->getDevice();
+        $contentType = $device->getPreferredContentType($databaseURI);
+        $content = preg_replace('/(\r\n|\r|\n)UID:.*?(\r\n|\r|\n)/', '\1', $content, 1);
+        $duid = $registry->call($database. '/import',
+                                array($content, $contentType, 
+                                      SyncML_Backend::getParameter($databaseURI,'source')));
+        if (is_a($duid, 'PEAR_Error')) {
+            $this->logMessage("duplicating entry failed.", __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            return false;
+        }
+        $this->logMessage("duplicated server entry to $duid.", __FILE__, __LINE__, PEAR_LOG_DEBUG);
         return true;
     }
 
diff -r b614f6a2eb9f framework/SyncML/SyncML/Constants.php
--- a/framework/SyncML/SyncML/Constants.php	Wed May 07 12:12:00 2008 +0200
+++ b/framework/SyncML/SyncML/Constants.php	Wed May 07 12:41:58 2008 +0200
@@ -64,7 +64,7 @@ define('RESPONSE_PARTIAL_CONTENT', 206);
 define('RESPONSE_PARTIAL_CONTENT', 206);
 define('RESPONSE_CONFLICT_RESOLVED_WITH_MERGE', 207);
 define('RESPONSE_CONFLICT_RESOLVED_WITH_CLIENT_WINNING', 208);
-define('RESPONSE_CONFILCT_RESOLVED_WITH_DUPLICATE', 209);
+define('RESPONSE_CONFLICT_RESOLVED_WITH_DUPLICATE', 209);
 define('RESPONSE_DELETE_WITHOUT_ARCHIVE', 210);
 define('RESPONSE_ITEM_NO_DELETED', 211);
 define('RESPONSE_AUTHENTICATION_ACCEPTED', 212);
diff -r b614f6a2eb9f framework/SyncML/SyncML/Sync.php
--- a/framework/SyncML/SyncML/Sync.php	Wed May 07 12:12:00 2008 +0200
+++ b/framework/SyncML/SyncML/Sync.php	Wed May 07 12:41:58 2008 +0200
@@ -214,7 +214,13 @@ class SyncML_Sync {
             }
         } elseif ($item->elementType =='Delete') {
             /* Handle client delete requests. */
+            $duplicate = $backend->getConflict($hordedatabase, $cuid, $this->_serverAnchorLast);
             $ok = $backend->deleteEntry($database, $cuid);
+            $duplicated = false;
+            if ($duplicate) {
+                $duplicated = $backend->duplicateConflict($hordedatabase, $duplicate);
+            }
+
             if (!$ok && $tasksincalendar) {
                 $backend->logMessage(
                     'Task ' . $cuid . ' deletion sent with calendar request',
@@ -224,8 +230,14 @@ class SyncML_Sync {
 
             if ($ok) {
                 $this->_client_delete_count++;
-                $item->responseCode = RESPONSE_OK;
                 $backend->logMessage('Deleted entry ' . $suid . ' due to client request', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                if (!$duplicated) {
+                    $item->responseCode = RESPONSE_OK;
+                } else {
+                    $this->_client_add_count++;
+                    $backend->logMessage('Duplicated entry ' . $suid . ' due to client/server conflict', __FILE__, __LINE__, PEAR_LOG_INFO);
+                    $item->responseCode = RESPONSE_CONFLICT_RESOLVED_WITH_DUPLICATE;
+                }
             } else {
                 $this->_errors++;
                 $item->responseCode = RESPONSE_ITEM_NO_DELETED;
@@ -234,13 +246,24 @@ class SyncML_Sync {
 
         } elseif ($item->elementType == 'Replace') {
             /* Handle client replace requests. */
+            $duplicate = $backend->getConflict($hordedatabase, $cuid, $this->_serverAnchorLast);
             $suid = $backend->replaceEntry($hordedatabase, $content,
                                            $contentType, $cuid);
+            $duplicated = false;
+            if ($duplicate) {
+                $duplicated = $backend->duplicateConflict($hordedatabase, $duplicate);
+            }
 
             if (!is_a($suid, 'PEAR_Error')) {
                 $this->_client_replace_count++;
-                $item->responseCode = RESPONSE_OK;
                 $backend->logMessage('Replaced entry ' . $suid . ' due to client request', __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                if (!$duplicated) {
+                    $item->responseCode = RESPONSE_OK;
+                } else {
+                    $this->_client_add_count++;
+                    $backend->logMessage('Duplicated entry ' . $suid . ' due to client/server conflict', __FILE__, __LINE__, PEAR_LOG_INFO);
+                    $item->responseCode = RESPONSE_CONFLICT_RESOLVED_WITH_DUPLICATE;
+                }
             } else {
                 $backend->logMessage($suid->message, __FILE__, __LINE__, PEAR_LOG_DEBUG);