@@ -73,7 +73,9 @@ discard block |
||
| 73 | 73 | |
| 74 | 74 | // Checking if we can remove any of these locks |
| 75 | 75 | foreach ($newLocks as $k => $lock) { |
| 76 | - if (time() > $lock->timeout + $lock->created) unset($newLocks[$k]); |
|
| 76 | + if (time() > $lock->timeout + $lock->created) { |
|
| 77 | + unset($newLocks[$k]); |
|
| 78 | + } |
|
| 77 | 79 | } |
| 78 | 80 | return $newLocks; |
| 79 | 81 | |
@@ -140,7 +142,9 @@ discard block |
||
| 140 | 142 | */ |
| 141 | 143 | protected function getData() { |
| 142 | 144 | |
| 143 | - if (!file_exists($this->locksFile)) return []; |
|
| 145 | + if (!file_exists($this->locksFile)) { |
|
| 146 | + return []; |
|
| 147 | + } |
|
| 144 | 148 | |
| 145 | 149 | // opening up the file, and creating a shared lock |
| 146 | 150 | $handle = fopen($this->locksFile, 'r'); |
@@ -155,7 +159,9 @@ discard block |
||
| 155 | 159 | |
| 156 | 160 | // Unserializing and checking if the resource file contains data for this file |
| 157 | 161 | $data = unserialize($data); |
| 158 | - if (!$data) return []; |
|
| 162 | + if (!$data) { |
|
| 163 | + return []; |
|
| 164 | + } |
|
| 159 | 165 | return $data; |
| 160 | 166 | |
| 161 | 167 | } |
@@ -19,167 +19,167 @@ |
||
| 19 | 19 | */ |
| 20 | 20 | class File extends AbstractBackend { |
| 21 | 21 | |
| 22 | - /** |
|
| 23 | - * The storage file |
|
| 24 | - * |
|
| 25 | - * @var string |
|
| 26 | - */ |
|
| 27 | - private $locksFile; |
|
| 28 | - |
|
| 29 | - /** |
|
| 30 | - * Constructor |
|
| 31 | - * |
|
| 32 | - * @param string $locksFile path to file |
|
| 33 | - */ |
|
| 34 | - public function __construct($locksFile) { |
|
| 35 | - |
|
| 36 | - $this->locksFile = $locksFile; |
|
| 37 | - |
|
| 38 | - } |
|
| 39 | - |
|
| 40 | - /** |
|
| 41 | - * Returns a list of Sabre\DAV\Locks\LockInfo objects |
|
| 42 | - * |
|
| 43 | - * This method should return all the locks for a particular uri, including |
|
| 44 | - * locks that might be set on a parent uri. |
|
| 45 | - * |
|
| 46 | - * If returnChildLocks is set to true, this method should also look for |
|
| 47 | - * any locks in the subtree of the uri for locks. |
|
| 48 | - * |
|
| 49 | - * @param string $uri |
|
| 50 | - * @param bool $returnChildLocks |
|
| 51 | - * @return array |
|
| 52 | - */ |
|
| 53 | - public function getLocks($uri, $returnChildLocks) { |
|
| 54 | - |
|
| 55 | - $newLocks = []; |
|
| 56 | - |
|
| 57 | - $locks = $this->getData(); |
|
| 58 | - |
|
| 59 | - foreach ($locks as $lock) { |
|
| 60 | - |
|
| 61 | - if ($lock->uri === $uri || |
|
| 62 | - //deep locks on parents |
|
| 63 | - ($lock->depth != 0 && strpos($uri, $lock->uri . '/') === 0) || |
|
| 64 | - |
|
| 65 | - // locks on children |
|
| 66 | - ($returnChildLocks && (strpos($lock->uri, $uri . '/') === 0))) { |
|
| 67 | - |
|
| 68 | - $newLocks[] = $lock; |
|
| 69 | - |
|
| 70 | - } |
|
| 71 | - |
|
| 72 | - } |
|
| 73 | - |
|
| 74 | - // Checking if we can remove any of these locks |
|
| 75 | - foreach ($newLocks as $k => $lock) { |
|
| 76 | - if (time() > $lock->timeout + $lock->created) unset($newLocks[$k]); |
|
| 77 | - } |
|
| 78 | - return $newLocks; |
|
| 79 | - |
|
| 80 | - } |
|
| 81 | - |
|
| 82 | - /** |
|
| 83 | - * Locks a uri |
|
| 84 | - * |
|
| 85 | - * @param string $uri |
|
| 86 | - * @param LockInfo $lockInfo |
|
| 87 | - * @return bool |
|
| 88 | - */ |
|
| 89 | - public function lock($uri, LockInfo $lockInfo) { |
|
| 90 | - |
|
| 91 | - // We're making the lock timeout 30 minutes |
|
| 92 | - $lockInfo->timeout = 1800; |
|
| 93 | - $lockInfo->created = time(); |
|
| 94 | - $lockInfo->uri = $uri; |
|
| 95 | - |
|
| 96 | - $locks = $this->getData(); |
|
| 97 | - |
|
| 98 | - foreach ($locks as $k => $lock) { |
|
| 99 | - if ( |
|
| 100 | - ($lock->token == $lockInfo->token) || |
|
| 101 | - (time() > $lock->timeout + $lock->created) |
|
| 102 | - ) { |
|
| 103 | - unset($locks[$k]); |
|
| 104 | - } |
|
| 105 | - } |
|
| 106 | - $locks[] = $lockInfo; |
|
| 107 | - $this->putData($locks); |
|
| 108 | - return true; |
|
| 109 | - |
|
| 110 | - } |
|
| 111 | - |
|
| 112 | - /** |
|
| 113 | - * Removes a lock from a uri |
|
| 114 | - * |
|
| 115 | - * @param string $uri |
|
| 116 | - * @param LockInfo $lockInfo |
|
| 117 | - * @return bool |
|
| 118 | - */ |
|
| 119 | - public function unlock($uri, LockInfo $lockInfo) { |
|
| 120 | - |
|
| 121 | - $locks = $this->getData(); |
|
| 122 | - foreach ($locks as $k => $lock) { |
|
| 123 | - |
|
| 124 | - if ($lock->token == $lockInfo->token) { |
|
| 125 | - |
|
| 126 | - unset($locks[$k]); |
|
| 127 | - $this->putData($locks); |
|
| 128 | - return true; |
|
| 129 | - |
|
| 130 | - } |
|
| 131 | - } |
|
| 132 | - return false; |
|
| 133 | - |
|
| 134 | - } |
|
| 135 | - |
|
| 136 | - /** |
|
| 137 | - * Loads the lockdata from the filesystem. |
|
| 138 | - * |
|
| 139 | - * @return array |
|
| 140 | - */ |
|
| 141 | - protected function getData() { |
|
| 142 | - |
|
| 143 | - if (!file_exists($this->locksFile)) return []; |
|
| 144 | - |
|
| 145 | - // opening up the file, and creating a shared lock |
|
| 146 | - $handle = fopen($this->locksFile, 'r'); |
|
| 147 | - flock($handle, LOCK_SH); |
|
| 148 | - |
|
| 149 | - // Reading data until the eof |
|
| 150 | - $data = stream_get_contents($handle); |
|
| 22 | + /** |
|
| 23 | + * The storage file |
|
| 24 | + * |
|
| 25 | + * @var string |
|
| 26 | + */ |
|
| 27 | + private $locksFile; |
|
| 28 | + |
|
| 29 | + /** |
|
| 30 | + * Constructor |
|
| 31 | + * |
|
| 32 | + * @param string $locksFile path to file |
|
| 33 | + */ |
|
| 34 | + public function __construct($locksFile) { |
|
| 35 | + |
|
| 36 | + $this->locksFile = $locksFile; |
|
| 37 | + |
|
| 38 | + } |
|
| 39 | + |
|
| 40 | + /** |
|
| 41 | + * Returns a list of Sabre\DAV\Locks\LockInfo objects |
|
| 42 | + * |
|
| 43 | + * This method should return all the locks for a particular uri, including |
|
| 44 | + * locks that might be set on a parent uri. |
|
| 45 | + * |
|
| 46 | + * If returnChildLocks is set to true, this method should also look for |
|
| 47 | + * any locks in the subtree of the uri for locks. |
|
| 48 | + * |
|
| 49 | + * @param string $uri |
|
| 50 | + * @param bool $returnChildLocks |
|
| 51 | + * @return array |
|
| 52 | + */ |
|
| 53 | + public function getLocks($uri, $returnChildLocks) { |
|
| 54 | + |
|
| 55 | + $newLocks = []; |
|
| 56 | + |
|
| 57 | + $locks = $this->getData(); |
|
| 58 | + |
|
| 59 | + foreach ($locks as $lock) { |
|
| 60 | + |
|
| 61 | + if ($lock->uri === $uri || |
|
| 62 | + //deep locks on parents |
|
| 63 | + ($lock->depth != 0 && strpos($uri, $lock->uri . '/') === 0) || |
|
| 64 | + |
|
| 65 | + // locks on children |
|
| 66 | + ($returnChildLocks && (strpos($lock->uri, $uri . '/') === 0))) { |
|
| 67 | + |
|
| 68 | + $newLocks[] = $lock; |
|
| 69 | + |
|
| 70 | + } |
|
| 71 | + |
|
| 72 | + } |
|
| 73 | + |
|
| 74 | + // Checking if we can remove any of these locks |
|
| 75 | + foreach ($newLocks as $k => $lock) { |
|
| 76 | + if (time() > $lock->timeout + $lock->created) unset($newLocks[$k]); |
|
| 77 | + } |
|
| 78 | + return $newLocks; |
|
| 79 | + |
|
| 80 | + } |
|
| 81 | + |
|
| 82 | + /** |
|
| 83 | + * Locks a uri |
|
| 84 | + * |
|
| 85 | + * @param string $uri |
|
| 86 | + * @param LockInfo $lockInfo |
|
| 87 | + * @return bool |
|
| 88 | + */ |
|
| 89 | + public function lock($uri, LockInfo $lockInfo) { |
|
| 90 | + |
|
| 91 | + // We're making the lock timeout 30 minutes |
|
| 92 | + $lockInfo->timeout = 1800; |
|
| 93 | + $lockInfo->created = time(); |
|
| 94 | + $lockInfo->uri = $uri; |
|
| 95 | + |
|
| 96 | + $locks = $this->getData(); |
|
| 97 | + |
|
| 98 | + foreach ($locks as $k => $lock) { |
|
| 99 | + if ( |
|
| 100 | + ($lock->token == $lockInfo->token) || |
|
| 101 | + (time() > $lock->timeout + $lock->created) |
|
| 102 | + ) { |
|
| 103 | + unset($locks[$k]); |
|
| 104 | + } |
|
| 105 | + } |
|
| 106 | + $locks[] = $lockInfo; |
|
| 107 | + $this->putData($locks); |
|
| 108 | + return true; |
|
| 109 | + |
|
| 110 | + } |
|
| 111 | + |
|
| 112 | + /** |
|
| 113 | + * Removes a lock from a uri |
|
| 114 | + * |
|
| 115 | + * @param string $uri |
|
| 116 | + * @param LockInfo $lockInfo |
|
| 117 | + * @return bool |
|
| 118 | + */ |
|
| 119 | + public function unlock($uri, LockInfo $lockInfo) { |
|
| 120 | + |
|
| 121 | + $locks = $this->getData(); |
|
| 122 | + foreach ($locks as $k => $lock) { |
|
| 123 | + |
|
| 124 | + if ($lock->token == $lockInfo->token) { |
|
| 125 | + |
|
| 126 | + unset($locks[$k]); |
|
| 127 | + $this->putData($locks); |
|
| 128 | + return true; |
|
| 129 | + |
|
| 130 | + } |
|
| 131 | + } |
|
| 132 | + return false; |
|
| 133 | + |
|
| 134 | + } |
|
| 135 | + |
|
| 136 | + /** |
|
| 137 | + * Loads the lockdata from the filesystem. |
|
| 138 | + * |
|
| 139 | + * @return array |
|
| 140 | + */ |
|
| 141 | + protected function getData() { |
|
| 142 | + |
|
| 143 | + if (!file_exists($this->locksFile)) return []; |
|
| 144 | + |
|
| 145 | + // opening up the file, and creating a shared lock |
|
| 146 | + $handle = fopen($this->locksFile, 'r'); |
|
| 147 | + flock($handle, LOCK_SH); |
|
| 148 | + |
|
| 149 | + // Reading data until the eof |
|
| 150 | + $data = stream_get_contents($handle); |
|
| 151 | 151 | |
| 152 | - // We're all good |
|
| 153 | - flock($handle, LOCK_UN); |
|
| 154 | - fclose($handle); |
|
| 152 | + // We're all good |
|
| 153 | + flock($handle, LOCK_UN); |
|
| 154 | + fclose($handle); |
|
| 155 | 155 | |
| 156 | - // Unserializing and checking if the resource file contains data for this file |
|
| 157 | - $data = unserialize($data); |
|
| 158 | - if (!$data) return []; |
|
| 159 | - return $data; |
|
| 160 | - |
|
| 161 | - } |
|
| 162 | - |
|
| 163 | - /** |
|
| 164 | - * Saves the lockdata |
|
| 165 | - * |
|
| 166 | - * @param array $newData |
|
| 167 | - * @return void |
|
| 168 | - */ |
|
| 169 | - protected function putData(array $newData) { |
|
| 170 | - |
|
| 171 | - // opening up the file, and creating an exclusive lock |
|
| 172 | - $handle = fopen($this->locksFile, 'a+'); |
|
| 173 | - flock($handle, LOCK_EX); |
|
| 174 | - |
|
| 175 | - // We can only truncate and rewind once the lock is acquired. |
|
| 176 | - ftruncate($handle, 0); |
|
| 177 | - rewind($handle); |
|
| 178 | - |
|
| 179 | - fwrite($handle, serialize($newData)); |
|
| 180 | - flock($handle, LOCK_UN); |
|
| 181 | - fclose($handle); |
|
| 182 | - |
|
| 183 | - } |
|
| 156 | + // Unserializing and checking if the resource file contains data for this file |
|
| 157 | + $data = unserialize($data); |
|
| 158 | + if (!$data) return []; |
|
| 159 | + return $data; |
|
| 160 | + |
|
| 161 | + } |
|
| 162 | + |
|
| 163 | + /** |
|
| 164 | + * Saves the lockdata |
|
| 165 | + * |
|
| 166 | + * @param array $newData |
|
| 167 | + * @return void |
|
| 168 | + */ |
|
| 169 | + protected function putData(array $newData) { |
|
| 170 | + |
|
| 171 | + // opening up the file, and creating an exclusive lock |
|
| 172 | + $handle = fopen($this->locksFile, 'a+'); |
|
| 173 | + flock($handle, LOCK_EX); |
|
| 174 | + |
|
| 175 | + // We can only truncate and rewind once the lock is acquired. |
|
| 176 | + ftruncate($handle, 0); |
|
| 177 | + rewind($handle); |
|
| 178 | + |
|
| 179 | + fwrite($handle, serialize($newData)); |
|
| 180 | + flock($handle, LOCK_UN); |
|
| 181 | + fclose($handle); |
|
| 182 | + |
|
| 183 | + } |
|
| 184 | 184 | |
| 185 | 185 | } |
@@ -229,7 +229,7 @@ discard block |
||
| 229 | 229 | |
| 230 | 230 | $response = $this->send($request); |
| 231 | 231 | |
| 232 | - if ((int)$response->getStatus() >= 400) { |
|
| 232 | + if ((int) $response->getStatus() >= 400) { |
|
| 233 | 233 | throw new \Sabre\HTTP\ClientHttpException($response); |
| 234 | 234 | } |
| 235 | 235 | |
@@ -372,7 +372,7 @@ discard block |
||
| 372 | 372 | $response = $this->send(new HTTP\Request($method, $url, $headers, $body)); |
| 373 | 373 | return [ |
| 374 | 374 | 'body' => $response->getBodyAsString(), |
| 375 | - 'statusCode' => (int)$response->getStatus(), |
|
| 375 | + 'statusCode' => (int) $response->getStatus(), |
|
| 376 | 376 | 'headers' => array_change_key_case($response->getHeaders()), |
| 377 | 377 | ]; |
| 378 | 378 | |
@@ -18,431 +18,431 @@ |
||
| 18 | 18 | */ |
| 19 | 19 | class Client extends HTTP\Client { |
| 20 | 20 | |
| 21 | - /** |
|
| 22 | - * The xml service. |
|
| 23 | - * |
|
| 24 | - * Uset this service to configure the property and namespace maps. |
|
| 25 | - * |
|
| 26 | - * @var mixed |
|
| 27 | - */ |
|
| 28 | - public $xml; |
|
| 29 | - |
|
| 30 | - /** |
|
| 31 | - * The elementMap |
|
| 32 | - * |
|
| 33 | - * This property is linked via reference to $this->xml->elementMap. |
|
| 34 | - * It's deprecated as of version 3.0.0, and should no longer be used. |
|
| 35 | - * |
|
| 36 | - * @deprecated |
|
| 37 | - * @var array |
|
| 38 | - */ |
|
| 39 | - public $propertyMap = []; |
|
| 40 | - |
|
| 41 | - /** |
|
| 42 | - * Base URI |
|
| 43 | - * |
|
| 44 | - * This URI will be used to resolve relative urls. |
|
| 45 | - * |
|
| 46 | - * @var string |
|
| 47 | - */ |
|
| 48 | - protected $baseUri; |
|
| 49 | - |
|
| 50 | - /** |
|
| 51 | - * Basic authentication |
|
| 52 | - */ |
|
| 53 | - const AUTH_BASIC = 1; |
|
| 54 | - |
|
| 55 | - /** |
|
| 56 | - * Digest authentication |
|
| 57 | - */ |
|
| 58 | - const AUTH_DIGEST = 2; |
|
| 59 | - |
|
| 60 | - /** |
|
| 61 | - * NTLM authentication |
|
| 62 | - */ |
|
| 63 | - const AUTH_NTLM = 4; |
|
| 64 | - |
|
| 65 | - /** |
|
| 66 | - * Identity encoding, which basically does not nothing. |
|
| 67 | - */ |
|
| 68 | - const ENCODING_IDENTITY = 1; |
|
| 69 | - |
|
| 70 | - /** |
|
| 71 | - * Deflate encoding |
|
| 72 | - */ |
|
| 73 | - const ENCODING_DEFLATE = 2; |
|
| 74 | - |
|
| 75 | - /** |
|
| 76 | - * Gzip encoding |
|
| 77 | - */ |
|
| 78 | - const ENCODING_GZIP = 4; |
|
| 79 | - |
|
| 80 | - /** |
|
| 81 | - * Sends all encoding headers. |
|
| 82 | - */ |
|
| 83 | - const ENCODING_ALL = 7; |
|
| 84 | - |
|
| 85 | - /** |
|
| 86 | - * Content-encoding |
|
| 87 | - * |
|
| 88 | - * @var int |
|
| 89 | - */ |
|
| 90 | - protected $encoding = self::ENCODING_IDENTITY; |
|
| 91 | - |
|
| 92 | - /** |
|
| 93 | - * Constructor |
|
| 94 | - * |
|
| 95 | - * Settings are provided through the 'settings' argument. The following |
|
| 96 | - * settings are supported: |
|
| 97 | - * |
|
| 98 | - * * baseUri |
|
| 99 | - * * userName (optional) |
|
| 100 | - * * password (optional) |
|
| 101 | - * * proxy (optional) |
|
| 102 | - * * authType (optional) |
|
| 103 | - * * encoding (optional) |
|
| 104 | - * |
|
| 105 | - * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST |
|
| 106 | - * and self::AUTH_NTLM. If you know which authentication method will be |
|
| 107 | - * used, it's recommended to set it, as it will save a great deal of |
|
| 108 | - * requests to 'discover' this information. |
|
| 109 | - * |
|
| 110 | - * Encoding is a bitmap with one of the ENCODING constants. |
|
| 111 | - * |
|
| 112 | - * @param array $settings |
|
| 113 | - */ |
|
| 114 | - public function __construct(array $settings) { |
|
| 115 | - |
|
| 116 | - if (!isset($settings['baseUri'])) { |
|
| 117 | - throw new \InvalidArgumentException('A baseUri must be provided'); |
|
| 118 | - } |
|
| 119 | - |
|
| 120 | - parent::__construct(); |
|
| 121 | - |
|
| 122 | - $this->baseUri = $settings['baseUri']; |
|
| 123 | - |
|
| 124 | - if (isset($settings['proxy'])) { |
|
| 125 | - $this->addCurlSetting(CURLOPT_PROXY, $settings['proxy']); |
|
| 126 | - } |
|
| 127 | - |
|
| 128 | - if (isset($settings['userName'])) { |
|
| 129 | - $userName = $settings['userName']; |
|
| 130 | - $password = isset($settings['password']) ? $settings['password'] : ''; |
|
| 131 | - |
|
| 132 | - if (isset($settings['authType'])) { |
|
| 133 | - $curlType = 0; |
|
| 134 | - if ($settings['authType'] & self::AUTH_BASIC) { |
|
| 135 | - $curlType |= CURLAUTH_BASIC; |
|
| 136 | - } |
|
| 137 | - if ($settings['authType'] & self::AUTH_DIGEST) { |
|
| 138 | - $curlType |= CURLAUTH_DIGEST; |
|
| 139 | - } |
|
| 140 | - if ($settings['authType'] & self::AUTH_NTLM) { |
|
| 141 | - $curlType |= CURLAUTH_NTLM; |
|
| 142 | - } |
|
| 143 | - } else { |
|
| 144 | - $curlType = CURLAUTH_BASIC | CURLAUTH_DIGEST; |
|
| 145 | - } |
|
| 146 | - |
|
| 147 | - $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType); |
|
| 148 | - $this->addCurlSetting(CURLOPT_USERPWD, $userName . ':' . $password); |
|
| 149 | - |
|
| 150 | - } |
|
| 151 | - |
|
| 152 | - if (isset($settings['encoding'])) { |
|
| 153 | - $encoding = $settings['encoding']; |
|
| 154 | - |
|
| 155 | - $encodings = []; |
|
| 156 | - if ($encoding & self::ENCODING_IDENTITY) { |
|
| 157 | - $encodings[] = 'identity'; |
|
| 158 | - } |
|
| 159 | - if ($encoding & self::ENCODING_DEFLATE) { |
|
| 160 | - $encodings[] = 'deflate'; |
|
| 161 | - } |
|
| 162 | - if ($encoding & self::ENCODING_GZIP) { |
|
| 163 | - $encodings[] = 'gzip'; |
|
| 164 | - } |
|
| 165 | - $this->addCurlSetting(CURLOPT_ENCODING, implode(',', $encodings)); |
|
| 166 | - } |
|
| 167 | - |
|
| 168 | - $this->addCurlSetting(CURLOPT_USERAGENT, 'sabre-dav/' . Version::VERSION . ' (http://sabre.io/)'); |
|
| 169 | - |
|
| 170 | - $this->xml = new Xml\Service(); |
|
| 171 | - // BC |
|
| 172 | - $this->propertyMap = & $this->xml->elementMap; |
|
| 173 | - |
|
| 174 | - } |
|
| 175 | - |
|
| 176 | - /** |
|
| 177 | - * Does a PROPFIND request |
|
| 178 | - * |
|
| 179 | - * The list of requested properties must be specified as an array, in clark |
|
| 180 | - * notation. |
|
| 181 | - * |
|
| 182 | - * The returned array will contain a list of filenames as keys, and |
|
| 183 | - * properties as values. |
|
| 184 | - * |
|
| 185 | - * The properties array will contain the list of properties. Only properties |
|
| 186 | - * that are actually returned from the server (without error) will be |
|
| 187 | - * returned, anything else is discarded. |
|
| 188 | - * |
|
| 189 | - * Depth should be either 0 or 1. A depth of 1 will cause a request to be |
|
| 190 | - * made to the server to also return all child resources. |
|
| 191 | - * |
|
| 192 | - * @param string $url |
|
| 193 | - * @param array $properties |
|
| 194 | - * @param int $depth |
|
| 195 | - * @return array |
|
| 196 | - */ |
|
| 197 | - public function propFind($url, array $properties, $depth = 0) { |
|
| 198 | - |
|
| 199 | - $dom = new \DOMDocument('1.0', 'UTF-8'); |
|
| 200 | - $dom->formatOutput = true; |
|
| 201 | - $root = $dom->createElementNS('DAV:', 'd:propfind'); |
|
| 202 | - $prop = $dom->createElement('d:prop'); |
|
| 203 | - |
|
| 204 | - foreach ($properties as $property) { |
|
| 205 | - |
|
| 206 | - list( |
|
| 207 | - $namespace, |
|
| 208 | - $elementName |
|
| 209 | - ) = \Sabre\Xml\Service::parseClarkNotation($property); |
|
| 210 | - |
|
| 211 | - if ($namespace === 'DAV:') { |
|
| 212 | - $element = $dom->createElement('d:' . $elementName); |
|
| 213 | - } else { |
|
| 214 | - $element = $dom->createElementNS($namespace, 'x:' . $elementName); |
|
| 215 | - } |
|
| 216 | - |
|
| 217 | - $prop->appendChild($element); |
|
| 218 | - } |
|
| 219 | - |
|
| 220 | - $dom->appendChild($root)->appendChild($prop); |
|
| 221 | - $body = $dom->saveXML(); |
|
| 222 | - |
|
| 223 | - $url = $this->getAbsoluteUrl($url); |
|
| 224 | - |
|
| 225 | - $request = new HTTP\Request('PROPFIND', $url, [ |
|
| 226 | - 'Depth' => $depth, |
|
| 227 | - 'Content-Type' => 'application/xml' |
|
| 228 | - ], $body); |
|
| 229 | - |
|
| 230 | - $response = $this->send($request); |
|
| 231 | - |
|
| 232 | - if ((int)$response->getStatus() >= 400) { |
|
| 233 | - throw new \Sabre\HTTP\ClientHttpException($response); |
|
| 234 | - } |
|
| 235 | - |
|
| 236 | - $result = $this->parseMultiStatus($response->getBodyAsString()); |
|
| 237 | - |
|
| 238 | - // If depth was 0, we only return the top item |
|
| 239 | - if ($depth === 0) { |
|
| 240 | - reset($result); |
|
| 241 | - $result = current($result); |
|
| 242 | - return isset($result[200]) ? $result[200] : []; |
|
| 243 | - } |
|
| 244 | - |
|
| 245 | - $newResult = []; |
|
| 246 | - foreach ($result as $href => $statusList) { |
|
| 247 | - |
|
| 248 | - $newResult[$href] = isset($statusList[200]) ? $statusList[200] : []; |
|
| 249 | - |
|
| 250 | - } |
|
| 251 | - |
|
| 252 | - return $newResult; |
|
| 253 | - |
|
| 254 | - } |
|
| 255 | - |
|
| 256 | - /** |
|
| 257 | - * Updates a list of properties on the server |
|
| 258 | - * |
|
| 259 | - * The list of properties must have clark-notation properties for the keys, |
|
| 260 | - * and the actual (string) value for the value. If the value is null, an |
|
| 261 | - * attempt is made to delete the property. |
|
| 262 | - * |
|
| 263 | - * @param string $url |
|
| 264 | - * @param array $properties |
|
| 265 | - * @return bool |
|
| 266 | - */ |
|
| 267 | - public function propPatch($url, array $properties) { |
|
| 268 | - |
|
| 269 | - $propPatch = new Xml\Request\PropPatch(); |
|
| 270 | - $propPatch->properties = $properties; |
|
| 271 | - $xml = $this->xml->write( |
|
| 272 | - '{DAV:}propertyupdate', |
|
| 273 | - $propPatch |
|
| 274 | - ); |
|
| 275 | - |
|
| 276 | - $url = $this->getAbsoluteUrl($url); |
|
| 277 | - $request = new HTTP\Request('PROPPATCH', $url, [ |
|
| 278 | - 'Content-Type' => 'application/xml', |
|
| 279 | - ], $xml); |
|
| 280 | - $response = $this->send($request); |
|
| 281 | - |
|
| 282 | - if ($response->getStatus() >= 400) { |
|
| 283 | - throw new \Sabre\HTTP\ClientHttpException($response); |
|
| 284 | - } |
|
| 285 | - |
|
| 286 | - if ($response->getStatus() === 207) { |
|
| 287 | - // If it's a 207, the request could still have failed, but the |
|
| 288 | - // information is hidden in the response body. |
|
| 289 | - $result = $this->parseMultiStatus($response->getBodyAsString()); |
|
| 290 | - |
|
| 291 | - $errorProperties = []; |
|
| 292 | - foreach ($result as $href => $statusList) { |
|
| 293 | - foreach ($statusList as $status => $properties) { |
|
| 294 | - |
|
| 295 | - if ($status >= 400) { |
|
| 296 | - foreach ($properties as $propName => $propValue) { |
|
| 297 | - $errorProperties[] = $propName . ' (' . $status . ')'; |
|
| 298 | - } |
|
| 299 | - } |
|
| 300 | - |
|
| 301 | - } |
|
| 302 | - } |
|
| 303 | - if ($errorProperties) { |
|
| 304 | - |
|
| 305 | - throw new \Sabre\HTTP\ClientException('PROPPATCH failed. The following properties errored: ' . implode(', ', $errorProperties)); |
|
| 306 | - } |
|
| 307 | - } |
|
| 308 | - return true; |
|
| 309 | - |
|
| 310 | - } |
|
| 311 | - |
|
| 312 | - /** |
|
| 313 | - * Performs an HTTP options request |
|
| 314 | - * |
|
| 315 | - * This method returns all the features from the 'DAV:' header as an array. |
|
| 316 | - * If there was no DAV header, or no contents this method will return an |
|
| 317 | - * empty array. |
|
| 318 | - * |
|
| 319 | - * @return array |
|
| 320 | - */ |
|
| 321 | - public function options() { |
|
| 322 | - |
|
| 323 | - $request = new HTTP\Request('OPTIONS', $this->getAbsoluteUrl('')); |
|
| 324 | - $response = $this->send($request); |
|
| 325 | - |
|
| 326 | - $dav = $response->getHeader('Dav'); |
|
| 327 | - if (!$dav) { |
|
| 328 | - return []; |
|
| 329 | - } |
|
| 330 | - |
|
| 331 | - $features = explode(',', $dav); |
|
| 332 | - foreach ($features as &$v) { |
|
| 333 | - $v = trim($v); |
|
| 334 | - } |
|
| 335 | - return $features; |
|
| 336 | - |
|
| 337 | - } |
|
| 338 | - |
|
| 339 | - /** |
|
| 340 | - * Performs an actual HTTP request, and returns the result. |
|
| 341 | - * |
|
| 342 | - * If the specified url is relative, it will be expanded based on the base |
|
| 343 | - * url. |
|
| 344 | - * |
|
| 345 | - * The returned array contains 3 keys: |
|
| 346 | - * * body - the response body |
|
| 347 | - * * httpCode - a HTTP code (200, 404, etc) |
|
| 348 | - * * headers - a list of response http headers. The header names have |
|
| 349 | - * been lowercased. |
|
| 350 | - * |
|
| 351 | - * For large uploads, it's highly recommended to specify body as a stream |
|
| 352 | - * resource. You can easily do this by simply passing the result of |
|
| 353 | - * fopen(..., 'r'). |
|
| 354 | - * |
|
| 355 | - * This method will throw an exception if an HTTP error was received. Any |
|
| 356 | - * HTTP status code above 399 is considered an error. |
|
| 357 | - * |
|
| 358 | - * Note that it is no longer recommended to use this method, use the send() |
|
| 359 | - * method instead. |
|
| 360 | - * |
|
| 361 | - * @param string $method |
|
| 362 | - * @param string $url |
|
| 363 | - * @param string|resource|null $body |
|
| 364 | - * @param array $headers |
|
| 365 | - * @throws ClientException, in case a curl error occurred. |
|
| 366 | - * @return array |
|
| 367 | - */ |
|
| 368 | - public function request($method, $url = '', $body = null, array $headers = []) { |
|
| 369 | - |
|
| 370 | - $url = $this->getAbsoluteUrl($url); |
|
| 371 | - |
|
| 372 | - $response = $this->send(new HTTP\Request($method, $url, $headers, $body)); |
|
| 373 | - return [ |
|
| 374 | - 'body' => $response->getBodyAsString(), |
|
| 375 | - 'statusCode' => (int)$response->getStatus(), |
|
| 376 | - 'headers' => array_change_key_case($response->getHeaders()), |
|
| 377 | - ]; |
|
| 378 | - |
|
| 379 | - } |
|
| 380 | - |
|
| 381 | - /** |
|
| 382 | - * Returns the full url based on the given url (which may be relative). All |
|
| 383 | - * urls are expanded based on the base url as given by the server. |
|
| 384 | - * |
|
| 385 | - * @param string $url |
|
| 386 | - * @return string |
|
| 387 | - */ |
|
| 388 | - public function getAbsoluteUrl($url) { |
|
| 389 | - |
|
| 390 | - // If the url starts with http:// or https://, the url is already absolute. |
|
| 391 | - if (preg_match('/^http(s?):\/\//', $url)) { |
|
| 392 | - return $url; |
|
| 393 | - } |
|
| 394 | - |
|
| 395 | - // If the url starts with a slash, we must calculate the url based off |
|
| 396 | - // the root of the base url. |
|
| 397 | - if (strpos($url, '/') === 0) { |
|
| 398 | - $parts = parse_url($this->baseUri); |
|
| 399 | - return $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : '') . $url; |
|
| 400 | - } |
|
| 401 | - |
|
| 402 | - // Otherwise... |
|
| 403 | - return $this->baseUri . $url; |
|
| 404 | - |
|
| 405 | - } |
|
| 406 | - |
|
| 407 | - /** |
|
| 408 | - * Parses a WebDAV multistatus response body |
|
| 409 | - * |
|
| 410 | - * This method returns an array with the following structure |
|
| 411 | - * |
|
| 412 | - * [ |
|
| 413 | - * 'url/to/resource' => [ |
|
| 414 | - * '200' => [ |
|
| 415 | - * '{DAV:}property1' => 'value1', |
|
| 416 | - * '{DAV:}property2' => 'value2', |
|
| 417 | - * ], |
|
| 418 | - * '404' => [ |
|
| 419 | - * '{DAV:}property1' => null, |
|
| 420 | - * '{DAV:}property2' => null, |
|
| 421 | - * ], |
|
| 422 | - * ], |
|
| 423 | - * 'url/to/resource2' => [ |
|
| 424 | - * .. etc .. |
|
| 425 | - * ] |
|
| 426 | - * ] |
|
| 427 | - * |
|
| 428 | - * |
|
| 429 | - * @param string $body xml body |
|
| 430 | - * @return array |
|
| 431 | - */ |
|
| 432 | - public function parseMultiStatus($body) { |
|
| 433 | - |
|
| 434 | - $multistatus = $this->xml->expect('{DAV:}multistatus', $body); |
|
| 435 | - |
|
| 436 | - $result = []; |
|
| 437 | - |
|
| 438 | - foreach ($multistatus->getResponses() as $response) { |
|
| 439 | - |
|
| 440 | - $result[$response->getHref()] = $response->getResponseProperties(); |
|
| 441 | - |
|
| 442 | - } |
|
| 443 | - |
|
| 444 | - return $result; |
|
| 445 | - |
|
| 446 | - } |
|
| 21 | + /** |
|
| 22 | + * The xml service. |
|
| 23 | + * |
|
| 24 | + * Uset this service to configure the property and namespace maps. |
|
| 25 | + * |
|
| 26 | + * @var mixed |
|
| 27 | + */ |
|
| 28 | + public $xml; |
|
| 29 | + |
|
| 30 | + /** |
|
| 31 | + * The elementMap |
|
| 32 | + * |
|
| 33 | + * This property is linked via reference to $this->xml->elementMap. |
|
| 34 | + * It's deprecated as of version 3.0.0, and should no longer be used. |
|
| 35 | + * |
|
| 36 | + * @deprecated |
|
| 37 | + * @var array |
|
| 38 | + */ |
|
| 39 | + public $propertyMap = []; |
|
| 40 | + |
|
| 41 | + /** |
|
| 42 | + * Base URI |
|
| 43 | + * |
|
| 44 | + * This URI will be used to resolve relative urls. |
|
| 45 | + * |
|
| 46 | + * @var string |
|
| 47 | + */ |
|
| 48 | + protected $baseUri; |
|
| 49 | + |
|
| 50 | + /** |
|
| 51 | + * Basic authentication |
|
| 52 | + */ |
|
| 53 | + const AUTH_BASIC = 1; |
|
| 54 | + |
|
| 55 | + /** |
|
| 56 | + * Digest authentication |
|
| 57 | + */ |
|
| 58 | + const AUTH_DIGEST = 2; |
|
| 59 | + |
|
| 60 | + /** |
|
| 61 | + * NTLM authentication |
|
| 62 | + */ |
|
| 63 | + const AUTH_NTLM = 4; |
|
| 64 | + |
|
| 65 | + /** |
|
| 66 | + * Identity encoding, which basically does not nothing. |
|
| 67 | + */ |
|
| 68 | + const ENCODING_IDENTITY = 1; |
|
| 69 | + |
|
| 70 | + /** |
|
| 71 | + * Deflate encoding |
|
| 72 | + */ |
|
| 73 | + const ENCODING_DEFLATE = 2; |
|
| 74 | + |
|
| 75 | + /** |
|
| 76 | + * Gzip encoding |
|
| 77 | + */ |
|
| 78 | + const ENCODING_GZIP = 4; |
|
| 79 | + |
|
| 80 | + /** |
|
| 81 | + * Sends all encoding headers. |
|
| 82 | + */ |
|
| 83 | + const ENCODING_ALL = 7; |
|
| 84 | + |
|
| 85 | + /** |
|
| 86 | + * Content-encoding |
|
| 87 | + * |
|
| 88 | + * @var int |
|
| 89 | + */ |
|
| 90 | + protected $encoding = self::ENCODING_IDENTITY; |
|
| 91 | + |
|
| 92 | + /** |
|
| 93 | + * Constructor |
|
| 94 | + * |
|
| 95 | + * Settings are provided through the 'settings' argument. The following |
|
| 96 | + * settings are supported: |
|
| 97 | + * |
|
| 98 | + * * baseUri |
|
| 99 | + * * userName (optional) |
|
| 100 | + * * password (optional) |
|
| 101 | + * * proxy (optional) |
|
| 102 | + * * authType (optional) |
|
| 103 | + * * encoding (optional) |
|
| 104 | + * |
|
| 105 | + * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST |
|
| 106 | + * and self::AUTH_NTLM. If you know which authentication method will be |
|
| 107 | + * used, it's recommended to set it, as it will save a great deal of |
|
| 108 | + * requests to 'discover' this information. |
|
| 109 | + * |
|
| 110 | + * Encoding is a bitmap with one of the ENCODING constants. |
|
| 111 | + * |
|
| 112 | + * @param array $settings |
|
| 113 | + */ |
|
| 114 | + public function __construct(array $settings) { |
|
| 115 | + |
|
| 116 | + if (!isset($settings['baseUri'])) { |
|
| 117 | + throw new \InvalidArgumentException('A baseUri must be provided'); |
|
| 118 | + } |
|
| 119 | + |
|
| 120 | + parent::__construct(); |
|
| 121 | + |
|
| 122 | + $this->baseUri = $settings['baseUri']; |
|
| 123 | + |
|
| 124 | + if (isset($settings['proxy'])) { |
|
| 125 | + $this->addCurlSetting(CURLOPT_PROXY, $settings['proxy']); |
|
| 126 | + } |
|
| 127 | + |
|
| 128 | + if (isset($settings['userName'])) { |
|
| 129 | + $userName = $settings['userName']; |
|
| 130 | + $password = isset($settings['password']) ? $settings['password'] : ''; |
|
| 131 | + |
|
| 132 | + if (isset($settings['authType'])) { |
|
| 133 | + $curlType = 0; |
|
| 134 | + if ($settings['authType'] & self::AUTH_BASIC) { |
|
| 135 | + $curlType |= CURLAUTH_BASIC; |
|
| 136 | + } |
|
| 137 | + if ($settings['authType'] & self::AUTH_DIGEST) { |
|
| 138 | + $curlType |= CURLAUTH_DIGEST; |
|
| 139 | + } |
|
| 140 | + if ($settings['authType'] & self::AUTH_NTLM) { |
|
| 141 | + $curlType |= CURLAUTH_NTLM; |
|
| 142 | + } |
|
| 143 | + } else { |
|
| 144 | + $curlType = CURLAUTH_BASIC | CURLAUTH_DIGEST; |
|
| 145 | + } |
|
| 146 | + |
|
| 147 | + $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType); |
|
| 148 | + $this->addCurlSetting(CURLOPT_USERPWD, $userName . ':' . $password); |
|
| 149 | + |
|
| 150 | + } |
|
| 151 | + |
|
| 152 | + if (isset($settings['encoding'])) { |
|
| 153 | + $encoding = $settings['encoding']; |
|
| 154 | + |
|
| 155 | + $encodings = []; |
|
| 156 | + if ($encoding & self::ENCODING_IDENTITY) { |
|
| 157 | + $encodings[] = 'identity'; |
|
| 158 | + } |
|
| 159 | + if ($encoding & self::ENCODING_DEFLATE) { |
|
| 160 | + $encodings[] = 'deflate'; |
|
| 161 | + } |
|
| 162 | + if ($encoding & self::ENCODING_GZIP) { |
|
| 163 | + $encodings[] = 'gzip'; |
|
| 164 | + } |
|
| 165 | + $this->addCurlSetting(CURLOPT_ENCODING, implode(',', $encodings)); |
|
| 166 | + } |
|
| 167 | + |
|
| 168 | + $this->addCurlSetting(CURLOPT_USERAGENT, 'sabre-dav/' . Version::VERSION . ' (http://sabre.io/)'); |
|
| 169 | + |
|
| 170 | + $this->xml = new Xml\Service(); |
|
| 171 | + // BC |
|
| 172 | + $this->propertyMap = & $this->xml->elementMap; |
|
| 173 | + |
|
| 174 | + } |
|
| 175 | + |
|
| 176 | + /** |
|
| 177 | + * Does a PROPFIND request |
|
| 178 | + * |
|
| 179 | + * The list of requested properties must be specified as an array, in clark |
|
| 180 | + * notation. |
|
| 181 | + * |
|
| 182 | + * The returned array will contain a list of filenames as keys, and |
|
| 183 | + * properties as values. |
|
| 184 | + * |
|
| 185 | + * The properties array will contain the list of properties. Only properties |
|
| 186 | + * that are actually returned from the server (without error) will be |
|
| 187 | + * returned, anything else is discarded. |
|
| 188 | + * |
|
| 189 | + * Depth should be either 0 or 1. A depth of 1 will cause a request to be |
|
| 190 | + * made to the server to also return all child resources. |
|
| 191 | + * |
|
| 192 | + * @param string $url |
|
| 193 | + * @param array $properties |
|
| 194 | + * @param int $depth |
|
| 195 | + * @return array |
|
| 196 | + */ |
|
| 197 | + public function propFind($url, array $properties, $depth = 0) { |
|
| 198 | + |
|
| 199 | + $dom = new \DOMDocument('1.0', 'UTF-8'); |
|
| 200 | + $dom->formatOutput = true; |
|
| 201 | + $root = $dom->createElementNS('DAV:', 'd:propfind'); |
|
| 202 | + $prop = $dom->createElement('d:prop'); |
|
| 203 | + |
|
| 204 | + foreach ($properties as $property) { |
|
| 205 | + |
|
| 206 | + list( |
|
| 207 | + $namespace, |
|
| 208 | + $elementName |
|
| 209 | + ) = \Sabre\Xml\Service::parseClarkNotation($property); |
|
| 210 | + |
|
| 211 | + if ($namespace === 'DAV:') { |
|
| 212 | + $element = $dom->createElement('d:' . $elementName); |
|
| 213 | + } else { |
|
| 214 | + $element = $dom->createElementNS($namespace, 'x:' . $elementName); |
|
| 215 | + } |
|
| 216 | + |
|
| 217 | + $prop->appendChild($element); |
|
| 218 | + } |
|
| 219 | + |
|
| 220 | + $dom->appendChild($root)->appendChild($prop); |
|
| 221 | + $body = $dom->saveXML(); |
|
| 222 | + |
|
| 223 | + $url = $this->getAbsoluteUrl($url); |
|
| 224 | + |
|
| 225 | + $request = new HTTP\Request('PROPFIND', $url, [ |
|
| 226 | + 'Depth' => $depth, |
|
| 227 | + 'Content-Type' => 'application/xml' |
|
| 228 | + ], $body); |
|
| 229 | + |
|
| 230 | + $response = $this->send($request); |
|
| 231 | + |
|
| 232 | + if ((int)$response->getStatus() >= 400) { |
|
| 233 | + throw new \Sabre\HTTP\ClientHttpException($response); |
|
| 234 | + } |
|
| 235 | + |
|
| 236 | + $result = $this->parseMultiStatus($response->getBodyAsString()); |
|
| 237 | + |
|
| 238 | + // If depth was 0, we only return the top item |
|
| 239 | + if ($depth === 0) { |
|
| 240 | + reset($result); |
|
| 241 | + $result = current($result); |
|
| 242 | + return isset($result[200]) ? $result[200] : []; |
|
| 243 | + } |
|
| 244 | + |
|
| 245 | + $newResult = []; |
|
| 246 | + foreach ($result as $href => $statusList) { |
|
| 247 | + |
|
| 248 | + $newResult[$href] = isset($statusList[200]) ? $statusList[200] : []; |
|
| 249 | + |
|
| 250 | + } |
|
| 251 | + |
|
| 252 | + return $newResult; |
|
| 253 | + |
|
| 254 | + } |
|
| 255 | + |
|
| 256 | + /** |
|
| 257 | + * Updates a list of properties on the server |
|
| 258 | + * |
|
| 259 | + * The list of properties must have clark-notation properties for the keys, |
|
| 260 | + * and the actual (string) value for the value. If the value is null, an |
|
| 261 | + * attempt is made to delete the property. |
|
| 262 | + * |
|
| 263 | + * @param string $url |
|
| 264 | + * @param array $properties |
|
| 265 | + * @return bool |
|
| 266 | + */ |
|
| 267 | + public function propPatch($url, array $properties) { |
|
| 268 | + |
|
| 269 | + $propPatch = new Xml\Request\PropPatch(); |
|
| 270 | + $propPatch->properties = $properties; |
|
| 271 | + $xml = $this->xml->write( |
|
| 272 | + '{DAV:}propertyupdate', |
|
| 273 | + $propPatch |
|
| 274 | + ); |
|
| 275 | + |
|
| 276 | + $url = $this->getAbsoluteUrl($url); |
|
| 277 | + $request = new HTTP\Request('PROPPATCH', $url, [ |
|
| 278 | + 'Content-Type' => 'application/xml', |
|
| 279 | + ], $xml); |
|
| 280 | + $response = $this->send($request); |
|
| 281 | + |
|
| 282 | + if ($response->getStatus() >= 400) { |
|
| 283 | + throw new \Sabre\HTTP\ClientHttpException($response); |
|
| 284 | + } |
|
| 285 | + |
|
| 286 | + if ($response->getStatus() === 207) { |
|
| 287 | + // If it's a 207, the request could still have failed, but the |
|
| 288 | + // information is hidden in the response body. |
|
| 289 | + $result = $this->parseMultiStatus($response->getBodyAsString()); |
|
| 290 | + |
|
| 291 | + $errorProperties = []; |
|
| 292 | + foreach ($result as $href => $statusList) { |
|
| 293 | + foreach ($statusList as $status => $properties) { |
|
| 294 | + |
|
| 295 | + if ($status >= 400) { |
|
| 296 | + foreach ($properties as $propName => $propValue) { |
|
| 297 | + $errorProperties[] = $propName . ' (' . $status . ')'; |
|
| 298 | + } |
|
| 299 | + } |
|
| 300 | + |
|
| 301 | + } |
|
| 302 | + } |
|
| 303 | + if ($errorProperties) { |
|
| 304 | + |
|
| 305 | + throw new \Sabre\HTTP\ClientException('PROPPATCH failed. The following properties errored: ' . implode(', ', $errorProperties)); |
|
| 306 | + } |
|
| 307 | + } |
|
| 308 | + return true; |
|
| 309 | + |
|
| 310 | + } |
|
| 311 | + |
|
| 312 | + /** |
|
| 313 | + * Performs an HTTP options request |
|
| 314 | + * |
|
| 315 | + * This method returns all the features from the 'DAV:' header as an array. |
|
| 316 | + * If there was no DAV header, or no contents this method will return an |
|
| 317 | + * empty array. |
|
| 318 | + * |
|
| 319 | + * @return array |
|
| 320 | + */ |
|
| 321 | + public function options() { |
|
| 322 | + |
|
| 323 | + $request = new HTTP\Request('OPTIONS', $this->getAbsoluteUrl('')); |
|
| 324 | + $response = $this->send($request); |
|
| 325 | + |
|
| 326 | + $dav = $response->getHeader('Dav'); |
|
| 327 | + if (!$dav) { |
|
| 328 | + return []; |
|
| 329 | + } |
|
| 330 | + |
|
| 331 | + $features = explode(',', $dav); |
|
| 332 | + foreach ($features as &$v) { |
|
| 333 | + $v = trim($v); |
|
| 334 | + } |
|
| 335 | + return $features; |
|
| 336 | + |
|
| 337 | + } |
|
| 338 | + |
|
| 339 | + /** |
|
| 340 | + * Performs an actual HTTP request, and returns the result. |
|
| 341 | + * |
|
| 342 | + * If the specified url is relative, it will be expanded based on the base |
|
| 343 | + * url. |
|
| 344 | + * |
|
| 345 | + * The returned array contains 3 keys: |
|
| 346 | + * * body - the response body |
|
| 347 | + * * httpCode - a HTTP code (200, 404, etc) |
|
| 348 | + * * headers - a list of response http headers. The header names have |
|
| 349 | + * been lowercased. |
|
| 350 | + * |
|
| 351 | + * For large uploads, it's highly recommended to specify body as a stream |
|
| 352 | + * resource. You can easily do this by simply passing the result of |
|
| 353 | + * fopen(..., 'r'). |
|
| 354 | + * |
|
| 355 | + * This method will throw an exception if an HTTP error was received. Any |
|
| 356 | + * HTTP status code above 399 is considered an error. |
|
| 357 | + * |
|
| 358 | + * Note that it is no longer recommended to use this method, use the send() |
|
| 359 | + * method instead. |
|
| 360 | + * |
|
| 361 | + * @param string $method |
|
| 362 | + * @param string $url |
|
| 363 | + * @param string|resource|null $body |
|
| 364 | + * @param array $headers |
|
| 365 | + * @throws ClientException, in case a curl error occurred. |
|
| 366 | + * @return array |
|
| 367 | + */ |
|
| 368 | + public function request($method, $url = '', $body = null, array $headers = []) { |
|
| 369 | + |
|
| 370 | + $url = $this->getAbsoluteUrl($url); |
|
| 371 | + |
|
| 372 | + $response = $this->send(new HTTP\Request($method, $url, $headers, $body)); |
|
| 373 | + return [ |
|
| 374 | + 'body' => $response->getBodyAsString(), |
|
| 375 | + 'statusCode' => (int)$response->getStatus(), |
|
| 376 | + 'headers' => array_change_key_case($response->getHeaders()), |
|
| 377 | + ]; |
|
| 378 | + |
|
| 379 | + } |
|
| 380 | + |
|
| 381 | + /** |
|
| 382 | + * Returns the full url based on the given url (which may be relative). All |
|
| 383 | + * urls are expanded based on the base url as given by the server. |
|
| 384 | + * |
|
| 385 | + * @param string $url |
|
| 386 | + * @return string |
|
| 387 | + */ |
|
| 388 | + public function getAbsoluteUrl($url) { |
|
| 389 | + |
|
| 390 | + // If the url starts with http:// or https://, the url is already absolute. |
|
| 391 | + if (preg_match('/^http(s?):\/\//', $url)) { |
|
| 392 | + return $url; |
|
| 393 | + } |
|
| 394 | + |
|
| 395 | + // If the url starts with a slash, we must calculate the url based off |
|
| 396 | + // the root of the base url. |
|
| 397 | + if (strpos($url, '/') === 0) { |
|
| 398 | + $parts = parse_url($this->baseUri); |
|
| 399 | + return $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : '') . $url; |
|
| 400 | + } |
|
| 401 | + |
|
| 402 | + // Otherwise... |
|
| 403 | + return $this->baseUri . $url; |
|
| 404 | + |
|
| 405 | + } |
|
| 406 | + |
|
| 407 | + /** |
|
| 408 | + * Parses a WebDAV multistatus response body |
|
| 409 | + * |
|
| 410 | + * This method returns an array with the following structure |
|
| 411 | + * |
|
| 412 | + * [ |
|
| 413 | + * 'url/to/resource' => [ |
|
| 414 | + * '200' => [ |
|
| 415 | + * '{DAV:}property1' => 'value1', |
|
| 416 | + * '{DAV:}property2' => 'value2', |
|
| 417 | + * ], |
|
| 418 | + * '404' => [ |
|
| 419 | + * '{DAV:}property1' => null, |
|
| 420 | + * '{DAV:}property2' => null, |
|
| 421 | + * ], |
|
| 422 | + * ], |
|
| 423 | + * 'url/to/resource2' => [ |
|
| 424 | + * .. etc .. |
|
| 425 | + * ] |
|
| 426 | + * ] |
|
| 427 | + * |
|
| 428 | + * |
|
| 429 | + * @param string $body xml body |
|
| 430 | + * @return array |
|
| 431 | + */ |
|
| 432 | + public function parseMultiStatus($body) { |
|
| 433 | + |
|
| 434 | + $multistatus = $this->xml->expect('{DAV:}multistatus', $body); |
|
| 435 | + |
|
| 436 | + $result = []; |
|
| 437 | + |
|
| 438 | + foreach ($multistatus->getResponses() as $response) { |
|
| 439 | + |
|
| 440 | + $result[$response->getHref()] = $response->getResponseProperties(); |
|
| 441 | + |
|
| 442 | + } |
|
| 443 | + |
|
| 444 | + return $result; |
|
| 445 | + |
|
| 446 | + } |
|
| 447 | 447 | |
| 448 | 448 | } |
@@ -593,7 +593,7 @@ discard block |
||
| 593 | 593 | // If its an unknown value. we'll grab the default |
| 594 | 594 | if (!ctype_digit($depth)) return $default; |
| 595 | 595 | |
| 596 | - return (int)$depth; |
|
| 596 | + return (int) $depth; |
|
| 597 | 597 | |
| 598 | 598 | } |
| 599 | 599 | |
@@ -914,7 +914,7 @@ discard block |
||
| 914 | 914 | $path = trim($path, '/'); |
| 915 | 915 | |
| 916 | 916 | $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; |
| 917 | - $propFind = new PropFind($path, (array)$propertyNames, $depth, $propFindType); |
|
| 917 | + $propFind = new PropFind($path, (array) $propertyNames, $depth, $propFindType); |
|
| 918 | 918 | |
| 919 | 919 | $parentNode = $this->tree->getNodeForPath($path); |
| 920 | 920 | |
@@ -1399,7 +1399,7 @@ discard block |
||
| 1399 | 1399 | // Plugins are responsible for validating all the tokens. |
| 1400 | 1400 | // If a plugin deemed a token 'valid', it will set 'validToken' to |
| 1401 | 1401 | // true. |
| 1402 | - $this->emit('validateTokens', [ $request, &$ifConditions ]); |
|
| 1402 | + $this->emit('validateTokens', [$request, &$ifConditions]); |
|
| 1403 | 1403 | |
| 1404 | 1404 | // Now we're going to analyze the result. |
| 1405 | 1405 | |
@@ -18,1610 +18,1610 @@ |
||
| 18 | 18 | */ |
| 19 | 19 | class Server extends EventEmitter { |
| 20 | 20 | |
| 21 | - /** |
|
| 22 | - * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree |
|
| 23 | - */ |
|
| 24 | - const DEPTH_INFINITY = -1; |
|
| 25 | - |
|
| 26 | - /** |
|
| 27 | - * XML namespace for all SabreDAV related elements |
|
| 28 | - */ |
|
| 29 | - const NS_SABREDAV = 'http://sabredav.org/ns'; |
|
| 30 | - |
|
| 31 | - /** |
|
| 32 | - * The tree object |
|
| 33 | - * |
|
| 34 | - * @var Sabre\DAV\Tree |
|
| 35 | - */ |
|
| 36 | - public $tree; |
|
| 37 | - |
|
| 38 | - /** |
|
| 39 | - * The base uri |
|
| 40 | - * |
|
| 41 | - * @var string |
|
| 42 | - */ |
|
| 43 | - protected $baseUri = null; |
|
| 44 | - |
|
| 45 | - /** |
|
| 46 | - * httpResponse |
|
| 47 | - * |
|
| 48 | - * @var Sabre\HTTP\Response |
|
| 49 | - */ |
|
| 50 | - public $httpResponse; |
|
| 51 | - |
|
| 52 | - /** |
|
| 53 | - * httpRequest |
|
| 54 | - * |
|
| 55 | - * @var Sabre\HTTP\Request |
|
| 56 | - */ |
|
| 57 | - public $httpRequest; |
|
| 58 | - |
|
| 59 | - /** |
|
| 60 | - * PHP HTTP Sapi |
|
| 61 | - * |
|
| 62 | - * @var Sabre\HTTP\Sapi |
|
| 63 | - */ |
|
| 64 | - public $sapi; |
|
| 65 | - |
|
| 66 | - /** |
|
| 67 | - * The list of plugins |
|
| 68 | - * |
|
| 69 | - * @var array |
|
| 70 | - */ |
|
| 71 | - protected $plugins = []; |
|
| 72 | - |
|
| 73 | - /** |
|
| 74 | - * This property will be filled with a unique string that describes the |
|
| 75 | - * transaction. This is useful for performance measuring and logging |
|
| 76 | - * purposes. |
|
| 77 | - * |
|
| 78 | - * By default it will just fill it with a lowercased HTTP method name, but |
|
| 79 | - * plugins override this. For example, the WebDAV-Sync sync-collection |
|
| 80 | - * report will set this to 'report-sync-collection'. |
|
| 81 | - * |
|
| 82 | - * @var string |
|
| 83 | - */ |
|
| 84 | - public $transactionType; |
|
| 85 | - |
|
| 86 | - /** |
|
| 87 | - * This is a list of properties that are always server-controlled, and |
|
| 88 | - * must not get modified with PROPPATCH. |
|
| 89 | - * |
|
| 90 | - * Plugins may add to this list. |
|
| 91 | - * |
|
| 92 | - * @var string[] |
|
| 93 | - */ |
|
| 94 | - public $protectedProperties = [ |
|
| 95 | - |
|
| 96 | - // RFC4918 |
|
| 97 | - '{DAV:}getcontentlength', |
|
| 98 | - '{DAV:}getetag', |
|
| 99 | - '{DAV:}getlastmodified', |
|
| 100 | - '{DAV:}lockdiscovery', |
|
| 101 | - '{DAV:}supportedlock', |
|
| 102 | - |
|
| 103 | - // RFC4331 |
|
| 104 | - '{DAV:}quota-available-bytes', |
|
| 105 | - '{DAV:}quota-used-bytes', |
|
| 106 | - |
|
| 107 | - // RFC3744 |
|
| 108 | - '{DAV:}supported-privilege-set', |
|
| 109 | - '{DAV:}current-user-privilege-set', |
|
| 110 | - '{DAV:}acl', |
|
| 111 | - '{DAV:}acl-restrictions', |
|
| 112 | - '{DAV:}inherited-acl-set', |
|
| 113 | - |
|
| 114 | - // RFC3253 |
|
| 115 | - '{DAV:}supported-method-set', |
|
| 116 | - '{DAV:}supported-report-set', |
|
| 117 | - |
|
| 118 | - // RFC6578 |
|
| 119 | - '{DAV:}sync-token', |
|
| 120 | - |
|
| 121 | - // calendarserver.org extensions |
|
| 122 | - '{http://calendarserver.org/ns/}ctag', |
|
| 123 | - |
|
| 124 | - // sabredav extensions |
|
| 125 | - '{http://sabredav.org/ns}sync-token', |
|
| 126 | - |
|
| 127 | - ]; |
|
| 128 | - |
|
| 129 | - /** |
|
| 130 | - * This is a flag that allow or not showing file, line and code |
|
| 131 | - * of the exception in the returned XML |
|
| 132 | - * |
|
| 133 | - * @var bool |
|
| 134 | - */ |
|
| 135 | - public $debugExceptions = false; |
|
| 136 | - |
|
| 137 | - /** |
|
| 138 | - * This property allows you to automatically add the 'resourcetype' value |
|
| 139 | - * based on a node's classname or interface. |
|
| 140 | - * |
|
| 141 | - * The preset ensures that {DAV:}collection is automatically added for nodes |
|
| 142 | - * implementing Sabre\DAV\ICollection. |
|
| 143 | - * |
|
| 144 | - * @var array |
|
| 145 | - */ |
|
| 146 | - public $resourceTypeMapping = [ |
|
| 147 | - 'Sabre\\DAV\\ICollection' => '{DAV:}collection', |
|
| 148 | - ]; |
|
| 149 | - |
|
| 150 | - /** |
|
| 151 | - * This property allows the usage of Depth: infinity on PROPFIND requests. |
|
| 152 | - * |
|
| 153 | - * By default Depth: infinity is treated as Depth: 1. Allowing Depth: |
|
| 154 | - * infinity is potentially risky, as it allows a single client to do a full |
|
| 155 | - * index of the webdav server, which is an easy DoS attack vector. |
|
| 156 | - * |
|
| 157 | - * Only turn this on if you know what you're doing. |
|
| 158 | - * |
|
| 159 | - * @var bool |
|
| 160 | - */ |
|
| 161 | - public $enablePropfindDepthInfinity = false; |
|
| 162 | - |
|
| 163 | - /** |
|
| 164 | - * Reference to the XML utility object. |
|
| 165 | - * |
|
| 166 | - * @var Xml\Service |
|
| 167 | - */ |
|
| 168 | - public $xml; |
|
| 169 | - |
|
| 170 | - /** |
|
| 171 | - * If this setting is turned off, SabreDAV's version number will be hidden |
|
| 172 | - * from various places. |
|
| 173 | - * |
|
| 174 | - * Some people feel this is a good security measure. |
|
| 175 | - * |
|
| 176 | - * @var bool |
|
| 177 | - */ |
|
| 178 | - static $exposeVersion = true; |
|
| 179 | - |
|
| 180 | - /** |
|
| 181 | - * Sets up the server |
|
| 182 | - * |
|
| 183 | - * If a Sabre\DAV\Tree object is passed as an argument, it will |
|
| 184 | - * use it as the directory tree. If a Sabre\DAV\INode is passed, it |
|
| 185 | - * will create a Sabre\DAV\Tree and use the node as the root. |
|
| 186 | - * |
|
| 187 | - * If nothing is passed, a Sabre\DAV\SimpleCollection is created in |
|
| 188 | - * a Sabre\DAV\Tree. |
|
| 189 | - * |
|
| 190 | - * If an array is passed, we automatically create a root node, and use |
|
| 191 | - * the nodes in the array as top-level children. |
|
| 192 | - * |
|
| 193 | - * @param Tree|INode|array|null $treeOrNode The tree object |
|
| 194 | - */ |
|
| 195 | - public function __construct($treeOrNode = null) { |
|
| 196 | - |
|
| 197 | - if ($treeOrNode instanceof Tree) { |
|
| 198 | - $this->tree = $treeOrNode; |
|
| 199 | - } elseif ($treeOrNode instanceof INode) { |
|
| 200 | - $this->tree = new Tree($treeOrNode); |
|
| 201 | - } elseif (is_array($treeOrNode)) { |
|
| 202 | - |
|
| 203 | - // If it's an array, a list of nodes was passed, and we need to |
|
| 204 | - // create the root node. |
|
| 205 | - foreach ($treeOrNode as $node) { |
|
| 206 | - if (!($node instanceof INode)) { |
|
| 207 | - throw new Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Sabre\\DAV\\INode'); |
|
| 208 | - } |
|
| 209 | - } |
|
| 210 | - |
|
| 211 | - $root = new SimpleCollection('root', $treeOrNode); |
|
| 212 | - $this->tree = new Tree($root); |
|
| 213 | - |
|
| 214 | - } elseif (is_null($treeOrNode)) { |
|
| 215 | - $root = new SimpleCollection('root'); |
|
| 216 | - $this->tree = new Tree($root); |
|
| 217 | - } else { |
|
| 218 | - throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null'); |
|
| 219 | - } |
|
| 220 | - |
|
| 221 | - $this->xml = new Xml\Service(); |
|
| 222 | - $this->sapi = new HTTP\Sapi(); |
|
| 223 | - $this->httpResponse = new HTTP\Response(); |
|
| 224 | - $this->httpRequest = $this->sapi->getRequest(); |
|
| 225 | - $this->addPlugin(new CorePlugin()); |
|
| 226 | - |
|
| 227 | - } |
|
| 228 | - |
|
| 229 | - /** |
|
| 230 | - * Starts the DAV Server |
|
| 231 | - * |
|
| 232 | - * @return void |
|
| 233 | - */ |
|
| 234 | - public function exec() { |
|
| 235 | - |
|
| 236 | - try { |
|
| 237 | - |
|
| 238 | - // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an |
|
| 239 | - // origin, we must make sure we send back HTTP/1.0 if this was |
|
| 240 | - // requested. |
|
| 241 | - // This is mainly because nginx doesn't support Chunked Transfer |
|
| 242 | - // Encoding, and this forces the webserver SabreDAV is running on, |
|
| 243 | - // to buffer entire responses to calculate Content-Length. |
|
| 244 | - $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion()); |
|
| 245 | - |
|
| 246 | - // Setting the base url |
|
| 247 | - $this->httpRequest->setBaseUrl($this->getBaseUri()); |
|
| 248 | - $this->invokeMethod($this->httpRequest, $this->httpResponse); |
|
| 249 | - |
|
| 250 | - } catch (\Exception $e) { |
|
| 251 | - |
|
| 252 | - try { |
|
| 253 | - $this->emit('exception', [$e]); |
|
| 254 | - } catch (\Exception $ignore) { |
|
| 255 | - } |
|
| 256 | - $DOM = new \DOMDocument('1.0', 'utf-8'); |
|
| 257 | - $DOM->formatOutput = true; |
|
| 258 | - |
|
| 259 | - $error = $DOM->createElementNS('DAV:', 'd:error'); |
|
| 260 | - $error->setAttribute('xmlns:s', self::NS_SABREDAV); |
|
| 261 | - $DOM->appendChild($error); |
|
| 262 | - |
|
| 263 | - $h = function($v) { |
|
| 264 | - |
|
| 265 | - return htmlspecialchars($v, ENT_NOQUOTES, 'UTF-8'); |
|
| 266 | - |
|
| 267 | - }; |
|
| 268 | - |
|
| 269 | - if (self::$exposeVersion) { |
|
| 270 | - $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION))); |
|
| 271 | - } |
|
| 272 | - |
|
| 273 | - $error->appendChild($DOM->createElement('s:exception', $h(get_class($e)))); |
|
| 274 | - $error->appendChild($DOM->createElement('s:message', $h($e->getMessage()))); |
|
| 275 | - if ($this->debugExceptions) { |
|
| 276 | - $error->appendChild($DOM->createElement('s:file', $h($e->getFile()))); |
|
| 277 | - $error->appendChild($DOM->createElement('s:line', $h($e->getLine()))); |
|
| 278 | - $error->appendChild($DOM->createElement('s:code', $h($e->getCode()))); |
|
| 279 | - $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString()))); |
|
| 280 | - } |
|
| 281 | - |
|
| 282 | - if ($this->debugExceptions) { |
|
| 283 | - $previous = $e; |
|
| 284 | - while ($previous = $previous->getPrevious()) { |
|
| 285 | - $xPrevious = $DOM->createElement('s:previous-exception'); |
|
| 286 | - $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous)))); |
|
| 287 | - $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage()))); |
|
| 288 | - $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile()))); |
|
| 289 | - $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine()))); |
|
| 290 | - $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode()))); |
|
| 291 | - $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString()))); |
|
| 292 | - $error->appendChild($xPrevious); |
|
| 293 | - } |
|
| 294 | - } |
|
| 295 | - |
|
| 296 | - |
|
| 297 | - if ($e instanceof Exception) { |
|
| 298 | - |
|
| 299 | - $httpCode = $e->getHTTPCode(); |
|
| 300 | - $e->serialize($this, $error); |
|
| 301 | - $headers = $e->getHTTPHeaders($this); |
|
| 302 | - |
|
| 303 | - } else { |
|
| 304 | - |
|
| 305 | - $httpCode = 500; |
|
| 306 | - $headers = []; |
|
| 307 | - |
|
| 308 | - } |
|
| 309 | - $headers['Content-Type'] = 'application/xml; charset=utf-8'; |
|
| 310 | - |
|
| 311 | - $this->httpResponse->setStatus($httpCode); |
|
| 312 | - $this->httpResponse->setHeaders($headers); |
|
| 313 | - $this->httpResponse->setBody($DOM->saveXML()); |
|
| 314 | - $this->sapi->sendResponse($this->httpResponse); |
|
| 315 | - |
|
| 316 | - } |
|
| 317 | - |
|
| 318 | - } |
|
| 319 | - |
|
| 320 | - /** |
|
| 321 | - * Sets the base server uri |
|
| 322 | - * |
|
| 323 | - * @param string $uri |
|
| 324 | - * @return void |
|
| 325 | - */ |
|
| 326 | - public function setBaseUri($uri) { |
|
| 327 | - |
|
| 328 | - // If the baseUri does not end with a slash, we must add it |
|
| 329 | - if ($uri[strlen($uri) - 1] !== '/') |
|
| 330 | - $uri .= '/'; |
|
| 331 | - |
|
| 332 | - $this->baseUri = $uri; |
|
| 333 | - |
|
| 334 | - } |
|
| 335 | - |
|
| 336 | - /** |
|
| 337 | - * Returns the base responding uri |
|
| 338 | - * |
|
| 339 | - * @return string |
|
| 340 | - */ |
|
| 341 | - public function getBaseUri() { |
|
| 342 | - |
|
| 343 | - if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri(); |
|
| 344 | - return $this->baseUri; |
|
| 345 | - |
|
| 346 | - } |
|
| 347 | - |
|
| 348 | - /** |
|
| 349 | - * This method attempts to detect the base uri. |
|
| 350 | - * Only the PATH_INFO variable is considered. |
|
| 351 | - * |
|
| 352 | - * If this variable is not set, the root (/) is assumed. |
|
| 353 | - * |
|
| 354 | - * @return string |
|
| 355 | - */ |
|
| 356 | - public function guessBaseUri() { |
|
| 357 | - |
|
| 358 | - $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO'); |
|
| 359 | - $uri = $this->httpRequest->getRawServerValue('REQUEST_URI'); |
|
| 360 | - |
|
| 361 | - // If PATH_INFO is found, we can assume it's accurate. |
|
| 362 | - if (!empty($pathInfo)) { |
|
| 363 | - |
|
| 364 | - // We need to make sure we ignore the QUERY_STRING part |
|
| 365 | - if ($pos = strpos($uri, '?')) |
|
| 366 | - $uri = substr($uri, 0, $pos); |
|
| 367 | - |
|
| 368 | - // PATH_INFO is only set for urls, such as: /example.php/path |
|
| 369 | - // in that case PATH_INFO contains '/path'. |
|
| 370 | - // Note that REQUEST_URI is percent encoded, while PATH_INFO is |
|
| 371 | - // not, Therefore they are only comparable if we first decode |
|
| 372 | - // REQUEST_INFO as well. |
|
| 373 | - $decodedUri = URLUtil::decodePath($uri); |
|
| 374 | - |
|
| 375 | - // A simple sanity check: |
|
| 376 | - if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) { |
|
| 377 | - $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo)); |
|
| 378 | - return rtrim($baseUri, '/') . '/'; |
|
| 379 | - } |
|
| 380 | - |
|
| 381 | - throw new Exception('The REQUEST_URI (' . $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.'); |
|
| 382 | - |
|
| 383 | - } |
|
| 384 | - |
|
| 385 | - // The last fallback is that we're just going to assume the server root. |
|
| 386 | - return '/'; |
|
| 387 | - |
|
| 388 | - } |
|
| 389 | - |
|
| 390 | - /** |
|
| 391 | - * Adds a plugin to the server |
|
| 392 | - * |
|
| 393 | - * For more information, console the documentation of Sabre\DAV\ServerPlugin |
|
| 394 | - * |
|
| 395 | - * @param ServerPlugin $plugin |
|
| 396 | - * @return void |
|
| 397 | - */ |
|
| 398 | - public function addPlugin(ServerPlugin $plugin) { |
|
| 399 | - |
|
| 400 | - $this->plugins[$plugin->getPluginName()] = $plugin; |
|
| 401 | - $plugin->initialize($this); |
|
| 402 | - |
|
| 403 | - } |
|
| 404 | - |
|
| 405 | - /** |
|
| 406 | - * Returns an initialized plugin by it's name. |
|
| 407 | - * |
|
| 408 | - * This function returns null if the plugin was not found. |
|
| 409 | - * |
|
| 410 | - * @param string $name |
|
| 411 | - * @return ServerPlugin |
|
| 412 | - */ |
|
| 413 | - public function getPlugin($name) { |
|
| 414 | - |
|
| 415 | - if (isset($this->plugins[$name])) |
|
| 416 | - return $this->plugins[$name]; |
|
| 417 | - |
|
| 418 | - return null; |
|
| 419 | - |
|
| 420 | - } |
|
| 421 | - |
|
| 422 | - /** |
|
| 423 | - * Returns all plugins |
|
| 424 | - * |
|
| 425 | - * @return array |
|
| 426 | - */ |
|
| 427 | - public function getPlugins() { |
|
| 428 | - |
|
| 429 | - return $this->plugins; |
|
| 430 | - |
|
| 431 | - } |
|
| 432 | - |
|
| 433 | - /** |
|
| 434 | - * Handles a http request, and execute a method based on its name |
|
| 435 | - * |
|
| 436 | - * @param RequestInterface $request |
|
| 437 | - * @param ResponseInterface $response |
|
| 438 | - * @param $sendResponse Whether to send the HTTP response to the DAV client. |
|
| 439 | - * @return void |
|
| 440 | - */ |
|
| 441 | - public function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true) { |
|
| 442 | - |
|
| 443 | - $method = $request->getMethod(); |
|
| 444 | - |
|
| 445 | - if (!$this->emit('beforeMethod:' . $method, [$request, $response])) return; |
|
| 446 | - if (!$this->emit('beforeMethod', [$request, $response])) return; |
|
| 447 | - |
|
| 448 | - if (self::$exposeVersion) { |
|
| 449 | - $response->setHeader('X-Sabre-Version', Version::VERSION); |
|
| 450 | - } |
|
| 451 | - |
|
| 452 | - $this->transactionType = strtolower($method); |
|
| 453 | - |
|
| 454 | - if (!$this->checkPreconditions($request, $response)) { |
|
| 455 | - $this->sapi->sendResponse($response); |
|
| 456 | - return; |
|
| 457 | - } |
|
| 458 | - |
|
| 459 | - if ($this->emit('method:' . $method, [$request, $response])) { |
|
| 460 | - if ($this->emit('method', [$request, $response])) { |
|
| 461 | - $exMessage = "There was no plugin in the system that was willing to handle this " . $method . " method."; |
|
| 462 | - if ($method === "GET") { |
|
| 463 | - $exMessage .= " Enable the Browser plugin to get a better result here."; |
|
| 464 | - } |
|
| 465 | - |
|
| 466 | - // Unsupported method |
|
| 467 | - throw new Exception\NotImplemented($exMessage); |
|
| 468 | - } |
|
| 469 | - } |
|
| 470 | - |
|
| 471 | - if (!$this->emit('afterMethod:' . $method, [$request, $response])) return; |
|
| 472 | - if (!$this->emit('afterMethod', [$request, $response])) return; |
|
| 21 | + /** |
|
| 22 | + * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree |
|
| 23 | + */ |
|
| 24 | + const DEPTH_INFINITY = -1; |
|
| 25 | + |
|
| 26 | + /** |
|
| 27 | + * XML namespace for all SabreDAV related elements |
|
| 28 | + */ |
|
| 29 | + const NS_SABREDAV = 'http://sabredav.org/ns'; |
|
| 30 | + |
|
| 31 | + /** |
|
| 32 | + * The tree object |
|
| 33 | + * |
|
| 34 | + * @var Sabre\DAV\Tree |
|
| 35 | + */ |
|
| 36 | + public $tree; |
|
| 37 | + |
|
| 38 | + /** |
|
| 39 | + * The base uri |
|
| 40 | + * |
|
| 41 | + * @var string |
|
| 42 | + */ |
|
| 43 | + protected $baseUri = null; |
|
| 44 | + |
|
| 45 | + /** |
|
| 46 | + * httpResponse |
|
| 47 | + * |
|
| 48 | + * @var Sabre\HTTP\Response |
|
| 49 | + */ |
|
| 50 | + public $httpResponse; |
|
| 51 | + |
|
| 52 | + /** |
|
| 53 | + * httpRequest |
|
| 54 | + * |
|
| 55 | + * @var Sabre\HTTP\Request |
|
| 56 | + */ |
|
| 57 | + public $httpRequest; |
|
| 58 | + |
|
| 59 | + /** |
|
| 60 | + * PHP HTTP Sapi |
|
| 61 | + * |
|
| 62 | + * @var Sabre\HTTP\Sapi |
|
| 63 | + */ |
|
| 64 | + public $sapi; |
|
| 65 | + |
|
| 66 | + /** |
|
| 67 | + * The list of plugins |
|
| 68 | + * |
|
| 69 | + * @var array |
|
| 70 | + */ |
|
| 71 | + protected $plugins = []; |
|
| 72 | + |
|
| 73 | + /** |
|
| 74 | + * This property will be filled with a unique string that describes the |
|
| 75 | + * transaction. This is useful for performance measuring and logging |
|
| 76 | + * purposes. |
|
| 77 | + * |
|
| 78 | + * By default it will just fill it with a lowercased HTTP method name, but |
|
| 79 | + * plugins override this. For example, the WebDAV-Sync sync-collection |
|
| 80 | + * report will set this to 'report-sync-collection'. |
|
| 81 | + * |
|
| 82 | + * @var string |
|
| 83 | + */ |
|
| 84 | + public $transactionType; |
|
| 85 | + |
|
| 86 | + /** |
|
| 87 | + * This is a list of properties that are always server-controlled, and |
|
| 88 | + * must not get modified with PROPPATCH. |
|
| 89 | + * |
|
| 90 | + * Plugins may add to this list. |
|
| 91 | + * |
|
| 92 | + * @var string[] |
|
| 93 | + */ |
|
| 94 | + public $protectedProperties = [ |
|
| 95 | + |
|
| 96 | + // RFC4918 |
|
| 97 | + '{DAV:}getcontentlength', |
|
| 98 | + '{DAV:}getetag', |
|
| 99 | + '{DAV:}getlastmodified', |
|
| 100 | + '{DAV:}lockdiscovery', |
|
| 101 | + '{DAV:}supportedlock', |
|
| 102 | + |
|
| 103 | + // RFC4331 |
|
| 104 | + '{DAV:}quota-available-bytes', |
|
| 105 | + '{DAV:}quota-used-bytes', |
|
| 106 | + |
|
| 107 | + // RFC3744 |
|
| 108 | + '{DAV:}supported-privilege-set', |
|
| 109 | + '{DAV:}current-user-privilege-set', |
|
| 110 | + '{DAV:}acl', |
|
| 111 | + '{DAV:}acl-restrictions', |
|
| 112 | + '{DAV:}inherited-acl-set', |
|
| 113 | + |
|
| 114 | + // RFC3253 |
|
| 115 | + '{DAV:}supported-method-set', |
|
| 116 | + '{DAV:}supported-report-set', |
|
| 117 | + |
|
| 118 | + // RFC6578 |
|
| 119 | + '{DAV:}sync-token', |
|
| 120 | + |
|
| 121 | + // calendarserver.org extensions |
|
| 122 | + '{http://calendarserver.org/ns/}ctag', |
|
| 123 | + |
|
| 124 | + // sabredav extensions |
|
| 125 | + '{http://sabredav.org/ns}sync-token', |
|
| 126 | + |
|
| 127 | + ]; |
|
| 128 | + |
|
| 129 | + /** |
|
| 130 | + * This is a flag that allow or not showing file, line and code |
|
| 131 | + * of the exception in the returned XML |
|
| 132 | + * |
|
| 133 | + * @var bool |
|
| 134 | + */ |
|
| 135 | + public $debugExceptions = false; |
|
| 136 | + |
|
| 137 | + /** |
|
| 138 | + * This property allows you to automatically add the 'resourcetype' value |
|
| 139 | + * based on a node's classname or interface. |
|
| 140 | + * |
|
| 141 | + * The preset ensures that {DAV:}collection is automatically added for nodes |
|
| 142 | + * implementing Sabre\DAV\ICollection. |
|
| 143 | + * |
|
| 144 | + * @var array |
|
| 145 | + */ |
|
| 146 | + public $resourceTypeMapping = [ |
|
| 147 | + 'Sabre\\DAV\\ICollection' => '{DAV:}collection', |
|
| 148 | + ]; |
|
| 149 | + |
|
| 150 | + /** |
|
| 151 | + * This property allows the usage of Depth: infinity on PROPFIND requests. |
|
| 152 | + * |
|
| 153 | + * By default Depth: infinity is treated as Depth: 1. Allowing Depth: |
|
| 154 | + * infinity is potentially risky, as it allows a single client to do a full |
|
| 155 | + * index of the webdav server, which is an easy DoS attack vector. |
|
| 156 | + * |
|
| 157 | + * Only turn this on if you know what you're doing. |
|
| 158 | + * |
|
| 159 | + * @var bool |
|
| 160 | + */ |
|
| 161 | + public $enablePropfindDepthInfinity = false; |
|
| 162 | + |
|
| 163 | + /** |
|
| 164 | + * Reference to the XML utility object. |
|
| 165 | + * |
|
| 166 | + * @var Xml\Service |
|
| 167 | + */ |
|
| 168 | + public $xml; |
|
| 169 | + |
|
| 170 | + /** |
|
| 171 | + * If this setting is turned off, SabreDAV's version number will be hidden |
|
| 172 | + * from various places. |
|
| 173 | + * |
|
| 174 | + * Some people feel this is a good security measure. |
|
| 175 | + * |
|
| 176 | + * @var bool |
|
| 177 | + */ |
|
| 178 | + static $exposeVersion = true; |
|
| 179 | + |
|
| 180 | + /** |
|
| 181 | + * Sets up the server |
|
| 182 | + * |
|
| 183 | + * If a Sabre\DAV\Tree object is passed as an argument, it will |
|
| 184 | + * use it as the directory tree. If a Sabre\DAV\INode is passed, it |
|
| 185 | + * will create a Sabre\DAV\Tree and use the node as the root. |
|
| 186 | + * |
|
| 187 | + * If nothing is passed, a Sabre\DAV\SimpleCollection is created in |
|
| 188 | + * a Sabre\DAV\Tree. |
|
| 189 | + * |
|
| 190 | + * If an array is passed, we automatically create a root node, and use |
|
| 191 | + * the nodes in the array as top-level children. |
|
| 192 | + * |
|
| 193 | + * @param Tree|INode|array|null $treeOrNode The tree object |
|
| 194 | + */ |
|
| 195 | + public function __construct($treeOrNode = null) { |
|
| 196 | + |
|
| 197 | + if ($treeOrNode instanceof Tree) { |
|
| 198 | + $this->tree = $treeOrNode; |
|
| 199 | + } elseif ($treeOrNode instanceof INode) { |
|
| 200 | + $this->tree = new Tree($treeOrNode); |
|
| 201 | + } elseif (is_array($treeOrNode)) { |
|
| 202 | + |
|
| 203 | + // If it's an array, a list of nodes was passed, and we need to |
|
| 204 | + // create the root node. |
|
| 205 | + foreach ($treeOrNode as $node) { |
|
| 206 | + if (!($node instanceof INode)) { |
|
| 207 | + throw new Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Sabre\\DAV\\INode'); |
|
| 208 | + } |
|
| 209 | + } |
|
| 210 | + |
|
| 211 | + $root = new SimpleCollection('root', $treeOrNode); |
|
| 212 | + $this->tree = new Tree($root); |
|
| 213 | + |
|
| 214 | + } elseif (is_null($treeOrNode)) { |
|
| 215 | + $root = new SimpleCollection('root'); |
|
| 216 | + $this->tree = new Tree($root); |
|
| 217 | + } else { |
|
| 218 | + throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null'); |
|
| 219 | + } |
|
| 220 | + |
|
| 221 | + $this->xml = new Xml\Service(); |
|
| 222 | + $this->sapi = new HTTP\Sapi(); |
|
| 223 | + $this->httpResponse = new HTTP\Response(); |
|
| 224 | + $this->httpRequest = $this->sapi->getRequest(); |
|
| 225 | + $this->addPlugin(new CorePlugin()); |
|
| 226 | + |
|
| 227 | + } |
|
| 228 | + |
|
| 229 | + /** |
|
| 230 | + * Starts the DAV Server |
|
| 231 | + * |
|
| 232 | + * @return void |
|
| 233 | + */ |
|
| 234 | + public function exec() { |
|
| 235 | + |
|
| 236 | + try { |
|
| 237 | + |
|
| 238 | + // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an |
|
| 239 | + // origin, we must make sure we send back HTTP/1.0 if this was |
|
| 240 | + // requested. |
|
| 241 | + // This is mainly because nginx doesn't support Chunked Transfer |
|
| 242 | + // Encoding, and this forces the webserver SabreDAV is running on, |
|
| 243 | + // to buffer entire responses to calculate Content-Length. |
|
| 244 | + $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion()); |
|
| 245 | + |
|
| 246 | + // Setting the base url |
|
| 247 | + $this->httpRequest->setBaseUrl($this->getBaseUri()); |
|
| 248 | + $this->invokeMethod($this->httpRequest, $this->httpResponse); |
|
| 249 | + |
|
| 250 | + } catch (\Exception $e) { |
|
| 251 | + |
|
| 252 | + try { |
|
| 253 | + $this->emit('exception', [$e]); |
|
| 254 | + } catch (\Exception $ignore) { |
|
| 255 | + } |
|
| 256 | + $DOM = new \DOMDocument('1.0', 'utf-8'); |
|
| 257 | + $DOM->formatOutput = true; |
|
| 258 | + |
|
| 259 | + $error = $DOM->createElementNS('DAV:', 'd:error'); |
|
| 260 | + $error->setAttribute('xmlns:s', self::NS_SABREDAV); |
|
| 261 | + $DOM->appendChild($error); |
|
| 262 | + |
|
| 263 | + $h = function($v) { |
|
| 264 | + |
|
| 265 | + return htmlspecialchars($v, ENT_NOQUOTES, 'UTF-8'); |
|
| 266 | + |
|
| 267 | + }; |
|
| 268 | + |
|
| 269 | + if (self::$exposeVersion) { |
|
| 270 | + $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION))); |
|
| 271 | + } |
|
| 272 | + |
|
| 273 | + $error->appendChild($DOM->createElement('s:exception', $h(get_class($e)))); |
|
| 274 | + $error->appendChild($DOM->createElement('s:message', $h($e->getMessage()))); |
|
| 275 | + if ($this->debugExceptions) { |
|
| 276 | + $error->appendChild($DOM->createElement('s:file', $h($e->getFile()))); |
|
| 277 | + $error->appendChild($DOM->createElement('s:line', $h($e->getLine()))); |
|
| 278 | + $error->appendChild($DOM->createElement('s:code', $h($e->getCode()))); |
|
| 279 | + $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString()))); |
|
| 280 | + } |
|
| 281 | + |
|
| 282 | + if ($this->debugExceptions) { |
|
| 283 | + $previous = $e; |
|
| 284 | + while ($previous = $previous->getPrevious()) { |
|
| 285 | + $xPrevious = $DOM->createElement('s:previous-exception'); |
|
| 286 | + $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous)))); |
|
| 287 | + $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage()))); |
|
| 288 | + $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile()))); |
|
| 289 | + $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine()))); |
|
| 290 | + $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode()))); |
|
| 291 | + $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString()))); |
|
| 292 | + $error->appendChild($xPrevious); |
|
| 293 | + } |
|
| 294 | + } |
|
| 295 | + |
|
| 296 | + |
|
| 297 | + if ($e instanceof Exception) { |
|
| 298 | + |
|
| 299 | + $httpCode = $e->getHTTPCode(); |
|
| 300 | + $e->serialize($this, $error); |
|
| 301 | + $headers = $e->getHTTPHeaders($this); |
|
| 302 | + |
|
| 303 | + } else { |
|
| 304 | + |
|
| 305 | + $httpCode = 500; |
|
| 306 | + $headers = []; |
|
| 307 | + |
|
| 308 | + } |
|
| 309 | + $headers['Content-Type'] = 'application/xml; charset=utf-8'; |
|
| 310 | + |
|
| 311 | + $this->httpResponse->setStatus($httpCode); |
|
| 312 | + $this->httpResponse->setHeaders($headers); |
|
| 313 | + $this->httpResponse->setBody($DOM->saveXML()); |
|
| 314 | + $this->sapi->sendResponse($this->httpResponse); |
|
| 315 | + |
|
| 316 | + } |
|
| 317 | + |
|
| 318 | + } |
|
| 319 | + |
|
| 320 | + /** |
|
| 321 | + * Sets the base server uri |
|
| 322 | + * |
|
| 323 | + * @param string $uri |
|
| 324 | + * @return void |
|
| 325 | + */ |
|
| 326 | + public function setBaseUri($uri) { |
|
| 327 | + |
|
| 328 | + // If the baseUri does not end with a slash, we must add it |
|
| 329 | + if ($uri[strlen($uri) - 1] !== '/') |
|
| 330 | + $uri .= '/'; |
|
| 331 | + |
|
| 332 | + $this->baseUri = $uri; |
|
| 333 | + |
|
| 334 | + } |
|
| 335 | + |
|
| 336 | + /** |
|
| 337 | + * Returns the base responding uri |
|
| 338 | + * |
|
| 339 | + * @return string |
|
| 340 | + */ |
|
| 341 | + public function getBaseUri() { |
|
| 342 | + |
|
| 343 | + if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri(); |
|
| 344 | + return $this->baseUri; |
|
| 345 | + |
|
| 346 | + } |
|
| 347 | + |
|
| 348 | + /** |
|
| 349 | + * This method attempts to detect the base uri. |
|
| 350 | + * Only the PATH_INFO variable is considered. |
|
| 351 | + * |
|
| 352 | + * If this variable is not set, the root (/) is assumed. |
|
| 353 | + * |
|
| 354 | + * @return string |
|
| 355 | + */ |
|
| 356 | + public function guessBaseUri() { |
|
| 357 | + |
|
| 358 | + $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO'); |
|
| 359 | + $uri = $this->httpRequest->getRawServerValue('REQUEST_URI'); |
|
| 360 | + |
|
| 361 | + // If PATH_INFO is found, we can assume it's accurate. |
|
| 362 | + if (!empty($pathInfo)) { |
|
| 363 | + |
|
| 364 | + // We need to make sure we ignore the QUERY_STRING part |
|
| 365 | + if ($pos = strpos($uri, '?')) |
|
| 366 | + $uri = substr($uri, 0, $pos); |
|
| 367 | + |
|
| 368 | + // PATH_INFO is only set for urls, such as: /example.php/path |
|
| 369 | + // in that case PATH_INFO contains '/path'. |
|
| 370 | + // Note that REQUEST_URI is percent encoded, while PATH_INFO is |
|
| 371 | + // not, Therefore they are only comparable if we first decode |
|
| 372 | + // REQUEST_INFO as well. |
|
| 373 | + $decodedUri = URLUtil::decodePath($uri); |
|
| 374 | + |
|
| 375 | + // A simple sanity check: |
|
| 376 | + if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) { |
|
| 377 | + $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo)); |
|
| 378 | + return rtrim($baseUri, '/') . '/'; |
|
| 379 | + } |
|
| 380 | + |
|
| 381 | + throw new Exception('The REQUEST_URI (' . $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.'); |
|
| 382 | + |
|
| 383 | + } |
|
| 384 | + |
|
| 385 | + // The last fallback is that we're just going to assume the server root. |
|
| 386 | + return '/'; |
|
| 387 | + |
|
| 388 | + } |
|
| 389 | + |
|
| 390 | + /** |
|
| 391 | + * Adds a plugin to the server |
|
| 392 | + * |
|
| 393 | + * For more information, console the documentation of Sabre\DAV\ServerPlugin |
|
| 394 | + * |
|
| 395 | + * @param ServerPlugin $plugin |
|
| 396 | + * @return void |
|
| 397 | + */ |
|
| 398 | + public function addPlugin(ServerPlugin $plugin) { |
|
| 399 | + |
|
| 400 | + $this->plugins[$plugin->getPluginName()] = $plugin; |
|
| 401 | + $plugin->initialize($this); |
|
| 402 | + |
|
| 403 | + } |
|
| 404 | + |
|
| 405 | + /** |
|
| 406 | + * Returns an initialized plugin by it's name. |
|
| 407 | + * |
|
| 408 | + * This function returns null if the plugin was not found. |
|
| 409 | + * |
|
| 410 | + * @param string $name |
|
| 411 | + * @return ServerPlugin |
|
| 412 | + */ |
|
| 413 | + public function getPlugin($name) { |
|
| 414 | + |
|
| 415 | + if (isset($this->plugins[$name])) |
|
| 416 | + return $this->plugins[$name]; |
|
| 417 | + |
|
| 418 | + return null; |
|
| 419 | + |
|
| 420 | + } |
|
| 421 | + |
|
| 422 | + /** |
|
| 423 | + * Returns all plugins |
|
| 424 | + * |
|
| 425 | + * @return array |
|
| 426 | + */ |
|
| 427 | + public function getPlugins() { |
|
| 428 | + |
|
| 429 | + return $this->plugins; |
|
| 430 | + |
|
| 431 | + } |
|
| 432 | + |
|
| 433 | + /** |
|
| 434 | + * Handles a http request, and execute a method based on its name |
|
| 435 | + * |
|
| 436 | + * @param RequestInterface $request |
|
| 437 | + * @param ResponseInterface $response |
|
| 438 | + * @param $sendResponse Whether to send the HTTP response to the DAV client. |
|
| 439 | + * @return void |
|
| 440 | + */ |
|
| 441 | + public function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true) { |
|
| 442 | + |
|
| 443 | + $method = $request->getMethod(); |
|
| 444 | + |
|
| 445 | + if (!$this->emit('beforeMethod:' . $method, [$request, $response])) return; |
|
| 446 | + if (!$this->emit('beforeMethod', [$request, $response])) return; |
|
| 447 | + |
|
| 448 | + if (self::$exposeVersion) { |
|
| 449 | + $response->setHeader('X-Sabre-Version', Version::VERSION); |
|
| 450 | + } |
|
| 451 | + |
|
| 452 | + $this->transactionType = strtolower($method); |
|
| 453 | + |
|
| 454 | + if (!$this->checkPreconditions($request, $response)) { |
|
| 455 | + $this->sapi->sendResponse($response); |
|
| 456 | + return; |
|
| 457 | + } |
|
| 458 | + |
|
| 459 | + if ($this->emit('method:' . $method, [$request, $response])) { |
|
| 460 | + if ($this->emit('method', [$request, $response])) { |
|
| 461 | + $exMessage = "There was no plugin in the system that was willing to handle this " . $method . " method."; |
|
| 462 | + if ($method === "GET") { |
|
| 463 | + $exMessage .= " Enable the Browser plugin to get a better result here."; |
|
| 464 | + } |
|
| 465 | + |
|
| 466 | + // Unsupported method |
|
| 467 | + throw new Exception\NotImplemented($exMessage); |
|
| 468 | + } |
|
| 469 | + } |
|
| 470 | + |
|
| 471 | + if (!$this->emit('afterMethod:' . $method, [$request, $response])) return; |
|
| 472 | + if (!$this->emit('afterMethod', [$request, $response])) return; |
|
| 473 | 473 | |
| 474 | - if ($response->getStatus() === null) { |
|
| 475 | - throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.'); |
|
| 476 | - } |
|
| 477 | - if ($sendResponse) { |
|
| 478 | - $this->sapi->sendResponse($response); |
|
| 479 | - $this->emit('afterResponse', [$request, $response]); |
|
| 480 | - } |
|
| 481 | - |
|
| 482 | - } |
|
| 483 | - |
|
| 484 | - // {{{ HTTP/WebDAV protocol helpers |
|
| 485 | - |
|
| 486 | - /** |
|
| 487 | - * Returns an array with all the supported HTTP methods for a specific uri. |
|
| 488 | - * |
|
| 489 | - * @param string $path |
|
| 490 | - * @return array |
|
| 491 | - */ |
|
| 492 | - public function getAllowedMethods($path) { |
|
| 493 | - |
|
| 494 | - $methods = [ |
|
| 495 | - 'OPTIONS', |
|
| 496 | - 'GET', |
|
| 497 | - 'HEAD', |
|
| 498 | - 'DELETE', |
|
| 499 | - 'PROPFIND', |
|
| 500 | - 'PUT', |
|
| 501 | - 'PROPPATCH', |
|
| 502 | - 'COPY', |
|
| 503 | - 'MOVE', |
|
| 504 | - 'REPORT' |
|
| 505 | - ]; |
|
| 506 | - |
|
| 507 | - // The MKCOL is only allowed on an unmapped uri |
|
| 508 | - try { |
|
| 509 | - $this->tree->getNodeForPath($path); |
|
| 510 | - } catch (Exception\NotFound $e) { |
|
| 511 | - $methods[] = 'MKCOL'; |
|
| 512 | - } |
|
| 513 | - |
|
| 514 | - // We're also checking if any of the plugins register any new methods |
|
| 515 | - foreach ($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($path)); |
|
| 516 | - array_unique($methods); |
|
| 517 | - |
|
| 518 | - return $methods; |
|
| 519 | - |
|
| 520 | - } |
|
| 521 | - |
|
| 522 | - /** |
|
| 523 | - * Gets the uri for the request, keeping the base uri into consideration |
|
| 524 | - * |
|
| 525 | - * @return string |
|
| 526 | - */ |
|
| 527 | - public function getRequestUri() { |
|
| 474 | + if ($response->getStatus() === null) { |
|
| 475 | + throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.'); |
|
| 476 | + } |
|
| 477 | + if ($sendResponse) { |
|
| 478 | + $this->sapi->sendResponse($response); |
|
| 479 | + $this->emit('afterResponse', [$request, $response]); |
|
| 480 | + } |
|
| 481 | + |
|
| 482 | + } |
|
| 483 | + |
|
| 484 | + // {{{ HTTP/WebDAV protocol helpers |
|
| 485 | + |
|
| 486 | + /** |
|
| 487 | + * Returns an array with all the supported HTTP methods for a specific uri. |
|
| 488 | + * |
|
| 489 | + * @param string $path |
|
| 490 | + * @return array |
|
| 491 | + */ |
|
| 492 | + public function getAllowedMethods($path) { |
|
| 493 | + |
|
| 494 | + $methods = [ |
|
| 495 | + 'OPTIONS', |
|
| 496 | + 'GET', |
|
| 497 | + 'HEAD', |
|
| 498 | + 'DELETE', |
|
| 499 | + 'PROPFIND', |
|
| 500 | + 'PUT', |
|
| 501 | + 'PROPPATCH', |
|
| 502 | + 'COPY', |
|
| 503 | + 'MOVE', |
|
| 504 | + 'REPORT' |
|
| 505 | + ]; |
|
| 506 | + |
|
| 507 | + // The MKCOL is only allowed on an unmapped uri |
|
| 508 | + try { |
|
| 509 | + $this->tree->getNodeForPath($path); |
|
| 510 | + } catch (Exception\NotFound $e) { |
|
| 511 | + $methods[] = 'MKCOL'; |
|
| 512 | + } |
|
| 513 | + |
|
| 514 | + // We're also checking if any of the plugins register any new methods |
|
| 515 | + foreach ($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($path)); |
|
| 516 | + array_unique($methods); |
|
| 517 | + |
|
| 518 | + return $methods; |
|
| 519 | + |
|
| 520 | + } |
|
| 521 | + |
|
| 522 | + /** |
|
| 523 | + * Gets the uri for the request, keeping the base uri into consideration |
|
| 524 | + * |
|
| 525 | + * @return string |
|
| 526 | + */ |
|
| 527 | + public function getRequestUri() { |
|
| 528 | 528 | |
| 529 | - return $this->calculateUri($this->httpRequest->getUrl()); |
|
| 529 | + return $this->calculateUri($this->httpRequest->getUrl()); |
|
| 530 | 530 | |
| 531 | - } |
|
| 531 | + } |
|
| 532 | 532 | |
| 533 | - /** |
|
| 534 | - * Turns a URI such as the REQUEST_URI into a local path. |
|
| 535 | - * |
|
| 536 | - * This method: |
|
| 537 | - * * strips off the base path |
|
| 538 | - * * normalizes the path |
|
| 539 | - * * uri-decodes the path |
|
| 540 | - * |
|
| 541 | - * @param string $uri |
|
| 542 | - * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri |
|
| 543 | - * @return string |
|
| 544 | - */ |
|
| 545 | - public function calculateUri($uri) { |
|
| 546 | - |
|
| 547 | - if ($uri[0] != '/' && strpos($uri, '://')) { |
|
| 548 | - |
|
| 549 | - $uri = parse_url($uri, PHP_URL_PATH); |
|
| 550 | - |
|
| 551 | - } |
|
| 552 | - |
|
| 553 | - $uri = Uri\normalize(str_replace('//', '/', $uri)); |
|
| 554 | - $baseUri = Uri\normalize($this->getBaseUri()); |
|
| 555 | - |
|
| 556 | - if (strpos($uri, $baseUri) === 0) { |
|
| 557 | - |
|
| 558 | - return trim(URLUtil::decodePath(substr($uri, strlen($baseUri))), '/'); |
|
| 559 | - |
|
| 560 | - // A special case, if the baseUri was accessed without a trailing |
|
| 561 | - // slash, we'll accept it as well. |
|
| 562 | - } elseif ($uri . '/' === $baseUri) { |
|
| 563 | - |
|
| 564 | - return ''; |
|
| 565 | - |
|
| 566 | - } else { |
|
| 567 | - |
|
| 568 | - throw new Exception\Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')'); |
|
| 569 | - |
|
| 570 | - } |
|
| 571 | - |
|
| 572 | - } |
|
| 573 | - |
|
| 574 | - /** |
|
| 575 | - * Returns the HTTP depth header |
|
| 576 | - * |
|
| 577 | - * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object |
|
| 578 | - * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent |
|
| 579 | - * |
|
| 580 | - * @param mixed $default |
|
| 581 | - * @return int |
|
| 582 | - */ |
|
| 583 | - public function getHTTPDepth($default = self::DEPTH_INFINITY) { |
|
| 584 | - |
|
| 585 | - // If its not set, we'll grab the default |
|
| 586 | - $depth = $this->httpRequest->getHeader('Depth'); |
|
| 587 | - |
|
| 588 | - if (is_null($depth)) return $default; |
|
| 589 | - |
|
| 590 | - if ($depth == 'infinity') return self::DEPTH_INFINITY; |
|
| 591 | - |
|
| 592 | - |
|
| 593 | - // If its an unknown value. we'll grab the default |
|
| 594 | - if (!ctype_digit($depth)) return $default; |
|
| 595 | - |
|
| 596 | - return (int)$depth; |
|
| 597 | - |
|
| 598 | - } |
|
| 599 | - |
|
| 600 | - /** |
|
| 601 | - * Returns the HTTP range header |
|
| 602 | - * |
|
| 603 | - * This method returns null if there is no well-formed HTTP range request |
|
| 604 | - * header or array($start, $end). |
|
| 605 | - * |
|
| 606 | - * The first number is the offset of the first byte in the range. |
|
| 607 | - * The second number is the offset of the last byte in the range. |
|
| 608 | - * |
|
| 609 | - * If the second offset is null, it should be treated as the offset of the last byte of the entity |
|
| 610 | - * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity |
|
| 611 | - * |
|
| 612 | - * @return array|null |
|
| 613 | - */ |
|
| 614 | - public function getHTTPRange() { |
|
| 615 | - |
|
| 616 | - $range = $this->httpRequest->getHeader('range'); |
|
| 617 | - if (is_null($range)) return null; |
|
| 618 | - |
|
| 619 | - // Matching "Range: bytes=1234-5678: both numbers are optional |
|
| 620 | - |
|
| 621 | - if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) return null; |
|
| 622 | - |
|
| 623 | - if ($matches[1] === '' && $matches[2] === '') return null; |
|
| 624 | - |
|
| 625 | - return [ |
|
| 626 | - $matches[1] !== '' ? $matches[1] : null, |
|
| 627 | - $matches[2] !== '' ? $matches[2] : null, |
|
| 628 | - ]; |
|
| 629 | - |
|
| 630 | - } |
|
| 631 | - |
|
| 632 | - /** |
|
| 633 | - * Returns the HTTP Prefer header information. |
|
| 634 | - * |
|
| 635 | - * The prefer header is defined in: |
|
| 636 | - * http://tools.ietf.org/html/draft-snell-http-prefer-14 |
|
| 637 | - * |
|
| 638 | - * This method will return an array with options. |
|
| 639 | - * |
|
| 640 | - * Currently, the following options may be returned: |
|
| 641 | - * [ |
|
| 642 | - * 'return-asynch' => true, |
|
| 643 | - * 'return-minimal' => true, |
|
| 644 | - * 'return-representation' => true, |
|
| 645 | - * 'wait' => 30, |
|
| 646 | - * 'strict' => true, |
|
| 647 | - * 'lenient' => true, |
|
| 648 | - * ] |
|
| 649 | - * |
|
| 650 | - * This method also supports the Brief header, and will also return |
|
| 651 | - * 'return-minimal' if the brief header was set to 't'. |
|
| 652 | - * |
|
| 653 | - * For the boolean options, false will be returned if the headers are not |
|
| 654 | - * specified. For the integer options it will be 'null'. |
|
| 655 | - * |
|
| 656 | - * @return array |
|
| 657 | - */ |
|
| 658 | - public function getHTTPPrefer() { |
|
| 659 | - |
|
| 660 | - $result = [ |
|
| 661 | - // can be true or false |
|
| 662 | - 'respond-async' => false, |
|
| 663 | - // Could be set to 'representation' or 'minimal'. |
|
| 664 | - 'return' => null, |
|
| 665 | - // Used as a timeout, is usually a number. |
|
| 666 | - 'wait' => null, |
|
| 667 | - // can be 'strict' or 'lenient'. |
|
| 668 | - 'handling' => false, |
|
| 669 | - ]; |
|
| 670 | - |
|
| 671 | - if ($prefer = $this->httpRequest->getHeader('Prefer')) { |
|
| 672 | - |
|
| 673 | - $result = array_merge( |
|
| 674 | - $result, |
|
| 675 | - \Sabre\HTTP\parsePrefer($prefer) |
|
| 676 | - ); |
|
| 677 | - |
|
| 678 | - } elseif ($this->httpRequest->getHeader('Brief') == 't') { |
|
| 679 | - $result['return'] = 'minimal'; |
|
| 680 | - } |
|
| 681 | - |
|
| 682 | - return $result; |
|
| 683 | - |
|
| 684 | - } |
|
| 685 | - |
|
| 686 | - |
|
| 687 | - /** |
|
| 688 | - * Returns information about Copy and Move requests |
|
| 689 | - * |
|
| 690 | - * This function is created to help getting information about the source and the destination for the |
|
| 691 | - * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions |
|
| 692 | - * |
|
| 693 | - * The returned value is an array with the following keys: |
|
| 694 | - * * destination - Destination path |
|
| 695 | - * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten) |
|
| 696 | - * |
|
| 697 | - * @param RequestInterface $request |
|
| 698 | - * @throws Exception\BadRequest upon missing or broken request headers |
|
| 699 | - * @throws Exception\UnsupportedMediaType when trying to copy into a |
|
| 700 | - * non-collection. |
|
| 701 | - * @throws Exception\PreconditionFailed If overwrite is set to false, but |
|
| 702 | - * the destination exists. |
|
| 703 | - * @throws Exception\Forbidden when source and destination paths are |
|
| 704 | - * identical. |
|
| 705 | - * @throws Exception\Conflict When trying to copy a node into its own |
|
| 706 | - * subtree. |
|
| 707 | - * @return array |
|
| 708 | - */ |
|
| 709 | - public function getCopyAndMoveInfo(RequestInterface $request) { |
|
| 710 | - |
|
| 711 | - // Collecting the relevant HTTP headers |
|
| 712 | - if (!$request->getHeader('Destination')) throw new Exception\BadRequest('The destination header was not supplied'); |
|
| 713 | - $destination = $this->calculateUri($request->getHeader('Destination')); |
|
| 714 | - $overwrite = $request->getHeader('Overwrite'); |
|
| 715 | - if (!$overwrite) $overwrite = 'T'; |
|
| 716 | - if (strtoupper($overwrite) == 'T') $overwrite = true; |
|
| 717 | - elseif (strtoupper($overwrite) == 'F') $overwrite = false; |
|
| 718 | - // We need to throw a bad request exception, if the header was invalid |
|
| 719 | - else throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F'); |
|
| 720 | - |
|
| 721 | - list($destinationDir) = URLUtil::splitPath($destination); |
|
| 722 | - |
|
| 723 | - try { |
|
| 724 | - $destinationParent = $this->tree->getNodeForPath($destinationDir); |
|
| 725 | - if (!($destinationParent instanceof ICollection)) throw new Exception\UnsupportedMediaType('The destination node is not a collection'); |
|
| 726 | - } catch (Exception\NotFound $e) { |
|
| 727 | - |
|
| 728 | - // If the destination parent node is not found, we throw a 409 |
|
| 729 | - throw new Exception\Conflict('The destination node is not found'); |
|
| 730 | - } |
|
| 731 | - |
|
| 732 | - try { |
|
| 733 | - |
|
| 734 | - $destinationNode = $this->tree->getNodeForPath($destination); |
|
| 735 | - |
|
| 736 | - // If this succeeded, it means the destination already exists |
|
| 737 | - // we'll need to throw precondition failed in case overwrite is false |
|
| 738 | - if (!$overwrite) throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite'); |
|
| 739 | - |
|
| 740 | - } catch (Exception\NotFound $e) { |
|
| 741 | - |
|
| 742 | - // Destination didn't exist, we're all good |
|
| 743 | - $destinationNode = false; |
|
| 744 | - |
|
| 745 | - } |
|
| 746 | - |
|
| 747 | - $requestPath = $request->getPath(); |
|
| 748 | - if ($destination === $requestPath) { |
|
| 749 | - throw new Exception\Forbidden('Source and destination uri are identical.'); |
|
| 750 | - } |
|
| 751 | - if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath . '/') { |
|
| 752 | - throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.'); |
|
| 753 | - } |
|
| 754 | - |
|
| 755 | - // These are the three relevant properties we need to return |
|
| 756 | - return [ |
|
| 757 | - 'destination' => $destination, |
|
| 758 | - 'destinationExists' => !!$destinationNode, |
|
| 759 | - 'destinationNode' => $destinationNode, |
|
| 760 | - ]; |
|
| 761 | - |
|
| 762 | - } |
|
| 763 | - |
|
| 764 | - /** |
|
| 765 | - * Returns a list of properties for a path |
|
| 766 | - * |
|
| 767 | - * This is a simplified version getPropertiesForPath. If you aren't |
|
| 768 | - * interested in status codes, but you just want to have a flat list of |
|
| 769 | - * properties, use this method. |
|
| 770 | - * |
|
| 771 | - * Please note though that any problems related to retrieving properties, |
|
| 772 | - * such as permission issues will just result in an empty array being |
|
| 773 | - * returned. |
|
| 774 | - * |
|
| 775 | - * @param string $path |
|
| 776 | - * @param array $propertyNames |
|
| 777 | - */ |
|
| 778 | - public function getProperties($path, $propertyNames) { |
|
| 779 | - |
|
| 780 | - $result = $this->getPropertiesForPath($path, $propertyNames, 0); |
|
| 781 | - if (isset($result[0][200])) { |
|
| 782 | - return $result[0][200]; |
|
| 783 | - } else { |
|
| 784 | - return []; |
|
| 785 | - } |
|
| 786 | - |
|
| 787 | - } |
|
| 788 | - |
|
| 789 | - /** |
|
| 790 | - * A kid-friendly way to fetch properties for a node's children. |
|
| 791 | - * |
|
| 792 | - * The returned array will be indexed by the path of the of child node. |
|
| 793 | - * Only properties that are actually found will be returned. |
|
| 794 | - * |
|
| 795 | - * The parent node will not be returned. |
|
| 796 | - * |
|
| 797 | - * @param string $path |
|
| 798 | - * @param array $propertyNames |
|
| 799 | - * @return array |
|
| 800 | - */ |
|
| 801 | - public function getPropertiesForChildren($path, $propertyNames) { |
|
| 802 | - |
|
| 803 | - $result = []; |
|
| 804 | - foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) { |
|
| 805 | - |
|
| 806 | - // Skipping the parent path |
|
| 807 | - if ($k === 0) continue; |
|
| 808 | - |
|
| 809 | - $result[$row['href']] = $row[200]; |
|
| 810 | - |
|
| 811 | - } |
|
| 812 | - return $result; |
|
| 813 | - |
|
| 814 | - } |
|
| 815 | - |
|
| 816 | - /** |
|
| 817 | - * Returns a list of HTTP headers for a particular resource |
|
| 818 | - * |
|
| 819 | - * The generated http headers are based on properties provided by the |
|
| 820 | - * resource. The method basically provides a simple mapping between |
|
| 821 | - * DAV property and HTTP header. |
|
| 822 | - * |
|
| 823 | - * The headers are intended to be used for HEAD and GET requests. |
|
| 824 | - * |
|
| 825 | - * @param string $path |
|
| 826 | - * @return array |
|
| 827 | - */ |
|
| 828 | - public function getHTTPHeaders($path) { |
|
| 829 | - |
|
| 830 | - $propertyMap = [ |
|
| 831 | - '{DAV:}getcontenttype' => 'Content-Type', |
|
| 832 | - '{DAV:}getcontentlength' => 'Content-Length', |
|
| 833 | - '{DAV:}getlastmodified' => 'Last-Modified', |
|
| 834 | - '{DAV:}getetag' => 'ETag', |
|
| 835 | - ]; |
|
| 836 | - |
|
| 837 | - $properties = $this->getProperties($path, array_keys($propertyMap)); |
|
| 838 | - |
|
| 839 | - $headers = []; |
|
| 840 | - foreach ($propertyMap as $property => $header) { |
|
| 841 | - if (!isset($properties[$property])) continue; |
|
| 842 | - |
|
| 843 | - if (is_scalar($properties[$property])) { |
|
| 844 | - $headers[$header] = $properties[$property]; |
|
| 845 | - |
|
| 846 | - // GetLastModified gets special cased |
|
| 847 | - } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) { |
|
| 848 | - $headers[$header] = HTTP\Util::toHTTPDate($properties[$property]->getTime()); |
|
| 849 | - } |
|
| 850 | - |
|
| 851 | - } |
|
| 852 | - |
|
| 853 | - return $headers; |
|
| 854 | - |
|
| 855 | - } |
|
| 856 | - |
|
| 857 | - /** |
|
| 858 | - * Small helper to support PROPFIND with DEPTH_INFINITY. |
|
| 859 | - * |
|
| 860 | - * @param array[] $propFindRequests |
|
| 861 | - * @param PropFind $propFind |
|
| 862 | - * @return void |
|
| 863 | - */ |
|
| 864 | - private function addPathNodesRecursively(&$propFindRequests, PropFind $propFind) { |
|
| 865 | - |
|
| 866 | - $newDepth = $propFind->getDepth(); |
|
| 867 | - $path = $propFind->getPath(); |
|
| 868 | - |
|
| 869 | - if ($newDepth !== self::DEPTH_INFINITY) { |
|
| 870 | - $newDepth--; |
|
| 871 | - } |
|
| 872 | - |
|
| 873 | - foreach ($this->tree->getChildren($path) as $childNode) { |
|
| 874 | - $subPropFind = clone $propFind; |
|
| 875 | - $subPropFind->setDepth($newDepth); |
|
| 876 | - if ($path !== '') { |
|
| 877 | - $subPath = $path . '/' . $childNode->getName(); |
|
| 878 | - } else { |
|
| 879 | - $subPath = $childNode->getName(); |
|
| 880 | - } |
|
| 881 | - $subPropFind->setPath($subPath); |
|
| 882 | - |
|
| 883 | - $propFindRequests[] = [ |
|
| 884 | - $subPropFind, |
|
| 885 | - $childNode |
|
| 886 | - ]; |
|
| 887 | - |
|
| 888 | - if (($newDepth === self::DEPTH_INFINITY || $newDepth >= 1) && $childNode instanceof ICollection) { |
|
| 889 | - $this->addPathNodesRecursively($propFindRequests, $subPropFind); |
|
| 890 | - } |
|
| 891 | - |
|
| 892 | - } |
|
| 893 | - } |
|
| 894 | - |
|
| 895 | - /** |
|
| 896 | - * Returns a list of properties for a given path |
|
| 897 | - * |
|
| 898 | - * The path that should be supplied should have the baseUrl stripped out |
|
| 899 | - * The list of properties should be supplied in Clark notation. If the list is empty |
|
| 900 | - * 'allprops' is assumed. |
|
| 901 | - * |
|
| 902 | - * If a depth of 1 is requested child elements will also be returned. |
|
| 903 | - * |
|
| 904 | - * @param string $path |
|
| 905 | - * @param array $propertyNames |
|
| 906 | - * @param int $depth |
|
| 907 | - * @return array |
|
| 908 | - */ |
|
| 909 | - public function getPropertiesForPath($path, $propertyNames = [], $depth = 0) { |
|
| 910 | - |
|
| 911 | - // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled |
|
| 912 | - if (!$this->enablePropfindDepthInfinity && $depth != 0) $depth = 1; |
|
| 913 | - |
|
| 914 | - $path = trim($path, '/'); |
|
| 915 | - |
|
| 916 | - $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; |
|
| 917 | - $propFind = new PropFind($path, (array)$propertyNames, $depth, $propFindType); |
|
| 918 | - |
|
| 919 | - $parentNode = $this->tree->getNodeForPath($path); |
|
| 920 | - |
|
| 921 | - $propFindRequests = [[ |
|
| 922 | - $propFind, |
|
| 923 | - $parentNode |
|
| 924 | - ]]; |
|
| 925 | - |
|
| 926 | - if (($depth > 0 || $depth === self::DEPTH_INFINITY) && $parentNode instanceof ICollection) { |
|
| 927 | - $this->addPathNodesRecursively($propFindRequests, $propFind); |
|
| 928 | - } |
|
| 929 | - |
|
| 930 | - $returnPropertyList = []; |
|
| 931 | - |
|
| 932 | - foreach ($propFindRequests as $propFindRequest) { |
|
| 933 | - |
|
| 934 | - list($propFind, $node) = $propFindRequest; |
|
| 935 | - $r = $this->getPropertiesByNode($propFind, $node); |
|
| 936 | - if ($r) { |
|
| 937 | - $result = $propFind->getResultForMultiStatus(); |
|
| 938 | - $result['href'] = $propFind->getPath(); |
|
| 939 | - |
|
| 940 | - // WebDAV recommends adding a slash to the path, if the path is |
|
| 941 | - // a collection. |
|
| 942 | - // Furthermore, iCal also demands this to be the case for |
|
| 943 | - // principals. This is non-standard, but we support it. |
|
| 944 | - $resourceType = $this->getResourceTypeForNode($node); |
|
| 945 | - if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { |
|
| 946 | - $result['href'] .= '/'; |
|
| 947 | - } |
|
| 948 | - $returnPropertyList[] = $result; |
|
| 949 | - } |
|
| 950 | - |
|
| 951 | - } |
|
| 952 | - |
|
| 953 | - return $returnPropertyList; |
|
| 954 | - |
|
| 955 | - } |
|
| 956 | - |
|
| 957 | - /** |
|
| 958 | - * Returns a list of properties for a list of paths. |
|
| 959 | - * |
|
| 960 | - * The path that should be supplied should have the baseUrl stripped out |
|
| 961 | - * The list of properties should be supplied in Clark notation. If the list is empty |
|
| 962 | - * 'allprops' is assumed. |
|
| 963 | - * |
|
| 964 | - * The result is returned as an array, with paths for it's keys. |
|
| 965 | - * The result may be returned out of order. |
|
| 966 | - * |
|
| 967 | - * @param array $paths |
|
| 968 | - * @param array $propertyNames |
|
| 969 | - * @return array |
|
| 970 | - */ |
|
| 971 | - public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) { |
|
| 972 | - |
|
| 973 | - $result = [ |
|
| 974 | - ]; |
|
| 975 | - |
|
| 976 | - $nodes = $this->tree->getMultipleNodes($paths); |
|
| 977 | - |
|
| 978 | - foreach ($nodes as $path => $node) { |
|
| 979 | - |
|
| 980 | - $propFind = new PropFind($path, $propertyNames); |
|
| 981 | - $r = $this->getPropertiesByNode($propFind, $node); |
|
| 982 | - if ($r) { |
|
| 983 | - $result[$path] = $propFind->getResultForMultiStatus(); |
|
| 984 | - $result[$path]['href'] = $path; |
|
| 985 | - |
|
| 986 | - $resourceType = $this->getResourceTypeForNode($node); |
|
| 987 | - if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { |
|
| 988 | - $result[$path]['href'] .= '/'; |
|
| 989 | - } |
|
| 990 | - } |
|
| 991 | - |
|
| 992 | - } |
|
| 993 | - |
|
| 994 | - return $result; |
|
| 995 | - |
|
| 996 | - } |
|
| 997 | - |
|
| 998 | - |
|
| 999 | - /** |
|
| 1000 | - * Determines all properties for a node. |
|
| 1001 | - * |
|
| 1002 | - * This method tries to grab all properties for a node. This method is used |
|
| 1003 | - * internally getPropertiesForPath and a few others. |
|
| 1004 | - * |
|
| 1005 | - * It could be useful to call this, if you already have an instance of your |
|
| 1006 | - * target node and simply want to run through the system to get a correct |
|
| 1007 | - * list of properties. |
|
| 1008 | - * |
|
| 1009 | - * @param PropFind $propFind |
|
| 1010 | - * @param INode $node |
|
| 1011 | - * @return bool |
|
| 1012 | - */ |
|
| 1013 | - public function getPropertiesByNode(PropFind $propFind, INode $node) { |
|
| 1014 | - |
|
| 1015 | - return $this->emit('propFind', [$propFind, $node]); |
|
| 1016 | - |
|
| 1017 | - } |
|
| 1018 | - |
|
| 1019 | - /** |
|
| 1020 | - * This method is invoked by sub-systems creating a new file. |
|
| 1021 | - * |
|
| 1022 | - * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). |
|
| 1023 | - * It was important to get this done through a centralized function, |
|
| 1024 | - * allowing plugins to intercept this using the beforeCreateFile event. |
|
| 1025 | - * |
|
| 1026 | - * This method will return true if the file was actually created |
|
| 1027 | - * |
|
| 1028 | - * @param string $uri |
|
| 1029 | - * @param resource $data |
|
| 1030 | - * @param string $etag |
|
| 1031 | - * @return bool |
|
| 1032 | - */ |
|
| 1033 | - public function createFile($uri, $data, &$etag = null) { |
|
| 1034 | - |
|
| 1035 | - list($dir, $name) = URLUtil::splitPath($uri); |
|
| 1036 | - |
|
| 1037 | - if (!$this->emit('beforeBind', [$uri])) return false; |
|
| 1038 | - |
|
| 1039 | - $parent = $this->tree->getNodeForPath($dir); |
|
| 1040 | - if (!$parent instanceof ICollection) { |
|
| 1041 | - throw new Exception\Conflict('Files can only be created as children of collections'); |
|
| 1042 | - } |
|
| 1043 | - |
|
| 1044 | - // It is possible for an event handler to modify the content of the |
|
| 1045 | - // body, before it gets written. If this is the case, $modified |
|
| 1046 | - // should be set to true. |
|
| 1047 | - // |
|
| 1048 | - // If $modified is true, we must not send back an ETag. |
|
| 1049 | - $modified = false; |
|
| 1050 | - if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) return false; |
|
| 1051 | - |
|
| 1052 | - $etag = $parent->createFile($name, $data); |
|
| 1053 | - |
|
| 1054 | - if ($modified) $etag = null; |
|
| 1055 | - |
|
| 1056 | - $this->tree->markDirty($dir . '/' . $name); |
|
| 1057 | - |
|
| 1058 | - $this->emit('afterBind', [$uri]); |
|
| 1059 | - $this->emit('afterCreateFile', [$uri, $parent]); |
|
| 1060 | - |
|
| 1061 | - return true; |
|
| 1062 | - } |
|
| 1063 | - |
|
| 1064 | - /** |
|
| 1065 | - * This method is invoked by sub-systems updating a file. |
|
| 1066 | - * |
|
| 1067 | - * This method will return true if the file was actually updated |
|
| 1068 | - * |
|
| 1069 | - * @param string $uri |
|
| 1070 | - * @param resource $data |
|
| 1071 | - * @param string $etag |
|
| 1072 | - * @return bool |
|
| 1073 | - */ |
|
| 1074 | - public function updateFile($uri, $data, &$etag = null) { |
|
| 1075 | - |
|
| 1076 | - $node = $this->tree->getNodeForPath($uri); |
|
| 1077 | - |
|
| 1078 | - // It is possible for an event handler to modify the content of the |
|
| 1079 | - // body, before it gets written. If this is the case, $modified |
|
| 1080 | - // should be set to true. |
|
| 1081 | - // |
|
| 1082 | - // If $modified is true, we must not send back an ETag. |
|
| 1083 | - $modified = false; |
|
| 1084 | - if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) return false; |
|
| 1085 | - |
|
| 1086 | - $etag = $node->put($data); |
|
| 1087 | - if ($modified) $etag = null; |
|
| 1088 | - $this->emit('afterWriteContent', [$uri, $node]); |
|
| 1089 | - |
|
| 1090 | - return true; |
|
| 1091 | - } |
|
| 1092 | - |
|
| 1093 | - |
|
| 1094 | - |
|
| 1095 | - /** |
|
| 1096 | - * This method is invoked by sub-systems creating a new directory. |
|
| 1097 | - * |
|
| 1098 | - * @param string $uri |
|
| 1099 | - * @return void |
|
| 1100 | - */ |
|
| 1101 | - public function createDirectory($uri) { |
|
| 1102 | - |
|
| 1103 | - $this->createCollection($uri, new MkCol(['{DAV:}collection'], [])); |
|
| 1104 | - |
|
| 1105 | - } |
|
| 1106 | - |
|
| 1107 | - /** |
|
| 1108 | - * Use this method to create a new collection |
|
| 1109 | - * |
|
| 1110 | - * @param string $uri The new uri |
|
| 1111 | - * @param MkCol $mkCol |
|
| 1112 | - * @return array|null |
|
| 1113 | - */ |
|
| 1114 | - public function createCollection($uri, MkCol $mkCol) { |
|
| 1115 | - |
|
| 1116 | - list($parentUri, $newName) = URLUtil::splitPath($uri); |
|
| 1117 | - |
|
| 1118 | - // Making sure the parent exists |
|
| 1119 | - try { |
|
| 1120 | - $parent = $this->tree->getNodeForPath($parentUri); |
|
| 1121 | - |
|
| 1122 | - } catch (Exception\NotFound $e) { |
|
| 1123 | - throw new Exception\Conflict('Parent node does not exist'); |
|
| 1124 | - |
|
| 1125 | - } |
|
| 1126 | - |
|
| 1127 | - // Making sure the parent is a collection |
|
| 1128 | - if (!$parent instanceof ICollection) { |
|
| 1129 | - throw new Exception\Conflict('Parent node is not a collection'); |
|
| 1130 | - } |
|
| 1131 | - |
|
| 1132 | - // Making sure the child does not already exist |
|
| 1133 | - try { |
|
| 1134 | - $parent->getChild($newName); |
|
| 1135 | - |
|
| 1136 | - // If we got here.. it means there's already a node on that url, and we need to throw a 405 |
|
| 1137 | - throw new Exception\MethodNotAllowed('The resource you tried to create already exists'); |
|
| 1138 | - |
|
| 1139 | - } catch (Exception\NotFound $e) { |
|
| 1140 | - // NotFound is the expected behavior. |
|
| 1141 | - } |
|
| 1142 | - |
|
| 1143 | - |
|
| 1144 | - if (!$this->emit('beforeBind', [$uri])) return; |
|
| 1145 | - |
|
| 1146 | - if ($parent instanceof IExtendedCollection) { |
|
| 1147 | - |
|
| 1148 | - /** |
|
| 1149 | - * If the parent is an instance of IExtendedCollection, it means that |
|
| 1150 | - * we can pass the MkCol object directly as it may be able to store |
|
| 1151 | - * properties immediately. |
|
| 1152 | - */ |
|
| 1153 | - $parent->createExtendedCollection($newName, $mkCol); |
|
| 1154 | - |
|
| 1155 | - } else { |
|
| 1156 | - |
|
| 1157 | - /** |
|
| 1158 | - * If the parent is a standard ICollection, it means only |
|
| 1159 | - * 'standard' collections can be created, so we should fail any |
|
| 1160 | - * MKCOL operation that carries extra resourcetypes. |
|
| 1161 | - */ |
|
| 1162 | - if (count($mkCol->getResourceType()) > 1) { |
|
| 1163 | - throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.'); |
|
| 1164 | - } |
|
| 1165 | - |
|
| 1166 | - $parent->createDirectory($newName); |
|
| 1167 | - |
|
| 1168 | - } |
|
| 1169 | - |
|
| 1170 | - // If there are any properties that have not been handled/stored, |
|
| 1171 | - // we ask the 'propPatch' event to handle them. This will allow for |
|
| 1172 | - // example the propertyStorage system to store properties upon MKCOL. |
|
| 1173 | - if ($mkCol->getRemainingMutations()) { |
|
| 1174 | - $this->emit('propPatch', [$uri, $mkCol]); |
|
| 1175 | - } |
|
| 1176 | - $success = $mkCol->commit(); |
|
| 1177 | - |
|
| 1178 | - if (!$success) { |
|
| 1179 | - $result = $mkCol->getResult(); |
|
| 1180 | - // generateMkCol needs the href key to exist. |
|
| 1181 | - $result['href'] = $uri; |
|
| 1182 | - return $result; |
|
| 1183 | - } |
|
| 1184 | - |
|
| 1185 | - $this->tree->markDirty($parentUri); |
|
| 1186 | - $this->emit('afterBind', [$uri]); |
|
| 1187 | - |
|
| 1188 | - } |
|
| 1189 | - |
|
| 1190 | - /** |
|
| 1191 | - * This method updates a resource's properties |
|
| 1192 | - * |
|
| 1193 | - * The properties array must be a list of properties. Array-keys are |
|
| 1194 | - * property names in clarknotation, array-values are it's values. |
|
| 1195 | - * If a property must be deleted, the value should be null. |
|
| 1196 | - * |
|
| 1197 | - * Note that this request should either completely succeed, or |
|
| 1198 | - * completely fail. |
|
| 1199 | - * |
|
| 1200 | - * The response is an array with properties for keys, and http status codes |
|
| 1201 | - * as their values. |
|
| 1202 | - * |
|
| 1203 | - * @param string $path |
|
| 1204 | - * @param array $properties |
|
| 1205 | - * @return array |
|
| 1206 | - */ |
|
| 1207 | - public function updateProperties($path, array $properties) { |
|
| 1208 | - |
|
| 1209 | - $propPatch = new PropPatch($properties); |
|
| 1210 | - $this->emit('propPatch', [$path, $propPatch]); |
|
| 1211 | - $propPatch->commit(); |
|
| 1212 | - |
|
| 1213 | - return $propPatch->getResult(); |
|
| 1214 | - |
|
| 1215 | - } |
|
| 1216 | - |
|
| 1217 | - /** |
|
| 1218 | - * This method checks the main HTTP preconditions. |
|
| 1219 | - * |
|
| 1220 | - * Currently these are: |
|
| 1221 | - * * If-Match |
|
| 1222 | - * * If-None-Match |
|
| 1223 | - * * If-Modified-Since |
|
| 1224 | - * * If-Unmodified-Since |
|
| 1225 | - * |
|
| 1226 | - * The method will return true if all preconditions are met |
|
| 1227 | - * The method will return false, or throw an exception if preconditions |
|
| 1228 | - * failed. If false is returned the operation should be aborted, and |
|
| 1229 | - * the appropriate HTTP response headers are already set. |
|
| 1230 | - * |
|
| 1231 | - * Normally this method will throw 412 Precondition Failed for failures |
|
| 1232 | - * related to If-None-Match, If-Match and If-Unmodified Since. It will |
|
| 1233 | - * set the status to 304 Not Modified for If-Modified_since. |
|
| 1234 | - * |
|
| 1235 | - * @param RequestInterface $request |
|
| 1236 | - * @param ResponseInterface $response |
|
| 1237 | - * @return bool |
|
| 1238 | - */ |
|
| 1239 | - public function checkPreconditions(RequestInterface $request, ResponseInterface $response) { |
|
| 1240 | - |
|
| 1241 | - $path = $request->getPath(); |
|
| 1242 | - $node = null; |
|
| 1243 | - $lastMod = null; |
|
| 1244 | - $etag = null; |
|
| 1245 | - |
|
| 1246 | - if ($ifMatch = $request->getHeader('If-Match')) { |
|
| 1247 | - |
|
| 1248 | - // If-Match contains an entity tag. Only if the entity-tag |
|
| 1249 | - // matches we are allowed to make the request succeed. |
|
| 1250 | - // If the entity-tag is '*' we are only allowed to make the |
|
| 1251 | - // request succeed if a resource exists at that url. |
|
| 1252 | - try { |
|
| 1253 | - $node = $this->tree->getNodeForPath($path); |
|
| 1254 | - } catch (Exception\NotFound $e) { |
|
| 1255 | - throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match'); |
|
| 1256 | - } |
|
| 1257 | - |
|
| 1258 | - // Only need to check entity tags if they are not * |
|
| 1259 | - if ($ifMatch !== '*') { |
|
| 1260 | - |
|
| 1261 | - // There can be multiple ETags |
|
| 1262 | - $ifMatch = explode(',', $ifMatch); |
|
| 1263 | - $haveMatch = false; |
|
| 1264 | - foreach ($ifMatch as $ifMatchItem) { |
|
| 1265 | - |
|
| 1266 | - // Stripping any extra spaces |
|
| 1267 | - $ifMatchItem = trim($ifMatchItem, ' '); |
|
| 1268 | - |
|
| 1269 | - $etag = $node instanceof IFile ? $node->getETag() : null; |
|
| 1270 | - if ($etag === $ifMatchItem) { |
|
| 1271 | - $haveMatch = true; |
|
| 1272 | - } else { |
|
| 1273 | - // Evolution has a bug where it sometimes prepends the " |
|
| 1274 | - // with a \. This is our workaround. |
|
| 1275 | - if (str_replace('\\"', '"', $ifMatchItem) === $etag) { |
|
| 1276 | - $haveMatch = true; |
|
| 1277 | - } |
|
| 1278 | - } |
|
| 1279 | - |
|
| 1280 | - } |
|
| 1281 | - if (!$haveMatch) { |
|
| 1282 | - if ($etag) $response->setHeader('ETag', $etag); |
|
| 1283 | - throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.', 'If-Match'); |
|
| 1284 | - } |
|
| 1285 | - } |
|
| 1286 | - } |
|
| 1287 | - |
|
| 1288 | - if ($ifNoneMatch = $request->getHeader('If-None-Match')) { |
|
| 1289 | - |
|
| 1290 | - // The If-None-Match header contains an ETag. |
|
| 1291 | - // Only if the ETag does not match the current ETag, the request will succeed |
|
| 1292 | - // The header can also contain *, in which case the request |
|
| 1293 | - // will only succeed if the entity does not exist at all. |
|
| 1294 | - $nodeExists = true; |
|
| 1295 | - if (!$node) { |
|
| 1296 | - try { |
|
| 1297 | - $node = $this->tree->getNodeForPath($path); |
|
| 1298 | - } catch (Exception\NotFound $e) { |
|
| 1299 | - $nodeExists = false; |
|
| 1300 | - } |
|
| 1301 | - } |
|
| 1302 | - if ($nodeExists) { |
|
| 1303 | - $haveMatch = false; |
|
| 1304 | - if ($ifNoneMatch === '*') $haveMatch = true; |
|
| 1305 | - else { |
|
| 1306 | - |
|
| 1307 | - // There might be multiple ETags |
|
| 1308 | - $ifNoneMatch = explode(',', $ifNoneMatch); |
|
| 1309 | - $etag = $node instanceof IFile ? $node->getETag() : null; |
|
| 1310 | - |
|
| 1311 | - foreach ($ifNoneMatch as $ifNoneMatchItem) { |
|
| 1312 | - |
|
| 1313 | - // Stripping any extra spaces |
|
| 1314 | - $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); |
|
| 1315 | - |
|
| 1316 | - if ($etag === $ifNoneMatchItem) $haveMatch = true; |
|
| 1317 | - |
|
| 1318 | - } |
|
| 1319 | - |
|
| 1320 | - } |
|
| 1321 | - |
|
| 1322 | - if ($haveMatch) { |
|
| 1323 | - if ($etag) $response->setHeader('ETag', $etag); |
|
| 1324 | - if ($request->getMethod() === 'GET') { |
|
| 1325 | - $response->setStatus(304); |
|
| 1326 | - return false; |
|
| 1327 | - } else { |
|
| 1328 | - throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match'); |
|
| 1329 | - } |
|
| 1330 | - } |
|
| 1331 | - } |
|
| 1332 | - |
|
| 1333 | - } |
|
| 1334 | - |
|
| 1335 | - if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) { |
|
| 1336 | - |
|
| 1337 | - // The If-Modified-Since header contains a date. We |
|
| 1338 | - // will only return the entity if it has been changed since |
|
| 1339 | - // that date. If it hasn't been changed, we return a 304 |
|
| 1340 | - // header |
|
| 1341 | - // Note that this header only has to be checked if there was no If-None-Match header |
|
| 1342 | - // as per the HTTP spec. |
|
| 1343 | - $date = HTTP\Util::parseHTTPDate($ifModifiedSince); |
|
| 1344 | - |
|
| 1345 | - if ($date) { |
|
| 1346 | - if (is_null($node)) { |
|
| 1347 | - $node = $this->tree->getNodeForPath($path); |
|
| 1348 | - } |
|
| 1349 | - $lastMod = $node->getLastModified(); |
|
| 1350 | - if ($lastMod) { |
|
| 1351 | - $lastMod = new \DateTime('@' . $lastMod); |
|
| 1352 | - if ($lastMod <= $date) { |
|
| 1353 | - $response->setStatus(304); |
|
| 1354 | - $response->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod)); |
|
| 1355 | - return false; |
|
| 1356 | - } |
|
| 1357 | - } |
|
| 1358 | - } |
|
| 1359 | - } |
|
| 1360 | - |
|
| 1361 | - if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) { |
|
| 1362 | - |
|
| 1363 | - // The If-Unmodified-Since will allow allow the request if the |
|
| 1364 | - // entity has not changed since the specified date. |
|
| 1365 | - $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince); |
|
| 1366 | - |
|
| 1367 | - // We must only check the date if it's valid |
|
| 1368 | - if ($date) { |
|
| 1369 | - if (is_null($node)) { |
|
| 1370 | - $node = $this->tree->getNodeForPath($path); |
|
| 1371 | - } |
|
| 1372 | - $lastMod = $node->getLastModified(); |
|
| 1373 | - if ($lastMod) { |
|
| 1374 | - $lastMod = new \DateTime('@' . $lastMod); |
|
| 1375 | - if ($lastMod > $date) { |
|
| 1376 | - throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since'); |
|
| 1377 | - } |
|
| 1378 | - } |
|
| 1379 | - } |
|
| 1380 | - |
|
| 1381 | - } |
|
| 1382 | - |
|
| 1383 | - // Now the hardest, the If: header. The If: header can contain multiple |
|
| 1384 | - // urls, ETags and so-called 'state tokens'. |
|
| 1385 | - // |
|
| 1386 | - // Examples of state tokens include lock-tokens (as defined in rfc4918) |
|
| 1387 | - // and sync-tokens (as defined in rfc6578). |
|
| 1388 | - // |
|
| 1389 | - // The only proper way to deal with these, is to emit events, that a |
|
| 1390 | - // Sync and Lock plugin can pick up. |
|
| 1391 | - $ifConditions = $this->getIfConditions($request); |
|
| 1392 | - |
|
| 1393 | - foreach ($ifConditions as $kk => $ifCondition) { |
|
| 1394 | - foreach ($ifCondition['tokens'] as $ii => $token) { |
|
| 1395 | - $ifConditions[$kk]['tokens'][$ii]['validToken'] = false; |
|
| 1396 | - } |
|
| 1397 | - } |
|
| 1398 | - |
|
| 1399 | - // Plugins are responsible for validating all the tokens. |
|
| 1400 | - // If a plugin deemed a token 'valid', it will set 'validToken' to |
|
| 1401 | - // true. |
|
| 1402 | - $this->emit('validateTokens', [ $request, &$ifConditions ]); |
|
| 1403 | - |
|
| 1404 | - // Now we're going to analyze the result. |
|
| 1405 | - |
|
| 1406 | - // Every ifCondition needs to validate to true, so we exit as soon as |
|
| 1407 | - // we have an invalid condition. |
|
| 1408 | - foreach ($ifConditions as $ifCondition) { |
|
| 1409 | - |
|
| 1410 | - $uri = $ifCondition['uri']; |
|
| 1411 | - $tokens = $ifCondition['tokens']; |
|
| 1412 | - |
|
| 1413 | - // We only need 1 valid token for the condition to succeed. |
|
| 1414 | - foreach ($tokens as $token) { |
|
| 1415 | - |
|
| 1416 | - $tokenValid = $token['validToken'] || !$token['token']; |
|
| 1417 | - |
|
| 1418 | - $etagValid = false; |
|
| 1419 | - if (!$token['etag']) { |
|
| 1420 | - $etagValid = true; |
|
| 1421 | - } |
|
| 1422 | - // Checking the ETag, only if the token was already deamed |
|
| 1423 | - // valid and there is one. |
|
| 1424 | - if ($token['etag'] && $tokenValid) { |
|
| 1425 | - |
|
| 1426 | - // The token was valid, and there was an ETag. We must |
|
| 1427 | - // grab the current ETag and check it. |
|
| 1428 | - $node = $this->tree->getNodeForPath($uri); |
|
| 1429 | - $etagValid = $node instanceof IFile && $node->getETag() == $token['etag']; |
|
| 1430 | - |
|
| 1431 | - } |
|
| 1432 | - |
|
| 1433 | - |
|
| 1434 | - if (($tokenValid && $etagValid) ^ $token['negate']) { |
|
| 1435 | - // Both were valid, so we can go to the next condition. |
|
| 1436 | - continue 2; |
|
| 1437 | - } |
|
| 1438 | - |
|
| 1439 | - |
|
| 1440 | - } |
|
| 1441 | - |
|
| 1442 | - // If we ended here, it means there was no valid ETag + token |
|
| 1443 | - // combination found for the current condition. This means we fail! |
|
| 1444 | - throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for ' . $uri, 'If'); |
|
| 1445 | - |
|
| 1446 | - } |
|
| 1447 | - |
|
| 1448 | - return true; |
|
| 1449 | - |
|
| 1450 | - } |
|
| 1451 | - |
|
| 1452 | - /** |
|
| 1453 | - * This method is created to extract information from the WebDAV HTTP 'If:' header |
|
| 1454 | - * |
|
| 1455 | - * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information |
|
| 1456 | - * The function will return an array, containing structs with the following keys |
|
| 1457 | - * |
|
| 1458 | - * * uri - the uri the condition applies to. |
|
| 1459 | - * * tokens - The lock token. another 2 dimensional array containing 3 elements |
|
| 1460 | - * |
|
| 1461 | - * Example 1: |
|
| 1462 | - * |
|
| 1463 | - * If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>) |
|
| 1464 | - * |
|
| 1465 | - * Would result in: |
|
| 1466 | - * |
|
| 1467 | - * [ |
|
| 1468 | - * [ |
|
| 1469 | - * 'uri' => '/request/uri', |
|
| 1470 | - * 'tokens' => [ |
|
| 1471 | - * [ |
|
| 1472 | - * [ |
|
| 1473 | - * 'negate' => false, |
|
| 1474 | - * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', |
|
| 1475 | - * 'etag' => "" |
|
| 1476 | - * ] |
|
| 1477 | - * ] |
|
| 1478 | - * ], |
|
| 1479 | - * ] |
|
| 1480 | - * ] |
|
| 1481 | - * |
|
| 1482 | - * Example 2: |
|
| 1483 | - * |
|
| 1484 | - * If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"]) |
|
| 1485 | - * |
|
| 1486 | - * Would result in: |
|
| 1487 | - * |
|
| 1488 | - * [ |
|
| 1489 | - * [ |
|
| 1490 | - * 'uri' => 'path', |
|
| 1491 | - * 'tokens' => [ |
|
| 1492 | - * [ |
|
| 1493 | - * [ |
|
| 1494 | - * 'negate' => true, |
|
| 1495 | - * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', |
|
| 1496 | - * 'etag' => '"Im An ETag"' |
|
| 1497 | - * ], |
|
| 1498 | - * [ |
|
| 1499 | - * 'negate' => false, |
|
| 1500 | - * 'token' => '', |
|
| 1501 | - * 'etag' => '"Another ETag"' |
|
| 1502 | - * ] |
|
| 1503 | - * ] |
|
| 1504 | - * ], |
|
| 1505 | - * ], |
|
| 1506 | - * [ |
|
| 1507 | - * 'uri' => 'path2', |
|
| 1508 | - * 'tokens' => [ |
|
| 1509 | - * [ |
|
| 1510 | - * [ |
|
| 1511 | - * 'negate' => true, |
|
| 1512 | - * 'token' => '', |
|
| 1513 | - * 'etag' => '"Path2 ETag"' |
|
| 1514 | - * ] |
|
| 1515 | - * ] |
|
| 1516 | - * ], |
|
| 1517 | - * ], |
|
| 1518 | - * ] |
|
| 1519 | - * |
|
| 1520 | - * @param RequestInterface $request |
|
| 1521 | - * @return array |
|
| 1522 | - */ |
|
| 1523 | - public function getIfConditions(RequestInterface $request) { |
|
| 1524 | - |
|
| 1525 | - $header = $request->getHeader('If'); |
|
| 1526 | - if (!$header) return []; |
|
| 1527 | - |
|
| 1528 | - $matches = []; |
|
| 1529 | - |
|
| 1530 | - $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im'; |
|
| 1531 | - preg_match_all($regex, $header, $matches, PREG_SET_ORDER); |
|
| 1532 | - |
|
| 1533 | - $conditions = []; |
|
| 1534 | - |
|
| 1535 | - foreach ($matches as $match) { |
|
| 1536 | - |
|
| 1537 | - // If there was no uri specified in this match, and there were |
|
| 1538 | - // already conditions parsed, we add the condition to the list of |
|
| 1539 | - // conditions for the previous uri. |
|
| 1540 | - if (!$match['uri'] && count($conditions)) { |
|
| 1541 | - $conditions[count($conditions) - 1]['tokens'][] = [ |
|
| 1542 | - 'negate' => $match['not'] ? true : false, |
|
| 1543 | - 'token' => $match['token'], |
|
| 1544 | - 'etag' => isset($match['etag']) ? $match['etag'] : '' |
|
| 1545 | - ]; |
|
| 1546 | - } else { |
|
| 1547 | - |
|
| 1548 | - if (!$match['uri']) { |
|
| 1549 | - $realUri = $request->getPath(); |
|
| 1550 | - } else { |
|
| 1551 | - $realUri = $this->calculateUri($match['uri']); |
|
| 1552 | - } |
|
| 1553 | - |
|
| 1554 | - $conditions[] = [ |
|
| 1555 | - 'uri' => $realUri, |
|
| 1556 | - 'tokens' => [ |
|
| 1557 | - [ |
|
| 1558 | - 'negate' => $match['not'] ? true : false, |
|
| 1559 | - 'token' => $match['token'], |
|
| 1560 | - 'etag' => isset($match['etag']) ? $match['etag'] : '' |
|
| 1561 | - ] |
|
| 1562 | - ], |
|
| 1563 | - |
|
| 1564 | - ]; |
|
| 1565 | - } |
|
| 1566 | - |
|
| 1567 | - } |
|
| 1568 | - |
|
| 1569 | - return $conditions; |
|
| 1570 | - |
|
| 1571 | - } |
|
| 1572 | - |
|
| 1573 | - /** |
|
| 1574 | - * Returns an array with resourcetypes for a node. |
|
| 1575 | - * |
|
| 1576 | - * @param INode $node |
|
| 1577 | - * @return array |
|
| 1578 | - */ |
|
| 1579 | - public function getResourceTypeForNode(INode $node) { |
|
| 1580 | - |
|
| 1581 | - $result = []; |
|
| 1582 | - foreach ($this->resourceTypeMapping as $className => $resourceType) { |
|
| 1583 | - if ($node instanceof $className) $result[] = $resourceType; |
|
| 1584 | - } |
|
| 1585 | - return $result; |
|
| 1586 | - |
|
| 1587 | - } |
|
| 1588 | - |
|
| 1589 | - // }}} |
|
| 1590 | - // {{{ XML Readers & Writers |
|
| 1591 | - |
|
| 1592 | - |
|
| 1593 | - /** |
|
| 1594 | - * Generates a WebDAV propfind response body based on a list of nodes. |
|
| 1595 | - * |
|
| 1596 | - * If 'strip404s' is set to true, all 404 responses will be removed. |
|
| 1597 | - * |
|
| 1598 | - * @param array $fileProperties The list with nodes |
|
| 1599 | - * @param bool strip404s |
|
| 1600 | - * @return string |
|
| 1601 | - */ |
|
| 1602 | - public function generateMultiStatus(array $fileProperties, $strip404s = false) { |
|
| 1603 | - |
|
| 1604 | - $xml = []; |
|
| 1605 | - |
|
| 1606 | - foreach ($fileProperties as $entry) { |
|
| 1607 | - |
|
| 1608 | - $href = $entry['href']; |
|
| 1609 | - unset($entry['href']); |
|
| 1610 | - if ($strip404s) { |
|
| 1611 | - unset($entry[404]); |
|
| 1612 | - } |
|
| 1613 | - $response = new Xml\Element\Response( |
|
| 1614 | - ltrim($href, '/'), |
|
| 1615 | - $entry |
|
| 1616 | - ); |
|
| 1617 | - $xml[] = [ |
|
| 1618 | - 'name' => '{DAV:}response', |
|
| 1619 | - 'value' => $response |
|
| 1620 | - ]; |
|
| 1621 | - |
|
| 1622 | - } |
|
| 1623 | - return $this->xml->write('{DAV:}multistatus', $xml, $this->baseUri); |
|
| 1624 | - |
|
| 1625 | - } |
|
| 533 | + /** |
|
| 534 | + * Turns a URI such as the REQUEST_URI into a local path. |
|
| 535 | + * |
|
| 536 | + * This method: |
|
| 537 | + * * strips off the base path |
|
| 538 | + * * normalizes the path |
|
| 539 | + * * uri-decodes the path |
|
| 540 | + * |
|
| 541 | + * @param string $uri |
|
| 542 | + * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri |
|
| 543 | + * @return string |
|
| 544 | + */ |
|
| 545 | + public function calculateUri($uri) { |
|
| 546 | + |
|
| 547 | + if ($uri[0] != '/' && strpos($uri, '://')) { |
|
| 548 | + |
|
| 549 | + $uri = parse_url($uri, PHP_URL_PATH); |
|
| 550 | + |
|
| 551 | + } |
|
| 552 | + |
|
| 553 | + $uri = Uri\normalize(str_replace('//', '/', $uri)); |
|
| 554 | + $baseUri = Uri\normalize($this->getBaseUri()); |
|
| 555 | + |
|
| 556 | + if (strpos($uri, $baseUri) === 0) { |
|
| 557 | + |
|
| 558 | + return trim(URLUtil::decodePath(substr($uri, strlen($baseUri))), '/'); |
|
| 559 | + |
|
| 560 | + // A special case, if the baseUri was accessed without a trailing |
|
| 561 | + // slash, we'll accept it as well. |
|
| 562 | + } elseif ($uri . '/' === $baseUri) { |
|
| 563 | + |
|
| 564 | + return ''; |
|
| 565 | + |
|
| 566 | + } else { |
|
| 567 | + |
|
| 568 | + throw new Exception\Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')'); |
|
| 569 | + |
|
| 570 | + } |
|
| 571 | + |
|
| 572 | + } |
|
| 573 | + |
|
| 574 | + /** |
|
| 575 | + * Returns the HTTP depth header |
|
| 576 | + * |
|
| 577 | + * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object |
|
| 578 | + * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent |
|
| 579 | + * |
|
| 580 | + * @param mixed $default |
|
| 581 | + * @return int |
|
| 582 | + */ |
|
| 583 | + public function getHTTPDepth($default = self::DEPTH_INFINITY) { |
|
| 584 | + |
|
| 585 | + // If its not set, we'll grab the default |
|
| 586 | + $depth = $this->httpRequest->getHeader('Depth'); |
|
| 587 | + |
|
| 588 | + if (is_null($depth)) return $default; |
|
| 589 | + |
|
| 590 | + if ($depth == 'infinity') return self::DEPTH_INFINITY; |
|
| 591 | + |
|
| 592 | + |
|
| 593 | + // If its an unknown value. we'll grab the default |
|
| 594 | + if (!ctype_digit($depth)) return $default; |
|
| 595 | + |
|
| 596 | + return (int)$depth; |
|
| 597 | + |
|
| 598 | + } |
|
| 599 | + |
|
| 600 | + /** |
|
| 601 | + * Returns the HTTP range header |
|
| 602 | + * |
|
| 603 | + * This method returns null if there is no well-formed HTTP range request |
|
| 604 | + * header or array($start, $end). |
|
| 605 | + * |
|
| 606 | + * The first number is the offset of the first byte in the range. |
|
| 607 | + * The second number is the offset of the last byte in the range. |
|
| 608 | + * |
|
| 609 | + * If the second offset is null, it should be treated as the offset of the last byte of the entity |
|
| 610 | + * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity |
|
| 611 | + * |
|
| 612 | + * @return array|null |
|
| 613 | + */ |
|
| 614 | + public function getHTTPRange() { |
|
| 615 | + |
|
| 616 | + $range = $this->httpRequest->getHeader('range'); |
|
| 617 | + if (is_null($range)) return null; |
|
| 618 | + |
|
| 619 | + // Matching "Range: bytes=1234-5678: both numbers are optional |
|
| 620 | + |
|
| 621 | + if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) return null; |
|
| 622 | + |
|
| 623 | + if ($matches[1] === '' && $matches[2] === '') return null; |
|
| 624 | + |
|
| 625 | + return [ |
|
| 626 | + $matches[1] !== '' ? $matches[1] : null, |
|
| 627 | + $matches[2] !== '' ? $matches[2] : null, |
|
| 628 | + ]; |
|
| 629 | + |
|
| 630 | + } |
|
| 631 | + |
|
| 632 | + /** |
|
| 633 | + * Returns the HTTP Prefer header information. |
|
| 634 | + * |
|
| 635 | + * The prefer header is defined in: |
|
| 636 | + * http://tools.ietf.org/html/draft-snell-http-prefer-14 |
|
| 637 | + * |
|
| 638 | + * This method will return an array with options. |
|
| 639 | + * |
|
| 640 | + * Currently, the following options may be returned: |
|
| 641 | + * [ |
|
| 642 | + * 'return-asynch' => true, |
|
| 643 | + * 'return-minimal' => true, |
|
| 644 | + * 'return-representation' => true, |
|
| 645 | + * 'wait' => 30, |
|
| 646 | + * 'strict' => true, |
|
| 647 | + * 'lenient' => true, |
|
| 648 | + * ] |
|
| 649 | + * |
|
| 650 | + * This method also supports the Brief header, and will also return |
|
| 651 | + * 'return-minimal' if the brief header was set to 't'. |
|
| 652 | + * |
|
| 653 | + * For the boolean options, false will be returned if the headers are not |
|
| 654 | + * specified. For the integer options it will be 'null'. |
|
| 655 | + * |
|
| 656 | + * @return array |
|
| 657 | + */ |
|
| 658 | + public function getHTTPPrefer() { |
|
| 659 | + |
|
| 660 | + $result = [ |
|
| 661 | + // can be true or false |
|
| 662 | + 'respond-async' => false, |
|
| 663 | + // Could be set to 'representation' or 'minimal'. |
|
| 664 | + 'return' => null, |
|
| 665 | + // Used as a timeout, is usually a number. |
|
| 666 | + 'wait' => null, |
|
| 667 | + // can be 'strict' or 'lenient'. |
|
| 668 | + 'handling' => false, |
|
| 669 | + ]; |
|
| 670 | + |
|
| 671 | + if ($prefer = $this->httpRequest->getHeader('Prefer')) { |
|
| 672 | + |
|
| 673 | + $result = array_merge( |
|
| 674 | + $result, |
|
| 675 | + \Sabre\HTTP\parsePrefer($prefer) |
|
| 676 | + ); |
|
| 677 | + |
|
| 678 | + } elseif ($this->httpRequest->getHeader('Brief') == 't') { |
|
| 679 | + $result['return'] = 'minimal'; |
|
| 680 | + } |
|
| 681 | + |
|
| 682 | + return $result; |
|
| 683 | + |
|
| 684 | + } |
|
| 685 | + |
|
| 686 | + |
|
| 687 | + /** |
|
| 688 | + * Returns information about Copy and Move requests |
|
| 689 | + * |
|
| 690 | + * This function is created to help getting information about the source and the destination for the |
|
| 691 | + * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions |
|
| 692 | + * |
|
| 693 | + * The returned value is an array with the following keys: |
|
| 694 | + * * destination - Destination path |
|
| 695 | + * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten) |
|
| 696 | + * |
|
| 697 | + * @param RequestInterface $request |
|
| 698 | + * @throws Exception\BadRequest upon missing or broken request headers |
|
| 699 | + * @throws Exception\UnsupportedMediaType when trying to copy into a |
|
| 700 | + * non-collection. |
|
| 701 | + * @throws Exception\PreconditionFailed If overwrite is set to false, but |
|
| 702 | + * the destination exists. |
|
| 703 | + * @throws Exception\Forbidden when source and destination paths are |
|
| 704 | + * identical. |
|
| 705 | + * @throws Exception\Conflict When trying to copy a node into its own |
|
| 706 | + * subtree. |
|
| 707 | + * @return array |
|
| 708 | + */ |
|
| 709 | + public function getCopyAndMoveInfo(RequestInterface $request) { |
|
| 710 | + |
|
| 711 | + // Collecting the relevant HTTP headers |
|
| 712 | + if (!$request->getHeader('Destination')) throw new Exception\BadRequest('The destination header was not supplied'); |
|
| 713 | + $destination = $this->calculateUri($request->getHeader('Destination')); |
|
| 714 | + $overwrite = $request->getHeader('Overwrite'); |
|
| 715 | + if (!$overwrite) $overwrite = 'T'; |
|
| 716 | + if (strtoupper($overwrite) == 'T') $overwrite = true; |
|
| 717 | + elseif (strtoupper($overwrite) == 'F') $overwrite = false; |
|
| 718 | + // We need to throw a bad request exception, if the header was invalid |
|
| 719 | + else throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F'); |
|
| 720 | + |
|
| 721 | + list($destinationDir) = URLUtil::splitPath($destination); |
|
| 722 | + |
|
| 723 | + try { |
|
| 724 | + $destinationParent = $this->tree->getNodeForPath($destinationDir); |
|
| 725 | + if (!($destinationParent instanceof ICollection)) throw new Exception\UnsupportedMediaType('The destination node is not a collection'); |
|
| 726 | + } catch (Exception\NotFound $e) { |
|
| 727 | + |
|
| 728 | + // If the destination parent node is not found, we throw a 409 |
|
| 729 | + throw new Exception\Conflict('The destination node is not found'); |
|
| 730 | + } |
|
| 731 | + |
|
| 732 | + try { |
|
| 733 | + |
|
| 734 | + $destinationNode = $this->tree->getNodeForPath($destination); |
|
| 735 | + |
|
| 736 | + // If this succeeded, it means the destination already exists |
|
| 737 | + // we'll need to throw precondition failed in case overwrite is false |
|
| 738 | + if (!$overwrite) throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite'); |
|
| 739 | + |
|
| 740 | + } catch (Exception\NotFound $e) { |
|
| 741 | + |
|
| 742 | + // Destination didn't exist, we're all good |
|
| 743 | + $destinationNode = false; |
|
| 744 | + |
|
| 745 | + } |
|
| 746 | + |
|
| 747 | + $requestPath = $request->getPath(); |
|
| 748 | + if ($destination === $requestPath) { |
|
| 749 | + throw new Exception\Forbidden('Source and destination uri are identical.'); |
|
| 750 | + } |
|
| 751 | + if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath . '/') { |
|
| 752 | + throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.'); |
|
| 753 | + } |
|
| 754 | + |
|
| 755 | + // These are the three relevant properties we need to return |
|
| 756 | + return [ |
|
| 757 | + 'destination' => $destination, |
|
| 758 | + 'destinationExists' => !!$destinationNode, |
|
| 759 | + 'destinationNode' => $destinationNode, |
|
| 760 | + ]; |
|
| 761 | + |
|
| 762 | + } |
|
| 763 | + |
|
| 764 | + /** |
|
| 765 | + * Returns a list of properties for a path |
|
| 766 | + * |
|
| 767 | + * This is a simplified version getPropertiesForPath. If you aren't |
|
| 768 | + * interested in status codes, but you just want to have a flat list of |
|
| 769 | + * properties, use this method. |
|
| 770 | + * |
|
| 771 | + * Please note though that any problems related to retrieving properties, |
|
| 772 | + * such as permission issues will just result in an empty array being |
|
| 773 | + * returned. |
|
| 774 | + * |
|
| 775 | + * @param string $path |
|
| 776 | + * @param array $propertyNames |
|
| 777 | + */ |
|
| 778 | + public function getProperties($path, $propertyNames) { |
|
| 779 | + |
|
| 780 | + $result = $this->getPropertiesForPath($path, $propertyNames, 0); |
|
| 781 | + if (isset($result[0][200])) { |
|
| 782 | + return $result[0][200]; |
|
| 783 | + } else { |
|
| 784 | + return []; |
|
| 785 | + } |
|
| 786 | + |
|
| 787 | + } |
|
| 788 | + |
|
| 789 | + /** |
|
| 790 | + * A kid-friendly way to fetch properties for a node's children. |
|
| 791 | + * |
|
| 792 | + * The returned array will be indexed by the path of the of child node. |
|
| 793 | + * Only properties that are actually found will be returned. |
|
| 794 | + * |
|
| 795 | + * The parent node will not be returned. |
|
| 796 | + * |
|
| 797 | + * @param string $path |
|
| 798 | + * @param array $propertyNames |
|
| 799 | + * @return array |
|
| 800 | + */ |
|
| 801 | + public function getPropertiesForChildren($path, $propertyNames) { |
|
| 802 | + |
|
| 803 | + $result = []; |
|
| 804 | + foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) { |
|
| 805 | + |
|
| 806 | + // Skipping the parent path |
|
| 807 | + if ($k === 0) continue; |
|
| 808 | + |
|
| 809 | + $result[$row['href']] = $row[200]; |
|
| 810 | + |
|
| 811 | + } |
|
| 812 | + return $result; |
|
| 813 | + |
|
| 814 | + } |
|
| 815 | + |
|
| 816 | + /** |
|
| 817 | + * Returns a list of HTTP headers for a particular resource |
|
| 818 | + * |
|
| 819 | + * The generated http headers are based on properties provided by the |
|
| 820 | + * resource. The method basically provides a simple mapping between |
|
| 821 | + * DAV property and HTTP header. |
|
| 822 | + * |
|
| 823 | + * The headers are intended to be used for HEAD and GET requests. |
|
| 824 | + * |
|
| 825 | + * @param string $path |
|
| 826 | + * @return array |
|
| 827 | + */ |
|
| 828 | + public function getHTTPHeaders($path) { |
|
| 829 | + |
|
| 830 | + $propertyMap = [ |
|
| 831 | + '{DAV:}getcontenttype' => 'Content-Type', |
|
| 832 | + '{DAV:}getcontentlength' => 'Content-Length', |
|
| 833 | + '{DAV:}getlastmodified' => 'Last-Modified', |
|
| 834 | + '{DAV:}getetag' => 'ETag', |
|
| 835 | + ]; |
|
| 836 | + |
|
| 837 | + $properties = $this->getProperties($path, array_keys($propertyMap)); |
|
| 838 | + |
|
| 839 | + $headers = []; |
|
| 840 | + foreach ($propertyMap as $property => $header) { |
|
| 841 | + if (!isset($properties[$property])) continue; |
|
| 842 | + |
|
| 843 | + if (is_scalar($properties[$property])) { |
|
| 844 | + $headers[$header] = $properties[$property]; |
|
| 845 | + |
|
| 846 | + // GetLastModified gets special cased |
|
| 847 | + } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) { |
|
| 848 | + $headers[$header] = HTTP\Util::toHTTPDate($properties[$property]->getTime()); |
|
| 849 | + } |
|
| 850 | + |
|
| 851 | + } |
|
| 852 | + |
|
| 853 | + return $headers; |
|
| 854 | + |
|
| 855 | + } |
|
| 856 | + |
|
| 857 | + /** |
|
| 858 | + * Small helper to support PROPFIND with DEPTH_INFINITY. |
|
| 859 | + * |
|
| 860 | + * @param array[] $propFindRequests |
|
| 861 | + * @param PropFind $propFind |
|
| 862 | + * @return void |
|
| 863 | + */ |
|
| 864 | + private function addPathNodesRecursively(&$propFindRequests, PropFind $propFind) { |
|
| 865 | + |
|
| 866 | + $newDepth = $propFind->getDepth(); |
|
| 867 | + $path = $propFind->getPath(); |
|
| 868 | + |
|
| 869 | + if ($newDepth !== self::DEPTH_INFINITY) { |
|
| 870 | + $newDepth--; |
|
| 871 | + } |
|
| 872 | + |
|
| 873 | + foreach ($this->tree->getChildren($path) as $childNode) { |
|
| 874 | + $subPropFind = clone $propFind; |
|
| 875 | + $subPropFind->setDepth($newDepth); |
|
| 876 | + if ($path !== '') { |
|
| 877 | + $subPath = $path . '/' . $childNode->getName(); |
|
| 878 | + } else { |
|
| 879 | + $subPath = $childNode->getName(); |
|
| 880 | + } |
|
| 881 | + $subPropFind->setPath($subPath); |
|
| 882 | + |
|
| 883 | + $propFindRequests[] = [ |
|
| 884 | + $subPropFind, |
|
| 885 | + $childNode |
|
| 886 | + ]; |
|
| 887 | + |
|
| 888 | + if (($newDepth === self::DEPTH_INFINITY || $newDepth >= 1) && $childNode instanceof ICollection) { |
|
| 889 | + $this->addPathNodesRecursively($propFindRequests, $subPropFind); |
|
| 890 | + } |
|
| 891 | + |
|
| 892 | + } |
|
| 893 | + } |
|
| 894 | + |
|
| 895 | + /** |
|
| 896 | + * Returns a list of properties for a given path |
|
| 897 | + * |
|
| 898 | + * The path that should be supplied should have the baseUrl stripped out |
|
| 899 | + * The list of properties should be supplied in Clark notation. If the list is empty |
|
| 900 | + * 'allprops' is assumed. |
|
| 901 | + * |
|
| 902 | + * If a depth of 1 is requested child elements will also be returned. |
|
| 903 | + * |
|
| 904 | + * @param string $path |
|
| 905 | + * @param array $propertyNames |
|
| 906 | + * @param int $depth |
|
| 907 | + * @return array |
|
| 908 | + */ |
|
| 909 | + public function getPropertiesForPath($path, $propertyNames = [], $depth = 0) { |
|
| 910 | + |
|
| 911 | + // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled |
|
| 912 | + if (!$this->enablePropfindDepthInfinity && $depth != 0) $depth = 1; |
|
| 913 | + |
|
| 914 | + $path = trim($path, '/'); |
|
| 915 | + |
|
| 916 | + $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; |
|
| 917 | + $propFind = new PropFind($path, (array)$propertyNames, $depth, $propFindType); |
|
| 918 | + |
|
| 919 | + $parentNode = $this->tree->getNodeForPath($path); |
|
| 920 | + |
|
| 921 | + $propFindRequests = [[ |
|
| 922 | + $propFind, |
|
| 923 | + $parentNode |
|
| 924 | + ]]; |
|
| 925 | + |
|
| 926 | + if (($depth > 0 || $depth === self::DEPTH_INFINITY) && $parentNode instanceof ICollection) { |
|
| 927 | + $this->addPathNodesRecursively($propFindRequests, $propFind); |
|
| 928 | + } |
|
| 929 | + |
|
| 930 | + $returnPropertyList = []; |
|
| 931 | + |
|
| 932 | + foreach ($propFindRequests as $propFindRequest) { |
|
| 933 | + |
|
| 934 | + list($propFind, $node) = $propFindRequest; |
|
| 935 | + $r = $this->getPropertiesByNode($propFind, $node); |
|
| 936 | + if ($r) { |
|
| 937 | + $result = $propFind->getResultForMultiStatus(); |
|
| 938 | + $result['href'] = $propFind->getPath(); |
|
| 939 | + |
|
| 940 | + // WebDAV recommends adding a slash to the path, if the path is |
|
| 941 | + // a collection. |
|
| 942 | + // Furthermore, iCal also demands this to be the case for |
|
| 943 | + // principals. This is non-standard, but we support it. |
|
| 944 | + $resourceType = $this->getResourceTypeForNode($node); |
|
| 945 | + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { |
|
| 946 | + $result['href'] .= '/'; |
|
| 947 | + } |
|
| 948 | + $returnPropertyList[] = $result; |
|
| 949 | + } |
|
| 950 | + |
|
| 951 | + } |
|
| 952 | + |
|
| 953 | + return $returnPropertyList; |
|
| 954 | + |
|
| 955 | + } |
|
| 956 | + |
|
| 957 | + /** |
|
| 958 | + * Returns a list of properties for a list of paths. |
|
| 959 | + * |
|
| 960 | + * The path that should be supplied should have the baseUrl stripped out |
|
| 961 | + * The list of properties should be supplied in Clark notation. If the list is empty |
|
| 962 | + * 'allprops' is assumed. |
|
| 963 | + * |
|
| 964 | + * The result is returned as an array, with paths for it's keys. |
|
| 965 | + * The result may be returned out of order. |
|
| 966 | + * |
|
| 967 | + * @param array $paths |
|
| 968 | + * @param array $propertyNames |
|
| 969 | + * @return array |
|
| 970 | + */ |
|
| 971 | + public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) { |
|
| 972 | + |
|
| 973 | + $result = [ |
|
| 974 | + ]; |
|
| 975 | + |
|
| 976 | + $nodes = $this->tree->getMultipleNodes($paths); |
|
| 977 | + |
|
| 978 | + foreach ($nodes as $path => $node) { |
|
| 979 | + |
|
| 980 | + $propFind = new PropFind($path, $propertyNames); |
|
| 981 | + $r = $this->getPropertiesByNode($propFind, $node); |
|
| 982 | + if ($r) { |
|
| 983 | + $result[$path] = $propFind->getResultForMultiStatus(); |
|
| 984 | + $result[$path]['href'] = $path; |
|
| 985 | + |
|
| 986 | + $resourceType = $this->getResourceTypeForNode($node); |
|
| 987 | + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { |
|
| 988 | + $result[$path]['href'] .= '/'; |
|
| 989 | + } |
|
| 990 | + } |
|
| 991 | + |
|
| 992 | + } |
|
| 993 | + |
|
| 994 | + return $result; |
|
| 995 | + |
|
| 996 | + } |
|
| 997 | + |
|
| 998 | + |
|
| 999 | + /** |
|
| 1000 | + * Determines all properties for a node. |
|
| 1001 | + * |
|
| 1002 | + * This method tries to grab all properties for a node. This method is used |
|
| 1003 | + * internally getPropertiesForPath and a few others. |
|
| 1004 | + * |
|
| 1005 | + * It could be useful to call this, if you already have an instance of your |
|
| 1006 | + * target node and simply want to run through the system to get a correct |
|
| 1007 | + * list of properties. |
|
| 1008 | + * |
|
| 1009 | + * @param PropFind $propFind |
|
| 1010 | + * @param INode $node |
|
| 1011 | + * @return bool |
|
| 1012 | + */ |
|
| 1013 | + public function getPropertiesByNode(PropFind $propFind, INode $node) { |
|
| 1014 | + |
|
| 1015 | + return $this->emit('propFind', [$propFind, $node]); |
|
| 1016 | + |
|
| 1017 | + } |
|
| 1018 | + |
|
| 1019 | + /** |
|
| 1020 | + * This method is invoked by sub-systems creating a new file. |
|
| 1021 | + * |
|
| 1022 | + * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). |
|
| 1023 | + * It was important to get this done through a centralized function, |
|
| 1024 | + * allowing plugins to intercept this using the beforeCreateFile event. |
|
| 1025 | + * |
|
| 1026 | + * This method will return true if the file was actually created |
|
| 1027 | + * |
|
| 1028 | + * @param string $uri |
|
| 1029 | + * @param resource $data |
|
| 1030 | + * @param string $etag |
|
| 1031 | + * @return bool |
|
| 1032 | + */ |
|
| 1033 | + public function createFile($uri, $data, &$etag = null) { |
|
| 1034 | + |
|
| 1035 | + list($dir, $name) = URLUtil::splitPath($uri); |
|
| 1036 | + |
|
| 1037 | + if (!$this->emit('beforeBind', [$uri])) return false; |
|
| 1038 | + |
|
| 1039 | + $parent = $this->tree->getNodeForPath($dir); |
|
| 1040 | + if (!$parent instanceof ICollection) { |
|
| 1041 | + throw new Exception\Conflict('Files can only be created as children of collections'); |
|
| 1042 | + } |
|
| 1043 | + |
|
| 1044 | + // It is possible for an event handler to modify the content of the |
|
| 1045 | + // body, before it gets written. If this is the case, $modified |
|
| 1046 | + // should be set to true. |
|
| 1047 | + // |
|
| 1048 | + // If $modified is true, we must not send back an ETag. |
|
| 1049 | + $modified = false; |
|
| 1050 | + if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) return false; |
|
| 1051 | + |
|
| 1052 | + $etag = $parent->createFile($name, $data); |
|
| 1053 | + |
|
| 1054 | + if ($modified) $etag = null; |
|
| 1055 | + |
|
| 1056 | + $this->tree->markDirty($dir . '/' . $name); |
|
| 1057 | + |
|
| 1058 | + $this->emit('afterBind', [$uri]); |
|
| 1059 | + $this->emit('afterCreateFile', [$uri, $parent]); |
|
| 1060 | + |
|
| 1061 | + return true; |
|
| 1062 | + } |
|
| 1063 | + |
|
| 1064 | + /** |
|
| 1065 | + * This method is invoked by sub-systems updating a file. |
|
| 1066 | + * |
|
| 1067 | + * This method will return true if the file was actually updated |
|
| 1068 | + * |
|
| 1069 | + * @param string $uri |
|
| 1070 | + * @param resource $data |
|
| 1071 | + * @param string $etag |
|
| 1072 | + * @return bool |
|
| 1073 | + */ |
|
| 1074 | + public function updateFile($uri, $data, &$etag = null) { |
|
| 1075 | + |
|
| 1076 | + $node = $this->tree->getNodeForPath($uri); |
|
| 1077 | + |
|
| 1078 | + // It is possible for an event handler to modify the content of the |
|
| 1079 | + // body, before it gets written. If this is the case, $modified |
|
| 1080 | + // should be set to true. |
|
| 1081 | + // |
|
| 1082 | + // If $modified is true, we must not send back an ETag. |
|
| 1083 | + $modified = false; |
|
| 1084 | + if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) return false; |
|
| 1085 | + |
|
| 1086 | + $etag = $node->put($data); |
|
| 1087 | + if ($modified) $etag = null; |
|
| 1088 | + $this->emit('afterWriteContent', [$uri, $node]); |
|
| 1089 | + |
|
| 1090 | + return true; |
|
| 1091 | + } |
|
| 1092 | + |
|
| 1093 | + |
|
| 1094 | + |
|
| 1095 | + /** |
|
| 1096 | + * This method is invoked by sub-systems creating a new directory. |
|
| 1097 | + * |
|
| 1098 | + * @param string $uri |
|
| 1099 | + * @return void |
|
| 1100 | + */ |
|
| 1101 | + public function createDirectory($uri) { |
|
| 1102 | + |
|
| 1103 | + $this->createCollection($uri, new MkCol(['{DAV:}collection'], [])); |
|
| 1104 | + |
|
| 1105 | + } |
|
| 1106 | + |
|
| 1107 | + /** |
|
| 1108 | + * Use this method to create a new collection |
|
| 1109 | + * |
|
| 1110 | + * @param string $uri The new uri |
|
| 1111 | + * @param MkCol $mkCol |
|
| 1112 | + * @return array|null |
|
| 1113 | + */ |
|
| 1114 | + public function createCollection($uri, MkCol $mkCol) { |
|
| 1115 | + |
|
| 1116 | + list($parentUri, $newName) = URLUtil::splitPath($uri); |
|
| 1117 | + |
|
| 1118 | + // Making sure the parent exists |
|
| 1119 | + try { |
|
| 1120 | + $parent = $this->tree->getNodeForPath($parentUri); |
|
| 1121 | + |
|
| 1122 | + } catch (Exception\NotFound $e) { |
|
| 1123 | + throw new Exception\Conflict('Parent node does not exist'); |
|
| 1124 | + |
|
| 1125 | + } |
|
| 1126 | + |
|
| 1127 | + // Making sure the parent is a collection |
|
| 1128 | + if (!$parent instanceof ICollection) { |
|
| 1129 | + throw new Exception\Conflict('Parent node is not a collection'); |
|
| 1130 | + } |
|
| 1131 | + |
|
| 1132 | + // Making sure the child does not already exist |
|
| 1133 | + try { |
|
| 1134 | + $parent->getChild($newName); |
|
| 1135 | + |
|
| 1136 | + // If we got here.. it means there's already a node on that url, and we need to throw a 405 |
|
| 1137 | + throw new Exception\MethodNotAllowed('The resource you tried to create already exists'); |
|
| 1138 | + |
|
| 1139 | + } catch (Exception\NotFound $e) { |
|
| 1140 | + // NotFound is the expected behavior. |
|
| 1141 | + } |
|
| 1142 | + |
|
| 1143 | + |
|
| 1144 | + if (!$this->emit('beforeBind', [$uri])) return; |
|
| 1145 | + |
|
| 1146 | + if ($parent instanceof IExtendedCollection) { |
|
| 1147 | + |
|
| 1148 | + /** |
|
| 1149 | + * If the parent is an instance of IExtendedCollection, it means that |
|
| 1150 | + * we can pass the MkCol object directly as it may be able to store |
|
| 1151 | + * properties immediately. |
|
| 1152 | + */ |
|
| 1153 | + $parent->createExtendedCollection($newName, $mkCol); |
|
| 1154 | + |
|
| 1155 | + } else { |
|
| 1156 | + |
|
| 1157 | + /** |
|
| 1158 | + * If the parent is a standard ICollection, it means only |
|
| 1159 | + * 'standard' collections can be created, so we should fail any |
|
| 1160 | + * MKCOL operation that carries extra resourcetypes. |
|
| 1161 | + */ |
|
| 1162 | + if (count($mkCol->getResourceType()) > 1) { |
|
| 1163 | + throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.'); |
|
| 1164 | + } |
|
| 1165 | + |
|
| 1166 | + $parent->createDirectory($newName); |
|
| 1167 | + |
|
| 1168 | + } |
|
| 1169 | + |
|
| 1170 | + // If there are any properties that have not been handled/stored, |
|
| 1171 | + // we ask the 'propPatch' event to handle them. This will allow for |
|
| 1172 | + // example the propertyStorage system to store properties upon MKCOL. |
|
| 1173 | + if ($mkCol->getRemainingMutations()) { |
|
| 1174 | + $this->emit('propPatch', [$uri, $mkCol]); |
|
| 1175 | + } |
|
| 1176 | + $success = $mkCol->commit(); |
|
| 1177 | + |
|
| 1178 | + if (!$success) { |
|
| 1179 | + $result = $mkCol->getResult(); |
|
| 1180 | + // generateMkCol needs the href key to exist. |
|
| 1181 | + $result['href'] = $uri; |
|
| 1182 | + return $result; |
|
| 1183 | + } |
|
| 1184 | + |
|
| 1185 | + $this->tree->markDirty($parentUri); |
|
| 1186 | + $this->emit('afterBind', [$uri]); |
|
| 1187 | + |
|
| 1188 | + } |
|
| 1189 | + |
|
| 1190 | + /** |
|
| 1191 | + * This method updates a resource's properties |
|
| 1192 | + * |
|
| 1193 | + * The properties array must be a list of properties. Array-keys are |
|
| 1194 | + * property names in clarknotation, array-values are it's values. |
|
| 1195 | + * If a property must be deleted, the value should be null. |
|
| 1196 | + * |
|
| 1197 | + * Note that this request should either completely succeed, or |
|
| 1198 | + * completely fail. |
|
| 1199 | + * |
|
| 1200 | + * The response is an array with properties for keys, and http status codes |
|
| 1201 | + * as their values. |
|
| 1202 | + * |
|
| 1203 | + * @param string $path |
|
| 1204 | + * @param array $properties |
|
| 1205 | + * @return array |
|
| 1206 | + */ |
|
| 1207 | + public function updateProperties($path, array $properties) { |
|
| 1208 | + |
|
| 1209 | + $propPatch = new PropPatch($properties); |
|
| 1210 | + $this->emit('propPatch', [$path, $propPatch]); |
|
| 1211 | + $propPatch->commit(); |
|
| 1212 | + |
|
| 1213 | + return $propPatch->getResult(); |
|
| 1214 | + |
|
| 1215 | + } |
|
| 1216 | + |
|
| 1217 | + /** |
|
| 1218 | + * This method checks the main HTTP preconditions. |
|
| 1219 | + * |
|
| 1220 | + * Currently these are: |
|
| 1221 | + * * If-Match |
|
| 1222 | + * * If-None-Match |
|
| 1223 | + * * If-Modified-Since |
|
| 1224 | + * * If-Unmodified-Since |
|
| 1225 | + * |
|
| 1226 | + * The method will return true if all preconditions are met |
|
| 1227 | + * The method will return false, or throw an exception if preconditions |
|
| 1228 | + * failed. If false is returned the operation should be aborted, and |
|
| 1229 | + * the appropriate HTTP response headers are already set. |
|
| 1230 | + * |
|
| 1231 | + * Normally this method will throw 412 Precondition Failed for failures |
|
| 1232 | + * related to If-None-Match, If-Match and If-Unmodified Since. It will |
|
| 1233 | + * set the status to 304 Not Modified for If-Modified_since. |
|
| 1234 | + * |
|
| 1235 | + * @param RequestInterface $request |
|
| 1236 | + * @param ResponseInterface $response |
|
| 1237 | + * @return bool |
|
| 1238 | + */ |
|
| 1239 | + public function checkPreconditions(RequestInterface $request, ResponseInterface $response) { |
|
| 1240 | + |
|
| 1241 | + $path = $request->getPath(); |
|
| 1242 | + $node = null; |
|
| 1243 | + $lastMod = null; |
|
| 1244 | + $etag = null; |
|
| 1245 | + |
|
| 1246 | + if ($ifMatch = $request->getHeader('If-Match')) { |
|
| 1247 | + |
|
| 1248 | + // If-Match contains an entity tag. Only if the entity-tag |
|
| 1249 | + // matches we are allowed to make the request succeed. |
|
| 1250 | + // If the entity-tag is '*' we are only allowed to make the |
|
| 1251 | + // request succeed if a resource exists at that url. |
|
| 1252 | + try { |
|
| 1253 | + $node = $this->tree->getNodeForPath($path); |
|
| 1254 | + } catch (Exception\NotFound $e) { |
|
| 1255 | + throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match'); |
|
| 1256 | + } |
|
| 1257 | + |
|
| 1258 | + // Only need to check entity tags if they are not * |
|
| 1259 | + if ($ifMatch !== '*') { |
|
| 1260 | + |
|
| 1261 | + // There can be multiple ETags |
|
| 1262 | + $ifMatch = explode(',', $ifMatch); |
|
| 1263 | + $haveMatch = false; |
|
| 1264 | + foreach ($ifMatch as $ifMatchItem) { |
|
| 1265 | + |
|
| 1266 | + // Stripping any extra spaces |
|
| 1267 | + $ifMatchItem = trim($ifMatchItem, ' '); |
|
| 1268 | + |
|
| 1269 | + $etag = $node instanceof IFile ? $node->getETag() : null; |
|
| 1270 | + if ($etag === $ifMatchItem) { |
|
| 1271 | + $haveMatch = true; |
|
| 1272 | + } else { |
|
| 1273 | + // Evolution has a bug where it sometimes prepends the " |
|
| 1274 | + // with a \. This is our workaround. |
|
| 1275 | + if (str_replace('\\"', '"', $ifMatchItem) === $etag) { |
|
| 1276 | + $haveMatch = true; |
|
| 1277 | + } |
|
| 1278 | + } |
|
| 1279 | + |
|
| 1280 | + } |
|
| 1281 | + if (!$haveMatch) { |
|
| 1282 | + if ($etag) $response->setHeader('ETag', $etag); |
|
| 1283 | + throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.', 'If-Match'); |
|
| 1284 | + } |
|
| 1285 | + } |
|
| 1286 | + } |
|
| 1287 | + |
|
| 1288 | + if ($ifNoneMatch = $request->getHeader('If-None-Match')) { |
|
| 1289 | + |
|
| 1290 | + // The If-None-Match header contains an ETag. |
|
| 1291 | + // Only if the ETag does not match the current ETag, the request will succeed |
|
| 1292 | + // The header can also contain *, in which case the request |
|
| 1293 | + // will only succeed if the entity does not exist at all. |
|
| 1294 | + $nodeExists = true; |
|
| 1295 | + if (!$node) { |
|
| 1296 | + try { |
|
| 1297 | + $node = $this->tree->getNodeForPath($path); |
|
| 1298 | + } catch (Exception\NotFound $e) { |
|
| 1299 | + $nodeExists = false; |
|
| 1300 | + } |
|
| 1301 | + } |
|
| 1302 | + if ($nodeExists) { |
|
| 1303 | + $haveMatch = false; |
|
| 1304 | + if ($ifNoneMatch === '*') $haveMatch = true; |
|
| 1305 | + else { |
|
| 1306 | + |
|
| 1307 | + // There might be multiple ETags |
|
| 1308 | + $ifNoneMatch = explode(',', $ifNoneMatch); |
|
| 1309 | + $etag = $node instanceof IFile ? $node->getETag() : null; |
|
| 1310 | + |
|
| 1311 | + foreach ($ifNoneMatch as $ifNoneMatchItem) { |
|
| 1312 | + |
|
| 1313 | + // Stripping any extra spaces |
|
| 1314 | + $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); |
|
| 1315 | + |
|
| 1316 | + if ($etag === $ifNoneMatchItem) $haveMatch = true; |
|
| 1317 | + |
|
| 1318 | + } |
|
| 1319 | + |
|
| 1320 | + } |
|
| 1321 | + |
|
| 1322 | + if ($haveMatch) { |
|
| 1323 | + if ($etag) $response->setHeader('ETag', $etag); |
|
| 1324 | + if ($request->getMethod() === 'GET') { |
|
| 1325 | + $response->setStatus(304); |
|
| 1326 | + return false; |
|
| 1327 | + } else { |
|
| 1328 | + throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match'); |
|
| 1329 | + } |
|
| 1330 | + } |
|
| 1331 | + } |
|
| 1332 | + |
|
| 1333 | + } |
|
| 1334 | + |
|
| 1335 | + if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) { |
|
| 1336 | + |
|
| 1337 | + // The If-Modified-Since header contains a date. We |
|
| 1338 | + // will only return the entity if it has been changed since |
|
| 1339 | + // that date. If it hasn't been changed, we return a 304 |
|
| 1340 | + // header |
|
| 1341 | + // Note that this header only has to be checked if there was no If-None-Match header |
|
| 1342 | + // as per the HTTP spec. |
|
| 1343 | + $date = HTTP\Util::parseHTTPDate($ifModifiedSince); |
|
| 1344 | + |
|
| 1345 | + if ($date) { |
|
| 1346 | + if (is_null($node)) { |
|
| 1347 | + $node = $this->tree->getNodeForPath($path); |
|
| 1348 | + } |
|
| 1349 | + $lastMod = $node->getLastModified(); |
|
| 1350 | + if ($lastMod) { |
|
| 1351 | + $lastMod = new \DateTime('@' . $lastMod); |
|
| 1352 | + if ($lastMod <= $date) { |
|
| 1353 | + $response->setStatus(304); |
|
| 1354 | + $response->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod)); |
|
| 1355 | + return false; |
|
| 1356 | + } |
|
| 1357 | + } |
|
| 1358 | + } |
|
| 1359 | + } |
|
| 1360 | + |
|
| 1361 | + if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) { |
|
| 1362 | + |
|
| 1363 | + // The If-Unmodified-Since will allow allow the request if the |
|
| 1364 | + // entity has not changed since the specified date. |
|
| 1365 | + $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince); |
|
| 1366 | + |
|
| 1367 | + // We must only check the date if it's valid |
|
| 1368 | + if ($date) { |
|
| 1369 | + if (is_null($node)) { |
|
| 1370 | + $node = $this->tree->getNodeForPath($path); |
|
| 1371 | + } |
|
| 1372 | + $lastMod = $node->getLastModified(); |
|
| 1373 | + if ($lastMod) { |
|
| 1374 | + $lastMod = new \DateTime('@' . $lastMod); |
|
| 1375 | + if ($lastMod > $date) { |
|
| 1376 | + throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since'); |
|
| 1377 | + } |
|
| 1378 | + } |
|
| 1379 | + } |
|
| 1380 | + |
|
| 1381 | + } |
|
| 1382 | + |
|
| 1383 | + // Now the hardest, the If: header. The If: header can contain multiple |
|
| 1384 | + // urls, ETags and so-called 'state tokens'. |
|
| 1385 | + // |
|
| 1386 | + // Examples of state tokens include lock-tokens (as defined in rfc4918) |
|
| 1387 | + // and sync-tokens (as defined in rfc6578). |
|
| 1388 | + // |
|
| 1389 | + // The only proper way to deal with these, is to emit events, that a |
|
| 1390 | + // Sync and Lock plugin can pick up. |
|
| 1391 | + $ifConditions = $this->getIfConditions($request); |
|
| 1392 | + |
|
| 1393 | + foreach ($ifConditions as $kk => $ifCondition) { |
|
| 1394 | + foreach ($ifCondition['tokens'] as $ii => $token) { |
|
| 1395 | + $ifConditions[$kk]['tokens'][$ii]['validToken'] = false; |
|
| 1396 | + } |
|
| 1397 | + } |
|
| 1398 | + |
|
| 1399 | + // Plugins are responsible for validating all the tokens. |
|
| 1400 | + // If a plugin deemed a token 'valid', it will set 'validToken' to |
|
| 1401 | + // true. |
|
| 1402 | + $this->emit('validateTokens', [ $request, &$ifConditions ]); |
|
| 1403 | + |
|
| 1404 | + // Now we're going to analyze the result. |
|
| 1405 | + |
|
| 1406 | + // Every ifCondition needs to validate to true, so we exit as soon as |
|
| 1407 | + // we have an invalid condition. |
|
| 1408 | + foreach ($ifConditions as $ifCondition) { |
|
| 1409 | + |
|
| 1410 | + $uri = $ifCondition['uri']; |
|
| 1411 | + $tokens = $ifCondition['tokens']; |
|
| 1412 | + |
|
| 1413 | + // We only need 1 valid token for the condition to succeed. |
|
| 1414 | + foreach ($tokens as $token) { |
|
| 1415 | + |
|
| 1416 | + $tokenValid = $token['validToken'] || !$token['token']; |
|
| 1417 | + |
|
| 1418 | + $etagValid = false; |
|
| 1419 | + if (!$token['etag']) { |
|
| 1420 | + $etagValid = true; |
|
| 1421 | + } |
|
| 1422 | + // Checking the ETag, only if the token was already deamed |
|
| 1423 | + // valid and there is one. |
|
| 1424 | + if ($token['etag'] && $tokenValid) { |
|
| 1425 | + |
|
| 1426 | + // The token was valid, and there was an ETag. We must |
|
| 1427 | + // grab the current ETag and check it. |
|
| 1428 | + $node = $this->tree->getNodeForPath($uri); |
|
| 1429 | + $etagValid = $node instanceof IFile && $node->getETag() == $token['etag']; |
|
| 1430 | + |
|
| 1431 | + } |
|
| 1432 | + |
|
| 1433 | + |
|
| 1434 | + if (($tokenValid && $etagValid) ^ $token['negate']) { |
|
| 1435 | + // Both were valid, so we can go to the next condition. |
|
| 1436 | + continue 2; |
|
| 1437 | + } |
|
| 1438 | + |
|
| 1439 | + |
|
| 1440 | + } |
|
| 1441 | + |
|
| 1442 | + // If we ended here, it means there was no valid ETag + token |
|
| 1443 | + // combination found for the current condition. This means we fail! |
|
| 1444 | + throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for ' . $uri, 'If'); |
|
| 1445 | + |
|
| 1446 | + } |
|
| 1447 | + |
|
| 1448 | + return true; |
|
| 1449 | + |
|
| 1450 | + } |
|
| 1451 | + |
|
| 1452 | + /** |
|
| 1453 | + * This method is created to extract information from the WebDAV HTTP 'If:' header |
|
| 1454 | + * |
|
| 1455 | + * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information |
|
| 1456 | + * The function will return an array, containing structs with the following keys |
|
| 1457 | + * |
|
| 1458 | + * * uri - the uri the condition applies to. |
|
| 1459 | + * * tokens - The lock token. another 2 dimensional array containing 3 elements |
|
| 1460 | + * |
|
| 1461 | + * Example 1: |
|
| 1462 | + * |
|
| 1463 | + * If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>) |
|
| 1464 | + * |
|
| 1465 | + * Would result in: |
|
| 1466 | + * |
|
| 1467 | + * [ |
|
| 1468 | + * [ |
|
| 1469 | + * 'uri' => '/request/uri', |
|
| 1470 | + * 'tokens' => [ |
|
| 1471 | + * [ |
|
| 1472 | + * [ |
|
| 1473 | + * 'negate' => false, |
|
| 1474 | + * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', |
|
| 1475 | + * 'etag' => "" |
|
| 1476 | + * ] |
|
| 1477 | + * ] |
|
| 1478 | + * ], |
|
| 1479 | + * ] |
|
| 1480 | + * ] |
|
| 1481 | + * |
|
| 1482 | + * Example 2: |
|
| 1483 | + * |
|
| 1484 | + * If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"]) |
|
| 1485 | + * |
|
| 1486 | + * Would result in: |
|
| 1487 | + * |
|
| 1488 | + * [ |
|
| 1489 | + * [ |
|
| 1490 | + * 'uri' => 'path', |
|
| 1491 | + * 'tokens' => [ |
|
| 1492 | + * [ |
|
| 1493 | + * [ |
|
| 1494 | + * 'negate' => true, |
|
| 1495 | + * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', |
|
| 1496 | + * 'etag' => '"Im An ETag"' |
|
| 1497 | + * ], |
|
| 1498 | + * [ |
|
| 1499 | + * 'negate' => false, |
|
| 1500 | + * 'token' => '', |
|
| 1501 | + * 'etag' => '"Another ETag"' |
|
| 1502 | + * ] |
|
| 1503 | + * ] |
|
| 1504 | + * ], |
|
| 1505 | + * ], |
|
| 1506 | + * [ |
|
| 1507 | + * 'uri' => 'path2', |
|
| 1508 | + * 'tokens' => [ |
|
| 1509 | + * [ |
|
| 1510 | + * [ |
|
| 1511 | + * 'negate' => true, |
|
| 1512 | + * 'token' => '', |
|
| 1513 | + * 'etag' => '"Path2 ETag"' |
|
| 1514 | + * ] |
|
| 1515 | + * ] |
|
| 1516 | + * ], |
|
| 1517 | + * ], |
|
| 1518 | + * ] |
|
| 1519 | + * |
|
| 1520 | + * @param RequestInterface $request |
|
| 1521 | + * @return array |
|
| 1522 | + */ |
|
| 1523 | + public function getIfConditions(RequestInterface $request) { |
|
| 1524 | + |
|
| 1525 | + $header = $request->getHeader('If'); |
|
| 1526 | + if (!$header) return []; |
|
| 1527 | + |
|
| 1528 | + $matches = []; |
|
| 1529 | + |
|
| 1530 | + $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im'; |
|
| 1531 | + preg_match_all($regex, $header, $matches, PREG_SET_ORDER); |
|
| 1532 | + |
|
| 1533 | + $conditions = []; |
|
| 1534 | + |
|
| 1535 | + foreach ($matches as $match) { |
|
| 1536 | + |
|
| 1537 | + // If there was no uri specified in this match, and there were |
|
| 1538 | + // already conditions parsed, we add the condition to the list of |
|
| 1539 | + // conditions for the previous uri. |
|
| 1540 | + if (!$match['uri'] && count($conditions)) { |
|
| 1541 | + $conditions[count($conditions) - 1]['tokens'][] = [ |
|
| 1542 | + 'negate' => $match['not'] ? true : false, |
|
| 1543 | + 'token' => $match['token'], |
|
| 1544 | + 'etag' => isset($match['etag']) ? $match['etag'] : '' |
|
| 1545 | + ]; |
|
| 1546 | + } else { |
|
| 1547 | + |
|
| 1548 | + if (!$match['uri']) { |
|
| 1549 | + $realUri = $request->getPath(); |
|
| 1550 | + } else { |
|
| 1551 | + $realUri = $this->calculateUri($match['uri']); |
|
| 1552 | + } |
|
| 1553 | + |
|
| 1554 | + $conditions[] = [ |
|
| 1555 | + 'uri' => $realUri, |
|
| 1556 | + 'tokens' => [ |
|
| 1557 | + [ |
|
| 1558 | + 'negate' => $match['not'] ? true : false, |
|
| 1559 | + 'token' => $match['token'], |
|
| 1560 | + 'etag' => isset($match['etag']) ? $match['etag'] : '' |
|
| 1561 | + ] |
|
| 1562 | + ], |
|
| 1563 | + |
|
| 1564 | + ]; |
|
| 1565 | + } |
|
| 1566 | + |
|
| 1567 | + } |
|
| 1568 | + |
|
| 1569 | + return $conditions; |
|
| 1570 | + |
|
| 1571 | + } |
|
| 1572 | + |
|
| 1573 | + /** |
|
| 1574 | + * Returns an array with resourcetypes for a node. |
|
| 1575 | + * |
|
| 1576 | + * @param INode $node |
|
| 1577 | + * @return array |
|
| 1578 | + */ |
|
| 1579 | + public function getResourceTypeForNode(INode $node) { |
|
| 1580 | + |
|
| 1581 | + $result = []; |
|
| 1582 | + foreach ($this->resourceTypeMapping as $className => $resourceType) { |
|
| 1583 | + if ($node instanceof $className) $result[] = $resourceType; |
|
| 1584 | + } |
|
| 1585 | + return $result; |
|
| 1586 | + |
|
| 1587 | + } |
|
| 1588 | + |
|
| 1589 | + // }}} |
|
| 1590 | + // {{{ XML Readers & Writers |
|
| 1591 | + |
|
| 1592 | + |
|
| 1593 | + /** |
|
| 1594 | + * Generates a WebDAV propfind response body based on a list of nodes. |
|
| 1595 | + * |
|
| 1596 | + * If 'strip404s' is set to true, all 404 responses will be removed. |
|
| 1597 | + * |
|
| 1598 | + * @param array $fileProperties The list with nodes |
|
| 1599 | + * @param bool strip404s |
|
| 1600 | + * @return string |
|
| 1601 | + */ |
|
| 1602 | + public function generateMultiStatus(array $fileProperties, $strip404s = false) { |
|
| 1603 | + |
|
| 1604 | + $xml = []; |
|
| 1605 | + |
|
| 1606 | + foreach ($fileProperties as $entry) { |
|
| 1607 | + |
|
| 1608 | + $href = $entry['href']; |
|
| 1609 | + unset($entry['href']); |
|
| 1610 | + if ($strip404s) { |
|
| 1611 | + unset($entry[404]); |
|
| 1612 | + } |
|
| 1613 | + $response = new Xml\Element\Response( |
|
| 1614 | + ltrim($href, '/'), |
|
| 1615 | + $entry |
|
| 1616 | + ); |
|
| 1617 | + $xml[] = [ |
|
| 1618 | + 'name' => '{DAV:}response', |
|
| 1619 | + 'value' => $response |
|
| 1620 | + ]; |
|
| 1621 | + |
|
| 1622 | + } |
|
| 1623 | + return $this->xml->write('{DAV:}multistatus', $xml, $this->baseUri); |
|
| 1624 | + |
|
| 1625 | + } |
|
| 1626 | 1626 | |
| 1627 | 1627 | } |
@@ -326,8 +326,9 @@ discard block |
||
| 326 | 326 | public function setBaseUri($uri) { |
| 327 | 327 | |
| 328 | 328 | // If the baseUri does not end with a slash, we must add it |
| 329 | - if ($uri[strlen($uri) - 1] !== '/') |
|
| 330 | - $uri .= '/'; |
|
| 329 | + if ($uri[strlen($uri) - 1] !== '/') { |
|
| 330 | + $uri .= '/'; |
|
| 331 | + } |
|
| 331 | 332 | |
| 332 | 333 | $this->baseUri = $uri; |
| 333 | 334 | |
@@ -340,7 +341,9 @@ discard block |
||
| 340 | 341 | */ |
| 341 | 342 | public function getBaseUri() { |
| 342 | 343 | |
| 343 | - if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri(); |
|
| 344 | + if (is_null($this->baseUri)) { |
|
| 345 | + $this->baseUri = $this->guessBaseUri(); |
|
| 346 | + } |
|
| 344 | 347 | return $this->baseUri; |
| 345 | 348 | |
| 346 | 349 | } |
@@ -362,8 +365,9 @@ discard block |
||
| 362 | 365 | if (!empty($pathInfo)) { |
| 363 | 366 | |
| 364 | 367 | // We need to make sure we ignore the QUERY_STRING part |
| 365 | - if ($pos = strpos($uri, '?')) |
|
| 366 | - $uri = substr($uri, 0, $pos); |
|
| 368 | + if ($pos = strpos($uri, '?')) { |
|
| 369 | + $uri = substr($uri, 0, $pos); |
|
| 370 | + } |
|
| 367 | 371 | |
| 368 | 372 | // PATH_INFO is only set for urls, such as: /example.php/path |
| 369 | 373 | // in that case PATH_INFO contains '/path'. |
@@ -412,8 +416,9 @@ discard block |
||
| 412 | 416 | */ |
| 413 | 417 | public function getPlugin($name) { |
| 414 | 418 | |
| 415 | - if (isset($this->plugins[$name])) |
|
| 416 | - return $this->plugins[$name]; |
|
| 419 | + if (isset($this->plugins[$name])) { |
|
| 420 | + return $this->plugins[$name]; |
|
| 421 | + } |
|
| 417 | 422 | |
| 418 | 423 | return null; |
| 419 | 424 | |
@@ -442,8 +447,12 @@ discard block |
||
| 442 | 447 | |
| 443 | 448 | $method = $request->getMethod(); |
| 444 | 449 | |
| 445 | - if (!$this->emit('beforeMethod:' . $method, [$request, $response])) return; |
|
| 446 | - if (!$this->emit('beforeMethod', [$request, $response])) return; |
|
| 450 | + if (!$this->emit('beforeMethod:' . $method, [$request, $response])) { |
|
| 451 | + return; |
|
| 452 | + } |
|
| 453 | + if (!$this->emit('beforeMethod', [$request, $response])) { |
|
| 454 | + return; |
|
| 455 | + } |
|
| 447 | 456 | |
| 448 | 457 | if (self::$exposeVersion) { |
| 449 | 458 | $response->setHeader('X-Sabre-Version', Version::VERSION); |
@@ -468,8 +477,12 @@ discard block |
||
| 468 | 477 | } |
| 469 | 478 | } |
| 470 | 479 | |
| 471 | - if (!$this->emit('afterMethod:' . $method, [$request, $response])) return; |
|
| 472 | - if (!$this->emit('afterMethod', [$request, $response])) return; |
|
| 480 | + if (!$this->emit('afterMethod:' . $method, [$request, $response])) { |
|
| 481 | + return; |
|
| 482 | + } |
|
| 483 | + if (!$this->emit('afterMethod', [$request, $response])) { |
|
| 484 | + return; |
|
| 485 | + } |
|
| 473 | 486 | |
| 474 | 487 | if ($response->getStatus() === null) { |
| 475 | 488 | throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.'); |
@@ -512,7 +525,9 @@ discard block |
||
| 512 | 525 | } |
| 513 | 526 | |
| 514 | 527 | // We're also checking if any of the plugins register any new methods |
| 515 | - foreach ($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($path)); |
|
| 528 | + foreach ($this->plugins as $plugin) { |
|
| 529 | + $methods = array_merge($methods, $plugin->getHTTPMethods($path)); |
|
| 530 | + } |
|
| 516 | 531 | array_unique($methods); |
| 517 | 532 | |
| 518 | 533 | return $methods; |
@@ -585,13 +600,19 @@ discard block |
||
| 585 | 600 | // If its not set, we'll grab the default |
| 586 | 601 | $depth = $this->httpRequest->getHeader('Depth'); |
| 587 | 602 | |
| 588 | - if (is_null($depth)) return $default; |
|
| 603 | + if (is_null($depth)) { |
|
| 604 | + return $default; |
|
| 605 | + } |
|
| 589 | 606 | |
| 590 | - if ($depth == 'infinity') return self::DEPTH_INFINITY; |
|
| 607 | + if ($depth == 'infinity') { |
|
| 608 | + return self::DEPTH_INFINITY; |
|
| 609 | + } |
|
| 591 | 610 | |
| 592 | 611 | |
| 593 | 612 | // If its an unknown value. we'll grab the default |
| 594 | - if (!ctype_digit($depth)) return $default; |
|
| 613 | + if (!ctype_digit($depth)) { |
|
| 614 | + return $default; |
|
| 615 | + } |
|
| 595 | 616 | |
| 596 | 617 | return (int)$depth; |
| 597 | 618 | |
@@ -614,13 +635,19 @@ discard block |
||
| 614 | 635 | public function getHTTPRange() { |
| 615 | 636 | |
| 616 | 637 | $range = $this->httpRequest->getHeader('range'); |
| 617 | - if (is_null($range)) return null; |
|
| 638 | + if (is_null($range)) { |
|
| 639 | + return null; |
|
| 640 | + } |
|
| 618 | 641 | |
| 619 | 642 | // Matching "Range: bytes=1234-5678: both numbers are optional |
| 620 | 643 | |
| 621 | - if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) return null; |
|
| 644 | + if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) { |
|
| 645 | + return null; |
|
| 646 | + } |
|
| 622 | 647 | |
| 623 | - if ($matches[1] === '' && $matches[2] === '') return null; |
|
| 648 | + if ($matches[1] === '' && $matches[2] === '') { |
|
| 649 | + return null; |
|
| 650 | + } |
|
| 624 | 651 | |
| 625 | 652 | return [ |
| 626 | 653 | $matches[1] !== '' ? $matches[1] : null, |
@@ -709,20 +736,31 @@ discard block |
||
| 709 | 736 | public function getCopyAndMoveInfo(RequestInterface $request) { |
| 710 | 737 | |
| 711 | 738 | // Collecting the relevant HTTP headers |
| 712 | - if (!$request->getHeader('Destination')) throw new Exception\BadRequest('The destination header was not supplied'); |
|
| 739 | + if (!$request->getHeader('Destination')) { |
|
| 740 | + throw new Exception\BadRequest('The destination header was not supplied'); |
|
| 741 | + } |
|
| 713 | 742 | $destination = $this->calculateUri($request->getHeader('Destination')); |
| 714 | 743 | $overwrite = $request->getHeader('Overwrite'); |
| 715 | - if (!$overwrite) $overwrite = 'T'; |
|
| 716 | - if (strtoupper($overwrite) == 'T') $overwrite = true; |
|
| 717 | - elseif (strtoupper($overwrite) == 'F') $overwrite = false; |
|
| 744 | + if (!$overwrite) { |
|
| 745 | + $overwrite = 'T'; |
|
| 746 | + } |
|
| 747 | + if (strtoupper($overwrite) == 'T') { |
|
| 748 | + $overwrite = true; |
|
| 749 | + } elseif (strtoupper($overwrite) == 'F') { |
|
| 750 | + $overwrite = false; |
|
| 751 | + } |
|
| 718 | 752 | // We need to throw a bad request exception, if the header was invalid |
| 719 | - else throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F'); |
|
| 753 | + else { |
|
| 754 | + throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F'); |
|
| 755 | + } |
|
| 720 | 756 | |
| 721 | 757 | list($destinationDir) = URLUtil::splitPath($destination); |
| 722 | 758 | |
| 723 | 759 | try { |
| 724 | 760 | $destinationParent = $this->tree->getNodeForPath($destinationDir); |
| 725 | - if (!($destinationParent instanceof ICollection)) throw new Exception\UnsupportedMediaType('The destination node is not a collection'); |
|
| 761 | + if (!($destinationParent instanceof ICollection)) { |
|
| 762 | + throw new Exception\UnsupportedMediaType('The destination node is not a collection'); |
|
| 763 | + } |
|
| 726 | 764 | } catch (Exception\NotFound $e) { |
| 727 | 765 | |
| 728 | 766 | // If the destination parent node is not found, we throw a 409 |
@@ -735,7 +773,9 @@ discard block |
||
| 735 | 773 | |
| 736 | 774 | // If this succeeded, it means the destination already exists |
| 737 | 775 | // we'll need to throw precondition failed in case overwrite is false |
| 738 | - if (!$overwrite) throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite'); |
|
| 776 | + if (!$overwrite) { |
|
| 777 | + throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite'); |
|
| 778 | + } |
|
| 739 | 779 | |
| 740 | 780 | } catch (Exception\NotFound $e) { |
| 741 | 781 | |
@@ -804,7 +844,9 @@ discard block |
||
| 804 | 844 | foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) { |
| 805 | 845 | |
| 806 | 846 | // Skipping the parent path |
| 807 | - if ($k === 0) continue; |
|
| 847 | + if ($k === 0) { |
|
| 848 | + continue; |
|
| 849 | + } |
|
| 808 | 850 | |
| 809 | 851 | $result[$row['href']] = $row[200]; |
| 810 | 852 | |
@@ -838,7 +880,9 @@ discard block |
||
| 838 | 880 | |
| 839 | 881 | $headers = []; |
| 840 | 882 | foreach ($propertyMap as $property => $header) { |
| 841 | - if (!isset($properties[$property])) continue; |
|
| 883 | + if (!isset($properties[$property])) { |
|
| 884 | + continue; |
|
| 885 | + } |
|
| 842 | 886 | |
| 843 | 887 | if (is_scalar($properties[$property])) { |
| 844 | 888 | $headers[$header] = $properties[$property]; |
@@ -909,7 +953,9 @@ discard block |
||
| 909 | 953 | public function getPropertiesForPath($path, $propertyNames = [], $depth = 0) { |
| 910 | 954 | |
| 911 | 955 | // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled |
| 912 | - if (!$this->enablePropfindDepthInfinity && $depth != 0) $depth = 1; |
|
| 956 | + if (!$this->enablePropfindDepthInfinity && $depth != 0) { |
|
| 957 | + $depth = 1; |
|
| 958 | + } |
|
| 913 | 959 | |
| 914 | 960 | $path = trim($path, '/'); |
| 915 | 961 | |
@@ -1034,7 +1080,9 @@ discard block |
||
| 1034 | 1080 | |
| 1035 | 1081 | list($dir, $name) = URLUtil::splitPath($uri); |
| 1036 | 1082 | |
| 1037 | - if (!$this->emit('beforeBind', [$uri])) return false; |
|
| 1083 | + if (!$this->emit('beforeBind', [$uri])) { |
|
| 1084 | + return false; |
|
| 1085 | + } |
|
| 1038 | 1086 | |
| 1039 | 1087 | $parent = $this->tree->getNodeForPath($dir); |
| 1040 | 1088 | if (!$parent instanceof ICollection) { |
@@ -1047,11 +1095,15 @@ discard block |
||
| 1047 | 1095 | // |
| 1048 | 1096 | // If $modified is true, we must not send back an ETag. |
| 1049 | 1097 | $modified = false; |
| 1050 | - if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) return false; |
|
| 1098 | + if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) { |
|
| 1099 | + return false; |
|
| 1100 | + } |
|
| 1051 | 1101 | |
| 1052 | 1102 | $etag = $parent->createFile($name, $data); |
| 1053 | 1103 | |
| 1054 | - if ($modified) $etag = null; |
|
| 1104 | + if ($modified) { |
|
| 1105 | + $etag = null; |
|
| 1106 | + } |
|
| 1055 | 1107 | |
| 1056 | 1108 | $this->tree->markDirty($dir . '/' . $name); |
| 1057 | 1109 | |
@@ -1081,10 +1133,14 @@ discard block |
||
| 1081 | 1133 | // |
| 1082 | 1134 | // If $modified is true, we must not send back an ETag. |
| 1083 | 1135 | $modified = false; |
| 1084 | - if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) return false; |
|
| 1136 | + if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) { |
|
| 1137 | + return false; |
|
| 1138 | + } |
|
| 1085 | 1139 | |
| 1086 | 1140 | $etag = $node->put($data); |
| 1087 | - if ($modified) $etag = null; |
|
| 1141 | + if ($modified) { |
|
| 1142 | + $etag = null; |
|
| 1143 | + } |
|
| 1088 | 1144 | $this->emit('afterWriteContent', [$uri, $node]); |
| 1089 | 1145 | |
| 1090 | 1146 | return true; |
@@ -1141,7 +1197,9 @@ discard block |
||
| 1141 | 1197 | } |
| 1142 | 1198 | |
| 1143 | 1199 | |
| 1144 | - if (!$this->emit('beforeBind', [$uri])) return; |
|
| 1200 | + if (!$this->emit('beforeBind', [$uri])) { |
|
| 1201 | + return; |
|
| 1202 | + } |
|
| 1145 | 1203 | |
| 1146 | 1204 | if ($parent instanceof IExtendedCollection) { |
| 1147 | 1205 | |
@@ -1279,7 +1337,9 @@ discard block |
||
| 1279 | 1337 | |
| 1280 | 1338 | } |
| 1281 | 1339 | if (!$haveMatch) { |
| 1282 | - if ($etag) $response->setHeader('ETag', $etag); |
|
| 1340 | + if ($etag) { |
|
| 1341 | + $response->setHeader('ETag', $etag); |
|
| 1342 | + } |
|
| 1283 | 1343 | throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.', 'If-Match'); |
| 1284 | 1344 | } |
| 1285 | 1345 | } |
@@ -1301,8 +1361,9 @@ discard block |
||
| 1301 | 1361 | } |
| 1302 | 1362 | if ($nodeExists) { |
| 1303 | 1363 | $haveMatch = false; |
| 1304 | - if ($ifNoneMatch === '*') $haveMatch = true; |
|
| 1305 | - else { |
|
| 1364 | + if ($ifNoneMatch === '*') { |
|
| 1365 | + $haveMatch = true; |
|
| 1366 | + } else { |
|
| 1306 | 1367 | |
| 1307 | 1368 | // There might be multiple ETags |
| 1308 | 1369 | $ifNoneMatch = explode(',', $ifNoneMatch); |
@@ -1313,14 +1374,18 @@ discard block |
||
| 1313 | 1374 | // Stripping any extra spaces |
| 1314 | 1375 | $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); |
| 1315 | 1376 | |
| 1316 | - if ($etag === $ifNoneMatchItem) $haveMatch = true; |
|
| 1377 | + if ($etag === $ifNoneMatchItem) { |
|
| 1378 | + $haveMatch = true; |
|
| 1379 | + } |
|
| 1317 | 1380 | |
| 1318 | 1381 | } |
| 1319 | 1382 | |
| 1320 | 1383 | } |
| 1321 | 1384 | |
| 1322 | 1385 | if ($haveMatch) { |
| 1323 | - if ($etag) $response->setHeader('ETag', $etag); |
|
| 1386 | + if ($etag) { |
|
| 1387 | + $response->setHeader('ETag', $etag); |
|
| 1388 | + } |
|
| 1324 | 1389 | if ($request->getMethod() === 'GET') { |
| 1325 | 1390 | $response->setStatus(304); |
| 1326 | 1391 | return false; |
@@ -1523,7 +1588,9 @@ discard block |
||
| 1523 | 1588 | public function getIfConditions(RequestInterface $request) { |
| 1524 | 1589 | |
| 1525 | 1590 | $header = $request->getHeader('If'); |
| 1526 | - if (!$header) return []; |
|
| 1591 | + if (!$header) { |
|
| 1592 | + return []; |
|
| 1593 | + } |
|
| 1527 | 1594 | |
| 1528 | 1595 | $matches = []; |
| 1529 | 1596 | |
@@ -1580,7 +1647,9 @@ discard block |
||
| 1580 | 1647 | |
| 1581 | 1648 | $result = []; |
| 1582 | 1649 | foreach ($this->resourceTypeMapping as $className => $resourceType) { |
| 1583 | - if ($node instanceof $className) $result[] = $resourceType; |
|
| 1650 | + if ($node instanceof $className) { |
|
| 1651 | + $result[] = $resourceType; |
|
| 1652 | + } |
|
| 1584 | 1653 | } |
| 1585 | 1654 | return $result; |
| 1586 | 1655 | |
@@ -78,7 +78,9 @@ discard block |
||
| 78 | 78 | $path = $request->getPath(); |
| 79 | 79 | $node = $this->server->tree->getNodeForPath($path); |
| 80 | 80 | |
| 81 | - if (!$node instanceof IFile) return; |
|
| 81 | + if (!$node instanceof IFile) { |
|
| 82 | + return; |
|
| 83 | + } |
|
| 82 | 84 | |
| 83 | 85 | $body = $node->get(); |
| 84 | 86 | |
@@ -132,17 +134,23 @@ discard block |
||
| 132 | 134 | |
| 133 | 135 | // It's a date. We must check if the entity is modified since |
| 134 | 136 | // the specified date. |
| 135 | - if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true; |
|
| 136 | - else { |
|
| 137 | + if (!isset($httpHeaders['Last-Modified'])) { |
|
| 138 | + $ignoreRangeHeader = true; |
|
| 139 | + } else { |
|
| 137 | 140 | $modified = new \DateTime($httpHeaders['Last-Modified']); |
| 138 | - if ($modified > $ifRangeDate) $ignoreRangeHeader = true; |
|
| 141 | + if ($modified > $ifRangeDate) { |
|
| 142 | + $ignoreRangeHeader = true; |
|
| 143 | + } |
|
| 139 | 144 | } |
| 140 | 145 | |
| 141 | 146 | } catch (\Exception $e) { |
| 142 | 147 | |
| 143 | 148 | // It's an entity. We can do a simple comparison. |
| 144 | - if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true; |
|
| 145 | - elseif ($httpHeaders['ETag'] !== $ifRange) $ignoreRangeHeader = true; |
|
| 149 | + if (!isset($httpHeaders['ETag'])) { |
|
| 150 | + $ignoreRangeHeader = true; |
|
| 151 | + } elseif ($httpHeaders['ETag'] !== $ifRange) { |
|
| 152 | + $ignoreRangeHeader = true; |
|
| 153 | + } |
|
| 146 | 154 | } |
| 147 | 155 | } |
| 148 | 156 | |
@@ -154,18 +162,25 @@ discard block |
||
| 154 | 162 | |
| 155 | 163 | $start = $range[0]; |
| 156 | 164 | $end = $range[1] ? $range[1] : $nodeSize - 1; |
| 157 | - if ($start >= $nodeSize) |
|
| 158 | - throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')'); |
|
| 165 | + if ($start >= $nodeSize) { |
|
| 166 | + throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')'); |
|
| 167 | + } |
|
| 159 | 168 | |
| 160 | - if ($end < $start) throw new Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')'); |
|
| 161 | - if ($end >= $nodeSize) $end = $nodeSize - 1; |
|
| 169 | + if ($end < $start) { |
|
| 170 | + throw new Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')'); |
|
| 171 | + } |
|
| 172 | + if ($end >= $nodeSize) { |
|
| 173 | + $end = $nodeSize - 1; |
|
| 174 | + } |
|
| 162 | 175 | |
| 163 | 176 | } else { |
| 164 | 177 | |
| 165 | 178 | $start = $nodeSize - $range[1]; |
| 166 | 179 | $end = $nodeSize - 1; |
| 167 | 180 | |
| 168 | - if ($start < 0) $start = 0; |
|
| 181 | + if ($start < 0) { |
|
| 182 | + $start = 0; |
|
| 183 | + } |
|
| 169 | 184 | |
| 170 | 185 | } |
| 171 | 186 | |
@@ -175,7 +190,9 @@ discard block |
||
| 175 | 190 | if (!stream_get_meta_data($body)['seekable'] || fseek($body, $start, SEEK_SET) === -1) { |
| 176 | 191 | $consumeBlock = 8192; |
| 177 | 192 | for ($consumed = 0; $start - $consumed > 0;){ |
| 178 | - if (feof($body)) throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $start . ') exceeded the size of the entity (' . $consumed . ')'); |
|
| 193 | + if (feof($body)) { |
|
| 194 | + throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $start . ') exceeded the size of the entity (' . $consumed . ')'); |
|
| 195 | + } |
|
| 179 | 196 | $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock))); |
| 180 | 197 | } |
| 181 | 198 | } |
@@ -187,7 +204,9 @@ discard block |
||
| 187 | 204 | |
| 188 | 205 | } else { |
| 189 | 206 | |
| 190 | - if ($nodeSize) $response->setHeader('Content-Length', $nodeSize); |
|
| 207 | + if ($nodeSize) { |
|
| 208 | + $response->setHeader('Content-Length', $nodeSize); |
|
| 209 | + } |
|
| 191 | 210 | $response->setStatus(200); |
| 192 | 211 | $response->setBody($body); |
| 193 | 212 | |
@@ -281,7 +300,9 @@ discard block |
||
| 281 | 300 | |
| 282 | 301 | $path = $request->getPath(); |
| 283 | 302 | |
| 284 | - if (!$this->server->emit('beforeUnbind', [$path])) return false; |
|
| 303 | + if (!$this->server->emit('beforeUnbind', [$path])) { |
|
| 304 | + return false; |
|
| 305 | + } |
|
| 285 | 306 | $this->server->tree->delete($path); |
| 286 | 307 | $this->server->emit('afterUnbind', [$path]); |
| 287 | 308 | |
@@ -329,7 +350,9 @@ discard block |
||
| 329 | 350 | |
| 330 | 351 | $depth = $this->server->getHTTPDepth(1); |
| 331 | 352 | // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled |
| 332 | - if (!$this->server->enablePropfindDepthInfinity && $depth != 0) $depth = 1; |
|
| 353 | + if (!$this->server->enablePropfindDepthInfinity && $depth != 0) { |
|
| 354 | + $depth = 1; |
|
| 355 | + } |
|
| 333 | 356 | |
| 334 | 357 | $newProperties = $this->server->getPropertiesForPath($path, $propFindXml->properties, $depth); |
| 335 | 358 | |
@@ -506,14 +529,18 @@ discard block |
||
| 506 | 529 | $node = $this->server->tree->getNodeForPath($path); |
| 507 | 530 | |
| 508 | 531 | // If the node is a collection, we'll deny it |
| 509 | - if (!($node instanceof IFile)) throw new Exception\Conflict('PUT is not allowed on non-files.'); |
|
| 532 | + if (!($node instanceof IFile)) { |
|
| 533 | + throw new Exception\Conflict('PUT is not allowed on non-files.'); |
|
| 534 | + } |
|
| 510 | 535 | |
| 511 | 536 | if (!$this->server->updateFile($path, $body, $etag)) { |
| 512 | 537 | return false; |
| 513 | 538 | } |
| 514 | 539 | |
| 515 | 540 | $response->setHeader('Content-Length', '0'); |
| 516 | - if ($etag) $response->setHeader('ETag', $etag); |
|
| 541 | + if ($etag) { |
|
| 542 | + $response->setHeader('ETag', $etag); |
|
| 543 | + } |
|
| 517 | 544 | $response->setStatus(204); |
| 518 | 545 | |
| 519 | 546 | } else { |
@@ -526,7 +553,9 @@ discard block |
||
| 526 | 553 | } |
| 527 | 554 | |
| 528 | 555 | $response->setHeader('Content-Length', '0'); |
| 529 | - if ($etag) $response->setHeader('ETag', $etag); |
|
| 556 | + if ($etag) { |
|
| 557 | + $response->setHeader('ETag', $etag); |
|
| 558 | + } |
|
| 530 | 559 | $response->setStatus(201); |
| 531 | 560 | |
| 532 | 561 | } |
@@ -570,8 +599,9 @@ discard block |
||
| 570 | 599 | |
| 571 | 600 | $properties = $mkcol->getProperties(); |
| 572 | 601 | |
| 573 | - if (!isset($properties['{DAV:}resourcetype'])) |
|
| 574 | - throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property'); |
|
| 602 | + if (!isset($properties['{DAV:}resourcetype'])) { |
|
| 603 | + throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property'); |
|
| 604 | + } |
|
| 575 | 605 | |
| 576 | 606 | $resourceType = $properties['{DAV:}resourcetype']->getValue(); |
| 577 | 607 | unset($properties['{DAV:}resourcetype']); |
@@ -623,12 +653,20 @@ discard block |
||
| 623 | 653 | |
| 624 | 654 | if ($moveInfo['destinationExists']) { |
| 625 | 655 | |
| 626 | - if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) return false; |
|
| 656 | + if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) { |
|
| 657 | + return false; |
|
| 658 | + } |
|
| 627 | 659 | |
| 628 | 660 | } |
| 629 | - if (!$this->server->emit('beforeUnbind', [$path])) return false; |
|
| 630 | - if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) return false; |
|
| 631 | - if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) return false; |
|
| 661 | + if (!$this->server->emit('beforeUnbind', [$path])) { |
|
| 662 | + return false; |
|
| 663 | + } |
|
| 664 | + if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) { |
|
| 665 | + return false; |
|
| 666 | + } |
|
| 667 | + if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) { |
|
| 668 | + return false; |
|
| 669 | + } |
|
| 632 | 670 | |
| 633 | 671 | if ($moveInfo['destinationExists']) { |
| 634 | 672 | |
@@ -673,9 +711,13 @@ discard block |
||
| 673 | 711 | |
| 674 | 712 | $copyInfo = $this->server->getCopyAndMoveInfo($request); |
| 675 | 713 | |
| 676 | - if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) return false; |
|
| 714 | + if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) { |
|
| 715 | + return false; |
|
| 716 | + } |
|
| 677 | 717 | if ($copyInfo['destinationExists']) { |
| 678 | - if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) return false; |
|
| 718 | + if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) { |
|
| 719 | + return false; |
|
| 720 | + } |
|
| 679 | 721 | $this->server->tree->delete($copyInfo['destination']); |
| 680 | 722 | } |
| 681 | 723 | |
@@ -871,7 +913,9 @@ discard block |
||
| 871 | 913 | // If we already have a sync-token from the current propFind |
| 872 | 914 | // request, we can re-use that. |
| 873 | 915 | $val = $propFind->get('{http://sabredav.org/ns}sync-token'); |
| 874 | - if ($val) return $val; |
|
| 916 | + if ($val) { |
|
| 917 | + return $val; |
|
| 918 | + } |
|
| 875 | 919 | |
| 876 | 920 | $val = $propFind->get('{DAV:}sync-token'); |
| 877 | 921 | if ($val && is_scalar($val)) { |
@@ -16,449 +16,449 @@ discard block |
||
| 16 | 16 | */ |
| 17 | 17 | class CorePlugin extends ServerPlugin { |
| 18 | 18 | |
| 19 | - /** |
|
| 20 | - * Reference to server object. |
|
| 21 | - * |
|
| 22 | - * @var Server |
|
| 23 | - */ |
|
| 24 | - protected $server; |
|
| 25 | - |
|
| 26 | - /** |
|
| 27 | - * Sets up the plugin |
|
| 28 | - * |
|
| 29 | - * @param Server $server |
|
| 30 | - * @return void |
|
| 31 | - */ |
|
| 32 | - public function initialize(Server $server) { |
|
| 33 | - |
|
| 34 | - $this->server = $server; |
|
| 35 | - $server->on('method:GET', [$this, 'httpGet']); |
|
| 36 | - $server->on('method:OPTIONS', [$this, 'httpOptions']); |
|
| 37 | - $server->on('method:HEAD', [$this, 'httpHead']); |
|
| 38 | - $server->on('method:DELETE', [$this, 'httpDelete']); |
|
| 39 | - $server->on('method:PROPFIND', [$this, 'httpPropFind']); |
|
| 40 | - $server->on('method:PROPPATCH', [$this, 'httpPropPatch']); |
|
| 41 | - $server->on('method:PUT', [$this, 'httpPut']); |
|
| 42 | - $server->on('method:MKCOL', [$this, 'httpMkcol']); |
|
| 43 | - $server->on('method:MOVE', [$this, 'httpMove']); |
|
| 44 | - $server->on('method:COPY', [$this, 'httpCopy']); |
|
| 45 | - $server->on('method:REPORT', [$this, 'httpReport']); |
|
| 46 | - |
|
| 47 | - $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90); |
|
| 48 | - $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200); |
|
| 49 | - $server->on('propFind', [$this, 'propFind']); |
|
| 50 | - $server->on('propFind', [$this, 'propFindNode'], 120); |
|
| 51 | - $server->on('propFind', [$this, 'propFindLate'], 200); |
|
| 52 | - |
|
| 53 | - } |
|
| 54 | - |
|
| 55 | - /** |
|
| 56 | - * Returns a plugin name. |
|
| 57 | - * |
|
| 58 | - * Using this name other plugins will be able to access other plugins |
|
| 59 | - * using DAV\Server::getPlugin |
|
| 60 | - * |
|
| 61 | - * @return string |
|
| 62 | - */ |
|
| 63 | - public function getPluginName() { |
|
| 64 | - |
|
| 65 | - return 'core'; |
|
| 66 | - |
|
| 67 | - } |
|
| 68 | - |
|
| 69 | - /** |
|
| 70 | - * This is the default implementation for the GET method. |
|
| 71 | - * |
|
| 72 | - * @param RequestInterface $request |
|
| 73 | - * @param ResponseInterface $response |
|
| 74 | - * @return bool |
|
| 75 | - */ |
|
| 76 | - public function httpGet(RequestInterface $request, ResponseInterface $response) { |
|
| 77 | - |
|
| 78 | - $path = $request->getPath(); |
|
| 79 | - $node = $this->server->tree->getNodeForPath($path); |
|
| 80 | - |
|
| 81 | - if (!$node instanceof IFile) return; |
|
| 82 | - |
|
| 83 | - $body = $node->get(); |
|
| 84 | - |
|
| 85 | - // Converting string into stream, if needed. |
|
| 86 | - if (is_string($body)) { |
|
| 87 | - $stream = fopen('php://temp', 'r+'); |
|
| 88 | - fwrite($stream, $body); |
|
| 89 | - rewind($stream); |
|
| 90 | - $body = $stream; |
|
| 91 | - } |
|
| 92 | - |
|
| 93 | - $httpHeaders = $this->server->getHTTPHeaders($path); |
|
| 94 | - |
|
| 95 | - /* ContentType needs to get a default, because many webservers will otherwise |
|
| 19 | + /** |
|
| 20 | + * Reference to server object. |
|
| 21 | + * |
|
| 22 | + * @var Server |
|
| 23 | + */ |
|
| 24 | + protected $server; |
|
| 25 | + |
|
| 26 | + /** |
|
| 27 | + * Sets up the plugin |
|
| 28 | + * |
|
| 29 | + * @param Server $server |
|
| 30 | + * @return void |
|
| 31 | + */ |
|
| 32 | + public function initialize(Server $server) { |
|
| 33 | + |
|
| 34 | + $this->server = $server; |
|
| 35 | + $server->on('method:GET', [$this, 'httpGet']); |
|
| 36 | + $server->on('method:OPTIONS', [$this, 'httpOptions']); |
|
| 37 | + $server->on('method:HEAD', [$this, 'httpHead']); |
|
| 38 | + $server->on('method:DELETE', [$this, 'httpDelete']); |
|
| 39 | + $server->on('method:PROPFIND', [$this, 'httpPropFind']); |
|
| 40 | + $server->on('method:PROPPATCH', [$this, 'httpPropPatch']); |
|
| 41 | + $server->on('method:PUT', [$this, 'httpPut']); |
|
| 42 | + $server->on('method:MKCOL', [$this, 'httpMkcol']); |
|
| 43 | + $server->on('method:MOVE', [$this, 'httpMove']); |
|
| 44 | + $server->on('method:COPY', [$this, 'httpCopy']); |
|
| 45 | + $server->on('method:REPORT', [$this, 'httpReport']); |
|
| 46 | + |
|
| 47 | + $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90); |
|
| 48 | + $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200); |
|
| 49 | + $server->on('propFind', [$this, 'propFind']); |
|
| 50 | + $server->on('propFind', [$this, 'propFindNode'], 120); |
|
| 51 | + $server->on('propFind', [$this, 'propFindLate'], 200); |
|
| 52 | + |
|
| 53 | + } |
|
| 54 | + |
|
| 55 | + /** |
|
| 56 | + * Returns a plugin name. |
|
| 57 | + * |
|
| 58 | + * Using this name other plugins will be able to access other plugins |
|
| 59 | + * using DAV\Server::getPlugin |
|
| 60 | + * |
|
| 61 | + * @return string |
|
| 62 | + */ |
|
| 63 | + public function getPluginName() { |
|
| 64 | + |
|
| 65 | + return 'core'; |
|
| 66 | + |
|
| 67 | + } |
|
| 68 | + |
|
| 69 | + /** |
|
| 70 | + * This is the default implementation for the GET method. |
|
| 71 | + * |
|
| 72 | + * @param RequestInterface $request |
|
| 73 | + * @param ResponseInterface $response |
|
| 74 | + * @return bool |
|
| 75 | + */ |
|
| 76 | + public function httpGet(RequestInterface $request, ResponseInterface $response) { |
|
| 77 | + |
|
| 78 | + $path = $request->getPath(); |
|
| 79 | + $node = $this->server->tree->getNodeForPath($path); |
|
| 80 | + |
|
| 81 | + if (!$node instanceof IFile) return; |
|
| 82 | + |
|
| 83 | + $body = $node->get(); |
|
| 84 | + |
|
| 85 | + // Converting string into stream, if needed. |
|
| 86 | + if (is_string($body)) { |
|
| 87 | + $stream = fopen('php://temp', 'r+'); |
|
| 88 | + fwrite($stream, $body); |
|
| 89 | + rewind($stream); |
|
| 90 | + $body = $stream; |
|
| 91 | + } |
|
| 92 | + |
|
| 93 | + $httpHeaders = $this->server->getHTTPHeaders($path); |
|
| 94 | + |
|
| 95 | + /* ContentType needs to get a default, because many webservers will otherwise |
|
| 96 | 96 | * default to text/html, and we don't want this for security reasons. |
| 97 | 97 | */ |
| 98 | - if (!isset($httpHeaders['Content-Type'])) { |
|
| 99 | - $httpHeaders['Content-Type'] = 'application/octet-stream'; |
|
| 100 | - } |
|
| 98 | + if (!isset($httpHeaders['Content-Type'])) { |
|
| 99 | + $httpHeaders['Content-Type'] = 'application/octet-stream'; |
|
| 100 | + } |
|
| 101 | 101 | |
| 102 | 102 | |
| 103 | - if (isset($httpHeaders['Content-Length'])) { |
|
| 103 | + if (isset($httpHeaders['Content-Length'])) { |
|
| 104 | 104 | |
| 105 | - $nodeSize = $httpHeaders['Content-Length']; |
|
| 105 | + $nodeSize = $httpHeaders['Content-Length']; |
|
| 106 | 106 | |
| 107 | - // Need to unset Content-Length, because we'll handle that during figuring out the range |
|
| 108 | - unset($httpHeaders['Content-Length']); |
|
| 107 | + // Need to unset Content-Length, because we'll handle that during figuring out the range |
|
| 108 | + unset($httpHeaders['Content-Length']); |
|
| 109 | 109 | |
| 110 | - } else { |
|
| 111 | - $nodeSize = null; |
|
| 112 | - } |
|
| 110 | + } else { |
|
| 111 | + $nodeSize = null; |
|
| 112 | + } |
|
| 113 | 113 | |
| 114 | - $response->addHeaders($httpHeaders); |
|
| 114 | + $response->addHeaders($httpHeaders); |
|
| 115 | 115 | |
| 116 | - $range = $this->server->getHTTPRange(); |
|
| 117 | - $ifRange = $request->getHeader('If-Range'); |
|
| 118 | - $ignoreRangeHeader = false; |
|
| 116 | + $range = $this->server->getHTTPRange(); |
|
| 117 | + $ifRange = $request->getHeader('If-Range'); |
|
| 118 | + $ignoreRangeHeader = false; |
|
| 119 | 119 | |
| 120 | - // If ifRange is set, and range is specified, we first need to check |
|
| 121 | - // the precondition. |
|
| 122 | - if ($nodeSize && $range && $ifRange) { |
|
| 120 | + // If ifRange is set, and range is specified, we first need to check |
|
| 121 | + // the precondition. |
|
| 122 | + if ($nodeSize && $range && $ifRange) { |
|
| 123 | 123 | |
| 124 | - // if IfRange is parsable as a date we'll treat it as a DateTime |
|
| 125 | - // otherwise, we must treat it as an etag. |
|
| 126 | - try { |
|
| 127 | - $ifRangeDate = new \DateTime($ifRange); |
|
| 124 | + // if IfRange is parsable as a date we'll treat it as a DateTime |
|
| 125 | + // otherwise, we must treat it as an etag. |
|
| 126 | + try { |
|
| 127 | + $ifRangeDate = new \DateTime($ifRange); |
|
| 128 | 128 | |
| 129 | - // It's a date. We must check if the entity is modified since |
|
| 130 | - // the specified date. |
|
| 131 | - if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true; |
|
| 132 | - else { |
|
| 133 | - $modified = new \DateTime($httpHeaders['Last-Modified']); |
|
| 134 | - if ($modified > $ifRangeDate) $ignoreRangeHeader = true; |
|
| 135 | - } |
|
| 129 | + // It's a date. We must check if the entity is modified since |
|
| 130 | + // the specified date. |
|
| 131 | + if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true; |
|
| 132 | + else { |
|
| 133 | + $modified = new \DateTime($httpHeaders['Last-Modified']); |
|
| 134 | + if ($modified > $ifRangeDate) $ignoreRangeHeader = true; |
|
| 135 | + } |
|
| 136 | 136 | |
| 137 | - } catch (\Exception $e) { |
|
| 138 | - |
|
| 139 | - // It's an entity. We can do a simple comparison. |
|
| 140 | - if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true; |
|
| 141 | - elseif ($httpHeaders['ETag'] !== $ifRange) $ignoreRangeHeader = true; |
|
| 142 | - } |
|
| 143 | - } |
|
| 144 | - |
|
| 145 | - // We're only going to support HTTP ranges if the backend provided a filesize |
|
| 146 | - if (!$ignoreRangeHeader && $nodeSize && $range) { |
|
| 147 | - |
|
| 148 | - // Determining the exact byte offsets |
|
| 149 | - if (!is_null($range[0])) { |
|
| 150 | - |
|
| 151 | - $start = $range[0]; |
|
| 152 | - $end = $range[1] ? $range[1] : $nodeSize - 1; |
|
| 153 | - if ($start >= $nodeSize) |
|
| 154 | - throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')'); |
|
| 155 | - |
|
| 156 | - if ($end < $start) throw new Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')'); |
|
| 157 | - if ($end >= $nodeSize) $end = $nodeSize - 1; |
|
| 158 | - |
|
| 159 | - } else { |
|
| 160 | - |
|
| 161 | - $start = $nodeSize - $range[1]; |
|
| 162 | - $end = $nodeSize - 1; |
|
| 163 | - |
|
| 164 | - if ($start < 0) $start = 0; |
|
| 165 | - |
|
| 166 | - } |
|
| 167 | - |
|
| 168 | - // Streams may advertise themselves as seekable, but still not |
|
| 169 | - // actually allow fseek. We'll manually go forward in the stream |
|
| 170 | - // if fseek failed. |
|
| 171 | - if (!stream_get_meta_data($body)['seekable'] || fseek($body, $start, SEEK_SET) === -1) { |
|
| 172 | - $consumeBlock = 8192; |
|
| 173 | - for ($consumed = 0; $start - $consumed > 0;){ |
|
| 174 | - if (feof($body)) throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $start . ') exceeded the size of the entity (' . $consumed . ')'); |
|
| 175 | - $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock))); |
|
| 176 | - } |
|
| 177 | - } |
|
| 178 | - |
|
| 179 | - $response->setHeader('Content-Length', $end - $start + 1); |
|
| 180 | - $response->setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $nodeSize); |
|
| 181 | - $response->setStatus(206); |
|
| 182 | - $response->setBody($body); |
|
| 183 | - |
|
| 184 | - } else { |
|
| 185 | - |
|
| 186 | - if ($nodeSize) $response->setHeader('Content-Length', $nodeSize); |
|
| 187 | - $response->setStatus(200); |
|
| 188 | - $response->setBody($body); |
|
| 189 | - |
|
| 190 | - } |
|
| 191 | - // Sending back false will interupt the event chain and tell the server |
|
| 192 | - // we've handled this method. |
|
| 193 | - return false; |
|
| 194 | - |
|
| 195 | - } |
|
| 196 | - |
|
| 197 | - /** |
|
| 198 | - * HTTP OPTIONS |
|
| 199 | - * |
|
| 200 | - * @param RequestInterface $request |
|
| 201 | - * @param ResponseInterface $response |
|
| 202 | - * @return bool |
|
| 203 | - */ |
|
| 204 | - public function httpOptions(RequestInterface $request, ResponseInterface $response) { |
|
| 205 | - |
|
| 206 | - $methods = $this->server->getAllowedMethods($request->getPath()); |
|
| 207 | - |
|
| 208 | - $response->setHeader('Allow', strtoupper(implode(', ', $methods))); |
|
| 209 | - $features = ['1', '3', 'extended-mkcol']; |
|
| 210 | - |
|
| 211 | - foreach ($this->server->getPlugins() as $plugin) { |
|
| 212 | - $features = array_merge($features, $plugin->getFeatures()); |
|
| 213 | - } |
|
| 214 | - |
|
| 215 | - $response->setHeader('DAV', implode(', ', $features)); |
|
| 216 | - $response->setHeader('MS-Author-Via', 'DAV'); |
|
| 217 | - $response->setHeader('Accept-Ranges', 'bytes'); |
|
| 218 | - $response->setHeader('Content-Length', '0'); |
|
| 219 | - $response->setStatus(200); |
|
| 220 | - |
|
| 221 | - // Sending back false will interupt the event chain and tell the server |
|
| 222 | - // we've handled this method. |
|
| 223 | - return false; |
|
| 224 | - |
|
| 225 | - } |
|
| 226 | - |
|
| 227 | - /** |
|
| 228 | - * HTTP HEAD |
|
| 229 | - * |
|
| 230 | - * This method is normally used to take a peak at a url, and only get the |
|
| 231 | - * HTTP response headers, without the body. This is used by clients to |
|
| 232 | - * determine if a remote file was changed, so they can use a local cached |
|
| 233 | - * version, instead of downloading it again |
|
| 234 | - * |
|
| 235 | - * @param RequestInterface $request |
|
| 236 | - * @param ResponseInterface $response |
|
| 237 | - * @return bool |
|
| 238 | - */ |
|
| 239 | - public function httpHead(RequestInterface $request, ResponseInterface $response) { |
|
| 240 | - |
|
| 241 | - // This is implemented by changing the HEAD request to a GET request, |
|
| 242 | - // and dropping the response body. |
|
| 243 | - $subRequest = clone $request; |
|
| 244 | - $subRequest->setMethod('GET'); |
|
| 245 | - |
|
| 246 | - try { |
|
| 247 | - $this->server->invokeMethod($subRequest, $response, false); |
|
| 248 | - $response->setBody(''); |
|
| 249 | - } catch (Exception\NotImplemented $e) { |
|
| 250 | - // Some clients may do HEAD requests on collections, however, GET |
|
| 251 | - // requests and HEAD requests _may_ not be defined on a collection, |
|
| 252 | - // which would trigger a 501. |
|
| 253 | - // This breaks some clients though, so we're transforming these |
|
| 254 | - // 501s into 200s. |
|
| 255 | - $response->setStatus(200); |
|
| 256 | - $response->setBody(''); |
|
| 257 | - $response->setHeader('Content-Type', 'text/plain'); |
|
| 258 | - $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode()); |
|
| 259 | - } |
|
| 260 | - |
|
| 261 | - // Sending back false will interupt the event chain and tell the server |
|
| 262 | - // we've handled this method. |
|
| 263 | - return false; |
|
| 264 | - |
|
| 265 | - } |
|
| 266 | - |
|
| 267 | - /** |
|
| 268 | - * HTTP Delete |
|
| 269 | - * |
|
| 270 | - * The HTTP delete method, deletes a given uri |
|
| 271 | - * |
|
| 272 | - * @param RequestInterface $request |
|
| 273 | - * @param ResponseInterface $response |
|
| 274 | - * @return void |
|
| 275 | - */ |
|
| 276 | - public function httpDelete(RequestInterface $request, ResponseInterface $response) { |
|
| 277 | - |
|
| 278 | - $path = $request->getPath(); |
|
| 279 | - |
|
| 280 | - if (!$this->server->emit('beforeUnbind', [$path])) return false; |
|
| 281 | - $this->server->tree->delete($path); |
|
| 282 | - $this->server->emit('afterUnbind', [$path]); |
|
| 283 | - |
|
| 284 | - $response->setStatus(204); |
|
| 285 | - $response->setHeader('Content-Length', '0'); |
|
| 286 | - |
|
| 287 | - // Sending back false will interupt the event chain and tell the server |
|
| 288 | - // we've handled this method. |
|
| 289 | - return false; |
|
| 290 | - |
|
| 291 | - } |
|
| 292 | - |
|
| 293 | - /** |
|
| 294 | - * WebDAV PROPFIND |
|
| 295 | - * |
|
| 296 | - * This WebDAV method requests information about an uri resource, or a list of resources |
|
| 297 | - * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value |
|
| 298 | - * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory) |
|
| 299 | - * |
|
| 300 | - * The request body contains an XML data structure that has a list of properties the client understands |
|
| 301 | - * The response body is also an xml document, containing information about every uri resource and the requested properties |
|
| 302 | - * |
|
| 303 | - * It has to return a HTTP 207 Multi-status status code |
|
| 304 | - * |
|
| 305 | - * @param RequestInterface $request |
|
| 306 | - * @param ResponseInterface $response |
|
| 307 | - * @return void |
|
| 308 | - */ |
|
| 309 | - public function httpPropFind(RequestInterface $request, ResponseInterface $response) { |
|
| 310 | - |
|
| 311 | - $path = $request->getPath(); |
|
| 312 | - |
|
| 313 | - $requestBody = $request->getBodyAsString(); |
|
| 314 | - if (strlen($requestBody)) { |
|
| 315 | - try { |
|
| 316 | - $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody); |
|
| 317 | - } catch (ParseException $e) { |
|
| 318 | - throw new BadRequest($e->getMessage(), null, $e); |
|
| 319 | - } |
|
| 320 | - } else { |
|
| 321 | - $propFindXml = new Xml\Request\PropFind(); |
|
| 322 | - $propFindXml->allProp = true; |
|
| 323 | - $propFindXml->properties = []; |
|
| 324 | - } |
|
| 325 | - |
|
| 326 | - $depth = $this->server->getHTTPDepth(1); |
|
| 327 | - // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled |
|
| 328 | - if (!$this->server->enablePropfindDepthInfinity && $depth != 0) $depth = 1; |
|
| 329 | - |
|
| 330 | - $newProperties = $this->server->getPropertiesForPath($path, $propFindXml->properties, $depth); |
|
| 331 | - |
|
| 332 | - // This is a multi-status response |
|
| 333 | - $response->setStatus(207); |
|
| 334 | - $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 335 | - $response->setHeader('Vary', 'Brief,Prefer'); |
|
| 336 | - |
|
| 337 | - // Normally this header is only needed for OPTIONS responses, however.. |
|
| 338 | - // iCal seems to also depend on these being set for PROPFIND. Since |
|
| 339 | - // this is not harmful, we'll add it. |
|
| 340 | - $features = ['1', '3', 'extended-mkcol']; |
|
| 341 | - foreach ($this->server->getPlugins() as $plugin) { |
|
| 342 | - $features = array_merge($features, $plugin->getFeatures()); |
|
| 343 | - } |
|
| 344 | - $response->setHeader('DAV', implode(', ', $features)); |
|
| 345 | - |
|
| 346 | - $prefer = $this->server->getHTTPPrefer(); |
|
| 347 | - $minimal = $prefer['return'] === 'minimal'; |
|
| 348 | - |
|
| 349 | - $data = $this->server->generateMultiStatus($newProperties, $minimal); |
|
| 350 | - $response->setBody($data); |
|
| 351 | - |
|
| 352 | - // Sending back false will interupt the event chain and tell the server |
|
| 353 | - // we've handled this method. |
|
| 354 | - return false; |
|
| 355 | - |
|
| 356 | - } |
|
| 357 | - |
|
| 358 | - /** |
|
| 359 | - * WebDAV PROPPATCH |
|
| 360 | - * |
|
| 361 | - * This method is called to update properties on a Node. The request is an XML body with all the mutations. |
|
| 362 | - * In this XML body it is specified which properties should be set/updated and/or deleted |
|
| 363 | - * |
|
| 364 | - * @param RequestInterface $request |
|
| 365 | - * @param ResponseInterface $response |
|
| 366 | - * @return bool |
|
| 367 | - */ |
|
| 368 | - public function httpPropPatch(RequestInterface $request, ResponseInterface $response) { |
|
| 369 | - |
|
| 370 | - $path = $request->getPath(); |
|
| 371 | - |
|
| 372 | - try { |
|
| 373 | - $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody()); |
|
| 374 | - } catch (ParseException $e) { |
|
| 375 | - throw new BadRequest($e->getMessage(), null, $e); |
|
| 376 | - } |
|
| 377 | - $newProperties = $propPatch->properties; |
|
| 378 | - |
|
| 379 | - $result = $this->server->updateProperties($path, $newProperties); |
|
| 380 | - |
|
| 381 | - $prefer = $this->server->getHTTPPrefer(); |
|
| 382 | - $response->setHeader('Vary', 'Brief,Prefer'); |
|
| 383 | - |
|
| 384 | - if ($prefer['return'] === 'minimal') { |
|
| 385 | - |
|
| 386 | - // If return-minimal is specified, we only have to check if the |
|
| 387 | - // request was succesful, and don't need to return the |
|
| 388 | - // multi-status. |
|
| 389 | - $ok = true; |
|
| 390 | - foreach ($result as $prop => $code) { |
|
| 391 | - if ((int)$code > 299) { |
|
| 392 | - $ok = false; |
|
| 393 | - } |
|
| 394 | - } |
|
| 395 | - |
|
| 396 | - if ($ok) { |
|
| 397 | - |
|
| 398 | - $response->setStatus(204); |
|
| 399 | - return false; |
|
| 400 | - |
|
| 401 | - } |
|
| 402 | - |
|
| 403 | - } |
|
| 404 | - |
|
| 405 | - $response->setStatus(207); |
|
| 406 | - $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 407 | - |
|
| 408 | - |
|
| 409 | - // Reorganizing the result for generateMultiStatus |
|
| 410 | - $multiStatus = []; |
|
| 411 | - foreach ($result as $propertyName => $code) { |
|
| 412 | - if (isset($multiStatus[$code])) { |
|
| 413 | - $multiStatus[$code][$propertyName] = null; |
|
| 414 | - } else { |
|
| 415 | - $multiStatus[$code] = [$propertyName => null]; |
|
| 416 | - } |
|
| 417 | - } |
|
| 418 | - $multiStatus['href'] = $path; |
|
| 419 | - |
|
| 420 | - $response->setBody( |
|
| 421 | - $this->server->generateMultiStatus([$multiStatus]) |
|
| 422 | - ); |
|
| 423 | - |
|
| 424 | - // Sending back false will interupt the event chain and tell the server |
|
| 425 | - // we've handled this method. |
|
| 426 | - return false; |
|
| 427 | - |
|
| 428 | - } |
|
| 429 | - |
|
| 430 | - /** |
|
| 431 | - * HTTP PUT method |
|
| 432 | - * |
|
| 433 | - * This HTTP method updates a file, or creates a new one. |
|
| 434 | - * |
|
| 435 | - * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content |
|
| 436 | - * |
|
| 437 | - * @param RequestInterface $request |
|
| 438 | - * @param ResponseInterface $response |
|
| 439 | - * @return bool |
|
| 440 | - */ |
|
| 441 | - public function httpPut(RequestInterface $request, ResponseInterface $response) { |
|
| 442 | - |
|
| 443 | - $body = $request->getBodyAsStream(); |
|
| 444 | - $path = $request->getPath(); |
|
| 445 | - |
|
| 446 | - // Intercepting Content-Range |
|
| 447 | - if ($request->getHeader('Content-Range')) { |
|
| 448 | - /* |
|
| 137 | + } catch (\Exception $e) { |
|
| 138 | + |
|
| 139 | + // It's an entity. We can do a simple comparison. |
|
| 140 | + if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true; |
|
| 141 | + elseif ($httpHeaders['ETag'] !== $ifRange) $ignoreRangeHeader = true; |
|
| 142 | + } |
|
| 143 | + } |
|
| 144 | + |
|
| 145 | + // We're only going to support HTTP ranges if the backend provided a filesize |
|
| 146 | + if (!$ignoreRangeHeader && $nodeSize && $range) { |
|
| 147 | + |
|
| 148 | + // Determining the exact byte offsets |
|
| 149 | + if (!is_null($range[0])) { |
|
| 150 | + |
|
| 151 | + $start = $range[0]; |
|
| 152 | + $end = $range[1] ? $range[1] : $nodeSize - 1; |
|
| 153 | + if ($start >= $nodeSize) |
|
| 154 | + throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')'); |
|
| 155 | + |
|
| 156 | + if ($end < $start) throw new Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')'); |
|
| 157 | + if ($end >= $nodeSize) $end = $nodeSize - 1; |
|
| 158 | + |
|
| 159 | + } else { |
|
| 160 | + |
|
| 161 | + $start = $nodeSize - $range[1]; |
|
| 162 | + $end = $nodeSize - 1; |
|
| 163 | + |
|
| 164 | + if ($start < 0) $start = 0; |
|
| 165 | + |
|
| 166 | + } |
|
| 167 | + |
|
| 168 | + // Streams may advertise themselves as seekable, but still not |
|
| 169 | + // actually allow fseek. We'll manually go forward in the stream |
|
| 170 | + // if fseek failed. |
|
| 171 | + if (!stream_get_meta_data($body)['seekable'] || fseek($body, $start, SEEK_SET) === -1) { |
|
| 172 | + $consumeBlock = 8192; |
|
| 173 | + for ($consumed = 0; $start - $consumed > 0;){ |
|
| 174 | + if (feof($body)) throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $start . ') exceeded the size of the entity (' . $consumed . ')'); |
|
| 175 | + $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock))); |
|
| 176 | + } |
|
| 177 | + } |
|
| 178 | + |
|
| 179 | + $response->setHeader('Content-Length', $end - $start + 1); |
|
| 180 | + $response->setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $nodeSize); |
|
| 181 | + $response->setStatus(206); |
|
| 182 | + $response->setBody($body); |
|
| 183 | + |
|
| 184 | + } else { |
|
| 185 | + |
|
| 186 | + if ($nodeSize) $response->setHeader('Content-Length', $nodeSize); |
|
| 187 | + $response->setStatus(200); |
|
| 188 | + $response->setBody($body); |
|
| 189 | + |
|
| 190 | + } |
|
| 191 | + // Sending back false will interupt the event chain and tell the server |
|
| 192 | + // we've handled this method. |
|
| 193 | + return false; |
|
| 194 | + |
|
| 195 | + } |
|
| 196 | + |
|
| 197 | + /** |
|
| 198 | + * HTTP OPTIONS |
|
| 199 | + * |
|
| 200 | + * @param RequestInterface $request |
|
| 201 | + * @param ResponseInterface $response |
|
| 202 | + * @return bool |
|
| 203 | + */ |
|
| 204 | + public function httpOptions(RequestInterface $request, ResponseInterface $response) { |
|
| 205 | + |
|
| 206 | + $methods = $this->server->getAllowedMethods($request->getPath()); |
|
| 207 | + |
|
| 208 | + $response->setHeader('Allow', strtoupper(implode(', ', $methods))); |
|
| 209 | + $features = ['1', '3', 'extended-mkcol']; |
|
| 210 | + |
|
| 211 | + foreach ($this->server->getPlugins() as $plugin) { |
|
| 212 | + $features = array_merge($features, $plugin->getFeatures()); |
|
| 213 | + } |
|
| 214 | + |
|
| 215 | + $response->setHeader('DAV', implode(', ', $features)); |
|
| 216 | + $response->setHeader('MS-Author-Via', 'DAV'); |
|
| 217 | + $response->setHeader('Accept-Ranges', 'bytes'); |
|
| 218 | + $response->setHeader('Content-Length', '0'); |
|
| 219 | + $response->setStatus(200); |
|
| 220 | + |
|
| 221 | + // Sending back false will interupt the event chain and tell the server |
|
| 222 | + // we've handled this method. |
|
| 223 | + return false; |
|
| 224 | + |
|
| 225 | + } |
|
| 226 | + |
|
| 227 | + /** |
|
| 228 | + * HTTP HEAD |
|
| 229 | + * |
|
| 230 | + * This method is normally used to take a peak at a url, and only get the |
|
| 231 | + * HTTP response headers, without the body. This is used by clients to |
|
| 232 | + * determine if a remote file was changed, so they can use a local cached |
|
| 233 | + * version, instead of downloading it again |
|
| 234 | + * |
|
| 235 | + * @param RequestInterface $request |
|
| 236 | + * @param ResponseInterface $response |
|
| 237 | + * @return bool |
|
| 238 | + */ |
|
| 239 | + public function httpHead(RequestInterface $request, ResponseInterface $response) { |
|
| 240 | + |
|
| 241 | + // This is implemented by changing the HEAD request to a GET request, |
|
| 242 | + // and dropping the response body. |
|
| 243 | + $subRequest = clone $request; |
|
| 244 | + $subRequest->setMethod('GET'); |
|
| 245 | + |
|
| 246 | + try { |
|
| 247 | + $this->server->invokeMethod($subRequest, $response, false); |
|
| 248 | + $response->setBody(''); |
|
| 249 | + } catch (Exception\NotImplemented $e) { |
|
| 250 | + // Some clients may do HEAD requests on collections, however, GET |
|
| 251 | + // requests and HEAD requests _may_ not be defined on a collection, |
|
| 252 | + // which would trigger a 501. |
|
| 253 | + // This breaks some clients though, so we're transforming these |
|
| 254 | + // 501s into 200s. |
|
| 255 | + $response->setStatus(200); |
|
| 256 | + $response->setBody(''); |
|
| 257 | + $response->setHeader('Content-Type', 'text/plain'); |
|
| 258 | + $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode()); |
|
| 259 | + } |
|
| 260 | + |
|
| 261 | + // Sending back false will interupt the event chain and tell the server |
|
| 262 | + // we've handled this method. |
|
| 263 | + return false; |
|
| 264 | + |
|
| 265 | + } |
|
| 266 | + |
|
| 267 | + /** |
|
| 268 | + * HTTP Delete |
|
| 269 | + * |
|
| 270 | + * The HTTP delete method, deletes a given uri |
|
| 271 | + * |
|
| 272 | + * @param RequestInterface $request |
|
| 273 | + * @param ResponseInterface $response |
|
| 274 | + * @return void |
|
| 275 | + */ |
|
| 276 | + public function httpDelete(RequestInterface $request, ResponseInterface $response) { |
|
| 277 | + |
|
| 278 | + $path = $request->getPath(); |
|
| 279 | + |
|
| 280 | + if (!$this->server->emit('beforeUnbind', [$path])) return false; |
|
| 281 | + $this->server->tree->delete($path); |
|
| 282 | + $this->server->emit('afterUnbind', [$path]); |
|
| 283 | + |
|
| 284 | + $response->setStatus(204); |
|
| 285 | + $response->setHeader('Content-Length', '0'); |
|
| 286 | + |
|
| 287 | + // Sending back false will interupt the event chain and tell the server |
|
| 288 | + // we've handled this method. |
|
| 289 | + return false; |
|
| 290 | + |
|
| 291 | + } |
|
| 292 | + |
|
| 293 | + /** |
|
| 294 | + * WebDAV PROPFIND |
|
| 295 | + * |
|
| 296 | + * This WebDAV method requests information about an uri resource, or a list of resources |
|
| 297 | + * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value |
|
| 298 | + * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory) |
|
| 299 | + * |
|
| 300 | + * The request body contains an XML data structure that has a list of properties the client understands |
|
| 301 | + * The response body is also an xml document, containing information about every uri resource and the requested properties |
|
| 302 | + * |
|
| 303 | + * It has to return a HTTP 207 Multi-status status code |
|
| 304 | + * |
|
| 305 | + * @param RequestInterface $request |
|
| 306 | + * @param ResponseInterface $response |
|
| 307 | + * @return void |
|
| 308 | + */ |
|
| 309 | + public function httpPropFind(RequestInterface $request, ResponseInterface $response) { |
|
| 310 | + |
|
| 311 | + $path = $request->getPath(); |
|
| 312 | + |
|
| 313 | + $requestBody = $request->getBodyAsString(); |
|
| 314 | + if (strlen($requestBody)) { |
|
| 315 | + try { |
|
| 316 | + $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody); |
|
| 317 | + } catch (ParseException $e) { |
|
| 318 | + throw new BadRequest($e->getMessage(), null, $e); |
|
| 319 | + } |
|
| 320 | + } else { |
|
| 321 | + $propFindXml = new Xml\Request\PropFind(); |
|
| 322 | + $propFindXml->allProp = true; |
|
| 323 | + $propFindXml->properties = []; |
|
| 324 | + } |
|
| 325 | + |
|
| 326 | + $depth = $this->server->getHTTPDepth(1); |
|
| 327 | + // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled |
|
| 328 | + if (!$this->server->enablePropfindDepthInfinity && $depth != 0) $depth = 1; |
|
| 329 | + |
|
| 330 | + $newProperties = $this->server->getPropertiesForPath($path, $propFindXml->properties, $depth); |
|
| 331 | + |
|
| 332 | + // This is a multi-status response |
|
| 333 | + $response->setStatus(207); |
|
| 334 | + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 335 | + $response->setHeader('Vary', 'Brief,Prefer'); |
|
| 336 | + |
|
| 337 | + // Normally this header is only needed for OPTIONS responses, however.. |
|
| 338 | + // iCal seems to also depend on these being set for PROPFIND. Since |
|
| 339 | + // this is not harmful, we'll add it. |
|
| 340 | + $features = ['1', '3', 'extended-mkcol']; |
|
| 341 | + foreach ($this->server->getPlugins() as $plugin) { |
|
| 342 | + $features = array_merge($features, $plugin->getFeatures()); |
|
| 343 | + } |
|
| 344 | + $response->setHeader('DAV', implode(', ', $features)); |
|
| 345 | + |
|
| 346 | + $prefer = $this->server->getHTTPPrefer(); |
|
| 347 | + $minimal = $prefer['return'] === 'minimal'; |
|
| 348 | + |
|
| 349 | + $data = $this->server->generateMultiStatus($newProperties, $minimal); |
|
| 350 | + $response->setBody($data); |
|
| 351 | + |
|
| 352 | + // Sending back false will interupt the event chain and tell the server |
|
| 353 | + // we've handled this method. |
|
| 354 | + return false; |
|
| 355 | + |
|
| 356 | + } |
|
| 357 | + |
|
| 358 | + /** |
|
| 359 | + * WebDAV PROPPATCH |
|
| 360 | + * |
|
| 361 | + * This method is called to update properties on a Node. The request is an XML body with all the mutations. |
|
| 362 | + * In this XML body it is specified which properties should be set/updated and/or deleted |
|
| 363 | + * |
|
| 364 | + * @param RequestInterface $request |
|
| 365 | + * @param ResponseInterface $response |
|
| 366 | + * @return bool |
|
| 367 | + */ |
|
| 368 | + public function httpPropPatch(RequestInterface $request, ResponseInterface $response) { |
|
| 369 | + |
|
| 370 | + $path = $request->getPath(); |
|
| 371 | + |
|
| 372 | + try { |
|
| 373 | + $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody()); |
|
| 374 | + } catch (ParseException $e) { |
|
| 375 | + throw new BadRequest($e->getMessage(), null, $e); |
|
| 376 | + } |
|
| 377 | + $newProperties = $propPatch->properties; |
|
| 378 | + |
|
| 379 | + $result = $this->server->updateProperties($path, $newProperties); |
|
| 380 | + |
|
| 381 | + $prefer = $this->server->getHTTPPrefer(); |
|
| 382 | + $response->setHeader('Vary', 'Brief,Prefer'); |
|
| 383 | + |
|
| 384 | + if ($prefer['return'] === 'minimal') { |
|
| 385 | + |
|
| 386 | + // If return-minimal is specified, we only have to check if the |
|
| 387 | + // request was succesful, and don't need to return the |
|
| 388 | + // multi-status. |
|
| 389 | + $ok = true; |
|
| 390 | + foreach ($result as $prop => $code) { |
|
| 391 | + if ((int)$code > 299) { |
|
| 392 | + $ok = false; |
|
| 393 | + } |
|
| 394 | + } |
|
| 395 | + |
|
| 396 | + if ($ok) { |
|
| 397 | + |
|
| 398 | + $response->setStatus(204); |
|
| 399 | + return false; |
|
| 400 | + |
|
| 401 | + } |
|
| 402 | + |
|
| 403 | + } |
|
| 404 | + |
|
| 405 | + $response->setStatus(207); |
|
| 406 | + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 407 | + |
|
| 408 | + |
|
| 409 | + // Reorganizing the result for generateMultiStatus |
|
| 410 | + $multiStatus = []; |
|
| 411 | + foreach ($result as $propertyName => $code) { |
|
| 412 | + if (isset($multiStatus[$code])) { |
|
| 413 | + $multiStatus[$code][$propertyName] = null; |
|
| 414 | + } else { |
|
| 415 | + $multiStatus[$code] = [$propertyName => null]; |
|
| 416 | + } |
|
| 417 | + } |
|
| 418 | + $multiStatus['href'] = $path; |
|
| 419 | + |
|
| 420 | + $response->setBody( |
|
| 421 | + $this->server->generateMultiStatus([$multiStatus]) |
|
| 422 | + ); |
|
| 423 | + |
|
| 424 | + // Sending back false will interupt the event chain and tell the server |
|
| 425 | + // we've handled this method. |
|
| 426 | + return false; |
|
| 427 | + |
|
| 428 | + } |
|
| 429 | + |
|
| 430 | + /** |
|
| 431 | + * HTTP PUT method |
|
| 432 | + * |
|
| 433 | + * This HTTP method updates a file, or creates a new one. |
|
| 434 | + * |
|
| 435 | + * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content |
|
| 436 | + * |
|
| 437 | + * @param RequestInterface $request |
|
| 438 | + * @param ResponseInterface $response |
|
| 439 | + * @return bool |
|
| 440 | + */ |
|
| 441 | + public function httpPut(RequestInterface $request, ResponseInterface $response) { |
|
| 442 | + |
|
| 443 | + $body = $request->getBodyAsStream(); |
|
| 444 | + $path = $request->getPath(); |
|
| 445 | + |
|
| 446 | + // Intercepting Content-Range |
|
| 447 | + if ($request->getHeader('Content-Range')) { |
|
| 448 | + /* |
|
| 449 | 449 | An origin server that allows PUT on a given target resource MUST send |
| 450 | 450 | a 400 (Bad Request) response to a PUT request that contains a |
| 451 | 451 | Content-Range header field. |
| 452 | 452 | |
| 453 | 453 | Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4 |
| 454 | 454 | */ |
| 455 | - throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.'); |
|
| 456 | - } |
|
| 455 | + throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.'); |
|
| 456 | + } |
|
| 457 | 457 | |
| 458 | - // Intercepting the Finder problem |
|
| 459 | - if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) { |
|
| 458 | + // Intercepting the Finder problem |
|
| 459 | + if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) { |
|
| 460 | 460 | |
| 461 | - /* |
|
| 461 | + /* |
|
| 462 | 462 | Many webservers will not cooperate well with Finder PUT requests, |
| 463 | 463 | because it uses 'Chunked' transfer encoding for the request body. |
| 464 | 464 | |
@@ -479,445 +479,445 @@ discard block |
||
| 479 | 479 | protect the end-user. |
| 480 | 480 | */ |
| 481 | 481 | |
| 482 | - // Only reading first byte |
|
| 483 | - $firstByte = fread($body, 1); |
|
| 484 | - if (strlen($firstByte) !== 1) { |
|
| 485 | - throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.'); |
|
| 486 | - } |
|
| 482 | + // Only reading first byte |
|
| 483 | + $firstByte = fread($body, 1); |
|
| 484 | + if (strlen($firstByte) !== 1) { |
|
| 485 | + throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.'); |
|
| 486 | + } |
|
| 487 | 487 | |
| 488 | - // The body needs to stay intact, so we copy everything to a |
|
| 489 | - // temporary stream. |
|
| 488 | + // The body needs to stay intact, so we copy everything to a |
|
| 489 | + // temporary stream. |
|
| 490 | 490 | |
| 491 | - $newBody = fopen('php://temp', 'r+'); |
|
| 492 | - fwrite($newBody, $firstByte); |
|
| 493 | - stream_copy_to_stream($body, $newBody); |
|
| 494 | - rewind($newBody); |
|
| 491 | + $newBody = fopen('php://temp', 'r+'); |
|
| 492 | + fwrite($newBody, $firstByte); |
|
| 493 | + stream_copy_to_stream($body, $newBody); |
|
| 494 | + rewind($newBody); |
|
| 495 | 495 | |
| 496 | - $body = $newBody; |
|
| 496 | + $body = $newBody; |
|
| 497 | 497 | |
| 498 | - } |
|
| 498 | + } |
|
| 499 | 499 | |
| 500 | - if ($this->server->tree->nodeExists($path)) { |
|
| 500 | + if ($this->server->tree->nodeExists($path)) { |
|
| 501 | 501 | |
| 502 | - $node = $this->server->tree->getNodeForPath($path); |
|
| 502 | + $node = $this->server->tree->getNodeForPath($path); |
|
| 503 | 503 | |
| 504 | - // If the node is a collection, we'll deny it |
|
| 505 | - if (!($node instanceof IFile)) throw new Exception\Conflict('PUT is not allowed on non-files.'); |
|
| 504 | + // If the node is a collection, we'll deny it |
|
| 505 | + if (!($node instanceof IFile)) throw new Exception\Conflict('PUT is not allowed on non-files.'); |
|
| 506 | 506 | |
| 507 | - if (!$this->server->updateFile($path, $body, $etag)) { |
|
| 508 | - return false; |
|
| 509 | - } |
|
| 507 | + if (!$this->server->updateFile($path, $body, $etag)) { |
|
| 508 | + return false; |
|
| 509 | + } |
|
| 510 | 510 | |
| 511 | - $response->setHeader('Content-Length', '0'); |
|
| 512 | - if ($etag) $response->setHeader('ETag', $etag); |
|
| 513 | - $response->setStatus(204); |
|
| 511 | + $response->setHeader('Content-Length', '0'); |
|
| 512 | + if ($etag) $response->setHeader('ETag', $etag); |
|
| 513 | + $response->setStatus(204); |
|
| 514 | 514 | |
| 515 | - } else { |
|
| 515 | + } else { |
|
| 516 | 516 | |
| 517 | - $etag = null; |
|
| 518 | - // If we got here, the resource didn't exist yet. |
|
| 519 | - if (!$this->server->createFile($path, $body, $etag)) { |
|
| 520 | - // For one reason or another the file was not created. |
|
| 521 | - return false; |
|
| 522 | - } |
|
| 517 | + $etag = null; |
|
| 518 | + // If we got here, the resource didn't exist yet. |
|
| 519 | + if (!$this->server->createFile($path, $body, $etag)) { |
|
| 520 | + // For one reason or another the file was not created. |
|
| 521 | + return false; |
|
| 522 | + } |
|
| 523 | 523 | |
| 524 | - $response->setHeader('Content-Length', '0'); |
|
| 525 | - if ($etag) $response->setHeader('ETag', $etag); |
|
| 526 | - $response->setStatus(201); |
|
| 524 | + $response->setHeader('Content-Length', '0'); |
|
| 525 | + if ($etag) $response->setHeader('ETag', $etag); |
|
| 526 | + $response->setStatus(201); |
|
| 527 | 527 | |
| 528 | - } |
|
| 528 | + } |
|
| 529 | 529 | |
| 530 | - // Sending back false will interupt the event chain and tell the server |
|
| 531 | - // we've handled this method. |
|
| 532 | - return false; |
|
| 530 | + // Sending back false will interupt the event chain and tell the server |
|
| 531 | + // we've handled this method. |
|
| 532 | + return false; |
|
| 533 | 533 | |
| 534 | - } |
|
| 534 | + } |
|
| 535 | 535 | |
| 536 | 536 | |
| 537 | - /** |
|
| 538 | - * WebDAV MKCOL |
|
| 539 | - * |
|
| 540 | - * The MKCOL method is used to create a new collection (directory) on the server |
|
| 541 | - * |
|
| 542 | - * @param RequestInterface $request |
|
| 543 | - * @param ResponseInterface $response |
|
| 544 | - * @return bool |
|
| 545 | - */ |
|
| 546 | - public function httpMkcol(RequestInterface $request, ResponseInterface $response) { |
|
| 537 | + /** |
|
| 538 | + * WebDAV MKCOL |
|
| 539 | + * |
|
| 540 | + * The MKCOL method is used to create a new collection (directory) on the server |
|
| 541 | + * |
|
| 542 | + * @param RequestInterface $request |
|
| 543 | + * @param ResponseInterface $response |
|
| 544 | + * @return bool |
|
| 545 | + */ |
|
| 546 | + public function httpMkcol(RequestInterface $request, ResponseInterface $response) { |
|
| 547 | 547 | |
| 548 | - $requestBody = $request->getBodyAsString(); |
|
| 549 | - $path = $request->getPath(); |
|
| 548 | + $requestBody = $request->getBodyAsString(); |
|
| 549 | + $path = $request->getPath(); |
|
| 550 | 550 | |
| 551 | - if ($requestBody) { |
|
| 551 | + if ($requestBody) { |
|
| 552 | 552 | |
| 553 | - $contentType = $request->getHeader('Content-Type'); |
|
| 554 | - if (strpos($contentType, 'application/xml') !== 0 && strpos($contentType, 'text/xml') !== 0) { |
|
| 553 | + $contentType = $request->getHeader('Content-Type'); |
|
| 554 | + if (strpos($contentType, 'application/xml') !== 0 && strpos($contentType, 'text/xml') !== 0) { |
|
| 555 | 555 | |
| 556 | - // We must throw 415 for unsupported mkcol bodies |
|
| 557 | - throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type'); |
|
| 556 | + // We must throw 415 for unsupported mkcol bodies |
|
| 557 | + throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type'); |
|
| 558 | 558 | |
| 559 | - } |
|
| 559 | + } |
|
| 560 | 560 | |
| 561 | - try { |
|
| 562 | - $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody); |
|
| 563 | - } catch (\Sabre\Xml\ParseException $e) { |
|
| 564 | - throw new Exception\BadRequest($e->getMessage(), null, $e); |
|
| 565 | - } |
|
| 561 | + try { |
|
| 562 | + $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody); |
|
| 563 | + } catch (\Sabre\Xml\ParseException $e) { |
|
| 564 | + throw new Exception\BadRequest($e->getMessage(), null, $e); |
|
| 565 | + } |
|
| 566 | 566 | |
| 567 | - $properties = $mkcol->getProperties(); |
|
| 567 | + $properties = $mkcol->getProperties(); |
|
| 568 | 568 | |
| 569 | - if (!isset($properties['{DAV:}resourcetype'])) |
|
| 570 | - throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property'); |
|
| 569 | + if (!isset($properties['{DAV:}resourcetype'])) |
|
| 570 | + throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property'); |
|
| 571 | 571 | |
| 572 | - $resourceType = $properties['{DAV:}resourcetype']->getValue(); |
|
| 573 | - unset($properties['{DAV:}resourcetype']); |
|
| 572 | + $resourceType = $properties['{DAV:}resourcetype']->getValue(); |
|
| 573 | + unset($properties['{DAV:}resourcetype']); |
|
| 574 | 574 | |
| 575 | - } else { |
|
| 575 | + } else { |
|
| 576 | 576 | |
| 577 | - $properties = []; |
|
| 578 | - $resourceType = ['{DAV:}collection']; |
|
| 577 | + $properties = []; |
|
| 578 | + $resourceType = ['{DAV:}collection']; |
|
| 579 | 579 | |
| 580 | - } |
|
| 580 | + } |
|
| 581 | 581 | |
| 582 | - $mkcol = new MkCol($resourceType, $properties); |
|
| 582 | + $mkcol = new MkCol($resourceType, $properties); |
|
| 583 | 583 | |
| 584 | - $result = $this->server->createCollection($path, $mkcol); |
|
| 584 | + $result = $this->server->createCollection($path, $mkcol); |
|
| 585 | 585 | |
| 586 | - if (is_array($result)) { |
|
| 587 | - $response->setStatus(207); |
|
| 588 | - $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 586 | + if (is_array($result)) { |
|
| 587 | + $response->setStatus(207); |
|
| 588 | + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 589 | 589 | |
| 590 | - $response->setBody( |
|
| 591 | - $this->server->generateMultiStatus([$result]) |
|
| 592 | - ); |
|
| 590 | + $response->setBody( |
|
| 591 | + $this->server->generateMultiStatus([$result]) |
|
| 592 | + ); |
|
| 593 | 593 | |
| 594 | - } else { |
|
| 595 | - $response->setHeader('Content-Length', '0'); |
|
| 596 | - $response->setStatus(201); |
|
| 597 | - } |
|
| 594 | + } else { |
|
| 595 | + $response->setHeader('Content-Length', '0'); |
|
| 596 | + $response->setStatus(201); |
|
| 597 | + } |
|
| 598 | 598 | |
| 599 | - // Sending back false will interupt the event chain and tell the server |
|
| 600 | - // we've handled this method. |
|
| 601 | - return false; |
|
| 599 | + // Sending back false will interupt the event chain and tell the server |
|
| 600 | + // we've handled this method. |
|
| 601 | + return false; |
|
| 602 | 602 | |
| 603 | - } |
|
| 603 | + } |
|
| 604 | 604 | |
| 605 | - /** |
|
| 606 | - * WebDAV HTTP MOVE method |
|
| 607 | - * |
|
| 608 | - * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo |
|
| 609 | - * |
|
| 610 | - * @param RequestInterface $request |
|
| 611 | - * @param ResponseInterface $response |
|
| 612 | - * @return bool |
|
| 613 | - */ |
|
| 614 | - public function httpMove(RequestInterface $request, ResponseInterface $response) { |
|
| 605 | + /** |
|
| 606 | + * WebDAV HTTP MOVE method |
|
| 607 | + * |
|
| 608 | + * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo |
|
| 609 | + * |
|
| 610 | + * @param RequestInterface $request |
|
| 611 | + * @param ResponseInterface $response |
|
| 612 | + * @return bool |
|
| 613 | + */ |
|
| 614 | + public function httpMove(RequestInterface $request, ResponseInterface $response) { |
|
| 615 | 615 | |
| 616 | - $path = $request->getPath(); |
|
| 616 | + $path = $request->getPath(); |
|
| 617 | 617 | |
| 618 | - $moveInfo = $this->server->getCopyAndMoveInfo($request); |
|
| 618 | + $moveInfo = $this->server->getCopyAndMoveInfo($request); |
|
| 619 | 619 | |
| 620 | - if ($moveInfo['destinationExists']) { |
|
| 620 | + if ($moveInfo['destinationExists']) { |
|
| 621 | 621 | |
| 622 | - if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) return false; |
|
| 622 | + if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) return false; |
|
| 623 | 623 | |
| 624 | - } |
|
| 625 | - if (!$this->server->emit('beforeUnbind', [$path])) return false; |
|
| 626 | - if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) return false; |
|
| 627 | - if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) return false; |
|
| 624 | + } |
|
| 625 | + if (!$this->server->emit('beforeUnbind', [$path])) return false; |
|
| 626 | + if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) return false; |
|
| 627 | + if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) return false; |
|
| 628 | 628 | |
| 629 | - if ($moveInfo['destinationExists']) { |
|
| 629 | + if ($moveInfo['destinationExists']) { |
|
| 630 | 630 | |
| 631 | - $this->server->tree->delete($moveInfo['destination']); |
|
| 632 | - $this->server->emit('afterUnbind', [$moveInfo['destination']]); |
|
| 631 | + $this->server->tree->delete($moveInfo['destination']); |
|
| 632 | + $this->server->emit('afterUnbind', [$moveInfo['destination']]); |
|
| 633 | 633 | |
| 634 | - } |
|
| 634 | + } |
|
| 635 | 635 | |
| 636 | - $this->server->tree->move($path, $moveInfo['destination']); |
|
| 636 | + $this->server->tree->move($path, $moveInfo['destination']); |
|
| 637 | 637 | |
| 638 | - // Its important afterMove is called before afterUnbind, because it |
|
| 639 | - // allows systems to transfer data from one path to another. |
|
| 640 | - // PropertyStorage uses this. If afterUnbind was first, it would clean |
|
| 641 | - // up all the properties before it has a chance. |
|
| 642 | - $this->server->emit('afterMove', [$path, $moveInfo['destination']]); |
|
| 643 | - $this->server->emit('afterUnbind', [$path]); |
|
| 644 | - $this->server->emit('afterBind', [$moveInfo['destination']]); |
|
| 638 | + // Its important afterMove is called before afterUnbind, because it |
|
| 639 | + // allows systems to transfer data from one path to another. |
|
| 640 | + // PropertyStorage uses this. If afterUnbind was first, it would clean |
|
| 641 | + // up all the properties before it has a chance. |
|
| 642 | + $this->server->emit('afterMove', [$path, $moveInfo['destination']]); |
|
| 643 | + $this->server->emit('afterUnbind', [$path]); |
|
| 644 | + $this->server->emit('afterBind', [$moveInfo['destination']]); |
|
| 645 | 645 | |
| 646 | - // If a resource was overwritten we should send a 204, otherwise a 201 |
|
| 647 | - $response->setHeader('Content-Length', '0'); |
|
| 648 | - $response->setStatus($moveInfo['destinationExists'] ? 204 : 201); |
|
| 646 | + // If a resource was overwritten we should send a 204, otherwise a 201 |
|
| 647 | + $response->setHeader('Content-Length', '0'); |
|
| 648 | + $response->setStatus($moveInfo['destinationExists'] ? 204 : 201); |
|
| 649 | 649 | |
| 650 | - // Sending back false will interupt the event chain and tell the server |
|
| 651 | - // we've handled this method. |
|
| 652 | - return false; |
|
| 650 | + // Sending back false will interupt the event chain and tell the server |
|
| 651 | + // we've handled this method. |
|
| 652 | + return false; |
|
| 653 | 653 | |
| 654 | - } |
|
| 654 | + } |
|
| 655 | 655 | |
| 656 | - /** |
|
| 657 | - * WebDAV HTTP COPY method |
|
| 658 | - * |
|
| 659 | - * This method copies one uri to a different uri, and works much like the MOVE request |
|
| 660 | - * A lot of the actual request processing is done in getCopyMoveInfo |
|
| 661 | - * |
|
| 662 | - * @param RequestInterface $request |
|
| 663 | - * @param ResponseInterface $response |
|
| 664 | - * @return bool |
|
| 665 | - */ |
|
| 666 | - public function httpCopy(RequestInterface $request, ResponseInterface $response) { |
|
| 656 | + /** |
|
| 657 | + * WebDAV HTTP COPY method |
|
| 658 | + * |
|
| 659 | + * This method copies one uri to a different uri, and works much like the MOVE request |
|
| 660 | + * A lot of the actual request processing is done in getCopyMoveInfo |
|
| 661 | + * |
|
| 662 | + * @param RequestInterface $request |
|
| 663 | + * @param ResponseInterface $response |
|
| 664 | + * @return bool |
|
| 665 | + */ |
|
| 666 | + public function httpCopy(RequestInterface $request, ResponseInterface $response) { |
|
| 667 | 667 | |
| 668 | - $path = $request->getPath(); |
|
| 668 | + $path = $request->getPath(); |
|
| 669 | 669 | |
| 670 | - $copyInfo = $this->server->getCopyAndMoveInfo($request); |
|
| 670 | + $copyInfo = $this->server->getCopyAndMoveInfo($request); |
|
| 671 | 671 | |
| 672 | - if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) return false; |
|
| 673 | - if ($copyInfo['destinationExists']) { |
|
| 674 | - if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) return false; |
|
| 675 | - $this->server->tree->delete($copyInfo['destination']); |
|
| 676 | - } |
|
| 677 | - |
|
| 678 | - $this->server->tree->copy($path, $copyInfo['destination']); |
|
| 679 | - $this->server->emit('afterBind', [$copyInfo['destination']]); |
|
| 680 | - |
|
| 681 | - // If a resource was overwritten we should send a 204, otherwise a 201 |
|
| 682 | - $response->setHeader('Content-Length', '0'); |
|
| 683 | - $response->setStatus($copyInfo['destinationExists'] ? 204 : 201); |
|
| 684 | - |
|
| 685 | - // Sending back false will interupt the event chain and tell the server |
|
| 686 | - // we've handled this method. |
|
| 687 | - return false; |
|
| 688 | - |
|
| 689 | - |
|
| 690 | - } |
|
| 691 | - |
|
| 692 | - /** |
|
| 693 | - * HTTP REPORT method implementation |
|
| 694 | - * |
|
| 695 | - * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) |
|
| 696 | - * It's used in a lot of extensions, so it made sense to implement it into the core. |
|
| 697 | - * |
|
| 698 | - * @param RequestInterface $request |
|
| 699 | - * @param ResponseInterface $response |
|
| 700 | - * @return bool |
|
| 701 | - */ |
|
| 702 | - public function httpReport(RequestInterface $request, ResponseInterface $response) { |
|
| 703 | - |
|
| 704 | - $path = $request->getPath(); |
|
| 705 | - |
|
| 706 | - $result = $this->server->xml->parse( |
|
| 707 | - $request->getBody(), |
|
| 708 | - $request->getUrl(), |
|
| 709 | - $rootElementName |
|
| 710 | - ); |
|
| 711 | - |
|
| 712 | - if ($this->server->emit('report', [$rootElementName, $result, $path])) { |
|
| 713 | - |
|
| 714 | - // If emit returned true, it means the report was not supported |
|
| 715 | - throw new Exception\ReportNotSupported(); |
|
| 716 | - |
|
| 717 | - } |
|
| 718 | - |
|
| 719 | - // Sending back false will interupt the event chain and tell the server |
|
| 720 | - // we've handled this method. |
|
| 721 | - return false; |
|
| 722 | - |
|
| 723 | - } |
|
| 724 | - |
|
| 725 | - /** |
|
| 726 | - * This method is called during property updates. |
|
| 727 | - * |
|
| 728 | - * Here we check if a user attempted to update a protected property and |
|
| 729 | - * ensure that the process fails if this is the case. |
|
| 730 | - * |
|
| 731 | - * @param string $path |
|
| 732 | - * @param PropPatch $propPatch |
|
| 733 | - * @return void |
|
| 734 | - */ |
|
| 735 | - public function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) { |
|
| 736 | - |
|
| 737 | - // Comparing the mutation list to the list of propetected properties. |
|
| 738 | - $mutations = $propPatch->getMutations(); |
|
| 739 | - |
|
| 740 | - $protected = array_intersect( |
|
| 741 | - $this->server->protectedProperties, |
|
| 742 | - array_keys($mutations) |
|
| 743 | - ); |
|
| 744 | - |
|
| 745 | - if ($protected) { |
|
| 746 | - $propPatch->setResultCode($protected, 403); |
|
| 747 | - } |
|
| 748 | - |
|
| 749 | - } |
|
| 750 | - |
|
| 751 | - /** |
|
| 752 | - * This method is called during property updates. |
|
| 753 | - * |
|
| 754 | - * Here we check if a node implements IProperties and let the node handle |
|
| 755 | - * updating of (some) properties. |
|
| 756 | - * |
|
| 757 | - * @param string $path |
|
| 758 | - * @param PropPatch $propPatch |
|
| 759 | - * @return void |
|
| 760 | - */ |
|
| 761 | - public function propPatchNodeUpdate($path, PropPatch $propPatch) { |
|
| 762 | - |
|
| 763 | - // This should trigger a 404 if the node doesn't exist. |
|
| 764 | - $node = $this->server->tree->getNodeForPath($path); |
|
| 765 | - |
|
| 766 | - if ($node instanceof IProperties) { |
|
| 767 | - $node->propPatch($propPatch); |
|
| 768 | - } |
|
| 769 | - |
|
| 770 | - } |
|
| 771 | - |
|
| 772 | - /** |
|
| 773 | - * This method is called when properties are retrieved. |
|
| 774 | - * |
|
| 775 | - * Here we add all the default properties. |
|
| 776 | - * |
|
| 777 | - * @param PropFind $propFind |
|
| 778 | - * @param INode $node |
|
| 779 | - * @return void |
|
| 780 | - */ |
|
| 781 | - public function propFind(PropFind $propFind, INode $node) { |
|
| 782 | - |
|
| 783 | - $propFind->handle('{DAV:}getlastmodified', function() use ($node) { |
|
| 784 | - $lm = $node->getLastModified(); |
|
| 785 | - if ($lm) { |
|
| 786 | - return new Xml\Property\GetLastModified($lm); |
|
| 787 | - } |
|
| 788 | - }); |
|
| 789 | - |
|
| 790 | - if ($node instanceof IFile) { |
|
| 791 | - $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']); |
|
| 792 | - $propFind->handle('{DAV:}getetag', [$node, 'getETag']); |
|
| 793 | - $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']); |
|
| 794 | - } |
|
| 795 | - |
|
| 796 | - if ($node instanceof IQuota) { |
|
| 797 | - $quotaInfo = null; |
|
| 798 | - $propFind->handle('{DAV:}quota-used-bytes', function() use (&$quotaInfo, $node) { |
|
| 799 | - $quotaInfo = $node->getQuotaInfo(); |
|
| 800 | - return $quotaInfo[0]; |
|
| 801 | - }); |
|
| 802 | - $propFind->handle('{DAV:}quota-available-bytes', function() use (&$quotaInfo, $node) { |
|
| 803 | - if (!$quotaInfo) { |
|
| 804 | - $quotaInfo = $node->getQuotaInfo(); |
|
| 805 | - } |
|
| 806 | - return $quotaInfo[1]; |
|
| 807 | - }); |
|
| 808 | - } |
|
| 809 | - |
|
| 810 | - $propFind->handle('{DAV:}supported-report-set', function() use ($propFind) { |
|
| 811 | - $reports = []; |
|
| 812 | - foreach ($this->server->getPlugins() as $plugin) { |
|
| 813 | - $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath())); |
|
| 814 | - } |
|
| 815 | - return new Xml\Property\SupportedReportSet($reports); |
|
| 816 | - }); |
|
| 817 | - $propFind->handle('{DAV:}resourcetype', function() use ($node) { |
|
| 818 | - return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node)); |
|
| 819 | - }); |
|
| 820 | - $propFind->handle('{DAV:}supported-method-set', function() use ($propFind) { |
|
| 821 | - return new Xml\Property\SupportedMethodSet( |
|
| 822 | - $this->server->getAllowedMethods($propFind->getPath()) |
|
| 823 | - ); |
|
| 824 | - }); |
|
| 825 | - |
|
| 826 | - } |
|
| 827 | - |
|
| 828 | - /** |
|
| 829 | - * Fetches properties for a node. |
|
| 830 | - * |
|
| 831 | - * This event is called a bit later, so plugins have a chance first to |
|
| 832 | - * populate the result. |
|
| 833 | - * |
|
| 834 | - * @param PropFind $propFind |
|
| 835 | - * @param INode $node |
|
| 836 | - * @return void |
|
| 837 | - */ |
|
| 838 | - public function propFindNode(PropFind $propFind, INode $node) { |
|
| 839 | - |
|
| 840 | - if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) { |
|
| 841 | - |
|
| 842 | - $nodeProperties = $node->getProperties($propertyNames); |
|
| 843 | - foreach ($propertyNames as $propertyName) { |
|
| 844 | - if (array_key_exists($propertyName, $nodeProperties)) { |
|
| 845 | - $propFind->set($propertyName, $nodeProperties[$propertyName], 200); |
|
| 846 | - } |
|
| 847 | - } |
|
| 848 | - |
|
| 849 | - } |
|
| 850 | - |
|
| 851 | - } |
|
| 852 | - |
|
| 853 | - /** |
|
| 854 | - * This method is called when properties are retrieved. |
|
| 855 | - * |
|
| 856 | - * This specific handler is called very late in the process, because we |
|
| 857 | - * want other systems to first have a chance to handle the properties. |
|
| 858 | - * |
|
| 859 | - * @param PropFind $propFind |
|
| 860 | - * @param INode $node |
|
| 861 | - * @return void |
|
| 862 | - */ |
|
| 863 | - public function propFindLate(PropFind $propFind, INode $node) { |
|
| 864 | - |
|
| 865 | - $propFind->handle('{http://calendarserver.org/ns/}getctag', function() use ($propFind) { |
|
| 866 | - |
|
| 867 | - // If we already have a sync-token from the current propFind |
|
| 868 | - // request, we can re-use that. |
|
| 869 | - $val = $propFind->get('{http://sabredav.org/ns}sync-token'); |
|
| 870 | - if ($val) return $val; |
|
| 871 | - |
|
| 872 | - $val = $propFind->get('{DAV:}sync-token'); |
|
| 873 | - if ($val && is_scalar($val)) { |
|
| 874 | - return $val; |
|
| 875 | - } |
|
| 876 | - if ($val && $val instanceof Xml\Property\Href) { |
|
| 877 | - return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); |
|
| 878 | - } |
|
| 879 | - |
|
| 880 | - // If we got here, the earlier two properties may simply not have |
|
| 881 | - // been part of the earlier request. We're going to fetch them. |
|
| 882 | - $result = $this->server->getProperties($propFind->getPath(), [ |
|
| 883 | - '{http://sabredav.org/ns}sync-token', |
|
| 884 | - '{DAV:}sync-token', |
|
| 885 | - ]); |
|
| 886 | - |
|
| 887 | - if (isset($result['{http://sabredav.org/ns}sync-token'])) { |
|
| 888 | - return $result['{http://sabredav.org/ns}sync-token']; |
|
| 889 | - } |
|
| 890 | - if (isset($result['{DAV:}sync-token'])) { |
|
| 891 | - $val = $result['{DAV:}sync-token']; |
|
| 892 | - if (is_scalar($val)) { |
|
| 893 | - return $val; |
|
| 894 | - } elseif ($val instanceof Xml\Property\Href) { |
|
| 895 | - return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); |
|
| 896 | - } |
|
| 897 | - } |
|
| 898 | - |
|
| 899 | - }); |
|
| 900 | - |
|
| 901 | - } |
|
| 902 | - |
|
| 903 | - /** |
|
| 904 | - * Returns a bunch of meta-data about the plugin. |
|
| 905 | - * |
|
| 906 | - * Providing this information is optional, and is mainly displayed by the |
|
| 907 | - * Browser plugin. |
|
| 908 | - * |
|
| 909 | - * The description key in the returned array may contain html and will not |
|
| 910 | - * be sanitized. |
|
| 911 | - * |
|
| 912 | - * @return array |
|
| 913 | - */ |
|
| 914 | - public function getPluginInfo() { |
|
| 915 | - |
|
| 916 | - return [ |
|
| 917 | - 'name' => $this->getPluginName(), |
|
| 918 | - 'description' => 'The Core plugin provides a lot of the basic functionality required by WebDAV, such as a default implementation for all HTTP and WebDAV methods.', |
|
| 919 | - 'link' => null, |
|
| 920 | - ]; |
|
| 921 | - |
|
| 922 | - } |
|
| 672 | + if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) return false; |
|
| 673 | + if ($copyInfo['destinationExists']) { |
|
| 674 | + if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) return false; |
|
| 675 | + $this->server->tree->delete($copyInfo['destination']); |
|
| 676 | + } |
|
| 677 | + |
|
| 678 | + $this->server->tree->copy($path, $copyInfo['destination']); |
|
| 679 | + $this->server->emit('afterBind', [$copyInfo['destination']]); |
|
| 680 | + |
|
| 681 | + // If a resource was overwritten we should send a 204, otherwise a 201 |
|
| 682 | + $response->setHeader('Content-Length', '0'); |
|
| 683 | + $response->setStatus($copyInfo['destinationExists'] ? 204 : 201); |
|
| 684 | + |
|
| 685 | + // Sending back false will interupt the event chain and tell the server |
|
| 686 | + // we've handled this method. |
|
| 687 | + return false; |
|
| 688 | + |
|
| 689 | + |
|
| 690 | + } |
|
| 691 | + |
|
| 692 | + /** |
|
| 693 | + * HTTP REPORT method implementation |
|
| 694 | + * |
|
| 695 | + * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) |
|
| 696 | + * It's used in a lot of extensions, so it made sense to implement it into the core. |
|
| 697 | + * |
|
| 698 | + * @param RequestInterface $request |
|
| 699 | + * @param ResponseInterface $response |
|
| 700 | + * @return bool |
|
| 701 | + */ |
|
| 702 | + public function httpReport(RequestInterface $request, ResponseInterface $response) { |
|
| 703 | + |
|
| 704 | + $path = $request->getPath(); |
|
| 705 | + |
|
| 706 | + $result = $this->server->xml->parse( |
|
| 707 | + $request->getBody(), |
|
| 708 | + $request->getUrl(), |
|
| 709 | + $rootElementName |
|
| 710 | + ); |
|
| 711 | + |
|
| 712 | + if ($this->server->emit('report', [$rootElementName, $result, $path])) { |
|
| 713 | + |
|
| 714 | + // If emit returned true, it means the report was not supported |
|
| 715 | + throw new Exception\ReportNotSupported(); |
|
| 716 | + |
|
| 717 | + } |
|
| 718 | + |
|
| 719 | + // Sending back false will interupt the event chain and tell the server |
|
| 720 | + // we've handled this method. |
|
| 721 | + return false; |
|
| 722 | + |
|
| 723 | + } |
|
| 724 | + |
|
| 725 | + /** |
|
| 726 | + * This method is called during property updates. |
|
| 727 | + * |
|
| 728 | + * Here we check if a user attempted to update a protected property and |
|
| 729 | + * ensure that the process fails if this is the case. |
|
| 730 | + * |
|
| 731 | + * @param string $path |
|
| 732 | + * @param PropPatch $propPatch |
|
| 733 | + * @return void |
|
| 734 | + */ |
|
| 735 | + public function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) { |
|
| 736 | + |
|
| 737 | + // Comparing the mutation list to the list of propetected properties. |
|
| 738 | + $mutations = $propPatch->getMutations(); |
|
| 739 | + |
|
| 740 | + $protected = array_intersect( |
|
| 741 | + $this->server->protectedProperties, |
|
| 742 | + array_keys($mutations) |
|
| 743 | + ); |
|
| 744 | + |
|
| 745 | + if ($protected) { |
|
| 746 | + $propPatch->setResultCode($protected, 403); |
|
| 747 | + } |
|
| 748 | + |
|
| 749 | + } |
|
| 750 | + |
|
| 751 | + /** |
|
| 752 | + * This method is called during property updates. |
|
| 753 | + * |
|
| 754 | + * Here we check if a node implements IProperties and let the node handle |
|
| 755 | + * updating of (some) properties. |
|
| 756 | + * |
|
| 757 | + * @param string $path |
|
| 758 | + * @param PropPatch $propPatch |
|
| 759 | + * @return void |
|
| 760 | + */ |
|
| 761 | + public function propPatchNodeUpdate($path, PropPatch $propPatch) { |
|
| 762 | + |
|
| 763 | + // This should trigger a 404 if the node doesn't exist. |
|
| 764 | + $node = $this->server->tree->getNodeForPath($path); |
|
| 765 | + |
|
| 766 | + if ($node instanceof IProperties) { |
|
| 767 | + $node->propPatch($propPatch); |
|
| 768 | + } |
|
| 769 | + |
|
| 770 | + } |
|
| 771 | + |
|
| 772 | + /** |
|
| 773 | + * This method is called when properties are retrieved. |
|
| 774 | + * |
|
| 775 | + * Here we add all the default properties. |
|
| 776 | + * |
|
| 777 | + * @param PropFind $propFind |
|
| 778 | + * @param INode $node |
|
| 779 | + * @return void |
|
| 780 | + */ |
|
| 781 | + public function propFind(PropFind $propFind, INode $node) { |
|
| 782 | + |
|
| 783 | + $propFind->handle('{DAV:}getlastmodified', function() use ($node) { |
|
| 784 | + $lm = $node->getLastModified(); |
|
| 785 | + if ($lm) { |
|
| 786 | + return new Xml\Property\GetLastModified($lm); |
|
| 787 | + } |
|
| 788 | + }); |
|
| 789 | + |
|
| 790 | + if ($node instanceof IFile) { |
|
| 791 | + $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']); |
|
| 792 | + $propFind->handle('{DAV:}getetag', [$node, 'getETag']); |
|
| 793 | + $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']); |
|
| 794 | + } |
|
| 795 | + |
|
| 796 | + if ($node instanceof IQuota) { |
|
| 797 | + $quotaInfo = null; |
|
| 798 | + $propFind->handle('{DAV:}quota-used-bytes', function() use (&$quotaInfo, $node) { |
|
| 799 | + $quotaInfo = $node->getQuotaInfo(); |
|
| 800 | + return $quotaInfo[0]; |
|
| 801 | + }); |
|
| 802 | + $propFind->handle('{DAV:}quota-available-bytes', function() use (&$quotaInfo, $node) { |
|
| 803 | + if (!$quotaInfo) { |
|
| 804 | + $quotaInfo = $node->getQuotaInfo(); |
|
| 805 | + } |
|
| 806 | + return $quotaInfo[1]; |
|
| 807 | + }); |
|
| 808 | + } |
|
| 809 | + |
|
| 810 | + $propFind->handle('{DAV:}supported-report-set', function() use ($propFind) { |
|
| 811 | + $reports = []; |
|
| 812 | + foreach ($this->server->getPlugins() as $plugin) { |
|
| 813 | + $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath())); |
|
| 814 | + } |
|
| 815 | + return new Xml\Property\SupportedReportSet($reports); |
|
| 816 | + }); |
|
| 817 | + $propFind->handle('{DAV:}resourcetype', function() use ($node) { |
|
| 818 | + return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node)); |
|
| 819 | + }); |
|
| 820 | + $propFind->handle('{DAV:}supported-method-set', function() use ($propFind) { |
|
| 821 | + return new Xml\Property\SupportedMethodSet( |
|
| 822 | + $this->server->getAllowedMethods($propFind->getPath()) |
|
| 823 | + ); |
|
| 824 | + }); |
|
| 825 | + |
|
| 826 | + } |
|
| 827 | + |
|
| 828 | + /** |
|
| 829 | + * Fetches properties for a node. |
|
| 830 | + * |
|
| 831 | + * This event is called a bit later, so plugins have a chance first to |
|
| 832 | + * populate the result. |
|
| 833 | + * |
|
| 834 | + * @param PropFind $propFind |
|
| 835 | + * @param INode $node |
|
| 836 | + * @return void |
|
| 837 | + */ |
|
| 838 | + public function propFindNode(PropFind $propFind, INode $node) { |
|
| 839 | + |
|
| 840 | + if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) { |
|
| 841 | + |
|
| 842 | + $nodeProperties = $node->getProperties($propertyNames); |
|
| 843 | + foreach ($propertyNames as $propertyName) { |
|
| 844 | + if (array_key_exists($propertyName, $nodeProperties)) { |
|
| 845 | + $propFind->set($propertyName, $nodeProperties[$propertyName], 200); |
|
| 846 | + } |
|
| 847 | + } |
|
| 848 | + |
|
| 849 | + } |
|
| 850 | + |
|
| 851 | + } |
|
| 852 | + |
|
| 853 | + /** |
|
| 854 | + * This method is called when properties are retrieved. |
|
| 855 | + * |
|
| 856 | + * This specific handler is called very late in the process, because we |
|
| 857 | + * want other systems to first have a chance to handle the properties. |
|
| 858 | + * |
|
| 859 | + * @param PropFind $propFind |
|
| 860 | + * @param INode $node |
|
| 861 | + * @return void |
|
| 862 | + */ |
|
| 863 | + public function propFindLate(PropFind $propFind, INode $node) { |
|
| 864 | + |
|
| 865 | + $propFind->handle('{http://calendarserver.org/ns/}getctag', function() use ($propFind) { |
|
| 866 | + |
|
| 867 | + // If we already have a sync-token from the current propFind |
|
| 868 | + // request, we can re-use that. |
|
| 869 | + $val = $propFind->get('{http://sabredav.org/ns}sync-token'); |
|
| 870 | + if ($val) return $val; |
|
| 871 | + |
|
| 872 | + $val = $propFind->get('{DAV:}sync-token'); |
|
| 873 | + if ($val && is_scalar($val)) { |
|
| 874 | + return $val; |
|
| 875 | + } |
|
| 876 | + if ($val && $val instanceof Xml\Property\Href) { |
|
| 877 | + return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); |
|
| 878 | + } |
|
| 879 | + |
|
| 880 | + // If we got here, the earlier two properties may simply not have |
|
| 881 | + // been part of the earlier request. We're going to fetch them. |
|
| 882 | + $result = $this->server->getProperties($propFind->getPath(), [ |
|
| 883 | + '{http://sabredav.org/ns}sync-token', |
|
| 884 | + '{DAV:}sync-token', |
|
| 885 | + ]); |
|
| 886 | + |
|
| 887 | + if (isset($result['{http://sabredav.org/ns}sync-token'])) { |
|
| 888 | + return $result['{http://sabredav.org/ns}sync-token']; |
|
| 889 | + } |
|
| 890 | + if (isset($result['{DAV:}sync-token'])) { |
|
| 891 | + $val = $result['{DAV:}sync-token']; |
|
| 892 | + if (is_scalar($val)) { |
|
| 893 | + return $val; |
|
| 894 | + } elseif ($val instanceof Xml\Property\Href) { |
|
| 895 | + return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); |
|
| 896 | + } |
|
| 897 | + } |
|
| 898 | + |
|
| 899 | + }); |
|
| 900 | + |
|
| 901 | + } |
|
| 902 | + |
|
| 903 | + /** |
|
| 904 | + * Returns a bunch of meta-data about the plugin. |
|
| 905 | + * |
|
| 906 | + * Providing this information is optional, and is mainly displayed by the |
|
| 907 | + * Browser plugin. |
|
| 908 | + * |
|
| 909 | + * The description key in the returned array may contain html and will not |
|
| 910 | + * be sanitized. |
|
| 911 | + * |
|
| 912 | + * @return array |
|
| 913 | + */ |
|
| 914 | + public function getPluginInfo() { |
|
| 915 | + |
|
| 916 | + return [ |
|
| 917 | + 'name' => $this->getPluginName(), |
|
| 918 | + 'description' => 'The Core plugin provides a lot of the basic functionality required by WebDAV, such as a default implementation for all HTTP and WebDAV methods.', |
|
| 919 | + 'link' => null, |
|
| 920 | + ]; |
|
| 921 | + |
|
| 922 | + } |
|
| 923 | 923 | } |
@@ -32,23 +32,23 @@ discard block |
||
| 32 | 32 | public function initialize(Server $server) { |
| 33 | 33 | |
| 34 | 34 | $this->server = $server; |
| 35 | - $server->on('method:GET', [$this, 'httpGet']); |
|
| 36 | - $server->on('method:OPTIONS', [$this, 'httpOptions']); |
|
| 37 | - $server->on('method:HEAD', [$this, 'httpHead']); |
|
| 38 | - $server->on('method:DELETE', [$this, 'httpDelete']); |
|
| 39 | - $server->on('method:PROPFIND', [$this, 'httpPropFind']); |
|
| 35 | + $server->on('method:GET', [$this, 'httpGet']); |
|
| 36 | + $server->on('method:OPTIONS', [$this, 'httpOptions']); |
|
| 37 | + $server->on('method:HEAD', [$this, 'httpHead']); |
|
| 38 | + $server->on('method:DELETE', [$this, 'httpDelete']); |
|
| 39 | + $server->on('method:PROPFIND', [$this, 'httpPropFind']); |
|
| 40 | 40 | $server->on('method:PROPPATCH', [$this, 'httpPropPatch']); |
| 41 | - $server->on('method:PUT', [$this, 'httpPut']); |
|
| 42 | - $server->on('method:MKCOL', [$this, 'httpMkcol']); |
|
| 43 | - $server->on('method:MOVE', [$this, 'httpMove']); |
|
| 44 | - $server->on('method:COPY', [$this, 'httpCopy']); |
|
| 45 | - $server->on('method:REPORT', [$this, 'httpReport']); |
|
| 46 | - |
|
| 47 | - $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90); |
|
| 48 | - $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200); |
|
| 49 | - $server->on('propFind', [$this, 'propFind']); |
|
| 50 | - $server->on('propFind', [$this, 'propFindNode'], 120); |
|
| 51 | - $server->on('propFind', [$this, 'propFindLate'], 200); |
|
| 41 | + $server->on('method:PUT', [$this, 'httpPut']); |
|
| 42 | + $server->on('method:MKCOL', [$this, 'httpMkcol']); |
|
| 43 | + $server->on('method:MOVE', [$this, 'httpMove']); |
|
| 44 | + $server->on('method:COPY', [$this, 'httpCopy']); |
|
| 45 | + $server->on('method:REPORT', [$this, 'httpReport']); |
|
| 46 | + |
|
| 47 | + $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90); |
|
| 48 | + $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200); |
|
| 49 | + $server->on('propFind', [$this, 'propFind']); |
|
| 50 | + $server->on('propFind', [$this, 'propFindNode'], 120); |
|
| 51 | + $server->on('propFind', [$this, 'propFindLate'], 200); |
|
| 52 | 52 | |
| 53 | 53 | } |
| 54 | 54 | |
@@ -159,7 +159,7 @@ discard block |
||
| 159 | 159 | } else { |
| 160 | 160 | |
| 161 | 161 | $start = $nodeSize - $range[1]; |
| 162 | - $end = $nodeSize - 1; |
|
| 162 | + $end = $nodeSize - 1; |
|
| 163 | 163 | |
| 164 | 164 | if ($start < 0) $start = 0; |
| 165 | 165 | |
@@ -170,7 +170,7 @@ discard block |
||
| 170 | 170 | // if fseek failed. |
| 171 | 171 | if (!stream_get_meta_data($body)['seekable'] || fseek($body, $start, SEEK_SET) === -1) { |
| 172 | 172 | $consumeBlock = 8192; |
| 173 | - for ($consumed = 0; $start - $consumed > 0;){ |
|
| 173 | + for ($consumed = 0; $start - $consumed > 0;) { |
|
| 174 | 174 | if (feof($body)) throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $start . ') exceeded the size of the entity (' . $consumed . ')'); |
| 175 | 175 | $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock))); |
| 176 | 176 | } |
@@ -388,7 +388,7 @@ discard block |
||
| 388 | 388 | // multi-status. |
| 389 | 389 | $ok = true; |
| 390 | 390 | foreach ($result as $prop => $code) { |
| 391 | - if ((int)$code > 299) { |
|
| 391 | + if ((int) $code > 299) { |
|
| 392 | 392 | $ok = false; |
| 393 | 393 | } |
| 394 | 394 | } |
@@ -15,50 +15,50 @@ |
||
| 15 | 15 | */ |
| 16 | 16 | class UUIDUtil { |
| 17 | 17 | |
| 18 | - /** |
|
| 19 | - * Returns a pseudo-random v4 UUID |
|
| 20 | - * |
|
| 21 | - * This function is based on a comment by Andrew Moore on php.net |
|
| 22 | - * |
|
| 23 | - * @see http://www.php.net/manual/en/function.uniqid.php#94959 |
|
| 24 | - * @return string |
|
| 25 | - */ |
|
| 26 | - static function getUUID() { |
|
| 18 | + /** |
|
| 19 | + * Returns a pseudo-random v4 UUID |
|
| 20 | + * |
|
| 21 | + * This function is based on a comment by Andrew Moore on php.net |
|
| 22 | + * |
|
| 23 | + * @see http://www.php.net/manual/en/function.uniqid.php#94959 |
|
| 24 | + * @return string |
|
| 25 | + */ |
|
| 26 | + static function getUUID() { |
|
| 27 | 27 | |
| 28 | - return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', |
|
| 29 | - // 32 bits for "time_low" |
|
| 30 | - mt_rand(0, 0xffff), mt_rand(0, 0xffff), |
|
| 28 | + return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', |
|
| 29 | + // 32 bits for "time_low" |
|
| 30 | + mt_rand(0, 0xffff), mt_rand(0, 0xffff), |
|
| 31 | 31 | |
| 32 | - // 16 bits for "time_mid" |
|
| 33 | - mt_rand(0, 0xffff), |
|
| 32 | + // 16 bits for "time_mid" |
|
| 33 | + mt_rand(0, 0xffff), |
|
| 34 | 34 | |
| 35 | - // 16 bits for "time_hi_and_version", |
|
| 36 | - // four most significant bits holds version number 4 |
|
| 37 | - mt_rand(0, 0x0fff) | 0x4000, |
|
| 35 | + // 16 bits for "time_hi_and_version", |
|
| 36 | + // four most significant bits holds version number 4 |
|
| 37 | + mt_rand(0, 0x0fff) | 0x4000, |
|
| 38 | 38 | |
| 39 | - // 16 bits, 8 bits for "clk_seq_hi_res", |
|
| 40 | - // 8 bits for "clk_seq_low", |
|
| 41 | - // two most significant bits holds zero and one for variant DCE1.1 |
|
| 42 | - mt_rand(0, 0x3fff) | 0x8000, |
|
| 39 | + // 16 bits, 8 bits for "clk_seq_hi_res", |
|
| 40 | + // 8 bits for "clk_seq_low", |
|
| 41 | + // two most significant bits holds zero and one for variant DCE1.1 |
|
| 42 | + mt_rand(0, 0x3fff) | 0x8000, |
|
| 43 | 43 | |
| 44 | - // 48 bits for "node" |
|
| 45 | - mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) |
|
| 46 | - ); |
|
| 47 | - } |
|
| 44 | + // 48 bits for "node" |
|
| 45 | + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) |
|
| 46 | + ); |
|
| 47 | + } |
|
| 48 | 48 | |
| 49 | - /** |
|
| 50 | - * Checks if a string is a valid UUID. |
|
| 51 | - * |
|
| 52 | - * @param string $uuid |
|
| 53 | - * @return bool |
|
| 54 | - */ |
|
| 55 | - static function validateUUID($uuid) { |
|
| 49 | + /** |
|
| 50 | + * Checks if a string is a valid UUID. |
|
| 51 | + * |
|
| 52 | + * @param string $uuid |
|
| 53 | + * @return bool |
|
| 54 | + */ |
|
| 55 | + static function validateUUID($uuid) { |
|
| 56 | 56 | |
| 57 | - return preg_match( |
|
| 58 | - '/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', |
|
| 59 | - $uuid |
|
| 60 | - ) !== 0; |
|
| 57 | + return preg_match( |
|
| 58 | + '/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', |
|
| 59 | + $uuid |
|
| 60 | + ) !== 0; |
|
| 61 | 61 | |
| 62 | - } |
|
| 62 | + } |
|
| 63 | 63 | |
| 64 | 64 | } |
@@ -201,7 +201,9 @@ |
||
| 201 | 201 | |
| 202 | 202 | // Sanity check. SQL may select too many records, such as records |
| 203 | 203 | // with different cases. |
| 204 | - if ($row['path'] !== $source && strpos($row['path'], $source . '/') !== 0) continue; |
|
| 204 | + if ($row['path'] !== $source && strpos($row['path'], $source . '/') !== 0) { |
|
| 205 | + continue; |
|
| 206 | + } |
|
| 205 | 207 | |
| 206 | 208 | $trailingPart = substr($row['path'], strlen($source) + 1); |
| 207 | 209 | $newPath = $destination; |
@@ -20,198 +20,198 @@ |
||
| 20 | 20 | */ |
| 21 | 21 | class PDO implements BackendInterface { |
| 22 | 22 | |
| 23 | - /** |
|
| 24 | - * Value is stored as string. |
|
| 25 | - */ |
|
| 26 | - const VT_STRING = 1; |
|
| 27 | - |
|
| 28 | - /** |
|
| 29 | - * Value is stored as XML fragment. |
|
| 30 | - */ |
|
| 31 | - const VT_XML = 2; |
|
| 32 | - |
|
| 33 | - /** |
|
| 34 | - * Value is stored as a property object. |
|
| 35 | - */ |
|
| 36 | - const VT_OBJECT = 3; |
|
| 37 | - |
|
| 38 | - /** |
|
| 39 | - * PDO |
|
| 40 | - * |
|
| 41 | - * @var \PDO |
|
| 42 | - */ |
|
| 43 | - protected $pdo; |
|
| 44 | - |
|
| 45 | - /** |
|
| 46 | - * PDO table name we'll be using |
|
| 47 | - * |
|
| 48 | - * @var string |
|
| 49 | - */ |
|
| 50 | - public $tableName = 'propertystorage'; |
|
| 51 | - |
|
| 52 | - /** |
|
| 53 | - * Creates the PDO property storage engine |
|
| 54 | - * |
|
| 55 | - * @param \PDO $pdo |
|
| 56 | - */ |
|
| 57 | - public function __construct(\PDO $pdo) { |
|
| 58 | - |
|
| 59 | - $this->pdo = $pdo; |
|
| 60 | - |
|
| 61 | - } |
|
| 62 | - |
|
| 63 | - /** |
|
| 64 | - * Fetches properties for a path. |
|
| 65 | - * |
|
| 66 | - * This method received a PropFind object, which contains all the |
|
| 67 | - * information about the properties that need to be fetched. |
|
| 68 | - * |
|
| 69 | - * Ususually you would just want to call 'get404Properties' on this object, |
|
| 70 | - * as this will give you the _exact_ list of properties that need to be |
|
| 71 | - * fetched, and haven't yet. |
|
| 72 | - * |
|
| 73 | - * However, you can also support the 'allprops' property here. In that |
|
| 74 | - * case, you should check for $propFind->isAllProps(). |
|
| 75 | - * |
|
| 76 | - * @param string $path |
|
| 77 | - * @param PropFind $propFind |
|
| 78 | - * @return void |
|
| 79 | - */ |
|
| 80 | - public function propFind($path, PropFind $propFind) { |
|
| 81 | - |
|
| 82 | - if (!$propFind->isAllProps() && count($propFind->get404Properties()) === 0) { |
|
| 83 | - return; |
|
| 84 | - } |
|
| 85 | - |
|
| 86 | - $query = sprintf('SELECT name, value, valuetype FROM %s WHERE path = ?', $this->tableName); |
|
| 87 | - $stmt = $this->pdo->prepare($query); |
|
| 88 | - $stmt->execute([$path]); |
|
| 89 | - |
|
| 90 | - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 91 | - switch ($row['valuetype']) { |
|
| 92 | - case null : |
|
| 93 | - case self::VT_STRING : |
|
| 94 | - $propFind->set($row['name'], $row['value']); |
|
| 95 | - break; |
|
| 96 | - case self::VT_XML : |
|
| 97 | - $propFind->set($row['name'], new Complex($row['value'])); |
|
| 98 | - break; |
|
| 99 | - case self::VT_OBJECT : |
|
| 100 | - $propFind->set($row['name'], unserialize($row['value'])); |
|
| 101 | - break; |
|
| 102 | - } |
|
| 103 | - } |
|
| 104 | - |
|
| 105 | - } |
|
| 106 | - |
|
| 107 | - /** |
|
| 108 | - * Updates properties for a path |
|
| 109 | - * |
|
| 110 | - * This method received a PropPatch object, which contains all the |
|
| 111 | - * information about the update. |
|
| 112 | - * |
|
| 113 | - * Usually you would want to call 'handleRemaining' on this object, to get; |
|
| 114 | - * a list of all properties that need to be stored. |
|
| 115 | - * |
|
| 116 | - * @param string $path |
|
| 117 | - * @param PropPatch $propPatch |
|
| 118 | - * @return void |
|
| 119 | - */ |
|
| 120 | - public function propPatch($path, PropPatch $propPatch) { |
|
| 121 | - |
|
| 122 | - $propPatch->handleRemaining(function($properties) use ($path) { |
|
| 123 | - |
|
| 124 | - $updateStmt = $this->pdo->prepare("REPLACE INTO " . $this->tableName . " (path, name, valuetype, value) VALUES (?, ?, ?, ?)"); |
|
| 125 | - $deleteStmt = $this->pdo->prepare(sprintf("DELETE FROM %s WHERE path = ? && name = ?", $this->tableName)); |
|
| 126 | - |
|
| 127 | - foreach ($properties as $name => $value) { |
|
| 128 | - |
|
| 129 | - if (!is_null($value)) { |
|
| 130 | - if (is_scalar($value)) { |
|
| 131 | - $valueType = self::VT_STRING; |
|
| 132 | - } elseif ($value instanceof Complex) { |
|
| 133 | - $valueType = self::VT_XML; |
|
| 134 | - $value = $value->getXml(); |
|
| 135 | - } else { |
|
| 136 | - $valueType = self::VT_OBJECT; |
|
| 137 | - $value = serialize($value); |
|
| 138 | - } |
|
| 139 | - $updateStmt->execute([$path, $name, $valueType, $value]); |
|
| 140 | - } else { |
|
| 141 | - $deleteStmt->execute([$path, $name]); |
|
| 142 | - } |
|
| 143 | - |
|
| 144 | - } |
|
| 145 | - |
|
| 146 | - return true; |
|
| 147 | - |
|
| 148 | - }); |
|
| 149 | - |
|
| 150 | - } |
|
| 151 | - |
|
| 152 | - /** |
|
| 153 | - * This method is called after a node is deleted. |
|
| 154 | - * |
|
| 155 | - * This allows a backend to clean up all associated properties. |
|
| 156 | - * |
|
| 157 | - * The delete method will get called once for the deletion of an entire |
|
| 158 | - * tree. |
|
| 159 | - * |
|
| 160 | - * @param string $path |
|
| 161 | - * @return void |
|
| 162 | - */ |
|
| 163 | - public function delete($path) { |
|
| 164 | - |
|
| 165 | - $stmt = $this->pdo->prepare(sprintf("DELETE FROM %s WHERE path = ? || path LIKE ? ESCAPE '='", $this->tableName)); |
|
| 166 | - $childPath = strtr( |
|
| 167 | - $path, |
|
| 168 | - [ |
|
| 169 | - '=' => '==', |
|
| 170 | - '%' => '=%', |
|
| 171 | - '_' => '=_' |
|
| 172 | - ] |
|
| 173 | - ) . '/%'; |
|
| 174 | - |
|
| 175 | - $stmt->execute([$path, $childPath]); |
|
| 176 | - |
|
| 177 | - } |
|
| 178 | - |
|
| 179 | - /** |
|
| 180 | - * This method is called after a successful MOVE |
|
| 181 | - * |
|
| 182 | - * This should be used to migrate all properties from one path to another. |
|
| 183 | - * Note that entire collections may be moved, so ensure that all properties |
|
| 184 | - * for children are also moved along. |
|
| 185 | - * |
|
| 186 | - * @param string $source |
|
| 187 | - * @param string $destination |
|
| 188 | - * @return void |
|
| 189 | - */ |
|
| 190 | - public function move($source, $destination) { |
|
| 191 | - |
|
| 192 | - // I don't know a way to write this all in a single sql query that's |
|
| 193 | - // also compatible across db engines, so we're letting PHP do all the |
|
| 194 | - // updates. Much slower, but it should still be pretty fast in most |
|
| 195 | - // cases. |
|
| 196 | - $select = $this->pdo->prepare(sprintf('SELECT id, path FROM %s WHERE path = ? || path LIKE ?', $this->tableName)); |
|
| 197 | - $select->execute([$source, $source . '/%']); |
|
| 198 | - |
|
| 199 | - $update = $this->pdo->prepare(sprintf('UPDATE %s SET path = ? WHERE id = ?', $this->tableName)); |
|
| 200 | - while ($row = $select->fetch(\PDO::FETCH_ASSOC)) { |
|
| 201 | - |
|
| 202 | - // Sanity check. SQL may select too many records, such as records |
|
| 203 | - // with different cases. |
|
| 204 | - if ($row['path'] !== $source && strpos($row['path'], $source . '/') !== 0) continue; |
|
| 205 | - |
|
| 206 | - $trailingPart = substr($row['path'], strlen($source) + 1); |
|
| 207 | - $newPath = $destination; |
|
| 208 | - if ($trailingPart) { |
|
| 209 | - $newPath .= '/' . $trailingPart; |
|
| 210 | - } |
|
| 211 | - $update->execute([$newPath, $row['id']]); |
|
| 212 | - |
|
| 213 | - } |
|
| 214 | - |
|
| 215 | - } |
|
| 23 | + /** |
|
| 24 | + * Value is stored as string. |
|
| 25 | + */ |
|
| 26 | + const VT_STRING = 1; |
|
| 27 | + |
|
| 28 | + /** |
|
| 29 | + * Value is stored as XML fragment. |
|
| 30 | + */ |
|
| 31 | + const VT_XML = 2; |
|
| 32 | + |
|
| 33 | + /** |
|
| 34 | + * Value is stored as a property object. |
|
| 35 | + */ |
|
| 36 | + const VT_OBJECT = 3; |
|
| 37 | + |
|
| 38 | + /** |
|
| 39 | + * PDO |
|
| 40 | + * |
|
| 41 | + * @var \PDO |
|
| 42 | + */ |
|
| 43 | + protected $pdo; |
|
| 44 | + |
|
| 45 | + /** |
|
| 46 | + * PDO table name we'll be using |
|
| 47 | + * |
|
| 48 | + * @var string |
|
| 49 | + */ |
|
| 50 | + public $tableName = 'propertystorage'; |
|
| 51 | + |
|
| 52 | + /** |
|
| 53 | + * Creates the PDO property storage engine |
|
| 54 | + * |
|
| 55 | + * @param \PDO $pdo |
|
| 56 | + */ |
|
| 57 | + public function __construct(\PDO $pdo) { |
|
| 58 | + |
|
| 59 | + $this->pdo = $pdo; |
|
| 60 | + |
|
| 61 | + } |
|
| 62 | + |
|
| 63 | + /** |
|
| 64 | + * Fetches properties for a path. |
|
| 65 | + * |
|
| 66 | + * This method received a PropFind object, which contains all the |
|
| 67 | + * information about the properties that need to be fetched. |
|
| 68 | + * |
|
| 69 | + * Ususually you would just want to call 'get404Properties' on this object, |
|
| 70 | + * as this will give you the _exact_ list of properties that need to be |
|
| 71 | + * fetched, and haven't yet. |
|
| 72 | + * |
|
| 73 | + * However, you can also support the 'allprops' property here. In that |
|
| 74 | + * case, you should check for $propFind->isAllProps(). |
|
| 75 | + * |
|
| 76 | + * @param string $path |
|
| 77 | + * @param PropFind $propFind |
|
| 78 | + * @return void |
|
| 79 | + */ |
|
| 80 | + public function propFind($path, PropFind $propFind) { |
|
| 81 | + |
|
| 82 | + if (!$propFind->isAllProps() && count($propFind->get404Properties()) === 0) { |
|
| 83 | + return; |
|
| 84 | + } |
|
| 85 | + |
|
| 86 | + $query = sprintf('SELECT name, value, valuetype FROM %s WHERE path = ?', $this->tableName); |
|
| 87 | + $stmt = $this->pdo->prepare($query); |
|
| 88 | + $stmt->execute([$path]); |
|
| 89 | + |
|
| 90 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 91 | + switch ($row['valuetype']) { |
|
| 92 | + case null : |
|
| 93 | + case self::VT_STRING : |
|
| 94 | + $propFind->set($row['name'], $row['value']); |
|
| 95 | + break; |
|
| 96 | + case self::VT_XML : |
|
| 97 | + $propFind->set($row['name'], new Complex($row['value'])); |
|
| 98 | + break; |
|
| 99 | + case self::VT_OBJECT : |
|
| 100 | + $propFind->set($row['name'], unserialize($row['value'])); |
|
| 101 | + break; |
|
| 102 | + } |
|
| 103 | + } |
|
| 104 | + |
|
| 105 | + } |
|
| 106 | + |
|
| 107 | + /** |
|
| 108 | + * Updates properties for a path |
|
| 109 | + * |
|
| 110 | + * This method received a PropPatch object, which contains all the |
|
| 111 | + * information about the update. |
|
| 112 | + * |
|
| 113 | + * Usually you would want to call 'handleRemaining' on this object, to get; |
|
| 114 | + * a list of all properties that need to be stored. |
|
| 115 | + * |
|
| 116 | + * @param string $path |
|
| 117 | + * @param PropPatch $propPatch |
|
| 118 | + * @return void |
|
| 119 | + */ |
|
| 120 | + public function propPatch($path, PropPatch $propPatch) { |
|
| 121 | + |
|
| 122 | + $propPatch->handleRemaining(function($properties) use ($path) { |
|
| 123 | + |
|
| 124 | + $updateStmt = $this->pdo->prepare("REPLACE INTO " . $this->tableName . " (path, name, valuetype, value) VALUES (?, ?, ?, ?)"); |
|
| 125 | + $deleteStmt = $this->pdo->prepare(sprintf("DELETE FROM %s WHERE path = ? && name = ?", $this->tableName)); |
|
| 126 | + |
|
| 127 | + foreach ($properties as $name => $value) { |
|
| 128 | + |
|
| 129 | + if (!is_null($value)) { |
|
| 130 | + if (is_scalar($value)) { |
|
| 131 | + $valueType = self::VT_STRING; |
|
| 132 | + } elseif ($value instanceof Complex) { |
|
| 133 | + $valueType = self::VT_XML; |
|
| 134 | + $value = $value->getXml(); |
|
| 135 | + } else { |
|
| 136 | + $valueType = self::VT_OBJECT; |
|
| 137 | + $value = serialize($value); |
|
| 138 | + } |
|
| 139 | + $updateStmt->execute([$path, $name, $valueType, $value]); |
|
| 140 | + } else { |
|
| 141 | + $deleteStmt->execute([$path, $name]); |
|
| 142 | + } |
|
| 143 | + |
|
| 144 | + } |
|
| 145 | + |
|
| 146 | + return true; |
|
| 147 | + |
|
| 148 | + }); |
|
| 149 | + |
|
| 150 | + } |
|
| 151 | + |
|
| 152 | + /** |
|
| 153 | + * This method is called after a node is deleted. |
|
| 154 | + * |
|
| 155 | + * This allows a backend to clean up all associated properties. |
|
| 156 | + * |
|
| 157 | + * The delete method will get called once for the deletion of an entire |
|
| 158 | + * tree. |
|
| 159 | + * |
|
| 160 | + * @param string $path |
|
| 161 | + * @return void |
|
| 162 | + */ |
|
| 163 | + public function delete($path) { |
|
| 164 | + |
|
| 165 | + $stmt = $this->pdo->prepare(sprintf("DELETE FROM %s WHERE path = ? || path LIKE ? ESCAPE '='", $this->tableName)); |
|
| 166 | + $childPath = strtr( |
|
| 167 | + $path, |
|
| 168 | + [ |
|
| 169 | + '=' => '==', |
|
| 170 | + '%' => '=%', |
|
| 171 | + '_' => '=_' |
|
| 172 | + ] |
|
| 173 | + ) . '/%'; |
|
| 174 | + |
|
| 175 | + $stmt->execute([$path, $childPath]); |
|
| 176 | + |
|
| 177 | + } |
|
| 178 | + |
|
| 179 | + /** |
|
| 180 | + * This method is called after a successful MOVE |
|
| 181 | + * |
|
| 182 | + * This should be used to migrate all properties from one path to another. |
|
| 183 | + * Note that entire collections may be moved, so ensure that all properties |
|
| 184 | + * for children are also moved along. |
|
| 185 | + * |
|
| 186 | + * @param string $source |
|
| 187 | + * @param string $destination |
|
| 188 | + * @return void |
|
| 189 | + */ |
|
| 190 | + public function move($source, $destination) { |
|
| 191 | + |
|
| 192 | + // I don't know a way to write this all in a single sql query that's |
|
| 193 | + // also compatible across db engines, so we're letting PHP do all the |
|
| 194 | + // updates. Much slower, but it should still be pretty fast in most |
|
| 195 | + // cases. |
|
| 196 | + $select = $this->pdo->prepare(sprintf('SELECT id, path FROM %s WHERE path = ? || path LIKE ?', $this->tableName)); |
|
| 197 | + $select->execute([$source, $source . '/%']); |
|
| 198 | + |
|
| 199 | + $update = $this->pdo->prepare(sprintf('UPDATE %s SET path = ? WHERE id = ?', $this->tableName)); |
|
| 200 | + while ($row = $select->fetch(\PDO::FETCH_ASSOC)) { |
|
| 201 | + |
|
| 202 | + // Sanity check. SQL may select too many records, such as records |
|
| 203 | + // with different cases. |
|
| 204 | + if ($row['path'] !== $source && strpos($row['path'], $source . '/') !== 0) continue; |
|
| 205 | + |
|
| 206 | + $trailingPart = substr($row['path'], strlen($source) + 1); |
|
| 207 | + $newPath = $destination; |
|
| 208 | + if ($trailingPart) { |
|
| 209 | + $newPath .= '/' . $trailingPart; |
|
| 210 | + } |
|
| 211 | + $update->execute([$newPath, $row['id']]); |
|
| 212 | + |
|
| 213 | + } |
|
| 214 | + |
|
| 215 | + } |
|
| 216 | 216 | |
| 217 | 217 | } |
@@ -32,7 +32,9 @@ |
||
| 32 | 32 | |
| 33 | 33 | foreach ($this->getChildren() as $child) { |
| 34 | 34 | |
| 35 | - if ($child->getName() === $name) return $child; |
|
| 35 | + if ($child->getName() === $name) { |
|
| 36 | + return $child; |
|
| 37 | + } |
|
| 36 | 38 | |
| 37 | 39 | } |
| 38 | 40 | throw new Exception\NotFound('File not found: ' . $name); |
@@ -14,96 +14,96 @@ |
||
| 14 | 14 | */ |
| 15 | 15 | abstract class Collection extends Node implements ICollection { |
| 16 | 16 | |
| 17 | - /** |
|
| 18 | - * Returns a child object, by its name. |
|
| 19 | - * |
|
| 20 | - * This method makes use of the getChildren method to grab all the child |
|
| 21 | - * nodes, and compares the name. |
|
| 22 | - * Generally its wise to override this, as this can usually be optimized |
|
| 23 | - * |
|
| 24 | - * This method must throw Sabre\DAV\Exception\NotFound if the node does not |
|
| 25 | - * exist. |
|
| 26 | - * |
|
| 27 | - * @param string $name |
|
| 28 | - * @throws Exception\NotFound |
|
| 29 | - * @return INode |
|
| 30 | - */ |
|
| 31 | - public function getChild($name) { |
|
| 32 | - |
|
| 33 | - foreach ($this->getChildren() as $child) { |
|
| 34 | - |
|
| 35 | - if ($child->getName() === $name) return $child; |
|
| 36 | - |
|
| 37 | - } |
|
| 38 | - throw new Exception\NotFound('File not found: ' . $name); |
|
| 39 | - |
|
| 40 | - } |
|
| 41 | - |
|
| 42 | - /** |
|
| 43 | - * Checks is a child-node exists. |
|
| 44 | - * |
|
| 45 | - * It is generally a good idea to try and override this. Usually it can be optimized. |
|
| 46 | - * |
|
| 47 | - * @param string $name |
|
| 48 | - * @return bool |
|
| 49 | - */ |
|
| 50 | - public function childExists($name) { |
|
| 51 | - |
|
| 52 | - try { |
|
| 53 | - |
|
| 54 | - $this->getChild($name); |
|
| 55 | - return true; |
|
| 56 | - |
|
| 57 | - } catch (Exception\NotFound $e) { |
|
| 58 | - |
|
| 59 | - return false; |
|
| 60 | - |
|
| 61 | - } |
|
| 62 | - |
|
| 63 | - } |
|
| 64 | - |
|
| 65 | - /** |
|
| 66 | - * Creates a new file in the directory |
|
| 67 | - * |
|
| 68 | - * Data will either be supplied as a stream resource, or in certain cases |
|
| 69 | - * as a string. Keep in mind that you may have to support either. |
|
| 70 | - * |
|
| 71 | - * After succesful creation of the file, you may choose to return the ETag |
|
| 72 | - * of the new file here. |
|
| 73 | - * |
|
| 74 | - * The returned ETag must be surrounded by double-quotes (The quotes should |
|
| 75 | - * be part of the actual string). |
|
| 76 | - * |
|
| 77 | - * If you cannot accurately determine the ETag, you should not return it. |
|
| 78 | - * If you don't store the file exactly as-is (you're transforming it |
|
| 79 | - * somehow) you should also not return an ETag. |
|
| 80 | - * |
|
| 81 | - * This means that if a subsequent GET to this new file does not exactly |
|
| 82 | - * return the same contents of what was submitted here, you are strongly |
|
| 83 | - * recommended to omit the ETag. |
|
| 84 | - * |
|
| 85 | - * @param string $name Name of the file |
|
| 86 | - * @param resource|string $data Initial payload |
|
| 87 | - * @return null|string |
|
| 88 | - */ |
|
| 89 | - public function createFile($name, $data = null) { |
|
| 90 | - |
|
| 91 | - throw new Exception\Forbidden('Permission denied to create file (filename ' . $name . ')'); |
|
| 92 | - |
|
| 93 | - } |
|
| 94 | - |
|
| 95 | - /** |
|
| 96 | - * Creates a new subdirectory |
|
| 97 | - * |
|
| 98 | - * @param string $name |
|
| 99 | - * @throws Exception\Forbidden |
|
| 100 | - * @return void |
|
| 101 | - */ |
|
| 102 | - public function createDirectory($name) { |
|
| 103 | - |
|
| 104 | - throw new Exception\Forbidden('Permission denied to create directory'); |
|
| 105 | - |
|
| 106 | - } |
|
| 17 | + /** |
|
| 18 | + * Returns a child object, by its name. |
|
| 19 | + * |
|
| 20 | + * This method makes use of the getChildren method to grab all the child |
|
| 21 | + * nodes, and compares the name. |
|
| 22 | + * Generally its wise to override this, as this can usually be optimized |
|
| 23 | + * |
|
| 24 | + * This method must throw Sabre\DAV\Exception\NotFound if the node does not |
|
| 25 | + * exist. |
|
| 26 | + * |
|
| 27 | + * @param string $name |
|
| 28 | + * @throws Exception\NotFound |
|
| 29 | + * @return INode |
|
| 30 | + */ |
|
| 31 | + public function getChild($name) { |
|
| 32 | + |
|
| 33 | + foreach ($this->getChildren() as $child) { |
|
| 34 | + |
|
| 35 | + if ($child->getName() === $name) return $child; |
|
| 36 | + |
|
| 37 | + } |
|
| 38 | + throw new Exception\NotFound('File not found: ' . $name); |
|
| 39 | + |
|
| 40 | + } |
|
| 41 | + |
|
| 42 | + /** |
|
| 43 | + * Checks is a child-node exists. |
|
| 44 | + * |
|
| 45 | + * It is generally a good idea to try and override this. Usually it can be optimized. |
|
| 46 | + * |
|
| 47 | + * @param string $name |
|
| 48 | + * @return bool |
|
| 49 | + */ |
|
| 50 | + public function childExists($name) { |
|
| 51 | + |
|
| 52 | + try { |
|
| 53 | + |
|
| 54 | + $this->getChild($name); |
|
| 55 | + return true; |
|
| 56 | + |
|
| 57 | + } catch (Exception\NotFound $e) { |
|
| 58 | + |
|
| 59 | + return false; |
|
| 60 | + |
|
| 61 | + } |
|
| 62 | + |
|
| 63 | + } |
|
| 64 | + |
|
| 65 | + /** |
|
| 66 | + * Creates a new file in the directory |
|
| 67 | + * |
|
| 68 | + * Data will either be supplied as a stream resource, or in certain cases |
|
| 69 | + * as a string. Keep in mind that you may have to support either. |
|
| 70 | + * |
|
| 71 | + * After succesful creation of the file, you may choose to return the ETag |
|
| 72 | + * of the new file here. |
|
| 73 | + * |
|
| 74 | + * The returned ETag must be surrounded by double-quotes (The quotes should |
|
| 75 | + * be part of the actual string). |
|
| 76 | + * |
|
| 77 | + * If you cannot accurately determine the ETag, you should not return it. |
|
| 78 | + * If you don't store the file exactly as-is (you're transforming it |
|
| 79 | + * somehow) you should also not return an ETag. |
|
| 80 | + * |
|
| 81 | + * This means that if a subsequent GET to this new file does not exactly |
|
| 82 | + * return the same contents of what was submitted here, you are strongly |
|
| 83 | + * recommended to omit the ETag. |
|
| 84 | + * |
|
| 85 | + * @param string $name Name of the file |
|
| 86 | + * @param resource|string $data Initial payload |
|
| 87 | + * @return null|string |
|
| 88 | + */ |
|
| 89 | + public function createFile($name, $data = null) { |
|
| 90 | + |
|
| 91 | + throw new Exception\Forbidden('Permission denied to create file (filename ' . $name . ')'); |
|
| 92 | + |
|
| 93 | + } |
|
| 94 | + |
|
| 95 | + /** |
|
| 96 | + * Creates a new subdirectory |
|
| 97 | + * |
|
| 98 | + * @param string $name |
|
| 99 | + * @throws Exception\Forbidden |
|
| 100 | + * @return void |
|
| 101 | + */ |
|
| 102 | + public function createDirectory($name) { |
|
| 103 | + |
|
| 104 | + throw new Exception\Forbidden('Permission denied to create directory'); |
|
| 105 | + |
|
| 106 | + } |
|
| 107 | 107 | |
| 108 | 108 | |
| 109 | 109 | } |
@@ -317,7 +317,7 @@ discard block |
||
| 317 | 317 | |
| 318 | 318 | $buttonActions = ''; |
| 319 | 319 | if ($subProps['subNode'] instanceof DAV\IFile) { |
| 320 | - $buttonActions = '<a href="' . $this->escapeHTML($subProps['fullPath']) . '?sabreAction=info"><span class="oi" data-glyph="info"></span></a>'; |
|
| 320 | + $buttonActions = '<a href="' . $this->escapeHTML($subProps['fullPath']) . '?sabreAction=info"><span class="oi" data-glyph="info"></span></a>'; |
|
| 321 | 321 | } |
| 322 | 322 | $this->server->emit('browserButtonActions', [$subProps['fullPath'], $subProps['subNode'], &$buttonActions]); |
| 323 | 323 | |
@@ -450,7 +450,7 @@ discard block |
||
| 450 | 450 | HTML; |
| 451 | 451 | |
| 452 | 452 | // If the path is empty, there's no parent. |
| 453 | - if ($path) { |
|
| 453 | + if ($path) { |
|
| 454 | 454 | list($parentUri) = URLUtil::splitPath($path); |
| 455 | 455 | $fullPath = $this->server->getBaseUri() . URLUtil::encodePath($parentUri); |
| 456 | 456 | $html .= '<a href="' . $fullPath . '" class="btn">⇤ Go to parent</a>'; |
@@ -23,413 +23,413 @@ discard block |
||
| 23 | 23 | */ |
| 24 | 24 | class Plugin extends DAV\ServerPlugin { |
| 25 | 25 | |
| 26 | - /** |
|
| 27 | - * reference to server class |
|
| 28 | - * |
|
| 29 | - * @var Sabre\DAV\Server |
|
| 30 | - */ |
|
| 31 | - protected $server; |
|
| 32 | - |
|
| 33 | - /** |
|
| 34 | - * enablePost turns on the 'actions' panel, which allows people to create |
|
| 35 | - * folders and upload files straight from a browser. |
|
| 36 | - * |
|
| 37 | - * @var bool |
|
| 38 | - */ |
|
| 39 | - protected $enablePost = true; |
|
| 40 | - |
|
| 41 | - /** |
|
| 42 | - * A list of properties that are usually not interesting. This can cut down |
|
| 43 | - * the browser output a bit by removing the properties that most people |
|
| 44 | - * will likely not want to see. |
|
| 45 | - * |
|
| 46 | - * @var array |
|
| 47 | - */ |
|
| 48 | - public $uninterestingProperties = [ |
|
| 49 | - '{DAV:}supportedlock', |
|
| 50 | - '{DAV:}acl-restrictions', |
|
| 51 | - '{DAV:}supported-privilege-set', |
|
| 52 | - '{DAV:}supported-method-set', |
|
| 53 | - ]; |
|
| 54 | - |
|
| 55 | - /** |
|
| 56 | - * Creates the object. |
|
| 57 | - * |
|
| 58 | - * By default it will allow file creation and uploads. |
|
| 59 | - * Specify the first argument as false to disable this |
|
| 60 | - * |
|
| 61 | - * @param bool $enablePost |
|
| 62 | - */ |
|
| 63 | - public function __construct($enablePost = true) { |
|
| 64 | - |
|
| 65 | - $this->enablePost = $enablePost; |
|
| 66 | - |
|
| 67 | - } |
|
| 68 | - |
|
| 69 | - /** |
|
| 70 | - * Initializes the plugin and subscribes to events |
|
| 71 | - * |
|
| 72 | - * @param DAV\Server $server |
|
| 73 | - * @return void |
|
| 74 | - */ |
|
| 75 | - public function initialize(DAV\Server $server) { |
|
| 76 | - |
|
| 77 | - $this->server = $server; |
|
| 78 | - $this->server->on('method:GET', [$this, 'httpGetEarly'], 90); |
|
| 79 | - $this->server->on('method:GET', [$this, 'httpGet'], 200); |
|
| 80 | - $this->server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel'], 200); |
|
| 81 | - if ($this->enablePost) $this->server->on('method:POST', [$this, 'httpPOST']); |
|
| 82 | - } |
|
| 83 | - |
|
| 84 | - /** |
|
| 85 | - * This method intercepts GET requests that have ?sabreAction=info |
|
| 86 | - * appended to the URL |
|
| 87 | - * |
|
| 88 | - * @param RequestInterface $request |
|
| 89 | - * @param ResponseInterface $response |
|
| 90 | - * @return bool |
|
| 91 | - */ |
|
| 92 | - public function httpGetEarly(RequestInterface $request, ResponseInterface $response) { |
|
| 93 | - |
|
| 94 | - $params = $request->getQueryParameters(); |
|
| 95 | - if (isset($params['sabreAction']) && $params['sabreAction'] === 'info') { |
|
| 96 | - return $this->httpGet($request, $response); |
|
| 97 | - } |
|
| 98 | - |
|
| 99 | - } |
|
| 100 | - |
|
| 101 | - /** |
|
| 102 | - * This method intercepts GET requests to collections and returns the html |
|
| 103 | - * |
|
| 104 | - * @param RequestInterface $request |
|
| 105 | - * @param ResponseInterface $response |
|
| 106 | - * @return bool |
|
| 107 | - */ |
|
| 108 | - public function httpGet(RequestInterface $request, ResponseInterface $response) { |
|
| 109 | - |
|
| 110 | - // We're not using straight-up $_GET, because we want everything to be |
|
| 111 | - // unit testable. |
|
| 112 | - $getVars = $request->getQueryParameters(); |
|
| 113 | - |
|
| 114 | - // CSP headers |
|
| 115 | - $this->server->httpResponse->setHeader('Content-Security-Policy', "img-src 'self'; style-src 'self';"); |
|
| 116 | - |
|
| 117 | - $sabreAction = isset($getVars['sabreAction']) ? $getVars['sabreAction'] : null; |
|
| 118 | - |
|
| 119 | - switch ($sabreAction) { |
|
| 120 | - |
|
| 121 | - case 'asset' : |
|
| 122 | - // Asset handling, such as images |
|
| 123 | - $this->serveAsset(isset($getVars['assetName']) ? $getVars['assetName'] : null); |
|
| 124 | - return false; |
|
| 125 | - default : |
|
| 126 | - case 'info' : |
|
| 127 | - try { |
|
| 128 | - $this->server->tree->getNodeForPath($request->getPath()); |
|
| 129 | - } catch (DAV\Exception\NotFound $e) { |
|
| 130 | - // We're simply stopping when the file isn't found to not interfere |
|
| 131 | - // with other plugins. |
|
| 132 | - return; |
|
| 133 | - } |
|
| 134 | - |
|
| 135 | - $response->setStatus(200); |
|
| 136 | - $response->setHeader('Content-Type', 'text/html; charset=utf-8'); |
|
| 137 | - |
|
| 138 | - $response->setBody( |
|
| 139 | - $this->generateDirectoryIndex($request->getPath()) |
|
| 140 | - ); |
|
| 141 | - |
|
| 142 | - return false; |
|
| 143 | - |
|
| 144 | - case 'plugins' : |
|
| 145 | - $response->setStatus(200); |
|
| 146 | - $response->setHeader('Content-Type', 'text/html; charset=utf-8'); |
|
| 147 | - |
|
| 148 | - $response->setBody( |
|
| 149 | - $this->generatePluginListing() |
|
| 150 | - ); |
|
| 151 | - |
|
| 152 | - return false; |
|
| 153 | - |
|
| 154 | - } |
|
| 155 | - |
|
| 156 | - } |
|
| 157 | - |
|
| 158 | - /** |
|
| 159 | - * Handles POST requests for tree operations. |
|
| 160 | - * |
|
| 161 | - * @param RequestInterface $request |
|
| 162 | - * @param ResponseInterface $response |
|
| 163 | - * @return bool |
|
| 164 | - */ |
|
| 165 | - public function httpPOST(RequestInterface $request, ResponseInterface $response) { |
|
| 166 | - |
|
| 167 | - $contentType = $request->getHeader('Content-Type'); |
|
| 168 | - list($contentType) = explode(';', $contentType); |
|
| 169 | - if ($contentType !== 'application/x-www-form-urlencoded' && |
|
| 170 | - $contentType !== 'multipart/form-data') { |
|
| 171 | - return; |
|
| 172 | - } |
|
| 173 | - $postVars = $request->getPostData(); |
|
| 174 | - |
|
| 175 | - if (!isset($postVars['sabreAction'])) |
|
| 176 | - return; |
|
| 177 | - |
|
| 178 | - $uri = $request->getPath(); |
|
| 179 | - |
|
| 180 | - if ($this->server->emit('onBrowserPostAction', [$uri, $postVars['sabreAction'], $postVars])) { |
|
| 181 | - |
|
| 182 | - switch ($postVars['sabreAction']) { |
|
| 183 | - |
|
| 184 | - case 'mkcol' : |
|
| 185 | - if (isset($postVars['name']) && trim($postVars['name'])) { |
|
| 186 | - // Using basename() because we won't allow slashes |
|
| 187 | - list(, $folderName) = URLUtil::splitPath(trim($postVars['name'])); |
|
| 188 | - |
|
| 189 | - if (isset($postVars['resourceType'])) { |
|
| 190 | - $resourceType = explode(',', $postVars['resourceType']); |
|
| 191 | - } else { |
|
| 192 | - $resourceType = ['{DAV:}collection']; |
|
| 193 | - } |
|
| 194 | - |
|
| 195 | - $properties = []; |
|
| 196 | - foreach ($postVars as $varName => $varValue) { |
|
| 197 | - // Any _POST variable in clark notation is treated |
|
| 198 | - // like a property. |
|
| 199 | - if ($varName[0] === '{') { |
|
| 200 | - // PHP will convert any dots to underscores. |
|
| 201 | - // This leaves us with no way to differentiate |
|
| 202 | - // the two. |
|
| 203 | - // Therefore we replace the string *DOT* with a |
|
| 204 | - // real dot. * is not allowed in uris so we |
|
| 205 | - // should be good. |
|
| 206 | - $varName = str_replace('*DOT*', '.', $varName); |
|
| 207 | - $properties[$varName] = $varValue; |
|
| 208 | - } |
|
| 209 | - } |
|
| 210 | - |
|
| 211 | - $mkCol = new MkCol( |
|
| 212 | - $resourceType, |
|
| 213 | - $properties |
|
| 214 | - ); |
|
| 215 | - $this->server->createCollection($uri . '/' . $folderName, $mkCol); |
|
| 216 | - } |
|
| 217 | - break; |
|
| 218 | - |
|
| 219 | - // @codeCoverageIgnoreStart |
|
| 220 | - case 'put' : |
|
| 221 | - |
|
| 222 | - if ($_FILES) $file = current($_FILES); |
|
| 223 | - else break; |
|
| 224 | - |
|
| 225 | - list(, $newName) = URLUtil::splitPath(trim($file['name'])); |
|
| 226 | - if (isset($postVars['name']) && trim($postVars['name'])) |
|
| 227 | - $newName = trim($postVars['name']); |
|
| 228 | - |
|
| 229 | - // Making sure we only have a 'basename' component |
|
| 230 | - list(, $newName) = URLUtil::splitPath($newName); |
|
| 231 | - |
|
| 232 | - if (is_uploaded_file($file['tmp_name'])) { |
|
| 233 | - $this->server->createFile($uri . '/' . $newName, fopen($file['tmp_name'], 'r')); |
|
| 234 | - } |
|
| 235 | - break; |
|
| 236 | - // @codeCoverageIgnoreEnd |
|
| 237 | - |
|
| 238 | - } |
|
| 239 | - |
|
| 240 | - } |
|
| 241 | - $response->setHeader('Location', $request->getUrl()); |
|
| 242 | - $response->setStatus(302); |
|
| 243 | - return false; |
|
| 244 | - |
|
| 245 | - } |
|
| 246 | - |
|
| 247 | - /** |
|
| 248 | - * Escapes a string for html. |
|
| 249 | - * |
|
| 250 | - * @param string $value |
|
| 251 | - * @return string |
|
| 252 | - */ |
|
| 253 | - public function escapeHTML($value) { |
|
| 254 | - |
|
| 255 | - return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); |
|
| 256 | - |
|
| 257 | - } |
|
| 258 | - |
|
| 259 | - /** |
|
| 260 | - * Generates the html directory index for a given url |
|
| 261 | - * |
|
| 262 | - * @param string $path |
|
| 263 | - * @return string |
|
| 264 | - */ |
|
| 265 | - public function generateDirectoryIndex($path) { |
|
| 266 | - |
|
| 267 | - $html = $this->generateHeader($path ? $path : '/', $path); |
|
| 268 | - |
|
| 269 | - $node = $this->server->tree->getNodeForPath($path); |
|
| 270 | - if ($node instanceof DAV\ICollection) { |
|
| 271 | - |
|
| 272 | - $html .= "<section><h1>Nodes</h1>\n"; |
|
| 273 | - $html .= "<table class=\"nodeTable\">"; |
|
| 274 | - |
|
| 275 | - $subNodes = $this->server->getPropertiesForChildren($path, [ |
|
| 276 | - '{DAV:}displayname', |
|
| 277 | - '{DAV:}resourcetype', |
|
| 278 | - '{DAV:}getcontenttype', |
|
| 279 | - '{DAV:}getcontentlength', |
|
| 280 | - '{DAV:}getlastmodified', |
|
| 281 | - ]); |
|
| 282 | - |
|
| 283 | - foreach ($subNodes as $subPath => $subProps) { |
|
| 284 | - |
|
| 285 | - $subNode = $this->server->tree->getNodeForPath($subPath); |
|
| 286 | - $fullPath = $this->server->getBaseUri() . URLUtil::encodePath($subPath); |
|
| 287 | - list(, $displayPath) = URLUtil::splitPath($subPath); |
|
| 288 | - |
|
| 289 | - $subNodes[$subPath]['subNode'] = $subNode; |
|
| 290 | - $subNodes[$subPath]['fullPath'] = $fullPath; |
|
| 291 | - $subNodes[$subPath]['displayPath'] = $displayPath; |
|
| 292 | - } |
|
| 293 | - uasort($subNodes, [$this, 'compareNodes']); |
|
| 294 | - |
|
| 295 | - foreach ($subNodes as $subProps) { |
|
| 296 | - $type = [ |
|
| 297 | - 'string' => 'Unknown', |
|
| 298 | - 'icon' => 'cog', |
|
| 299 | - ]; |
|
| 300 | - if (isset($subProps['{DAV:}resourcetype'])) { |
|
| 301 | - $type = $this->mapResourceType($subProps['{DAV:}resourcetype']->getValue(), $subProps['subNode']); |
|
| 302 | - } |
|
| 303 | - |
|
| 304 | - $html .= '<tr>'; |
|
| 305 | - $html .= '<td class="nameColumn"><a href="' . $this->escapeHTML($subProps['fullPath']) . '"><span class="oi" data-glyph="' . $this->escapeHTML($type['icon']) . '"></span> ' . $this->escapeHTML($subProps['displayPath']) . '</a></td>'; |
|
| 306 | - $html .= '<td class="typeColumn">' . $this->escapeHTML($type['string']) . '</td>'; |
|
| 307 | - $html .= '<td>'; |
|
| 308 | - if (isset($subProps['{DAV:}getcontentlength'])) { |
|
| 309 | - $html .= $this->escapeHTML($subProps['{DAV:}getcontentlength'] . ' bytes'); |
|
| 310 | - } |
|
| 311 | - $html .= '</td><td>'; |
|
| 312 | - if (isset($subProps['{DAV:}getlastmodified'])) { |
|
| 313 | - $lastMod = $subProps['{DAV:}getlastmodified']->getTime(); |
|
| 314 | - $html .= $this->escapeHTML($lastMod->format('F j, Y, g:i a')); |
|
| 315 | - } |
|
| 316 | - $html .= '</td>'; |
|
| 317 | - |
|
| 318 | - $buttonActions = ''; |
|
| 319 | - if ($subProps['subNode'] instanceof DAV\IFile) { |
|
| 320 | - $buttonActions = '<a href="' . $this->escapeHTML($subProps['fullPath']) . '?sabreAction=info"><span class="oi" data-glyph="info"></span></a>'; |
|
| 321 | - } |
|
| 322 | - $this->server->emit('browserButtonActions', [$subProps['fullPath'], $subProps['subNode'], &$buttonActions]); |
|
| 323 | - |
|
| 324 | - $html .= '<td>' . $buttonActions . '</td>'; |
|
| 325 | - $html .= '</tr>'; |
|
| 326 | - } |
|
| 327 | - |
|
| 328 | - $html .= '</table>'; |
|
| 329 | - |
|
| 330 | - } |
|
| 331 | - |
|
| 332 | - $html .= "</section>"; |
|
| 333 | - $html .= "<section><h1>Properties</h1>"; |
|
| 334 | - $html .= "<table class=\"propTable\">"; |
|
| 335 | - |
|
| 336 | - // Allprops request |
|
| 337 | - $propFind = new PropFindAll($path); |
|
| 338 | - $properties = $this->server->getPropertiesByNode($propFind, $node); |
|
| 339 | - |
|
| 340 | - $properties = $propFind->getResultForMultiStatus()[200]; |
|
| 341 | - |
|
| 342 | - foreach ($properties as $propName => $propValue) { |
|
| 343 | - if (!in_array($propName, $this->uninterestingProperties)) { |
|
| 344 | - $html .= $this->drawPropertyRow($propName, $propValue); |
|
| 345 | - } |
|
| 346 | - |
|
| 347 | - } |
|
| 348 | - |
|
| 349 | - |
|
| 350 | - $html .= "</table>"; |
|
| 351 | - $html .= "</section>"; |
|
| 352 | - |
|
| 353 | - /* Start of generating actions */ |
|
| 354 | - |
|
| 355 | - $output = ''; |
|
| 356 | - if ($this->enablePost) { |
|
| 357 | - $this->server->emit('onHTMLActionsPanel', [$node, &$output]); |
|
| 358 | - } |
|
| 359 | - |
|
| 360 | - if ($output) { |
|
| 361 | - |
|
| 362 | - $html .= "<section><h1>Actions</h1>"; |
|
| 363 | - $html .= "<div class=\"actions\">\n"; |
|
| 364 | - $html .= $output; |
|
| 365 | - $html .= "</div>\n"; |
|
| 366 | - $html .= "</section>\n"; |
|
| 367 | - } |
|
| 368 | - |
|
| 369 | - $html .= $this->generateFooter(); |
|
| 370 | - |
|
| 371 | - $this->server->httpResponse->setHeader('Content-Security-Policy', "img-src 'self'; style-src 'self';"); |
|
| 372 | - |
|
| 373 | - return $html; |
|
| 374 | - |
|
| 375 | - } |
|
| 376 | - |
|
| 377 | - /** |
|
| 378 | - * Generates the 'plugins' page. |
|
| 379 | - * |
|
| 380 | - * @return string |
|
| 381 | - */ |
|
| 382 | - public function generatePluginListing() { |
|
| 383 | - |
|
| 384 | - $html = $this->generateHeader('Plugins'); |
|
| 385 | - |
|
| 386 | - $html .= "<section><h1>Plugins</h1>"; |
|
| 387 | - $html .= "<table class=\"propTable\">"; |
|
| 388 | - foreach ($this->server->getPlugins() as $plugin) { |
|
| 389 | - $info = $plugin->getPluginInfo(); |
|
| 390 | - $html .= '<tr><th>' . $info['name'] . '</th>'; |
|
| 391 | - $html .= '<td>' . $info['description'] . '</td>'; |
|
| 392 | - $html .= '<td>'; |
|
| 393 | - if (isset($info['link']) && $info['link']) { |
|
| 394 | - $html .= '<a href="' . $this->escapeHTML($info['link']) . '"><span class="oi" data-glyph="book"></span></a>'; |
|
| 395 | - } |
|
| 396 | - $html .= '</td></tr>'; |
|
| 397 | - } |
|
| 398 | - $html .= "</table>"; |
|
| 399 | - $html .= "</section>"; |
|
| 400 | - |
|
| 401 | - /* Start of generating actions */ |
|
| 402 | - |
|
| 403 | - $html .= $this->generateFooter(); |
|
| 404 | - |
|
| 405 | - return $html; |
|
| 406 | - |
|
| 407 | - } |
|
| 408 | - |
|
| 409 | - /** |
|
| 410 | - * Generates the first block of HTML, including the <head> tag and page |
|
| 411 | - * header. |
|
| 412 | - * |
|
| 413 | - * Returns footer. |
|
| 414 | - * |
|
| 415 | - * @param string $title |
|
| 416 | - * @param string $path |
|
| 417 | - * @return void |
|
| 418 | - */ |
|
| 419 | - public function generateHeader($title, $path = null) { |
|
| 26 | + /** |
|
| 27 | + * reference to server class |
|
| 28 | + * |
|
| 29 | + * @var Sabre\DAV\Server |
|
| 30 | + */ |
|
| 31 | + protected $server; |
|
| 32 | + |
|
| 33 | + /** |
|
| 34 | + * enablePost turns on the 'actions' panel, which allows people to create |
|
| 35 | + * folders and upload files straight from a browser. |
|
| 36 | + * |
|
| 37 | + * @var bool |
|
| 38 | + */ |
|
| 39 | + protected $enablePost = true; |
|
| 40 | + |
|
| 41 | + /** |
|
| 42 | + * A list of properties that are usually not interesting. This can cut down |
|
| 43 | + * the browser output a bit by removing the properties that most people |
|
| 44 | + * will likely not want to see. |
|
| 45 | + * |
|
| 46 | + * @var array |
|
| 47 | + */ |
|
| 48 | + public $uninterestingProperties = [ |
|
| 49 | + '{DAV:}supportedlock', |
|
| 50 | + '{DAV:}acl-restrictions', |
|
| 51 | + '{DAV:}supported-privilege-set', |
|
| 52 | + '{DAV:}supported-method-set', |
|
| 53 | + ]; |
|
| 54 | + |
|
| 55 | + /** |
|
| 56 | + * Creates the object. |
|
| 57 | + * |
|
| 58 | + * By default it will allow file creation and uploads. |
|
| 59 | + * Specify the first argument as false to disable this |
|
| 60 | + * |
|
| 61 | + * @param bool $enablePost |
|
| 62 | + */ |
|
| 63 | + public function __construct($enablePost = true) { |
|
| 64 | + |
|
| 65 | + $this->enablePost = $enablePost; |
|
| 66 | + |
|
| 67 | + } |
|
| 68 | + |
|
| 69 | + /** |
|
| 70 | + * Initializes the plugin and subscribes to events |
|
| 71 | + * |
|
| 72 | + * @param DAV\Server $server |
|
| 73 | + * @return void |
|
| 74 | + */ |
|
| 75 | + public function initialize(DAV\Server $server) { |
|
| 76 | + |
|
| 77 | + $this->server = $server; |
|
| 78 | + $this->server->on('method:GET', [$this, 'httpGetEarly'], 90); |
|
| 79 | + $this->server->on('method:GET', [$this, 'httpGet'], 200); |
|
| 80 | + $this->server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel'], 200); |
|
| 81 | + if ($this->enablePost) $this->server->on('method:POST', [$this, 'httpPOST']); |
|
| 82 | + } |
|
| 83 | + |
|
| 84 | + /** |
|
| 85 | + * This method intercepts GET requests that have ?sabreAction=info |
|
| 86 | + * appended to the URL |
|
| 87 | + * |
|
| 88 | + * @param RequestInterface $request |
|
| 89 | + * @param ResponseInterface $response |
|
| 90 | + * @return bool |
|
| 91 | + */ |
|
| 92 | + public function httpGetEarly(RequestInterface $request, ResponseInterface $response) { |
|
| 93 | + |
|
| 94 | + $params = $request->getQueryParameters(); |
|
| 95 | + if (isset($params['sabreAction']) && $params['sabreAction'] === 'info') { |
|
| 96 | + return $this->httpGet($request, $response); |
|
| 97 | + } |
|
| 98 | + |
|
| 99 | + } |
|
| 100 | + |
|
| 101 | + /** |
|
| 102 | + * This method intercepts GET requests to collections and returns the html |
|
| 103 | + * |
|
| 104 | + * @param RequestInterface $request |
|
| 105 | + * @param ResponseInterface $response |
|
| 106 | + * @return bool |
|
| 107 | + */ |
|
| 108 | + public function httpGet(RequestInterface $request, ResponseInterface $response) { |
|
| 109 | + |
|
| 110 | + // We're not using straight-up $_GET, because we want everything to be |
|
| 111 | + // unit testable. |
|
| 112 | + $getVars = $request->getQueryParameters(); |
|
| 113 | + |
|
| 114 | + // CSP headers |
|
| 115 | + $this->server->httpResponse->setHeader('Content-Security-Policy', "img-src 'self'; style-src 'self';"); |
|
| 116 | + |
|
| 117 | + $sabreAction = isset($getVars['sabreAction']) ? $getVars['sabreAction'] : null; |
|
| 118 | + |
|
| 119 | + switch ($sabreAction) { |
|
| 120 | + |
|
| 121 | + case 'asset' : |
|
| 122 | + // Asset handling, such as images |
|
| 123 | + $this->serveAsset(isset($getVars['assetName']) ? $getVars['assetName'] : null); |
|
| 124 | + return false; |
|
| 125 | + default : |
|
| 126 | + case 'info' : |
|
| 127 | + try { |
|
| 128 | + $this->server->tree->getNodeForPath($request->getPath()); |
|
| 129 | + } catch (DAV\Exception\NotFound $e) { |
|
| 130 | + // We're simply stopping when the file isn't found to not interfere |
|
| 131 | + // with other plugins. |
|
| 132 | + return; |
|
| 133 | + } |
|
| 134 | + |
|
| 135 | + $response->setStatus(200); |
|
| 136 | + $response->setHeader('Content-Type', 'text/html; charset=utf-8'); |
|
| 137 | + |
|
| 138 | + $response->setBody( |
|
| 139 | + $this->generateDirectoryIndex($request->getPath()) |
|
| 140 | + ); |
|
| 141 | + |
|
| 142 | + return false; |
|
| 143 | + |
|
| 144 | + case 'plugins' : |
|
| 145 | + $response->setStatus(200); |
|
| 146 | + $response->setHeader('Content-Type', 'text/html; charset=utf-8'); |
|
| 147 | + |
|
| 148 | + $response->setBody( |
|
| 149 | + $this->generatePluginListing() |
|
| 150 | + ); |
|
| 151 | + |
|
| 152 | + return false; |
|
| 153 | + |
|
| 154 | + } |
|
| 155 | + |
|
| 156 | + } |
|
| 157 | + |
|
| 158 | + /** |
|
| 159 | + * Handles POST requests for tree operations. |
|
| 160 | + * |
|
| 161 | + * @param RequestInterface $request |
|
| 162 | + * @param ResponseInterface $response |
|
| 163 | + * @return bool |
|
| 164 | + */ |
|
| 165 | + public function httpPOST(RequestInterface $request, ResponseInterface $response) { |
|
| 166 | + |
|
| 167 | + $contentType = $request->getHeader('Content-Type'); |
|
| 168 | + list($contentType) = explode(';', $contentType); |
|
| 169 | + if ($contentType !== 'application/x-www-form-urlencoded' && |
|
| 170 | + $contentType !== 'multipart/form-data') { |
|
| 171 | + return; |
|
| 172 | + } |
|
| 173 | + $postVars = $request->getPostData(); |
|
| 174 | + |
|
| 175 | + if (!isset($postVars['sabreAction'])) |
|
| 176 | + return; |
|
| 177 | + |
|
| 178 | + $uri = $request->getPath(); |
|
| 179 | + |
|
| 180 | + if ($this->server->emit('onBrowserPostAction', [$uri, $postVars['sabreAction'], $postVars])) { |
|
| 181 | + |
|
| 182 | + switch ($postVars['sabreAction']) { |
|
| 183 | + |
|
| 184 | + case 'mkcol' : |
|
| 185 | + if (isset($postVars['name']) && trim($postVars['name'])) { |
|
| 186 | + // Using basename() because we won't allow slashes |
|
| 187 | + list(, $folderName) = URLUtil::splitPath(trim($postVars['name'])); |
|
| 188 | + |
|
| 189 | + if (isset($postVars['resourceType'])) { |
|
| 190 | + $resourceType = explode(',', $postVars['resourceType']); |
|
| 191 | + } else { |
|
| 192 | + $resourceType = ['{DAV:}collection']; |
|
| 193 | + } |
|
| 194 | + |
|
| 195 | + $properties = []; |
|
| 196 | + foreach ($postVars as $varName => $varValue) { |
|
| 197 | + // Any _POST variable in clark notation is treated |
|
| 198 | + // like a property. |
|
| 199 | + if ($varName[0] === '{') { |
|
| 200 | + // PHP will convert any dots to underscores. |
|
| 201 | + // This leaves us with no way to differentiate |
|
| 202 | + // the two. |
|
| 203 | + // Therefore we replace the string *DOT* with a |
|
| 204 | + // real dot. * is not allowed in uris so we |
|
| 205 | + // should be good. |
|
| 206 | + $varName = str_replace('*DOT*', '.', $varName); |
|
| 207 | + $properties[$varName] = $varValue; |
|
| 208 | + } |
|
| 209 | + } |
|
| 210 | + |
|
| 211 | + $mkCol = new MkCol( |
|
| 212 | + $resourceType, |
|
| 213 | + $properties |
|
| 214 | + ); |
|
| 215 | + $this->server->createCollection($uri . '/' . $folderName, $mkCol); |
|
| 216 | + } |
|
| 217 | + break; |
|
| 218 | + |
|
| 219 | + // @codeCoverageIgnoreStart |
|
| 220 | + case 'put' : |
|
| 221 | + |
|
| 222 | + if ($_FILES) $file = current($_FILES); |
|
| 223 | + else break; |
|
| 224 | + |
|
| 225 | + list(, $newName) = URLUtil::splitPath(trim($file['name'])); |
|
| 226 | + if (isset($postVars['name']) && trim($postVars['name'])) |
|
| 227 | + $newName = trim($postVars['name']); |
|
| 228 | + |
|
| 229 | + // Making sure we only have a 'basename' component |
|
| 230 | + list(, $newName) = URLUtil::splitPath($newName); |
|
| 231 | + |
|
| 232 | + if (is_uploaded_file($file['tmp_name'])) { |
|
| 233 | + $this->server->createFile($uri . '/' . $newName, fopen($file['tmp_name'], 'r')); |
|
| 234 | + } |
|
| 235 | + break; |
|
| 236 | + // @codeCoverageIgnoreEnd |
|
| 237 | + |
|
| 238 | + } |
|
| 239 | + |
|
| 240 | + } |
|
| 241 | + $response->setHeader('Location', $request->getUrl()); |
|
| 242 | + $response->setStatus(302); |
|
| 243 | + return false; |
|
| 244 | + |
|
| 245 | + } |
|
| 246 | + |
|
| 247 | + /** |
|
| 248 | + * Escapes a string for html. |
|
| 249 | + * |
|
| 250 | + * @param string $value |
|
| 251 | + * @return string |
|
| 252 | + */ |
|
| 253 | + public function escapeHTML($value) { |
|
| 254 | + |
|
| 255 | + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); |
|
| 256 | + |
|
| 257 | + } |
|
| 258 | + |
|
| 259 | + /** |
|
| 260 | + * Generates the html directory index for a given url |
|
| 261 | + * |
|
| 262 | + * @param string $path |
|
| 263 | + * @return string |
|
| 264 | + */ |
|
| 265 | + public function generateDirectoryIndex($path) { |
|
| 266 | + |
|
| 267 | + $html = $this->generateHeader($path ? $path : '/', $path); |
|
| 268 | + |
|
| 269 | + $node = $this->server->tree->getNodeForPath($path); |
|
| 270 | + if ($node instanceof DAV\ICollection) { |
|
| 271 | + |
|
| 272 | + $html .= "<section><h1>Nodes</h1>\n"; |
|
| 273 | + $html .= "<table class=\"nodeTable\">"; |
|
| 274 | + |
|
| 275 | + $subNodes = $this->server->getPropertiesForChildren($path, [ |
|
| 276 | + '{DAV:}displayname', |
|
| 277 | + '{DAV:}resourcetype', |
|
| 278 | + '{DAV:}getcontenttype', |
|
| 279 | + '{DAV:}getcontentlength', |
|
| 280 | + '{DAV:}getlastmodified', |
|
| 281 | + ]); |
|
| 282 | + |
|
| 283 | + foreach ($subNodes as $subPath => $subProps) { |
|
| 284 | + |
|
| 285 | + $subNode = $this->server->tree->getNodeForPath($subPath); |
|
| 286 | + $fullPath = $this->server->getBaseUri() . URLUtil::encodePath($subPath); |
|
| 287 | + list(, $displayPath) = URLUtil::splitPath($subPath); |
|
| 288 | + |
|
| 289 | + $subNodes[$subPath]['subNode'] = $subNode; |
|
| 290 | + $subNodes[$subPath]['fullPath'] = $fullPath; |
|
| 291 | + $subNodes[$subPath]['displayPath'] = $displayPath; |
|
| 292 | + } |
|
| 293 | + uasort($subNodes, [$this, 'compareNodes']); |
|
| 294 | + |
|
| 295 | + foreach ($subNodes as $subProps) { |
|
| 296 | + $type = [ |
|
| 297 | + 'string' => 'Unknown', |
|
| 298 | + 'icon' => 'cog', |
|
| 299 | + ]; |
|
| 300 | + if (isset($subProps['{DAV:}resourcetype'])) { |
|
| 301 | + $type = $this->mapResourceType($subProps['{DAV:}resourcetype']->getValue(), $subProps['subNode']); |
|
| 302 | + } |
|
| 303 | + |
|
| 304 | + $html .= '<tr>'; |
|
| 305 | + $html .= '<td class="nameColumn"><a href="' . $this->escapeHTML($subProps['fullPath']) . '"><span class="oi" data-glyph="' . $this->escapeHTML($type['icon']) . '"></span> ' . $this->escapeHTML($subProps['displayPath']) . '</a></td>'; |
|
| 306 | + $html .= '<td class="typeColumn">' . $this->escapeHTML($type['string']) . '</td>'; |
|
| 307 | + $html .= '<td>'; |
|
| 308 | + if (isset($subProps['{DAV:}getcontentlength'])) { |
|
| 309 | + $html .= $this->escapeHTML($subProps['{DAV:}getcontentlength'] . ' bytes'); |
|
| 310 | + } |
|
| 311 | + $html .= '</td><td>'; |
|
| 312 | + if (isset($subProps['{DAV:}getlastmodified'])) { |
|
| 313 | + $lastMod = $subProps['{DAV:}getlastmodified']->getTime(); |
|
| 314 | + $html .= $this->escapeHTML($lastMod->format('F j, Y, g:i a')); |
|
| 315 | + } |
|
| 316 | + $html .= '</td>'; |
|
| 317 | + |
|
| 318 | + $buttonActions = ''; |
|
| 319 | + if ($subProps['subNode'] instanceof DAV\IFile) { |
|
| 320 | + $buttonActions = '<a href="' . $this->escapeHTML($subProps['fullPath']) . '?sabreAction=info"><span class="oi" data-glyph="info"></span></a>'; |
|
| 321 | + } |
|
| 322 | + $this->server->emit('browserButtonActions', [$subProps['fullPath'], $subProps['subNode'], &$buttonActions]); |
|
| 323 | + |
|
| 324 | + $html .= '<td>' . $buttonActions . '</td>'; |
|
| 325 | + $html .= '</tr>'; |
|
| 326 | + } |
|
| 327 | + |
|
| 328 | + $html .= '</table>'; |
|
| 329 | + |
|
| 330 | + } |
|
| 331 | + |
|
| 332 | + $html .= "</section>"; |
|
| 333 | + $html .= "<section><h1>Properties</h1>"; |
|
| 334 | + $html .= "<table class=\"propTable\">"; |
|
| 335 | + |
|
| 336 | + // Allprops request |
|
| 337 | + $propFind = new PropFindAll($path); |
|
| 338 | + $properties = $this->server->getPropertiesByNode($propFind, $node); |
|
| 339 | + |
|
| 340 | + $properties = $propFind->getResultForMultiStatus()[200]; |
|
| 341 | + |
|
| 342 | + foreach ($properties as $propName => $propValue) { |
|
| 343 | + if (!in_array($propName, $this->uninterestingProperties)) { |
|
| 344 | + $html .= $this->drawPropertyRow($propName, $propValue); |
|
| 345 | + } |
|
| 346 | + |
|
| 347 | + } |
|
| 348 | + |
|
| 349 | + |
|
| 350 | + $html .= "</table>"; |
|
| 351 | + $html .= "</section>"; |
|
| 352 | + |
|
| 353 | + /* Start of generating actions */ |
|
| 354 | + |
|
| 355 | + $output = ''; |
|
| 356 | + if ($this->enablePost) { |
|
| 357 | + $this->server->emit('onHTMLActionsPanel', [$node, &$output]); |
|
| 358 | + } |
|
| 359 | + |
|
| 360 | + if ($output) { |
|
| 361 | + |
|
| 362 | + $html .= "<section><h1>Actions</h1>"; |
|
| 363 | + $html .= "<div class=\"actions\">\n"; |
|
| 364 | + $html .= $output; |
|
| 365 | + $html .= "</div>\n"; |
|
| 366 | + $html .= "</section>\n"; |
|
| 367 | + } |
|
| 368 | + |
|
| 369 | + $html .= $this->generateFooter(); |
|
| 370 | + |
|
| 371 | + $this->server->httpResponse->setHeader('Content-Security-Policy', "img-src 'self'; style-src 'self';"); |
|
| 372 | + |
|
| 373 | + return $html; |
|
| 374 | + |
|
| 375 | + } |
|
| 376 | + |
|
| 377 | + /** |
|
| 378 | + * Generates the 'plugins' page. |
|
| 379 | + * |
|
| 380 | + * @return string |
|
| 381 | + */ |
|
| 382 | + public function generatePluginListing() { |
|
| 383 | + |
|
| 384 | + $html = $this->generateHeader('Plugins'); |
|
| 385 | + |
|
| 386 | + $html .= "<section><h1>Plugins</h1>"; |
|
| 387 | + $html .= "<table class=\"propTable\">"; |
|
| 388 | + foreach ($this->server->getPlugins() as $plugin) { |
|
| 389 | + $info = $plugin->getPluginInfo(); |
|
| 390 | + $html .= '<tr><th>' . $info['name'] . '</th>'; |
|
| 391 | + $html .= '<td>' . $info['description'] . '</td>'; |
|
| 392 | + $html .= '<td>'; |
|
| 393 | + if (isset($info['link']) && $info['link']) { |
|
| 394 | + $html .= '<a href="' . $this->escapeHTML($info['link']) . '"><span class="oi" data-glyph="book"></span></a>'; |
|
| 395 | + } |
|
| 396 | + $html .= '</td></tr>'; |
|
| 397 | + } |
|
| 398 | + $html .= "</table>"; |
|
| 399 | + $html .= "</section>"; |
|
| 400 | + |
|
| 401 | + /* Start of generating actions */ |
|
| 402 | + |
|
| 403 | + $html .= $this->generateFooter(); |
|
| 404 | + |
|
| 405 | + return $html; |
|
| 406 | + |
|
| 407 | + } |
|
| 408 | + |
|
| 409 | + /** |
|
| 410 | + * Generates the first block of HTML, including the <head> tag and page |
|
| 411 | + * header. |
|
| 412 | + * |
|
| 413 | + * Returns footer. |
|
| 414 | + * |
|
| 415 | + * @param string $title |
|
| 416 | + * @param string $path |
|
| 417 | + * @return void |
|
| 418 | + */ |
|
| 419 | + public function generateHeader($title, $path = null) { |
|
| 420 | 420 | |
| 421 | - $version = DAV\Version::VERSION; |
|
| 421 | + $version = DAV\Version::VERSION; |
|
| 422 | 422 | |
| 423 | - $vars = [ |
|
| 424 | - 'title' => $this->escapeHTML($title), |
|
| 425 | - 'favicon' => $this->escapeHTML($this->getAssetUrl('favicon.ico')), |
|
| 426 | - 'style' => $this->escapeHTML($this->getAssetUrl('sabredav.css')), |
|
| 427 | - 'iconstyle' => $this->escapeHTML($this->getAssetUrl('openiconic/open-iconic.css')), |
|
| 428 | - 'logo' => $this->escapeHTML($this->getAssetUrl('sabredav.png')), |
|
| 429 | - 'baseUrl' => $this->server->getBaseUri(), |
|
| 430 | - ]; |
|
| 423 | + $vars = [ |
|
| 424 | + 'title' => $this->escapeHTML($title), |
|
| 425 | + 'favicon' => $this->escapeHTML($this->getAssetUrl('favicon.ico')), |
|
| 426 | + 'style' => $this->escapeHTML($this->getAssetUrl('sabredav.css')), |
|
| 427 | + 'iconstyle' => $this->escapeHTML($this->getAssetUrl('openiconic/open-iconic.css')), |
|
| 428 | + 'logo' => $this->escapeHTML($this->getAssetUrl('sabredav.png')), |
|
| 429 | + 'baseUrl' => $this->server->getBaseUri(), |
|
| 430 | + ]; |
|
| 431 | 431 | |
| 432 | - $html = <<<HTML |
|
| 432 | + $html = <<<HTML |
|
| 433 | 433 | <!DOCTYPE html> |
| 434 | 434 | <html> |
| 435 | 435 | <head> |
@@ -449,64 +449,64 @@ discard block |
||
| 449 | 449 | <nav> |
| 450 | 450 | HTML; |
| 451 | 451 | |
| 452 | - // If the path is empty, there's no parent. |
|
| 453 | - if ($path) { |
|
| 454 | - list($parentUri) = URLUtil::splitPath($path); |
|
| 455 | - $fullPath = $this->server->getBaseUri() . URLUtil::encodePath($parentUri); |
|
| 456 | - $html .= '<a href="' . $fullPath . '" class="btn">⇤ Go to parent</a>'; |
|
| 457 | - } else { |
|
| 458 | - $html .= '<span class="btn disabled">⇤ Go to parent</span>'; |
|
| 459 | - } |
|
| 452 | + // If the path is empty, there's no parent. |
|
| 453 | + if ($path) { |
|
| 454 | + list($parentUri) = URLUtil::splitPath($path); |
|
| 455 | + $fullPath = $this->server->getBaseUri() . URLUtil::encodePath($parentUri); |
|
| 456 | + $html .= '<a href="' . $fullPath . '" class="btn">⇤ Go to parent</a>'; |
|
| 457 | + } else { |
|
| 458 | + $html .= '<span class="btn disabled">⇤ Go to parent</span>'; |
|
| 459 | + } |
|
| 460 | 460 | |
| 461 | - $html .= ' <a href="?sabreAction=plugins" class="btn"><span class="oi" data-glyph="puzzle-piece"></span> Plugins</a>'; |
|
| 461 | + $html .= ' <a href="?sabreAction=plugins" class="btn"><span class="oi" data-glyph="puzzle-piece"></span> Plugins</a>'; |
|
| 462 | 462 | |
| 463 | - $html .= "</nav>"; |
|
| 463 | + $html .= "</nav>"; |
|
| 464 | 464 | |
| 465 | - return $html; |
|
| 465 | + return $html; |
|
| 466 | 466 | |
| 467 | - } |
|
| 467 | + } |
|
| 468 | 468 | |
| 469 | - /** |
|
| 470 | - * Generates the page footer. |
|
| 471 | - * |
|
| 472 | - * Returns html. |
|
| 473 | - * |
|
| 474 | - * @return string |
|
| 475 | - */ |
|
| 476 | - public function generateFooter() { |
|
| 469 | + /** |
|
| 470 | + * Generates the page footer. |
|
| 471 | + * |
|
| 472 | + * Returns html. |
|
| 473 | + * |
|
| 474 | + * @return string |
|
| 475 | + */ |
|
| 476 | + public function generateFooter() { |
|
| 477 | 477 | |
| 478 | - $version = DAV\Version::VERSION; |
|
| 479 | - return <<<HTML |
|
| 478 | + $version = DAV\Version::VERSION; |
|
| 479 | + return <<<HTML |
|
| 480 | 480 | <footer>Generated by SabreDAV $version (c)2007-2015 <a href="http://sabre.io/">http://sabre.io/</a></footer> |
| 481 | 481 | </body> |
| 482 | 482 | </html> |
| 483 | 483 | HTML; |
| 484 | 484 | |
| 485 | - } |
|
| 486 | - |
|
| 487 | - /** |
|
| 488 | - * This method is used to generate the 'actions panel' output for |
|
| 489 | - * collections. |
|
| 490 | - * |
|
| 491 | - * This specifically generates the interfaces for creating new files, and |
|
| 492 | - * creating new directories. |
|
| 493 | - * |
|
| 494 | - * @param DAV\INode $node |
|
| 495 | - * @param mixed $output |
|
| 496 | - * @return void |
|
| 497 | - */ |
|
| 498 | - public function htmlActionsPanel(DAV\INode $node, &$output) { |
|
| 499 | - |
|
| 500 | - if (!$node instanceof DAV\ICollection) |
|
| 501 | - return; |
|
| 502 | - |
|
| 503 | - // We also know fairly certain that if an object is a non-extended |
|
| 504 | - // SimpleCollection, we won't need to show the panel either. |
|
| 505 | - if (get_class($node) === 'Sabre\\DAV\\SimpleCollection') |
|
| 506 | - return; |
|
| 507 | - |
|
| 508 | - ob_start(); |
|
| 509 | - echo '<form method="post" action=""> |
|
| 485 | + } |
|
| 486 | + |
|
| 487 | + /** |
|
| 488 | + * This method is used to generate the 'actions panel' output for |
|
| 489 | + * collections. |
|
| 490 | + * |
|
| 491 | + * This specifically generates the interfaces for creating new files, and |
|
| 492 | + * creating new directories. |
|
| 493 | + * |
|
| 494 | + * @param DAV\INode $node |
|
| 495 | + * @param mixed $output |
|
| 496 | + * @return void |
|
| 497 | + */ |
|
| 498 | + public function htmlActionsPanel(DAV\INode $node, &$output) { |
|
| 499 | + |
|
| 500 | + if (!$node instanceof DAV\ICollection) |
|
| 501 | + return; |
|
| 502 | + |
|
| 503 | + // We also know fairly certain that if an object is a non-extended |
|
| 504 | + // SimpleCollection, we won't need to show the panel either. |
|
| 505 | + if (get_class($node) === 'Sabre\\DAV\\SimpleCollection') |
|
| 506 | + return; |
|
| 507 | + |
|
| 508 | + ob_start(); |
|
| 509 | + echo '<form method="post" action=""> |
|
| 510 | 510 | <h3>Create new folder</h3> |
| 511 | 511 | <input type="hidden" name="sabreAction" value="mkcol" /> |
| 512 | 512 | <label>Name:</label> <input type="text" name="name" /><br /> |
@@ -521,277 +521,277 @@ discard block |
||
| 521 | 521 | </form> |
| 522 | 522 | '; |
| 523 | 523 | |
| 524 | - $output .= ob_get_clean(); |
|
| 525 | - |
|
| 526 | - } |
|
| 527 | - |
|
| 528 | - /** |
|
| 529 | - * This method takes a path/name of an asset and turns it into url |
|
| 530 | - * suiteable for http access. |
|
| 531 | - * |
|
| 532 | - * @param string $assetName |
|
| 533 | - * @return string |
|
| 534 | - */ |
|
| 535 | - protected function getAssetUrl($assetName) { |
|
| 536 | - |
|
| 537 | - return $this->server->getBaseUri() . '?sabreAction=asset&assetName=' . urlencode($assetName); |
|
| 538 | - |
|
| 539 | - } |
|
| 540 | - |
|
| 541 | - /** |
|
| 542 | - * This method returns a local pathname to an asset. |
|
| 543 | - * |
|
| 544 | - * @param string $assetName |
|
| 545 | - * @return string |
|
| 546 | - * @throws DAV\Exception\NotFound |
|
| 547 | - */ |
|
| 548 | - protected function getLocalAssetPath($assetName) { |
|
| 549 | - |
|
| 550 | - $assetDir = __DIR__ . '/assets/'; |
|
| 551 | - $path = $assetDir . $assetName; |
|
| 552 | - |
|
| 553 | - // Making sure people aren't trying to escape from the base path. |
|
| 554 | - $path = str_replace('\\', '/', $path); |
|
| 555 | - if (strpos($path, '/../') !== false || strrchr($path, '/') === '/..') { |
|
| 556 | - throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); |
|
| 557 | - } |
|
| 558 | - if (strpos(realpath($path), realpath($assetDir)) === 0 && file_exists($path)) { |
|
| 559 | - return $path; |
|
| 560 | - } |
|
| 561 | - throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); |
|
| 562 | - } |
|
| 563 | - |
|
| 564 | - /** |
|
| 565 | - * This method reads an asset from disk and generates a full http response. |
|
| 566 | - * |
|
| 567 | - * @param string $assetName |
|
| 568 | - * @return void |
|
| 569 | - */ |
|
| 570 | - protected function serveAsset($assetName) { |
|
| 571 | - |
|
| 572 | - $assetPath = $this->getLocalAssetPath($assetName); |
|
| 573 | - |
|
| 574 | - // Rudimentary mime type detection |
|
| 575 | - $mime = 'application/octet-stream'; |
|
| 576 | - $map = [ |
|
| 577 | - 'ico' => 'image/vnd.microsoft.icon', |
|
| 578 | - 'png' => 'image/png', |
|
| 579 | - 'css' => 'text/css', |
|
| 580 | - ]; |
|
| 581 | - |
|
| 582 | - $ext = substr($assetName, strrpos($assetName, '.') + 1); |
|
| 583 | - if (isset($map[$ext])) { |
|
| 584 | - $mime = $map[$ext]; |
|
| 585 | - } |
|
| 586 | - |
|
| 587 | - $this->server->httpResponse->setHeader('Content-Type', $mime); |
|
| 588 | - $this->server->httpResponse->setHeader('Content-Length', filesize($assetPath)); |
|
| 589 | - $this->server->httpResponse->setHeader('Cache-Control', 'public, max-age=1209600'); |
|
| 590 | - $this->server->httpResponse->setStatus(200); |
|
| 591 | - $this->server->httpResponse->setBody(fopen($assetPath, 'r')); |
|
| 592 | - |
|
| 593 | - } |
|
| 594 | - |
|
| 595 | - /** |
|
| 596 | - * Sort helper function: compares two directory entries based on type and |
|
| 597 | - * display name. Collections sort above other types. |
|
| 598 | - * |
|
| 599 | - * @param array $a |
|
| 600 | - * @param array $b |
|
| 601 | - * @return int |
|
| 602 | - */ |
|
| 603 | - protected function compareNodes($a, $b) { |
|
| 604 | - |
|
| 605 | - $typeA = (isset($a['{DAV:}resourcetype'])) |
|
| 606 | - ? (in_array('{DAV:}collection', $a['{DAV:}resourcetype']->getValue())) |
|
| 607 | - : false; |
|
| 608 | - |
|
| 609 | - $typeB = (isset($b['{DAV:}resourcetype'])) |
|
| 610 | - ? (in_array('{DAV:}collection', $b['{DAV:}resourcetype']->getValue())) |
|
| 611 | - : false; |
|
| 612 | - |
|
| 613 | - // If same type, sort alphabetically by filename: |
|
| 614 | - if ($typeA === $typeB) { |
|
| 615 | - return strnatcasecmp($a['displayPath'], $b['displayPath']); |
|
| 616 | - } |
|
| 617 | - return (($typeA < $typeB) ? 1 : -1); |
|
| 618 | - |
|
| 619 | - } |
|
| 620 | - |
|
| 621 | - /** |
|
| 622 | - * Maps a resource type to a human-readable string and icon. |
|
| 623 | - * |
|
| 624 | - * @param array $resourceTypes |
|
| 625 | - * @param INode $node |
|
| 626 | - * @return array |
|
| 627 | - */ |
|
| 628 | - private function mapResourceType(array $resourceTypes, $node) { |
|
| 629 | - |
|
| 630 | - if (!$resourceTypes) { |
|
| 631 | - if ($node instanceof DAV\IFile) { |
|
| 632 | - return [ |
|
| 633 | - 'string' => 'File', |
|
| 634 | - 'icon' => 'file', |
|
| 635 | - ]; |
|
| 636 | - } else { |
|
| 637 | - return [ |
|
| 638 | - 'string' => 'Unknown', |
|
| 639 | - 'icon' => 'cog', |
|
| 640 | - ]; |
|
| 641 | - } |
|
| 642 | - } |
|
| 643 | - |
|
| 644 | - $types = [ |
|
| 645 | - '{http://calendarserver.org/ns/}calendar-proxy-write' => [ |
|
| 646 | - 'string' => 'Proxy-Write', |
|
| 647 | - 'icon' => 'people', |
|
| 648 | - ], |
|
| 649 | - '{http://calendarserver.org/ns/}calendar-proxy-read' => [ |
|
| 650 | - 'string' => 'Proxy-Read', |
|
| 651 | - 'icon' => 'people', |
|
| 652 | - ], |
|
| 653 | - '{urn:ietf:params:xml:ns:caldav}schedule-outbox' => [ |
|
| 654 | - 'string' => 'Outbox', |
|
| 655 | - 'icon' => 'inbox', |
|
| 656 | - ], |
|
| 657 | - '{urn:ietf:params:xml:ns:caldav}schedule-inbox' => [ |
|
| 658 | - 'string' => 'Inbox', |
|
| 659 | - 'icon' => 'inbox', |
|
| 660 | - ], |
|
| 661 | - '{urn:ietf:params:xml:ns:caldav}calendar' => [ |
|
| 662 | - 'string' => 'Calendar', |
|
| 663 | - 'icon' => 'calendar', |
|
| 664 | - ], |
|
| 665 | - '{http://calendarserver.org/ns/}shared-owner' => [ |
|
| 666 | - 'string' => 'Shared', |
|
| 667 | - 'icon' => 'calendar', |
|
| 668 | - ], |
|
| 669 | - '{http://calendarserver.org/ns/}subscribed' => [ |
|
| 670 | - 'string' => 'Subscription', |
|
| 671 | - 'icon' => 'calendar', |
|
| 672 | - ], |
|
| 673 | - '{urn:ietf:params:xml:ns:carddav}directory' => [ |
|
| 674 | - 'string' => 'Directory', |
|
| 675 | - 'icon' => 'globe', |
|
| 676 | - ], |
|
| 677 | - '{urn:ietf:params:xml:ns:carddav}addressbook' => [ |
|
| 678 | - 'string' => 'Address book', |
|
| 679 | - 'icon' => 'book', |
|
| 680 | - ], |
|
| 681 | - '{DAV:}principal' => [ |
|
| 682 | - 'string' => 'Principal', |
|
| 683 | - 'icon' => 'person', |
|
| 684 | - ], |
|
| 685 | - '{DAV:}collection' => [ |
|
| 686 | - 'string' => 'Collection', |
|
| 687 | - 'icon' => 'folder', |
|
| 688 | - ], |
|
| 689 | - ]; |
|
| 690 | - |
|
| 691 | - $info = [ |
|
| 692 | - 'string' => [], |
|
| 693 | - 'icon' => 'cog', |
|
| 694 | - ]; |
|
| 695 | - foreach ($resourceTypes as $k => $resourceType) { |
|
| 696 | - if (isset($types[$resourceType])) { |
|
| 697 | - $info['string'][] = $types[$resourceType]['string']; |
|
| 698 | - } else { |
|
| 699 | - $info['string'][] = $resourceType; |
|
| 700 | - } |
|
| 701 | - } |
|
| 702 | - foreach ($types as $key => $resourceInfo) { |
|
| 703 | - if (in_array($key, $resourceTypes)) { |
|
| 704 | - $info['icon'] = $resourceInfo['icon']; |
|
| 705 | - break; |
|
| 706 | - } |
|
| 707 | - } |
|
| 708 | - $info['string'] = implode(', ', $info['string']); |
|
| 709 | - |
|
| 710 | - return $info; |
|
| 711 | - |
|
| 712 | - } |
|
| 713 | - |
|
| 714 | - /** |
|
| 715 | - * Draws a table row for a property |
|
| 716 | - * |
|
| 717 | - * @param string $name |
|
| 718 | - * @param mixed $value |
|
| 719 | - * @return string |
|
| 720 | - */ |
|
| 721 | - private function drawPropertyRow($name, $value) { |
|
| 722 | - |
|
| 723 | - $html = new HtmlOutputHelper( |
|
| 724 | - $this->server->getBaseUri(), |
|
| 725 | - $this->server->xml->namespaceMap |
|
| 726 | - ); |
|
| 727 | - |
|
| 728 | - return "<tr><th>" . $html->xmlName($name) . "</th><td>" . $this->drawPropertyValue($html, $value) . "</td></tr>"; |
|
| 729 | - |
|
| 730 | - } |
|
| 731 | - |
|
| 732 | - /** |
|
| 733 | - * Draws a table row for a property |
|
| 734 | - * |
|
| 735 | - * @param HtmlOutputHelper $html |
|
| 736 | - * @param mixed $value |
|
| 737 | - * @return string |
|
| 738 | - */ |
|
| 739 | - private function drawPropertyValue($html, $value) { |
|
| 740 | - |
|
| 741 | - if (is_scalar($value)) { |
|
| 742 | - return $html->h($value); |
|
| 743 | - } elseif ($value instanceof HtmlOutput) { |
|
| 744 | - return $value->toHtml($html); |
|
| 745 | - } elseif ($value instanceof \Sabre\Xml\XmlSerializable) { |
|
| 746 | - |
|
| 747 | - // There's no default html output for this property, we're going |
|
| 748 | - // to output the actual xml serialization instead. |
|
| 749 | - $xml = $this->server->xml->write('{DAV:}root', $value, $this->server->getBaseUri()); |
|
| 750 | - // removing first and last line, as they contain our root |
|
| 751 | - // element. |
|
| 752 | - $xml = explode("\n", $xml); |
|
| 753 | - $xml = array_slice($xml, 2, -2); |
|
| 754 | - return "<pre>" . $html->h(implode("\n", $xml)) . "</pre>"; |
|
| 755 | - |
|
| 756 | - } else { |
|
| 757 | - return "<em>unknown</em>"; |
|
| 758 | - } |
|
| 759 | - |
|
| 760 | - } |
|
| 761 | - |
|
| 762 | - /** |
|
| 763 | - * Returns a plugin name. |
|
| 764 | - * |
|
| 765 | - * Using this name other plugins will be able to access other plugins; |
|
| 766 | - * using \Sabre\DAV\Server::getPlugin |
|
| 767 | - * |
|
| 768 | - * @return string |
|
| 769 | - */ |
|
| 770 | - public function getPluginName() { |
|
| 771 | - |
|
| 772 | - return 'browser'; |
|
| 773 | - |
|
| 774 | - } |
|
| 775 | - |
|
| 776 | - /** |
|
| 777 | - * Returns a bunch of meta-data about the plugin. |
|
| 778 | - * |
|
| 779 | - * Providing this information is optional, and is mainly displayed by the |
|
| 780 | - * Browser plugin. |
|
| 781 | - * |
|
| 782 | - * The description key in the returned array may contain html and will not |
|
| 783 | - * be sanitized. |
|
| 784 | - * |
|
| 785 | - * @return array |
|
| 786 | - */ |
|
| 787 | - public function getPluginInfo() { |
|
| 788 | - |
|
| 789 | - return [ |
|
| 790 | - 'name' => $this->getPluginName(), |
|
| 791 | - 'description' => 'Generates HTML indexes and debug information for your sabre/dav server', |
|
| 792 | - 'link' => 'http://sabre.io/dav/browser-plugin/', |
|
| 793 | - ]; |
|
| 794 | - |
|
| 795 | - } |
|
| 524 | + $output .= ob_get_clean(); |
|
| 525 | + |
|
| 526 | + } |
|
| 527 | + |
|
| 528 | + /** |
|
| 529 | + * This method takes a path/name of an asset and turns it into url |
|
| 530 | + * suiteable for http access. |
|
| 531 | + * |
|
| 532 | + * @param string $assetName |
|
| 533 | + * @return string |
|
| 534 | + */ |
|
| 535 | + protected function getAssetUrl($assetName) { |
|
| 536 | + |
|
| 537 | + return $this->server->getBaseUri() . '?sabreAction=asset&assetName=' . urlencode($assetName); |
|
| 538 | + |
|
| 539 | + } |
|
| 540 | + |
|
| 541 | + /** |
|
| 542 | + * This method returns a local pathname to an asset. |
|
| 543 | + * |
|
| 544 | + * @param string $assetName |
|
| 545 | + * @return string |
|
| 546 | + * @throws DAV\Exception\NotFound |
|
| 547 | + */ |
|
| 548 | + protected function getLocalAssetPath($assetName) { |
|
| 549 | + |
|
| 550 | + $assetDir = __DIR__ . '/assets/'; |
|
| 551 | + $path = $assetDir . $assetName; |
|
| 552 | + |
|
| 553 | + // Making sure people aren't trying to escape from the base path. |
|
| 554 | + $path = str_replace('\\', '/', $path); |
|
| 555 | + if (strpos($path, '/../') !== false || strrchr($path, '/') === '/..') { |
|
| 556 | + throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); |
|
| 557 | + } |
|
| 558 | + if (strpos(realpath($path), realpath($assetDir)) === 0 && file_exists($path)) { |
|
| 559 | + return $path; |
|
| 560 | + } |
|
| 561 | + throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); |
|
| 562 | + } |
|
| 563 | + |
|
| 564 | + /** |
|
| 565 | + * This method reads an asset from disk and generates a full http response. |
|
| 566 | + * |
|
| 567 | + * @param string $assetName |
|
| 568 | + * @return void |
|
| 569 | + */ |
|
| 570 | + protected function serveAsset($assetName) { |
|
| 571 | + |
|
| 572 | + $assetPath = $this->getLocalAssetPath($assetName); |
|
| 573 | + |
|
| 574 | + // Rudimentary mime type detection |
|
| 575 | + $mime = 'application/octet-stream'; |
|
| 576 | + $map = [ |
|
| 577 | + 'ico' => 'image/vnd.microsoft.icon', |
|
| 578 | + 'png' => 'image/png', |
|
| 579 | + 'css' => 'text/css', |
|
| 580 | + ]; |
|
| 581 | + |
|
| 582 | + $ext = substr($assetName, strrpos($assetName, '.') + 1); |
|
| 583 | + if (isset($map[$ext])) { |
|
| 584 | + $mime = $map[$ext]; |
|
| 585 | + } |
|
| 586 | + |
|
| 587 | + $this->server->httpResponse->setHeader('Content-Type', $mime); |
|
| 588 | + $this->server->httpResponse->setHeader('Content-Length', filesize($assetPath)); |
|
| 589 | + $this->server->httpResponse->setHeader('Cache-Control', 'public, max-age=1209600'); |
|
| 590 | + $this->server->httpResponse->setStatus(200); |
|
| 591 | + $this->server->httpResponse->setBody(fopen($assetPath, 'r')); |
|
| 592 | + |
|
| 593 | + } |
|
| 594 | + |
|
| 595 | + /** |
|
| 596 | + * Sort helper function: compares two directory entries based on type and |
|
| 597 | + * display name. Collections sort above other types. |
|
| 598 | + * |
|
| 599 | + * @param array $a |
|
| 600 | + * @param array $b |
|
| 601 | + * @return int |
|
| 602 | + */ |
|
| 603 | + protected function compareNodes($a, $b) { |
|
| 604 | + |
|
| 605 | + $typeA = (isset($a['{DAV:}resourcetype'])) |
|
| 606 | + ? (in_array('{DAV:}collection', $a['{DAV:}resourcetype']->getValue())) |
|
| 607 | + : false; |
|
| 608 | + |
|
| 609 | + $typeB = (isset($b['{DAV:}resourcetype'])) |
|
| 610 | + ? (in_array('{DAV:}collection', $b['{DAV:}resourcetype']->getValue())) |
|
| 611 | + : false; |
|
| 612 | + |
|
| 613 | + // If same type, sort alphabetically by filename: |
|
| 614 | + if ($typeA === $typeB) { |
|
| 615 | + return strnatcasecmp($a['displayPath'], $b['displayPath']); |
|
| 616 | + } |
|
| 617 | + return (($typeA < $typeB) ? 1 : -1); |
|
| 618 | + |
|
| 619 | + } |
|
| 620 | + |
|
| 621 | + /** |
|
| 622 | + * Maps a resource type to a human-readable string and icon. |
|
| 623 | + * |
|
| 624 | + * @param array $resourceTypes |
|
| 625 | + * @param INode $node |
|
| 626 | + * @return array |
|
| 627 | + */ |
|
| 628 | + private function mapResourceType(array $resourceTypes, $node) { |
|
| 629 | + |
|
| 630 | + if (!$resourceTypes) { |
|
| 631 | + if ($node instanceof DAV\IFile) { |
|
| 632 | + return [ |
|
| 633 | + 'string' => 'File', |
|
| 634 | + 'icon' => 'file', |
|
| 635 | + ]; |
|
| 636 | + } else { |
|
| 637 | + return [ |
|
| 638 | + 'string' => 'Unknown', |
|
| 639 | + 'icon' => 'cog', |
|
| 640 | + ]; |
|
| 641 | + } |
|
| 642 | + } |
|
| 643 | + |
|
| 644 | + $types = [ |
|
| 645 | + '{http://calendarserver.org/ns/}calendar-proxy-write' => [ |
|
| 646 | + 'string' => 'Proxy-Write', |
|
| 647 | + 'icon' => 'people', |
|
| 648 | + ], |
|
| 649 | + '{http://calendarserver.org/ns/}calendar-proxy-read' => [ |
|
| 650 | + 'string' => 'Proxy-Read', |
|
| 651 | + 'icon' => 'people', |
|
| 652 | + ], |
|
| 653 | + '{urn:ietf:params:xml:ns:caldav}schedule-outbox' => [ |
|
| 654 | + 'string' => 'Outbox', |
|
| 655 | + 'icon' => 'inbox', |
|
| 656 | + ], |
|
| 657 | + '{urn:ietf:params:xml:ns:caldav}schedule-inbox' => [ |
|
| 658 | + 'string' => 'Inbox', |
|
| 659 | + 'icon' => 'inbox', |
|
| 660 | + ], |
|
| 661 | + '{urn:ietf:params:xml:ns:caldav}calendar' => [ |
|
| 662 | + 'string' => 'Calendar', |
|
| 663 | + 'icon' => 'calendar', |
|
| 664 | + ], |
|
| 665 | + '{http://calendarserver.org/ns/}shared-owner' => [ |
|
| 666 | + 'string' => 'Shared', |
|
| 667 | + 'icon' => 'calendar', |
|
| 668 | + ], |
|
| 669 | + '{http://calendarserver.org/ns/}subscribed' => [ |
|
| 670 | + 'string' => 'Subscription', |
|
| 671 | + 'icon' => 'calendar', |
|
| 672 | + ], |
|
| 673 | + '{urn:ietf:params:xml:ns:carddav}directory' => [ |
|
| 674 | + 'string' => 'Directory', |
|
| 675 | + 'icon' => 'globe', |
|
| 676 | + ], |
|
| 677 | + '{urn:ietf:params:xml:ns:carddav}addressbook' => [ |
|
| 678 | + 'string' => 'Address book', |
|
| 679 | + 'icon' => 'book', |
|
| 680 | + ], |
|
| 681 | + '{DAV:}principal' => [ |
|
| 682 | + 'string' => 'Principal', |
|
| 683 | + 'icon' => 'person', |
|
| 684 | + ], |
|
| 685 | + '{DAV:}collection' => [ |
|
| 686 | + 'string' => 'Collection', |
|
| 687 | + 'icon' => 'folder', |
|
| 688 | + ], |
|
| 689 | + ]; |
|
| 690 | + |
|
| 691 | + $info = [ |
|
| 692 | + 'string' => [], |
|
| 693 | + 'icon' => 'cog', |
|
| 694 | + ]; |
|
| 695 | + foreach ($resourceTypes as $k => $resourceType) { |
|
| 696 | + if (isset($types[$resourceType])) { |
|
| 697 | + $info['string'][] = $types[$resourceType]['string']; |
|
| 698 | + } else { |
|
| 699 | + $info['string'][] = $resourceType; |
|
| 700 | + } |
|
| 701 | + } |
|
| 702 | + foreach ($types as $key => $resourceInfo) { |
|
| 703 | + if (in_array($key, $resourceTypes)) { |
|
| 704 | + $info['icon'] = $resourceInfo['icon']; |
|
| 705 | + break; |
|
| 706 | + } |
|
| 707 | + } |
|
| 708 | + $info['string'] = implode(', ', $info['string']); |
|
| 709 | + |
|
| 710 | + return $info; |
|
| 711 | + |
|
| 712 | + } |
|
| 713 | + |
|
| 714 | + /** |
|
| 715 | + * Draws a table row for a property |
|
| 716 | + * |
|
| 717 | + * @param string $name |
|
| 718 | + * @param mixed $value |
|
| 719 | + * @return string |
|
| 720 | + */ |
|
| 721 | + private function drawPropertyRow($name, $value) { |
|
| 722 | + |
|
| 723 | + $html = new HtmlOutputHelper( |
|
| 724 | + $this->server->getBaseUri(), |
|
| 725 | + $this->server->xml->namespaceMap |
|
| 726 | + ); |
|
| 727 | + |
|
| 728 | + return "<tr><th>" . $html->xmlName($name) . "</th><td>" . $this->drawPropertyValue($html, $value) . "</td></tr>"; |
|
| 729 | + |
|
| 730 | + } |
|
| 731 | + |
|
| 732 | + /** |
|
| 733 | + * Draws a table row for a property |
|
| 734 | + * |
|
| 735 | + * @param HtmlOutputHelper $html |
|
| 736 | + * @param mixed $value |
|
| 737 | + * @return string |
|
| 738 | + */ |
|
| 739 | + private function drawPropertyValue($html, $value) { |
|
| 740 | + |
|
| 741 | + if (is_scalar($value)) { |
|
| 742 | + return $html->h($value); |
|
| 743 | + } elseif ($value instanceof HtmlOutput) { |
|
| 744 | + return $value->toHtml($html); |
|
| 745 | + } elseif ($value instanceof \Sabre\Xml\XmlSerializable) { |
|
| 746 | + |
|
| 747 | + // There's no default html output for this property, we're going |
|
| 748 | + // to output the actual xml serialization instead. |
|
| 749 | + $xml = $this->server->xml->write('{DAV:}root', $value, $this->server->getBaseUri()); |
|
| 750 | + // removing first and last line, as they contain our root |
|
| 751 | + // element. |
|
| 752 | + $xml = explode("\n", $xml); |
|
| 753 | + $xml = array_slice($xml, 2, -2); |
|
| 754 | + return "<pre>" . $html->h(implode("\n", $xml)) . "</pre>"; |
|
| 755 | + |
|
| 756 | + } else { |
|
| 757 | + return "<em>unknown</em>"; |
|
| 758 | + } |
|
| 759 | + |
|
| 760 | + } |
|
| 761 | + |
|
| 762 | + /** |
|
| 763 | + * Returns a plugin name. |
|
| 764 | + * |
|
| 765 | + * Using this name other plugins will be able to access other plugins; |
|
| 766 | + * using \Sabre\DAV\Server::getPlugin |
|
| 767 | + * |
|
| 768 | + * @return string |
|
| 769 | + */ |
|
| 770 | + public function getPluginName() { |
|
| 771 | + |
|
| 772 | + return 'browser'; |
|
| 773 | + |
|
| 774 | + } |
|
| 775 | + |
|
| 776 | + /** |
|
| 777 | + * Returns a bunch of meta-data about the plugin. |
|
| 778 | + * |
|
| 779 | + * Providing this information is optional, and is mainly displayed by the |
|
| 780 | + * Browser plugin. |
|
| 781 | + * |
|
| 782 | + * The description key in the returned array may contain html and will not |
|
| 783 | + * be sanitized. |
|
| 784 | + * |
|
| 785 | + * @return array |
|
| 786 | + */ |
|
| 787 | + public function getPluginInfo() { |
|
| 788 | + |
|
| 789 | + return [ |
|
| 790 | + 'name' => $this->getPluginName(), |
|
| 791 | + 'description' => 'Generates HTML indexes and debug information for your sabre/dav server', |
|
| 792 | + 'link' => 'http://sabre.io/dav/browser-plugin/', |
|
| 793 | + ]; |
|
| 794 | + |
|
| 795 | + } |
|
| 796 | 796 | |
| 797 | 797 | } |
@@ -78,7 +78,9 @@ discard block |
||
| 78 | 78 | $this->server->on('method:GET', [$this, 'httpGetEarly'], 90); |
| 79 | 79 | $this->server->on('method:GET', [$this, 'httpGet'], 200); |
| 80 | 80 | $this->server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel'], 200); |
| 81 | - if ($this->enablePost) $this->server->on('method:POST', [$this, 'httpPOST']); |
|
| 81 | + if ($this->enablePost) { |
|
| 82 | + $this->server->on('method:POST', [$this, 'httpPOST']); |
|
| 83 | + } |
|
| 82 | 84 | } |
| 83 | 85 | |
| 84 | 86 | /** |
@@ -172,8 +174,9 @@ discard block |
||
| 172 | 174 | } |
| 173 | 175 | $postVars = $request->getPostData(); |
| 174 | 176 | |
| 175 | - if (!isset($postVars['sabreAction'])) |
|
| 176 | - return; |
|
| 177 | + if (!isset($postVars['sabreAction'])) { |
|
| 178 | + return; |
|
| 179 | + } |
|
| 177 | 180 | |
| 178 | 181 | $uri = $request->getPath(); |
| 179 | 182 | |
@@ -219,12 +222,16 @@ discard block |
||
| 219 | 222 | // @codeCoverageIgnoreStart |
| 220 | 223 | case 'put' : |
| 221 | 224 | |
| 222 | - if ($_FILES) $file = current($_FILES); |
|
| 223 | - else break; |
|
| 225 | + if ($_FILES) { |
|
| 226 | + $file = current($_FILES); |
|
| 227 | + } else { |
|
| 228 | + break; |
|
| 229 | + } |
|
| 224 | 230 | |
| 225 | 231 | list(, $newName) = URLUtil::splitPath(trim($file['name'])); |
| 226 | - if (isset($postVars['name']) && trim($postVars['name'])) |
|
| 227 | - $newName = trim($postVars['name']); |
|
| 232 | + if (isset($postVars['name']) && trim($postVars['name'])) { |
|
| 233 | + $newName = trim($postVars['name']); |
|
| 234 | + } |
|
| 228 | 235 | |
| 229 | 236 | // Making sure we only have a 'basename' component |
| 230 | 237 | list(, $newName) = URLUtil::splitPath($newName); |
@@ -497,13 +504,15 @@ discard block |
||
| 497 | 504 | */ |
| 498 | 505 | public function htmlActionsPanel(DAV\INode $node, &$output) { |
| 499 | 506 | |
| 500 | - if (!$node instanceof DAV\ICollection) |
|
| 501 | - return; |
|
| 507 | + if (!$node instanceof DAV\ICollection) { |
|
| 508 | + return; |
|
| 509 | + } |
|
| 502 | 510 | |
| 503 | 511 | // We also know fairly certain that if an object is a non-extended |
| 504 | 512 | // SimpleCollection, we won't need to show the panel either. |
| 505 | - if (get_class($node) === 'Sabre\\DAV\\SimpleCollection') |
|
| 506 | - return; |
|
| 513 | + if (get_class($node) === 'Sabre\\DAV\\SimpleCollection') { |
|
| 514 | + return; |
|
| 515 | + } |
|
| 507 | 516 | |
| 508 | 517 | ob_start(); |
| 509 | 518 | echo '<form method="post" action=""> |
@@ -19,65 +19,65 @@ |
||
| 19 | 19 | */ |
| 20 | 20 | class PropFind implements XmlDeserializable { |
| 21 | 21 | |
| 22 | - /** |
|
| 23 | - * If this is set to true, this was an 'allprop' request. |
|
| 24 | - * |
|
| 25 | - * @var bool |
|
| 26 | - */ |
|
| 27 | - public $allProp = false; |
|
| 28 | - |
|
| 29 | - /** |
|
| 30 | - * The property list |
|
| 31 | - * |
|
| 32 | - * @var null|array |
|
| 33 | - */ |
|
| 34 | - public $properties; |
|
| 35 | - |
|
| 36 | - /** |
|
| 37 | - * The deserialize method is called during xml parsing. |
|
| 38 | - * |
|
| 39 | - * This method is called statictly, this is because in theory this method |
|
| 40 | - * may be used as a type of constructor, or factory method. |
|
| 41 | - * |
|
| 42 | - * Often you want to return an instance of the current class, but you are |
|
| 43 | - * free to return other data as well. |
|
| 44 | - * |
|
| 45 | - * You are responsible for advancing the reader to the next element. Not |
|
| 46 | - * doing anything will result in a never-ending loop. |
|
| 47 | - * |
|
| 48 | - * If you just want to skip parsing for this element altogether, you can |
|
| 49 | - * just call $reader->next(); |
|
| 50 | - * |
|
| 51 | - * $reader->parseInnerTree() will parse the entire sub-tree, and advance to |
|
| 52 | - * the next element. |
|
| 53 | - * |
|
| 54 | - * @param Reader $reader |
|
| 55 | - * @return mixed |
|
| 56 | - */ |
|
| 57 | - static function xmlDeserialize(Reader $reader) { |
|
| 58 | - |
|
| 59 | - $self = new self(); |
|
| 60 | - |
|
| 61 | - $reader->pushContext(); |
|
| 62 | - $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Element\Elements'; |
|
| 63 | - |
|
| 64 | - foreach (KeyValue::xmlDeserialize($reader) as $k => $v) { |
|
| 65 | - |
|
| 66 | - switch ($k) { |
|
| 67 | - case '{DAV:}prop' : |
|
| 68 | - $self->properties = $v; |
|
| 69 | - break; |
|
| 70 | - case '{DAV:}allprop' : |
|
| 71 | - $self->allProp = true; |
|
| 72 | - |
|
| 73 | - } |
|
| 74 | - |
|
| 75 | - } |
|
| 76 | - |
|
| 77 | - $reader->popContext(); |
|
| 78 | - |
|
| 79 | - return $self; |
|
| 80 | - |
|
| 81 | - } |
|
| 22 | + /** |
|
| 23 | + * If this is set to true, this was an 'allprop' request. |
|
| 24 | + * |
|
| 25 | + * @var bool |
|
| 26 | + */ |
|
| 27 | + public $allProp = false; |
|
| 28 | + |
|
| 29 | + /** |
|
| 30 | + * The property list |
|
| 31 | + * |
|
| 32 | + * @var null|array |
|
| 33 | + */ |
|
| 34 | + public $properties; |
|
| 35 | + |
|
| 36 | + /** |
|
| 37 | + * The deserialize method is called during xml parsing. |
|
| 38 | + * |
|
| 39 | + * This method is called statictly, this is because in theory this method |
|
| 40 | + * may be used as a type of constructor, or factory method. |
|
| 41 | + * |
|
| 42 | + * Often you want to return an instance of the current class, but you are |
|
| 43 | + * free to return other data as well. |
|
| 44 | + * |
|
| 45 | + * You are responsible for advancing the reader to the next element. Not |
|
| 46 | + * doing anything will result in a never-ending loop. |
|
| 47 | + * |
|
| 48 | + * If you just want to skip parsing for this element altogether, you can |
|
| 49 | + * just call $reader->next(); |
|
| 50 | + * |
|
| 51 | + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to |
|
| 52 | + * the next element. |
|
| 53 | + * |
|
| 54 | + * @param Reader $reader |
|
| 55 | + * @return mixed |
|
| 56 | + */ |
|
| 57 | + static function xmlDeserialize(Reader $reader) { |
|
| 58 | + |
|
| 59 | + $self = new self(); |
|
| 60 | + |
|
| 61 | + $reader->pushContext(); |
|
| 62 | + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Element\Elements'; |
|
| 63 | + |
|
| 64 | + foreach (KeyValue::xmlDeserialize($reader) as $k => $v) { |
|
| 65 | + |
|
| 66 | + switch ($k) { |
|
| 67 | + case '{DAV:}prop' : |
|
| 68 | + $self->properties = $v; |
|
| 69 | + break; |
|
| 70 | + case '{DAV:}allprop' : |
|
| 71 | + $self->allProp = true; |
|
| 72 | + |
|
| 73 | + } |
|
| 74 | + |
|
| 75 | + } |
|
| 76 | + |
|
| 77 | + $reader->popContext(); |
|
| 78 | + |
|
| 79 | + return $self; |
|
| 80 | + |
|
| 81 | + } |
|
| 82 | 82 | |
| 83 | 83 | } |