0011450: caldav sync with thunderbird stopped working
[tine20] / tine20 / Tinebase / WebDav / Plugin / SyncToken.php
1 <?php
2 /**
3  * WebDAV plugin for sync-token support
4  * 
5  * This plugin provides functionality to request sync-tokens
6  * It is a backport of sabre/dav/sync/plugin.php
7  *
8  * see: https://tools.ietf.org/html/rfc6578
9  * see: http://sabre.io/dav/building-a-caldav-client/#speeding-up-sync-with-webdav-sync
10  *
11  * NOTE: xxx
12  *       xxx
13  *       
14  * @package    Tinebase
15  * @subpackage WebDav
16  * @copyright  Copyright (c) 2015-2015 Metaways Infosystems GmbH (http://www.metaways.de)
17  * @author     Paul Mehrer <p.mehrer@metaways.de>
18  * @license    http://sabre.io/license/ Modified BSD License
19  */
20 class Tinebase_WebDav_Plugin_SyncToken extends \Sabre\DAV\ServerPlugin
21 {
22     /**
23      * Reference to server object
24      *
25      * @var \Sabre\DAV\Server
26      */
27     protected $server;
28
29     const SYNCTOKEN_PREFIX = 'http://tine20.net/ns/sync/';
30
31     /**
32      * Returns a list of features for the DAV: HTTP header. 
33      * 
34      * @return array 
35      */
36     public function getFeatures() 
37     {
38         return array('sync-token');
39     }
40
41     /**
42      * Returns a plugin name.
43      * 
44      * Using this name other plugins will be able to access other plugins
45      * using \Sabre\DAV\Server::getPlugin 
46      * 
47      * @return string 
48      */
49     public function getPluginName() 
50     {
51         return 'calendarSyncToken';
52     }
53
54     /**
55      * Initializes the plugin 
56      * 
57      * @param \Sabre\DAV\Server $server 
58      * @return void
59      */
60     public function initialize(\Sabre\DAV\Server $server) 
61     {
62         $this->server = $server;
63
64         $self = $this;
65         $server->subscribeEvent('report', function($reportName, $dom, $uri) use ($self, $server) {
66             if ($reportName === '{DAV:}sync-collection') {
67                 $server->transactionType = 'report-sync-collection';
68                 $self->syncCollection($uri, $dom);
69                 return false;
70             }
71         });
72     }
73
74     /**
75      * Returns a list of reports this plugin supports.
76      *
77      * This will be used in the {DAV:}supported-report-set property.
78      * Note that you still need to subscribe to the 'report' event to actually
79      * implement them
80      *
81      * @param string $uri
82      * @return array
83      */
84     function getSupportedReportSet($uri)
85     {
86         $node = $this->server->tree->getNodeForPath($uri);
87
88         if ($node instanceof Tinebase_WebDav_Container_Abstract && $node->supportsSyncToken()) {
89             return array(
90                 '{DAV:}sync-collection',
91             );
92         }
93         return array();
94     }
95
96     /**
97      * This method handles the {DAV:}sync-collection HTTP REPORT.
98      *
99      * @param string $uri
100      * @param \DOMDocument $report
101      * @return void
102      */
103     function syncCollection($uri, \DOMDocument $report)
104     {
105         // Getting the sync token of the data requested
106         /**
107          * @var $node Tinebase_WebDav_Container_Abstract
108          */
109         $node = $this->server->tree->getNodeForPath($uri);
110         if (!($node instanceof Tinebase_WebDav_Container_Abstract) || !$node->supportsSyncToken()) {
111             throw new Sabre\DAV\Exception\ReportNotSupported('The {DAV:}sync-collection REPORT is not supported on this url.');
112         }
113         $token = $node->getSyncToken();
114         if (!$token) {
115             throw new Sabre\DAV\Exception\ReportNotSupported('No sync information is available at this node');
116         }
117
118         // getting the sync token send with the request
119         $syncToken = '';
120         $syncTokenList = $report->getElementsByTagNameNS('urn:DAV', 'sync-token');
121         if ($syncTokenList->length == 1) {
122             $syncToken = $syncTokenList->item(0)->textContent; //?!? //nodeValue;
123         }
124         if (strlen($syncToken) > 0 ) {
125             // Sync-token must start with our prefix
126             if (substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX || strlen($syncToken) <= strlen(self::SYNCTOKEN_PREFIX)) {
127                 throw new Sabre\DAV\Exception\BadRequest('Invalid or unknown sync token');
128             }
129             $syncToken = substr($syncToken, strlen(self::SYNCTOKEN_PREFIX));
130         } else {
131             $syncToken = 0;
132         }
133
134         // get the list of properties the client requested
135         $properties = array_keys(Sabre\DAV\XMLUtil::parseProperties($report->documentElement));
136
137         // get changes since client sync token
138         $changeInfo = $node->getChanges($syncToken);
139         if (is_null($changeInfo)) {
140             throw new Sabre\DAV\Exception\BadRequest('Invalid or unknown sync token');
141         }
142
143         // Encoding the response
144         $this->sendSyncCollectionResponse(
145             $changeInfo['syncToken'],
146             $uri,
147             $changeInfo[Tinebase_Model_ContainerContent::ACTION_CREATE],
148             $changeInfo[Tinebase_Model_ContainerContent::ACTION_UPDATE],
149             $changeInfo[Tinebase_Model_ContainerContent::ACTION_DELETE],
150             $properties
151         );
152     }
153
154     /**
155      * Sends the response to a sync-collection request.
156      *
157      * @param string $syncToken
158      * @param string $collectionUrl
159      * @param array $added
160      * @param array $modified
161      * @param array $deleted
162      * @param array $properties
163      * @return void
164      */
165     protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties)
166     {
167         $resolvedProperties = array();
168         foreach (array_merge($added, $modified) as $item) {
169             $fullPath = $collectionUrl . '/' . $item;
170             try {
171                 $resolvedProperties[$fullPath] = $this->server->getPropertiesForPath($fullPath, $properties);
172
173                 // in case the user doesnt have access to this
174             } catch (Sabre\DAV\Exception\NotFound $e) {
175                 unset($resolvedProperties[$fullPath]);
176             }
177         }
178         foreach($deleted as $item) {
179             $fullPath = $collectionUrl . '/' . $item;
180             $resolvedProperties[$fullPath] = array();
181         }
182
183         $data = $this->generateMultiStatus($resolvedProperties, $syncToken);
184
185         $this->server->httpResponse->sendStatus(207);
186         $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
187         $this->server->httpResponse->sendBody($data);
188     }
189
190     protected function generateMultiStatus($properties, $syncToken)
191     {
192         $dom = new \DOMDocument('1.0', 'utf-8');
193
194         //$dom->formatOutput = true;
195         $multiStatus = $dom->createElement('d:multistatus');
196
197         // Adding in default namespaces
198         foreach ($this->server->xmlNamespaces as $namespace => $prefix) {
199             $multiStatus->setAttribute('xmlns:' . $prefix, $namespace);
200         }
201
202         foreach ($properties as $href => $entries) {
203             if (count($entries) === 0) { //404
204                 $response = $dom->createElement('d:response');
205                 $href = $dom->createElement('d:href', $href);
206                 $response->appendChild($href);
207                 $status = $dom->createElement('d:status', $this->server->httpResponse->getStatusMessage(404));
208                 $response->appendChild($status);
209                 $multiStatus->appendChild($response);
210             } else {
211                 foreach($entries as $entry) {
212                     $ehref = $entry['href'];
213                     unset($entry['href']);
214
215                     $response = new Sabre\DAV\Property\Response($ehref, $entry);
216                     $response->serialize($this->server, $multiStatus);
217                 }
218             }
219         }
220
221         $multiStatus->appendChild($dom->createElement('d:sync-token', self::SYNCTOKEN_PREFIX . $syncToken));
222         $dom->appendChild($multiStatus);
223
224         return $dom->saveXML();
225     }
226 }