[sync] Turba and Syncml patch proposal

Karsten Fourmont fourmont at gmx.de
Sun Jan 23 11:50:18 PST 2005


Hi,

skimming Germany's biggest computer magazine this weekend, I was 
(pleasently) surprised to see a nice Horde screenshot in there. It 
contained the todo list of some guy called "Chuck Hagenbuch". :-))) And 
it had "syncing" as a priority 2 entry. Reminded like this, I feel very 
much motivated to continue work a bit. ;-)

Attached's a new patch proposal based on Chuck's suggestions. Here's an 
outline of the changes:

1) the api for nag, mnemo, kronolith, and turba now has a 
getTSforAction($uid, $action)

 > How about just getHistory($uid), for getting the history of that
 > object?

imho the (current) history object itself is too much an internal 
structure and should not be exposed by the external api. For sql 
backends it contains the conncection user and password.

For the moment I stick with getTSforAction. We can extend/generalize it 
when the need arises.

2) listBy in the api of nag, mnemo, kronolith, and turba now returns the 
plain uids rather than s.th. like turba:karsten:uid
This is done by
    return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));

3) as a result the sync code has slightly changed and is now only 
dependant on the horde external api: it no longer directly calls 
history. Much cleaner!

4) the turba api functions allow specifying a source (i.e. addressbook).
If no such source is specified, the pref 'default_dir' is used:
     if (empty($source)) {
         $source = $prefs->getValue('default_dir');
     }
5) some more stuff to get the turba api actual working. Used to be in my 
previous patch proposals already.

Let me know what you think. If there are no major objections I'll go 
ahead and commit this. Once this is done we can start looking at the 
problems with various clients again.

Two notes for anyboding applying this patch:
1) turba address book syncing uses the default address book specified in 
turba/options/Searching Options: "Default directory for your personal 
addressbook". So this has to be set to an address book writeable by you.
2) syncing only works for entries created with "new style" uids. These 
are used for roughly 3 months now. Anything created before may work or 
not. Sometimes the 2004-10-26_create_default_histories.php script helps, 
but maybe not in all cases.

  Cheers
    Karsten

P.S.
For those interested: the magazine is c't 3/2005, page 46.

P.P.S.
Sorry to everyone for the long response times. But atm I'm working 
onsite at a project with no ssh during the day and horrible phone rates 
from the hotel room. So horde work is basically limited to weekends.

P.P.P.S.
Though I really tried this time, I know I'm terrible bad at getting code 
conventions right.
As compensation I promise to pay a beer for every remaining stupid error 
(missing spaces) I make and somebody else has to fix. Caveat: location 
for redemption is Munich only ;-)
Anybody knows of something similar to Checkstlye for php?
-------------- next part --------------
Index: framework/SyncML/SyncML/Sync.php
===================================================================
RCS file: /repository/framework/SyncML/SyncML/Sync.php,v
retrieving revision 1.9
diff -u -r1.9 Sync.php
--- framework/SyncML/SyncML/Sync.php	3 Jan 2005 13:09:17 -0000	1.9
+++ framework/SyncML/SyncML/Sync.php	23 Jan 2005 19:31:18 -0000
@@ -88,9 +88,6 @@
     {
         global $registry;
 
-        require_once 'Horde/History.php';
-        $history = &Horde_History::singleton();
-
         $state = &$_SESSION['SyncML.state'];
         $hordeType = $type = $this->_targetLocURI;
         $contentType = $state->getPreferedContentType($type);
@@ -103,9 +100,9 @@
             $guid = $registry->call($hordeType . '/import',
                                     array($state->convertClient2Server($command->getContent(), $contentType), $contentType));
             if (!is_a($guid, 'PEAR_Error')) {
-                $ts = $history->getTSforAction($guid, 'add');
+                $ts = $registry->call($hordeType . '/getTSforAction',array($guid,'add'));
                 if(!$ts) {
-                    Horde::logMessage('SyncML: unable to find add-ts for ' . $guid . ' at ' . $ts, __FILE__, __LINE__, PEAR_LOG_ERROR);
+                    Horde::logMessage('SyncML: unable to find add-ts for ' . $guid . ' at ' . $ts, __FILE__, __LINE__, PEAR_LOG_ERR);
                 }
                 $state->setUID($type, $command->getLocURI(), $guid, $ts);
                 $state->log("Client-Add");
@@ -122,7 +119,7 @@
 
             if (!is_a($guid, 'PEAR_Error')) {
                 $registry->call($hordeType . '/delete', array($guid));
-                $ts = $history->getTSforAction($guid, 'delete');
+                $ts = $registry->call($hordeType . '/getTSforAction',array($guid,'delete'));
                 $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 +135,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 = $registry->call($hordeType . '/getTSforAction',array($guid,'modify'));
                     $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 +152,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 = $registry->call($hordeType . '/getTSforAction',array($guid,'add'));
                     $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.14
diff -u -r1.14 TwoWaySync.php
--- framework/SyncML/SyncML/Sync/TwoWaySync.php	3 Jan 2005 13:09:18 -0000	1.14
+++ framework/SyncML/SyncML/Sync/TwoWaySync.php	23 Jan 2005 19:31:19 -0000
@@ -47,15 +47,12 @@
     {
         global $registry;
 
-        require_once 'Horde/History.php';
-        $history = &Horde_History::singleton();
-
         $state = &$_SESSION['SyncML.state'];
 
         // Get changes.
         $changes = $registry->call($hordeType. '/listBy', array('action' => 'modify', 'timestamp' => $refts));
         foreach ($changes as $guid) {
-            $guid_ts = $history->getTSforAction($guid, 'modify');
+            $guid_ts = $registry->call($hordeType . '/getTSforAction',array($guid,'modify'));
             $sync_ts = $state->getChangeTS($syncType, $guid);
             if ($sync_ts && $sync_ts == $guid_ts) {
                 // Change was done by us upon request of client.
@@ -90,7 +87,7 @@
         // Get deletes.
         $deletes = $registry->call($hordeType. '/listBy', array('action' => 'delete', 'timestamp' => $refts));
         foreach ($deletes as $guid) {
-            $guid_ts = $history->getTSforAction($guid, 'delete');
+            $guid_ts = $registry->call($hordeType . '/getTSforAction',array($guid,'delete'));
             $sync_ts = $state->getChangeTS($syncType, $guid);
             if ($sync_ts && $sync_ts == $guid_ts) {
                 // Change was done by us upon request of client.
@@ -116,9 +113,9 @@
         // Get adds.
         $adds = $registry->call($hordeType. '/listBy', array('action' => 'add', 'timestamp' => $refts));
         foreach ($adds as $guid) {
-            $guid_ts = $history->getTSforAction($guid, 'add');
+            $guid_ts = $registry->call($hordeType . '/getTSforAction',array($guid,'add'));
             $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: kronolith/lib/api.php
===================================================================
RCS file: /repository/kronolith/lib/api.php,v
retrieving revision 1.128
diff -u -r1.128 api.php
--- kronolith/lib/api.php	20 Jan 2005 10:30:27 -0000	1.128
+++ kronolith/lib/api.php	23 Jan 2005 19:31:23 -0000
@@ -40,6 +40,11 @@
     'type' => 'stringArray'
 );
 
+$_services['getTSforAction'] = array(
+    'args' => array('uid', 'timestamp'),
+    'type' => 'integer',
+);
+
 $_services['import'] = array(
     'args' => array('content' => 'string', 'contentType' => 'string', 'calendar' => 'string'),
     'type' => 'integer'
@@ -116,7 +121,26 @@
         return $histories;
     }
 
-    return array_keys($histories);
+   /* strip leading kronolith:username: */
+   return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
+}
+
+/**
+ * Returns the timestamp of an operation for a given uid an action
+ *
+ * @param string  $uid        The uid to look for
+ * @param string  $action     The action to check for - add, modify, or delete.
+ *
+ * @return integer            The timestamp for this action
+ */
+function _kronolith_getTSforAction($uid, $action)
+{
+    require_once 'Horde/History.php';
+    $history = &Horde_History::singleton();
+
+    $guid = 'kronolith:' . Auth::getAuth() . ':' . $uid;
+
+    return $history->getTSforAction($guid, $action);
 }
 
 /**
Index: mnemo/lib/api.php
===================================================================
RCS file: /repository/mnemo/lib/api.php,v
retrieving revision 1.53
diff -u -r1.53 api.php
--- mnemo/lib/api.php	21 Dec 2004 15:26:47 -0000	1.53
+++ mnemo/lib/api.php	23 Jan 2005 19:31:26 -0000
@@ -20,6 +20,11 @@
     'type' => 'stringArray'
 );
 
+$_services['getTSforAction'] = array(
+    'args' => array('uid', 'timestamp'),
+    'type' => 'integer',
+);
+
 $_services['import'] = array(
     'args' => array('content', 'contentType'),
     'type' => 'integer'
@@ -90,7 +95,26 @@
         return $histories;
     }
 
-    return array_keys($histories);
+   /* strip leading mnemo:username: */
+   return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
+}
+
+/**
+ * Returns the timestamp of an operation for a given uid an action
+ *
+ * @param string  $uid        The uid to look for
+ * @param string  $action     The action to check for - add, modify, or delete.
+ *
+ * @return integer            The timestamp for this action
+ */
+function _mnemo_getTSforAction($uid, $action)
+{
+    require_once 'Horde/History.php';
+    $history = &Horde_History::singleton();
+
+    $guid = 'mnemo:' . Auth::getAuth() . ':' . $uid;
+
+    return $history->getTSforAction($guid, $action);
 }
 
 /**
Index: nag/lib/api.php
===================================================================
RCS file: /repository/nag/lib/api.php,v
retrieving revision 1.100
diff -u -r1.100 api.php
--- nag/lib/api.php	19 Oct 2004 15:22:11 -0000	1.100
+++ nag/lib/api.php	23 Jan 2005 19:31:29 -0000
@@ -44,6 +44,11 @@
     'type' => 'stringArray',
 );
 
+$_services['getTSforAction'] = array(
+    'args' => array('uid', 'timestamp'),
+    'type' => 'integer',
+);
+
 $_services['import'] = array(
     'args' => array('content', 'contentType', 'tasklist'),
     'type' => 'integer',
@@ -224,7 +229,26 @@
         return $histories;
     }
 
-    return array_keys($histories);
+   /* strip leading nag:username: */
+   return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
+}
+
+/**
+ * Returns the timestamp of an operation for a given uid an action
+ *
+ * @param string  $uid        The uid to look for
+ * @param string  $action     The action to check for - add, modify, or delete.
+ *
+ * @return integer            The timestamp for this action
+ */
+function _nag_getTSforAction($uid, $action)
+{
+    require_once 'Horde/History.php';
+    $history = &Horde_History::singleton();
+
+    $guid = 'nag:' . Auth::getAuth() . ':' . $uid;
+
+    return $history->getTSforAction($guid, $action);
 }
 
 /**
Index: turba/lib/api.php
===================================================================
RCS file: /repository/turba/lib/api.php,v
retrieving revision 1.122
diff -u -r1.122 api.php
--- turba/lib/api.php	20 Jan 2005 15:24:35 -0000	1.122
+++ turba/lib/api.php	23 Jan 2005 19:31:34 -0000
@@ -44,23 +44,28 @@
     'type' => '{urn:horde}stringArray',
 );
 
+$_services['getTSforAction'] = array(
+    'args' => array('uid', 'timestamp'),
+    'type' => 'integer',
+);
+
 $_services['import'] = array(
     'args' => array('content', 'contentType', 'source'),
     'type' => 'string',
 );
 
 $_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',
 );
 
@@ -291,10 +296,30 @@
         return $histories;
     }
 
-    return array_keys($histories);
+   /* strip leading turba:username: */
+   return preg_replace('/^([^:]*:){2}/', '', array_keys($histories));
 }
 
 /**
+ * Returns the timestamp of an operation for a given uid an action
+ *
+ * @param string  $uid        The uid to look for
+ * @param string  $action     The action to check for - add, modify, or delete.
+ *
+ * @return integer            The timestamp for this action
+ */
+function _turba_getTSforAction($uid, $action)
+{
+    require_once 'Horde/History.php';
+    $history = &Horde_History::singleton();
+
+    $guid = 'turba:' . Auth::getAuth() . ':' . $uid;
+
+    return $history->getTSforAction($guid, $action);
+}
+
+
+/**
  * Import a contact represented in the specified contentType.
  *
  * @param string $content      The content of the contact.
@@ -309,8 +334,18 @@
 function _turba_import($content, $contentType = 'array', $source = '')
 {
     require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
+    global $cfgSources, $prefs;
 
+    // get default address book from config
+    if (empty($source)) {
+        $source = $prefs->getValue('default_dir');
+    }
+
+    // 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 +413,7 @@
     }
 
     $object = &$driver->getObject($result);
-    return $object->getValue('__key');
+    return $object->getValue('__uid');
 }
 
 /**
@@ -391,10 +426,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, $prefs;
 
     if (is_array($contentType)) {
         $options = $contentType;
@@ -404,18 +439,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 = $prefs->getValue('default_dir');
+    }
 
     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 +462,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 +508,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 +524,18 @@
     }
 
     require_once dirname(__FILE__) . '/base.php';
-    global $cfgSources;
+    global $cfgSources, $prefs;
 
-    // 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 = $prefs->getValue('default_dir');
+    }
 
     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 +549,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,39 +574,59 @@
  *
  * @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, $prefs;
 
-    // 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 = $prefs->getValue('default_dir');
+    }
 
     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