0012516: CalDAV speedup for etag/content type property lookups
authorPaul Mehrer <p.mehrer@metaways.de>
Wed, 2 Sep 2015 11:29:01 +0000 (13:29 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Wed, 18 Jan 2017 14:39:19 +0000 (15:39 +0100)
Change-Id: I4a88f0293d556f4cdd069071249dc67ed8106b8f
Reviewed-on: http://gerrit.tine20.com/customers/4048
Tested-by: Jenkins CI (http://ci.tine20.com/)
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
12 files changed:
tests/tine20/Calendar/Frontend/CalDAV/AllTests.php
tests/tine20/Calendar/Frontend/CalDAV/SpeedUpPropfindPluginTest.php [new file with mode: 0644]
tests/tine20/Calendar/Frontend/WebDAV/ContainerTest.php
tests/tine20/Tinebase/WebDav/Plugin/SyncTokenTest.php
tine20/Calendar/Backend/Sql.php
tine20/Calendar/Frontend/CalDAV/FixMultiGet404Plugin.php [new file with mode: 0644]
tine20/Calendar/Frontend/CalDAV/PropertyResponse.php [new file with mode: 0644]
tine20/Calendar/Frontend/CalDAV/SpeedUpPropfindPlugin.php [new file with mode: 0644]
tine20/Calendar/Frontend/WebDAV/Container.php
tine20/Calendar/Frontend/WebDAV/Event.php
tine20/Tinebase/Server/WebDAV.php
tine20/Tinebase/WebDav/Container/Abstract.php

index 29a5c9b..2b0980c 100644 (file)
@@ -26,6 +26,7 @@ class Calendar_Frontend_CalDAV_AllTests
         $suite->addTestSuite('Calendar_Frontend_CalDAV_PluginDefaultAlarmsTest');
         $suite->addTestSuite('Calendar_Frontend_CalDAV_ProxyTest');
         $suite->addTestSuite('Calendar_Frontend_CalDAV_PluginManagedAttachmentsTest');
+        $suite->addTestSuite('Calendar_Frontend_CalDAV_SpeedUpPropfindPluginTest');
         return $suite;
     }
 }
diff --git a/tests/tine20/Calendar/Frontend/CalDAV/SpeedUpPropfindPluginTest.php b/tests/tine20/Calendar/Frontend/CalDAV/SpeedUpPropfindPluginTest.php
new file mode 100644 (file)
index 0000000..8e9a67d
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+/**
+ * Tine 2.0 - http://www.tine20.org
+ *
+ * @package     Tinebase
+ * @subpackage  Frontend
+ * @license     http://www.gnu.org/licenses/agpl.html
+ * @copyright   Copyright (c) 2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Paul Mehrer <p.mehrer@metaways.de>
+ */
+
+/**
+ * Test helper
+ */
+require_once __DIR__ . '/../../../../../tine20/vendor/sabre/dav/tests/Sabre/HTTP/ResponseMock.php';
+
+/**
+ * Test class for Tinebase_WebDav_Plugin_OwnCloud
+ */
+class Calendar_Frontend_CalDAV_SpeedUpPropfindPluginTest extends TestCase
+{
+    /**
+     *
+     * @var Sabre\DAV\Server
+     */
+    protected $server;
+
+    /**
+     * Sets up the fixture.
+     * This method is called before a test is executed.
+     *
+     * @access protected
+     */
+    protected function setUp()
+    {
+        $this->calDAVTests = new Calendar_Frontend_WebDAV_EventTest();
+        $this->calDAVTests->setup();
+
+        parent::setUp();
+
+        $this->server = new Sabre\DAV\Server(new Tinebase_WebDav_Root());
+
+        $this->plugin = new Calendar_Frontend_CalDAV_SpeedUpPropfindPlugin();
+
+        $this->server->addPlugin($this->plugin);
+
+        $this->response = new Sabre\HTTP\ResponseMock();
+        $this->server->httpResponse = $this->response;
+    }
+
+    /**
+     * test getPluginName method
+     */
+    public function testGetPluginName()
+    {
+        $pluginName = $this->plugin->getPluginName();
+
+        $this->assertEquals('speedUpPropfindPlugin', $pluginName);
+    }
+
+    /**
+     * test testGetProperties method
+     */
+    public function testGetProperties()
+    {
+        $event = $this->calDAVTests->testCreateRepeatingEventAndPutExdate();
+
+        $body = '<?xml version="1.0" encoding="utf-8"?>
+                 <propfind xmlns="DAV:">
+                    <prop>
+                        <getcontenttype/>
+                        <getetag/>
+                    </prop>
+                 </propfind>';
+
+        $request = new Sabre\HTTP\Request(array(
+            'REQUEST_METHOD' => 'PROPFIND',
+            'REQUEST_URI'    => '/calendars/' . Tinebase_Core::getUser()->contact_id . '/' . $event->getRecord()->container_id,
+            'HTTP_DEPTH'     => '1',
+        ));
+        $request->setBody($body);
+
+        $this->server->httpRequest = $request;
+        $this->server->exec();
+        //var_dump($this->response->body);
+        $this->assertEquals('HTTP/1.1 207 Multi-Status', $this->response->status);
+
+        $responseDoc = new DOMDocument();
+        $responseDoc->loadXML($this->response->body);
+        //echo $this->response->body;
+        //$responseDoc->formatOutput = true; echo $responseDoc->saveXML();
+        /*$xpath = new DomXPath($responseDoc);
+        $xpath->registerNamespace('cal', 'urn:ietf:params:xml:ns:caldav');
+
+        $nodes = $xpath->query('//d:multistatus/d:response/d:propstat/d:prop/cal:default-alarm-vevent-datetime');
+        $this->assertEquals(1, $nodes->length, $responseDoc->saveXML());
+        $this->assertNotEmpty($nodes->item(0)->nodeValue, $responseDoc->saveXML());
+
+        $nodes = $xpath->query('//d:multistatus/d:response/d:propstat/d:prop/cal:default-alarm-vevent-date');
+        $this->assertEquals(1, $nodes->length, $responseDoc->saveXML());
+        $this->assertNotEmpty($nodes->item(0)->nodeValue, $responseDoc->saveXML());
+
+        $nodes = $xpath->query('//d:multistatus/d:response/d:propstat/d:prop/cal:default-alarm-vtodo-datetime');
+        $this->assertEquals(1, $nodes->length, $responseDoc->saveXML());
+        $this->assertNotEmpty($nodes->item(0)->nodeValue, $responseDoc->saveXML());
+
+        $nodes = $xpath->query('//d:multistatus/d:response/d:propstat/d:prop/cal:default-alarm-vtodo-date');
+        $this->assertEquals(1, $nodes->length, $responseDoc->saveXML());
+        $this->assertNotEmpty($nodes->item(0)->nodeValue, $responseDoc->saveXML());*/
+    }
+}
index 050c152..35f83e8 100644 (file)
@@ -273,7 +273,7 @@ class Calendar_Frontend_WebDAV_ContainerTest extends PHPUnit_Framework_TestCase
             'time-range'     => null
         ));
         
-        $this->assertContains($event->getId(), $urls);
+        $this->assertContains($event->getId() . '.ics', $urls);
     }
     
     /**
@@ -342,8 +342,8 @@ class Calendar_Frontend_WebDAV_ContainerTest extends PHPUnit_Framework_TestCase
             'time-range'     => null
         ));
         
-        $this->assertContains($event1->getId(), $urls);
-        $this->assertContains($event2->getId(), $urls);
+        $this->assertContains($event1->getId() . '.ics', $urls);
+        $this->assertContains($event2->getId() . '.ics', $urls);
         $this->assertEquals(2, count($urls));
     }
     /**
index 45bd18e..045a5a5 100644 (file)
@@ -118,7 +118,7 @@ class Tinebase_WebDav_Plugin_SyncTokenTest extends Tinebase_WebDav_Plugin_Abstra
         $request = new Sabre\HTTP\Request(array(
             'REQUEST_METHOD' => 'PROPFIND',
             'REQUEST_URI'    => '/calendars/' . Tinebase_Core::getUser()->contact_id . '/' . $this->objects['initialContainer']->id,
-            'HTTP_DEPTH'     => '1',
+            'HTTP_DEPTH'     => '0',
         ));
         $request->setBody($body);
 
index 4d843ab..1923892 100644 (file)
@@ -947,10 +947,11 @@ class Calendar_Backend_Sql extends Tinebase_Backend_Sql_Abstract
      */
     public function getUidOfBaseEvents(array $eventIds)
     {
-        if (count($eventIds) == 0) {
+        if (count($eventIds) === 0) {
             return array();
         }
 
+        // we might want to return is_deleted = true here! so no condition to filter deleted events!
         $select = $this->_db->select()
             ->from($this->_tablePrefix . $this->_tableName, 'uid')
             ->where($this->_db->quoteIdentifier('id') . ' IN (?) AND ' . $this->_db->quoteIdentifier('recurid') . ' IS NULL', $eventIds);
@@ -959,4 +960,34 @@ class Calendar_Backend_Sql extends Tinebase_Backend_Sql_Abstract
 
         return $stmt->fetchAll(Zend_Db::FETCH_NUM);
     }
+
+    /**
+     * returns the seq of one event
+     *
+     * @param string $eventId
+     * @return string
+     * @throws Tinebase_Exception_NotFound
+     */
+    public function getIdSeq($eventId, $containerId)
+    {
+        $select = $this->_db->select()
+            ->from(array('ev' => $this->_tablePrefix . $this->_tableName), array('id', 'seq'))
+            ->joinLeft(array('at' => $this->_tablePrefix . 'cal_attendee'), 'ev.id = at.cal_event_id', NULL)
+            ->where($this->_db->quoteInto('(' . $this->_db->quoteIdentifier('ev.id') . ' = ? OR ', $eventId) .
+                $this->_db->quoteInto($this->_db->quoteIdentifier('ev.uid') . ' = ? ) AND ev.is_deleted = 0 AND ' .
+                    $this->_db->quoteIdentifier('ev.recurid') . ' IS NULL AND (', $eventId) .
+                $this->_db->quoteIdentifier('ev.container_id') . ' = ? OR ' .
+                $this->_db->quoteInto($this->_db->quoteIdentifier('at.displaycontainer_id') . ' = ? )', $containerId), $containerId);
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . " sql: " . $select->assemble());
+
+        $stmt = $this->_db->query($select);
+
+        if (($row = $stmt->fetch(Zend_Db::FETCH_NUM)) === false) {
+            throw new Tinebase_Exception_NotFound('event not found');
+        }
+
+        return $row;
+    }
 }
diff --git a/tine20/Calendar/Frontend/CalDAV/FixMultiGet404Plugin.php b/tine20/Calendar/Frontend/CalDAV/FixMultiGet404Plugin.php
new file mode 100644 (file)
index 0000000..ce107b3
--- /dev/null
@@ -0,0 +1,228 @@
+<?php
+
+
+class Calendar_Frontend_CalDAV_FixMultiGet404Plugin extends Sabre\CalDAV\Plugin
+{
+    protected $_fakeEvent = null;
+    protected $_calBackend = null;
+
+    /**
+     * This function handles the calendar-multiget REPORT.
+     *
+     * This report is used by the client to fetch the content of a series
+     * of urls. Effectively avoiding a lot of redundant requests.
+     *
+     * @param DOMNode $dom
+     * @return void
+     */
+    public function calendarMultiGetReport($dom) {
+
+        $properties = array_keys(Sabre\DAV\XMLUtil::parseProperties($dom->firstChild));
+        $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href');
+
+        $xpath = new \DOMXPath($dom);
+        $xpath->registerNameSpace('cal',self::NS_CALDAV);
+        $xpath->registerNameSpace('dav','urn:DAV');
+
+        $expand = $xpath->query('/cal:calendar-multiget/dav:prop/cal:calendar-data/cal:expand');
+        if ($expand->length>0) {
+            $expandElem = $expand->item(0);
+            $start = $expandElem->getAttribute('start');
+            $end = $expandElem->getAttribute('end');
+            if(!$start || !$end) {
+                throw new Sabre\DAV\Exception\BadRequest('The "start" and "end" attributes are required for the CALDAV:expand element');
+            }
+            $start = Sabre\VObject\DateTimeParser::parseDateTime($start);
+            $end = Sabre\VObject\DateTimeParser::parseDateTime($end);
+
+            if ($end <= $start) {
+                throw new Sabre\DAV\Exception\BadRequest('The end-date must be larger than the start-date in the expand element.');
+            }
+
+            $expand = true;
+
+        } else {
+
+            $expand = false;
+
+        }
+
+        foreach($hrefElems as $elem) {
+            $uri = $this->server->calculateUri($elem->nodeValue);
+            try {
+                list($objProps) = $this->server->getPropertiesForPath($uri, $properties);
+
+                if ($expand && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
+                    $vObject = Sabre\VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
+                    $vObject->expand($start, $end);
+                    $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
+                }
+            } catch (Sabre\DAV\Exception\NotFound $e) {
+
+                try {
+                    if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+                        Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .' returning fake properties for:' . $uri);
+
+                    // return fake events properties
+                    $node = $this->_getFakeEventFacade($uri);
+                    $objProps = $this->_getFakeProperties($uri, $node, $properties);
+
+                } catch (Tinebase_Exception_NotFound $tenf) {
+                    $objProps = array($uri => 404);
+                }
+            }
+
+            $propertyList[]=$objProps;
+
+        }
+
+        $prefer = $this->server->getHTTPPRefer();
+
+        $this->server->httpResponse->sendStatus(207);
+        $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+        $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
+        $this->server->httpResponse->sendBody($this->generateMultiStatus($propertyList, $prefer['return-minimal']));
+
+    }
+
+    /**
+     * @param string $path
+     * @param Calendar_Frontend_WebDAV_Event $node
+     * @param array $properties
+     * @return array
+     */
+    protected function _getFakeProperties($path, $node, $properties)
+    {
+        $newProperties = array();
+        $newProperties['href'] = trim($path,'/');
+
+        if (count($properties) === 0) {
+            // Default list of propertyNames, when all properties were requested.
+            $properties = array(
+                '{DAV:}getlastmodified',
+                '{DAV:}getcontentlength',
+                '{DAV:}resourcetype',
+                '{DAV:}quota-used-bytes',
+                '{DAV:}quota-available-bytes',
+                '{DAV:}getetag',
+                '{DAV:}getcontenttype',
+            );
+        }
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .' requested fake properties:' . print_r($properties, true));
+
+        foreach ($properties as $prop) {
+            switch($prop) {
+                case '{DAV:}getetag'               : if ($node instanceof Sabre\DAV\IFile && $etag = $node->getETag())  $newProperties[200][$prop] = $etag; break;
+                case '{DAV:}getcontenttype'        : if ($node instanceof Sabre\DAV\IFile && $ct = $node->getContentType())  $newProperties[200][$prop] = $ct; break;
+                /** @noinspection PhpMissingBreakStatementInspection */
+                case '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-data':
+                                                     if ($node instanceof Sabre\CalDAV\ICalendarObject) {
+                                                         $val = $node->get();
+                                                         if (is_resource($val))
+                                                             $val = stream_get_contents($val);
+                                                         $newProperties[200][$prop] = str_replace("\r","", $val);
+                                                         break;
+                                                     }
+                                                     // don't break here!
+                /** DO NOT ADD A CASE HERE, WE FALL THROUGH IN THE ABOVE CASE! */
+                default:
+                    $newProperties[404][$prop] = null;
+                    break;
+            }
+        }
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .' returning fake properties:' . print_r($newProperties, true));
+
+        return $newProperties;
+    }
+
+    /**
+     * @param string $path
+     * @return Calendar_Frontend_WebDAV_Event
+     * @throws Tinebase_Exception_NotFound
+     */
+    protected function _getFakeEventFacade($path)
+    {
+        $path = rtrim($path,'/');
+        $parentPath = explode('/', $path);
+
+        $id = array_pop($parentPath);
+        if (($icsPos = stripos($id, '.ics')) !== false) {
+            $id = substr($id, 0, $icsPos);
+        }
+
+        $parentPath = join('/', $parentPath);
+        $parentNode = $this->server->tree->getNodeForPath($parentPath);
+
+        if (null === $this->_fakeEvent) {
+            $this->_fakeEvent = new Calendar_Model_Event(
+                array(
+                    'originator_tz'     => 'UTC',
+                    'creation_time'     => '1976-06-06 06:06:06',
+                    'dtstart'           => '1977-07-07 07:07:07',
+                    'dtend'             => '1977-07-07 07:14:07',
+                    'summary'           => '-',
+                ), true);
+
+            $this->_calBackend = new Calendar_Backend_Sql(Tinebase_Core::getDb());
+        }
+
+        list($id, $seq) = $this->_calBackend->getIdSeq($id, $parentNode->getId());
+        $this->_fakeEvent->setId($id);
+        $this->_fakeEvent->seq = $seq;
+
+        return new Calendar_Frontend_WebDAV_Event($parentNode->getContainer(), $this->_fakeEvent);
+    }
+
+    /**
+     * Generates a WebDAV propfind response body based on a list of nodes.
+     *
+     * If 'strip404s' is set to true, all 404 responses will be removed.
+     *
+     * @param array $fileProperties The list with nodes
+     * @param bool strip404s
+     * @return string
+     */
+    public function generateMultiStatus(array $fileProperties, $strip404s = false) {
+
+        $dom = new DOMDocument('1.0','utf-8');
+        //$dom->formatOutput = true;
+        $multiStatus = $dom->createElement('d:multistatus');
+        $dom->appendChild($multiStatus);
+
+        // Adding in default namespaces
+        foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
+
+            $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
+
+        }
+
+        foreach($fileProperties as $entry) {
+
+            if (isset($entry[200])) {
+                $href = $entry['href'];
+                unset($entry['href']);
+
+                if ($strip404s && isset($entry[404])) {
+                    unset($entry[404]);
+                }
+            } else {
+                if ($strip404s) {
+                    continue;
+                }
+                list($href) = array_keys($entry);
+            }
+
+            $response = new Calendar_Frontend_CalDAV_PropertyResponse($href,$entry);
+            $response->serialize($this->server,$multiStatus);
+
+        }
+
+        return $dom->saveXML();
+
+    }
+
+}
\ No newline at end of file
diff --git a/tine20/Calendar/Frontend/CalDAV/PropertyResponse.php b/tine20/Calendar/Frontend/CalDAV/PropertyResponse.php
new file mode 100644 (file)
index 0000000..1d2f19d
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+
+
+class Calendar_Frontend_CalDAV_PropertyResponse implements Sabre\DAV\Property\IHref
+{
+    /**
+     * Url for the response
+     *
+     * @var string
+     */
+    private $href;
+
+    /**
+     * Propertylist, ordered by HTTP status code
+     *
+     * @var array
+     */
+    private $responseProperties;
+
+    /**
+     * The responseProperties argument is a list of properties
+     * within an array with keys representing HTTP status codes
+     *
+     * @param string $href
+     * @param array $responseProperties
+     */
+    public function __construct($href, array $responseProperties) {
+
+        $this->href = $href;
+        $this->responseProperties = $responseProperties;
+
+    }
+
+    /**
+     * Returns the url
+     *
+     * @return string
+     */
+    public function getHref() {
+
+        return $this->href;
+
+    }
+
+    /**
+     * Returns the property list
+     *
+     * @return array
+     */
+    public function getResponseProperties() {
+
+        return $this->responseProperties;
+
+    }
+
+    /**
+     * serialize
+     *
+     * @param Sabre\DAV\Server $server
+     * @param \DOMElement $dom
+     * @return void
+     */
+    public function serialize(Sabre\DAV\Server $server, DOMElement $dom) {
+
+        $document = $dom->ownerDocument;
+        $properties = $this->responseProperties;
+
+        $xresponse = $document->createElement('d:response');
+        $dom->appendChild($xresponse);
+
+        $uri = Sabre\DAV\URLUtil::encodePath($this->href);
+
+        // Adding the baseurl to the beginning of the url
+        $uri = $server->getBaseUri() . $uri;
+
+        $xresponse->appendChild($document->createElement('d:href',$uri));
+
+        if ( !isset($properties[200]) && isset($properties[$uri]) ) {
+            $xresponse->appendChild($document->createElement('d:status',$server->httpResponse->getStatusMessage($properties[$uri])));
+            return;
+        }
+
+        // The properties variable is an array containing properties, grouped by
+        // HTTP status
+        foreach($properties as $httpStatus=>$propertyGroup) {
+
+            // The 'href' is also in this array, and it's special cased.
+            // We will ignore it
+            if ($httpStatus=='href') continue;
+
+            if (!is_array($propertyGroup)) {
+                if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
+                    Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .' $propertyGroup is no array: ' . print_r($propertyGroup, true));
+                continue;
+            }
+
+            // If there are no properties in this group, we can also just carry on
+            if (!count($propertyGroup)) continue;
+
+            $xpropstat = $document->createElement('d:propstat');
+            $xresponse->appendChild($xpropstat);
+
+            $xprop = $document->createElement('d:prop');
+            $xpropstat->appendChild($xprop);
+
+            $nsList = $server->xmlNamespaces;
+
+            foreach($propertyGroup as $propertyName=>$propertyValue) {
+
+                $propName = null;
+                preg_match('/^{([^}]*)}(.*)$/',$propertyName,$propName);
+
+                // special case for empty namespaces
+                if ($propName[1]=='') {
+
+                    $currentProperty = $document->createElement($propName[2]);
+                    $xprop->appendChild($currentProperty);
+                    $currentProperty->setAttribute('xmlns','');
+
+                } else {
+
+                    if (!isset($nsList[$propName[1]])) {
+                        $nsList[$propName[1]] = 'x' . count($nsList);
+                    }
+
+                    // If the namespace was defined in the top-level xml namespaces, it means
+                    // there was already a namespace declaration, and we don't have to worry about it.
+                    if (isset($server->xmlNamespaces[$propName[1]])) {
+                        $currentProperty = $document->createElement($nsList[$propName[1]] . ':' . $propName[2]);
+                    } else {
+                        $currentProperty = $document->createElementNS($propName[1],$nsList[$propName[1]].':' . $propName[2]);
+                    }
+                    $xprop->appendChild($currentProperty);
+
+                }
+
+                if (is_scalar($propertyValue)) {
+                    $text = $document->createTextNode($propertyValue);
+                    $currentProperty->appendChild($text);
+                } elseif ($propertyValue instanceof Sabre\DAV\PropertyInterface) {
+                    $propertyValue->serialize($server,$currentProperty);
+                } elseif (!is_null($propertyValue)) {
+                    throw new Sabre\DAV\Exception('Unknown property value type: ' . gettype($propertyValue) . ' for property: ' . $propertyName);
+                }
+
+            }
+
+            $xpropstat->appendChild($document->createElement('d:status',$server->httpResponse->getStatusMessage($httpStatus)));
+
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/tine20/Calendar/Frontend/CalDAV/SpeedUpPropfindPlugin.php b/tine20/Calendar/Frontend/CalDAV/SpeedUpPropfindPlugin.php
new file mode 100644 (file)
index 0000000..4a32c0b
--- /dev/null
@@ -0,0 +1,219 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Calendar
+ * @subpackage  Frontend
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Paul Mehrer <p.mehrer@metaways.de>
+ * @copyright   Copyright (c) 2015-2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ *
+ */
+
+/**
+ * Sabre speedup plugin for propfind
+ *
+ * This plugin checks if all properties requested by propfind can be served with one single query.
+ *
+ * @package     Calendar
+ * @subpackage  Frontend
+ */
+
+class Calendar_Frontend_CalDAV_SpeedUpPropfindPlugin extends Sabre\DAV\ServerPlugin
+{
+    /**
+     * Reference to server object
+     *
+     * @var Sabre\DAV\Server
+     */
+    private $server;
+
+    /**
+     * 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 'speedUpPropfindPlugin';
+    }
+
+    /**
+     * Initializes the plugin
+     *
+     * @param Sabre\DAV\Server $server
+     * @return void
+     */
+    public function initialize(Sabre\DAV\Server $server)
+    {
+        $this->server = $server;
+
+        $self = $this;
+        $server->subscribeEvent('beforeMethod', function($method, $uri) use ($self) {
+            if ('PROPFIND' === $method)
+                return $self->propfind($uri);
+            elseif ('REPORT' === $method)
+                return $self->report($uri);
+            else
+                return true;
+        });
+    }
+
+    /**
+     * This functions handles REPORT requests specific to CalDAV
+     *
+     * @param string $uri
+     * @return bool
+     */
+    public function report($uri)
+    {
+        if ($this->server->httpRequest->getHeader('Depth') !== '1') {
+            return true;
+        }
+
+        $body = $this->server->httpRequest->getBody(true);
+        rewind($this->server->httpRequest->getBody());
+        $dom = Sabre\DAV\XMLUtil::loadDOMDocument($body);
+
+        $reportName = Sabre\DAV\XMLUtil::toClarkNotation($dom->firstChild);
+
+        if(strpos($reportName, 'calendar-query') !== false) {
+
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                . " in report speedup");
+
+            $properties = array_keys(\Sabre\DAV\XMLUtil::parseProperties($dom->firstChild));
+            if (count($properties) != 2 || !in_array('{DAV:}getetag', $properties) || !in_array('{DAV:}getcontenttype',$properties)) {
+
+                if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                    . " requested properties dont match speedup conditions, continuing");
+
+                return true;
+            }
+
+            $filter = $dom->getElementsByTagNameNS('urn:ietf:params:xml:ns:caldav','filter');
+            if ($filter->length != 1 || $filter->item(0)->childNodes->length != 1 ||
+                $filter->item(0)->childNodes->item(0)->getAttribute('name') !== 'VCALENDAR' ||
+                $filter->item(0)->childNodes->item(0)->childNodes->length != 1 ||
+                $filter->item(0)->childNodes->item(0)->childNodes->item(0)->getAttribute('name') !== 'VTODO' ||
+                $filter->item(0)->childNodes->item(0)->childNodes->item(0)->hasChildNodes()) {
+
+                if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                    . " requested properties dont match speedup conditions, continuing");
+
+                return true;
+            }
+
+
+            return $this->_speedUpRequest($uri);
+        }
+        return true;
+    }
+
+    /**
+     * This functions handles PROPFIND requests specific to CalDAV
+     * 
+     * @param string $uri
+     * @return bool
+     */
+    public function propfind($uri)
+    {
+        if ($this->server->httpRequest->getHeader('Depth') !== '1') {
+            return true;
+        }
+
+        $body = $this->server->httpRequest->getBody(true);
+        rewind($this->server->httpRequest->getBody());
+        $dom = Sabre\DAV\XMLUtil::loadDOMDocument($body);
+
+        $reportName = Sabre\DAV\XMLUtil::toClarkNotation($dom->firstChild);
+
+        if($reportName === '{DAV:}propfind') {
+
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                . " in propfind speedup");
+
+            $properties = array_keys(\Sabre\DAV\XMLUtil::parseProperties($dom->firstChild));
+            if (count($properties) != 2 || !in_array('{DAV:}getetag', $properties) || !in_array('{DAV:}getcontenttype',$properties)) {
+
+                if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                    . " requested properties dont match speedup conditions, continuing");
+
+                return true;
+            }
+
+
+            return $this->_speedUpRequest($uri);
+        }
+        return true;
+    }
+
+    /**
+     * @param string $uri
+     * @return bool
+     */
+    protected function _speedUpRequest($uri)
+    {
+        /**
+         * @var Calendar_Frontend_WebDAV_Container
+         */
+        $node = $this->server->tree->getNodeForPath($uri);
+        if (!($node instanceof Calendar_Frontend_WebDAV_Container) ) {
+            return true;
+        }
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . " speedup sql start");
+
+        $db = Tinebase_Core::getDb();
+
+        $stmt = $db->query('SELECT ev.id, SHA1(CONCAT(ev.id, ev.seq)) AS etag FROM tine20_cal_events AS ev WHERE ev.is_deleted = 0 AND ' .
+            'ev.recurid IS NULL AND (ev.container_id = ' . $db->quote($node->getId()) . ' OR ev.id IN (
+            SELECT cal_event_id FROM tine20_cal_attendee WHERE displaycontainer_id = ' . $db->quote($node->getId()) . ')) GROUP BY ev.id');
+
+        $result = $stmt->fetchAll();
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . " speedup sql done");
+
+        $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);
+        }
+
+        /*$response = $dom->createElement('d:response');
+        $href = $dom->createElement('d:href', $uri);
+        $response->appendChild($href);
+        $multiStatus->appendChild($response);*/
+
+        foreach ($result as $row) {
+            $a = array();
+            $a[200] = array(
+                '{DAV:}getetag' => '"' . $row['etag'] . '"',
+                '{DAV:}getcontenttype' => 'text/calendar',
+            );
+            $href = $uri . '/' . $row['id'] . '.ics';
+            $response = new Sabre\DAV\Property\Response($href, $a);
+            $response->serialize($this->server, $multiStatus);
+        }
+
+        $dom->appendChild($multiStatus);
+
+        $this->server->httpResponse->sendStatus(207);
+        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+        $this->server->httpResponse->sendBody($dom->saveXML());
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . " speedup successfully responded to request");
+
+        return false;
+    }
+}
\ No newline at end of file
index eb29651..d920c42 100644 (file)
@@ -122,14 +122,6 @@ class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstr
                     'field'     => 'container_id',
                     'operator'  => 'equals',
                     'value'     => $this->_container->getId()
-                ),
-                array(
-                    'field'    => 'period',
-                    'operator'  => 'within',
-                    'value'     => array(
-                        'from'  => Tinebase_DateTime::now()->subMonth($this->_getMaxPeriodFrom()),
-                        'until' => Tinebase_DateTime::now()->addYear(4)
-                    )
                 )
             ));
 
@@ -162,7 +154,7 @@ class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstr
         $children = array();
         
         foreach ($objects as $object) {
-            $children[$object->getId()] = $this->getChild($object);
+            $children[$object->getId() . $this->_suffix] = $this->getChild($object);
         }
         
         return $children;
@@ -214,7 +206,8 @@ class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstr
         
         return $response;
     }
-    
+
+
     protected function _getController()
     {
         if ($this->_controller === null) {
@@ -301,24 +294,26 @@ class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstr
             }
         }
 
-        // @see 0009162: CalDAV Performance issues for many events
-        // create default time-range end in 4 years from now and 2 months back (configurable) if no filter was set by client
-        if ($periodFrom === null) {
-            $periodFrom = Tinebase_DateTime::now()->subMonth($this->_getMaxPeriodFrom());
-        }
-        if ($periodUntil === null) {
-            $periodUntil = Tinebase_DateTime::now()->addYear(4);
+        if ($periodFrom !== null || $periodUntil !== null) {
+            // @see 0009162: CalDAV Performance issues for many events
+            // create default time-range end in 4 years from now and 2 months back (configurable) if no filter was set by client
+            if ($periodFrom === null) {
+                $periodFrom = Tinebase_DateTime::now()->subMonth($this->_getMaxPeriodFrom());
+            }
+            if ($periodUntil === null) {
+                $periodUntil = Tinebase_DateTime::now()->addYear(1000);
+            }
+
+            $filterArray[] = array(
+                'field' => 'period',
+                'operator' => 'within',
+                'value' => array(
+                    'from' => $periodFrom,
+                    'until' => $periodUntil
+                )
+            );
         }
         
-        $filterArray[] = array(
-            'field' => 'period',
-            'operator' => 'within',
-            'value' => array(
-                'from'  => $periodFrom,
-                'until' => $periodUntil
-            )
-        );
-        
         $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
         $filter = new $filterClass($filterArray);
     
@@ -328,17 +323,13 @@ class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstr
     }
     
     /**
-     * get max period (from) in months (default: 2)
+     * get max period (from) in months (default: 12000)
      * 
      * @return integer
      */
     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, 12000);
     }
     
     /**
index 5946b3d..7e1dff4 100644 (file)
@@ -570,8 +570,9 @@ class Calendar_Frontend_WebDAV_Event extends Sabre\DAV\File implements Sabre\Cal
         if (! $this->_event instanceof Calendar_Model_Event) {
             Calendar_Controller_MSEventFacade::getInstance()->assertEventFacadeParams($this->_container);
             $this->_event = Calendar_Controller_MSEventFacade::getInstance()->get($this->_event);
-            
-            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . print_r($this->_event->toArray(), true));
+
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+                Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . print_r($this->_event->toArray(), true));
         }
 
         return $this->_event;
index e6d2bba..334b974 100644 (file)
@@ -70,13 +70,14 @@ class Tinebase_Server_WebDAV extends Tinebase_Server_Abstract implements Tinebas
             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .' requestUri:' . $this->_request->getRequestUri());
         
         self::$_server = new \Sabre\DAV\Server(new Tinebase_WebDav_Root());
+        \Sabre\DAV\Server::$exposeVersion = false;
         
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
             self::$_server->debugExceptions = true;
             $contentType = self::$_server->httpRequest->getHeader('Content-Type');
             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " requestContentType: " . $contentType);
             
-            if (preg_match('/^text/', $contentType)) {
+            if (stripos($contentType, 'text') === 0 || stripos($contentType, '/xml') !== false) {
                 // NOTE inputstream can not be rewinded
                 $debugStream = fopen('php://temp','r+');
                 stream_copy_to_stream($this->_body, $debugStream);
@@ -124,7 +125,7 @@ class Tinebase_Server_WebDAV extends Tinebase_Server_Abstract implements Tinebas
         
         self::$_server->addPlugin(new \Sabre\CardDAV\Plugin());
         self::$_server->addPlugin(new Calendar_Frontend_CalDAV_SpeedUpPlugin); // this plugin must be loaded before CalDAV plugin
-        self::$_server->addPlugin(new \Sabre\CalDAV\Plugin());
+        self::$_server->addPlugin(new Calendar_Frontend_CalDAV_FixMultiGet404Plugin()); // replacement for new \Sabre\CalDAV\Plugin());
         self::$_server->addPlugin(new \Sabre\CalDAV\SharingPlugin());
         self::$_server->addPlugin(new Calendar_Frontend_CalDAV_PluginAutoSchedule());
         self::$_server->addPlugin(new Calendar_Frontend_CalDAV_PluginDefaultAlarms());
@@ -136,9 +137,10 @@ class Tinebase_Server_WebDAV extends Tinebase_Server_Abstract implements Tinebas
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_ExpandedPropertiesReport());
         self::$_server->addPlugin(new \Sabre\DAV\Browser\Plugin());
         self::$_server->addPlugin(new Tinebase_WebDav_Plugin_SyncToken());
+        self::$_server->addPlugin(new Calendar_Frontend_CalDAV_SpeedUpPropfindPlugin());
 
         $contentType = self::$_server->httpRequest->getHeader('Content-Type');
-        $logOutput = Tinebase_Core::isLogLevel(Zend_Log::DEBUG) && preg_match('/^text/', $contentType);
+        $logOutput = Tinebase_Core::isLogLevel(Zend_Log::DEBUG) && (stripos($contentType, 'text') === 0 || stripos($contentType, '/xml') !== false);
 
         if ($logOutput) {
             ob_start();
index 18a6dff..3d43b1f 100644 (file)
@@ -301,7 +301,17 @@ abstract class Tinebase_WebDav_Container_Abstract extends \Sabre\DAV\Collection
         
         return Tinebase_DateTime::now()->getTimestamp();
     }
-    
+
+    /**
+     * Returns the id of the node
+     *
+     * @return string
+     */
+    public function getId()
+    {
+        return $this->_container->getId();
+    }
+
     /**
      * Returns the name of the node
      *
@@ -594,4 +604,12 @@ abstract class Tinebase_WebDav_Container_Abstract extends \Sabre\DAV\Collection
 
         return $result;
     }
+
+    /**
+     * @return Tinebase_Model_Container
+     */
+    public function getContainer()
+    {
+        return $this->_container;
+    }
 }