2013-10-21 22:46:31 +00:00
< ? php
namespace Sabre\DAV\PartialUpdate ;
use Sabre\DAV ;
/**
* 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 );
*
2014-04-12 17:13:37 +00:00
* @ copyright Copyright ( C ) 2007 - 2014 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
*/
public function initialize ( DAV\Server $server ) {
$this -> server = $server ;
$server -> subscribeEvent ( 'unknownMethod' , array ( $this , 'unknownMethod' ));
}
/**
* Returns a plugin name .
*
* Using this name other plugins will be able to access other plugins
* using DAV\Server :: getPlugin
*
* @ return string
*/
public function getPluginName () {
return 'partialupdate' ;
}
/**
* This method is called by the Server if the user used an HTTP method
* the server didn ' t recognize .
*
* This plugin intercepts the PATCH methods .
*
* @ param string $method
* @ param string $uri
* @ return bool | null
*/
public function unknownMethod ( $method , $uri ) {
switch ( $method ) {
2014-06-28 20:28:08 +00:00
2013-10-21 22:46:31 +00:00
case 'PATCH' :
return $this -> httpPatch ( $uri );
}
}
/**
* 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
*
2013-10-21 22:46:31 +00:00
* We claim to support PATCH method ( partial update ) if and only if
* - the node exist
* - the node implements our partial update interface
*
* @ param string $uri
* @ return array
*/
public function getHTTPMethods ( $uri ) {
2014-06-28 20:28:08 +00:00
2013-10-21 22:46:31 +00:00
$tree = $this -> server -> tree ;
2014-06-28 20:28:08 +00:00
if ( $tree -> nodeExists ( $uri )) {
$node = $tree -> getNodeForPath ( $uri );
if ( $node instanceof IFile || $node instanceof IPatchSupport ) {
return array ( 'PATCH' );
}
}
return array ();
2013-10-21 22:46:31 +00:00
}
/**
* Returns a list of features for the HTTP OPTIONS Dav : header .
*
* @ return array
*/
public function getFeatures () {
return array ( 'sabredav-partialupdate' );
}
/**
* 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
*
* @ param string $uri
* @ return void
*/
protected function httpPatch ( $uri ) {
// Get the node. Will throw a 404 if not found
$node = $this -> server -> tree -> getNodeForPath ( $uri );
2014-06-28 20:28:08 +00:00
if ( ! $node instanceof IFile && ! $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.' );
}
$range = $this -> getHTTPUpdateRange ();
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 (
$this -> server -> httpRequest -> getHeader ( 'Content-Type' )
);
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' );
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 ;
}
2013-10-21 22:46:31 +00:00
// Checking If-None-Match and related headers.
if ( ! $this -> server -> checkPreconditions ()) return ;
if ( ! $this -> server -> broadcastEvent ( 'beforeWriteContent' , array ( $uri , $node , null )))
return ;
$body = $this -> server -> httpRequest -> getBody ();
2014-06-28 20:28:08 +00:00
if ( $node instanceof IPatchSupport ) {
$etag = $node -> patch ( $body , $range [ 0 ], isset ( $range [ 1 ]) ? $range [ 1 ] : null );
} else {
// The old interface
switch ( $range [ 0 ]) {
case self :: RANGE_APPEND :
throw new DAV\Exception\NotImplemented ( 'This node does not support the append syntax. Please upgrade it to IPatchSupport' );
case self :: RANGE_START :
$etag = $node -> putRange ( $body , $range [ 1 ]);
break ;
case self :: RANGE_END :
throw new DAV\Exception\NotImplemented ( 'This node does not support the end-range syntax. Please upgrade it to IPatchSupport' );
break ;
}
}
2013-10-21 22:46:31 +00:00
$this -> server -> broadcastEvent ( 'afterWriteContent' , array ( $uri , $node ));
$this -> server -> httpResponse -> setHeader ( 'Content-Length' , '0' );
if ( $etag ) $this -> server -> httpResponse -> setHeader ( 'ETag' , $etag );
$this -> server -> httpResponse -> sendStatus ( 204 );
return false ;
}
2014-06-28 20:28:08 +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
* array ( 1 ) - append
* array ( 2 , 10 , 15 ) - update bytes 10 , 11 , 12 , 13 , 14 , 15
* array ( 2 , 10 , null ) - update bytes 10 until the end of the patch body
* array ( 3 , - 5 ) - update from 5 bytes from the end of the file .
2013-10-21 22:46:31 +00:00
*
* @ return array | null
*/
public function getHTTPUpdateRange () {
$range = $this -> server -> httpRequest -> getHeader ( 'X-Update-Range' );
if ( is_null ( $range )) return null ;
// Matching "Range: bytes=1234-5678: both numbers are optional
2014-06-28 20:28:08 +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
2014-06-28 20:28:08 +00:00
if ( $matches [ 1 ] === 'append' ) {
return array ( self :: RANGE_APPEND );
} elseif ( strlen ( $matches [ 2 ]) > 0 ) {
return array ( self :: RANGE_START , $matches [ 2 ], $matches [ 3 ] ? : null );
} elseif ( $matches [ 4 ]) {
return array ( self :: RANGE_END , $matches [ 4 ]);
} else {
return null ;
}
2013-10-21 22:46:31 +00:00
}
}