247 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			PHP
		
	
	
	
			
		
		
	
	
			247 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			PHP
		
	
	
	
| <?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);
 | |
|  *
 | |
|  * @copyright Copyright (C) 2007-2015 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
 | |
|      */
 | |
|     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) {
 | |
| 
 | |
|             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.
 | |
|      *
 | |
|      * 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) {
 | |
| 
 | |
|         $tree = $this->server->tree;
 | |
|         if ($tree->nodeExists($uri)) {
 | |
|             $node = $tree->getNodeForPath($uri);
 | |
|             if ($node instanceof IFile || $node instanceof IPatchSupport) {
 | |
|                 return array('PATCH');
 | |
|             }
 | |
|         }
 | |
|         return array();
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns a list of features for the HTTP OPTIONS Dav: header.
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     public function getFeatures() {
 | |
| 
 | |
|         return array('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 string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     protected function httpPatch($uri) {
 | |
| 
 | |
|         // Get the node. Will throw a 404 if not found
 | |
|         $node = $this->server->tree->getNodeForPath($uri);
 | |
|         if (!$node instanceof IFile && !$node instanceof IPatchSupport) {
 | |
|             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');
 | |
|         }
 | |
| 
 | |
|         $contentType = strtolower(
 | |
|             $this->server->httpRequest->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;
 | |
|         }
 | |
|         // 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();
 | |
| 
 | |
| 
 | |
|         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;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         $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;
 | |
| 
 | |
|     }
 | |
| 
 | |
|    /**
 | |
|      * 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
 | |
|      * 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.
 | |
|      *
 | |
|      * @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
 | |
| 
 | |
|         if (!preg_match('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i',$range,$matches)) return null;
 | |
| 
 | |
|         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;
 | |
|         }
 | |
| 
 | |
|     }
 | |
| }
 |