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