PageRenderTime 29ms CodeModel.GetById 13ms app.highlight 11ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Sabre/DAV/Sync/Plugin.php

https://github.com/KOLANICH/SabreDAV
PHP | 342 lines | 159 code | 73 blank | 110 comment | 22 complexity | e24403490a0e26e5c257652ebf1b7f53 MD5 | raw file
  1<?php
  2
  3namespace Sabre\DAV\Sync;
  4
  5use Sabre\DAV;
  6
  7/**
  8 * This plugin all WebDAV-sync capabilities to the Server.
  9 *
 10 * WebDAV-sync is defined by rfc6578
 11 *
 12 * The sync capabilities only work with collections that implement
 13 * Sabreu\DAV\Sync\ISyncCollection.
 14 *
 15 * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
 16 * @author Evert Pot (http://evertpot.com/)
 17 * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
 18 */
 19class Plugin extends DAV\ServerPlugin {
 20
 21    /**
 22     * Reference to server object
 23     *
 24     * @var DAV\Server
 25     */
 26    protected $server;
 27
 28    const SYNCTOKEN_PREFIX = 'http://sabredav.org/ns/sync/';
 29
 30    /**
 31     * Returns a plugin name.
 32     *
 33     * Using this name other plugins will be able to access other plugins
 34     * using \Sabre\DAV\Server::getPlugin
 35     *
 36     * @return string
 37     */
 38    public function getPluginName() {
 39
 40        return 'sync';
 41
 42    }
 43
 44    /**
 45     * Initializes the plugin.
 46     *
 47     * This is when the plugin registers it's hooks.
 48     *
 49     * @param DAV\Server $server
 50     * @return void
 51     */
 52    public function initialize(DAV\Server $server) {
 53
 54        $this->server = $server;
 55
 56        $self = $this;
 57
 58        $server->subscribeEvent('report', function($reportName, $dom, $uri) use ($self) {
 59
 60            if ($reportName === '{DAV:}sync-collection') {
 61                $this->server->transactionType = 'report-sync-collection';
 62                $self->syncCollection($uri, $dom);
 63                return false;
 64            }
 65
 66        });
 67
 68        $server->subscribeEvent('beforeGetProperties', array($this, 'beforeGetProperties'));
 69        $server->subscribeEvent('validateTokens',      array($this, 'validateTokens'));
 70
 71    }
 72
 73    /**
 74     * Returns a list of reports this plugin supports.
 75     *
 76     * This will be used in the {DAV:}supported-report-set property.
 77     * Note that you still need to subscribe to the 'report' event to actually
 78     * implement them
 79     *
 80     * @param string $uri
 81     * @return array
 82     */
 83    public function getSupportedReportSet($uri) {
 84
 85        $node = $this->server->tree->getNodeForPath($uri);
 86        if ($node instanceof ISyncCollection && $node->getSyncToken()) {
 87            return array(
 88                '{DAV:}sync-collection',
 89            );
 90        }
 91
 92        return array();
 93
 94    }
 95
 96
 97    /**
 98     * This method handles the {DAV:}sync-collection HTTP REPORT.
 99     *
100     * @param string $uri
101     * @param \DOMDocument $dom
102     * @return void
103     */
104    public function syncCollection($uri, \DOMDocument $dom) {
105
106        // rfc3253 specifies 0 is the default value for Depth:
107        $depth = $this->server->getHTTPDepth(0);
108
109        list(
110            $syncToken,
111            $syncLevel,
112            $limit,
113            $properties
114        ) = $this->parseSyncCollectionRequest($dom, $depth);
115
116        // Getting the data
117        $node = $this->server->tree->getNodeForPath($uri);
118        if (!$node instanceof ISyncCollection) {
119            throw new DAV\Exception\ReportNotSupported('The {DAV:}sync-collection REPORT is not supported on this url.');
120        }
121        $token = $node->getSyncToken();
122        if (!$token) {
123            throw new DAV\Exception\ReportNotSupported('No sync information is available at this node');
124        }
125
126        if (!is_null($syncToken)) {
127            // Sync-token must start with our prefix
128            if (substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX) {
129                throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token');
130            }
131
132            $syncToken = substr($syncToken, strlen(self::SYNCTOKEN_PREFIX));
133
134        }
135        $changeInfo = $node->getChanges($syncToken, $syncLevel, $limit);
136
137        if (is_null($changeInfo)) {
138
139            throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token');
140
141        }
142
143        // Encoding the response
144        $this->sendSyncCollectionResponse(
145            $changeInfo['syncToken'],
146            $uri,
147            $changeInfo['added'],
148            $changeInfo['modified'],
149            $changeInfo['deleted'],
150            $properties
151        );
152
153    }
154
155    /**
156     * Parses the {DAV:}sync-collection REPORT request body.
157     *
158     * This method returns an array with 3 values:
159     *   0 - the value of the {DAV:}sync-token element
160     *   1 - the value of the {DAV:}sync-level element
161     *   2 - The value of the {DAV:}limit element
162     *   3 - A list of requested properties
163     *
164     * @param \DOMDocument $dom
165     * @param int $depth
166     * @return void
167     */
168    protected function parseSyncCollectionRequest(\DOMDocument $dom, $depth) {
169
170        $xpath = new \DOMXPath($dom);
171        $xpath->registerNamespace('d','urn:DAV');
172
173        $syncToken = $xpath->query("//d:sync-token");
174        if ($syncToken->length !== 1) {
175            throw new DAV\Exception\BadRequest('You must specify a {DAV:}sync-token element, and it must appear exactly once');
176        }
177        $syncToken = $syncToken->item(0)->nodeValue;
178        // Initial sync
179        if (!$syncToken) $syncToken = null;
180
181        $syncLevel = $xpath->query("//d:sync-level");
182        if ($syncLevel->length === 0) {
183            // In case there was no sync-level, it could mean that we're dealing
184            // with an old client. For these we must use the depth header
185            // instead.
186            $syncLevel = $depth;
187        } else {
188            $syncLevel = $syncLevel->item(0)->nodeValue;
189            if ($syncLevel === 'infinite') {
190                $syncLevel = DAV\Server::DEPTH_INFINITY;
191            }
192
193        }
194        $limit = $xpath->query("//d:limit/d:nresults");
195        if ($limit->length === 0) {
196            $limit = null;
197        } else {
198            $limit = $limit->item(0)->nodeValue;
199        }
200
201        $prop = $xpath->query('d:prop');
202        if ($prop->length !== 1) {
203            throw new DAV\Exception\BadRequest('The {DAV:}sync-collection must contain extactly 1 {DAV:}prop');
204        }
205
206        $properties = array_keys(
207            DAV\XMLUtil::parseProperties($dom->documentElement)
208        );
209
210        return array(
211            $syncToken,
212            $syncLevel,
213            $limit,
214            $properties,
215        );
216
217    }
218
219    /**
220     * Sends the response to a sync-collection request.
221     *
222     * @param string $syncToken
223     * @param string $collectionUrl
224     * @param array $added
225     * @param array $modified
226     * @param array $deleted
227     * @param array $properties
228     * @return void
229     */
230    protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties) {
231
232        $dom = new \DOMDocument('1.0','utf-8');
233        $dom->formatOutput = true;
234        $multiStatus = $dom->createElement('d:multistatus');
235        $dom->appendChild($multiStatus);
236
237        // Adding in default namespaces
238        foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
239
240            $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
241
242        }
243
244        $fullPaths = [];
245
246        // Pre-fetching children, if this is possible.
247        foreach(array_merge($added, $modified) as $item) {
248            $fullPath = $collectionUrl . '/' . $item;
249            $fullPaths[] = $fullPath;
250        }
251
252        foreach($this->server->getPropertiesForMultiplePaths($fullPaths, $properties) as $fullPath => $props) {
253
254            // The 'Property_Response' class is responsible for generating a
255            // single {DAV:}response xml element.
256            $response = new DAV\Property\Response($fullPath, $props);
257            $response->serialize($this->server, $multiStatus);
258
259        }
260
261        // Deleted items also show up as 'responses'. They have no properties,
262        // and a single {DAV:}status element set as 'HTTP/1.1 404 Not Found'.
263        foreach($deleted as $item) {
264
265            $fullPath = $collectionUrl . '/' . $item;
266            $response = new DAV\Property\Response($fullPath, array(), 404);
267            $response->serialize($this->server, $multiStatus);
268
269        }
270
271        $syncToken = $dom->createElement('d:sync-token', self::SYNCTOKEN_PREFIX . $syncToken);
272        $multiStatus->appendChild($syncToken);
273
274        $this->server->httpResponse->sendStatus(207);
275        $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
276        $this->server->httpResponse->sendBody($dom->saveXML());
277
278    }
279
280    /**
281     * This method is triggered whenever properties are requested for a node.
282     * We intercept this to see if we can must return a {DAV:}sync-token.
283     *
284     * @param string $path
285     * @param DAV\INode $node
286     * @param array $requestedProperties
287     * @param array $returnedProperties
288     * @return void
289     */
290    public function beforeGetProperties($path, DAV\INode $node, array &$requestedProperties, array &$returnedProperties) {
291
292        if (!in_array('{DAV:}sync-token', $requestedProperties)) {
293            return;
294        }
295
296        if ($node instanceof ISyncCollection && $token = $node->getSyncToken()) {
297            // Unsetting the property from requested properties.
298            $index = array_search('{DAV:}sync-token', $requestedProperties);
299            unset($requestedProperties[$index]);
300            $returnedProperties[200]['{DAV:}sync-token'] = self::SYNCTOKEN_PREFIX . $token;
301        }
302
303    }
304
305    /**
306     * The validateTokens event is triggered before every request.
307     *
308     * It's a moment where this plugin can check all the supplied lock tokens
309     * in the If: header, and check if they are valid.
310     *
311     * @param mixed $conditions
312     * @return void
313     */
314    public function validateTokens( &$conditions ) {
315
316        foreach($conditions as $kk=>$condition) {
317
318            foreach($condition['tokens'] as $ii=>$token) {
319
320                // Sync-tokens must always start with our designated prefix.
321                if (substr($token['token'], 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX) {
322                    continue;
323                }
324
325                // Checking if the token is a match.
326                $node = $this->server->tree->getNodeForPath($condition['uri']);
327
328                if (
329                    $node instanceof ISyncCollection &&
330                    $node->getSyncToken() == substr($token['token'], strlen(self::SYNCTOKEN_PREFIX))
331                ) {
332                    $conditions[$kk]['tokens'][$ii]['validToken'] = true;
333                }
334
335            }
336
337        }
338
339    }
340
341}
342