[sync] Turba and Syncml patch proposal

Karsten Fourmont fourmont at gmx.de
Sun Jan 2 10:04:26 PST 2005


Hi,

I adjusted the SyncML to reflect the new style "plain" UIDs (i.e. no 
longer kronolith:...)

I ran into a few bumps:

1) turba has the already discussed "which source should I use" problem. 
I added an additional optional $source='' parameter to 
_turba_{import|delete|replace|export}. If $source is not specified 
$conf['client']['addressbook'] is used. This works fine for SyncML. The 
attached patch replaces my previous suggestion.

2) the _import functions now return a plain UID. However the history 
functions still require a "extended" guid like "memo:karsten:xxxx". This 
is a bit of a fracture: most API functions deal with plain UIDs, but 
when calling the /listBy Api function, I get an "extended" guid (as it 
comes from the datatree). This is an issue for SyncML: after adding a 
new entry to the Horde Database, Syncml needs to get the log timestamp 
for that action. We need that as we have to manually remove these 
entries from /listBy results to avoid duplication by echoing back new 
entries to the client. However /import now returns only a plain guid and 
I can't feed this to the history object (for timestamp retrival) unless 
I add a prefix like 'turba:karsten:'. But this would be a bad solution: 
SyncML deals with the external API and only knows 'contacts', not 'turba'.

So what I did is this: History gets a getLastLoggedTS() functions that 
returns the timestamp of the last logged action. This value is stored in 
a global variable _lastLoggedTS and updated by 
DataTreeObject_History::log. See attached patch for History.php. 
Initially I tried to made _lastLoggedTS a member var of History, but for 
some reason even though History is a singleton, two instances are 
created (by the singleton function itself!) during a SyncML-run. Don't 
know why.

Attached is a patch for History.php and the appropriate changes to 
SyncML. I also included a patch for turba's api (adding the source 
parameter, added missing functionality). Is it OK to commit this or is 
my history modification too bad a hack?

Anyhow, IMHO the clean solution in the long run would be to expose the 
history through the external api:

1) somehow provide the timestamp of add/replace/delete operations. My 
suggestion: _import, _delete, _replace etc. should return an hash with 
lots of useful information (uid, timestamp, source for turba) instead of 
just the uid string.

2) the listBy API-Calls should provide similar information as well. 
Instead a simple array of GUIDs they should return an array
    of hashes: key is the uid, values are ts,action,user,[source].

As this would be a significant change to the external api I'd like to 
hear your opinions about it before going ahead. Any thoughts or objections?

Doing this right may take some time, so the applied patch should do for 
the moment.

Cheers
  Karsten


-------------- next part --------------
Index: framework/History/History.php
===================================================================
RCS file: /repository/framework/History/History.php,v
retrieving revision 1.28
diff -u -r1.28 History.php
--- framework/History/History.php	7 Dec 2004 16:09:13 -0000	1.28
+++ framework/History/History.php	2 Jan 2005 17:53:46 -0000
@@ -308,6 +308,17 @@
     }
 
     /**
+     * Timestamp for last logged action. 
+     * Helps  retrieve the TS after an update/modify/delete-operation
+     * @return int  Timestamp of last logging action
+     */
+    
+    function getLastLoggedTS() {
+        global $_lastLoggedTS;
+        return $_lastLoggedTS;
+    }
+    
+    /**
      * Returns a new history entry object.
      *
      * @access private
@@ -440,6 +451,10 @@
             $attributes['ts'] = time();
         }
 
+        /* save ts in History Object: */
+        global $_lastLoggedTS;
+        $_lastLoggedTS = $attributes['ts'];
+        
         /* If we want to replace an entry with the same action, try and find
          * one. Track whether or not we succeed in $done, so we know whether
          * or not to add the entry later. */
Index: framework/SyncML/SyncML/Sync.php
===================================================================
RCS file: /repository/framework/SyncML/SyncML/Sync.php,v
retrieving revision 1.8
diff -u -r1.8 Sync.php
--- framework/SyncML/SyncML/Sync.php	30 Nov 2004 19:59:03 -0000	1.8
+++ framework/SyncML/SyncML/Sync.php	2 Jan 2005 17:53:48 -0000
@@ -103,7 +103,7 @@
             $guid = $registry->call($hordeType . '/import',
                                     array($state->convertClient2Server($command->getContent(), $contentType), $contentType));
             if (!is_a($guid, 'PEAR_Error')) {
-                $ts = $history->getTSforAction($guid, 'add');
+                $ts = $history->getLastLoggedTS();
                 if(!$ts) {
                     Horde::logMessage('SyncML: unable to find add-ts for ' . $guid . ' at ' . $ts, __FILE__, __LINE__, PEAR_LOG_ERROR);
                 }
@@ -122,7 +122,7 @@
 
             if (!is_a($guid, 'PEAR_Error')) {
                 $registry->call($hordeType . '/delete', array($guid));
-                $ts = $history->getTSforAction($guid, 'delete');
+                $ts = $history->getLastLoggedTS();
                 $state->setUID($type, $command->getLocURI(), $guid, $ts);
                 $state->log("Client-Delete");
                 Horde::logMessage('SyncML: deleted entry ' . $guid . ' due to client request', __FILE__, __LINE__, PEAR_LOG_DEBUG);
@@ -138,7 +138,7 @@
                 $ok = $registry->call($hordeType . '/replace',
                                       array($guid, $state->convertClient2Server($command->getContent(), $contentType), $contentType));
                 if (!is_a($ok, 'PEAR_Error')) {
-                    $ts = $history->getTSforAction($guid, 'modify');
+                    $ts = $history->getLastLoggedTS();
                     $state->setUID($type, $command->getLocURI(), $guid, $ts);
                     Horde::logMessage('SyncML: replaced entry ' . $guid . ' due to client request', __FILE__, __LINE__, PEAR_LOG_DEBUG);
                     $state->log("Client-Replace");
@@ -155,7 +155,7 @@
                 $guid = $registry->call($hordeType . '/import',
                                         array($state->convertClient2Server($command->getContent(), $contentType), $contentType));
                 if (!is_a($guid, 'PEAR_Error')) {
-                    $ts = $history->getTSforAction($guid, 'add');
+                    $ts = $history->getLastLoggedTS();
                     $state->setUID($type, $command->getLocURI(), $guid, $ts);
                     $state->log("Client-AddReplace");
                     Horde::logMessage('SyncML: r/ added client entry as ' . $guid, __FILE__, __LINE__, PEAR_LOG_DEBUG);
Index: framework/SyncML/SyncML/Sync/TwoWaySync.php
===================================================================
RCS file: /repository/framework/SyncML/SyncML/Sync/TwoWaySync.php,v
retrieving revision 1.13
diff -u -r1.13 TwoWaySync.php
--- framework/SyncML/SyncML/Sync/TwoWaySync.php	30 Nov 2004 19:59:03 -0000	1.13
+++ framework/SyncML/SyncML/Sync/TwoWaySync.php	2 Jan 2005 17:53:48 -0000
@@ -20,6 +20,14 @@
  */
 class Horde_SyncML_Sync_TwoWaySync extends Horde_SyncML_Sync {
 
+    /**
+     * remove leading source modifier to deal with
+     * old style guids and guids as returned from history calls
+     */
+    function cleanguid($guid) {
+        return preg_replace('/^.*:/','',$guid);
+    }
+
     function endSync($currentCmdID, &$output)
     {
         global $registry;
@@ -56,6 +64,7 @@
         $changes = $registry->call($hordeType. '/listBy', array('action' => 'modify', 'timestamp' => $refts));
         foreach ($changes as $guid) {
             $guid_ts = $history->getTSforAction($guid, 'modify');
+            $guid = $this->cleanguid($guid);
             $sync_ts = $state->getChangeTS($syncType, $guid);
             if ($sync_ts && $sync_ts == $guid_ts) {
                 // Change was done by us upon request of client.
@@ -91,6 +100,7 @@
         $deletes = $registry->call($hordeType. '/listBy', array('action' => 'delete', 'timestamp' => $refts));
         foreach ($deletes as $guid) {
             $guid_ts = $history->getTSforAction($guid, 'delete');
+            $guid = $this->cleanguid($guid);
             $sync_ts = $state->getChangeTS($syncType, $guid);
             if ($sync_ts && $sync_ts == $guid_ts) {
                 // Change was done by us upon request of client.
@@ -116,9 +126,11 @@
         // Get adds.
         $adds = $registry->call($hordeType. '/listBy', array('action' => 'add', 'timestamp' => $refts));
         foreach ($adds as $guid) {
+
             $guid_ts = $history->getTSforAction($guid, 'add');
+            $guid = $this->cleanguid($guid);
             $sync_ts = $state->getChangeTS($syncType, $guid);
-Horde::logMessage("SyncML: add: guid_ts: $guid_ts sync_ts:$sync_ts", __FILE__, __LINE__, PEAR_LOG_DEBUG);
+            Horde::logMessage("SyncML: add: guid_ts: $guid_ts sync_ts:$sync_ts", __FILE__, __LINE__, PEAR_LOG_DEBUG);
 
             if ($sync_ts && $sync_ts == $guid_ts) {
                 // Change was done by us upon request of client.
Index: turba/lib/api.php
===================================================================
RCS file: /repository/turba/lib/api.php,v
retrieving revision 1.120
diff -u -r1.120 api.php
--- turba/lib/api.php	13 Dec 2004 23:34:27 -0000	1.120
+++ turba/lib/api.php	2 Jan 2005 17:54:01 -0000
@@ -50,17 +50,17 @@
 );
 
 $_services['export'] = array(
-    'args' => array('uid', 'contentType'),
+    'args' => array('uid', 'contentType', 'source'),
     'type' => 'string',
 );
 
 $_services['delete'] = array(
-    'args' => array('uid'),
+    'args' => array('uid', 'source'),
     'type' => 'boolean',
 );
 
 $_services['replace'] = array(
-    'args' => array('uid', 'content', 'contentType'),
+    'args' => array('uid', 'content', 'contentType', 'source'),
     'type' => 'boolean',
 );
 
@@ -309,8 +309,18 @@
 function _turba_import($content, $contentType = 'array', $source = '')
 {
     require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
+    global $cfgSources,$conf;
+
+    // get default address book from config
+    if(empty($source)) {
+        $source = $conf['client']['addressbook'];
+    }
 
+    // check if source is writeable
+    if (!empty($source) && !empty($cfgSources[$source]['readonly'])) {
+        $source = false;
+    }
+    
     /* Check existance of and permissions on the specified source. */
     if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources) || !isset($cfgSources[$source])) {
         return PEAR::raiseError(_("Invalid address book"), 'horde.warning');
@@ -378,7 +388,7 @@
     }
 
     $object = &$driver->getObject($result);
-    return $object->getValue('__key');
+    return $object->getValue('__uid');
 }
 
 /**
@@ -391,10 +401,10 @@
  *
  * @return string  The requested data.
  */
-function _turba_export($uid, $contentType)
+function _turba_export($uid, $contentType, $source = '')
 {
     require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
+    global $cfgSources,$conf;
 
     if (is_array($contentType)) {
         $options = $contentType;
@@ -404,18 +414,16 @@
         $options = array();
     }
 
-    // Need to reference some sort of mapping here to turn the UID
-    // into a source id and contact id:
-    //
-    // $source = $uid['addressbook'];
-    // $objectId = $uid['contactId'];
-    return PEAR::raiseError('Turba needs a UID map');
+    // get default address book from config
+    if(empty($source)) {
+        $source = $conf['client']['addressbook'];
+    }
 
     if (empty($source) || !isset($cfgSources[$source])) {
         return PEAR::raiseError(_("Invalid address book"), 'horde.error', null, null, $source);
     }
 
-    if (empty($objectId)) {
+    if (empty($uid)) {
         return PEAR::raiseError(_("Invalid ID"), 'horde.error', null, null, $source);
     }
 
@@ -429,7 +437,7 @@
     // true. Otherwise, try to delete it and return success or
     // failure.
     $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
-    $result = $driver->search(array('__key' => $objectId));
+    $result = $driver->search(array('__uid' => $uid));
     if (is_a($result, 'PEAR_Error')) {
         return $result;
     } elseif ($result->count() == 0) {
@@ -475,13 +483,13 @@
  *
  * @return boolean  Success or failure.
  */
-function _turba_delete($uid)
+function _turba_delete($uid, $source='')
 {
     // Handle an arrray of UIDs for convenience of deleting multiple
     // contacts at once.
     if (is_array($uid)) {
         foreach ($uid as $g) {
-            $result = _turba_delete($uid);
+            $result = _turba_delete($uid, $source);
             if (is_a($result, 'PEAR_Error')) {
                 return $result;
             }
@@ -491,20 +499,18 @@
     }
 
     require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
+    global $cfgSources,$conf;
 
-    // Need to reference some sort of mapping here to turn the UID
-    // into a source id and contact id:
-    //
-    // $source = $uid['addressbook'];
-    // $objectId = $uid['contactId'];
-    return PEAR::raiseError('Turba needs a UID map');
+    // get default address book from config
+    if(empty($source)) {
+        $source = $conf['client']['addressbook'];
+    }
 
     if (empty($source) || !isset($cfgSources[$source])) {
         return PEAR::raiseError(_("Invalid address book"), 'horde.error', null, null, $source);
     }
 
-    if (empty($objectId)) {
+    if (empty($uid)) {
         return PEAR::raiseError(_("Invalid ID"), 'horde.error', null, null, $source);
     }
 
@@ -518,13 +524,16 @@
     // true. Otherwise, try to delete it and return success or
     // failure.
     $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
-    $result = $driver->search(array('__key' => $objectId));
+    $result = $driver->search(array('__uid' => $uid));
     if (is_a($result, 'PEAR_Error')) {
         return $result;
     } elseif ($result->count() == 0) {
         return true;
     } else {
-        return $driver->delete($objectId);
+        // we need to delete by uid rather than by key. So
+        // use _delete instead of delete
+        $result = $driver->_delete($driver->toDriver('__uid'), $uid);
+        return $result;
     }
 }
 
@@ -540,38 +549,57 @@
  *
  * @return boolean  Success or failure.
  */
-function _turba_replace($uid, $content, $contentType)
+function _turba_replace($uid, $content, $contentType, $source = '')
 {
     require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
+    global $cfgSources,$conf;
 
-    // Need to reference some sort of mapping here to turn the UID
-    // into a source id and contact id:
-    //
-    // $source = $uid['addressbook'];
-    // $objectId = $uid['contactId'];
-    return PEAR::raiseError('Turba needs a UID map');
+    // get default address book from config
+    if(empty($source)) {
+        $source = $conf['client']['addressbook'];
+    }
 
     if (!isset($cfgSources) || !is_array($cfgSources) || !count($cfgSources) || !isset($cfgSources[$source])) {
         return PEAR::raiseError(_("Invalid address book"), 'horde.warning');
     }
 
-    if (empty($objectId)) {
+    if (empty($uid)) {
         return PEAR::raiseError(_("Invalid objectId"), 'horde.error', null, null, $source);
     }
 
     /* Check permissions. */
     $driver = &Turba_Driver::singleton($source, $cfgSources[$source]);
-    $object = $driver->getObject($objectId);
-    if (is_a($object, 'PEAR_Error')) {
-        return $object;
-    } elseif (!Turba::hasPermission($object, 'object', PERMS_EDIT)) {
-        return PEAR::raiseError(_("Permission Denied"), 'horde.warning');
+    $result = $driver->search(array('__uid' => $uid));
+    if (is_a($result, 'PEAR_Error')) {
+        return $result;
+    } elseif ($result->count() == 0) {
+        return PEAR::raiseError(_("Object not found"), 'horde.error', null, null, $source);
+        return true;
+    } elseif ($result->count() > 1) {
+        return PEAR::raiseError("Internal Horde Error: multiple turba objects with same objectId.", 'horde.error', null, null, $source);
     }
+    $object = $result->objects[0];
 
     switch ($contentType) {
     case 'array':
         break;
+    case 'text/x-vcard':
+        require_once 'Horde/iCalendar.php';
+        $iCal = &new Horde_iCalendar();
+        if (!$iCal->parsevCalendar($content)) {
+            return PEAR::raiseError(_("There was an error importing the iCalendar data."));
+        }
+        switch ($iCal->getComponentCount()) {
+        case 0:
+            return PEAR::raiseError(_("No vCard data was found."));
+        case 1:
+            $content = $iCal->getComponent(0);
+            $content = $driver->toHash($content);
+            break;
+        default:
+            return PEAR::raiseError(_("Only one vcard supported."));
+        }
+        break;
 
     default:
         return PEAR::raiseError(_("Invalid Content-Type"));


More information about the sync mailing list