xref: /plugin/davcal/vendor/sabre/dav/lib/DAV/PartialUpdate/Plugin.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
1*a1a3b679SAndreas Boehler<?php
2*a1a3b679SAndreas Boehler
3*a1a3b679SAndreas Boehlernamespace Sabre\DAV\PartialUpdate;
4*a1a3b679SAndreas Boehler
5*a1a3b679SAndreas Boehleruse Sabre\DAV;
6*a1a3b679SAndreas Boehleruse Sabre\HTTP\RequestInterface;
7*a1a3b679SAndreas Boehleruse Sabre\HTTP\ResponseInterface;
8*a1a3b679SAndreas Boehler
9*a1a3b679SAndreas Boehler/**
10*a1a3b679SAndreas Boehler * Partial update plugin (Patch method)
11*a1a3b679SAndreas Boehler *
12*a1a3b679SAndreas Boehler * This plugin provides a way to modify only part of a target resource
13*a1a3b679SAndreas Boehler * It may bu used to update a file chunk, upload big a file into smaller
14*a1a3b679SAndreas Boehler * chunks or resume an upload.
15*a1a3b679SAndreas Boehler *
16*a1a3b679SAndreas Boehler * $patchPlugin = new \Sabre\DAV\PartialUpdate\Plugin();
17*a1a3b679SAndreas Boehler * $server->addPlugin($patchPlugin);
18*a1a3b679SAndreas Boehler *
19*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
20*a1a3b679SAndreas Boehler * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/)
21*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License
22*a1a3b679SAndreas Boehler */
23*a1a3b679SAndreas Boehlerclass Plugin extends DAV\ServerPlugin {
24*a1a3b679SAndreas Boehler
25*a1a3b679SAndreas Boehler    const RANGE_APPEND = 1;
26*a1a3b679SAndreas Boehler    const RANGE_START = 2;
27*a1a3b679SAndreas Boehler    const RANGE_END = 3;
28*a1a3b679SAndreas Boehler
29*a1a3b679SAndreas Boehler    /**
30*a1a3b679SAndreas Boehler     * Reference to server
31*a1a3b679SAndreas Boehler     *
32*a1a3b679SAndreas Boehler     * @var Sabre\DAV\Server
33*a1a3b679SAndreas Boehler     */
34*a1a3b679SAndreas Boehler    protected $server;
35*a1a3b679SAndreas Boehler
36*a1a3b679SAndreas Boehler    /**
37*a1a3b679SAndreas Boehler     * Initializes the plugin
38*a1a3b679SAndreas Boehler     *
39*a1a3b679SAndreas Boehler     * This method is automatically called by the Server class after addPlugin.
40*a1a3b679SAndreas Boehler     *
41*a1a3b679SAndreas Boehler     * @param DAV\Server $server
42*a1a3b679SAndreas Boehler     * @return void
43*a1a3b679SAndreas Boehler     */
44*a1a3b679SAndreas Boehler    function initialize(DAV\Server $server) {
45*a1a3b679SAndreas Boehler
46*a1a3b679SAndreas Boehler        $this->server = $server;
47*a1a3b679SAndreas Boehler        $server->on('method:PATCH', [$this, 'httpPatch']);
48*a1a3b679SAndreas Boehler
49*a1a3b679SAndreas Boehler    }
50*a1a3b679SAndreas Boehler
51*a1a3b679SAndreas Boehler    /**
52*a1a3b679SAndreas Boehler     * Returns a plugin name.
53*a1a3b679SAndreas Boehler     *
54*a1a3b679SAndreas Boehler     * Using this name other plugins will be able to access other plugins
55*a1a3b679SAndreas Boehler     * using DAV\Server::getPlugin
56*a1a3b679SAndreas Boehler     *
57*a1a3b679SAndreas Boehler     * @return string
58*a1a3b679SAndreas Boehler     */
59*a1a3b679SAndreas Boehler    function getPluginName() {
60*a1a3b679SAndreas Boehler
61*a1a3b679SAndreas Boehler        return 'partialupdate';
62*a1a3b679SAndreas Boehler
63*a1a3b679SAndreas Boehler    }
64*a1a3b679SAndreas Boehler
65*a1a3b679SAndreas Boehler    /**
66*a1a3b679SAndreas Boehler     * Use this method to tell the server this plugin defines additional
67*a1a3b679SAndreas Boehler     * HTTP methods.
68*a1a3b679SAndreas Boehler     *
69*a1a3b679SAndreas Boehler     * This method is passed a uri. It should only return HTTP methods that are
70*a1a3b679SAndreas Boehler     * available for the specified uri.
71*a1a3b679SAndreas Boehler     *
72*a1a3b679SAndreas Boehler     * We claim to support PATCH method (partirl update) if and only if
73*a1a3b679SAndreas Boehler     *     - the node exist
74*a1a3b679SAndreas Boehler     *     - the node implements our partial update interface
75*a1a3b679SAndreas Boehler     *
76*a1a3b679SAndreas Boehler     * @param string $uri
77*a1a3b679SAndreas Boehler     * @return array
78*a1a3b679SAndreas Boehler     */
79*a1a3b679SAndreas Boehler    function getHTTPMethods($uri) {
80*a1a3b679SAndreas Boehler
81*a1a3b679SAndreas Boehler        $tree = $this->server->tree;
82*a1a3b679SAndreas Boehler
83*a1a3b679SAndreas Boehler        if ($tree->nodeExists($uri)) {
84*a1a3b679SAndreas Boehler            $node = $tree->getNodeForPath($uri);
85*a1a3b679SAndreas Boehler            if ($node instanceof IPatchSupport) {
86*a1a3b679SAndreas Boehler                return ['PATCH'];
87*a1a3b679SAndreas Boehler            }
88*a1a3b679SAndreas Boehler        }
89*a1a3b679SAndreas Boehler        return [];
90*a1a3b679SAndreas Boehler
91*a1a3b679SAndreas Boehler    }
92*a1a3b679SAndreas Boehler
93*a1a3b679SAndreas Boehler    /**
94*a1a3b679SAndreas Boehler     * Returns a list of features for the HTTP OPTIONS Dav: header.
95*a1a3b679SAndreas Boehler     *
96*a1a3b679SAndreas Boehler     * @return array
97*a1a3b679SAndreas Boehler     */
98*a1a3b679SAndreas Boehler    function getFeatures() {
99*a1a3b679SAndreas Boehler
100*a1a3b679SAndreas Boehler        return ['sabredav-partialupdate'];
101*a1a3b679SAndreas Boehler
102*a1a3b679SAndreas Boehler    }
103*a1a3b679SAndreas Boehler
104*a1a3b679SAndreas Boehler    /**
105*a1a3b679SAndreas Boehler     * Patch an uri
106*a1a3b679SAndreas Boehler     *
107*a1a3b679SAndreas Boehler     * The WebDAV patch request can be used to modify only a part of an
108*a1a3b679SAndreas Boehler     * existing resource. If the resource does not exist yet and the first
109*a1a3b679SAndreas Boehler     * offset is not 0, the request fails
110*a1a3b679SAndreas Boehler     *
111*a1a3b679SAndreas Boehler     * @param RequestInterface $request
112*a1a3b679SAndreas Boehler     * @param ResponseInterface $response
113*a1a3b679SAndreas Boehler     * @return void
114*a1a3b679SAndreas Boehler     */
115*a1a3b679SAndreas Boehler    function httpPatch(RequestInterface $request, ResponseInterface $response) {
116*a1a3b679SAndreas Boehler
117*a1a3b679SAndreas Boehler        $path = $request->getPath();
118*a1a3b679SAndreas Boehler
119*a1a3b679SAndreas Boehler        // Get the node. Will throw a 404 if not found
120*a1a3b679SAndreas Boehler        $node = $this->server->tree->getNodeForPath($path);
121*a1a3b679SAndreas Boehler        if (!$node instanceof IPatchSupport) {
122*a1a3b679SAndreas Boehler            throw new DAV\Exception\MethodNotAllowed('The target resource does not support the PATCH method.');
123*a1a3b679SAndreas Boehler        }
124*a1a3b679SAndreas Boehler
125*a1a3b679SAndreas Boehler        $range = $this->getHTTPUpdateRange($request);
126*a1a3b679SAndreas Boehler
127*a1a3b679SAndreas Boehler        if (!$range) {
128*a1a3b679SAndreas Boehler            throw new DAV\Exception\BadRequest('No valid "X-Update-Range" found in the headers');
129*a1a3b679SAndreas Boehler        }
130*a1a3b679SAndreas Boehler
131*a1a3b679SAndreas Boehler        $contentType = strtolower(
132*a1a3b679SAndreas Boehler            $request->getHeader('Content-Type')
133*a1a3b679SAndreas Boehler        );
134*a1a3b679SAndreas Boehler
135*a1a3b679SAndreas Boehler        if ($contentType != 'application/x-sabredav-partialupdate') {
136*a1a3b679SAndreas Boehler            throw new DAV\Exception\UnsupportedMediaType('Unknown Content-Type header "' . $contentType . '"');
137*a1a3b679SAndreas Boehler        }
138*a1a3b679SAndreas Boehler
139*a1a3b679SAndreas Boehler        $len = $this->server->httpRequest->getHeader('Content-Length');
140*a1a3b679SAndreas Boehler        if (!$len) throw new DAV\Exception\LengthRequired('A Content-Length header is required');
141*a1a3b679SAndreas Boehler
142*a1a3b679SAndreas Boehler        switch ($range[0]) {
143*a1a3b679SAndreas Boehler            case self::RANGE_START :
144*a1a3b679SAndreas Boehler                // Calculate the end-range if it doesn't exist.
145*a1a3b679SAndreas Boehler                if (!$range[2]) {
146*a1a3b679SAndreas Boehler                    $range[2] = $range[1] + $len - 1;
147*a1a3b679SAndreas Boehler                } else {
148*a1a3b679SAndreas Boehler                    if ($range[2] < $range[1]) {
149*a1a3b679SAndreas Boehler                        throw new DAV\Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[2] . ') is lower than the start offset (' . $range[1] . ')');
150*a1a3b679SAndreas Boehler                    }
151*a1a3b679SAndreas Boehler                    if ($range[2] - $range[1] + 1 != $len) {
152*a1a3b679SAndreas Boehler                        throw new DAV\Exception\RequestedRangeNotSatisfiable('Actual data length (' . $len . ') is not consistent with begin (' . $range[1] . ') and end (' . $range[2] . ') offsets');
153*a1a3b679SAndreas Boehler                    }
154*a1a3b679SAndreas Boehler                }
155*a1a3b679SAndreas Boehler                break;
156*a1a3b679SAndreas Boehler        }
157*a1a3b679SAndreas Boehler
158*a1a3b679SAndreas Boehler        if (!$this->server->emit('beforeWriteContent', [$path, $node, null]))
159*a1a3b679SAndreas Boehler            return;
160*a1a3b679SAndreas Boehler
161*a1a3b679SAndreas Boehler        $body = $this->server->httpRequest->getBody();
162*a1a3b679SAndreas Boehler
163*a1a3b679SAndreas Boehler
164*a1a3b679SAndreas Boehler        $etag = $node->patch($body, $range[0], isset($range[1]) ? $range[1] : null);
165*a1a3b679SAndreas Boehler
166*a1a3b679SAndreas Boehler        $this->server->emit('afterWriteContent', [$path, $node]);
167*a1a3b679SAndreas Boehler
168*a1a3b679SAndreas Boehler        $response->setHeader('Content-Length', '0');
169*a1a3b679SAndreas Boehler        if ($etag) $response->setHeader('ETag', $etag);
170*a1a3b679SAndreas Boehler        $response->setStatus(204);
171*a1a3b679SAndreas Boehler
172*a1a3b679SAndreas Boehler        // Breaks the event chain
173*a1a3b679SAndreas Boehler        return false;
174*a1a3b679SAndreas Boehler
175*a1a3b679SAndreas Boehler    }
176*a1a3b679SAndreas Boehler
177*a1a3b679SAndreas Boehler    /**
178*a1a3b679SAndreas Boehler     * Returns the HTTP custom range update header
179*a1a3b679SAndreas Boehler     *
180*a1a3b679SAndreas Boehler     * This method returns null if there is no well-formed HTTP range request
181*a1a3b679SAndreas Boehler     * header. It returns array(1) if it was an append request, array(2,
182*a1a3b679SAndreas Boehler     * $start, $end) if it's a start and end range, lastly it's array(3,
183*a1a3b679SAndreas Boehler     * $endoffset) if the offset was negative, and should be calculated from
184*a1a3b679SAndreas Boehler     * the end of the file.
185*a1a3b679SAndreas Boehler     *
186*a1a3b679SAndreas Boehler     * Examples:
187*a1a3b679SAndreas Boehler     *
188*a1a3b679SAndreas Boehler     * null - invalid
189*a1a3b679SAndreas Boehler     * [1] - append
190*a1a3b679SAndreas Boehler     * [2,10,15] - update bytes 10, 11, 12, 13, 14, 15
191*a1a3b679SAndreas Boehler     * [2,10,null] - update bytes 10 until the end of the patch body
192*a1a3b679SAndreas Boehler     * [3,-5] - update from 5 bytes from the end of the file.
193*a1a3b679SAndreas Boehler     *
194*a1a3b679SAndreas Boehler     * @param RequestInterface $request
195*a1a3b679SAndreas Boehler     * @return array|null
196*a1a3b679SAndreas Boehler     */
197*a1a3b679SAndreas Boehler    function getHTTPUpdateRange(RequestInterface $request) {
198*a1a3b679SAndreas Boehler
199*a1a3b679SAndreas Boehler        $range = $request->getHeader('X-Update-Range');
200*a1a3b679SAndreas Boehler        if (is_null($range)) return null;
201*a1a3b679SAndreas Boehler
202*a1a3b679SAndreas Boehler        // Matching "Range: bytes=1234-5678: both numbers are optional
203*a1a3b679SAndreas Boehler
204*a1a3b679SAndreas Boehler        if (!preg_match('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i', $range, $matches)) return null;
205*a1a3b679SAndreas Boehler
206*a1a3b679SAndreas Boehler        if ($matches[1] === 'append') {
207*a1a3b679SAndreas Boehler            return [self::RANGE_APPEND];
208*a1a3b679SAndreas Boehler        } elseif (strlen($matches[2]) > 0) {
209*a1a3b679SAndreas Boehler            return [self::RANGE_START, $matches[2], $matches[3] ?: null];
210*a1a3b679SAndreas Boehler        } else {
211*a1a3b679SAndreas Boehler            return [self::RANGE_END, $matches[4]];
212*a1a3b679SAndreas Boehler        }
213*a1a3b679SAndreas Boehler
214*a1a3b679SAndreas Boehler    }
215*a1a3b679SAndreas Boehler}
216