1<?php
2
3namespace Sabre\DAV\Sync;
4
5use Sabre\DAV;
6use Sabre\DAV\Xml\Request\SyncCollectionReport;
7use Sabre\HTTP\RequestInterface;
8
9/**
10 * This plugin all WebDAV-sync capabilities to the Server.
11 *
12 * WebDAV-sync is defined by rfc6578
13 *
14 * The sync capabilities only work with collections that implement
15 * Sabre\DAV\Sync\ISyncCollection.
16 *
17 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
18 * @author Evert Pot (http://evertpot.com/)
19 * @license http://sabre.io/license/ Modified BSD License
20 */
21class Plugin extends DAV\ServerPlugin {
22
23    /**
24     * Reference to server object
25     *
26     * @var DAV\Server
27     */
28    protected $server;
29
30    const SYNCTOKEN_PREFIX = 'http://sabre.io/ns/sync/';
31
32    /**
33     * Returns a plugin name.
34     *
35     * Using this name other plugins will be able to access other plugins
36     * using \Sabre\DAV\Server::getPlugin
37     *
38     * @return string
39     */
40    function getPluginName() {
41
42        return 'sync';
43
44    }
45
46    /**
47     * Initializes the plugin.
48     *
49     * This is when the plugin registers it's hooks.
50     *
51     * @param DAV\Server $server
52     * @return void
53     */
54    function initialize(DAV\Server $server) {
55
56        $this->server = $server;
57        $server->xml->elementMap['{DAV:}sync-collection'] = 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport';
58
59        $self = $this;
60
61        $server->on('report', function($reportName, $dom, $uri) use ($self) {
62
63            if ($reportName === '{DAV:}sync-collection') {
64                $this->server->transactionType = 'report-sync-collection';
65                $self->syncCollection($uri, $dom);
66                return false;
67            }
68
69        });
70
71        $server->on('propFind',       [$this, 'propFind']);
72        $server->on('validateTokens', [$this, 'validateTokens']);
73
74    }
75
76    /**
77     * Returns a list of reports this plugin supports.
78     *
79     * This will be used in the {DAV:}supported-report-set property.
80     * Note that you still need to subscribe to the 'report' event to actually
81     * implement them
82     *
83     * @param string $uri
84     * @return array
85     */
86    function getSupportedReportSet($uri) {
87
88        $node = $this->server->tree->getNodeForPath($uri);
89        if ($node instanceof ISyncCollection && $node->getSyncToken()) {
90            return [
91                '{DAV:}sync-collection',
92            ];
93        }
94
95        return [];
96
97    }
98
99
100    /**
101     * This method handles the {DAV:}sync-collection HTTP REPORT.
102     *
103     * @param string $uri
104     * @param SyncCollectionReport $report
105     * @return void
106     */
107    function syncCollection($uri, SyncCollectionReport $report) {
108
109        // Getting the data
110        $node = $this->server->tree->getNodeForPath($uri);
111        if (!$node instanceof ISyncCollection) {
112            throw new DAV\Exception\ReportNotSupported('The {DAV:}sync-collection REPORT is not supported on this url.');
113        }
114        $token = $node->getSyncToken();
115        if (!$token) {
116            throw new DAV\Exception\ReportNotSupported('No sync information is available at this node');
117        }
118
119        $syncToken = $report->syncToken;
120        if (!is_null($syncToken)) {
121            // Sync-token must start with our prefix
122            if (substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX) {
123                throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token');
124            }
125
126            $syncToken = substr($syncToken, strlen(self::SYNCTOKEN_PREFIX));
127
128        }
129        $changeInfo = $node->getChanges($syncToken, $report->syncLevel, $report->limit);
130
131        if (is_null($changeInfo)) {
132
133            throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token');
134
135        }
136
137        // Encoding the response
138        $this->sendSyncCollectionResponse(
139            $changeInfo['syncToken'],
140            $uri,
141            $changeInfo['added'],
142            $changeInfo['modified'],
143            $changeInfo['deleted'],
144            $report->properties
145        );
146
147    }
148
149    /**
150     * Sends the response to a sync-collection request.
151     *
152     * @param string $syncToken
153     * @param string $collectionUrl
154     * @param array $added
155     * @param array $modified
156     * @param array $deleted
157     * @param array $properties
158     * @return void
159     */
160    protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties) {
161
162
163        $fullPaths = [];
164
165        // Pre-fetching children, if this is possible.
166        foreach (array_merge($added, $modified) as $item) {
167            $fullPath = $collectionUrl . '/' . $item;
168            $fullPaths[] = $fullPath;
169        }
170
171        $responses = [];
172        foreach ($this->server->getPropertiesForMultiplePaths($fullPaths, $properties) as $fullPath => $props) {
173
174            // The 'Property_Response' class is responsible for generating a
175            // single {DAV:}response xml element.
176            $responses[] = new DAV\Xml\Element\Response($fullPath, $props);
177
178        }
179
180
181
182        // Deleted items also show up as 'responses'. They have no properties,
183        // and a single {DAV:}status element set as 'HTTP/1.1 404 Not Found'.
184        foreach ($deleted as $item) {
185
186            $fullPath = $collectionUrl . '/' . $item;
187            $responses[] = new DAV\Xml\Element\Response($fullPath, [], 404);
188
189        }
190        $multiStatus = new DAV\Xml\Response\MultiStatus($responses, self::SYNCTOKEN_PREFIX . $syncToken);
191
192        $this->server->httpResponse->setStatus(207);
193        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
194        $this->server->httpResponse->setBody(
195            $this->server->xml->write('{DAV:}multistatus', $multiStatus, $this->server->getBaseUri())
196        );
197
198    }
199
200    /**
201     * This method is triggered whenever properties are requested for a node.
202     * We intercept this to see if we must return a {DAV:}sync-token.
203     *
204     * @param DAV\PropFind $propFind
205     * @param DAV\INode $node
206     * @return void
207     */
208    function propFind(DAV\PropFind $propFind, DAV\INode $node) {
209
210        $propFind->handle('{DAV:}sync-token', function() use ($node) {
211            if (!$node instanceof ISyncCollection || !$token = $node->getSyncToken()) {
212                return;
213            }
214            return self::SYNCTOKEN_PREFIX . $token;
215        });
216
217    }
218
219    /**
220     * The validateTokens event is triggered before every request.
221     *
222     * It's a moment where this plugin can check all the supplied lock tokens
223     * in the If: header, and check if they are valid.
224     *
225     * @param RequestInterface $request
226     * @param array $conditions
227     * @return void
228     */
229    function validateTokens(RequestInterface $request, &$conditions) {
230
231        foreach ($conditions as $kk => $condition) {
232
233            foreach ($condition['tokens'] as $ii => $token) {
234
235                // Sync-tokens must always start with our designated prefix.
236                if (substr($token['token'], 0, strlen(self::SYNCTOKEN_PREFIX)) !== self::SYNCTOKEN_PREFIX) {
237                    continue;
238                }
239
240                // Checking if the token is a match.
241                $node = $this->server->tree->getNodeForPath($condition['uri']);
242
243                if (
244                    $node instanceof ISyncCollection &&
245                    $node->getSyncToken() == substr($token['token'], strlen(self::SYNCTOKEN_PREFIX))
246                ) {
247                    $conditions[$kk]['tokens'][$ii]['validToken'] = true;
248                }
249
250            }
251
252        }
253
254    }
255
256    /**
257     * Returns a bunch of meta-data about the plugin.
258     *
259     * Providing this information is optional, and is mainly displayed by the
260     * Browser plugin.
261     *
262     * The description key in the returned array may contain html and will not
263     * be sanitized.
264     *
265     * @return array
266     */
267    function getPluginInfo() {
268
269        return [
270            'name'        => $this->getPluginName(),
271            'description' => 'Adds support for WebDAV Collection Sync (rfc6578)',
272            'link'        => 'http://sabre.io/dav/sync/',
273        ];
274
275    }
276
277}
278