Plugin.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. <?php
  2. declare(strict_types=1);
  3. namespace Sabre\DAV\PartialUpdate;
  4. use Sabre\DAV;
  5. use Sabre\HTTP\RequestInterface;
  6. use Sabre\HTTP\ResponseInterface;
  7. /**
  8. * Partial update plugin (Patch method).
  9. *
  10. * This plugin provides a way to modify only part of a target resource
  11. * It may bu used to update a file chunk, upload big a file into smaller
  12. * chunks or resume an upload.
  13. *
  14. * $patchPlugin = new \Sabre\DAV\PartialUpdate\Plugin();
  15. * $server->addPlugin($patchPlugin);
  16. *
  17. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  18. * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/)
  19. * @license http://sabre.io/license/ Modified BSD License
  20. */
  21. class Plugin extends DAV\ServerPlugin
  22. {
  23. const RANGE_APPEND = 1;
  24. const RANGE_START = 2;
  25. const RANGE_END = 3;
  26. /**
  27. * Reference to server.
  28. *
  29. * @var DAV\Server
  30. */
  31. protected $server;
  32. /**
  33. * Initializes the plugin.
  34. *
  35. * This method is automatically called by the Server class after addPlugin.
  36. */
  37. public function initialize(DAV\Server $server)
  38. {
  39. $this->server = $server;
  40. $server->on('method:PATCH', [$this, 'httpPatch']);
  41. }
  42. /**
  43. * Returns a plugin name.
  44. *
  45. * Using this name other plugins will be able to access other plugins
  46. * using DAV\Server::getPlugin
  47. *
  48. * @return string
  49. */
  50. public function getPluginName()
  51. {
  52. return 'partialupdate';
  53. }
  54. /**
  55. * Use this method to tell the server this plugin defines additional
  56. * HTTP methods.
  57. *
  58. * This method is passed a uri. It should only return HTTP methods that are
  59. * available for the specified uri.
  60. *
  61. * We claim to support PATCH method (partirl update) if and only if
  62. * - the node exist
  63. * - the node implements our partial update interface
  64. *
  65. * @param string $uri
  66. *
  67. * @return array
  68. */
  69. public function getHTTPMethods($uri)
  70. {
  71. $tree = $this->server->tree;
  72. if ($tree->nodeExists($uri)) {
  73. $node = $tree->getNodeForPath($uri);
  74. if ($node instanceof IPatchSupport) {
  75. return ['PATCH'];
  76. }
  77. }
  78. return [];
  79. }
  80. /**
  81. * Returns a list of features for the HTTP OPTIONS Dav: header.
  82. *
  83. * @return array
  84. */
  85. public function getFeatures()
  86. {
  87. return ['sabredav-partialupdate'];
  88. }
  89. /**
  90. * Patch an uri.
  91. *
  92. * The WebDAV patch request can be used to modify only a part of an
  93. * existing resource. If the resource does not exist yet and the first
  94. * offset is not 0, the request fails
  95. */
  96. public function httpPatch(RequestInterface $request, ResponseInterface $response)
  97. {
  98. $path = $request->getPath();
  99. // Get the node. Will throw a 404 if not found
  100. $node = $this->server->tree->getNodeForPath($path);
  101. if (!$node instanceof IPatchSupport) {
  102. throw new DAV\Exception\MethodNotAllowed('The target resource does not support the PATCH method.');
  103. }
  104. $range = $this->getHTTPUpdateRange($request);
  105. if (!$range) {
  106. throw new DAV\Exception\BadRequest('No valid "X-Update-Range" found in the headers');
  107. }
  108. $contentType = strtolower(
  109. (string) $request->getHeader('Content-Type')
  110. );
  111. if ('application/x-sabredav-partialupdate' != $contentType) {
  112. throw new DAV\Exception\UnsupportedMediaType('Unknown Content-Type header "'.$contentType.'"');
  113. }
  114. $len = $this->server->httpRequest->getHeader('Content-Length');
  115. if (!$len) {
  116. throw new DAV\Exception\LengthRequired('A Content-Length header is required');
  117. }
  118. switch ($range[0]) {
  119. case self::RANGE_START:
  120. // Calculate the end-range if it doesn't exist.
  121. if (!$range[2]) {
  122. $range[2] = $range[1] + $len - 1;
  123. } else {
  124. if ($range[2] < $range[1]) {
  125. throw new DAV\Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[2].') is lower than the start offset ('.$range[1].')');
  126. }
  127. if ($range[2] - $range[1] + 1 != $len) {
  128. throw new DAV\Exception\RequestedRangeNotSatisfiable('Actual data length ('.$len.') is not consistent with begin ('.$range[1].') and end ('.$range[2].') offsets');
  129. }
  130. }
  131. break;
  132. }
  133. if (!$this->server->emit('beforeWriteContent', [$path, $node, null])) {
  134. return;
  135. }
  136. $body = $this->server->httpRequest->getBody();
  137. $etag = $node->patch($body, $range[0], isset($range[1]) ? $range[1] : null);
  138. $this->server->emit('afterWriteContent', [$path, $node]);
  139. $response->setHeader('Content-Length', '0');
  140. if ($etag) {
  141. $response->setHeader('ETag', $etag);
  142. }
  143. $response->setStatus(204);
  144. // Breaks the event chain
  145. return false;
  146. }
  147. /**
  148. * Returns the HTTP custom range update header.
  149. *
  150. * This method returns null if there is no well-formed HTTP range request
  151. * header. It returns array(1) if it was an append request, array(2,
  152. * $start, $end) if it's a start and end range, lastly it's array(3,
  153. * $endoffset) if the offset was negative, and should be calculated from
  154. * the end of the file.
  155. *
  156. * Examples:
  157. *
  158. * null - invalid
  159. * [1] - append
  160. * [2,10,15] - update bytes 10, 11, 12, 13, 14, 15
  161. * [2,10,null] - update bytes 10 until the end of the patch body
  162. * [3,-5] - update from 5 bytes from the end of the file.
  163. *
  164. * @return array|null
  165. */
  166. public function getHTTPUpdateRange(RequestInterface $request)
  167. {
  168. $range = $request->getHeader('X-Update-Range');
  169. if (is_null($range)) {
  170. return null;
  171. }
  172. // Matching "Range: bytes=1234-5678: both numbers are optional
  173. if (!preg_match('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i', $range, $matches)) {
  174. return null;
  175. }
  176. if ('append' === $matches[1]) {
  177. return [self::RANGE_APPEND];
  178. } elseif (strlen($matches[2]) > 0) {
  179. return [self::RANGE_START, (int) $matches[2], (int) $matches[3] ?: null];
  180. } else {
  181. return [self::RANGE_END, (int) $matches[4]];
  182. }
  183. }
  184. }