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