2013-10-21 22:46:31 +00:00
< ? php
namespace Sabre\DAV\PartialUpdate ;
use Sabre\DAV ;
2016-05-11 00:26:44 +00:00
use Sabre\HTTP\RequestInterface ;
use Sabre\HTTP\ResponseInterface ;
2013-10-21 22:46:31 +00:00
/**
* Partial update plugin ( Patch method )
*
* This plugin provides a way to modify only part of a target resource
* It may bu used to update a file chunk , upload big a file into smaller
* chunks or resume an upload .
*
* $patchPlugin = new \Sabre\DAV\PartialUpdate\Plugin ();
* $server -> addPlugin ( $patchPlugin );
*
2016-05-11 00:26:44 +00:00
* @ copyright Copyright ( C ) fruux GmbH ( https :// fruux . com / )
2013-10-21 22:46:31 +00:00
* @ author Jean - Tiare LE BIGOT ( http :// www . jtlebi . fr / )
2014-06-28 20:28:08 +00:00
* @ license http :// sabre . io / license / Modified BSD License
2013-10-21 22:46:31 +00:00
*/
class Plugin extends DAV\ServerPlugin {
2014-06-28 20:28:08 +00:00
const RANGE_APPEND = 1 ;
const RANGE_START = 2 ;
const RANGE_END = 3 ;
2013-10-21 22:46:31 +00:00
/**
* 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
*/
2016-05-11 00:26:44 +00:00
function initialize ( DAV\Server $server ) {
2013-10-21 22:46:31 +00:00
$this -> server = $server ;
2016-05-11 00:26:44 +00:00
$server -> on ( 'method:PATCH' , [ $this , 'httpPatch' ]);
2013-10-21 22:46:31 +00:00
}
/**
* Returns a plugin name .
*
* Using this name other plugins will be able to access other plugins
* using DAV\Server :: getPlugin
*
* @ return string
*/
2016-05-11 00:26:44 +00:00
function getPluginName () {
2013-10-21 22:46:31 +00:00
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 .
2014-06-28 20:28:08 +00:00
*
2016-05-11 00:26:44 +00:00
* We claim to support PATCH method ( partirl update ) if and only if
2013-10-21 22:46:31 +00:00
* - the node exist
* - the node implements our partial update interface
*
* @ param string $uri
* @ return array
*/
2016-05-11 00:26:44 +00:00
function getHTTPMethods ( $uri ) {
2014-06-28 20:28:08 +00:00
2013-10-21 22:46:31 +00:00
$tree = $this -> server -> tree ;
2016-05-11 00:26:44 +00:00
2014-06-28 20:28:08 +00:00
if ( $tree -> nodeExists ( $uri )) {
$node = $tree -> getNodeForPath ( $uri );
2016-05-11 00:26:44 +00:00
if ( $node instanceof IPatchSupport ) {
return [ 'PATCH' ];
2014-06-28 20:28:08 +00:00
}
}
2016-05-11 00:26:44 +00:00
return [];
2013-10-21 22:46:31 +00:00
}
/**
* Returns a list of features for the HTTP OPTIONS Dav : header .
*
* @ return array
*/
2016-05-11 00:26:44 +00:00
function getFeatures () {
2013-10-21 22:46:31 +00:00
2016-05-11 00:26:44 +00:00
return [ 'sabredav-partialupdate' ];
2013-10-21 22:46:31 +00:00
}
/**
* Patch an uri
*
2014-06-28 20:28:08 +00:00
* The WebDAV patch request can be used to modify only a part of an
2013-10-21 22:46:31 +00:00
* existing resource . If the resource does not exist yet and the first
* offset is not 0 , the request fails
*
2016-05-11 00:26:44 +00:00
* @ param RequestInterface $request
* @ param ResponseInterface $response
2013-10-21 22:46:31 +00:00
* @ return void
*/
2016-05-11 00:26:44 +00:00
function httpPatch ( RequestInterface $request , ResponseInterface $response ) {
$path = $request -> getPath ();
2013-10-21 22:46:31 +00:00
// Get the node. Will throw a 404 if not found
2016-05-11 00:26:44 +00:00
$node = $this -> server -> tree -> getNodeForPath ( $path );
if ( ! $node instanceof IPatchSupport ) {
2013-10-21 22:46:31 +00:00
throw new DAV\Exception\MethodNotAllowed ( 'The target resource does not support the PATCH method.' );
}
2016-05-11 00:26:44 +00:00
$range = $this -> getHTTPUpdateRange ( $request );
2013-10-21 22:46:31 +00:00
if ( ! $range ) {
throw new DAV\Exception\BadRequest ( 'No valid "X-Update-Range" found in the headers' );
}
2014-06-28 20:28:08 +00:00
2013-10-21 22:46:31 +00:00
$contentType = strtolower (
2016-05-11 00:26:44 +00:00
$request -> getHeader ( 'Content-Type' )
2013-10-21 22:46:31 +00:00
);
2014-06-28 20:28:08 +00:00
2013-10-21 22:46:31 +00:00
if ( $contentType != 'application/x-sabredav-partialupdate' ) {
throw new DAV\Exception\UnsupportedMediaType ( 'Unknown Content-Type header "' . $contentType . '"' );
}
$len = $this -> server -> httpRequest -> getHeader ( 'Content-Length' );
2014-06-28 20:28:08 +00:00
if ( ! $len ) throw new DAV\Exception\LengthRequired ( 'A Content-Length header is required' );
2016-05-11 00:26:44 +00:00
switch ( $range [ 0 ]) {
2014-06-28 20:28:08 +00:00
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 ] . ')' );
}
2016-05-11 00:26:44 +00:00
if ( $range [ 2 ] - $range [ 1 ] + 1 != $len ) {
2014-06-28 20:28:08 +00:00
throw new DAV\Exception\RequestedRangeNotSatisfiable ( 'Actual data length (' . $len . ') is not consistent with begin (' . $range [ 1 ] . ') and end (' . $range [ 2 ] . ') offsets' );
}
}
break ;
}
2013-10-21 22:46:31 +00:00
2016-05-11 00:26:44 +00:00
if ( ! $this -> server -> emit ( 'beforeWriteContent' , [ $path , $node , null ]))
2013-10-21 22:46:31 +00:00
return ;
$body = $this -> server -> httpRequest -> getBody ();
2014-06-28 20:28:08 +00:00
2016-05-11 00:26:44 +00:00
$etag = $node -> patch ( $body , $range [ 0 ], isset ( $range [ 1 ]) ? $range [ 1 ] : null );
2013-10-21 22:46:31 +00:00
2016-05-11 00:26:44 +00:00
$this -> server -> emit ( 'afterWriteContent' , [ $path , $node ]);
2013-10-21 22:46:31 +00:00
2016-05-11 00:26:44 +00:00
$response -> setHeader ( 'Content-Length' , '0' );
if ( $etag ) $response -> setHeader ( 'ETag' , $etag );
$response -> setStatus ( 204 );
2013-10-21 22:46:31 +00:00
2016-05-11 00:26:44 +00:00
// Breaks the event chain
2013-10-21 22:46:31 +00:00
return false ;
}
2014-06-28 20:28:08 +00:00
2016-05-11 00:26:44 +00:00
/**
2013-10-21 22:46:31 +00:00
* Returns the HTTP custom range update header
*
* This method returns null if there is no well - formed HTTP range request
2014-06-28 20:28:08 +00:00
* 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 .
2013-10-21 22:46:31 +00:00
*
2014-06-28 20:28:08 +00:00
* Examples :
2013-10-21 22:46:31 +00:00
*
2014-06-28 20:28:08 +00:00
* null - invalid
2016-05-11 00:26:44 +00:00
* [ 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 .
2013-10-21 22:46:31 +00:00
*
2016-05-11 00:26:44 +00:00
* @ param RequestInterface $request
2013-10-21 22:46:31 +00:00
* @ return array | null
*/
2016-05-11 00:26:44 +00:00
function getHTTPUpdateRange ( RequestInterface $request ) {
2013-10-21 22:46:31 +00:00
2016-05-11 00:26:44 +00:00
$range = $request -> getHeader ( 'X-Update-Range' );
2013-10-21 22:46:31 +00:00
if ( is_null ( $range )) return null ;
// Matching "Range: bytes=1234-5678: both numbers are optional
2016-05-11 00:26:44 +00:00
if ( ! preg_match ( '/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i' , $range , $matches )) return null ;
2013-10-21 22:46:31 +00:00
2016-05-11 00:26:44 +00:00
if ( $matches [ 1 ] === 'append' ) {
return [ self :: RANGE_APPEND ];
} elseif ( strlen ( $matches [ 2 ]) > 0 ) {
return [ self :: RANGE_START , $matches [ 2 ], $matches [ 3 ] ? : null ];
2014-06-28 20:28:08 +00:00
} else {
2016-05-11 00:26:44 +00:00
return [ self :: RANGE_END , $matches [ 4 ]];
2014-06-28 20:28:08 +00:00
}
2013-10-21 22:46:31 +00:00
}
}