0011428: support caldav sync token
authorPaul Mehrer <p.mehrer@metaways.de>
Tue, 11 Aug 2015 14:49:14 +0000 (16:49 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Thu, 19 Nov 2015 08:34:51 +0000 (09:34 +0100)
https://forge.tine20.org/view.php?id=11428

Change-Id: Ie410dab5183d45ee231feb2c01b9b187e1ba6137
Reviewed-on: http://gerrit.tine20.com/customers/2220
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
Tested-by: Jenkins CI (http://ci.tine20.com/)
13 files changed:
scripts/vagrant/Vagrantfile
tests/tine20/Tinebase/WebDav/AllTests.php
tests/tine20/Tinebase/WebDav/Plugin/AbstractBaseTest.php [new file with mode: 0644]
tests/tine20/Tinebase/WebDav/Plugin/InverseTest.php
tests/tine20/Tinebase/WebDav/Plugin/OwnCloudTest.php
tests/tine20/Tinebase/WebDav/Plugin/PrincipalSearchTest.php
tests/tine20/Tinebase/WebDav/Plugin/SyncTokenTest.php [new file with mode: 0644]
tine20/Calendar/Convert/Event/VCalendar/Factory.php
tine20/Calendar/Frontend/WebDAV/Container.php
tine20/Tinebase/Container.php
tine20/Tinebase/Server/WebDAV.php
tine20/Tinebase/WebDav/Container/Abstract.php
tine20/Tinebase/WebDav/Plugin/SyncToken.php [new file with mode: 0644]

index f00c9b2..ec2b764 100644 (file)
@@ -42,4 +42,6 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
                vb.customize ["modifyvm", :id, "--cpus", "2"]
                vb.customize ["modifyvm", :id, "--ioapic", "on"]
        end
                vb.customize ["modifyvm", :id, "--cpus", "2"]
                vb.customize ["modifyvm", :id, "--ioapic", "on"]
        end
+
+       config.vm.network "forwarded_port", guest: 80, host: 8080
 end
 end
index 88a7315..000e017 100644 (file)
@@ -4,18 +4,10 @@
  * 
  * @package     Tinebase
  * @license     http://www.gnu.org/licenses/agpl.html
  * 
  * @package     Tinebase
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2008 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2008-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
-/**
- * Test helper
- */
-require_once dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
-if (! defined('PHPUnit_MAIN_METHOD')) {
-    define('PHPUnit_MAIN_METHOD', 'Tinebase_WebDav_AllTests::main');
-}
-
 class Tinebase_WebDav_AllTests
 {
     public static function main ()
 class Tinebase_WebDav_AllTests
 {
     public static function main ()
@@ -30,12 +22,9 @@ class Tinebase_WebDav_AllTests
         $suite->addTestSuite('Tinebase_WebDav_Plugin_InverseTest');
         $suite->addTestSuite('Tinebase_WebDav_Plugin_OwnCloudTest');
         $suite->addTestSuite('Tinebase_WebDav_Plugin_PrincipalSearchTest');
         $suite->addTestSuite('Tinebase_WebDav_Plugin_InverseTest');
         $suite->addTestSuite('Tinebase_WebDav_Plugin_OwnCloudTest');
         $suite->addTestSuite('Tinebase_WebDav_Plugin_PrincipalSearchTest');
+        $suite->addTestSuite('Tinebase_WebDav_Plugin_SyncTokenTest');
         $suite->addTestSuite('Tinebase_WebDav_RootTest');
         $suite->addTestSuite('Tinebase_WebDav_RootTest');
-        
+
         return $suite;
     }
 }
         return $suite;
     }
 }
-
-if (PHPUnit_MAIN_METHOD == 'Tinebase_WebDav_AllTests::main') {
-    Tinebase_WebDav_AllTests::main();
-}
diff --git a/tests/tine20/Tinebase/WebDav/Plugin/AbstractBaseTest.php b/tests/tine20/Tinebase/WebDav/Plugin/AbstractBaseTest.php
new file mode 100644 (file)
index 0000000..5f004c5
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Tine 2.0 - http://www.tine20.org
+ *
+ * @package     Tinebase
+ * @subpackage  WebDav
+ * @license     http://www.gnu.org/licenses/agpl.html
+ * @copyright   Copyright (c) 2015-2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Paul Mehrer <p.mehrer@metaways.de>
+ */
+
+/**
+ * Test helper
+ */
+require_once 'vendor/sabre/dav/tests/Sabre/HTTP/ResponseMock.php';
+
+
+/**
+ * Abstract test class for Tinebase_WebDav_Plugin_*
+ */
+abstract class Tinebase_WebDav_Plugin_AbstractBaseTest extends TestCase
+{
+    /**
+     *
+     * @var Sabre\DAV\Server
+     */
+    protected $server;
+
+    /**
+     *
+     * @var Sabre\HTTP\ResponseMock
+     */
+    protected $response;
+
+    /**
+     * @var array test objects
+     */
+    protected $objects = array();
+
+    /**
+     * Sets up the fixture.
+     * This method is called before a test is executed.
+     *
+     * @access protected
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->server = new Sabre\DAV\Server(new Tinebase_WebDav_Root());
+
+        $this->response = new Sabre\HTTP\ResponseMock();
+        $this->server->httpResponse = $this->response;
+    }
+
+    /**
+     * Setups a personal calendar
+     */
+    protected function setupCalendarContainer()
+    {
+        $this->objects['initialContainer'] = Tinebase_Container::getInstance()->addContainer(new Tinebase_Model_Container(array(
+            'name'              => Tinebase_Record_Abstract::generateUID(),
+            'type'              => Tinebase_Model_Container::TYPE_PERSONAL,
+            'backend'           => 'Sql',
+            'application_id'    => Tinebase_Application::getInstance()->getApplicationByName('Calendar')->getId(),
+        )));
+        Tinebase_Container::getInstance()->increaseContentSequence($this->objects['initialContainer']);
+    }
+}
\ No newline at end of file
index b645dd7..774c991 100644 (file)
@@ -5,30 +5,16 @@
  * @package     Tinebase
  * @subpackage  Frontend
  * @license     http://www.gnu.org/licenses/agpl.html
  * @package     Tinebase
  * @subpackage  Frontend
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2013-2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2013-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
 /**
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
 /**
- * Test helper
- */
-require_once 'vendor/sabre/dav/tests/Sabre/HTTP/ResponseMock.php';
-
-/**
  * Test class for Tinebase_WebDav_Plugin_Inverse
  */
  * Test class for Tinebase_WebDav_Plugin_Inverse
  */
-class Tinebase_WebDav_Plugin_InverseTest extends TestCase
+class Tinebase_WebDav_Plugin_InverseTest extends Tinebase_WebDav_Plugin_AbstractBaseTest
 {
 {
-    /**
-     * @var array test objects
-     */
-    protected $objects = array();
-    
-    /**
-     * 
-     * @var Sabre\DAV\Server
-     */
-    protected $server;
+
     
     /**
      * Runs the test methods of this class.
     
     /**
      * Runs the test methods of this class.
@@ -52,21 +38,11 @@ class Tinebase_WebDav_Plugin_InverseTest extends TestCase
     {
         parent::setUp();
         
     {
         parent::setUp();
         
-        $this->objects['initialContainer'] = Tinebase_Container::getInstance()->addContainer(new Tinebase_Model_Container(array(
-            'name'              => Tinebase_Record_Abstract::generateUID(),
-            'type'              => Tinebase_Model_Container::TYPE_PERSONAL,
-            'backend'           => 'Sql',
-            'application_id'    => Tinebase_Application::getInstance()->getApplicationByName('Calendar')->getId(),
-        )));
-        
-        $this->server = new Sabre\DAV\Server(new Tinebase_WebDav_Root());
+        parent::setupCalendarContainer();
         
         $this->plugin = new Tinebase_WebDav_Plugin_Inverse();
         
         $this->server->addPlugin($this->plugin);
         
         $this->plugin = new Tinebase_WebDav_Plugin_Inverse();
         
         $this->server->addPlugin($this->plugin);
-        
-        $this->response = new Sabre\HTTP\ResponseMock();
-        $this->server->httpResponse = $this->response;
     }
 
     /**
     }
 
     /**
index a887099..c992c9f 100644 (file)
@@ -5,27 +5,17 @@
  * @package     Tinebase
  * @subpackage  Frontend
  * @license     http://www.gnu.org/licenses/agpl.html
  * @package     Tinebase
  * @subpackage  Frontend
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2013-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2013-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
-/**
- * Test helper
- */
-require_once 'vendor/sabre/dav/tests/Sabre/HTTP/ResponseMock.php';
 
 /**
  * Test class for Tinebase_WebDav_Plugin_OwnCloud
  */
 
 /**
  * Test class for Tinebase_WebDav_Plugin_OwnCloud
  */
-class Tinebase_WebDav_Plugin_OwnCloudTest extends TestCase
+class Tinebase_WebDav_Plugin_OwnCloudTest extends Tinebase_WebDav_Plugin_AbstractBaseTest
 {
     /**
 {
     /**
-     * 
-     * @var Sabre\DAV\Server
-     */
-    protected $server;
-    
-    /**
      * Runs the test methods of this class.
      *
      * @access public
      * Runs the test methods of this class.
      *
      * @access public
@@ -47,14 +37,9 @@ class Tinebase_WebDav_Plugin_OwnCloudTest extends TestCase
     {
         parent::setUp();
         
     {
         parent::setUp();
         
-        $this->server = new Sabre\DAV\Server(new Tinebase_WebDav_Root());
-        
         $this->plugin = new Tinebase_WebDav_Plugin_OwnCloud();
         
         $this->server->addPlugin($this->plugin);
         $this->plugin = new Tinebase_WebDav_Plugin_OwnCloud();
         
         $this->server->addPlugin($this->plugin);
-        
-        $this->response = new Sabre\HTTP\ResponseMock();
-        $this->server->httpResponse = $this->response;
     }
 
     /**
     }
 
     /**
index de7656c..d865fe6 100644 (file)
@@ -3,30 +3,23 @@
  * Tine 2.0 - http://www.tine20.org
  * 
  * @package     Tinebase
  * Tine 2.0 - http://www.tine20.org
  * 
  * @package     Tinebase
- * @subpackage  Frontend
+ * @subpackage  WebDav
  * @license     http://www.gnu.org/licenses/agpl.html
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2013-2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2013-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
 /**
  * Test helper
  */
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
 /**
  * Test helper
  */
-require_once 'vendor/sabre/dav/tests/Sabre/HTTP/ResponseMock.php';
 require_once 'vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/Mock.php';
 
 /**
  * Test class for Tinebase_WebDav_Plugin_OwnCloud
  */
 require_once 'vendor/sabre/dav/tests/Sabre/DAV/Auth/Backend/Mock.php';
 
 /**
  * Test class for Tinebase_WebDav_Plugin_OwnCloud
  */
-class Tinebase_WebDav_Plugin_PrincipalSearchTest extends TestCase
+class Tinebase_WebDav_Plugin_PrincipalSearchTest extends Tinebase_WebDav_Plugin_AbstractBaseTest
 {
     /**
 {
     /**
-     * 
-     * @var Sabre\DAV\Server
-     */
-    protected $server;
-    
-    /**
      * Sets up the fixture.
      * This method is called before a test is executed.
      *
      * Sets up the fixture.
      * This method is called before a test is executed.
      *
@@ -36,8 +29,6 @@ class Tinebase_WebDav_Plugin_PrincipalSearchTest extends TestCase
     {
         parent::setUp();
         
     {
         parent::setUp();
         
-        $this->server = new Sabre\DAV\Server(new Tinebase_WebDav_Root());
-        
         $mockBackend = new Sabre\DAV\Auth\Backend\Mock();
         $mockBackend->defaultUser = Tinebase_Core::getUser()->contact_id;
         
         $mockBackend = new Sabre\DAV\Auth\Backend\Mock();
         $mockBackend->defaultUser = Tinebase_Core::getUser()->contact_id;
         
@@ -54,9 +45,6 @@ class Tinebase_WebDav_Plugin_PrincipalSearchTest extends TestCase
         
         $this->plugin = new Tinebase_WebDav_Plugin_PrincipalSearch();
         $this->server->addPlugin($this->plugin);
         
         $this->plugin = new Tinebase_WebDav_Plugin_PrincipalSearch();
         $this->server->addPlugin($this->plugin);
-        
-        $this->response = new Sabre\HTTP\ResponseMock();
-        $this->server->httpResponse = $this->response;
     }
 
     /**
     }
 
     /**
diff --git a/tests/tine20/Tinebase/WebDav/Plugin/SyncTokenTest.php b/tests/tine20/Tinebase/WebDav/Plugin/SyncTokenTest.php
new file mode 100644 (file)
index 0000000..44aa786
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+/**
+ * Tine 2.0 - http://www.tine20.org
+ *
+ * @package     Tinebase
+ * @subpackage  WebDav
+ * @license     http://www.gnu.org/licenses/agpl.html
+ * @copyright   Copyright (c) 2015-2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Paul Mehrer <p.mehrer@metaways.de>
+ */
+
+class Tinebase_WebDav_Plugin_SyncTokenTest extends Tinebase_WebDav_Plugin_AbstractBaseTest
+{
+    /**
+     * @var Tinebase_WebDav_Plugin_SyncToken
+     */
+    protected $plugin;
+
+    /**
+     * Runs the test methods of this class.
+     *
+     * @access public
+     * @static
+     */
+    public static function main()
+    {
+        $suite  = new PHPUnit_Framework_TestSuite('Tine 2.0 Tinebase WebDav Plugin SyncToken Tests');
+        PHPUnit_TextUI_TestRunner::run($suite);
+    }
+
+    /**
+     * Sets up the fixture.
+     * This method is called before a test is executed.
+     *
+     * @access protected
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+
+        parent::setupCalendarContainer();
+
+        $this->setupCalendarContent();
+
+        $this->plugin = new Tinebase_WebDav_Plugin_SyncToken();
+        $this->server->addPlugin($this->plugin);
+    }
+
+    /**
+     * tear down tests
+     */
+    public function tearDown()
+    {
+        parent::tearDown();
+
+        Tinebase_Container::getInstance()->getContentBackend()->delete($this->objects['containerContent']);
+    }
+
+    protected function setupCalendarContent()
+    {
+        /**
+         * @var Tinebase_Backend_Sql
+         */
+        $contentBackend = Tinebase_Container::getInstance()->getContentBackend();
+        $this->objects['containerContent'] = new Tinebase_Model_ContainerContent(array(
+            'action'          => Tinebase_Model_ContainerContent::ACTION_CREATE,
+            'record_id'       => 'testRecordId',
+            'time'            => Tinebase_DateTime::now(),
+            'container_id'    => $this->objects['initialContainer']->id,
+            'content_seq'     => 1,
+        ));
+        $contentBackend->create($this->objects['containerContent']);
+    }
+
+    /**
+     * test getSupportedReportSet method
+     */
+    public function testGetSupportedReportSetForCalendarNode()
+    {
+        $set = $this->plugin->getSupportedReportSet('/calendars/' . Tinebase_Core::getUser()->contact_id . '/' . $this->objects['initialContainer']->id);
+
+        $this->assertContains('{DAV:}sync-collection', $set);
+    }
+
+    /**
+     * test propfind sync-token request
+     */
+    public function testPropfindSyncToken()
+    {
+        $body = '<?xml version="1.0" encoding="utf-8"?>
+                 <A:propfind xmlns:A="DAV:">
+                     <A:prop>
+                        <A:sync-token/>
+                     </A:prop>
+                 </A:propfind>';
+
+        $request = new Sabre\HTTP\Request(array(
+            'REQUEST_METHOD' => 'PROPFIND',
+            'REQUEST_URI'    => '/calendars/' . Tinebase_Core::getUser()->contact_id . '/' . $this->objects['initialContainer']->id,
+            'HTTP_DEPTH'     => '1',
+        ));
+        $request->setBody($body);
+
+        $this->server->httpRequest = $request;
+        $this->server->exec();
+        $this->assertEquals('HTTP/1.1 207 Multi-Status', $this->response->status);
+        $this->assertContains('<d:sync-token>http://tine20.net/ns/sync/1</d:sync-token></d:prop><d:status>HTTP/1.1 200 OK</d:status></d:propstat></d:response></d:multistatus>', $this->response->body);
+    }
+
+    /**
+     * test sync-collection request
+     */
+    public function testSyncCollection()
+    {
+        $body = '<?xml version="1.0" encoding="UTF-8"?>
+                <A:sync-collection xmlns:A="DAV:">
+                    <A:sync-token>http://tine20.net/ns/sync/0</A:sync-token>
+                    <A:sync-level>1</A:sync-level>
+                    <A:prop>
+                        <A:getetag/>
+                        <A:getcontenttype/>
+                    </A:prop>
+                </A:sync-collection>';
+
+        $request = new Sabre\HTTP\Request(array(
+            'REQUEST_METHOD' => 'REPORT',
+            'REQUEST_URI'    => '/calendars/' . Tinebase_Core::getUser()->contact_id . '/' . $this->objects['initialContainer']->id,
+            'HTTP_DEPTH'     => '1',
+        ));
+        $request->setBody($body);
+
+        $this->server->httpRequest = $request;
+        $this->server->exec();
+        
+        $this->assertEquals('HTTP/1.1 207 Multi-Status', $this->response->status);
+        $this->assertContains('<d:sync-token>http://tine20.net/ns/sync/1</d:sync-token></d:multistatus>', $this->response->body);
+    }
+}
\ No newline at end of file
index 44b2c2f..6a4536e 100644 (file)
@@ -125,4 +125,22 @@ class Calendar_Convert_Event_VCalendar_Factory
         
         return array($backend, $version);
     }
         
         return array($backend, $version);
     }
+
+    /**
+     * parse useragent and return backend and version
+     *
+     * @return array
+     */
+    static public function supportsSyncToken($_userAgent)
+    {
+        list($backend, $version) = self::parseUserAgent($_userAgent);
+        switch($backend)
+        {
+            case self::CLIENT_MACOSX:
+                if (version_compare($version, '10.9', '>='))
+                    return true;
+                break;
+        }
+        return false;
+    }
 }
 }
index 42e57c8..da2d3d5 100644 (file)
@@ -32,7 +32,9 @@ class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstr
      * @var array
      */
     protected $_calendarQueryCache = null;
      * @var array
      */
     protected $_calendarQueryCache = null;
-    
+
+
+
     /**
      * (non-PHPdoc)
      * @see Sabre\DAV\Collection::getChild()
     /**
      * (non-PHPdoc)
      * @see Sabre\DAV\Collection::getChild()
@@ -165,7 +167,7 @@ class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstr
         
         return $children;
     }
         
         return $children;
     }
-    
+
     /**
      * Returns the list of properties
      *
     /**
      * Returns the list of properties
      *
@@ -178,6 +180,7 @@ class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstr
         
         $properties = array(
             '{http://calendarserver.org/ns/}getctag' => $ctags,
         
         $properties = array(
             '{http://calendarserver.org/ns/}getctag' => $ctags,
+            '{DAV:}sync-token'  => Tinebase_WebDav_Plugin_SyncToken::SYNCTOKEN_PREFIX . $ctags,
             'id'                => $this->_container->getId(),
             'uri'               => $this->_useIdAsName == true ? $this->_container->getId() : $this->_container->name,
             '{DAV:}resource-id' => 'urn:uuid:' . $this->_container->getId(),
             'id'                => $this->_container->getId(),
             'uri'               => $this->_useIdAsName == true ? $this->_container->getId() : $this->_container->name,
             '{DAV:}resource-id' => 'urn:uuid:' . $this->_container->getId(),
@@ -331,6 +334,10 @@ class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstr
      */
     protected function _getMaxPeriodFrom()
     {
      */
     protected function _getMaxPeriodFrom()
     {
+        //if the client does support sync tokens and the plugin Tinebase_WebDav_Plugin_SyncToken is active, allow to filter for all calendar events => return 100 years
+        if (Calendar_Convert_Event_VCalendar_Factory::supportsSyncToken($_SERVER['HTTP_USER_AGENT'])) {
+            return 100;
+        }
         return Calendar_Config::getInstance()->get(Calendar_Config::MAX_FILTER_PERIOD_CALDAV, 2);
     }
     
         return Calendar_Config::getInstance()->get(Calendar_Config::MAX_FILTER_PERIOD_CALDAV, 2);
     }
     
@@ -439,4 +446,14 @@ class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstr
     {
         
     }
     {
         
     }
+
+    /**
+     * indicates whether the concrete class supports sync-token
+     *
+     * @return bool
+     */
+    public function supportsSyncToken()
+    {
+        return true;
+    }
 }
 }
index dc5f480..13a4cdf 100644 (file)
@@ -109,7 +109,7 @@ class Tinebase_Container extends Tinebase_Backend_Sql_Abstract
      * 
      * @todo move this to constructor when this no longer extends Tinebase_Backend_Sql_Abstract
      */
      * 
      * @todo move this to constructor when this no longer extends Tinebase_Backend_Sql_Abstract
      */
-    protected function _getContentBackend()
+    public function getContentBackend()
     {
         if ($this->_contentBackend === NULL) {
             $this->_contentBackend  = new Tinebase_Backend_Sql(array(
     {
         if ($this->_contentBackend === NULL) {
             $this->_contentBackend  = new Tinebase_Backend_Sql(array(
@@ -1695,7 +1695,7 @@ class Tinebase_Container extends Tinebase_Backend_Sql_Abstract
                 ));
                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
                     . ' Creating "' . $action . '" action content history record for record id ' . $recordId);
                 ));
                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
                     . ' Creating "' . $action . '" action content history record for record id ' . $recordId);
-                $this->_getContentBackend()->create($contentRecord);
+                $this->getContentBackend()->create($contentRecord);
             }
             
             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
             }
             
             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
@@ -1727,7 +1727,7 @@ class Tinebase_Container extends Tinebase_Backend_Sql_Abstract
         $pagination = new Tinebase_Model_Pagination(array(
             'sort' => 'content_seq'
         ));
         $pagination = new Tinebase_Model_Pagination(array(
             'sort' => 'content_seq'
         ));
-        $result = $this->_getContentBackend()->search($filter, $pagination);
+        $result = $this->getContentBackend()->search($filter, $pagination);
         return $result;
     }
 
         return $result;
     }
 
index 8e8151b..cdee29f 100644 (file)
@@ -5,7 +5,7 @@
  * @package     Tinebase
  * @subpackage  Server
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @package     Tinebase
  * @subpackage  Server
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
- * @copyright   Copyright (c) 2011-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2011-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
@@ -132,8 +132,8 @@ class Tinebase_Server_WebDAV extends Tinebase_Server_Abstract implements Tinebas
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_Inverse());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_OwnCloud());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_PrincipalSearch());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_Inverse());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_OwnCloud());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_PrincipalSearch());
-        #self::$_server->addPlugin(new DAV\Sync\Plugin());
         self::$_server->addPlugin(new \Sabre\DAV\Browser\Plugin());
         self::$_server->addPlugin(new \Sabre\DAV\Browser\Plugin());
+        self::$_server->addPlugin(new Tinebase_WebDav_Plugin_SyncToken());
         
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
             ob_start();
         
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
             ob_start();
index e57c45e..5e15bf9 100644 (file)
@@ -6,7 +6,7 @@
  * @subpackage  WebDAV
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  * @subpackage  WebDAV
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Lars Kneschke <l.kneschke@metaways.de>
- * @copyright   Copyright (c) 2011-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2011-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  *
  */
 
  *
  */
 
@@ -358,6 +358,10 @@ abstract class Tinebase_WebDav_Container_Abstract extends \Sabre\DAV\Collection
                 case '{DAV:}getetag':
                     $response[$prop] = $this->getETag();
                     break;
                 case '{DAV:}getetag':
                     $response[$prop] = $this->getETag();
                     break;
+
+                case '{DAV:}sync-token':
+                    $response[$prop] = $this->getSyncToken();
+                    break;
                     
                 default:
                     if (isset($properties[$prop])) $response[$prop] = $properties[$prop];
                     
                 default:
                     if (isset($properties[$prop])) $response[$prop] = $properties[$prop];
@@ -526,4 +530,59 @@ abstract class Tinebase_WebDav_Container_Abstract extends \Sabre\DAV\Collection
     {
         return null;
     }
     {
         return null;
     }
+
+    /**
+     * indicates whether the concrete class supports sync-token
+     *
+     * @return bool
+     */
+    public function supportsSyncToken()
+    {
+        return false;
+    }
+
+    /**
+     * Returns the content sequence for this container
+     *
+     * @return string
+     */
+    public function getSyncToken()
+    {
+       return Tinebase_Container::getInstance()->getContentSequence($this->_container);
+    }
+
+    /**
+     * returns the changes happened since the provided syncToken which is the content sequence
+     *
+     * @param string $syncToken
+     * @return array
+     */
+    public function getChanges($syncToken)
+    {
+        $result = array(
+            'syncToken' => $this->getSyncToken(),
+            Tinebase_Model_ContainerContent::ACTION_CREATE => array(),
+            Tinebase_Model_ContainerContent::ACTION_UPDATE => array(),
+            Tinebase_Model_ContainerContent::ACTION_DELETE => array(),
+        );
+
+        $resultSet = Tinebase_Container::getInstance()->getContentHistory($this->_container, $syncToken);
+        foreach($resultSet as $contentModel) {
+            switch($contentModel->action) {
+                case Tinebase_Model_ContainerContent::ACTION_DELETE:
+                    unset($result[Tinebase_Model_ContainerContent::ACTION_CREATE][$contentModel->record_id]);
+                    unset($result[Tinebase_Model_ContainerContent::ACTION_UPDATE][$contentModel->record_id]);
+                case Tinebase_Model_ContainerContent::ACTION_CREATE:
+                case Tinebase_Model_ContainerContent::ACTION_UPDATE:
+                    $result[$contentModel->action][$contentModel->record_id] = $contentModel->record_id . $this->_suffix;
+                    break;
+
+                default:
+                    Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' unknown Tinebase_Model_ContainerContent::ACTION_* found: ' . $contentModel->action . ' ... ignoring');
+                    break;
+            }
+        }
+
+        return $result;
+    }
 }
 }
diff --git a/tine20/Tinebase/WebDav/Plugin/SyncToken.php b/tine20/Tinebase/WebDav/Plugin/SyncToken.php
new file mode 100644 (file)
index 0000000..17dd1f2
--- /dev/null
@@ -0,0 +1,222 @@
+<?php
+/**
+ * WebDAV plugin for sync-token support
+ * 
+ * This plugin provides functionality to request sync-tokens
+ * It is a backport of sabre/dav/sync/plugin.php
+ *
+ * see: https://tools.ietf.org/html/rfc6578
+ * see: http://sabre.io/dav/building-a-caldav-client/#speeding-up-sync-with-webdav-sync
+ *
+ * NOTE: xxx
+ *       xxx
+ *       
+ * @package    Tinebase
+ * @subpackage WebDav
+ * @copyright  Copyright (c) 2015-2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author     Paul Mehrer <p.mehrer@metaways.de>
+ * @license    http://sabre.io/license/ Modified BSD License
+ */
+class Tinebase_WebDav_Plugin_SyncToken extends \Sabre\DAV\ServerPlugin
+{
+    /**
+     * Reference to server object
+     *
+     * @var \Sabre\DAV\Server
+     */
+    protected $server;
+
+    const SYNCTOKEN_PREFIX = 'http://tine20.net/ns/sync/';
+
+    /**
+     * Returns a list of features for the DAV: HTTP header. 
+     * 
+     * @return array 
+     */
+    public function getFeatures() 
+    {
+        return array('sync-token');
+    }
+
+    /**
+     * Returns a plugin name.
+     * 
+     * Using this name other plugins will be able to access other plugins
+     * using \Sabre\DAV\Server::getPlugin 
+     * 
+     * @return string 
+     */
+    public function getPluginName() 
+    {
+        return 'calendarSyncToken';
+    }
+
+    /**
+     * Initializes the plugin 
+     * 
+     * @param \Sabre\DAV\Server $server 
+     * @return void
+     */
+    public function initialize(\Sabre\DAV\Server $server) 
+    {
+        $this->server = $server;
+
+        $self = $this;
+        $server->subscribeEvent('report', function($reportName, $dom, $uri) use ($self, $server) {
+            if ($reportName === '{DAV:}sync-collection') {
+                $server->transactionType = 'report-sync-collection';
+                $self->syncCollection($uri, $dom);
+                return false;
+            }
+        });
+    }
+
+    /**
+     * Returns a list of reports this plugin supports.
+     *
+     * This will be used in the {DAV:}supported-report-set property.
+     * Note that you still need to subscribe to the 'report' event to actually
+     * implement them
+     *
+     * @param string $uri
+     * @return array
+     */
+    function getSupportedReportSet($uri)
+    {
+        $node = $this->server->tree->getNodeForPath($uri);
+
+        if ($node instanceof Tinebase_WebDav_Container_Abstract && $node->supportsSyncToken()) {
+            return array(
+                '{DAV:}sync-collection',
+            );
+        }
+        return array();
+    }
+
+    /**
+     * This method handles the {DAV:}sync-collection HTTP REPORT.
+     *
+     * @param string $uri
+     * @param \DOMDocument $report
+     * @return void
+     */
+    function syncCollection($uri, \DOMDocument $report)
+    {
+        // Getting the sync token of the data requested
+        /**
+         * @var $node Tinebase_WebDav_Container_Abstract
+         */
+        $node = $this->server->tree->getNodeForPath($uri);
+        if (!($node instanceof Tinebase_WebDav_Container_Abstract) || !$node->supportsSyncToken()) {
+            throw new Sabre\DAV\Exception\ReportNotSupported('The {DAV:}sync-collection REPORT is not supported on this url.');
+        }
+        $token = $node->getSyncToken();
+        if (!$token) {
+            throw new Sabre\DAV\Exception\ReportNotSupported('No sync information is available at this node');
+        }
+
+        // getting the sync token send with the request
+        $syncToken = '';
+        $syncTokenList = $report->getElementsByTagNameNS('urn:DAV', 'sync-token');
+        if ($syncTokenList->length == 1) {
+            $syncToken = $syncTokenList->item(0)->textContent; //?!? //nodeValue;
+        }
+        // Sync-token must start with our prefix
+        if (substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX || strlen($syncToken) <= strlen(self::SYNCTOKEN_PREFIX)) {
+            throw new Sabre\DAV\Exception\BadRequest('Invalid or unknown sync token');
+        }
+        $syncToken = substr($syncToken, strlen(self::SYNCTOKEN_PREFIX));
+
+        // get the list of properties the client requested
+        $properties = array_keys(Sabre\DAV\XMLUtil::parseProperties($report->documentElement));
+
+        // get changes since client sync token
+        $changeInfo = $node->getChanges($syncToken);
+        if (is_null($changeInfo)) {
+            throw new Sabre\DAV\Exception\BadRequest('Invalid or unknown sync token');
+        }
+
+        // Encoding the response
+        $this->sendSyncCollectionResponse(
+            $changeInfo['syncToken'],
+            $uri,
+            $changeInfo[Tinebase_Model_ContainerContent::ACTION_CREATE],
+            $changeInfo[Tinebase_Model_ContainerContent::ACTION_UPDATE],
+            $changeInfo[Tinebase_Model_ContainerContent::ACTION_DELETE],
+            $properties
+        );
+    }
+
+    /**
+     * Sends the response to a sync-collection request.
+     *
+     * @param string $syncToken
+     * @param string $collectionUrl
+     * @param array $added
+     * @param array $modified
+     * @param array $deleted
+     * @param array $properties
+     * @return void
+     */
+    protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties)
+    {
+        $resolvedProperties = array();
+        foreach (array_merge($added, $modified) as $item) {
+            $fullPath = $collectionUrl . '/' . $item;
+            try {
+                $resolvedProperties[$fullPath] = $this->server->getPropertiesForPath($fullPath, $properties);
+
+                // in case the user doesnt have access to this
+            } catch (Sabre\DAV\Exception\NotFound $e) {
+                unset($resolvedProperties[$fullPath]);
+            }
+        }
+        foreach($deleted as $item) {
+            $fullPath = $collectionUrl . '/' . $item;
+            $resolvedProperties[$fullPath] = array();
+        }
+
+        $data = $this->generateMultiStatus($resolvedProperties, $syncToken);
+
+        $this->server->httpResponse->sendStatus(207);
+        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+        $this->server->httpResponse->sendBody($data);
+    }
+
+    protected function generateMultiStatus($properties, $syncToken)
+    {
+        $dom = new \DOMDocument('1.0', 'utf-8');
+
+        //$dom->formatOutput = true;
+        $multiStatus = $dom->createElement('d:multistatus');
+
+        // Adding in default namespaces
+        foreach ($this->server->xmlNamespaces as $namespace => $prefix) {
+            $multiStatus->setAttribute('xmlns:' . $prefix, $namespace);
+        }
+
+        foreach ($properties as $href => $entries) {
+            if (count($entries) === 0) { //404
+                $response = $dom->createElement('d:response');
+                $href = $dom->createElement('d:href', $href);
+                $response->appendChild($href);
+                $status = $dom->createElement('d:status', $this->server->httpResponse->getStatusMessage(404));
+                $response->appendChild($status);
+                $multiStatus->appendChild($response);
+            } else {
+                foreach($entries as $entry) {
+                    $ehref = $entry['href'];
+                    unset($entry['href']);
+
+                    $response = new Sabre\DAV\Property\Response($ehref, $entry);
+                    $response->serialize($this->server, $multiStatus);
+                }
+            }
+        }
+
+        $multiStatus->appendChild($dom->createElement('d:sync-token', self::SYNCTOKEN_PREFIX . $syncToken));
+        $dom->appendChild($multiStatus);
+
+        return $dom->saveXML();
+    }
+}