0011428: support caldav sync token
[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         // Sync-token must start with our prefix
125         if (substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX || strlen($syncToken) <= strlen(self::SYNCTOKEN_PREFIX)) {
126             throw new Sabre\DAV\Exception\BadRequest('Invalid or unknown sync token');
127         }
128         $syncToken = substr($syncToken, strlen(self::SYNCTOKEN_PREFIX));
129
130         // get the list of properties the client requested
131         $properties = array_keys(Sabre\DAV\XMLUtil::parseProperties($report->documentElement));
132
133         // get changes since client sync token
134         $changeInfo = $node->getChanges($syncToken);
135         if (is_null($changeInfo)) {
136             throw new Sabre\DAV\Exception\BadRequest('Invalid or unknown sync token');
137         }
138
139         // Encoding the response
140         $this->sendSyncCollectionResponse(
141             $changeInfo['syncToken'],
142             $uri,
143             $changeInfo[Tinebase_Model_ContainerContent::ACTION_CREATE],
144             $changeInfo[Tinebase_Model_ContainerContent::ACTION_UPDATE],
145             $changeInfo[Tinebase_Model_ContainerContent::ACTION_DELETE],
146             $properties
147         );
148     }
149
150     /**
151      * Sends the response to a sync-collection request.
152      *
153      * @param string $syncToken
154      * @param string $collectionUrl
155      * @param array $added
156      * @param array $modified
157      * @param array $deleted
158      * @param array $properties
159      * @return void
160      */
161     protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties)
162     {
163         $resolvedProperties = array();
164         foreach (array_merge($added, $modified) as $item) {
165             $fullPath = $collectionUrl . '/' . $item;
166             try {
167                 $resolvedProperties[$fullPath] = $this->server->getPropertiesForPath($fullPath, $properties);
168
169                 // in case the user doesnt have access to this
170             } catch (Sabre\DAV\Exception\NotFound $e) {
171                 unset($resolvedProperties[$fullPath]);
172             }
173         }
174         foreach($deleted as $item) {
175             $fullPath = $collectionUrl . '/' . $item;
176             $resolvedProperties[$fullPath] = array();
177         }
178
179         $data = $this->generateMultiStatus($resolvedProperties, $syncToken);
180
181         $this->server->httpResponse->sendStatus(207);
182         $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
183         $this->server->httpResponse->sendBody($data);
184     }
185
186     protected function generateMultiStatus($properties, $syncToken)
187     {
188         $dom = new \DOMDocument('1.0', 'utf-8');
189
190         //$dom->formatOutput = true;
191         $multiStatus = $dom->createElement('d:multistatus');
192
193         // Adding in default namespaces
194         foreach ($this->server->xmlNamespaces as $namespace => $prefix) {
195             $multiStatus->setAttribute('xmlns:' . $prefix, $namespace);
196         }
197
198         foreach ($properties as $href => $entries) {
199             if (count($entries) === 0) { //404
200                 $response = $dom->createElement('d:response');
201                 $href = $dom->createElement('d:href', $href);
202                 $response->appendChild($href);
203                 $status = $dom->createElement('d:status', $this->server->httpResponse->getStatusMessage(404));
204                 $response->appendChild($status);
205                 $multiStatus->appendChild($response);
206             } else {
207                 foreach($entries as $entry) {
208                     $ehref = $entry['href'];
209                     unset($entry['href']);
210
211                     $response = new Sabre\DAV\Property\Response($ehref, $entry);
212                     $response->serialize($this->server, $multiStatus);
213                 }
214             }
215         }
216
217         $multiStatus->appendChild($dom->createElement('d:sync-token', self::SYNCTOKEN_PREFIX . $syncToken));
218         $dom->appendChild($multiStatus);
219
220         return $dom->saveXML();
221     }
222 }