addPlugin($patchPlugin); * * @copyright Copyright (C) fruux GmbH (https://fruux.com/) * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/) * @license http://sabre.io/license/ Modified BSD License */ class Plugin extends DAV\ServerPlugin { const RANGE_APPEND = 1; const RANGE_START = 2; const RANGE_END = 3; /** * Reference to server * * @var Sabre\DAV\Server */ protected $server; /** * Initializes the plugin * * This method is automatically called by the Server class after addPlugin. * * @param DAV\Server $server * @return void */ function initialize(DAV\Server $server) { $this->server = $server; $server->on('method:PATCH', [$this, 'httpPatch']); } /** * Returns a plugin name. * * Using this name other plugins will be able to access other plugins * using DAV\Server::getPlugin * * @return string */ function getPluginName() { return 'partialupdate'; } /** * Use this method to tell the server this plugin defines additional * HTTP methods. * * This method is passed a uri. It should only return HTTP methods that are * available for the specified uri. * * We claim to support PATCH method (partirl update) if and only if * - the node exist * - the node implements our partial update interface * * @param string $uri * @return array */ function getHTTPMethods($uri) { $tree = $this->server->tree; if ($tree->nodeExists($uri)) { $node = $tree->getNodeForPath($uri); if ($node instanceof IPatchSupport) { return ['PATCH']; } } return []; } /** * Returns a list of features for the HTTP OPTIONS Dav: header. * * @return array */ function getFeatures() { return ['sabredav-partialupdate']; } /** * Patch an uri * * The WebDAV patch request can be used to modify only a part of an * existing resource. If the resource does not exist yet and the first * offset is not 0, the request fails * * @param RequestInterface $request * @param ResponseInterface $response * @return void */ function httpPatch(RequestInterface $request, ResponseInterface $response) { $path = $request->getPath(); // Get the node. Will throw a 404 if not found $node = $this->server->tree->getNodeForPath($path); if (!$node instanceof IPatchSupport) { throw new DAV\Exception\MethodNotAllowed('The target resource does not support the PATCH method.'); } $range = $this->getHTTPUpdateRange($request); if (!$range) { throw new DAV\Exception\BadRequest('No valid "X-Update-Range" found in the headers'); } $contentType = strtolower( $request->getHeader('Content-Type') ); if ($contentType != 'application/x-sabredav-partialupdate') { throw new DAV\Exception\UnsupportedMediaType('Unknown Content-Type header "' . $contentType . '"'); } $len = $this->server->httpRequest->getHeader('Content-Length'); if (!$len) throw new DAV\Exception\LengthRequired('A Content-Length header is required'); switch ($range[0]) { case self::RANGE_START : // Calculate the end-range if it doesn't exist. if (!$range[2]) { $range[2] = $range[1] + $len - 1; } else { if ($range[2] < $range[1]) { throw new DAV\Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[2] . ') is lower than the start offset (' . $range[1] . ')'); } if ($range[2] - $range[1] + 1 != $len) { throw new DAV\Exception\RequestedRangeNotSatisfiable('Actual data length (' . $len . ') is not consistent with begin (' . $range[1] . ') and end (' . $range[2] . ') offsets'); } } break; } if (!$this->server->emit('beforeWriteContent', [$path, $node, null])) return; $body = $this->server->httpRequest->getBody(); $etag = $node->patch($body, $range[0], isset($range[1]) ? $range[1] : null); $this->server->emit('afterWriteContent', [$path, $node]); $response->setHeader('Content-Length', '0'); if ($etag) $response->setHeader('ETag', $etag); $response->setStatus(204); // Breaks the event chain return false; } /** * Returns the HTTP custom range update header * * This method returns null if there is no well-formed HTTP range request * header. It returns array(1) if it was an append request, array(2, * $start, $end) if it's a start and end range, lastly it's array(3, * $endoffset) if the offset was negative, and should be calculated from * the end of the file. * * Examples: * * null - invalid * [1] - append * [2,10,15] - update bytes 10, 11, 12, 13, 14, 15 * [2,10,null] - update bytes 10 until the end of the patch body * [3,-5] - update from 5 bytes from the end of the file. * * @param RequestInterface $request * @return array|null */ function getHTTPUpdateRange(RequestInterface $request) { $range = $request->getHeader('X-Update-Range'); if (is_null($range)) return null; // Matching "Range: bytes=1234-5678: both numbers are optional if (!preg_match('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i', $range, $matches)) return null; if ($matches[1] === 'append') { return [self::RANGE_APPEND]; } elseif (strlen($matches[2]) > 0) { return [self::RANGE_START, $matches[2], $matches[3] ?: null]; } else { return [self::RANGE_END, $matches[4]]; } } }