@@ -16,14 +16,14 @@ |
||
| 16 | 16 | */ |
| 17 | 17 | interface CreatePrincipalSupport extends BackendInterface |
| 18 | 18 | { |
| 19 | - /** |
|
| 20 | - * Creates a new principal. |
|
| 21 | - * |
|
| 22 | - * This method receives a full path for the new principal. The mkCol object |
|
| 23 | - * contains any additional webdav properties specified during the creation |
|
| 24 | - * of the principal. |
|
| 25 | - * |
|
| 26 | - * @param string $path |
|
| 27 | - */ |
|
| 28 | - public function createPrincipal($path, MkCol $mkCol); |
|
| 19 | + /** |
|
| 20 | + * Creates a new principal. |
|
| 21 | + * |
|
| 22 | + * This method receives a full path for the new principal. The mkCol object |
|
| 23 | + * contains any additional webdav properties specified during the creation |
|
| 24 | + * of the principal. |
|
| 25 | + * |
|
| 26 | + * @param string $path |
|
| 27 | + */ |
|
| 28 | + public function createPrincipal($path, MkCol $mkCol); |
|
| 29 | 29 | } |
@@ -17,38 +17,38 @@ |
||
| 17 | 17 | */ |
| 18 | 18 | abstract class AbstractBackend implements BackendInterface |
| 19 | 19 | { |
| 20 | - /** |
|
| 21 | - * Finds a principal by its URI. |
|
| 22 | - * |
|
| 23 | - * This method may receive any type of uri, but mailto: addresses will be |
|
| 24 | - * the most common. |
|
| 25 | - * |
|
| 26 | - * Implementation of this API is optional. It is currently used by the |
|
| 27 | - * CalDAV system to find principals based on their email addresses. If this |
|
| 28 | - * API is not implemented, some features may not work correctly. |
|
| 29 | - * |
|
| 30 | - * This method must return a relative principal path, or null, if the |
|
| 31 | - * principal was not found or you refuse to find it. |
|
| 32 | - * |
|
| 33 | - * @param string $uri |
|
| 34 | - * @param string $principalPrefix |
|
| 35 | - * |
|
| 36 | - * @return string|null |
|
| 37 | - */ |
|
| 38 | - public function findByUri($uri, $principalPrefix) |
|
| 39 | - { |
|
| 40 | - // Note that the default implementation here is a bit slow and could |
|
| 41 | - // likely be optimized. |
|
| 42 | - if ('mailto:' !== substr($uri, 0, 7)) { |
|
| 43 | - return; |
|
| 44 | - } |
|
| 45 | - $result = $this->searchPrincipals( |
|
| 46 | - $principalPrefix, |
|
| 47 | - ['{http://sabredav.org/ns}email-address' => substr($uri, 7)] |
|
| 48 | - ); |
|
| 20 | + /** |
|
| 21 | + * Finds a principal by its URI. |
|
| 22 | + * |
|
| 23 | + * This method may receive any type of uri, but mailto: addresses will be |
|
| 24 | + * the most common. |
|
| 25 | + * |
|
| 26 | + * Implementation of this API is optional. It is currently used by the |
|
| 27 | + * CalDAV system to find principals based on their email addresses. If this |
|
| 28 | + * API is not implemented, some features may not work correctly. |
|
| 29 | + * |
|
| 30 | + * This method must return a relative principal path, or null, if the |
|
| 31 | + * principal was not found or you refuse to find it. |
|
| 32 | + * |
|
| 33 | + * @param string $uri |
|
| 34 | + * @param string $principalPrefix |
|
| 35 | + * |
|
| 36 | + * @return string|null |
|
| 37 | + */ |
|
| 38 | + public function findByUri($uri, $principalPrefix) |
|
| 39 | + { |
|
| 40 | + // Note that the default implementation here is a bit slow and could |
|
| 41 | + // likely be optimized. |
|
| 42 | + if ('mailto:' !== substr($uri, 0, 7)) { |
|
| 43 | + return; |
|
| 44 | + } |
|
| 45 | + $result = $this->searchPrincipals( |
|
| 46 | + $principalPrefix, |
|
| 47 | + ['{http://sabredav.org/ns}email-address' => substr($uri, 7)] |
|
| 48 | + ); |
|
| 49 | 49 | |
| 50 | - if ($result) { |
|
| 51 | - return $result[0]; |
|
| 52 | - } |
|
| 53 | - } |
|
| 50 | + if ($result) { |
|
| 51 | + return $result[0]; |
|
| 52 | + } |
|
| 53 | + } |
|
| 54 | 54 | } |
@@ -20,424 +20,424 @@ |
||
| 20 | 20 | */ |
| 21 | 21 | class PDO extends AbstractBackend implements CreatePrincipalSupport |
| 22 | 22 | { |
| 23 | - /** |
|
| 24 | - * PDO table name for 'principals'. |
|
| 25 | - * |
|
| 26 | - * @var string |
|
| 27 | - */ |
|
| 28 | - public $tableName = 'principals'; |
|
| 29 | - |
|
| 30 | - /** |
|
| 31 | - * PDO table name for 'group members'. |
|
| 32 | - * |
|
| 33 | - * @var string |
|
| 34 | - */ |
|
| 35 | - public $groupMembersTableName = 'groupmembers'; |
|
| 36 | - |
|
| 37 | - /** |
|
| 38 | - * pdo. |
|
| 39 | - * |
|
| 40 | - * @var PDO |
|
| 41 | - */ |
|
| 42 | - protected $pdo; |
|
| 43 | - |
|
| 44 | - /** |
|
| 45 | - * A list of additional fields to support. |
|
| 46 | - * |
|
| 47 | - * @var array |
|
| 48 | - */ |
|
| 49 | - protected $fieldMap = [ |
|
| 50 | - /* |
|
| 23 | + /** |
|
| 24 | + * PDO table name for 'principals'. |
|
| 25 | + * |
|
| 26 | + * @var string |
|
| 27 | + */ |
|
| 28 | + public $tableName = 'principals'; |
|
| 29 | + |
|
| 30 | + /** |
|
| 31 | + * PDO table name for 'group members'. |
|
| 32 | + * |
|
| 33 | + * @var string |
|
| 34 | + */ |
|
| 35 | + public $groupMembersTableName = 'groupmembers'; |
|
| 36 | + |
|
| 37 | + /** |
|
| 38 | + * pdo. |
|
| 39 | + * |
|
| 40 | + * @var PDO |
|
| 41 | + */ |
|
| 42 | + protected $pdo; |
|
| 43 | + |
|
| 44 | + /** |
|
| 45 | + * A list of additional fields to support. |
|
| 46 | + * |
|
| 47 | + * @var array |
|
| 48 | + */ |
|
| 49 | + protected $fieldMap = [ |
|
| 50 | + /* |
|
| 51 | 51 | * This property can be used to display the users' real name. |
| 52 | 52 | */ |
| 53 | - '{DAV:}displayname' => [ |
|
| 54 | - 'dbField' => 'displayname', |
|
| 55 | - ], |
|
| 53 | + '{DAV:}displayname' => [ |
|
| 54 | + 'dbField' => 'displayname', |
|
| 55 | + ], |
|
| 56 | 56 | |
| 57 | - /* |
|
| 57 | + /* |
|
| 58 | 58 | * This is the users' primary email-address. |
| 59 | 59 | */ |
| 60 | - '{http://sabredav.org/ns}email-address' => [ |
|
| 61 | - 'dbField' => 'email', |
|
| 62 | - ], |
|
| 63 | - ]; |
|
| 64 | - |
|
| 65 | - /** |
|
| 66 | - * Sets up the backend. |
|
| 67 | - */ |
|
| 68 | - public function __construct(\PDO $pdo) |
|
| 69 | - { |
|
| 70 | - $this->pdo = $pdo; |
|
| 71 | - } |
|
| 72 | - |
|
| 73 | - /** |
|
| 74 | - * Returns a list of principals based on a prefix. |
|
| 75 | - * |
|
| 76 | - * This prefix will often contain something like 'principals'. You are only |
|
| 77 | - * expected to return principals that are in this base path. |
|
| 78 | - * |
|
| 79 | - * You are expected to return at least a 'uri' for every user, you can |
|
| 80 | - * return any additional properties if you wish so. Common properties are: |
|
| 81 | - * {DAV:}displayname |
|
| 82 | - * {http://sabredav.org/ns}email-address - This is a custom SabreDAV |
|
| 83 | - * field that's actualy injected in a number of other properties. If |
|
| 84 | - * you have an email address, use this property. |
|
| 85 | - * |
|
| 86 | - * @param string $prefixPath |
|
| 87 | - * |
|
| 88 | - * @return array |
|
| 89 | - */ |
|
| 90 | - public function getPrincipalsByPrefix($prefixPath) |
|
| 91 | - { |
|
| 92 | - $fields = [ |
|
| 93 | - 'uri', |
|
| 94 | - ]; |
|
| 95 | - |
|
| 96 | - foreach ($this->fieldMap as $key => $value) { |
|
| 97 | - $fields[] = $value['dbField']; |
|
| 98 | - } |
|
| 99 | - $result = $this->pdo->query('SELECT '.implode(',', $fields).' FROM '.$this->tableName); |
|
| 100 | - |
|
| 101 | - $principals = []; |
|
| 102 | - |
|
| 103 | - while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { |
|
| 104 | - // Checking if the principal is in the prefix |
|
| 105 | - list($rowPrefix) = Uri\split($row['uri']); |
|
| 106 | - if ($rowPrefix !== $prefixPath) { |
|
| 107 | - continue; |
|
| 108 | - } |
|
| 109 | - |
|
| 110 | - $principal = [ |
|
| 111 | - 'uri' => $row['uri'], |
|
| 112 | - ]; |
|
| 113 | - foreach ($this->fieldMap as $key => $value) { |
|
| 114 | - if ($row[$value['dbField']]) { |
|
| 115 | - $principal[$key] = $row[$value['dbField']]; |
|
| 116 | - } |
|
| 117 | - } |
|
| 118 | - $principals[] = $principal; |
|
| 119 | - } |
|
| 120 | - |
|
| 121 | - return $principals; |
|
| 122 | - } |
|
| 123 | - |
|
| 124 | - /** |
|
| 125 | - * Returns a specific principal, specified by it's path. |
|
| 126 | - * The returned structure should be the exact same as from |
|
| 127 | - * getPrincipalsByPrefix. |
|
| 128 | - * |
|
| 129 | - * @param string $path |
|
| 130 | - * |
|
| 131 | - * @return array |
|
| 132 | - */ |
|
| 133 | - public function getPrincipalByPath($path) |
|
| 134 | - { |
|
| 135 | - $fields = [ |
|
| 136 | - 'id', |
|
| 137 | - 'uri', |
|
| 138 | - ]; |
|
| 139 | - |
|
| 140 | - foreach ($this->fieldMap as $key => $value) { |
|
| 141 | - $fields[] = $value['dbField']; |
|
| 142 | - } |
|
| 143 | - $stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).' FROM '.$this->tableName.' WHERE uri = ?'); |
|
| 144 | - $stmt->execute([$path]); |
|
| 145 | - |
|
| 146 | - $row = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
| 147 | - if (!$row) { |
|
| 148 | - return; |
|
| 149 | - } |
|
| 150 | - |
|
| 151 | - $principal = [ |
|
| 152 | - 'id' => $row['id'], |
|
| 153 | - 'uri' => $row['uri'], |
|
| 154 | - ]; |
|
| 155 | - foreach ($this->fieldMap as $key => $value) { |
|
| 156 | - if ($row[$value['dbField']]) { |
|
| 157 | - $principal[$key] = $row[$value['dbField']]; |
|
| 158 | - } |
|
| 159 | - } |
|
| 160 | - |
|
| 161 | - return $principal; |
|
| 162 | - } |
|
| 163 | - |
|
| 164 | - /** |
|
| 165 | - * Updates one ore more webdav properties on a principal. |
|
| 166 | - * |
|
| 167 | - * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
| 168 | - * To do the actual updates, you must tell this object which properties |
|
| 169 | - * you're going to process with the handle() method. |
|
| 170 | - * |
|
| 171 | - * Calling the handle method is like telling the PropPatch object "I |
|
| 172 | - * promise I can handle updating this property". |
|
| 173 | - * |
|
| 174 | - * Read the PropPatch documentation for more info and examples. |
|
| 175 | - * |
|
| 176 | - * @param string $path |
|
| 177 | - */ |
|
| 178 | - public function updatePrincipal($path, DAV\PropPatch $propPatch) |
|
| 179 | - { |
|
| 180 | - $propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($path) { |
|
| 181 | - $query = 'UPDATE '.$this->tableName.' SET '; |
|
| 182 | - $first = true; |
|
| 183 | - |
|
| 184 | - $values = []; |
|
| 185 | - |
|
| 186 | - foreach ($properties as $key => $value) { |
|
| 187 | - $dbField = $this->fieldMap[$key]['dbField']; |
|
| 188 | - |
|
| 189 | - if (!$first) { |
|
| 190 | - $query .= ', '; |
|
| 191 | - } |
|
| 192 | - $first = false; |
|
| 193 | - $query .= $dbField.' = :'.$dbField; |
|
| 194 | - $values[$dbField] = $value; |
|
| 195 | - } |
|
| 196 | - |
|
| 197 | - $query .= ' WHERE uri = :uri'; |
|
| 198 | - $values['uri'] = $path; |
|
| 199 | - |
|
| 200 | - $stmt = $this->pdo->prepare($query); |
|
| 201 | - $stmt->execute($values); |
|
| 202 | - |
|
| 203 | - return true; |
|
| 204 | - }); |
|
| 205 | - } |
|
| 206 | - |
|
| 207 | - /** |
|
| 208 | - * This method is used to search for principals matching a set of |
|
| 209 | - * properties. |
|
| 210 | - * |
|
| 211 | - * This search is specifically used by RFC3744's principal-property-search |
|
| 212 | - * REPORT. |
|
| 213 | - * |
|
| 214 | - * The actual search should be a unicode-non-case-sensitive search. The |
|
| 215 | - * keys in searchProperties are the WebDAV property names, while the values |
|
| 216 | - * are the property values to search on. |
|
| 217 | - * |
|
| 218 | - * By default, if multiple properties are submitted to this method, the |
|
| 219 | - * various properties should be combined with 'AND'. If $test is set to |
|
| 220 | - * 'anyof', it should be combined using 'OR'. |
|
| 221 | - * |
|
| 222 | - * This method should simply return an array with full principal uri's. |
|
| 223 | - * |
|
| 224 | - * If somebody attempted to search on a property the backend does not |
|
| 225 | - * support, you should simply return 0 results. |
|
| 226 | - * |
|
| 227 | - * You can also just return 0 results if you choose to not support |
|
| 228 | - * searching at all, but keep in mind that this may stop certain features |
|
| 229 | - * from working. |
|
| 230 | - * |
|
| 231 | - * @param string $prefixPath |
|
| 232 | - * @param string $test |
|
| 233 | - * |
|
| 234 | - * @return array |
|
| 235 | - */ |
|
| 236 | - public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') |
|
| 237 | - { |
|
| 238 | - if (0 == count($searchProperties)) { |
|
| 239 | - return []; |
|
| 240 | - } //No criteria |
|
| 241 | - |
|
| 242 | - $query = 'SELECT uri FROM '.$this->tableName.' WHERE '; |
|
| 243 | - $values = []; |
|
| 244 | - foreach ($searchProperties as $property => $value) { |
|
| 245 | - switch ($property) { |
|
| 246 | - case '{DAV:}displayname': |
|
| 247 | - $column = 'displayname'; |
|
| 248 | - break; |
|
| 249 | - case '{http://sabredav.org/ns}email-address': |
|
| 250 | - $column = 'email'; |
|
| 251 | - break; |
|
| 252 | - default: |
|
| 253 | - // Unsupported property |
|
| 254 | - return []; |
|
| 255 | - } |
|
| 256 | - if (count($values) > 0) { |
|
| 257 | - $query .= (0 == strcmp($test, 'anyof') ? ' OR ' : ' AND '); |
|
| 258 | - } |
|
| 259 | - $query .= 'lower('.$column.') LIKE lower(?)'; |
|
| 260 | - $values[] = '%'.$value.'%'; |
|
| 261 | - } |
|
| 262 | - $stmt = $this->pdo->prepare($query); |
|
| 263 | - $stmt->execute($values); |
|
| 264 | - |
|
| 265 | - $principals = []; |
|
| 266 | - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 267 | - // Checking if the principal is in the prefix |
|
| 268 | - list($rowPrefix) = Uri\split($row['uri']); |
|
| 269 | - if ($rowPrefix !== $prefixPath) { |
|
| 270 | - continue; |
|
| 271 | - } |
|
| 272 | - |
|
| 273 | - $principals[] = $row['uri']; |
|
| 274 | - } |
|
| 275 | - |
|
| 276 | - return $principals; |
|
| 277 | - } |
|
| 278 | - |
|
| 279 | - /** |
|
| 280 | - * Finds a principal by its URI. |
|
| 281 | - * |
|
| 282 | - * This method may receive any type of uri, but mailto: addresses will be |
|
| 283 | - * the most common. |
|
| 284 | - * |
|
| 285 | - * Implementation of this API is optional. It is currently used by the |
|
| 286 | - * CalDAV system to find principals based on their email addresses. If this |
|
| 287 | - * API is not implemented, some features may not work correctly. |
|
| 288 | - * |
|
| 289 | - * This method must return a relative principal path, or null, if the |
|
| 290 | - * principal was not found or you refuse to find it. |
|
| 291 | - * |
|
| 292 | - * @param string $uri |
|
| 293 | - * @param string $principalPrefix |
|
| 294 | - * |
|
| 295 | - * @return string |
|
| 296 | - */ |
|
| 297 | - public function findByUri($uri, $principalPrefix) |
|
| 298 | - { |
|
| 299 | - $uriParts = Uri\parse($uri); |
|
| 300 | - |
|
| 301 | - // Only two types of uri are supported : |
|
| 302 | - // - the "mailto:" scheme with some non-empty address |
|
| 303 | - // - a principals uri, in the form "principals/NAME" |
|
| 304 | - // In both cases, `path` must not be empty. |
|
| 305 | - if (empty($uriParts['path'])) { |
|
| 306 | - return null; |
|
| 307 | - } |
|
| 308 | - |
|
| 309 | - $uri = null; |
|
| 310 | - if ('mailto' === $uriParts['scheme']) { |
|
| 311 | - $query = 'SELECT uri FROM '.$this->tableName.' WHERE lower(email)=lower(?)'; |
|
| 312 | - $stmt = $this->pdo->prepare($query); |
|
| 313 | - $stmt->execute([$uriParts['path']]); |
|
| 314 | - |
|
| 315 | - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 316 | - // Checking if the principal is in the prefix |
|
| 317 | - list($rowPrefix) = Uri\split($row['uri']); |
|
| 318 | - if ($rowPrefix !== $principalPrefix) { |
|
| 319 | - continue; |
|
| 320 | - } |
|
| 321 | - |
|
| 322 | - $uri = $row['uri']; |
|
| 323 | - break; //Stop on first match |
|
| 324 | - } |
|
| 325 | - } else { |
|
| 326 | - $pathParts = Uri\split($uriParts['path']); // We can do this since $uriParts['path'] is not null |
|
| 327 | - |
|
| 328 | - if (2 === count($pathParts) && $pathParts[0] === $principalPrefix) { |
|
| 329 | - // Checking that this uri exists |
|
| 330 | - $query = 'SELECT * FROM '.$this->tableName.' WHERE uri = ?'; |
|
| 331 | - $stmt = $this->pdo->prepare($query); |
|
| 332 | - $stmt->execute([$uriParts['path']]); |
|
| 333 | - $rows = $stmt->fetchAll(); |
|
| 334 | - |
|
| 335 | - if (count($rows) > 0) { |
|
| 336 | - $uri = $uriParts['path']; |
|
| 337 | - } |
|
| 338 | - } |
|
| 339 | - } |
|
| 340 | - |
|
| 341 | - return $uri; |
|
| 342 | - } |
|
| 343 | - |
|
| 344 | - /** |
|
| 345 | - * Returns the list of members for a group-principal. |
|
| 346 | - * |
|
| 347 | - * @param string $principal |
|
| 348 | - * |
|
| 349 | - * @return array |
|
| 350 | - */ |
|
| 351 | - public function getGroupMemberSet($principal) |
|
| 352 | - { |
|
| 353 | - $principal = $this->getPrincipalByPath($principal); |
|
| 354 | - if (!$principal) { |
|
| 355 | - throw new DAV\Exception('Principal not found'); |
|
| 356 | - } |
|
| 357 | - $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?'); |
|
| 358 | - $stmt->execute([$principal['id']]); |
|
| 359 | - |
|
| 360 | - $result = []; |
|
| 361 | - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 362 | - $result[] = $row['uri']; |
|
| 363 | - } |
|
| 364 | - |
|
| 365 | - return $result; |
|
| 366 | - } |
|
| 367 | - |
|
| 368 | - /** |
|
| 369 | - * Returns the list of groups a principal is a member of. |
|
| 370 | - * |
|
| 371 | - * @param string $principal |
|
| 372 | - * |
|
| 373 | - * @return array |
|
| 374 | - */ |
|
| 375 | - public function getGroupMembership($principal) |
|
| 376 | - { |
|
| 377 | - $principal = $this->getPrincipalByPath($principal); |
|
| 378 | - if (!$principal) { |
|
| 379 | - throw new DAV\Exception('Principal not found'); |
|
| 380 | - } |
|
| 381 | - $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?'); |
|
| 382 | - $stmt->execute([$principal['id']]); |
|
| 383 | - |
|
| 384 | - $result = []; |
|
| 385 | - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 386 | - $result[] = $row['uri']; |
|
| 387 | - } |
|
| 388 | - |
|
| 389 | - return $result; |
|
| 390 | - } |
|
| 391 | - |
|
| 392 | - /** |
|
| 393 | - * Updates the list of group members for a group principal. |
|
| 394 | - * |
|
| 395 | - * The principals should be passed as a list of uri's. |
|
| 396 | - * |
|
| 397 | - * @param string $principal |
|
| 398 | - */ |
|
| 399 | - public function setGroupMemberSet($principal, array $members) |
|
| 400 | - { |
|
| 401 | - // Grabbing the list of principal id's. |
|
| 402 | - $stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? '.str_repeat(', ? ', count($members)).');'); |
|
| 403 | - $stmt->execute(array_merge([$principal], $members)); |
|
| 404 | - |
|
| 405 | - $memberIds = []; |
|
| 406 | - $principalId = null; |
|
| 407 | - |
|
| 408 | - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 409 | - if ($row['uri'] == $principal) { |
|
| 410 | - $principalId = $row['id']; |
|
| 411 | - } else { |
|
| 412 | - $memberIds[] = $row['id']; |
|
| 413 | - } |
|
| 414 | - } |
|
| 415 | - if (!$principalId) { |
|
| 416 | - throw new DAV\Exception('Principal not found'); |
|
| 417 | - } |
|
| 418 | - // Wiping out old members |
|
| 419 | - $stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;'); |
|
| 420 | - $stmt->execute([$principalId]); |
|
| 421 | - |
|
| 422 | - foreach ($memberIds as $memberId) { |
|
| 423 | - $stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);'); |
|
| 424 | - $stmt->execute([$principalId, $memberId]); |
|
| 425 | - } |
|
| 426 | - } |
|
| 427 | - |
|
| 428 | - /** |
|
| 429 | - * Creates a new principal. |
|
| 430 | - * |
|
| 431 | - * This method receives a full path for the new principal. The mkCol object |
|
| 432 | - * contains any additional webdav properties specified during the creation |
|
| 433 | - * of the principal. |
|
| 434 | - * |
|
| 435 | - * @param string $path |
|
| 436 | - */ |
|
| 437 | - public function createPrincipal($path, MkCol $mkCol) |
|
| 438 | - { |
|
| 439 | - $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (uri) VALUES (?)'); |
|
| 440 | - $stmt->execute([$path]); |
|
| 441 | - $this->updatePrincipal($path, $mkCol); |
|
| 442 | - } |
|
| 60 | + '{http://sabredav.org/ns}email-address' => [ |
|
| 61 | + 'dbField' => 'email', |
|
| 62 | + ], |
|
| 63 | + ]; |
|
| 64 | + |
|
| 65 | + /** |
|
| 66 | + * Sets up the backend. |
|
| 67 | + */ |
|
| 68 | + public function __construct(\PDO $pdo) |
|
| 69 | + { |
|
| 70 | + $this->pdo = $pdo; |
|
| 71 | + } |
|
| 72 | + |
|
| 73 | + /** |
|
| 74 | + * Returns a list of principals based on a prefix. |
|
| 75 | + * |
|
| 76 | + * This prefix will often contain something like 'principals'. You are only |
|
| 77 | + * expected to return principals that are in this base path. |
|
| 78 | + * |
|
| 79 | + * You are expected to return at least a 'uri' for every user, you can |
|
| 80 | + * return any additional properties if you wish so. Common properties are: |
|
| 81 | + * {DAV:}displayname |
|
| 82 | + * {http://sabredav.org/ns}email-address - This is a custom SabreDAV |
|
| 83 | + * field that's actualy injected in a number of other properties. If |
|
| 84 | + * you have an email address, use this property. |
|
| 85 | + * |
|
| 86 | + * @param string $prefixPath |
|
| 87 | + * |
|
| 88 | + * @return array |
|
| 89 | + */ |
|
| 90 | + public function getPrincipalsByPrefix($prefixPath) |
|
| 91 | + { |
|
| 92 | + $fields = [ |
|
| 93 | + 'uri', |
|
| 94 | + ]; |
|
| 95 | + |
|
| 96 | + foreach ($this->fieldMap as $key => $value) { |
|
| 97 | + $fields[] = $value['dbField']; |
|
| 98 | + } |
|
| 99 | + $result = $this->pdo->query('SELECT '.implode(',', $fields).' FROM '.$this->tableName); |
|
| 100 | + |
|
| 101 | + $principals = []; |
|
| 102 | + |
|
| 103 | + while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { |
|
| 104 | + // Checking if the principal is in the prefix |
|
| 105 | + list($rowPrefix) = Uri\split($row['uri']); |
|
| 106 | + if ($rowPrefix !== $prefixPath) { |
|
| 107 | + continue; |
|
| 108 | + } |
|
| 109 | + |
|
| 110 | + $principal = [ |
|
| 111 | + 'uri' => $row['uri'], |
|
| 112 | + ]; |
|
| 113 | + foreach ($this->fieldMap as $key => $value) { |
|
| 114 | + if ($row[$value['dbField']]) { |
|
| 115 | + $principal[$key] = $row[$value['dbField']]; |
|
| 116 | + } |
|
| 117 | + } |
|
| 118 | + $principals[] = $principal; |
|
| 119 | + } |
|
| 120 | + |
|
| 121 | + return $principals; |
|
| 122 | + } |
|
| 123 | + |
|
| 124 | + /** |
|
| 125 | + * Returns a specific principal, specified by it's path. |
|
| 126 | + * The returned structure should be the exact same as from |
|
| 127 | + * getPrincipalsByPrefix. |
|
| 128 | + * |
|
| 129 | + * @param string $path |
|
| 130 | + * |
|
| 131 | + * @return array |
|
| 132 | + */ |
|
| 133 | + public function getPrincipalByPath($path) |
|
| 134 | + { |
|
| 135 | + $fields = [ |
|
| 136 | + 'id', |
|
| 137 | + 'uri', |
|
| 138 | + ]; |
|
| 139 | + |
|
| 140 | + foreach ($this->fieldMap as $key => $value) { |
|
| 141 | + $fields[] = $value['dbField']; |
|
| 142 | + } |
|
| 143 | + $stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).' FROM '.$this->tableName.' WHERE uri = ?'); |
|
| 144 | + $stmt->execute([$path]); |
|
| 145 | + |
|
| 146 | + $row = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
| 147 | + if (!$row) { |
|
| 148 | + return; |
|
| 149 | + } |
|
| 150 | + |
|
| 151 | + $principal = [ |
|
| 152 | + 'id' => $row['id'], |
|
| 153 | + 'uri' => $row['uri'], |
|
| 154 | + ]; |
|
| 155 | + foreach ($this->fieldMap as $key => $value) { |
|
| 156 | + if ($row[$value['dbField']]) { |
|
| 157 | + $principal[$key] = $row[$value['dbField']]; |
|
| 158 | + } |
|
| 159 | + } |
|
| 160 | + |
|
| 161 | + return $principal; |
|
| 162 | + } |
|
| 163 | + |
|
| 164 | + /** |
|
| 165 | + * Updates one ore more webdav properties on a principal. |
|
| 166 | + * |
|
| 167 | + * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
| 168 | + * To do the actual updates, you must tell this object which properties |
|
| 169 | + * you're going to process with the handle() method. |
|
| 170 | + * |
|
| 171 | + * Calling the handle method is like telling the PropPatch object "I |
|
| 172 | + * promise I can handle updating this property". |
|
| 173 | + * |
|
| 174 | + * Read the PropPatch documentation for more info and examples. |
|
| 175 | + * |
|
| 176 | + * @param string $path |
|
| 177 | + */ |
|
| 178 | + public function updatePrincipal($path, DAV\PropPatch $propPatch) |
|
| 179 | + { |
|
| 180 | + $propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($path) { |
|
| 181 | + $query = 'UPDATE '.$this->tableName.' SET '; |
|
| 182 | + $first = true; |
|
| 183 | + |
|
| 184 | + $values = []; |
|
| 185 | + |
|
| 186 | + foreach ($properties as $key => $value) { |
|
| 187 | + $dbField = $this->fieldMap[$key]['dbField']; |
|
| 188 | + |
|
| 189 | + if (!$first) { |
|
| 190 | + $query .= ', '; |
|
| 191 | + } |
|
| 192 | + $first = false; |
|
| 193 | + $query .= $dbField.' = :'.$dbField; |
|
| 194 | + $values[$dbField] = $value; |
|
| 195 | + } |
|
| 196 | + |
|
| 197 | + $query .= ' WHERE uri = :uri'; |
|
| 198 | + $values['uri'] = $path; |
|
| 199 | + |
|
| 200 | + $stmt = $this->pdo->prepare($query); |
|
| 201 | + $stmt->execute($values); |
|
| 202 | + |
|
| 203 | + return true; |
|
| 204 | + }); |
|
| 205 | + } |
|
| 206 | + |
|
| 207 | + /** |
|
| 208 | + * This method is used to search for principals matching a set of |
|
| 209 | + * properties. |
|
| 210 | + * |
|
| 211 | + * This search is specifically used by RFC3744's principal-property-search |
|
| 212 | + * REPORT. |
|
| 213 | + * |
|
| 214 | + * The actual search should be a unicode-non-case-sensitive search. The |
|
| 215 | + * keys in searchProperties are the WebDAV property names, while the values |
|
| 216 | + * are the property values to search on. |
|
| 217 | + * |
|
| 218 | + * By default, if multiple properties are submitted to this method, the |
|
| 219 | + * various properties should be combined with 'AND'. If $test is set to |
|
| 220 | + * 'anyof', it should be combined using 'OR'. |
|
| 221 | + * |
|
| 222 | + * This method should simply return an array with full principal uri's. |
|
| 223 | + * |
|
| 224 | + * If somebody attempted to search on a property the backend does not |
|
| 225 | + * support, you should simply return 0 results. |
|
| 226 | + * |
|
| 227 | + * You can also just return 0 results if you choose to not support |
|
| 228 | + * searching at all, but keep in mind that this may stop certain features |
|
| 229 | + * from working. |
|
| 230 | + * |
|
| 231 | + * @param string $prefixPath |
|
| 232 | + * @param string $test |
|
| 233 | + * |
|
| 234 | + * @return array |
|
| 235 | + */ |
|
| 236 | + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') |
|
| 237 | + { |
|
| 238 | + if (0 == count($searchProperties)) { |
|
| 239 | + return []; |
|
| 240 | + } //No criteria |
|
| 241 | + |
|
| 242 | + $query = 'SELECT uri FROM '.$this->tableName.' WHERE '; |
|
| 243 | + $values = []; |
|
| 244 | + foreach ($searchProperties as $property => $value) { |
|
| 245 | + switch ($property) { |
|
| 246 | + case '{DAV:}displayname': |
|
| 247 | + $column = 'displayname'; |
|
| 248 | + break; |
|
| 249 | + case '{http://sabredav.org/ns}email-address': |
|
| 250 | + $column = 'email'; |
|
| 251 | + break; |
|
| 252 | + default: |
|
| 253 | + // Unsupported property |
|
| 254 | + return []; |
|
| 255 | + } |
|
| 256 | + if (count($values) > 0) { |
|
| 257 | + $query .= (0 == strcmp($test, 'anyof') ? ' OR ' : ' AND '); |
|
| 258 | + } |
|
| 259 | + $query .= 'lower('.$column.') LIKE lower(?)'; |
|
| 260 | + $values[] = '%'.$value.'%'; |
|
| 261 | + } |
|
| 262 | + $stmt = $this->pdo->prepare($query); |
|
| 263 | + $stmt->execute($values); |
|
| 264 | + |
|
| 265 | + $principals = []; |
|
| 266 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 267 | + // Checking if the principal is in the prefix |
|
| 268 | + list($rowPrefix) = Uri\split($row['uri']); |
|
| 269 | + if ($rowPrefix !== $prefixPath) { |
|
| 270 | + continue; |
|
| 271 | + } |
|
| 272 | + |
|
| 273 | + $principals[] = $row['uri']; |
|
| 274 | + } |
|
| 275 | + |
|
| 276 | + return $principals; |
|
| 277 | + } |
|
| 278 | + |
|
| 279 | + /** |
|
| 280 | + * Finds a principal by its URI. |
|
| 281 | + * |
|
| 282 | + * This method may receive any type of uri, but mailto: addresses will be |
|
| 283 | + * the most common. |
|
| 284 | + * |
|
| 285 | + * Implementation of this API is optional. It is currently used by the |
|
| 286 | + * CalDAV system to find principals based on their email addresses. If this |
|
| 287 | + * API is not implemented, some features may not work correctly. |
|
| 288 | + * |
|
| 289 | + * This method must return a relative principal path, or null, if the |
|
| 290 | + * principal was not found or you refuse to find it. |
|
| 291 | + * |
|
| 292 | + * @param string $uri |
|
| 293 | + * @param string $principalPrefix |
|
| 294 | + * |
|
| 295 | + * @return string |
|
| 296 | + */ |
|
| 297 | + public function findByUri($uri, $principalPrefix) |
|
| 298 | + { |
|
| 299 | + $uriParts = Uri\parse($uri); |
|
| 300 | + |
|
| 301 | + // Only two types of uri are supported : |
|
| 302 | + // - the "mailto:" scheme with some non-empty address |
|
| 303 | + // - a principals uri, in the form "principals/NAME" |
|
| 304 | + // In both cases, `path` must not be empty. |
|
| 305 | + if (empty($uriParts['path'])) { |
|
| 306 | + return null; |
|
| 307 | + } |
|
| 308 | + |
|
| 309 | + $uri = null; |
|
| 310 | + if ('mailto' === $uriParts['scheme']) { |
|
| 311 | + $query = 'SELECT uri FROM '.$this->tableName.' WHERE lower(email)=lower(?)'; |
|
| 312 | + $stmt = $this->pdo->prepare($query); |
|
| 313 | + $stmt->execute([$uriParts['path']]); |
|
| 314 | + |
|
| 315 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 316 | + // Checking if the principal is in the prefix |
|
| 317 | + list($rowPrefix) = Uri\split($row['uri']); |
|
| 318 | + if ($rowPrefix !== $principalPrefix) { |
|
| 319 | + continue; |
|
| 320 | + } |
|
| 321 | + |
|
| 322 | + $uri = $row['uri']; |
|
| 323 | + break; //Stop on first match |
|
| 324 | + } |
|
| 325 | + } else { |
|
| 326 | + $pathParts = Uri\split($uriParts['path']); // We can do this since $uriParts['path'] is not null |
|
| 327 | + |
|
| 328 | + if (2 === count($pathParts) && $pathParts[0] === $principalPrefix) { |
|
| 329 | + // Checking that this uri exists |
|
| 330 | + $query = 'SELECT * FROM '.$this->tableName.' WHERE uri = ?'; |
|
| 331 | + $stmt = $this->pdo->prepare($query); |
|
| 332 | + $stmt->execute([$uriParts['path']]); |
|
| 333 | + $rows = $stmt->fetchAll(); |
|
| 334 | + |
|
| 335 | + if (count($rows) > 0) { |
|
| 336 | + $uri = $uriParts['path']; |
|
| 337 | + } |
|
| 338 | + } |
|
| 339 | + } |
|
| 340 | + |
|
| 341 | + return $uri; |
|
| 342 | + } |
|
| 343 | + |
|
| 344 | + /** |
|
| 345 | + * Returns the list of members for a group-principal. |
|
| 346 | + * |
|
| 347 | + * @param string $principal |
|
| 348 | + * |
|
| 349 | + * @return array |
|
| 350 | + */ |
|
| 351 | + public function getGroupMemberSet($principal) |
|
| 352 | + { |
|
| 353 | + $principal = $this->getPrincipalByPath($principal); |
|
| 354 | + if (!$principal) { |
|
| 355 | + throw new DAV\Exception('Principal not found'); |
|
| 356 | + } |
|
| 357 | + $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?'); |
|
| 358 | + $stmt->execute([$principal['id']]); |
|
| 359 | + |
|
| 360 | + $result = []; |
|
| 361 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 362 | + $result[] = $row['uri']; |
|
| 363 | + } |
|
| 364 | + |
|
| 365 | + return $result; |
|
| 366 | + } |
|
| 367 | + |
|
| 368 | + /** |
|
| 369 | + * Returns the list of groups a principal is a member of. |
|
| 370 | + * |
|
| 371 | + * @param string $principal |
|
| 372 | + * |
|
| 373 | + * @return array |
|
| 374 | + */ |
|
| 375 | + public function getGroupMembership($principal) |
|
| 376 | + { |
|
| 377 | + $principal = $this->getPrincipalByPath($principal); |
|
| 378 | + if (!$principal) { |
|
| 379 | + throw new DAV\Exception('Principal not found'); |
|
| 380 | + } |
|
| 381 | + $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?'); |
|
| 382 | + $stmt->execute([$principal['id']]); |
|
| 383 | + |
|
| 384 | + $result = []; |
|
| 385 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 386 | + $result[] = $row['uri']; |
|
| 387 | + } |
|
| 388 | + |
|
| 389 | + return $result; |
|
| 390 | + } |
|
| 391 | + |
|
| 392 | + /** |
|
| 393 | + * Updates the list of group members for a group principal. |
|
| 394 | + * |
|
| 395 | + * The principals should be passed as a list of uri's. |
|
| 396 | + * |
|
| 397 | + * @param string $principal |
|
| 398 | + */ |
|
| 399 | + public function setGroupMemberSet($principal, array $members) |
|
| 400 | + { |
|
| 401 | + // Grabbing the list of principal id's. |
|
| 402 | + $stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? '.str_repeat(', ? ', count($members)).');'); |
|
| 403 | + $stmt->execute(array_merge([$principal], $members)); |
|
| 404 | + |
|
| 405 | + $memberIds = []; |
|
| 406 | + $principalId = null; |
|
| 407 | + |
|
| 408 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
| 409 | + if ($row['uri'] == $principal) { |
|
| 410 | + $principalId = $row['id']; |
|
| 411 | + } else { |
|
| 412 | + $memberIds[] = $row['id']; |
|
| 413 | + } |
|
| 414 | + } |
|
| 415 | + if (!$principalId) { |
|
| 416 | + throw new DAV\Exception('Principal not found'); |
|
| 417 | + } |
|
| 418 | + // Wiping out old members |
|
| 419 | + $stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;'); |
|
| 420 | + $stmt->execute([$principalId]); |
|
| 421 | + |
|
| 422 | + foreach ($memberIds as $memberId) { |
|
| 423 | + $stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);'); |
|
| 424 | + $stmt->execute([$principalId, $memberId]); |
|
| 425 | + } |
|
| 426 | + } |
|
| 427 | + |
|
| 428 | + /** |
|
| 429 | + * Creates a new principal. |
|
| 430 | + * |
|
| 431 | + * This method receives a full path for the new principal. The mkCol object |
|
| 432 | + * contains any additional webdav properties specified during the creation |
|
| 433 | + * of the principal. |
|
| 434 | + * |
|
| 435 | + * @param string $path |
|
| 436 | + */ |
|
| 437 | + public function createPrincipal($path, MkCol $mkCol) |
|
| 438 | + { |
|
| 439 | + $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (uri) VALUES (?)'); |
|
| 440 | + $stmt->execute([$path]); |
|
| 441 | + $this->updatePrincipal($path, $mkCol); |
|
| 442 | + } |
|
| 443 | 443 | } |
@@ -20,77 +20,77 @@ |
||
| 20 | 20 | */ |
| 21 | 21 | class PrincipalCollection extends AbstractPrincipalCollection implements IExtendedCollection, IACL |
| 22 | 22 | { |
| 23 | - use ACLTrait; |
|
| 23 | + use ACLTrait; |
|
| 24 | 24 | |
| 25 | - /** |
|
| 26 | - * This method returns a node for a principal. |
|
| 27 | - * |
|
| 28 | - * The passed array contains principal information, and is guaranteed to |
|
| 29 | - * at least contain a uri item. Other properties may or may not be |
|
| 30 | - * supplied by the authentication backend. |
|
| 31 | - * |
|
| 32 | - * @return \Sabre\DAV\INode |
|
| 33 | - */ |
|
| 34 | - public function getChildForPrincipal(array $principal) |
|
| 35 | - { |
|
| 36 | - return new Principal($this->principalBackend, $principal); |
|
| 37 | - } |
|
| 25 | + /** |
|
| 26 | + * This method returns a node for a principal. |
|
| 27 | + * |
|
| 28 | + * The passed array contains principal information, and is guaranteed to |
|
| 29 | + * at least contain a uri item. Other properties may or may not be |
|
| 30 | + * supplied by the authentication backend. |
|
| 31 | + * |
|
| 32 | + * @return \Sabre\DAV\INode |
|
| 33 | + */ |
|
| 34 | + public function getChildForPrincipal(array $principal) |
|
| 35 | + { |
|
| 36 | + return new Principal($this->principalBackend, $principal); |
|
| 37 | + } |
|
| 38 | 38 | |
| 39 | - /** |
|
| 40 | - * Creates a new collection. |
|
| 41 | - * |
|
| 42 | - * This method will receive a MkCol object with all the information about |
|
| 43 | - * the new collection that's being created. |
|
| 44 | - * |
|
| 45 | - * The MkCol object contains information about the resourceType of the new |
|
| 46 | - * collection. If you don't support the specified resourceType, you should |
|
| 47 | - * throw Exception\InvalidResourceType. |
|
| 48 | - * |
|
| 49 | - * The object also contains a list of WebDAV properties for the new |
|
| 50 | - * collection. |
|
| 51 | - * |
|
| 52 | - * You should call the handle() method on this object to specify exactly |
|
| 53 | - * which properties you are storing. This allows the system to figure out |
|
| 54 | - * exactly which properties you didn't store, which in turn allows other |
|
| 55 | - * plugins (such as the propertystorage plugin) to handle storing the |
|
| 56 | - * property for you. |
|
| 57 | - * |
|
| 58 | - * @param string $name |
|
| 59 | - * |
|
| 60 | - * @throws InvalidResourceType |
|
| 61 | - */ |
|
| 62 | - public function createExtendedCollection($name, MkCol $mkCol) |
|
| 63 | - { |
|
| 64 | - if (!$mkCol->hasResourceType('{DAV:}principal')) { |
|
| 65 | - throw new InvalidResourceType('Only resources of type {DAV:}principal may be created here'); |
|
| 66 | - } |
|
| 39 | + /** |
|
| 40 | + * Creates a new collection. |
|
| 41 | + * |
|
| 42 | + * This method will receive a MkCol object with all the information about |
|
| 43 | + * the new collection that's being created. |
|
| 44 | + * |
|
| 45 | + * The MkCol object contains information about the resourceType of the new |
|
| 46 | + * collection. If you don't support the specified resourceType, you should |
|
| 47 | + * throw Exception\InvalidResourceType. |
|
| 48 | + * |
|
| 49 | + * The object also contains a list of WebDAV properties for the new |
|
| 50 | + * collection. |
|
| 51 | + * |
|
| 52 | + * You should call the handle() method on this object to specify exactly |
|
| 53 | + * which properties you are storing. This allows the system to figure out |
|
| 54 | + * exactly which properties you didn't store, which in turn allows other |
|
| 55 | + * plugins (such as the propertystorage plugin) to handle storing the |
|
| 56 | + * property for you. |
|
| 57 | + * |
|
| 58 | + * @param string $name |
|
| 59 | + * |
|
| 60 | + * @throws InvalidResourceType |
|
| 61 | + */ |
|
| 62 | + public function createExtendedCollection($name, MkCol $mkCol) |
|
| 63 | + { |
|
| 64 | + if (!$mkCol->hasResourceType('{DAV:}principal')) { |
|
| 65 | + throw new InvalidResourceType('Only resources of type {DAV:}principal may be created here'); |
|
| 66 | + } |
|
| 67 | 67 | |
| 68 | - $this->principalBackend->createPrincipal( |
|
| 69 | - $this->principalPrefix.'/'.$name, |
|
| 70 | - $mkCol |
|
| 71 | - ); |
|
| 72 | - } |
|
| 68 | + $this->principalBackend->createPrincipal( |
|
| 69 | + $this->principalPrefix.'/'.$name, |
|
| 70 | + $mkCol |
|
| 71 | + ); |
|
| 72 | + } |
|
| 73 | 73 | |
| 74 | - /** |
|
| 75 | - * Returns a list of ACE's for this node. |
|
| 76 | - * |
|
| 77 | - * Each ACE has the following properties: |
|
| 78 | - * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are |
|
| 79 | - * currently the only supported privileges |
|
| 80 | - * * 'principal', a url to the principal who owns the node |
|
| 81 | - * * 'protected' (optional), indicating that this ACE is not allowed to |
|
| 82 | - * be updated. |
|
| 83 | - * |
|
| 84 | - * @return array |
|
| 85 | - */ |
|
| 86 | - public function getACL() |
|
| 87 | - { |
|
| 88 | - return [ |
|
| 89 | - [ |
|
| 90 | - 'principal' => '{DAV:}authenticated', |
|
| 91 | - 'privilege' => '{DAV:}read', |
|
| 92 | - 'protected' => true, |
|
| 93 | - ], |
|
| 94 | - ]; |
|
| 95 | - } |
|
| 74 | + /** |
|
| 75 | + * Returns a list of ACE's for this node. |
|
| 76 | + * |
|
| 77 | + * Each ACE has the following properties: |
|
| 78 | + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are |
|
| 79 | + * currently the only supported privileges |
|
| 80 | + * * 'principal', a url to the principal who owns the node |
|
| 81 | + * * 'protected' (optional), indicating that this ACE is not allowed to |
|
| 82 | + * be updated. |
|
| 83 | + * |
|
| 84 | + * @return array |
|
| 85 | + */ |
|
| 86 | + public function getACL() |
|
| 87 | + { |
|
| 88 | + return [ |
|
| 89 | + [ |
|
| 90 | + 'principal' => '{DAV:}authenticated', |
|
| 91 | + 'privilege' => '{DAV:}read', |
|
| 92 | + 'protected' => true, |
|
| 93 | + ], |
|
| 94 | + ]; |
|
| 95 | + } |
|
| 96 | 96 | } |
@@ -17,22 +17,22 @@ |
||
| 17 | 17 | */ |
| 18 | 18 | interface ICopyTarget extends ICollection |
| 19 | 19 | { |
| 20 | - /** |
|
| 21 | - * Copies a node into this collection. |
|
| 22 | - * |
|
| 23 | - * It is up to the implementors to: |
|
| 24 | - * 1. Create the new resource. |
|
| 25 | - * 2. Copy the data and any properties. |
|
| 26 | - * |
|
| 27 | - * If you return true from this function, the assumption |
|
| 28 | - * is that the copy was successful. |
|
| 29 | - * If you return false, sabre/dav will handle the copy itself. |
|
| 30 | - * |
|
| 31 | - * @param string $targetName new local file/collection name |
|
| 32 | - * @param string $sourcePath Full path to source node |
|
| 33 | - * @param INode $sourceNode Source node itself |
|
| 34 | - * |
|
| 35 | - * @return bool |
|
| 36 | - */ |
|
| 37 | - public function copyInto($targetName, $sourcePath, INode $sourceNode); |
|
| 20 | + /** |
|
| 21 | + * Copies a node into this collection. |
|
| 22 | + * |
|
| 23 | + * It is up to the implementors to: |
|
| 24 | + * 1. Create the new resource. |
|
| 25 | + * 2. Copy the data and any properties. |
|
| 26 | + * |
|
| 27 | + * If you return true from this function, the assumption |
|
| 28 | + * is that the copy was successful. |
|
| 29 | + * If you return false, sabre/dav will handle the copy itself. |
|
| 30 | + * |
|
| 31 | + * @param string $targetName new local file/collection name |
|
| 32 | + * @param string $sourcePath Full path to source node |
|
| 33 | + * @param INode $sourceNode Source node itself |
|
| 34 | + * |
|
| 35 | + * @return bool |
|
| 36 | + */ |
|
| 37 | + public function copyInto($targetName, $sourcePath, INode $sourceNode); |
|
| 38 | 38 | } |
@@ -21,26 +21,26 @@ |
||
| 21 | 21 | */ |
| 22 | 22 | interface IMoveTarget extends ICollection |
| 23 | 23 | { |
| 24 | - /** |
|
| 25 | - * Moves a node into this collection. |
|
| 26 | - * |
|
| 27 | - * It is up to the implementors to: |
|
| 28 | - * 1. Create the new resource. |
|
| 29 | - * 2. Remove the old resource. |
|
| 30 | - * 3. Transfer any properties or other data. |
|
| 31 | - * |
|
| 32 | - * Generally you should make very sure that your collection can easily move |
|
| 33 | - * the move. |
|
| 34 | - * |
|
| 35 | - * If you don't, just return false, which will trigger sabre/dav to handle |
|
| 36 | - * the move itself. If you return true from this function, the assumption |
|
| 37 | - * is that the move was successful. |
|
| 38 | - * |
|
| 39 | - * @param string $targetName new local file/collection name |
|
| 40 | - * @param string $sourcePath Full path to source node |
|
| 41 | - * @param INode $sourceNode Source node itself |
|
| 42 | - * |
|
| 43 | - * @return bool |
|
| 44 | - */ |
|
| 45 | - public function moveInto($targetName, $sourcePath, INode $sourceNode); |
|
| 24 | + /** |
|
| 25 | + * Moves a node into this collection. |
|
| 26 | + * |
|
| 27 | + * It is up to the implementors to: |
|
| 28 | + * 1. Create the new resource. |
|
| 29 | + * 2. Remove the old resource. |
|
| 30 | + * 3. Transfer any properties or other data. |
|
| 31 | + * |
|
| 32 | + * Generally you should make very sure that your collection can easily move |
|
| 33 | + * the move. |
|
| 34 | + * |
|
| 35 | + * If you don't, just return false, which will trigger sabre/dav to handle |
|
| 36 | + * the move itself. If you return true from this function, the assumption |
|
| 37 | + * is that the move was successful. |
|
| 38 | + * |
|
| 39 | + * @param string $targetName new local file/collection name |
|
| 40 | + * @param string $sourcePath Full path to source node |
|
| 41 | + * @param INode $sourceNode Source node itself |
|
| 42 | + * |
|
| 43 | + * @return bool |
|
| 44 | + */ |
|
| 45 | + public function moveInto($targetName, $sourcePath, INode $sourceNode); |
|
| 46 | 46 | } |
@@ -18,92 +18,92 @@ |
||
| 18 | 18 | */ |
| 19 | 19 | class SimpleCollection extends Collection |
| 20 | 20 | { |
| 21 | - /** |
|
| 22 | - * List of childnodes. |
|
| 23 | - * |
|
| 24 | - * @var INode[] |
|
| 25 | - */ |
|
| 26 | - protected $children = []; |
|
| 21 | + /** |
|
| 22 | + * List of childnodes. |
|
| 23 | + * |
|
| 24 | + * @var INode[] |
|
| 25 | + */ |
|
| 26 | + protected $children = []; |
|
| 27 | 27 | |
| 28 | - /** |
|
| 29 | - * Name of this resource. |
|
| 30 | - * |
|
| 31 | - * @var string |
|
| 32 | - */ |
|
| 33 | - protected $name; |
|
| 28 | + /** |
|
| 29 | + * Name of this resource. |
|
| 30 | + * |
|
| 31 | + * @var string |
|
| 32 | + */ |
|
| 33 | + protected $name; |
|
| 34 | 34 | |
| 35 | - /** |
|
| 36 | - * Creates this node. |
|
| 37 | - * |
|
| 38 | - * The name of the node must be passed, child nodes can also be passed. |
|
| 39 | - * This nodes must be instances of INode |
|
| 40 | - * |
|
| 41 | - * @param string $name |
|
| 42 | - * @param INode[] $children |
|
| 43 | - */ |
|
| 44 | - public function __construct($name, array $children = []) |
|
| 45 | - { |
|
| 46 | - $this->name = $name; |
|
| 47 | - foreach ($children as $key => $child) { |
|
| 48 | - if (is_string($child)) { |
|
| 49 | - $child = new SimpleFile($key, $child); |
|
| 50 | - } elseif (is_array($child)) { |
|
| 51 | - $child = new self($key, $child); |
|
| 52 | - } elseif (!$child instanceof INode) { |
|
| 53 | - throw new InvalidArgumentException('Children must be specified as strings, arrays or instances of Sabre\DAV\INode'); |
|
| 54 | - } |
|
| 55 | - $this->addChild($child); |
|
| 56 | - } |
|
| 57 | - } |
|
| 35 | + /** |
|
| 36 | + * Creates this node. |
|
| 37 | + * |
|
| 38 | + * The name of the node must be passed, child nodes can also be passed. |
|
| 39 | + * This nodes must be instances of INode |
|
| 40 | + * |
|
| 41 | + * @param string $name |
|
| 42 | + * @param INode[] $children |
|
| 43 | + */ |
|
| 44 | + public function __construct($name, array $children = []) |
|
| 45 | + { |
|
| 46 | + $this->name = $name; |
|
| 47 | + foreach ($children as $key => $child) { |
|
| 48 | + if (is_string($child)) { |
|
| 49 | + $child = new SimpleFile($key, $child); |
|
| 50 | + } elseif (is_array($child)) { |
|
| 51 | + $child = new self($key, $child); |
|
| 52 | + } elseif (!$child instanceof INode) { |
|
| 53 | + throw new InvalidArgumentException('Children must be specified as strings, arrays or instances of Sabre\DAV\INode'); |
|
| 54 | + } |
|
| 55 | + $this->addChild($child); |
|
| 56 | + } |
|
| 57 | + } |
|
| 58 | 58 | |
| 59 | - /** |
|
| 60 | - * Adds a new childnode to this collection. |
|
| 61 | - */ |
|
| 62 | - public function addChild(INode $child) |
|
| 63 | - { |
|
| 64 | - $this->children[$child->getName()] = $child; |
|
| 65 | - } |
|
| 59 | + /** |
|
| 60 | + * Adds a new childnode to this collection. |
|
| 61 | + */ |
|
| 62 | + public function addChild(INode $child) |
|
| 63 | + { |
|
| 64 | + $this->children[$child->getName()] = $child; |
|
| 65 | + } |
|
| 66 | 66 | |
| 67 | - /** |
|
| 68 | - * Returns the name of the collection. |
|
| 69 | - * |
|
| 70 | - * @return string |
|
| 71 | - */ |
|
| 72 | - public function getName() |
|
| 73 | - { |
|
| 74 | - return $this->name; |
|
| 75 | - } |
|
| 67 | + /** |
|
| 68 | + * Returns the name of the collection. |
|
| 69 | + * |
|
| 70 | + * @return string |
|
| 71 | + */ |
|
| 72 | + public function getName() |
|
| 73 | + { |
|
| 74 | + return $this->name; |
|
| 75 | + } |
|
| 76 | 76 | |
| 77 | - /** |
|
| 78 | - * Returns a child object, by its name. |
|
| 79 | - * |
|
| 80 | - * This method makes use of the getChildren method to grab all the child nodes, and compares the name. |
|
| 81 | - * Generally its wise to override this, as this can usually be optimized |
|
| 82 | - * |
|
| 83 | - * This method must throw Sabre\DAV\Exception\NotFound if the node does not |
|
| 84 | - * exist. |
|
| 85 | - * |
|
| 86 | - * @param string $name |
|
| 87 | - * |
|
| 88 | - * @throws Exception\NotFound |
|
| 89 | - * |
|
| 90 | - * @return INode |
|
| 91 | - */ |
|
| 92 | - public function getChild($name) |
|
| 93 | - { |
|
| 94 | - if (isset($this->children[$name])) { |
|
| 95 | - return $this->children[$name]; |
|
| 96 | - } |
|
| 97 | - throw new Exception\NotFound('File not found: '.$name.' in \''.$this->getName().'\''); |
|
| 98 | - } |
|
| 77 | + /** |
|
| 78 | + * Returns a child object, by its name. |
|
| 79 | + * |
|
| 80 | + * This method makes use of the getChildren method to grab all the child nodes, and compares the name. |
|
| 81 | + * Generally its wise to override this, as this can usually be optimized |
|
| 82 | + * |
|
| 83 | + * This method must throw Sabre\DAV\Exception\NotFound if the node does not |
|
| 84 | + * exist. |
|
| 85 | + * |
|
| 86 | + * @param string $name |
|
| 87 | + * |
|
| 88 | + * @throws Exception\NotFound |
|
| 89 | + * |
|
| 90 | + * @return INode |
|
| 91 | + */ |
|
| 92 | + public function getChild($name) |
|
| 93 | + { |
|
| 94 | + if (isset($this->children[$name])) { |
|
| 95 | + return $this->children[$name]; |
|
| 96 | + } |
|
| 97 | + throw new Exception\NotFound('File not found: '.$name.' in \''.$this->getName().'\''); |
|
| 98 | + } |
|
| 99 | 99 | |
| 100 | - /** |
|
| 101 | - * Returns a list of children for this collection. |
|
| 102 | - * |
|
| 103 | - * @return INode[] |
|
| 104 | - */ |
|
| 105 | - public function getChildren() |
|
| 106 | - { |
|
| 107 | - return array_values($this->children); |
|
| 108 | - } |
|
| 100 | + /** |
|
| 101 | + * Returns a list of children for this collection. |
|
| 102 | + * |
|
| 103 | + * @return INode[] |
|
| 104 | + */ |
|
| 105 | + public function getChildren() |
|
| 106 | + { |
|
| 107 | + return array_values($this->children); |
|
| 108 | + } |
|
| 109 | 109 | } |
@@ -25,1656 +25,1656 @@ |
||
| 25 | 25 | */ |
| 26 | 26 | class Server implements LoggerAwareInterface, EmitterInterface |
| 27 | 27 | { |
| 28 | - use LoggerAwareTrait; |
|
| 29 | - use WildcardEmitterTrait; |
|
| 30 | - |
|
| 31 | - /** |
|
| 32 | - * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree. |
|
| 33 | - */ |
|
| 34 | - const DEPTH_INFINITY = -1; |
|
| 35 | - |
|
| 36 | - /** |
|
| 37 | - * XML namespace for all SabreDAV related elements. |
|
| 38 | - */ |
|
| 39 | - const NS_SABREDAV = 'http://sabredav.org/ns'; |
|
| 40 | - |
|
| 41 | - /** |
|
| 42 | - * The tree object. |
|
| 43 | - * |
|
| 44 | - * @var Tree |
|
| 45 | - */ |
|
| 46 | - public $tree; |
|
| 47 | - |
|
| 48 | - /** |
|
| 49 | - * The base uri. |
|
| 50 | - * |
|
| 51 | - * @var string |
|
| 52 | - */ |
|
| 53 | - protected $baseUri = null; |
|
| 54 | - |
|
| 55 | - /** |
|
| 56 | - * httpResponse. |
|
| 57 | - * |
|
| 58 | - * @var HTTP\Response |
|
| 59 | - */ |
|
| 60 | - public $httpResponse; |
|
| 61 | - |
|
| 62 | - /** |
|
| 63 | - * httpRequest. |
|
| 64 | - * |
|
| 65 | - * @var HTTP\Request |
|
| 66 | - */ |
|
| 67 | - public $httpRequest; |
|
| 68 | - |
|
| 69 | - /** |
|
| 70 | - * PHP HTTP Sapi. |
|
| 71 | - * |
|
| 72 | - * @var HTTP\Sapi |
|
| 73 | - */ |
|
| 74 | - public $sapi; |
|
| 75 | - |
|
| 76 | - /** |
|
| 77 | - * The list of plugins. |
|
| 78 | - * |
|
| 79 | - * @var array |
|
| 80 | - */ |
|
| 81 | - protected $plugins = []; |
|
| 82 | - |
|
| 83 | - /** |
|
| 84 | - * This property will be filled with a unique string that describes the |
|
| 85 | - * transaction. This is useful for performance measuring and logging |
|
| 86 | - * purposes. |
|
| 87 | - * |
|
| 88 | - * By default it will just fill it with a lowercased HTTP method name, but |
|
| 89 | - * plugins override this. For example, the WebDAV-Sync sync-collection |
|
| 90 | - * report will set this to 'report-sync-collection'. |
|
| 91 | - * |
|
| 92 | - * @var string |
|
| 93 | - */ |
|
| 94 | - public $transactionType; |
|
| 95 | - |
|
| 96 | - /** |
|
| 97 | - * This is a list of properties that are always server-controlled, and |
|
| 98 | - * must not get modified with PROPPATCH. |
|
| 99 | - * |
|
| 100 | - * Plugins may add to this list. |
|
| 101 | - * |
|
| 102 | - * @var string[] |
|
| 103 | - */ |
|
| 104 | - public $protectedProperties = [ |
|
| 105 | - // RFC4918 |
|
| 106 | - '{DAV:}getcontentlength', |
|
| 107 | - '{DAV:}getetag', |
|
| 108 | - '{DAV:}getlastmodified', |
|
| 109 | - '{DAV:}lockdiscovery', |
|
| 110 | - '{DAV:}supportedlock', |
|
| 111 | - |
|
| 112 | - // RFC4331 |
|
| 113 | - '{DAV:}quota-available-bytes', |
|
| 114 | - '{DAV:}quota-used-bytes', |
|
| 115 | - |
|
| 116 | - // RFC3744 |
|
| 117 | - '{DAV:}supported-privilege-set', |
|
| 118 | - '{DAV:}current-user-privilege-set', |
|
| 119 | - '{DAV:}acl', |
|
| 120 | - '{DAV:}acl-restrictions', |
|
| 121 | - '{DAV:}inherited-acl-set', |
|
| 122 | - |
|
| 123 | - // RFC3253 |
|
| 124 | - '{DAV:}supported-method-set', |
|
| 125 | - '{DAV:}supported-report-set', |
|
| 126 | - |
|
| 127 | - // RFC6578 |
|
| 128 | - '{DAV:}sync-token', |
|
| 129 | - |
|
| 130 | - // calendarserver.org extensions |
|
| 131 | - '{http://calendarserver.org/ns/}ctag', |
|
| 132 | - |
|
| 133 | - // sabredav extensions |
|
| 134 | - '{http://sabredav.org/ns}sync-token', |
|
| 135 | - ]; |
|
| 136 | - |
|
| 137 | - /** |
|
| 138 | - * This is a flag that allow or not showing file, line and code |
|
| 139 | - * of the exception in the returned XML. |
|
| 140 | - * |
|
| 141 | - * @var bool |
|
| 142 | - */ |
|
| 143 | - public $debugExceptions = false; |
|
| 144 | - |
|
| 145 | - /** |
|
| 146 | - * This property allows you to automatically add the 'resourcetype' value |
|
| 147 | - * based on a node's classname or interface. |
|
| 148 | - * |
|
| 149 | - * The preset ensures that {DAV:}collection is automatically added for nodes |
|
| 150 | - * implementing Sabre\DAV\ICollection. |
|
| 151 | - * |
|
| 152 | - * @var array |
|
| 153 | - */ |
|
| 154 | - public $resourceTypeMapping = [ |
|
| 155 | - 'Sabre\\DAV\\ICollection' => '{DAV:}collection', |
|
| 156 | - ]; |
|
| 157 | - |
|
| 158 | - /** |
|
| 159 | - * This property allows the usage of Depth: infinity on PROPFIND requests. |
|
| 160 | - * |
|
| 161 | - * By default Depth: infinity is treated as Depth: 1. Allowing Depth: |
|
| 162 | - * infinity is potentially risky, as it allows a single client to do a full |
|
| 163 | - * index of the webdav server, which is an easy DoS attack vector. |
|
| 164 | - * |
|
| 165 | - * Only turn this on if you know what you're doing. |
|
| 166 | - * |
|
| 167 | - * @var bool |
|
| 168 | - */ |
|
| 169 | - public $enablePropfindDepthInfinity = false; |
|
| 170 | - |
|
| 171 | - /** |
|
| 172 | - * Reference to the XML utility object. |
|
| 173 | - * |
|
| 174 | - * @var Xml\Service |
|
| 175 | - */ |
|
| 176 | - public $xml; |
|
| 177 | - |
|
| 178 | - /** |
|
| 179 | - * If this setting is turned off, SabreDAV's version number will be hidden |
|
| 180 | - * from various places. |
|
| 181 | - * |
|
| 182 | - * Some people feel this is a good security measure. |
|
| 183 | - * |
|
| 184 | - * @var bool |
|
| 185 | - */ |
|
| 186 | - public static $exposeVersion = true; |
|
| 187 | - |
|
| 188 | - /** |
|
| 189 | - * If this setting is turned on, any multi status response on any PROPFIND will be streamed to the output buffer. |
|
| 190 | - * This will be beneficial for large result sets which will no longer consume a large amount of memory as well as |
|
| 191 | - * send back data to the client earlier. |
|
| 192 | - * |
|
| 193 | - * @var bool |
|
| 194 | - */ |
|
| 195 | - public static $streamMultiStatus = false; |
|
| 196 | - |
|
| 197 | - /** |
|
| 198 | - * Sets up the server. |
|
| 199 | - * |
|
| 200 | - * If a Sabre\DAV\Tree object is passed as an argument, it will |
|
| 201 | - * use it as the directory tree. If a Sabre\DAV\INode is passed, it |
|
| 202 | - * will create a Sabre\DAV\Tree and use the node as the root. |
|
| 203 | - * |
|
| 204 | - * If nothing is passed, a Sabre\DAV\SimpleCollection is created in |
|
| 205 | - * a Sabre\DAV\Tree. |
|
| 206 | - * |
|
| 207 | - * If an array is passed, we automatically create a root node, and use |
|
| 208 | - * the nodes in the array as top-level children. |
|
| 209 | - * |
|
| 210 | - * @param Tree|INode|array|null $treeOrNode The tree object |
|
| 211 | - * |
|
| 212 | - * @throws Exception |
|
| 213 | - */ |
|
| 214 | - public function __construct($treeOrNode = null, HTTP\Sapi $sapi = null) |
|
| 215 | - { |
|
| 216 | - if ($treeOrNode instanceof Tree) { |
|
| 217 | - $this->tree = $treeOrNode; |
|
| 218 | - } elseif ($treeOrNode instanceof INode) { |
|
| 219 | - $this->tree = new Tree($treeOrNode); |
|
| 220 | - } elseif (is_array($treeOrNode)) { |
|
| 221 | - $root = new SimpleCollection('root', $treeOrNode); |
|
| 222 | - $this->tree = new Tree($root); |
|
| 223 | - } elseif (is_null($treeOrNode)) { |
|
| 224 | - $root = new SimpleCollection('root'); |
|
| 225 | - $this->tree = new Tree($root); |
|
| 226 | - } else { |
|
| 227 | - 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'); |
|
| 228 | - } |
|
| 229 | - |
|
| 230 | - $this->xml = new Xml\Service(); |
|
| 231 | - $this->sapi = $sapi ?? new HTTP\Sapi(); |
|
| 232 | - $this->httpResponse = new HTTP\Response(); |
|
| 233 | - $this->httpRequest = $this->sapi->getRequest(); |
|
| 234 | - $this->addPlugin(new CorePlugin()); |
|
| 235 | - } |
|
| 236 | - |
|
| 237 | - /** |
|
| 238 | - * Starts the DAV Server. |
|
| 239 | - */ |
|
| 240 | - public function start() |
|
| 241 | - { |
|
| 242 | - try { |
|
| 243 | - // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an |
|
| 244 | - // origin, we must make sure we send back HTTP/1.0 if this was |
|
| 245 | - // requested. |
|
| 246 | - // This is mainly because nginx doesn't support Chunked Transfer |
|
| 247 | - // Encoding, and this forces the webserver SabreDAV is running on, |
|
| 248 | - // to buffer entire responses to calculate Content-Length. |
|
| 249 | - $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion()); |
|
| 250 | - |
|
| 251 | - // Setting the base url |
|
| 252 | - $this->httpRequest->setBaseUrl($this->getBaseUri()); |
|
| 253 | - $this->invokeMethod($this->httpRequest, $this->httpResponse); |
|
| 254 | - } catch (\Throwable $e) { |
|
| 255 | - try { |
|
| 256 | - $this->emit('exception', [$e]); |
|
| 257 | - } catch (\Exception $ignore) { |
|
| 258 | - } |
|
| 259 | - $DOM = new \DOMDocument('1.0', 'utf-8'); |
|
| 260 | - $DOM->formatOutput = true; |
|
| 261 | - |
|
| 262 | - $error = $DOM->createElementNS('DAV:', 'd:error'); |
|
| 263 | - $error->setAttribute('xmlns:s', self::NS_SABREDAV); |
|
| 264 | - $DOM->appendChild($error); |
|
| 265 | - |
|
| 266 | - $h = function ($v) { |
|
| 267 | - return htmlspecialchars((string) $v, ENT_NOQUOTES, 'UTF-8'); |
|
| 268 | - }; |
|
| 269 | - |
|
| 270 | - if (self::$exposeVersion) { |
|
| 271 | - $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION))); |
|
| 272 | - } |
|
| 273 | - |
|
| 274 | - $error->appendChild($DOM->createElement('s:exception', $h(get_class($e)))); |
|
| 275 | - $error->appendChild($DOM->createElement('s:message', $h($e->getMessage()))); |
|
| 276 | - if ($this->debugExceptions) { |
|
| 277 | - $error->appendChild($DOM->createElement('s:file', $h($e->getFile()))); |
|
| 278 | - $error->appendChild($DOM->createElement('s:line', $h($e->getLine()))); |
|
| 279 | - $error->appendChild($DOM->createElement('s:code', $h($e->getCode()))); |
|
| 280 | - $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString()))); |
|
| 281 | - } |
|
| 282 | - |
|
| 283 | - if ($this->debugExceptions) { |
|
| 284 | - $previous = $e; |
|
| 285 | - while ($previous = $previous->getPrevious()) { |
|
| 286 | - $xPrevious = $DOM->createElement('s:previous-exception'); |
|
| 287 | - $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous)))); |
|
| 288 | - $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage()))); |
|
| 289 | - $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile()))); |
|
| 290 | - $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine()))); |
|
| 291 | - $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode()))); |
|
| 292 | - $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString()))); |
|
| 293 | - $error->appendChild($xPrevious); |
|
| 294 | - } |
|
| 295 | - } |
|
| 296 | - |
|
| 297 | - if ($e instanceof Exception) { |
|
| 298 | - $httpCode = $e->getHTTPCode(); |
|
| 299 | - $e->serialize($this, $error); |
|
| 300 | - $headers = $e->getHTTPHeaders($this); |
|
| 301 | - } else { |
|
| 302 | - $httpCode = 500; |
|
| 303 | - $headers = []; |
|
| 304 | - } |
|
| 305 | - $headers['Content-Type'] = 'application/xml; charset=utf-8'; |
|
| 306 | - |
|
| 307 | - $this->httpResponse->setStatus($httpCode); |
|
| 308 | - $this->httpResponse->setHeaders($headers); |
|
| 309 | - $this->httpResponse->setBody($DOM->saveXML()); |
|
| 310 | - $this->sapi->sendResponse($this->httpResponse); |
|
| 311 | - } |
|
| 312 | - } |
|
| 313 | - |
|
| 314 | - /** |
|
| 315 | - * Alias of start(). |
|
| 316 | - * |
|
| 317 | - * @deprecated |
|
| 318 | - */ |
|
| 319 | - public function exec() |
|
| 320 | - { |
|
| 321 | - $this->start(); |
|
| 322 | - } |
|
| 323 | - |
|
| 324 | - /** |
|
| 325 | - * Sets the base server uri. |
|
| 326 | - * |
|
| 327 | - * @param string $uri |
|
| 328 | - */ |
|
| 329 | - public function setBaseUri($uri) |
|
| 330 | - { |
|
| 331 | - // If the baseUri does not end with a slash, we must add it |
|
| 332 | - if ('/' !== $uri[strlen($uri) - 1]) { |
|
| 333 | - $uri .= '/'; |
|
| 334 | - } |
|
| 335 | - |
|
| 336 | - $this->baseUri = $uri; |
|
| 337 | - } |
|
| 338 | - |
|
| 339 | - /** |
|
| 340 | - * Returns the base responding uri. |
|
| 341 | - * |
|
| 342 | - * @return string |
|
| 343 | - */ |
|
| 344 | - public function getBaseUri() |
|
| 345 | - { |
|
| 346 | - if (is_null($this->baseUri)) { |
|
| 347 | - $this->baseUri = $this->guessBaseUri(); |
|
| 348 | - } |
|
| 349 | - |
|
| 350 | - return $this->baseUri; |
|
| 351 | - } |
|
| 352 | - |
|
| 353 | - /** |
|
| 354 | - * This method attempts to detect the base uri. |
|
| 355 | - * Only the PATH_INFO variable is considered. |
|
| 356 | - * |
|
| 357 | - * If this variable is not set, the root (/) is assumed. |
|
| 358 | - * |
|
| 359 | - * @return string |
|
| 360 | - */ |
|
| 361 | - public function guessBaseUri() |
|
| 362 | - { |
|
| 363 | - $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO'); |
|
| 364 | - $uri = $this->httpRequest->getRawServerValue('REQUEST_URI'); |
|
| 365 | - |
|
| 366 | - // If PATH_INFO is found, we can assume it's accurate. |
|
| 367 | - if (!empty($pathInfo)) { |
|
| 368 | - // We need to make sure we ignore the QUERY_STRING part |
|
| 369 | - if ($pos = strpos($uri, '?')) { |
|
| 370 | - $uri = substr($uri, 0, $pos); |
|
| 371 | - } |
|
| 372 | - |
|
| 373 | - // PATH_INFO is only set for urls, such as: /example.php/path |
|
| 374 | - // in that case PATH_INFO contains '/path'. |
|
| 375 | - // Note that REQUEST_URI is percent encoded, while PATH_INFO is |
|
| 376 | - // not, Therefore they are only comparable if we first decode |
|
| 377 | - // REQUEST_INFO as well. |
|
| 378 | - $decodedUri = HTTP\decodePath($uri); |
|
| 379 | - |
|
| 380 | - // A simple sanity check: |
|
| 381 | - if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) { |
|
| 382 | - $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo)); |
|
| 383 | - |
|
| 384 | - return rtrim($baseUri, '/').'/'; |
|
| 385 | - } |
|
| 386 | - |
|
| 387 | - throw new Exception('The REQUEST_URI ('.$uri.') did not end with the contents of PATH_INFO ('.$pathInfo.'). This server might be misconfigured.'); |
|
| 388 | - } |
|
| 389 | - |
|
| 390 | - // The last fallback is that we're just going to assume the server root. |
|
| 391 | - return '/'; |
|
| 392 | - } |
|
| 393 | - |
|
| 394 | - /** |
|
| 395 | - * Adds a plugin to the server. |
|
| 396 | - * |
|
| 397 | - * For more information, console the documentation of Sabre\DAV\ServerPlugin |
|
| 398 | - */ |
|
| 399 | - public function addPlugin(ServerPlugin $plugin) |
|
| 400 | - { |
|
| 401 | - $this->plugins[$plugin->getPluginName()] = $plugin; |
|
| 402 | - $plugin->initialize($this); |
|
| 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 | - * |
|
| 412 | - * @return ServerPlugin |
|
| 413 | - */ |
|
| 414 | - public function getPlugin($name) |
|
| 415 | - { |
|
| 416 | - if (isset($this->plugins[$name])) { |
|
| 417 | - return $this->plugins[$name]; |
|
| 418 | - } |
|
| 419 | - |
|
| 420 | - return null; |
|
| 421 | - } |
|
| 422 | - |
|
| 423 | - /** |
|
| 424 | - * Returns all plugins. |
|
| 425 | - * |
|
| 426 | - * @return array |
|
| 427 | - */ |
|
| 428 | - public function getPlugins() |
|
| 429 | - { |
|
| 430 | - return $this->plugins; |
|
| 431 | - } |
|
| 432 | - |
|
| 433 | - /** |
|
| 434 | - * Returns the PSR-3 logger object. |
|
| 435 | - * |
|
| 436 | - * @return LoggerInterface |
|
| 437 | - */ |
|
| 438 | - public function getLogger() |
|
| 439 | - { |
|
| 440 | - if (!$this->logger) { |
|
| 441 | - $this->logger = new NullLogger(); |
|
| 442 | - } |
|
| 443 | - |
|
| 444 | - return $this->logger; |
|
| 445 | - } |
|
| 446 | - |
|
| 447 | - /** |
|
| 448 | - * Handles a http request, and execute a method based on its name. |
|
| 449 | - * |
|
| 450 | - * @param bool $sendResponse whether to send the HTTP response to the DAV client |
|
| 451 | - */ |
|
| 452 | - public function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true) |
|
| 453 | - { |
|
| 454 | - $method = $request->getMethod(); |
|
| 455 | - |
|
| 456 | - if (!$this->emit('beforeMethod:'.$method, [$request, $response])) { |
|
| 457 | - return; |
|
| 458 | - } |
|
| 459 | - |
|
| 460 | - if (self::$exposeVersion) { |
|
| 461 | - $response->setHeader('X-Sabre-Version', Version::VERSION); |
|
| 462 | - } |
|
| 463 | - |
|
| 464 | - $this->transactionType = strtolower($method); |
|
| 465 | - |
|
| 466 | - if (!$this->checkPreconditions($request, $response)) { |
|
| 467 | - $this->sapi->sendResponse($response); |
|
| 468 | - |
|
| 469 | - return; |
|
| 470 | - } |
|
| 471 | - |
|
| 472 | - if ($this->emit('method:'.$method, [$request, $response])) { |
|
| 473 | - $exMessage = 'There was no plugin in the system that was willing to handle this '.$method.' method.'; |
|
| 474 | - if ('GET' === $method) { |
|
| 475 | - $exMessage .= ' Enable the Browser plugin to get a better result here.'; |
|
| 476 | - } |
|
| 477 | - |
|
| 478 | - // Unsupported method |
|
| 479 | - throw new Exception\NotImplemented($exMessage); |
|
| 480 | - } |
|
| 481 | - |
|
| 482 | - if (!$this->emit('afterMethod:'.$method, [$request, $response])) { |
|
| 483 | - return; |
|
| 484 | - } |
|
| 485 | - |
|
| 486 | - if (null === $response->getStatus()) { |
|
| 487 | - throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.'); |
|
| 488 | - } |
|
| 489 | - if ($sendResponse) { |
|
| 490 | - $this->sapi->sendResponse($response); |
|
| 491 | - $this->emit('afterResponse', [$request, $response]); |
|
| 492 | - } |
|
| 493 | - } |
|
| 494 | - |
|
| 495 | - // {{{ HTTP/WebDAV protocol helpers |
|
| 496 | - |
|
| 497 | - /** |
|
| 498 | - * Returns an array with all the supported HTTP methods for a specific uri. |
|
| 499 | - * |
|
| 500 | - * @param string $path |
|
| 501 | - * |
|
| 502 | - * @return array |
|
| 503 | - */ |
|
| 504 | - public function getAllowedMethods($path) |
|
| 505 | - { |
|
| 506 | - $methods = [ |
|
| 507 | - 'OPTIONS', |
|
| 508 | - 'GET', |
|
| 509 | - 'HEAD', |
|
| 510 | - 'DELETE', |
|
| 511 | - 'PROPFIND', |
|
| 512 | - 'PUT', |
|
| 513 | - 'PROPPATCH', |
|
| 514 | - 'COPY', |
|
| 515 | - 'MOVE', |
|
| 516 | - 'REPORT', |
|
| 517 | - ]; |
|
| 518 | - |
|
| 519 | - // The MKCOL is only allowed on an unmapped uri |
|
| 520 | - try { |
|
| 521 | - $this->tree->getNodeForPath($path); |
|
| 522 | - } catch (Exception\NotFound $e) { |
|
| 523 | - $methods[] = 'MKCOL'; |
|
| 524 | - } |
|
| 525 | - |
|
| 526 | - // We're also checking if any of the plugins register any new methods |
|
| 527 | - foreach ($this->plugins as $plugin) { |
|
| 528 | - $methods = array_merge($methods, $plugin->getHTTPMethods($path)); |
|
| 529 | - } |
|
| 530 | - array_unique($methods); |
|
| 531 | - |
|
| 532 | - return $methods; |
|
| 533 | - } |
|
| 534 | - |
|
| 535 | - /** |
|
| 536 | - * Gets the uri for the request, keeping the base uri into consideration. |
|
| 537 | - * |
|
| 538 | - * @return string |
|
| 539 | - */ |
|
| 540 | - public function getRequestUri() |
|
| 541 | - { |
|
| 542 | - return $this->calculateUri($this->httpRequest->getUrl()); |
|
| 543 | - } |
|
| 544 | - |
|
| 545 | - /** |
|
| 546 | - * Turns a URI such as the REQUEST_URI into a local path. |
|
| 547 | - * |
|
| 548 | - * This method: |
|
| 549 | - * * strips off the base path |
|
| 550 | - * * normalizes the path |
|
| 551 | - * * uri-decodes the path |
|
| 552 | - * |
|
| 553 | - * @param string $uri |
|
| 554 | - * |
|
| 555 | - * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri |
|
| 556 | - * |
|
| 557 | - * @return string |
|
| 558 | - */ |
|
| 559 | - public function calculateUri($uri) |
|
| 560 | - { |
|
| 561 | - if ('' != $uri && '/' != $uri[0] && strpos($uri, '://')) { |
|
| 562 | - $uri = parse_url($uri, PHP_URL_PATH); |
|
| 563 | - } |
|
| 564 | - |
|
| 565 | - $uri = Uri\normalize(preg_replace('|/+|', '/', $uri)); |
|
| 566 | - $baseUri = Uri\normalize($this->getBaseUri()); |
|
| 567 | - |
|
| 568 | - if (0 === strpos($uri, $baseUri)) { |
|
| 569 | - return trim(HTTP\decodePath(substr($uri, strlen($baseUri))), '/'); |
|
| 570 | - |
|
| 571 | - // A special case, if the baseUri was accessed without a trailing |
|
| 572 | - // slash, we'll accept it as well. |
|
| 573 | - } elseif ($uri.'/' === $baseUri) { |
|
| 574 | - return ''; |
|
| 575 | - } else { |
|
| 576 | - throw new Exception\Forbidden('Requested uri ('.$uri.') is out of base uri ('.$this->getBaseUri().')'); |
|
| 577 | - } |
|
| 578 | - } |
|
| 579 | - |
|
| 580 | - /** |
|
| 581 | - * Returns the HTTP depth header. |
|
| 582 | - * |
|
| 583 | - * 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 |
|
| 584 | - * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent |
|
| 585 | - * |
|
| 586 | - * @param mixed $default |
|
| 587 | - * |
|
| 588 | - * @return int |
|
| 589 | - */ |
|
| 590 | - public function getHTTPDepth($default = self::DEPTH_INFINITY) |
|
| 591 | - { |
|
| 592 | - // If its not set, we'll grab the default |
|
| 593 | - $depth = $this->httpRequest->getHeader('Depth'); |
|
| 594 | - |
|
| 595 | - if (is_null($depth)) { |
|
| 596 | - return $default; |
|
| 597 | - } |
|
| 598 | - |
|
| 599 | - if ('infinity' == $depth) { |
|
| 600 | - return self::DEPTH_INFINITY; |
|
| 601 | - } |
|
| 602 | - |
|
| 603 | - // If its an unknown value. we'll grab the default |
|
| 604 | - if (!ctype_digit($depth)) { |
|
| 605 | - return $default; |
|
| 606 | - } |
|
| 607 | - |
|
| 608 | - return (int) $depth; |
|
| 609 | - } |
|
| 610 | - |
|
| 611 | - /** |
|
| 612 | - * Returns the HTTP range header. |
|
| 613 | - * |
|
| 614 | - * This method returns null if there is no well-formed HTTP range request |
|
| 615 | - * header or array($start, $end). |
|
| 616 | - * |
|
| 617 | - * The first number is the offset of the first byte in the range. |
|
| 618 | - * The second number is the offset of the last byte in the range. |
|
| 619 | - * |
|
| 620 | - * If the second offset is null, it should be treated as the offset of the last byte of the entity |
|
| 621 | - * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity |
|
| 622 | - * |
|
| 623 | - * @return int[]|null |
|
| 624 | - */ |
|
| 625 | - public function getHTTPRange() |
|
| 626 | - { |
|
| 627 | - $range = $this->httpRequest->getHeader('range'); |
|
| 628 | - if (is_null($range)) { |
|
| 629 | - return null; |
|
| 630 | - } |
|
| 631 | - |
|
| 632 | - // Matching "Range: bytes=1234-5678: both numbers are optional |
|
| 633 | - |
|
| 634 | - if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) { |
|
| 635 | - return null; |
|
| 636 | - } |
|
| 637 | - |
|
| 638 | - if ('' === $matches[1] && '' === $matches[2]) { |
|
| 639 | - return null; |
|
| 640 | - } |
|
| 641 | - |
|
| 642 | - return [ |
|
| 643 | - '' !== $matches[1] ? (int) $matches[1] : null, |
|
| 644 | - '' !== $matches[2] ? (int) $matches[2] : null, |
|
| 645 | - ]; |
|
| 646 | - } |
|
| 647 | - |
|
| 648 | - /** |
|
| 649 | - * Returns the HTTP Prefer header information. |
|
| 650 | - * |
|
| 651 | - * The prefer header is defined in: |
|
| 652 | - * http://tools.ietf.org/html/draft-snell-http-prefer-14 |
|
| 653 | - * |
|
| 654 | - * This method will return an array with options. |
|
| 655 | - * |
|
| 656 | - * Currently, the following options may be returned: |
|
| 657 | - * [ |
|
| 658 | - * 'return-asynch' => true, |
|
| 659 | - * 'return-minimal' => true, |
|
| 660 | - * 'return-representation' => true, |
|
| 661 | - * 'wait' => 30, |
|
| 662 | - * 'strict' => true, |
|
| 663 | - * 'lenient' => true, |
|
| 664 | - * ] |
|
| 665 | - * |
|
| 666 | - * This method also supports the Brief header, and will also return |
|
| 667 | - * 'return-minimal' if the brief header was set to 't'. |
|
| 668 | - * |
|
| 669 | - * For the boolean options, false will be returned if the headers are not |
|
| 670 | - * specified. For the integer options it will be 'null'. |
|
| 671 | - * |
|
| 672 | - * @return array |
|
| 673 | - */ |
|
| 674 | - public function getHTTPPrefer() |
|
| 675 | - { |
|
| 676 | - $result = [ |
|
| 677 | - // can be true or false |
|
| 678 | - 'respond-async' => false, |
|
| 679 | - // Could be set to 'representation' or 'minimal'. |
|
| 680 | - 'return' => null, |
|
| 681 | - // Used as a timeout, is usually a number. |
|
| 682 | - 'wait' => null, |
|
| 683 | - // can be 'strict' or 'lenient'. |
|
| 684 | - 'handling' => false, |
|
| 685 | - ]; |
|
| 686 | - |
|
| 687 | - if ($prefer = $this->httpRequest->getHeader('Prefer')) { |
|
| 688 | - $result = array_merge( |
|
| 689 | - $result, |
|
| 690 | - HTTP\parsePrefer($prefer) |
|
| 691 | - ); |
|
| 692 | - } elseif ('t' == $this->httpRequest->getHeader('Brief')) { |
|
| 693 | - $result['return'] = 'minimal'; |
|
| 694 | - } |
|
| 695 | - |
|
| 696 | - return $result; |
|
| 697 | - } |
|
| 698 | - |
|
| 699 | - /** |
|
| 700 | - * Returns information about Copy and Move requests. |
|
| 701 | - * |
|
| 702 | - * This function is created to help getting information about the source and the destination for the |
|
| 703 | - * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions |
|
| 704 | - * |
|
| 705 | - * The returned value is an array with the following keys: |
|
| 706 | - * * destination - Destination path |
|
| 707 | - * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten) |
|
| 708 | - * |
|
| 709 | - * @throws Exception\BadRequest upon missing or broken request headers |
|
| 710 | - * @throws Exception\UnsupportedMediaType when trying to copy into a |
|
| 711 | - * non-collection |
|
| 712 | - * @throws Exception\PreconditionFailed if overwrite is set to false, but |
|
| 713 | - * the destination exists |
|
| 714 | - * @throws Exception\Forbidden when source and destination paths are |
|
| 715 | - * identical |
|
| 716 | - * @throws Exception\Conflict when trying to copy a node into its own |
|
| 717 | - * subtree |
|
| 718 | - * |
|
| 719 | - * @return array |
|
| 720 | - */ |
|
| 721 | - public function getCopyAndMoveInfo(RequestInterface $request) |
|
| 722 | - { |
|
| 723 | - // Collecting the relevant HTTP headers |
|
| 724 | - if (!$request->getHeader('Destination')) { |
|
| 725 | - throw new Exception\BadRequest('The destination header was not supplied'); |
|
| 726 | - } |
|
| 727 | - $destination = $this->calculateUri($request->getHeader('Destination')); |
|
| 728 | - $overwrite = $request->getHeader('Overwrite'); |
|
| 729 | - if (!$overwrite) { |
|
| 730 | - $overwrite = 'T'; |
|
| 731 | - } |
|
| 732 | - if ('T' == strtoupper($overwrite)) { |
|
| 733 | - $overwrite = true; |
|
| 734 | - } elseif ('F' == strtoupper($overwrite)) { |
|
| 735 | - $overwrite = false; |
|
| 736 | - } |
|
| 737 | - // We need to throw a bad request exception, if the header was invalid |
|
| 738 | - else { |
|
| 739 | - throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F'); |
|
| 740 | - } |
|
| 741 | - list($destinationDir) = Uri\split($destination); |
|
| 742 | - |
|
| 743 | - try { |
|
| 744 | - $destinationParent = $this->tree->getNodeForPath($destinationDir); |
|
| 745 | - if (!($destinationParent instanceof ICollection)) { |
|
| 746 | - throw new Exception\UnsupportedMediaType('The destination node is not a collection'); |
|
| 747 | - } |
|
| 748 | - } catch (Exception\NotFound $e) { |
|
| 749 | - // If the destination parent node is not found, we throw a 409 |
|
| 750 | - throw new Exception\Conflict('The destination node is not found'); |
|
| 751 | - } |
|
| 752 | - |
|
| 753 | - try { |
|
| 754 | - $destinationNode = $this->tree->getNodeForPath($destination); |
|
| 755 | - |
|
| 756 | - // If this succeeded, it means the destination already exists |
|
| 757 | - // we'll need to throw precondition failed in case overwrite is false |
|
| 758 | - if (!$overwrite) { |
|
| 759 | - throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite'); |
|
| 760 | - } |
|
| 761 | - } catch (Exception\NotFound $e) { |
|
| 762 | - // Destination didn't exist, we're all good |
|
| 763 | - $destinationNode = false; |
|
| 764 | - } |
|
| 765 | - |
|
| 766 | - $requestPath = $request->getPath(); |
|
| 767 | - if ($destination === $requestPath) { |
|
| 768 | - throw new Exception\Forbidden('Source and destination uri are identical.'); |
|
| 769 | - } |
|
| 770 | - if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath.'/') { |
|
| 771 | - throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.'); |
|
| 772 | - } |
|
| 773 | - |
|
| 774 | - // These are the three relevant properties we need to return |
|
| 775 | - return [ |
|
| 776 | - 'destination' => $destination, |
|
| 777 | - 'destinationExists' => (bool) $destinationNode, |
|
| 778 | - 'destinationNode' => $destinationNode, |
|
| 779 | - ]; |
|
| 780 | - } |
|
| 781 | - |
|
| 782 | - /** |
|
| 783 | - * Returns a list of properties for a path. |
|
| 784 | - * |
|
| 785 | - * This is a simplified version getPropertiesForPath. If you aren't |
|
| 786 | - * interested in status codes, but you just want to have a flat list of |
|
| 787 | - * properties, use this method. |
|
| 788 | - * |
|
| 789 | - * Please note though that any problems related to retrieving properties, |
|
| 790 | - * such as permission issues will just result in an empty array being |
|
| 791 | - * returned. |
|
| 792 | - * |
|
| 793 | - * @param string $path |
|
| 794 | - * @param array $propertyNames |
|
| 795 | - * |
|
| 796 | - * @return array |
|
| 797 | - */ |
|
| 798 | - public function getProperties($path, $propertyNames) |
|
| 799 | - { |
|
| 800 | - $result = $this->getPropertiesForPath($path, $propertyNames, 0); |
|
| 801 | - if (isset($result[0][200])) { |
|
| 802 | - return $result[0][200]; |
|
| 803 | - } else { |
|
| 804 | - return []; |
|
| 805 | - } |
|
| 806 | - } |
|
| 807 | - |
|
| 808 | - /** |
|
| 809 | - * A kid-friendly way to fetch properties for a node's children. |
|
| 810 | - * |
|
| 811 | - * The returned array will be indexed by the path of the of child node. |
|
| 812 | - * Only properties that are actually found will be returned. |
|
| 813 | - * |
|
| 814 | - * The parent node will not be returned. |
|
| 815 | - * |
|
| 816 | - * @param string $path |
|
| 817 | - * @param array $propertyNames |
|
| 818 | - * |
|
| 819 | - * @return array |
|
| 820 | - */ |
|
| 821 | - public function getPropertiesForChildren($path, $propertyNames) |
|
| 822 | - { |
|
| 823 | - $result = []; |
|
| 824 | - foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) { |
|
| 825 | - // Skipping the parent path |
|
| 826 | - if (0 === $k) { |
|
| 827 | - continue; |
|
| 828 | - } |
|
| 829 | - |
|
| 830 | - $result[$row['href']] = $row[200]; |
|
| 831 | - } |
|
| 832 | - |
|
| 833 | - return $result; |
|
| 834 | - } |
|
| 835 | - |
|
| 836 | - /** |
|
| 837 | - * Returns a list of HTTP headers for a particular resource. |
|
| 838 | - * |
|
| 839 | - * The generated http headers are based on properties provided by the |
|
| 840 | - * resource. The method basically provides a simple mapping between |
|
| 841 | - * DAV property and HTTP header. |
|
| 842 | - * |
|
| 843 | - * The headers are intended to be used for HEAD and GET requests. |
|
| 844 | - * |
|
| 845 | - * @param string $path |
|
| 846 | - * |
|
| 847 | - * @return array |
|
| 848 | - */ |
|
| 849 | - public function getHTTPHeaders($path) |
|
| 850 | - { |
|
| 851 | - $propertyMap = [ |
|
| 852 | - '{DAV:}getcontenttype' => 'Content-Type', |
|
| 853 | - '{DAV:}getcontentlength' => 'Content-Length', |
|
| 854 | - '{DAV:}getlastmodified' => 'Last-Modified', |
|
| 855 | - '{DAV:}getetag' => 'ETag', |
|
| 856 | - ]; |
|
| 857 | - |
|
| 858 | - $properties = $this->getProperties($path, array_keys($propertyMap)); |
|
| 859 | - |
|
| 860 | - $headers = []; |
|
| 861 | - foreach ($propertyMap as $property => $header) { |
|
| 862 | - if (!isset($properties[$property])) { |
|
| 863 | - continue; |
|
| 864 | - } |
|
| 865 | - |
|
| 866 | - if (is_scalar($properties[$property])) { |
|
| 867 | - $headers[$header] = $properties[$property]; |
|
| 868 | - |
|
| 869 | - // GetLastModified gets special cased |
|
| 870 | - } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) { |
|
| 871 | - $headers[$header] = HTTP\toDate($properties[$property]->getTime()); |
|
| 872 | - } |
|
| 873 | - } |
|
| 874 | - |
|
| 875 | - return $headers; |
|
| 876 | - } |
|
| 877 | - |
|
| 878 | - /** |
|
| 879 | - * Small helper to support PROPFIND with DEPTH_INFINITY. |
|
| 880 | - * |
|
| 881 | - * @param array $yieldFirst |
|
| 882 | - * |
|
| 883 | - * @return \Traversable |
|
| 884 | - */ |
|
| 885 | - private function generatePathNodes(PropFind $propFind, array $yieldFirst = null) |
|
| 886 | - { |
|
| 887 | - if (null !== $yieldFirst) { |
|
| 888 | - yield $yieldFirst; |
|
| 889 | - } |
|
| 890 | - $newDepth = $propFind->getDepth(); |
|
| 891 | - $path = $propFind->getPath(); |
|
| 892 | - |
|
| 893 | - if (self::DEPTH_INFINITY !== $newDepth) { |
|
| 894 | - --$newDepth; |
|
| 895 | - } |
|
| 896 | - |
|
| 897 | - $propertyNames = $propFind->getRequestedProperties(); |
|
| 898 | - $propFindType = !$propFind->isAllProps() ? PropFind::NORMAL : PropFind::ALLPROPS; |
|
| 899 | - |
|
| 900 | - foreach ($this->tree->getChildren($path) as $childNode) { |
|
| 901 | - if ('' !== $path) { |
|
| 902 | - $subPath = $path.'/'.$childNode->getName(); |
|
| 903 | - } else { |
|
| 904 | - $subPath = $childNode->getName(); |
|
| 905 | - } |
|
| 906 | - $subPropFind = new PropFind($subPath, $propertyNames, $newDepth, $propFindType); |
|
| 907 | - |
|
| 908 | - yield [ |
|
| 909 | - $subPropFind, |
|
| 910 | - $childNode, |
|
| 911 | - ]; |
|
| 912 | - |
|
| 913 | - if ((self::DEPTH_INFINITY === $newDepth || $newDepth >= 1) && $childNode instanceof ICollection) { |
|
| 914 | - foreach ($this->generatePathNodes($subPropFind) as $subItem) { |
|
| 915 | - yield $subItem; |
|
| 916 | - } |
|
| 917 | - } |
|
| 918 | - } |
|
| 919 | - } |
|
| 920 | - |
|
| 921 | - /** |
|
| 922 | - * Returns a list of properties for a given path. |
|
| 923 | - * |
|
| 924 | - * The path that should be supplied should have the baseUrl stripped out |
|
| 925 | - * The list of properties should be supplied in Clark notation. If the list is empty |
|
| 926 | - * 'allprops' is assumed. |
|
| 927 | - * |
|
| 928 | - * If a depth of 1 is requested child elements will also be returned. |
|
| 929 | - * |
|
| 930 | - * @param string $path |
|
| 931 | - * @param array $propertyNames |
|
| 932 | - * @param int $depth |
|
| 933 | - * |
|
| 934 | - * @return array |
|
| 935 | - * |
|
| 936 | - * @deprecated Use getPropertiesIteratorForPath() instead (as it's more memory efficient) |
|
| 937 | - * @see getPropertiesIteratorForPath() |
|
| 938 | - */ |
|
| 939 | - public function getPropertiesForPath($path, $propertyNames = [], $depth = 0) |
|
| 940 | - { |
|
| 941 | - return iterator_to_array($this->getPropertiesIteratorForPath($path, $propertyNames, $depth)); |
|
| 942 | - } |
|
| 943 | - |
|
| 944 | - /** |
|
| 945 | - * Returns a list of properties for a given path. |
|
| 946 | - * |
|
| 947 | - * The path that should be supplied should have the baseUrl stripped out |
|
| 948 | - * The list of properties should be supplied in Clark notation. If the list is empty |
|
| 949 | - * 'allprops' is assumed. |
|
| 950 | - * |
|
| 951 | - * If a depth of 1 is requested child elements will also be returned. |
|
| 952 | - * |
|
| 953 | - * @param string $path |
|
| 954 | - * @param array $propertyNames |
|
| 955 | - * @param int $depth |
|
| 956 | - * |
|
| 957 | - * @return \Iterator |
|
| 958 | - */ |
|
| 959 | - public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0) |
|
| 960 | - { |
|
| 961 | - // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled |
|
| 962 | - if (!$this->enablePropfindDepthInfinity && 0 != $depth) { |
|
| 963 | - $depth = 1; |
|
| 964 | - } |
|
| 965 | - |
|
| 966 | - $path = trim($path, '/'); |
|
| 967 | - |
|
| 968 | - $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; |
|
| 969 | - $propFind = new PropFind($path, (array) $propertyNames, $depth, $propFindType); |
|
| 970 | - |
|
| 971 | - $parentNode = $this->tree->getNodeForPath($path); |
|
| 972 | - |
|
| 973 | - $propFindRequests = [[ |
|
| 974 | - $propFind, |
|
| 975 | - $parentNode, |
|
| 976 | - ]]; |
|
| 977 | - |
|
| 978 | - if (($depth > 0 || self::DEPTH_INFINITY === $depth) && $parentNode instanceof ICollection) { |
|
| 979 | - $propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests)); |
|
| 980 | - } |
|
| 981 | - |
|
| 982 | - foreach ($propFindRequests as $propFindRequest) { |
|
| 983 | - list($propFind, $node) = $propFindRequest; |
|
| 984 | - $r = $this->getPropertiesByNode($propFind, $node); |
|
| 985 | - if ($r) { |
|
| 986 | - $result = $propFind->getResultForMultiStatus(); |
|
| 987 | - $result['href'] = $propFind->getPath(); |
|
| 988 | - |
|
| 989 | - // WebDAV recommends adding a slash to the path, if the path is |
|
| 990 | - // a collection. |
|
| 991 | - // Furthermore, iCal also demands this to be the case for |
|
| 992 | - // principals. This is non-standard, but we support it. |
|
| 993 | - $resourceType = $this->getResourceTypeForNode($node); |
|
| 994 | - if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { |
|
| 995 | - $result['href'] .= '/'; |
|
| 996 | - } |
|
| 997 | - yield $result; |
|
| 998 | - } |
|
| 999 | - } |
|
| 1000 | - } |
|
| 1001 | - |
|
| 1002 | - /** |
|
| 1003 | - * Returns a list of properties for a list of paths. |
|
| 1004 | - * |
|
| 1005 | - * The path that should be supplied should have the baseUrl stripped out |
|
| 1006 | - * The list of properties should be supplied in Clark notation. If the list is empty |
|
| 1007 | - * 'allprops' is assumed. |
|
| 1008 | - * |
|
| 1009 | - * The result is returned as an array, with paths for it's keys. |
|
| 1010 | - * The result may be returned out of order. |
|
| 1011 | - * |
|
| 1012 | - * @return array |
|
| 1013 | - */ |
|
| 1014 | - public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) |
|
| 1015 | - { |
|
| 1016 | - $result = [ |
|
| 1017 | - ]; |
|
| 1018 | - |
|
| 1019 | - $nodes = $this->tree->getMultipleNodes($paths); |
|
| 1020 | - |
|
| 1021 | - foreach ($nodes as $path => $node) { |
|
| 1022 | - $propFind = new PropFind($path, $propertyNames); |
|
| 1023 | - $r = $this->getPropertiesByNode($propFind, $node); |
|
| 1024 | - if ($r) { |
|
| 1025 | - $result[$path] = $propFind->getResultForMultiStatus(); |
|
| 1026 | - $result[$path]['href'] = $path; |
|
| 1027 | - |
|
| 1028 | - $resourceType = $this->getResourceTypeForNode($node); |
|
| 1029 | - if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { |
|
| 1030 | - $result[$path]['href'] .= '/'; |
|
| 1031 | - } |
|
| 1032 | - } |
|
| 1033 | - } |
|
| 1034 | - |
|
| 1035 | - return $result; |
|
| 1036 | - } |
|
| 1037 | - |
|
| 1038 | - /** |
|
| 1039 | - * Determines all properties for a node. |
|
| 1040 | - * |
|
| 1041 | - * This method tries to grab all properties for a node. This method is used |
|
| 1042 | - * internally getPropertiesForPath and a few others. |
|
| 1043 | - * |
|
| 1044 | - * It could be useful to call this, if you already have an instance of your |
|
| 1045 | - * target node and simply want to run through the system to get a correct |
|
| 1046 | - * list of properties. |
|
| 1047 | - * |
|
| 1048 | - * @return bool |
|
| 1049 | - */ |
|
| 1050 | - public function getPropertiesByNode(PropFind $propFind, INode $node) |
|
| 1051 | - { |
|
| 1052 | - return $this->emit('propFind', [$propFind, $node]); |
|
| 1053 | - } |
|
| 1054 | - |
|
| 1055 | - /** |
|
| 1056 | - * This method is invoked by sub-systems creating a new file. |
|
| 1057 | - * |
|
| 1058 | - * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). |
|
| 1059 | - * It was important to get this done through a centralized function, |
|
| 1060 | - * allowing plugins to intercept this using the beforeCreateFile event. |
|
| 1061 | - * |
|
| 1062 | - * This method will return true if the file was actually created |
|
| 1063 | - * |
|
| 1064 | - * @param string $uri |
|
| 1065 | - * @param resource $data |
|
| 1066 | - * @param string $etag |
|
| 1067 | - * |
|
| 1068 | - * @return bool |
|
| 1069 | - */ |
|
| 1070 | - public function createFile($uri, $data, &$etag = null) |
|
| 1071 | - { |
|
| 1072 | - list($dir, $name) = Uri\split($uri); |
|
| 1073 | - |
|
| 1074 | - if (!$this->emit('beforeBind', [$uri])) { |
|
| 1075 | - return false; |
|
| 1076 | - } |
|
| 1077 | - |
|
| 1078 | - try { |
|
| 1079 | - $parent = $this->tree->getNodeForPath($dir); |
|
| 1080 | - } catch (Exception\NotFound $e) { |
|
| 1081 | - throw new Exception\Conflict('Files cannot be created in non-existent collections'); |
|
| 1082 | - } |
|
| 1083 | - |
|
| 1084 | - if (!$parent instanceof ICollection) { |
|
| 1085 | - throw new Exception\Conflict('Files can only be created as children of collections'); |
|
| 1086 | - } |
|
| 1087 | - |
|
| 1088 | - // It is possible for an event handler to modify the content of the |
|
| 1089 | - // body, before it gets written. If this is the case, $modified |
|
| 1090 | - // should be set to true. |
|
| 1091 | - // |
|
| 1092 | - // If $modified is true, we must not send back an ETag. |
|
| 1093 | - $modified = false; |
|
| 1094 | - if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) { |
|
| 1095 | - return false; |
|
| 1096 | - } |
|
| 1097 | - |
|
| 1098 | - $etag = $parent->createFile($name, $data); |
|
| 1099 | - |
|
| 1100 | - if ($modified) { |
|
| 1101 | - $etag = null; |
|
| 1102 | - } |
|
| 1103 | - |
|
| 1104 | - $this->tree->markDirty($dir.'/'.$name); |
|
| 1105 | - |
|
| 1106 | - $this->emit('afterBind', [$uri]); |
|
| 1107 | - $this->emit('afterCreateFile', [$uri, $parent]); |
|
| 1108 | - |
|
| 1109 | - return true; |
|
| 1110 | - } |
|
| 1111 | - |
|
| 1112 | - /** |
|
| 1113 | - * This method is invoked by sub-systems updating a file. |
|
| 1114 | - * |
|
| 1115 | - * This method will return true if the file was actually updated |
|
| 1116 | - * |
|
| 1117 | - * @param string $uri |
|
| 1118 | - * @param resource $data |
|
| 1119 | - * @param string $etag |
|
| 1120 | - * |
|
| 1121 | - * @return bool |
|
| 1122 | - */ |
|
| 1123 | - public function updateFile($uri, $data, &$etag = null) |
|
| 1124 | - { |
|
| 1125 | - $node = $this->tree->getNodeForPath($uri); |
|
| 1126 | - |
|
| 1127 | - // It is possible for an event handler to modify the content of the |
|
| 1128 | - // body, before it gets written. If this is the case, $modified |
|
| 1129 | - // should be set to true. |
|
| 1130 | - // |
|
| 1131 | - // If $modified is true, we must not send back an ETag. |
|
| 1132 | - $modified = false; |
|
| 1133 | - if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) { |
|
| 1134 | - return false; |
|
| 1135 | - } |
|
| 1136 | - |
|
| 1137 | - $etag = $node->put($data); |
|
| 1138 | - if ($modified) { |
|
| 1139 | - $etag = null; |
|
| 1140 | - } |
|
| 1141 | - $this->emit('afterWriteContent', [$uri, $node]); |
|
| 1142 | - |
|
| 1143 | - return true; |
|
| 1144 | - } |
|
| 1145 | - |
|
| 1146 | - /** |
|
| 1147 | - * This method is invoked by sub-systems creating a new directory. |
|
| 1148 | - * |
|
| 1149 | - * @param string $uri |
|
| 1150 | - */ |
|
| 1151 | - public function createDirectory($uri) |
|
| 1152 | - { |
|
| 1153 | - $this->createCollection($uri, new MkCol(['{DAV:}collection'], [])); |
|
| 1154 | - } |
|
| 1155 | - |
|
| 1156 | - /** |
|
| 1157 | - * Use this method to create a new collection. |
|
| 1158 | - * |
|
| 1159 | - * @param string $uri The new uri |
|
| 1160 | - * |
|
| 1161 | - * @return array|null |
|
| 1162 | - */ |
|
| 1163 | - public function createCollection($uri, MkCol $mkCol) |
|
| 1164 | - { |
|
| 1165 | - list($parentUri, $newName) = Uri\split($uri); |
|
| 1166 | - |
|
| 1167 | - // Making sure the parent exists |
|
| 1168 | - try { |
|
| 1169 | - $parent = $this->tree->getNodeForPath($parentUri); |
|
| 1170 | - } catch (Exception\NotFound $e) { |
|
| 1171 | - throw new Exception\Conflict('Parent node does not exist'); |
|
| 1172 | - } |
|
| 1173 | - |
|
| 1174 | - // Making sure the parent is a collection |
|
| 1175 | - if (!$parent instanceof ICollection) { |
|
| 1176 | - throw new Exception\Conflict('Parent node is not a collection'); |
|
| 1177 | - } |
|
| 1178 | - |
|
| 1179 | - // Making sure the child does not already exist |
|
| 1180 | - try { |
|
| 1181 | - $parent->getChild($newName); |
|
| 1182 | - |
|
| 1183 | - // If we got here.. it means there's already a node on that url, and we need to throw a 405 |
|
| 1184 | - throw new Exception\MethodNotAllowed('The resource you tried to create already exists'); |
|
| 1185 | - } catch (Exception\NotFound $e) { |
|
| 1186 | - // NotFound is the expected behavior. |
|
| 1187 | - } |
|
| 1188 | - |
|
| 1189 | - if (!$this->emit('beforeBind', [$uri])) { |
|
| 1190 | - return; |
|
| 1191 | - } |
|
| 1192 | - |
|
| 1193 | - if ($parent instanceof IExtendedCollection) { |
|
| 1194 | - /* |
|
| 28 | + use LoggerAwareTrait; |
|
| 29 | + use WildcardEmitterTrait; |
|
| 30 | + |
|
| 31 | + /** |
|
| 32 | + * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree. |
|
| 33 | + */ |
|
| 34 | + const DEPTH_INFINITY = -1; |
|
| 35 | + |
|
| 36 | + /** |
|
| 37 | + * XML namespace for all SabreDAV related elements. |
|
| 38 | + */ |
|
| 39 | + const NS_SABREDAV = 'http://sabredav.org/ns'; |
|
| 40 | + |
|
| 41 | + /** |
|
| 42 | + * The tree object. |
|
| 43 | + * |
|
| 44 | + * @var Tree |
|
| 45 | + */ |
|
| 46 | + public $tree; |
|
| 47 | + |
|
| 48 | + /** |
|
| 49 | + * The base uri. |
|
| 50 | + * |
|
| 51 | + * @var string |
|
| 52 | + */ |
|
| 53 | + protected $baseUri = null; |
|
| 54 | + |
|
| 55 | + /** |
|
| 56 | + * httpResponse. |
|
| 57 | + * |
|
| 58 | + * @var HTTP\Response |
|
| 59 | + */ |
|
| 60 | + public $httpResponse; |
|
| 61 | + |
|
| 62 | + /** |
|
| 63 | + * httpRequest. |
|
| 64 | + * |
|
| 65 | + * @var HTTP\Request |
|
| 66 | + */ |
|
| 67 | + public $httpRequest; |
|
| 68 | + |
|
| 69 | + /** |
|
| 70 | + * PHP HTTP Sapi. |
|
| 71 | + * |
|
| 72 | + * @var HTTP\Sapi |
|
| 73 | + */ |
|
| 74 | + public $sapi; |
|
| 75 | + |
|
| 76 | + /** |
|
| 77 | + * The list of plugins. |
|
| 78 | + * |
|
| 79 | + * @var array |
|
| 80 | + */ |
|
| 81 | + protected $plugins = []; |
|
| 82 | + |
|
| 83 | + /** |
|
| 84 | + * This property will be filled with a unique string that describes the |
|
| 85 | + * transaction. This is useful for performance measuring and logging |
|
| 86 | + * purposes. |
|
| 87 | + * |
|
| 88 | + * By default it will just fill it with a lowercased HTTP method name, but |
|
| 89 | + * plugins override this. For example, the WebDAV-Sync sync-collection |
|
| 90 | + * report will set this to 'report-sync-collection'. |
|
| 91 | + * |
|
| 92 | + * @var string |
|
| 93 | + */ |
|
| 94 | + public $transactionType; |
|
| 95 | + |
|
| 96 | + /** |
|
| 97 | + * This is a list of properties that are always server-controlled, and |
|
| 98 | + * must not get modified with PROPPATCH. |
|
| 99 | + * |
|
| 100 | + * Plugins may add to this list. |
|
| 101 | + * |
|
| 102 | + * @var string[] |
|
| 103 | + */ |
|
| 104 | + public $protectedProperties = [ |
|
| 105 | + // RFC4918 |
|
| 106 | + '{DAV:}getcontentlength', |
|
| 107 | + '{DAV:}getetag', |
|
| 108 | + '{DAV:}getlastmodified', |
|
| 109 | + '{DAV:}lockdiscovery', |
|
| 110 | + '{DAV:}supportedlock', |
|
| 111 | + |
|
| 112 | + // RFC4331 |
|
| 113 | + '{DAV:}quota-available-bytes', |
|
| 114 | + '{DAV:}quota-used-bytes', |
|
| 115 | + |
|
| 116 | + // RFC3744 |
|
| 117 | + '{DAV:}supported-privilege-set', |
|
| 118 | + '{DAV:}current-user-privilege-set', |
|
| 119 | + '{DAV:}acl', |
|
| 120 | + '{DAV:}acl-restrictions', |
|
| 121 | + '{DAV:}inherited-acl-set', |
|
| 122 | + |
|
| 123 | + // RFC3253 |
|
| 124 | + '{DAV:}supported-method-set', |
|
| 125 | + '{DAV:}supported-report-set', |
|
| 126 | + |
|
| 127 | + // RFC6578 |
|
| 128 | + '{DAV:}sync-token', |
|
| 129 | + |
|
| 130 | + // calendarserver.org extensions |
|
| 131 | + '{http://calendarserver.org/ns/}ctag', |
|
| 132 | + |
|
| 133 | + // sabredav extensions |
|
| 134 | + '{http://sabredav.org/ns}sync-token', |
|
| 135 | + ]; |
|
| 136 | + |
|
| 137 | + /** |
|
| 138 | + * This is a flag that allow or not showing file, line and code |
|
| 139 | + * of the exception in the returned XML. |
|
| 140 | + * |
|
| 141 | + * @var bool |
|
| 142 | + */ |
|
| 143 | + public $debugExceptions = false; |
|
| 144 | + |
|
| 145 | + /** |
|
| 146 | + * This property allows you to automatically add the 'resourcetype' value |
|
| 147 | + * based on a node's classname or interface. |
|
| 148 | + * |
|
| 149 | + * The preset ensures that {DAV:}collection is automatically added for nodes |
|
| 150 | + * implementing Sabre\DAV\ICollection. |
|
| 151 | + * |
|
| 152 | + * @var array |
|
| 153 | + */ |
|
| 154 | + public $resourceTypeMapping = [ |
|
| 155 | + 'Sabre\\DAV\\ICollection' => '{DAV:}collection', |
|
| 156 | + ]; |
|
| 157 | + |
|
| 158 | + /** |
|
| 159 | + * This property allows the usage of Depth: infinity on PROPFIND requests. |
|
| 160 | + * |
|
| 161 | + * By default Depth: infinity is treated as Depth: 1. Allowing Depth: |
|
| 162 | + * infinity is potentially risky, as it allows a single client to do a full |
|
| 163 | + * index of the webdav server, which is an easy DoS attack vector. |
|
| 164 | + * |
|
| 165 | + * Only turn this on if you know what you're doing. |
|
| 166 | + * |
|
| 167 | + * @var bool |
|
| 168 | + */ |
|
| 169 | + public $enablePropfindDepthInfinity = false; |
|
| 170 | + |
|
| 171 | + /** |
|
| 172 | + * Reference to the XML utility object. |
|
| 173 | + * |
|
| 174 | + * @var Xml\Service |
|
| 175 | + */ |
|
| 176 | + public $xml; |
|
| 177 | + |
|
| 178 | + /** |
|
| 179 | + * If this setting is turned off, SabreDAV's version number will be hidden |
|
| 180 | + * from various places. |
|
| 181 | + * |
|
| 182 | + * Some people feel this is a good security measure. |
|
| 183 | + * |
|
| 184 | + * @var bool |
|
| 185 | + */ |
|
| 186 | + public static $exposeVersion = true; |
|
| 187 | + |
|
| 188 | + /** |
|
| 189 | + * If this setting is turned on, any multi status response on any PROPFIND will be streamed to the output buffer. |
|
| 190 | + * This will be beneficial for large result sets which will no longer consume a large amount of memory as well as |
|
| 191 | + * send back data to the client earlier. |
|
| 192 | + * |
|
| 193 | + * @var bool |
|
| 194 | + */ |
|
| 195 | + public static $streamMultiStatus = false; |
|
| 196 | + |
|
| 197 | + /** |
|
| 198 | + * Sets up the server. |
|
| 199 | + * |
|
| 200 | + * If a Sabre\DAV\Tree object is passed as an argument, it will |
|
| 201 | + * use it as the directory tree. If a Sabre\DAV\INode is passed, it |
|
| 202 | + * will create a Sabre\DAV\Tree and use the node as the root. |
|
| 203 | + * |
|
| 204 | + * If nothing is passed, a Sabre\DAV\SimpleCollection is created in |
|
| 205 | + * a Sabre\DAV\Tree. |
|
| 206 | + * |
|
| 207 | + * If an array is passed, we automatically create a root node, and use |
|
| 208 | + * the nodes in the array as top-level children. |
|
| 209 | + * |
|
| 210 | + * @param Tree|INode|array|null $treeOrNode The tree object |
|
| 211 | + * |
|
| 212 | + * @throws Exception |
|
| 213 | + */ |
|
| 214 | + public function __construct($treeOrNode = null, HTTP\Sapi $sapi = null) |
|
| 215 | + { |
|
| 216 | + if ($treeOrNode instanceof Tree) { |
|
| 217 | + $this->tree = $treeOrNode; |
|
| 218 | + } elseif ($treeOrNode instanceof INode) { |
|
| 219 | + $this->tree = new Tree($treeOrNode); |
|
| 220 | + } elseif (is_array($treeOrNode)) { |
|
| 221 | + $root = new SimpleCollection('root', $treeOrNode); |
|
| 222 | + $this->tree = new Tree($root); |
|
| 223 | + } elseif (is_null($treeOrNode)) { |
|
| 224 | + $root = new SimpleCollection('root'); |
|
| 225 | + $this->tree = new Tree($root); |
|
| 226 | + } else { |
|
| 227 | + 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'); |
|
| 228 | + } |
|
| 229 | + |
|
| 230 | + $this->xml = new Xml\Service(); |
|
| 231 | + $this->sapi = $sapi ?? new HTTP\Sapi(); |
|
| 232 | + $this->httpResponse = new HTTP\Response(); |
|
| 233 | + $this->httpRequest = $this->sapi->getRequest(); |
|
| 234 | + $this->addPlugin(new CorePlugin()); |
|
| 235 | + } |
|
| 236 | + |
|
| 237 | + /** |
|
| 238 | + * Starts the DAV Server. |
|
| 239 | + */ |
|
| 240 | + public function start() |
|
| 241 | + { |
|
| 242 | + try { |
|
| 243 | + // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an |
|
| 244 | + // origin, we must make sure we send back HTTP/1.0 if this was |
|
| 245 | + // requested. |
|
| 246 | + // This is mainly because nginx doesn't support Chunked Transfer |
|
| 247 | + // Encoding, and this forces the webserver SabreDAV is running on, |
|
| 248 | + // to buffer entire responses to calculate Content-Length. |
|
| 249 | + $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion()); |
|
| 250 | + |
|
| 251 | + // Setting the base url |
|
| 252 | + $this->httpRequest->setBaseUrl($this->getBaseUri()); |
|
| 253 | + $this->invokeMethod($this->httpRequest, $this->httpResponse); |
|
| 254 | + } catch (\Throwable $e) { |
|
| 255 | + try { |
|
| 256 | + $this->emit('exception', [$e]); |
|
| 257 | + } catch (\Exception $ignore) { |
|
| 258 | + } |
|
| 259 | + $DOM = new \DOMDocument('1.0', 'utf-8'); |
|
| 260 | + $DOM->formatOutput = true; |
|
| 261 | + |
|
| 262 | + $error = $DOM->createElementNS('DAV:', 'd:error'); |
|
| 263 | + $error->setAttribute('xmlns:s', self::NS_SABREDAV); |
|
| 264 | + $DOM->appendChild($error); |
|
| 265 | + |
|
| 266 | + $h = function ($v) { |
|
| 267 | + return htmlspecialchars((string) $v, ENT_NOQUOTES, 'UTF-8'); |
|
| 268 | + }; |
|
| 269 | + |
|
| 270 | + if (self::$exposeVersion) { |
|
| 271 | + $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION))); |
|
| 272 | + } |
|
| 273 | + |
|
| 274 | + $error->appendChild($DOM->createElement('s:exception', $h(get_class($e)))); |
|
| 275 | + $error->appendChild($DOM->createElement('s:message', $h($e->getMessage()))); |
|
| 276 | + if ($this->debugExceptions) { |
|
| 277 | + $error->appendChild($DOM->createElement('s:file', $h($e->getFile()))); |
|
| 278 | + $error->appendChild($DOM->createElement('s:line', $h($e->getLine()))); |
|
| 279 | + $error->appendChild($DOM->createElement('s:code', $h($e->getCode()))); |
|
| 280 | + $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString()))); |
|
| 281 | + } |
|
| 282 | + |
|
| 283 | + if ($this->debugExceptions) { |
|
| 284 | + $previous = $e; |
|
| 285 | + while ($previous = $previous->getPrevious()) { |
|
| 286 | + $xPrevious = $DOM->createElement('s:previous-exception'); |
|
| 287 | + $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous)))); |
|
| 288 | + $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage()))); |
|
| 289 | + $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile()))); |
|
| 290 | + $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine()))); |
|
| 291 | + $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode()))); |
|
| 292 | + $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString()))); |
|
| 293 | + $error->appendChild($xPrevious); |
|
| 294 | + } |
|
| 295 | + } |
|
| 296 | + |
|
| 297 | + if ($e instanceof Exception) { |
|
| 298 | + $httpCode = $e->getHTTPCode(); |
|
| 299 | + $e->serialize($this, $error); |
|
| 300 | + $headers = $e->getHTTPHeaders($this); |
|
| 301 | + } else { |
|
| 302 | + $httpCode = 500; |
|
| 303 | + $headers = []; |
|
| 304 | + } |
|
| 305 | + $headers['Content-Type'] = 'application/xml; charset=utf-8'; |
|
| 306 | + |
|
| 307 | + $this->httpResponse->setStatus($httpCode); |
|
| 308 | + $this->httpResponse->setHeaders($headers); |
|
| 309 | + $this->httpResponse->setBody($DOM->saveXML()); |
|
| 310 | + $this->sapi->sendResponse($this->httpResponse); |
|
| 311 | + } |
|
| 312 | + } |
|
| 313 | + |
|
| 314 | + /** |
|
| 315 | + * Alias of start(). |
|
| 316 | + * |
|
| 317 | + * @deprecated |
|
| 318 | + */ |
|
| 319 | + public function exec() |
|
| 320 | + { |
|
| 321 | + $this->start(); |
|
| 322 | + } |
|
| 323 | + |
|
| 324 | + /** |
|
| 325 | + * Sets the base server uri. |
|
| 326 | + * |
|
| 327 | + * @param string $uri |
|
| 328 | + */ |
|
| 329 | + public function setBaseUri($uri) |
|
| 330 | + { |
|
| 331 | + // If the baseUri does not end with a slash, we must add it |
|
| 332 | + if ('/' !== $uri[strlen($uri) - 1]) { |
|
| 333 | + $uri .= '/'; |
|
| 334 | + } |
|
| 335 | + |
|
| 336 | + $this->baseUri = $uri; |
|
| 337 | + } |
|
| 338 | + |
|
| 339 | + /** |
|
| 340 | + * Returns the base responding uri. |
|
| 341 | + * |
|
| 342 | + * @return string |
|
| 343 | + */ |
|
| 344 | + public function getBaseUri() |
|
| 345 | + { |
|
| 346 | + if (is_null($this->baseUri)) { |
|
| 347 | + $this->baseUri = $this->guessBaseUri(); |
|
| 348 | + } |
|
| 349 | + |
|
| 350 | + return $this->baseUri; |
|
| 351 | + } |
|
| 352 | + |
|
| 353 | + /** |
|
| 354 | + * This method attempts to detect the base uri. |
|
| 355 | + * Only the PATH_INFO variable is considered. |
|
| 356 | + * |
|
| 357 | + * If this variable is not set, the root (/) is assumed. |
|
| 358 | + * |
|
| 359 | + * @return string |
|
| 360 | + */ |
|
| 361 | + public function guessBaseUri() |
|
| 362 | + { |
|
| 363 | + $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO'); |
|
| 364 | + $uri = $this->httpRequest->getRawServerValue('REQUEST_URI'); |
|
| 365 | + |
|
| 366 | + // If PATH_INFO is found, we can assume it's accurate. |
|
| 367 | + if (!empty($pathInfo)) { |
|
| 368 | + // We need to make sure we ignore the QUERY_STRING part |
|
| 369 | + if ($pos = strpos($uri, '?')) { |
|
| 370 | + $uri = substr($uri, 0, $pos); |
|
| 371 | + } |
|
| 372 | + |
|
| 373 | + // PATH_INFO is only set for urls, such as: /example.php/path |
|
| 374 | + // in that case PATH_INFO contains '/path'. |
|
| 375 | + // Note that REQUEST_URI is percent encoded, while PATH_INFO is |
|
| 376 | + // not, Therefore they are only comparable if we first decode |
|
| 377 | + // REQUEST_INFO as well. |
|
| 378 | + $decodedUri = HTTP\decodePath($uri); |
|
| 379 | + |
|
| 380 | + // A simple sanity check: |
|
| 381 | + if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) { |
|
| 382 | + $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo)); |
|
| 383 | + |
|
| 384 | + return rtrim($baseUri, '/').'/'; |
|
| 385 | + } |
|
| 386 | + |
|
| 387 | + throw new Exception('The REQUEST_URI ('.$uri.') did not end with the contents of PATH_INFO ('.$pathInfo.'). This server might be misconfigured.'); |
|
| 388 | + } |
|
| 389 | + |
|
| 390 | + // The last fallback is that we're just going to assume the server root. |
|
| 391 | + return '/'; |
|
| 392 | + } |
|
| 393 | + |
|
| 394 | + /** |
|
| 395 | + * Adds a plugin to the server. |
|
| 396 | + * |
|
| 397 | + * For more information, console the documentation of Sabre\DAV\ServerPlugin |
|
| 398 | + */ |
|
| 399 | + public function addPlugin(ServerPlugin $plugin) |
|
| 400 | + { |
|
| 401 | + $this->plugins[$plugin->getPluginName()] = $plugin; |
|
| 402 | + $plugin->initialize($this); |
|
| 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 | + * |
|
| 412 | + * @return ServerPlugin |
|
| 413 | + */ |
|
| 414 | + public function getPlugin($name) |
|
| 415 | + { |
|
| 416 | + if (isset($this->plugins[$name])) { |
|
| 417 | + return $this->plugins[$name]; |
|
| 418 | + } |
|
| 419 | + |
|
| 420 | + return null; |
|
| 421 | + } |
|
| 422 | + |
|
| 423 | + /** |
|
| 424 | + * Returns all plugins. |
|
| 425 | + * |
|
| 426 | + * @return array |
|
| 427 | + */ |
|
| 428 | + public function getPlugins() |
|
| 429 | + { |
|
| 430 | + return $this->plugins; |
|
| 431 | + } |
|
| 432 | + |
|
| 433 | + /** |
|
| 434 | + * Returns the PSR-3 logger object. |
|
| 435 | + * |
|
| 436 | + * @return LoggerInterface |
|
| 437 | + */ |
|
| 438 | + public function getLogger() |
|
| 439 | + { |
|
| 440 | + if (!$this->logger) { |
|
| 441 | + $this->logger = new NullLogger(); |
|
| 442 | + } |
|
| 443 | + |
|
| 444 | + return $this->logger; |
|
| 445 | + } |
|
| 446 | + |
|
| 447 | + /** |
|
| 448 | + * Handles a http request, and execute a method based on its name. |
|
| 449 | + * |
|
| 450 | + * @param bool $sendResponse whether to send the HTTP response to the DAV client |
|
| 451 | + */ |
|
| 452 | + public function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true) |
|
| 453 | + { |
|
| 454 | + $method = $request->getMethod(); |
|
| 455 | + |
|
| 456 | + if (!$this->emit('beforeMethod:'.$method, [$request, $response])) { |
|
| 457 | + return; |
|
| 458 | + } |
|
| 459 | + |
|
| 460 | + if (self::$exposeVersion) { |
|
| 461 | + $response->setHeader('X-Sabre-Version', Version::VERSION); |
|
| 462 | + } |
|
| 463 | + |
|
| 464 | + $this->transactionType = strtolower($method); |
|
| 465 | + |
|
| 466 | + if (!$this->checkPreconditions($request, $response)) { |
|
| 467 | + $this->sapi->sendResponse($response); |
|
| 468 | + |
|
| 469 | + return; |
|
| 470 | + } |
|
| 471 | + |
|
| 472 | + if ($this->emit('method:'.$method, [$request, $response])) { |
|
| 473 | + $exMessage = 'There was no plugin in the system that was willing to handle this '.$method.' method.'; |
|
| 474 | + if ('GET' === $method) { |
|
| 475 | + $exMessage .= ' Enable the Browser plugin to get a better result here.'; |
|
| 476 | + } |
|
| 477 | + |
|
| 478 | + // Unsupported method |
|
| 479 | + throw new Exception\NotImplemented($exMessage); |
|
| 480 | + } |
|
| 481 | + |
|
| 482 | + if (!$this->emit('afterMethod:'.$method, [$request, $response])) { |
|
| 483 | + return; |
|
| 484 | + } |
|
| 485 | + |
|
| 486 | + if (null === $response->getStatus()) { |
|
| 487 | + throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.'); |
|
| 488 | + } |
|
| 489 | + if ($sendResponse) { |
|
| 490 | + $this->sapi->sendResponse($response); |
|
| 491 | + $this->emit('afterResponse', [$request, $response]); |
|
| 492 | + } |
|
| 493 | + } |
|
| 494 | + |
|
| 495 | + // {{{ HTTP/WebDAV protocol helpers |
|
| 496 | + |
|
| 497 | + /** |
|
| 498 | + * Returns an array with all the supported HTTP methods for a specific uri. |
|
| 499 | + * |
|
| 500 | + * @param string $path |
|
| 501 | + * |
|
| 502 | + * @return array |
|
| 503 | + */ |
|
| 504 | + public function getAllowedMethods($path) |
|
| 505 | + { |
|
| 506 | + $methods = [ |
|
| 507 | + 'OPTIONS', |
|
| 508 | + 'GET', |
|
| 509 | + 'HEAD', |
|
| 510 | + 'DELETE', |
|
| 511 | + 'PROPFIND', |
|
| 512 | + 'PUT', |
|
| 513 | + 'PROPPATCH', |
|
| 514 | + 'COPY', |
|
| 515 | + 'MOVE', |
|
| 516 | + 'REPORT', |
|
| 517 | + ]; |
|
| 518 | + |
|
| 519 | + // The MKCOL is only allowed on an unmapped uri |
|
| 520 | + try { |
|
| 521 | + $this->tree->getNodeForPath($path); |
|
| 522 | + } catch (Exception\NotFound $e) { |
|
| 523 | + $methods[] = 'MKCOL'; |
|
| 524 | + } |
|
| 525 | + |
|
| 526 | + // We're also checking if any of the plugins register any new methods |
|
| 527 | + foreach ($this->plugins as $plugin) { |
|
| 528 | + $methods = array_merge($methods, $plugin->getHTTPMethods($path)); |
|
| 529 | + } |
|
| 530 | + array_unique($methods); |
|
| 531 | + |
|
| 532 | + return $methods; |
|
| 533 | + } |
|
| 534 | + |
|
| 535 | + /** |
|
| 536 | + * Gets the uri for the request, keeping the base uri into consideration. |
|
| 537 | + * |
|
| 538 | + * @return string |
|
| 539 | + */ |
|
| 540 | + public function getRequestUri() |
|
| 541 | + { |
|
| 542 | + return $this->calculateUri($this->httpRequest->getUrl()); |
|
| 543 | + } |
|
| 544 | + |
|
| 545 | + /** |
|
| 546 | + * Turns a URI such as the REQUEST_URI into a local path. |
|
| 547 | + * |
|
| 548 | + * This method: |
|
| 549 | + * * strips off the base path |
|
| 550 | + * * normalizes the path |
|
| 551 | + * * uri-decodes the path |
|
| 552 | + * |
|
| 553 | + * @param string $uri |
|
| 554 | + * |
|
| 555 | + * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri |
|
| 556 | + * |
|
| 557 | + * @return string |
|
| 558 | + */ |
|
| 559 | + public function calculateUri($uri) |
|
| 560 | + { |
|
| 561 | + if ('' != $uri && '/' != $uri[0] && strpos($uri, '://')) { |
|
| 562 | + $uri = parse_url($uri, PHP_URL_PATH); |
|
| 563 | + } |
|
| 564 | + |
|
| 565 | + $uri = Uri\normalize(preg_replace('|/+|', '/', $uri)); |
|
| 566 | + $baseUri = Uri\normalize($this->getBaseUri()); |
|
| 567 | + |
|
| 568 | + if (0 === strpos($uri, $baseUri)) { |
|
| 569 | + return trim(HTTP\decodePath(substr($uri, strlen($baseUri))), '/'); |
|
| 570 | + |
|
| 571 | + // A special case, if the baseUri was accessed without a trailing |
|
| 572 | + // slash, we'll accept it as well. |
|
| 573 | + } elseif ($uri.'/' === $baseUri) { |
|
| 574 | + return ''; |
|
| 575 | + } else { |
|
| 576 | + throw new Exception\Forbidden('Requested uri ('.$uri.') is out of base uri ('.$this->getBaseUri().')'); |
|
| 577 | + } |
|
| 578 | + } |
|
| 579 | + |
|
| 580 | + /** |
|
| 581 | + * Returns the HTTP depth header. |
|
| 582 | + * |
|
| 583 | + * 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 |
|
| 584 | + * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent |
|
| 585 | + * |
|
| 586 | + * @param mixed $default |
|
| 587 | + * |
|
| 588 | + * @return int |
|
| 589 | + */ |
|
| 590 | + public function getHTTPDepth($default = self::DEPTH_INFINITY) |
|
| 591 | + { |
|
| 592 | + // If its not set, we'll grab the default |
|
| 593 | + $depth = $this->httpRequest->getHeader('Depth'); |
|
| 594 | + |
|
| 595 | + if (is_null($depth)) { |
|
| 596 | + return $default; |
|
| 597 | + } |
|
| 598 | + |
|
| 599 | + if ('infinity' == $depth) { |
|
| 600 | + return self::DEPTH_INFINITY; |
|
| 601 | + } |
|
| 602 | + |
|
| 603 | + // If its an unknown value. we'll grab the default |
|
| 604 | + if (!ctype_digit($depth)) { |
|
| 605 | + return $default; |
|
| 606 | + } |
|
| 607 | + |
|
| 608 | + return (int) $depth; |
|
| 609 | + } |
|
| 610 | + |
|
| 611 | + /** |
|
| 612 | + * Returns the HTTP range header. |
|
| 613 | + * |
|
| 614 | + * This method returns null if there is no well-formed HTTP range request |
|
| 615 | + * header or array($start, $end). |
|
| 616 | + * |
|
| 617 | + * The first number is the offset of the first byte in the range. |
|
| 618 | + * The second number is the offset of the last byte in the range. |
|
| 619 | + * |
|
| 620 | + * If the second offset is null, it should be treated as the offset of the last byte of the entity |
|
| 621 | + * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity |
|
| 622 | + * |
|
| 623 | + * @return int[]|null |
|
| 624 | + */ |
|
| 625 | + public function getHTTPRange() |
|
| 626 | + { |
|
| 627 | + $range = $this->httpRequest->getHeader('range'); |
|
| 628 | + if (is_null($range)) { |
|
| 629 | + return null; |
|
| 630 | + } |
|
| 631 | + |
|
| 632 | + // Matching "Range: bytes=1234-5678: both numbers are optional |
|
| 633 | + |
|
| 634 | + if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) { |
|
| 635 | + return null; |
|
| 636 | + } |
|
| 637 | + |
|
| 638 | + if ('' === $matches[1] && '' === $matches[2]) { |
|
| 639 | + return null; |
|
| 640 | + } |
|
| 641 | + |
|
| 642 | + return [ |
|
| 643 | + '' !== $matches[1] ? (int) $matches[1] : null, |
|
| 644 | + '' !== $matches[2] ? (int) $matches[2] : null, |
|
| 645 | + ]; |
|
| 646 | + } |
|
| 647 | + |
|
| 648 | + /** |
|
| 649 | + * Returns the HTTP Prefer header information. |
|
| 650 | + * |
|
| 651 | + * The prefer header is defined in: |
|
| 652 | + * http://tools.ietf.org/html/draft-snell-http-prefer-14 |
|
| 653 | + * |
|
| 654 | + * This method will return an array with options. |
|
| 655 | + * |
|
| 656 | + * Currently, the following options may be returned: |
|
| 657 | + * [ |
|
| 658 | + * 'return-asynch' => true, |
|
| 659 | + * 'return-minimal' => true, |
|
| 660 | + * 'return-representation' => true, |
|
| 661 | + * 'wait' => 30, |
|
| 662 | + * 'strict' => true, |
|
| 663 | + * 'lenient' => true, |
|
| 664 | + * ] |
|
| 665 | + * |
|
| 666 | + * This method also supports the Brief header, and will also return |
|
| 667 | + * 'return-minimal' if the brief header was set to 't'. |
|
| 668 | + * |
|
| 669 | + * For the boolean options, false will be returned if the headers are not |
|
| 670 | + * specified. For the integer options it will be 'null'. |
|
| 671 | + * |
|
| 672 | + * @return array |
|
| 673 | + */ |
|
| 674 | + public function getHTTPPrefer() |
|
| 675 | + { |
|
| 676 | + $result = [ |
|
| 677 | + // can be true or false |
|
| 678 | + 'respond-async' => false, |
|
| 679 | + // Could be set to 'representation' or 'minimal'. |
|
| 680 | + 'return' => null, |
|
| 681 | + // Used as a timeout, is usually a number. |
|
| 682 | + 'wait' => null, |
|
| 683 | + // can be 'strict' or 'lenient'. |
|
| 684 | + 'handling' => false, |
|
| 685 | + ]; |
|
| 686 | + |
|
| 687 | + if ($prefer = $this->httpRequest->getHeader('Prefer')) { |
|
| 688 | + $result = array_merge( |
|
| 689 | + $result, |
|
| 690 | + HTTP\parsePrefer($prefer) |
|
| 691 | + ); |
|
| 692 | + } elseif ('t' == $this->httpRequest->getHeader('Brief')) { |
|
| 693 | + $result['return'] = 'minimal'; |
|
| 694 | + } |
|
| 695 | + |
|
| 696 | + return $result; |
|
| 697 | + } |
|
| 698 | + |
|
| 699 | + /** |
|
| 700 | + * Returns information about Copy and Move requests. |
|
| 701 | + * |
|
| 702 | + * This function is created to help getting information about the source and the destination for the |
|
| 703 | + * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions |
|
| 704 | + * |
|
| 705 | + * The returned value is an array with the following keys: |
|
| 706 | + * * destination - Destination path |
|
| 707 | + * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten) |
|
| 708 | + * |
|
| 709 | + * @throws Exception\BadRequest upon missing or broken request headers |
|
| 710 | + * @throws Exception\UnsupportedMediaType when trying to copy into a |
|
| 711 | + * non-collection |
|
| 712 | + * @throws Exception\PreconditionFailed if overwrite is set to false, but |
|
| 713 | + * the destination exists |
|
| 714 | + * @throws Exception\Forbidden when source and destination paths are |
|
| 715 | + * identical |
|
| 716 | + * @throws Exception\Conflict when trying to copy a node into its own |
|
| 717 | + * subtree |
|
| 718 | + * |
|
| 719 | + * @return array |
|
| 720 | + */ |
|
| 721 | + public function getCopyAndMoveInfo(RequestInterface $request) |
|
| 722 | + { |
|
| 723 | + // Collecting the relevant HTTP headers |
|
| 724 | + if (!$request->getHeader('Destination')) { |
|
| 725 | + throw new Exception\BadRequest('The destination header was not supplied'); |
|
| 726 | + } |
|
| 727 | + $destination = $this->calculateUri($request->getHeader('Destination')); |
|
| 728 | + $overwrite = $request->getHeader('Overwrite'); |
|
| 729 | + if (!$overwrite) { |
|
| 730 | + $overwrite = 'T'; |
|
| 731 | + } |
|
| 732 | + if ('T' == strtoupper($overwrite)) { |
|
| 733 | + $overwrite = true; |
|
| 734 | + } elseif ('F' == strtoupper($overwrite)) { |
|
| 735 | + $overwrite = false; |
|
| 736 | + } |
|
| 737 | + // We need to throw a bad request exception, if the header was invalid |
|
| 738 | + else { |
|
| 739 | + throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F'); |
|
| 740 | + } |
|
| 741 | + list($destinationDir) = Uri\split($destination); |
|
| 742 | + |
|
| 743 | + try { |
|
| 744 | + $destinationParent = $this->tree->getNodeForPath($destinationDir); |
|
| 745 | + if (!($destinationParent instanceof ICollection)) { |
|
| 746 | + throw new Exception\UnsupportedMediaType('The destination node is not a collection'); |
|
| 747 | + } |
|
| 748 | + } catch (Exception\NotFound $e) { |
|
| 749 | + // If the destination parent node is not found, we throw a 409 |
|
| 750 | + throw new Exception\Conflict('The destination node is not found'); |
|
| 751 | + } |
|
| 752 | + |
|
| 753 | + try { |
|
| 754 | + $destinationNode = $this->tree->getNodeForPath($destination); |
|
| 755 | + |
|
| 756 | + // If this succeeded, it means the destination already exists |
|
| 757 | + // we'll need to throw precondition failed in case overwrite is false |
|
| 758 | + if (!$overwrite) { |
|
| 759 | + throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite'); |
|
| 760 | + } |
|
| 761 | + } catch (Exception\NotFound $e) { |
|
| 762 | + // Destination didn't exist, we're all good |
|
| 763 | + $destinationNode = false; |
|
| 764 | + } |
|
| 765 | + |
|
| 766 | + $requestPath = $request->getPath(); |
|
| 767 | + if ($destination === $requestPath) { |
|
| 768 | + throw new Exception\Forbidden('Source and destination uri are identical.'); |
|
| 769 | + } |
|
| 770 | + if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath.'/') { |
|
| 771 | + throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.'); |
|
| 772 | + } |
|
| 773 | + |
|
| 774 | + // These are the three relevant properties we need to return |
|
| 775 | + return [ |
|
| 776 | + 'destination' => $destination, |
|
| 777 | + 'destinationExists' => (bool) $destinationNode, |
|
| 778 | + 'destinationNode' => $destinationNode, |
|
| 779 | + ]; |
|
| 780 | + } |
|
| 781 | + |
|
| 782 | + /** |
|
| 783 | + * Returns a list of properties for a path. |
|
| 784 | + * |
|
| 785 | + * This is a simplified version getPropertiesForPath. If you aren't |
|
| 786 | + * interested in status codes, but you just want to have a flat list of |
|
| 787 | + * properties, use this method. |
|
| 788 | + * |
|
| 789 | + * Please note though that any problems related to retrieving properties, |
|
| 790 | + * such as permission issues will just result in an empty array being |
|
| 791 | + * returned. |
|
| 792 | + * |
|
| 793 | + * @param string $path |
|
| 794 | + * @param array $propertyNames |
|
| 795 | + * |
|
| 796 | + * @return array |
|
| 797 | + */ |
|
| 798 | + public function getProperties($path, $propertyNames) |
|
| 799 | + { |
|
| 800 | + $result = $this->getPropertiesForPath($path, $propertyNames, 0); |
|
| 801 | + if (isset($result[0][200])) { |
|
| 802 | + return $result[0][200]; |
|
| 803 | + } else { |
|
| 804 | + return []; |
|
| 805 | + } |
|
| 806 | + } |
|
| 807 | + |
|
| 808 | + /** |
|
| 809 | + * A kid-friendly way to fetch properties for a node's children. |
|
| 810 | + * |
|
| 811 | + * The returned array will be indexed by the path of the of child node. |
|
| 812 | + * Only properties that are actually found will be returned. |
|
| 813 | + * |
|
| 814 | + * The parent node will not be returned. |
|
| 815 | + * |
|
| 816 | + * @param string $path |
|
| 817 | + * @param array $propertyNames |
|
| 818 | + * |
|
| 819 | + * @return array |
|
| 820 | + */ |
|
| 821 | + public function getPropertiesForChildren($path, $propertyNames) |
|
| 822 | + { |
|
| 823 | + $result = []; |
|
| 824 | + foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) { |
|
| 825 | + // Skipping the parent path |
|
| 826 | + if (0 === $k) { |
|
| 827 | + continue; |
|
| 828 | + } |
|
| 829 | + |
|
| 830 | + $result[$row['href']] = $row[200]; |
|
| 831 | + } |
|
| 832 | + |
|
| 833 | + return $result; |
|
| 834 | + } |
|
| 835 | + |
|
| 836 | + /** |
|
| 837 | + * Returns a list of HTTP headers for a particular resource. |
|
| 838 | + * |
|
| 839 | + * The generated http headers are based on properties provided by the |
|
| 840 | + * resource. The method basically provides a simple mapping between |
|
| 841 | + * DAV property and HTTP header. |
|
| 842 | + * |
|
| 843 | + * The headers are intended to be used for HEAD and GET requests. |
|
| 844 | + * |
|
| 845 | + * @param string $path |
|
| 846 | + * |
|
| 847 | + * @return array |
|
| 848 | + */ |
|
| 849 | + public function getHTTPHeaders($path) |
|
| 850 | + { |
|
| 851 | + $propertyMap = [ |
|
| 852 | + '{DAV:}getcontenttype' => 'Content-Type', |
|
| 853 | + '{DAV:}getcontentlength' => 'Content-Length', |
|
| 854 | + '{DAV:}getlastmodified' => 'Last-Modified', |
|
| 855 | + '{DAV:}getetag' => 'ETag', |
|
| 856 | + ]; |
|
| 857 | + |
|
| 858 | + $properties = $this->getProperties($path, array_keys($propertyMap)); |
|
| 859 | + |
|
| 860 | + $headers = []; |
|
| 861 | + foreach ($propertyMap as $property => $header) { |
|
| 862 | + if (!isset($properties[$property])) { |
|
| 863 | + continue; |
|
| 864 | + } |
|
| 865 | + |
|
| 866 | + if (is_scalar($properties[$property])) { |
|
| 867 | + $headers[$header] = $properties[$property]; |
|
| 868 | + |
|
| 869 | + // GetLastModified gets special cased |
|
| 870 | + } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) { |
|
| 871 | + $headers[$header] = HTTP\toDate($properties[$property]->getTime()); |
|
| 872 | + } |
|
| 873 | + } |
|
| 874 | + |
|
| 875 | + return $headers; |
|
| 876 | + } |
|
| 877 | + |
|
| 878 | + /** |
|
| 879 | + * Small helper to support PROPFIND with DEPTH_INFINITY. |
|
| 880 | + * |
|
| 881 | + * @param array $yieldFirst |
|
| 882 | + * |
|
| 883 | + * @return \Traversable |
|
| 884 | + */ |
|
| 885 | + private function generatePathNodes(PropFind $propFind, array $yieldFirst = null) |
|
| 886 | + { |
|
| 887 | + if (null !== $yieldFirst) { |
|
| 888 | + yield $yieldFirst; |
|
| 889 | + } |
|
| 890 | + $newDepth = $propFind->getDepth(); |
|
| 891 | + $path = $propFind->getPath(); |
|
| 892 | + |
|
| 893 | + if (self::DEPTH_INFINITY !== $newDepth) { |
|
| 894 | + --$newDepth; |
|
| 895 | + } |
|
| 896 | + |
|
| 897 | + $propertyNames = $propFind->getRequestedProperties(); |
|
| 898 | + $propFindType = !$propFind->isAllProps() ? PropFind::NORMAL : PropFind::ALLPROPS; |
|
| 899 | + |
|
| 900 | + foreach ($this->tree->getChildren($path) as $childNode) { |
|
| 901 | + if ('' !== $path) { |
|
| 902 | + $subPath = $path.'/'.$childNode->getName(); |
|
| 903 | + } else { |
|
| 904 | + $subPath = $childNode->getName(); |
|
| 905 | + } |
|
| 906 | + $subPropFind = new PropFind($subPath, $propertyNames, $newDepth, $propFindType); |
|
| 907 | + |
|
| 908 | + yield [ |
|
| 909 | + $subPropFind, |
|
| 910 | + $childNode, |
|
| 911 | + ]; |
|
| 912 | + |
|
| 913 | + if ((self::DEPTH_INFINITY === $newDepth || $newDepth >= 1) && $childNode instanceof ICollection) { |
|
| 914 | + foreach ($this->generatePathNodes($subPropFind) as $subItem) { |
|
| 915 | + yield $subItem; |
|
| 916 | + } |
|
| 917 | + } |
|
| 918 | + } |
|
| 919 | + } |
|
| 920 | + |
|
| 921 | + /** |
|
| 922 | + * Returns a list of properties for a given path. |
|
| 923 | + * |
|
| 924 | + * The path that should be supplied should have the baseUrl stripped out |
|
| 925 | + * The list of properties should be supplied in Clark notation. If the list is empty |
|
| 926 | + * 'allprops' is assumed. |
|
| 927 | + * |
|
| 928 | + * If a depth of 1 is requested child elements will also be returned. |
|
| 929 | + * |
|
| 930 | + * @param string $path |
|
| 931 | + * @param array $propertyNames |
|
| 932 | + * @param int $depth |
|
| 933 | + * |
|
| 934 | + * @return array |
|
| 935 | + * |
|
| 936 | + * @deprecated Use getPropertiesIteratorForPath() instead (as it's more memory efficient) |
|
| 937 | + * @see getPropertiesIteratorForPath() |
|
| 938 | + */ |
|
| 939 | + public function getPropertiesForPath($path, $propertyNames = [], $depth = 0) |
|
| 940 | + { |
|
| 941 | + return iterator_to_array($this->getPropertiesIteratorForPath($path, $propertyNames, $depth)); |
|
| 942 | + } |
|
| 943 | + |
|
| 944 | + /** |
|
| 945 | + * Returns a list of properties for a given path. |
|
| 946 | + * |
|
| 947 | + * The path that should be supplied should have the baseUrl stripped out |
|
| 948 | + * The list of properties should be supplied in Clark notation. If the list is empty |
|
| 949 | + * 'allprops' is assumed. |
|
| 950 | + * |
|
| 951 | + * If a depth of 1 is requested child elements will also be returned. |
|
| 952 | + * |
|
| 953 | + * @param string $path |
|
| 954 | + * @param array $propertyNames |
|
| 955 | + * @param int $depth |
|
| 956 | + * |
|
| 957 | + * @return \Iterator |
|
| 958 | + */ |
|
| 959 | + public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0) |
|
| 960 | + { |
|
| 961 | + // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled |
|
| 962 | + if (!$this->enablePropfindDepthInfinity && 0 != $depth) { |
|
| 963 | + $depth = 1; |
|
| 964 | + } |
|
| 965 | + |
|
| 966 | + $path = trim($path, '/'); |
|
| 967 | + |
|
| 968 | + $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; |
|
| 969 | + $propFind = new PropFind($path, (array) $propertyNames, $depth, $propFindType); |
|
| 970 | + |
|
| 971 | + $parentNode = $this->tree->getNodeForPath($path); |
|
| 972 | + |
|
| 973 | + $propFindRequests = [[ |
|
| 974 | + $propFind, |
|
| 975 | + $parentNode, |
|
| 976 | + ]]; |
|
| 977 | + |
|
| 978 | + if (($depth > 0 || self::DEPTH_INFINITY === $depth) && $parentNode instanceof ICollection) { |
|
| 979 | + $propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests)); |
|
| 980 | + } |
|
| 981 | + |
|
| 982 | + foreach ($propFindRequests as $propFindRequest) { |
|
| 983 | + list($propFind, $node) = $propFindRequest; |
|
| 984 | + $r = $this->getPropertiesByNode($propFind, $node); |
|
| 985 | + if ($r) { |
|
| 986 | + $result = $propFind->getResultForMultiStatus(); |
|
| 987 | + $result['href'] = $propFind->getPath(); |
|
| 988 | + |
|
| 989 | + // WebDAV recommends adding a slash to the path, if the path is |
|
| 990 | + // a collection. |
|
| 991 | + // Furthermore, iCal also demands this to be the case for |
|
| 992 | + // principals. This is non-standard, but we support it. |
|
| 993 | + $resourceType = $this->getResourceTypeForNode($node); |
|
| 994 | + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { |
|
| 995 | + $result['href'] .= '/'; |
|
| 996 | + } |
|
| 997 | + yield $result; |
|
| 998 | + } |
|
| 999 | + } |
|
| 1000 | + } |
|
| 1001 | + |
|
| 1002 | + /** |
|
| 1003 | + * Returns a list of properties for a list of paths. |
|
| 1004 | + * |
|
| 1005 | + * The path that should be supplied should have the baseUrl stripped out |
|
| 1006 | + * The list of properties should be supplied in Clark notation. If the list is empty |
|
| 1007 | + * 'allprops' is assumed. |
|
| 1008 | + * |
|
| 1009 | + * The result is returned as an array, with paths for it's keys. |
|
| 1010 | + * The result may be returned out of order. |
|
| 1011 | + * |
|
| 1012 | + * @return array |
|
| 1013 | + */ |
|
| 1014 | + public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) |
|
| 1015 | + { |
|
| 1016 | + $result = [ |
|
| 1017 | + ]; |
|
| 1018 | + |
|
| 1019 | + $nodes = $this->tree->getMultipleNodes($paths); |
|
| 1020 | + |
|
| 1021 | + foreach ($nodes as $path => $node) { |
|
| 1022 | + $propFind = new PropFind($path, $propertyNames); |
|
| 1023 | + $r = $this->getPropertiesByNode($propFind, $node); |
|
| 1024 | + if ($r) { |
|
| 1025 | + $result[$path] = $propFind->getResultForMultiStatus(); |
|
| 1026 | + $result[$path]['href'] = $path; |
|
| 1027 | + |
|
| 1028 | + $resourceType = $this->getResourceTypeForNode($node); |
|
| 1029 | + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { |
|
| 1030 | + $result[$path]['href'] .= '/'; |
|
| 1031 | + } |
|
| 1032 | + } |
|
| 1033 | + } |
|
| 1034 | + |
|
| 1035 | + return $result; |
|
| 1036 | + } |
|
| 1037 | + |
|
| 1038 | + /** |
|
| 1039 | + * Determines all properties for a node. |
|
| 1040 | + * |
|
| 1041 | + * This method tries to grab all properties for a node. This method is used |
|
| 1042 | + * internally getPropertiesForPath and a few others. |
|
| 1043 | + * |
|
| 1044 | + * It could be useful to call this, if you already have an instance of your |
|
| 1045 | + * target node and simply want to run through the system to get a correct |
|
| 1046 | + * list of properties. |
|
| 1047 | + * |
|
| 1048 | + * @return bool |
|
| 1049 | + */ |
|
| 1050 | + public function getPropertiesByNode(PropFind $propFind, INode $node) |
|
| 1051 | + { |
|
| 1052 | + return $this->emit('propFind', [$propFind, $node]); |
|
| 1053 | + } |
|
| 1054 | + |
|
| 1055 | + /** |
|
| 1056 | + * This method is invoked by sub-systems creating a new file. |
|
| 1057 | + * |
|
| 1058 | + * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). |
|
| 1059 | + * It was important to get this done through a centralized function, |
|
| 1060 | + * allowing plugins to intercept this using the beforeCreateFile event. |
|
| 1061 | + * |
|
| 1062 | + * This method will return true if the file was actually created |
|
| 1063 | + * |
|
| 1064 | + * @param string $uri |
|
| 1065 | + * @param resource $data |
|
| 1066 | + * @param string $etag |
|
| 1067 | + * |
|
| 1068 | + * @return bool |
|
| 1069 | + */ |
|
| 1070 | + public function createFile($uri, $data, &$etag = null) |
|
| 1071 | + { |
|
| 1072 | + list($dir, $name) = Uri\split($uri); |
|
| 1073 | + |
|
| 1074 | + if (!$this->emit('beforeBind', [$uri])) { |
|
| 1075 | + return false; |
|
| 1076 | + } |
|
| 1077 | + |
|
| 1078 | + try { |
|
| 1079 | + $parent = $this->tree->getNodeForPath($dir); |
|
| 1080 | + } catch (Exception\NotFound $e) { |
|
| 1081 | + throw new Exception\Conflict('Files cannot be created in non-existent collections'); |
|
| 1082 | + } |
|
| 1083 | + |
|
| 1084 | + if (!$parent instanceof ICollection) { |
|
| 1085 | + throw new Exception\Conflict('Files can only be created as children of collections'); |
|
| 1086 | + } |
|
| 1087 | + |
|
| 1088 | + // It is possible for an event handler to modify the content of the |
|
| 1089 | + // body, before it gets written. If this is the case, $modified |
|
| 1090 | + // should be set to true. |
|
| 1091 | + // |
|
| 1092 | + // If $modified is true, we must not send back an ETag. |
|
| 1093 | + $modified = false; |
|
| 1094 | + if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) { |
|
| 1095 | + return false; |
|
| 1096 | + } |
|
| 1097 | + |
|
| 1098 | + $etag = $parent->createFile($name, $data); |
|
| 1099 | + |
|
| 1100 | + if ($modified) { |
|
| 1101 | + $etag = null; |
|
| 1102 | + } |
|
| 1103 | + |
|
| 1104 | + $this->tree->markDirty($dir.'/'.$name); |
|
| 1105 | + |
|
| 1106 | + $this->emit('afterBind', [$uri]); |
|
| 1107 | + $this->emit('afterCreateFile', [$uri, $parent]); |
|
| 1108 | + |
|
| 1109 | + return true; |
|
| 1110 | + } |
|
| 1111 | + |
|
| 1112 | + /** |
|
| 1113 | + * This method is invoked by sub-systems updating a file. |
|
| 1114 | + * |
|
| 1115 | + * This method will return true if the file was actually updated |
|
| 1116 | + * |
|
| 1117 | + * @param string $uri |
|
| 1118 | + * @param resource $data |
|
| 1119 | + * @param string $etag |
|
| 1120 | + * |
|
| 1121 | + * @return bool |
|
| 1122 | + */ |
|
| 1123 | + public function updateFile($uri, $data, &$etag = null) |
|
| 1124 | + { |
|
| 1125 | + $node = $this->tree->getNodeForPath($uri); |
|
| 1126 | + |
|
| 1127 | + // It is possible for an event handler to modify the content of the |
|
| 1128 | + // body, before it gets written. If this is the case, $modified |
|
| 1129 | + // should be set to true. |
|
| 1130 | + // |
|
| 1131 | + // If $modified is true, we must not send back an ETag. |
|
| 1132 | + $modified = false; |
|
| 1133 | + if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) { |
|
| 1134 | + return false; |
|
| 1135 | + } |
|
| 1136 | + |
|
| 1137 | + $etag = $node->put($data); |
|
| 1138 | + if ($modified) { |
|
| 1139 | + $etag = null; |
|
| 1140 | + } |
|
| 1141 | + $this->emit('afterWriteContent', [$uri, $node]); |
|
| 1142 | + |
|
| 1143 | + return true; |
|
| 1144 | + } |
|
| 1145 | + |
|
| 1146 | + /** |
|
| 1147 | + * This method is invoked by sub-systems creating a new directory. |
|
| 1148 | + * |
|
| 1149 | + * @param string $uri |
|
| 1150 | + */ |
|
| 1151 | + public function createDirectory($uri) |
|
| 1152 | + { |
|
| 1153 | + $this->createCollection($uri, new MkCol(['{DAV:}collection'], [])); |
|
| 1154 | + } |
|
| 1155 | + |
|
| 1156 | + /** |
|
| 1157 | + * Use this method to create a new collection. |
|
| 1158 | + * |
|
| 1159 | + * @param string $uri The new uri |
|
| 1160 | + * |
|
| 1161 | + * @return array|null |
|
| 1162 | + */ |
|
| 1163 | + public function createCollection($uri, MkCol $mkCol) |
|
| 1164 | + { |
|
| 1165 | + list($parentUri, $newName) = Uri\split($uri); |
|
| 1166 | + |
|
| 1167 | + // Making sure the parent exists |
|
| 1168 | + try { |
|
| 1169 | + $parent = $this->tree->getNodeForPath($parentUri); |
|
| 1170 | + } catch (Exception\NotFound $e) { |
|
| 1171 | + throw new Exception\Conflict('Parent node does not exist'); |
|
| 1172 | + } |
|
| 1173 | + |
|
| 1174 | + // Making sure the parent is a collection |
|
| 1175 | + if (!$parent instanceof ICollection) { |
|
| 1176 | + throw new Exception\Conflict('Parent node is not a collection'); |
|
| 1177 | + } |
|
| 1178 | + |
|
| 1179 | + // Making sure the child does not already exist |
|
| 1180 | + try { |
|
| 1181 | + $parent->getChild($newName); |
|
| 1182 | + |
|
| 1183 | + // If we got here.. it means there's already a node on that url, and we need to throw a 405 |
|
| 1184 | + throw new Exception\MethodNotAllowed('The resource you tried to create already exists'); |
|
| 1185 | + } catch (Exception\NotFound $e) { |
|
| 1186 | + // NotFound is the expected behavior. |
|
| 1187 | + } |
|
| 1188 | + |
|
| 1189 | + if (!$this->emit('beforeBind', [$uri])) { |
|
| 1190 | + return; |
|
| 1191 | + } |
|
| 1192 | + |
|
| 1193 | + if ($parent instanceof IExtendedCollection) { |
|
| 1194 | + /* |
|
| 1195 | 1195 | * If the parent is an instance of IExtendedCollection, it means that |
| 1196 | 1196 | * we can pass the MkCol object directly as it may be able to store |
| 1197 | 1197 | * properties immediately. |
| 1198 | 1198 | */ |
| 1199 | - $parent->createExtendedCollection($newName, $mkCol); |
|
| 1200 | - } else { |
|
| 1201 | - /* |
|
| 1199 | + $parent->createExtendedCollection($newName, $mkCol); |
|
| 1200 | + } else { |
|
| 1201 | + /* |
|
| 1202 | 1202 | * If the parent is a standard ICollection, it means only |
| 1203 | 1203 | * 'standard' collections can be created, so we should fail any |
| 1204 | 1204 | * MKCOL operation that carries extra resourcetypes. |
| 1205 | 1205 | */ |
| 1206 | - if (count($mkCol->getResourceType()) > 1) { |
|
| 1207 | - throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.'); |
|
| 1208 | - } |
|
| 1209 | - |
|
| 1210 | - $parent->createDirectory($newName); |
|
| 1211 | - } |
|
| 1212 | - |
|
| 1213 | - // If there are any properties that have not been handled/stored, |
|
| 1214 | - // we ask the 'propPatch' event to handle them. This will allow for |
|
| 1215 | - // example the propertyStorage system to store properties upon MKCOL. |
|
| 1216 | - if ($mkCol->getRemainingMutations()) { |
|
| 1217 | - $this->emit('propPatch', [$uri, $mkCol]); |
|
| 1218 | - } |
|
| 1219 | - $success = $mkCol->commit(); |
|
| 1220 | - |
|
| 1221 | - if (!$success) { |
|
| 1222 | - $result = $mkCol->getResult(); |
|
| 1223 | - |
|
| 1224 | - $formattedResult = [ |
|
| 1225 | - 'href' => $uri, |
|
| 1226 | - ]; |
|
| 1227 | - |
|
| 1228 | - foreach ($result as $propertyName => $status) { |
|
| 1229 | - if (!isset($formattedResult[$status])) { |
|
| 1230 | - $formattedResult[$status] = []; |
|
| 1231 | - } |
|
| 1232 | - $formattedResult[$status][$propertyName] = null; |
|
| 1233 | - } |
|
| 1234 | - |
|
| 1235 | - return $formattedResult; |
|
| 1236 | - } |
|
| 1237 | - |
|
| 1238 | - $this->tree->markDirty($parentUri); |
|
| 1239 | - $this->emit('afterBind', [$uri]); |
|
| 1240 | - $this->emit('afterCreateCollection', [$uri]); |
|
| 1241 | - } |
|
| 1242 | - |
|
| 1243 | - /** |
|
| 1244 | - * This method updates a resource's properties. |
|
| 1245 | - * |
|
| 1246 | - * The properties array must be a list of properties. Array-keys are |
|
| 1247 | - * property names in clarknotation, array-values are it's values. |
|
| 1248 | - * If a property must be deleted, the value should be null. |
|
| 1249 | - * |
|
| 1250 | - * Note that this request should either completely succeed, or |
|
| 1251 | - * completely fail. |
|
| 1252 | - * |
|
| 1253 | - * The response is an array with properties for keys, and http status codes |
|
| 1254 | - * as their values. |
|
| 1255 | - * |
|
| 1256 | - * @param string $path |
|
| 1257 | - * |
|
| 1258 | - * @return array |
|
| 1259 | - */ |
|
| 1260 | - public function updateProperties($path, array $properties) |
|
| 1261 | - { |
|
| 1262 | - $propPatch = new PropPatch($properties); |
|
| 1263 | - $this->emit('propPatch', [$path, $propPatch]); |
|
| 1264 | - $propPatch->commit(); |
|
| 1265 | - |
|
| 1266 | - return $propPatch->getResult(); |
|
| 1267 | - } |
|
| 1268 | - |
|
| 1269 | - /** |
|
| 1270 | - * This method checks the main HTTP preconditions. |
|
| 1271 | - * |
|
| 1272 | - * Currently these are: |
|
| 1273 | - * * If-Match |
|
| 1274 | - * * If-None-Match |
|
| 1275 | - * * If-Modified-Since |
|
| 1276 | - * * If-Unmodified-Since |
|
| 1277 | - * |
|
| 1278 | - * The method will return true if all preconditions are met |
|
| 1279 | - * The method will return false, or throw an exception if preconditions |
|
| 1280 | - * failed. If false is returned the operation should be aborted, and |
|
| 1281 | - * the appropriate HTTP response headers are already set. |
|
| 1282 | - * |
|
| 1283 | - * Normally this method will throw 412 Precondition Failed for failures |
|
| 1284 | - * related to If-None-Match, If-Match and If-Unmodified Since. It will |
|
| 1285 | - * set the status to 304 Not Modified for If-Modified_since. |
|
| 1286 | - * |
|
| 1287 | - * @return bool |
|
| 1288 | - */ |
|
| 1289 | - public function checkPreconditions(RequestInterface $request, ResponseInterface $response) |
|
| 1290 | - { |
|
| 1291 | - $path = $request->getPath(); |
|
| 1292 | - $node = null; |
|
| 1293 | - $lastMod = null; |
|
| 1294 | - $etag = null; |
|
| 1295 | - |
|
| 1296 | - if ($ifMatch = $request->getHeader('If-Match')) { |
|
| 1297 | - // If-Match contains an entity tag. Only if the entity-tag |
|
| 1298 | - // matches we are allowed to make the request succeed. |
|
| 1299 | - // If the entity-tag is '*' we are only allowed to make the |
|
| 1300 | - // request succeed if a resource exists at that url. |
|
| 1301 | - try { |
|
| 1302 | - $node = $this->tree->getNodeForPath($path); |
|
| 1303 | - } catch (Exception\NotFound $e) { |
|
| 1304 | - throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match'); |
|
| 1305 | - } |
|
| 1306 | - |
|
| 1307 | - // Only need to check entity tags if they are not * |
|
| 1308 | - if ('*' !== $ifMatch) { |
|
| 1309 | - // There can be multiple ETags |
|
| 1310 | - $ifMatch = explode(',', $ifMatch); |
|
| 1311 | - $haveMatch = false; |
|
| 1312 | - foreach ($ifMatch as $ifMatchItem) { |
|
| 1313 | - // Stripping any extra spaces |
|
| 1314 | - $ifMatchItem = trim($ifMatchItem, ' '); |
|
| 1315 | - |
|
| 1316 | - $etag = $node instanceof IFile ? $node->getETag() : null; |
|
| 1317 | - if ($etag === $ifMatchItem) { |
|
| 1318 | - $haveMatch = true; |
|
| 1319 | - } else { |
|
| 1320 | - // Evolution has a bug where it sometimes prepends the " |
|
| 1321 | - // with a \. This is our workaround. |
|
| 1322 | - if (str_replace('\\"', '"', $ifMatchItem) === $etag) { |
|
| 1323 | - $haveMatch = true; |
|
| 1324 | - } |
|
| 1325 | - } |
|
| 1326 | - } |
|
| 1327 | - if (!$haveMatch) { |
|
| 1328 | - if ($etag) { |
|
| 1329 | - $response->setHeader('ETag', $etag); |
|
| 1330 | - } |
|
| 1331 | - throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified ETags matched.', 'If-Match'); |
|
| 1332 | - } |
|
| 1333 | - } |
|
| 1334 | - } |
|
| 1335 | - |
|
| 1336 | - if ($ifNoneMatch = $request->getHeader('If-None-Match')) { |
|
| 1337 | - // The If-None-Match header contains an ETag. |
|
| 1338 | - // Only if the ETag does not match the current ETag, the request will succeed |
|
| 1339 | - // The header can also contain *, in which case the request |
|
| 1340 | - // will only succeed if the entity does not exist at all. |
|
| 1341 | - $nodeExists = true; |
|
| 1342 | - if (!$node) { |
|
| 1343 | - try { |
|
| 1344 | - $node = $this->tree->getNodeForPath($path); |
|
| 1345 | - } catch (Exception\NotFound $e) { |
|
| 1346 | - $nodeExists = false; |
|
| 1347 | - } |
|
| 1348 | - } |
|
| 1349 | - if ($nodeExists) { |
|
| 1350 | - $haveMatch = false; |
|
| 1351 | - if ('*' === $ifNoneMatch) { |
|
| 1352 | - $haveMatch = true; |
|
| 1353 | - } else { |
|
| 1354 | - // There might be multiple ETags |
|
| 1355 | - $ifNoneMatch = explode(',', $ifNoneMatch); |
|
| 1356 | - $etag = $node instanceof IFile ? $node->getETag() : null; |
|
| 1357 | - |
|
| 1358 | - foreach ($ifNoneMatch as $ifNoneMatchItem) { |
|
| 1359 | - // Stripping any extra spaces |
|
| 1360 | - $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); |
|
| 1361 | - |
|
| 1362 | - if ($etag === $ifNoneMatchItem) { |
|
| 1363 | - $haveMatch = true; |
|
| 1364 | - } |
|
| 1365 | - } |
|
| 1366 | - } |
|
| 1367 | - |
|
| 1368 | - if ($haveMatch) { |
|
| 1369 | - if ($etag) { |
|
| 1370 | - $response->setHeader('ETag', $etag); |
|
| 1371 | - } |
|
| 1372 | - if ('GET' === $request->getMethod()) { |
|
| 1373 | - $response->setStatus(304); |
|
| 1374 | - |
|
| 1375 | - return false; |
|
| 1376 | - } else { |
|
| 1377 | - throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match'); |
|
| 1378 | - } |
|
| 1379 | - } |
|
| 1380 | - } |
|
| 1381 | - } |
|
| 1382 | - |
|
| 1383 | - if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) { |
|
| 1384 | - // The If-Modified-Since header contains a date. We |
|
| 1385 | - // will only return the entity if it has been changed since |
|
| 1386 | - // that date. If it hasn't been changed, we return a 304 |
|
| 1387 | - // header |
|
| 1388 | - // Note that this header only has to be checked if there was no If-None-Match header |
|
| 1389 | - // as per the HTTP spec. |
|
| 1390 | - $date = HTTP\parseDate($ifModifiedSince); |
|
| 1391 | - |
|
| 1392 | - if ($date) { |
|
| 1393 | - if (is_null($node)) { |
|
| 1394 | - $node = $this->tree->getNodeForPath($path); |
|
| 1395 | - } |
|
| 1396 | - $lastMod = $node->getLastModified(); |
|
| 1397 | - if ($lastMod) { |
|
| 1398 | - $lastMod = new \DateTime('@'.$lastMod); |
|
| 1399 | - if ($lastMod <= $date) { |
|
| 1400 | - $response->setStatus(304); |
|
| 1401 | - $response->setHeader('Last-Modified', HTTP\toDate($lastMod)); |
|
| 1402 | - |
|
| 1403 | - return false; |
|
| 1404 | - } |
|
| 1405 | - } |
|
| 1406 | - } |
|
| 1407 | - } |
|
| 1408 | - |
|
| 1409 | - if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) { |
|
| 1410 | - // The If-Unmodified-Since will allow allow the request if the |
|
| 1411 | - // entity has not changed since the specified date. |
|
| 1412 | - $date = HTTP\parseDate($ifUnmodifiedSince); |
|
| 1413 | - |
|
| 1414 | - // We must only check the date if it's valid |
|
| 1415 | - if ($date) { |
|
| 1416 | - if (is_null($node)) { |
|
| 1417 | - $node = $this->tree->getNodeForPath($path); |
|
| 1418 | - } |
|
| 1419 | - $lastMod = $node->getLastModified(); |
|
| 1420 | - if ($lastMod) { |
|
| 1421 | - $lastMod = new \DateTime('@'.$lastMod); |
|
| 1422 | - if ($lastMod > $date) { |
|
| 1423 | - throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since'); |
|
| 1424 | - } |
|
| 1425 | - } |
|
| 1426 | - } |
|
| 1427 | - } |
|
| 1428 | - |
|
| 1429 | - // Now the hardest, the If: header. The If: header can contain multiple |
|
| 1430 | - // urls, ETags and so-called 'state tokens'. |
|
| 1431 | - // |
|
| 1432 | - // Examples of state tokens include lock-tokens (as defined in rfc4918) |
|
| 1433 | - // and sync-tokens (as defined in rfc6578). |
|
| 1434 | - // |
|
| 1435 | - // The only proper way to deal with these, is to emit events, that a |
|
| 1436 | - // Sync and Lock plugin can pick up. |
|
| 1437 | - $ifConditions = $this->getIfConditions($request); |
|
| 1438 | - |
|
| 1439 | - foreach ($ifConditions as $kk => $ifCondition) { |
|
| 1440 | - foreach ($ifCondition['tokens'] as $ii => $token) { |
|
| 1441 | - $ifConditions[$kk]['tokens'][$ii]['validToken'] = false; |
|
| 1442 | - } |
|
| 1443 | - } |
|
| 1444 | - |
|
| 1445 | - // Plugins are responsible for validating all the tokens. |
|
| 1446 | - // If a plugin deemed a token 'valid', it will set 'validToken' to |
|
| 1447 | - // true. |
|
| 1448 | - $this->emit('validateTokens', [$request, &$ifConditions]); |
|
| 1449 | - |
|
| 1450 | - // Now we're going to analyze the result. |
|
| 1451 | - |
|
| 1452 | - // Every ifCondition needs to validate to true, so we exit as soon as |
|
| 1453 | - // we have an invalid condition. |
|
| 1454 | - foreach ($ifConditions as $ifCondition) { |
|
| 1455 | - $uri = $ifCondition['uri']; |
|
| 1456 | - $tokens = $ifCondition['tokens']; |
|
| 1457 | - |
|
| 1458 | - // We only need 1 valid token for the condition to succeed. |
|
| 1459 | - foreach ($tokens as $token) { |
|
| 1460 | - $tokenValid = $token['validToken'] || !$token['token']; |
|
| 1461 | - |
|
| 1462 | - $etagValid = false; |
|
| 1463 | - if (!$token['etag']) { |
|
| 1464 | - $etagValid = true; |
|
| 1465 | - } |
|
| 1466 | - // Checking the ETag, only if the token was already deemed |
|
| 1467 | - // valid and there is one. |
|
| 1468 | - if ($token['etag'] && $tokenValid) { |
|
| 1469 | - // The token was valid, and there was an ETag. We must |
|
| 1470 | - // grab the current ETag and check it. |
|
| 1471 | - $node = $this->tree->getNodeForPath($uri); |
|
| 1472 | - $etagValid = $node instanceof IFile && $node->getETag() == $token['etag']; |
|
| 1473 | - } |
|
| 1474 | - |
|
| 1475 | - if (($tokenValid && $etagValid) ^ $token['negate']) { |
|
| 1476 | - // Both were valid, so we can go to the next condition. |
|
| 1477 | - continue 2; |
|
| 1478 | - } |
|
| 1479 | - } |
|
| 1480 | - |
|
| 1481 | - // If we ended here, it means there was no valid ETag + token |
|
| 1482 | - // combination found for the current condition. This means we fail! |
|
| 1483 | - throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for '.$uri, 'If'); |
|
| 1484 | - } |
|
| 1485 | - |
|
| 1486 | - return true; |
|
| 1487 | - } |
|
| 1488 | - |
|
| 1489 | - /** |
|
| 1490 | - * This method is created to extract information from the WebDAV HTTP 'If:' header. |
|
| 1491 | - * |
|
| 1492 | - * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information |
|
| 1493 | - * The function will return an array, containing structs with the following keys |
|
| 1494 | - * |
|
| 1495 | - * * uri - the uri the condition applies to. |
|
| 1496 | - * * tokens - The lock token. another 2 dimensional array containing 3 elements |
|
| 1497 | - * |
|
| 1498 | - * Example 1: |
|
| 1499 | - * |
|
| 1500 | - * If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>) |
|
| 1501 | - * |
|
| 1502 | - * Would result in: |
|
| 1503 | - * |
|
| 1504 | - * [ |
|
| 1505 | - * [ |
|
| 1506 | - * 'uri' => '/request/uri', |
|
| 1507 | - * 'tokens' => [ |
|
| 1508 | - * [ |
|
| 1509 | - * [ |
|
| 1510 | - * 'negate' => false, |
|
| 1511 | - * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', |
|
| 1512 | - * 'etag' => "" |
|
| 1513 | - * ] |
|
| 1514 | - * ] |
|
| 1515 | - * ], |
|
| 1516 | - * ] |
|
| 1517 | - * ] |
|
| 1518 | - * |
|
| 1519 | - * Example 2: |
|
| 1520 | - * |
|
| 1521 | - * If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"]) |
|
| 1522 | - * |
|
| 1523 | - * Would result in: |
|
| 1524 | - * |
|
| 1525 | - * [ |
|
| 1526 | - * [ |
|
| 1527 | - * 'uri' => 'path', |
|
| 1528 | - * 'tokens' => [ |
|
| 1529 | - * [ |
|
| 1530 | - * [ |
|
| 1531 | - * 'negate' => true, |
|
| 1532 | - * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', |
|
| 1533 | - * 'etag' => '"Im An ETag"' |
|
| 1534 | - * ], |
|
| 1535 | - * [ |
|
| 1536 | - * 'negate' => false, |
|
| 1537 | - * 'token' => '', |
|
| 1538 | - * 'etag' => '"Another ETag"' |
|
| 1539 | - * ] |
|
| 1540 | - * ] |
|
| 1541 | - * ], |
|
| 1542 | - * ], |
|
| 1543 | - * [ |
|
| 1544 | - * 'uri' => 'path2', |
|
| 1545 | - * 'tokens' => [ |
|
| 1546 | - * [ |
|
| 1547 | - * [ |
|
| 1548 | - * 'negate' => true, |
|
| 1549 | - * 'token' => '', |
|
| 1550 | - * 'etag' => '"Path2 ETag"' |
|
| 1551 | - * ] |
|
| 1552 | - * ] |
|
| 1553 | - * ], |
|
| 1554 | - * ], |
|
| 1555 | - * ] |
|
| 1556 | - * |
|
| 1557 | - * @return array |
|
| 1558 | - */ |
|
| 1559 | - public function getIfConditions(RequestInterface $request) |
|
| 1560 | - { |
|
| 1561 | - $header = $request->getHeader('If'); |
|
| 1562 | - if (!$header) { |
|
| 1563 | - return []; |
|
| 1564 | - } |
|
| 1565 | - |
|
| 1566 | - $matches = []; |
|
| 1567 | - |
|
| 1568 | - $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im'; |
|
| 1569 | - preg_match_all($regex, $header, $matches, PREG_SET_ORDER); |
|
| 1570 | - |
|
| 1571 | - $conditions = []; |
|
| 1572 | - |
|
| 1573 | - foreach ($matches as $match) { |
|
| 1574 | - // If there was no uri specified in this match, and there were |
|
| 1575 | - // already conditions parsed, we add the condition to the list of |
|
| 1576 | - // conditions for the previous uri. |
|
| 1577 | - if (!$match['uri'] && count($conditions)) { |
|
| 1578 | - $conditions[count($conditions) - 1]['tokens'][] = [ |
|
| 1579 | - 'negate' => $match['not'] ? true : false, |
|
| 1580 | - 'token' => $match['token'], |
|
| 1581 | - 'etag' => isset($match['etag']) ? $match['etag'] : '', |
|
| 1582 | - ]; |
|
| 1583 | - } else { |
|
| 1584 | - if (!$match['uri']) { |
|
| 1585 | - $realUri = $request->getPath(); |
|
| 1586 | - } else { |
|
| 1587 | - $realUri = $this->calculateUri($match['uri']); |
|
| 1588 | - } |
|
| 1589 | - |
|
| 1590 | - $conditions[] = [ |
|
| 1591 | - 'uri' => $realUri, |
|
| 1592 | - 'tokens' => [ |
|
| 1593 | - [ |
|
| 1594 | - 'negate' => $match['not'] ? true : false, |
|
| 1595 | - 'token' => $match['token'], |
|
| 1596 | - 'etag' => isset($match['etag']) ? $match['etag'] : '', |
|
| 1597 | - ], |
|
| 1598 | - ], |
|
| 1599 | - ]; |
|
| 1600 | - } |
|
| 1601 | - } |
|
| 1602 | - |
|
| 1603 | - return $conditions; |
|
| 1604 | - } |
|
| 1605 | - |
|
| 1606 | - /** |
|
| 1607 | - * Returns an array with resourcetypes for a node. |
|
| 1608 | - * |
|
| 1609 | - * @return array |
|
| 1610 | - */ |
|
| 1611 | - public function getResourceTypeForNode(INode $node) |
|
| 1612 | - { |
|
| 1613 | - $result = []; |
|
| 1614 | - foreach ($this->resourceTypeMapping as $className => $resourceType) { |
|
| 1615 | - if ($node instanceof $className) { |
|
| 1616 | - $result[] = $resourceType; |
|
| 1617 | - } |
|
| 1618 | - } |
|
| 1619 | - |
|
| 1620 | - return $result; |
|
| 1621 | - } |
|
| 1622 | - |
|
| 1623 | - // }}} |
|
| 1624 | - // {{{ XML Readers & Writers |
|
| 1625 | - |
|
| 1626 | - /** |
|
| 1627 | - * Returns a callback generating a WebDAV propfind response body based on a list of nodes. |
|
| 1628 | - * |
|
| 1629 | - * If 'strip404s' is set to true, all 404 responses will be removed. |
|
| 1630 | - * |
|
| 1631 | - * @param array|\Traversable $fileProperties The list with nodes |
|
| 1632 | - * @param bool $strip404s |
|
| 1633 | - * |
|
| 1634 | - * @return callable|string |
|
| 1635 | - */ |
|
| 1636 | - public function generateMultiStatus($fileProperties, $strip404s = false) |
|
| 1637 | - { |
|
| 1638 | - $w = $this->xml->getWriter(); |
|
| 1639 | - if (self::$streamMultiStatus) { |
|
| 1640 | - return function () use ($fileProperties, $strip404s, $w) { |
|
| 1641 | - $w->openUri('php://output'); |
|
| 1642 | - $this->writeMultiStatus($w, $fileProperties, $strip404s); |
|
| 1643 | - $w->flush(); |
|
| 1644 | - }; |
|
| 1645 | - } |
|
| 1646 | - $w->openMemory(); |
|
| 1647 | - $this->writeMultiStatus($w, $fileProperties, $strip404s); |
|
| 1648 | - |
|
| 1649 | - return $w->outputMemory(); |
|
| 1650 | - } |
|
| 1651 | - |
|
| 1652 | - /** |
|
| 1653 | - * @param $fileProperties |
|
| 1654 | - */ |
|
| 1655 | - private function writeMultiStatus(Writer $w, $fileProperties, bool $strip404s) |
|
| 1656 | - { |
|
| 1657 | - $w->contextUri = $this->baseUri; |
|
| 1658 | - $w->startDocument(); |
|
| 1659 | - |
|
| 1660 | - $w->startElement('{DAV:}multistatus'); |
|
| 1661 | - |
|
| 1662 | - foreach ($fileProperties as $entry) { |
|
| 1663 | - $href = $entry['href']; |
|
| 1664 | - unset($entry['href']); |
|
| 1665 | - if ($strip404s) { |
|
| 1666 | - unset($entry[404]); |
|
| 1667 | - } |
|
| 1668 | - $response = new Xml\Element\Response( |
|
| 1669 | - ltrim($href, '/'), |
|
| 1670 | - $entry |
|
| 1671 | - ); |
|
| 1672 | - $w->write([ |
|
| 1673 | - 'name' => '{DAV:}response', |
|
| 1674 | - 'value' => $response, |
|
| 1675 | - ]); |
|
| 1676 | - } |
|
| 1677 | - $w->endElement(); |
|
| 1678 | - $w->endDocument(); |
|
| 1679 | - } |
|
| 1206 | + if (count($mkCol->getResourceType()) > 1) { |
|
| 1207 | + throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.'); |
|
| 1208 | + } |
|
| 1209 | + |
|
| 1210 | + $parent->createDirectory($newName); |
|
| 1211 | + } |
|
| 1212 | + |
|
| 1213 | + // If there are any properties that have not been handled/stored, |
|
| 1214 | + // we ask the 'propPatch' event to handle them. This will allow for |
|
| 1215 | + // example the propertyStorage system to store properties upon MKCOL. |
|
| 1216 | + if ($mkCol->getRemainingMutations()) { |
|
| 1217 | + $this->emit('propPatch', [$uri, $mkCol]); |
|
| 1218 | + } |
|
| 1219 | + $success = $mkCol->commit(); |
|
| 1220 | + |
|
| 1221 | + if (!$success) { |
|
| 1222 | + $result = $mkCol->getResult(); |
|
| 1223 | + |
|
| 1224 | + $formattedResult = [ |
|
| 1225 | + 'href' => $uri, |
|
| 1226 | + ]; |
|
| 1227 | + |
|
| 1228 | + foreach ($result as $propertyName => $status) { |
|
| 1229 | + if (!isset($formattedResult[$status])) { |
|
| 1230 | + $formattedResult[$status] = []; |
|
| 1231 | + } |
|
| 1232 | + $formattedResult[$status][$propertyName] = null; |
|
| 1233 | + } |
|
| 1234 | + |
|
| 1235 | + return $formattedResult; |
|
| 1236 | + } |
|
| 1237 | + |
|
| 1238 | + $this->tree->markDirty($parentUri); |
|
| 1239 | + $this->emit('afterBind', [$uri]); |
|
| 1240 | + $this->emit('afterCreateCollection', [$uri]); |
|
| 1241 | + } |
|
| 1242 | + |
|
| 1243 | + /** |
|
| 1244 | + * This method updates a resource's properties. |
|
| 1245 | + * |
|
| 1246 | + * The properties array must be a list of properties. Array-keys are |
|
| 1247 | + * property names in clarknotation, array-values are it's values. |
|
| 1248 | + * If a property must be deleted, the value should be null. |
|
| 1249 | + * |
|
| 1250 | + * Note that this request should either completely succeed, or |
|
| 1251 | + * completely fail. |
|
| 1252 | + * |
|
| 1253 | + * The response is an array with properties for keys, and http status codes |
|
| 1254 | + * as their values. |
|
| 1255 | + * |
|
| 1256 | + * @param string $path |
|
| 1257 | + * |
|
| 1258 | + * @return array |
|
| 1259 | + */ |
|
| 1260 | + public function updateProperties($path, array $properties) |
|
| 1261 | + { |
|
| 1262 | + $propPatch = new PropPatch($properties); |
|
| 1263 | + $this->emit('propPatch', [$path, $propPatch]); |
|
| 1264 | + $propPatch->commit(); |
|
| 1265 | + |
|
| 1266 | + return $propPatch->getResult(); |
|
| 1267 | + } |
|
| 1268 | + |
|
| 1269 | + /** |
|
| 1270 | + * This method checks the main HTTP preconditions. |
|
| 1271 | + * |
|
| 1272 | + * Currently these are: |
|
| 1273 | + * * If-Match |
|
| 1274 | + * * If-None-Match |
|
| 1275 | + * * If-Modified-Since |
|
| 1276 | + * * If-Unmodified-Since |
|
| 1277 | + * |
|
| 1278 | + * The method will return true if all preconditions are met |
|
| 1279 | + * The method will return false, or throw an exception if preconditions |
|
| 1280 | + * failed. If false is returned the operation should be aborted, and |
|
| 1281 | + * the appropriate HTTP response headers are already set. |
|
| 1282 | + * |
|
| 1283 | + * Normally this method will throw 412 Precondition Failed for failures |
|
| 1284 | + * related to If-None-Match, If-Match and If-Unmodified Since. It will |
|
| 1285 | + * set the status to 304 Not Modified for If-Modified_since. |
|
| 1286 | + * |
|
| 1287 | + * @return bool |
|
| 1288 | + */ |
|
| 1289 | + public function checkPreconditions(RequestInterface $request, ResponseInterface $response) |
|
| 1290 | + { |
|
| 1291 | + $path = $request->getPath(); |
|
| 1292 | + $node = null; |
|
| 1293 | + $lastMod = null; |
|
| 1294 | + $etag = null; |
|
| 1295 | + |
|
| 1296 | + if ($ifMatch = $request->getHeader('If-Match')) { |
|
| 1297 | + // If-Match contains an entity tag. Only if the entity-tag |
|
| 1298 | + // matches we are allowed to make the request succeed. |
|
| 1299 | + // If the entity-tag is '*' we are only allowed to make the |
|
| 1300 | + // request succeed if a resource exists at that url. |
|
| 1301 | + try { |
|
| 1302 | + $node = $this->tree->getNodeForPath($path); |
|
| 1303 | + } catch (Exception\NotFound $e) { |
|
| 1304 | + throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match'); |
|
| 1305 | + } |
|
| 1306 | + |
|
| 1307 | + // Only need to check entity tags if they are not * |
|
| 1308 | + if ('*' !== $ifMatch) { |
|
| 1309 | + // There can be multiple ETags |
|
| 1310 | + $ifMatch = explode(',', $ifMatch); |
|
| 1311 | + $haveMatch = false; |
|
| 1312 | + foreach ($ifMatch as $ifMatchItem) { |
|
| 1313 | + // Stripping any extra spaces |
|
| 1314 | + $ifMatchItem = trim($ifMatchItem, ' '); |
|
| 1315 | + |
|
| 1316 | + $etag = $node instanceof IFile ? $node->getETag() : null; |
|
| 1317 | + if ($etag === $ifMatchItem) { |
|
| 1318 | + $haveMatch = true; |
|
| 1319 | + } else { |
|
| 1320 | + // Evolution has a bug where it sometimes prepends the " |
|
| 1321 | + // with a \. This is our workaround. |
|
| 1322 | + if (str_replace('\\"', '"', $ifMatchItem) === $etag) { |
|
| 1323 | + $haveMatch = true; |
|
| 1324 | + } |
|
| 1325 | + } |
|
| 1326 | + } |
|
| 1327 | + if (!$haveMatch) { |
|
| 1328 | + if ($etag) { |
|
| 1329 | + $response->setHeader('ETag', $etag); |
|
| 1330 | + } |
|
| 1331 | + throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified ETags matched.', 'If-Match'); |
|
| 1332 | + } |
|
| 1333 | + } |
|
| 1334 | + } |
|
| 1335 | + |
|
| 1336 | + if ($ifNoneMatch = $request->getHeader('If-None-Match')) { |
|
| 1337 | + // The If-None-Match header contains an ETag. |
|
| 1338 | + // Only if the ETag does not match the current ETag, the request will succeed |
|
| 1339 | + // The header can also contain *, in which case the request |
|
| 1340 | + // will only succeed if the entity does not exist at all. |
|
| 1341 | + $nodeExists = true; |
|
| 1342 | + if (!$node) { |
|
| 1343 | + try { |
|
| 1344 | + $node = $this->tree->getNodeForPath($path); |
|
| 1345 | + } catch (Exception\NotFound $e) { |
|
| 1346 | + $nodeExists = false; |
|
| 1347 | + } |
|
| 1348 | + } |
|
| 1349 | + if ($nodeExists) { |
|
| 1350 | + $haveMatch = false; |
|
| 1351 | + if ('*' === $ifNoneMatch) { |
|
| 1352 | + $haveMatch = true; |
|
| 1353 | + } else { |
|
| 1354 | + // There might be multiple ETags |
|
| 1355 | + $ifNoneMatch = explode(',', $ifNoneMatch); |
|
| 1356 | + $etag = $node instanceof IFile ? $node->getETag() : null; |
|
| 1357 | + |
|
| 1358 | + foreach ($ifNoneMatch as $ifNoneMatchItem) { |
|
| 1359 | + // Stripping any extra spaces |
|
| 1360 | + $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); |
|
| 1361 | + |
|
| 1362 | + if ($etag === $ifNoneMatchItem) { |
|
| 1363 | + $haveMatch = true; |
|
| 1364 | + } |
|
| 1365 | + } |
|
| 1366 | + } |
|
| 1367 | + |
|
| 1368 | + if ($haveMatch) { |
|
| 1369 | + if ($etag) { |
|
| 1370 | + $response->setHeader('ETag', $etag); |
|
| 1371 | + } |
|
| 1372 | + if ('GET' === $request->getMethod()) { |
|
| 1373 | + $response->setStatus(304); |
|
| 1374 | + |
|
| 1375 | + return false; |
|
| 1376 | + } else { |
|
| 1377 | + throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match'); |
|
| 1378 | + } |
|
| 1379 | + } |
|
| 1380 | + } |
|
| 1381 | + } |
|
| 1382 | + |
|
| 1383 | + if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) { |
|
| 1384 | + // The If-Modified-Since header contains a date. We |
|
| 1385 | + // will only return the entity if it has been changed since |
|
| 1386 | + // that date. If it hasn't been changed, we return a 304 |
|
| 1387 | + // header |
|
| 1388 | + // Note that this header only has to be checked if there was no If-None-Match header |
|
| 1389 | + // as per the HTTP spec. |
|
| 1390 | + $date = HTTP\parseDate($ifModifiedSince); |
|
| 1391 | + |
|
| 1392 | + if ($date) { |
|
| 1393 | + if (is_null($node)) { |
|
| 1394 | + $node = $this->tree->getNodeForPath($path); |
|
| 1395 | + } |
|
| 1396 | + $lastMod = $node->getLastModified(); |
|
| 1397 | + if ($lastMod) { |
|
| 1398 | + $lastMod = new \DateTime('@'.$lastMod); |
|
| 1399 | + if ($lastMod <= $date) { |
|
| 1400 | + $response->setStatus(304); |
|
| 1401 | + $response->setHeader('Last-Modified', HTTP\toDate($lastMod)); |
|
| 1402 | + |
|
| 1403 | + return false; |
|
| 1404 | + } |
|
| 1405 | + } |
|
| 1406 | + } |
|
| 1407 | + } |
|
| 1408 | + |
|
| 1409 | + if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) { |
|
| 1410 | + // The If-Unmodified-Since will allow allow the request if the |
|
| 1411 | + // entity has not changed since the specified date. |
|
| 1412 | + $date = HTTP\parseDate($ifUnmodifiedSince); |
|
| 1413 | + |
|
| 1414 | + // We must only check the date if it's valid |
|
| 1415 | + if ($date) { |
|
| 1416 | + if (is_null($node)) { |
|
| 1417 | + $node = $this->tree->getNodeForPath($path); |
|
| 1418 | + } |
|
| 1419 | + $lastMod = $node->getLastModified(); |
|
| 1420 | + if ($lastMod) { |
|
| 1421 | + $lastMod = new \DateTime('@'.$lastMod); |
|
| 1422 | + if ($lastMod > $date) { |
|
| 1423 | + throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since'); |
|
| 1424 | + } |
|
| 1425 | + } |
|
| 1426 | + } |
|
| 1427 | + } |
|
| 1428 | + |
|
| 1429 | + // Now the hardest, the If: header. The If: header can contain multiple |
|
| 1430 | + // urls, ETags and so-called 'state tokens'. |
|
| 1431 | + // |
|
| 1432 | + // Examples of state tokens include lock-tokens (as defined in rfc4918) |
|
| 1433 | + // and sync-tokens (as defined in rfc6578). |
|
| 1434 | + // |
|
| 1435 | + // The only proper way to deal with these, is to emit events, that a |
|
| 1436 | + // Sync and Lock plugin can pick up. |
|
| 1437 | + $ifConditions = $this->getIfConditions($request); |
|
| 1438 | + |
|
| 1439 | + foreach ($ifConditions as $kk => $ifCondition) { |
|
| 1440 | + foreach ($ifCondition['tokens'] as $ii => $token) { |
|
| 1441 | + $ifConditions[$kk]['tokens'][$ii]['validToken'] = false; |
|
| 1442 | + } |
|
| 1443 | + } |
|
| 1444 | + |
|
| 1445 | + // Plugins are responsible for validating all the tokens. |
|
| 1446 | + // If a plugin deemed a token 'valid', it will set 'validToken' to |
|
| 1447 | + // true. |
|
| 1448 | + $this->emit('validateTokens', [$request, &$ifConditions]); |
|
| 1449 | + |
|
| 1450 | + // Now we're going to analyze the result. |
|
| 1451 | + |
|
| 1452 | + // Every ifCondition needs to validate to true, so we exit as soon as |
|
| 1453 | + // we have an invalid condition. |
|
| 1454 | + foreach ($ifConditions as $ifCondition) { |
|
| 1455 | + $uri = $ifCondition['uri']; |
|
| 1456 | + $tokens = $ifCondition['tokens']; |
|
| 1457 | + |
|
| 1458 | + // We only need 1 valid token for the condition to succeed. |
|
| 1459 | + foreach ($tokens as $token) { |
|
| 1460 | + $tokenValid = $token['validToken'] || !$token['token']; |
|
| 1461 | + |
|
| 1462 | + $etagValid = false; |
|
| 1463 | + if (!$token['etag']) { |
|
| 1464 | + $etagValid = true; |
|
| 1465 | + } |
|
| 1466 | + // Checking the ETag, only if the token was already deemed |
|
| 1467 | + // valid and there is one. |
|
| 1468 | + if ($token['etag'] && $tokenValid) { |
|
| 1469 | + // The token was valid, and there was an ETag. We must |
|
| 1470 | + // grab the current ETag and check it. |
|
| 1471 | + $node = $this->tree->getNodeForPath($uri); |
|
| 1472 | + $etagValid = $node instanceof IFile && $node->getETag() == $token['etag']; |
|
| 1473 | + } |
|
| 1474 | + |
|
| 1475 | + if (($tokenValid && $etagValid) ^ $token['negate']) { |
|
| 1476 | + // Both were valid, so we can go to the next condition. |
|
| 1477 | + continue 2; |
|
| 1478 | + } |
|
| 1479 | + } |
|
| 1480 | + |
|
| 1481 | + // If we ended here, it means there was no valid ETag + token |
|
| 1482 | + // combination found for the current condition. This means we fail! |
|
| 1483 | + throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for '.$uri, 'If'); |
|
| 1484 | + } |
|
| 1485 | + |
|
| 1486 | + return true; |
|
| 1487 | + } |
|
| 1488 | + |
|
| 1489 | + /** |
|
| 1490 | + * This method is created to extract information from the WebDAV HTTP 'If:' header. |
|
| 1491 | + * |
|
| 1492 | + * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information |
|
| 1493 | + * The function will return an array, containing structs with the following keys |
|
| 1494 | + * |
|
| 1495 | + * * uri - the uri the condition applies to. |
|
| 1496 | + * * tokens - The lock token. another 2 dimensional array containing 3 elements |
|
| 1497 | + * |
|
| 1498 | + * Example 1: |
|
| 1499 | + * |
|
| 1500 | + * If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>) |
|
| 1501 | + * |
|
| 1502 | + * Would result in: |
|
| 1503 | + * |
|
| 1504 | + * [ |
|
| 1505 | + * [ |
|
| 1506 | + * 'uri' => '/request/uri', |
|
| 1507 | + * 'tokens' => [ |
|
| 1508 | + * [ |
|
| 1509 | + * [ |
|
| 1510 | + * 'negate' => false, |
|
| 1511 | + * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', |
|
| 1512 | + * 'etag' => "" |
|
| 1513 | + * ] |
|
| 1514 | + * ] |
|
| 1515 | + * ], |
|
| 1516 | + * ] |
|
| 1517 | + * ] |
|
| 1518 | + * |
|
| 1519 | + * Example 2: |
|
| 1520 | + * |
|
| 1521 | + * If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"]) |
|
| 1522 | + * |
|
| 1523 | + * Would result in: |
|
| 1524 | + * |
|
| 1525 | + * [ |
|
| 1526 | + * [ |
|
| 1527 | + * 'uri' => 'path', |
|
| 1528 | + * 'tokens' => [ |
|
| 1529 | + * [ |
|
| 1530 | + * [ |
|
| 1531 | + * 'negate' => true, |
|
| 1532 | + * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', |
|
| 1533 | + * 'etag' => '"Im An ETag"' |
|
| 1534 | + * ], |
|
| 1535 | + * [ |
|
| 1536 | + * 'negate' => false, |
|
| 1537 | + * 'token' => '', |
|
| 1538 | + * 'etag' => '"Another ETag"' |
|
| 1539 | + * ] |
|
| 1540 | + * ] |
|
| 1541 | + * ], |
|
| 1542 | + * ], |
|
| 1543 | + * [ |
|
| 1544 | + * 'uri' => 'path2', |
|
| 1545 | + * 'tokens' => [ |
|
| 1546 | + * [ |
|
| 1547 | + * [ |
|
| 1548 | + * 'negate' => true, |
|
| 1549 | + * 'token' => '', |
|
| 1550 | + * 'etag' => '"Path2 ETag"' |
|
| 1551 | + * ] |
|
| 1552 | + * ] |
|
| 1553 | + * ], |
|
| 1554 | + * ], |
|
| 1555 | + * ] |
|
| 1556 | + * |
|
| 1557 | + * @return array |
|
| 1558 | + */ |
|
| 1559 | + public function getIfConditions(RequestInterface $request) |
|
| 1560 | + { |
|
| 1561 | + $header = $request->getHeader('If'); |
|
| 1562 | + if (!$header) { |
|
| 1563 | + return []; |
|
| 1564 | + } |
|
| 1565 | + |
|
| 1566 | + $matches = []; |
|
| 1567 | + |
|
| 1568 | + $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im'; |
|
| 1569 | + preg_match_all($regex, $header, $matches, PREG_SET_ORDER); |
|
| 1570 | + |
|
| 1571 | + $conditions = []; |
|
| 1572 | + |
|
| 1573 | + foreach ($matches as $match) { |
|
| 1574 | + // If there was no uri specified in this match, and there were |
|
| 1575 | + // already conditions parsed, we add the condition to the list of |
|
| 1576 | + // conditions for the previous uri. |
|
| 1577 | + if (!$match['uri'] && count($conditions)) { |
|
| 1578 | + $conditions[count($conditions) - 1]['tokens'][] = [ |
|
| 1579 | + 'negate' => $match['not'] ? true : false, |
|
| 1580 | + 'token' => $match['token'], |
|
| 1581 | + 'etag' => isset($match['etag']) ? $match['etag'] : '', |
|
| 1582 | + ]; |
|
| 1583 | + } else { |
|
| 1584 | + if (!$match['uri']) { |
|
| 1585 | + $realUri = $request->getPath(); |
|
| 1586 | + } else { |
|
| 1587 | + $realUri = $this->calculateUri($match['uri']); |
|
| 1588 | + } |
|
| 1589 | + |
|
| 1590 | + $conditions[] = [ |
|
| 1591 | + 'uri' => $realUri, |
|
| 1592 | + 'tokens' => [ |
|
| 1593 | + [ |
|
| 1594 | + 'negate' => $match['not'] ? true : false, |
|
| 1595 | + 'token' => $match['token'], |
|
| 1596 | + 'etag' => isset($match['etag']) ? $match['etag'] : '', |
|
| 1597 | + ], |
|
| 1598 | + ], |
|
| 1599 | + ]; |
|
| 1600 | + } |
|
| 1601 | + } |
|
| 1602 | + |
|
| 1603 | + return $conditions; |
|
| 1604 | + } |
|
| 1605 | + |
|
| 1606 | + /** |
|
| 1607 | + * Returns an array with resourcetypes for a node. |
|
| 1608 | + * |
|
| 1609 | + * @return array |
|
| 1610 | + */ |
|
| 1611 | + public function getResourceTypeForNode(INode $node) |
|
| 1612 | + { |
|
| 1613 | + $result = []; |
|
| 1614 | + foreach ($this->resourceTypeMapping as $className => $resourceType) { |
|
| 1615 | + if ($node instanceof $className) { |
|
| 1616 | + $result[] = $resourceType; |
|
| 1617 | + } |
|
| 1618 | + } |
|
| 1619 | + |
|
| 1620 | + return $result; |
|
| 1621 | + } |
|
| 1622 | + |
|
| 1623 | + // }}} |
|
| 1624 | + // {{{ XML Readers & Writers |
|
| 1625 | + |
|
| 1626 | + /** |
|
| 1627 | + * Returns a callback generating a WebDAV propfind response body based on a list of nodes. |
|
| 1628 | + * |
|
| 1629 | + * If 'strip404s' is set to true, all 404 responses will be removed. |
|
| 1630 | + * |
|
| 1631 | + * @param array|\Traversable $fileProperties The list with nodes |
|
| 1632 | + * @param bool $strip404s |
|
| 1633 | + * |
|
| 1634 | + * @return callable|string |
|
| 1635 | + */ |
|
| 1636 | + public function generateMultiStatus($fileProperties, $strip404s = false) |
|
| 1637 | + { |
|
| 1638 | + $w = $this->xml->getWriter(); |
|
| 1639 | + if (self::$streamMultiStatus) { |
|
| 1640 | + return function () use ($fileProperties, $strip404s, $w) { |
|
| 1641 | + $w->openUri('php://output'); |
|
| 1642 | + $this->writeMultiStatus($w, $fileProperties, $strip404s); |
|
| 1643 | + $w->flush(); |
|
| 1644 | + }; |
|
| 1645 | + } |
|
| 1646 | + $w->openMemory(); |
|
| 1647 | + $this->writeMultiStatus($w, $fileProperties, $strip404s); |
|
| 1648 | + |
|
| 1649 | + return $w->outputMemory(); |
|
| 1650 | + } |
|
| 1651 | + |
|
| 1652 | + /** |
|
| 1653 | + * @param $fileProperties |
|
| 1654 | + */ |
|
| 1655 | + private function writeMultiStatus(Writer $w, $fileProperties, bool $strip404s) |
|
| 1656 | + { |
|
| 1657 | + $w->contextUri = $this->baseUri; |
|
| 1658 | + $w->startDocument(); |
|
| 1659 | + |
|
| 1660 | + $w->startElement('{DAV:}multistatus'); |
|
| 1661 | + |
|
| 1662 | + foreach ($fileProperties as $entry) { |
|
| 1663 | + $href = $entry['href']; |
|
| 1664 | + unset($entry['href']); |
|
| 1665 | + if ($strip404s) { |
|
| 1666 | + unset($entry[404]); |
|
| 1667 | + } |
|
| 1668 | + $response = new Xml\Element\Response( |
|
| 1669 | + ltrim($href, '/'), |
|
| 1670 | + $entry |
|
| 1671 | + ); |
|
| 1672 | + $w->write([ |
|
| 1673 | + 'name' => '{DAV:}response', |
|
| 1674 | + 'value' => $response, |
|
| 1675 | + ]); |
|
| 1676 | + } |
|
| 1677 | + $w->endElement(); |
|
| 1678 | + $w->endDocument(); |
|
| 1679 | + } |
|
| 1680 | 1680 | } |
@@ -18,433 +18,433 @@ discard block |
||
| 18 | 18 | */ |
| 19 | 19 | class CorePlugin extends ServerPlugin |
| 20 | 20 | { |
| 21 | - /** |
|
| 22 | - * Reference to server object. |
|
| 23 | - * |
|
| 24 | - * @var Server |
|
| 25 | - */ |
|
| 26 | - protected $server; |
|
| 27 | - |
|
| 28 | - /** |
|
| 29 | - * Sets up the plugin. |
|
| 30 | - */ |
|
| 31 | - public function initialize(Server $server) |
|
| 32 | - { |
|
| 33 | - $this->server = $server; |
|
| 34 | - $server->on('method:GET', [$this, 'httpGet']); |
|
| 35 | - $server->on('method:OPTIONS', [$this, 'httpOptions']); |
|
| 36 | - $server->on('method:HEAD', [$this, 'httpHead']); |
|
| 37 | - $server->on('method:DELETE', [$this, 'httpDelete']); |
|
| 38 | - $server->on('method:PROPFIND', [$this, 'httpPropFind']); |
|
| 39 | - $server->on('method:PROPPATCH', [$this, 'httpPropPatch']); |
|
| 40 | - $server->on('method:PUT', [$this, 'httpPut']); |
|
| 41 | - $server->on('method:MKCOL', [$this, 'httpMkcol']); |
|
| 42 | - $server->on('method:MOVE', [$this, 'httpMove']); |
|
| 43 | - $server->on('method:COPY', [$this, 'httpCopy']); |
|
| 44 | - $server->on('method:REPORT', [$this, 'httpReport']); |
|
| 45 | - |
|
| 46 | - $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90); |
|
| 47 | - $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200); |
|
| 48 | - $server->on('propFind', [$this, 'propFind']); |
|
| 49 | - $server->on('propFind', [$this, 'propFindNode'], 120); |
|
| 50 | - $server->on('propFind', [$this, 'propFindLate'], 200); |
|
| 51 | - |
|
| 52 | - $server->on('exception', [$this, 'exception']); |
|
| 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 | - * This is the default implementation for the GET method. |
|
| 70 | - * |
|
| 71 | - * @return bool |
|
| 72 | - */ |
|
| 73 | - public function httpGet(RequestInterface $request, ResponseInterface $response) |
|
| 74 | - { |
|
| 75 | - $path = $request->getPath(); |
|
| 76 | - $node = $this->server->tree->getNodeForPath($path); |
|
| 77 | - |
|
| 78 | - if (!$node instanceof IFile) { |
|
| 79 | - return; |
|
| 80 | - } |
|
| 81 | - |
|
| 82 | - if ('HEAD' === $request->getHeader('X-Sabre-Original-Method')) { |
|
| 83 | - $body = ''; |
|
| 84 | - } else { |
|
| 85 | - $body = $node->get(); |
|
| 86 | - |
|
| 87 | - // Converting string into stream, if needed. |
|
| 88 | - if (is_string($body)) { |
|
| 89 | - $stream = fopen('php://temp', 'r+'); |
|
| 90 | - fwrite($stream, $body); |
|
| 91 | - rewind($stream); |
|
| 92 | - $body = $stream; |
|
| 93 | - } |
|
| 94 | - } |
|
| 95 | - |
|
| 96 | - /* |
|
| 21 | + /** |
|
| 22 | + * Reference to server object. |
|
| 23 | + * |
|
| 24 | + * @var Server |
|
| 25 | + */ |
|
| 26 | + protected $server; |
|
| 27 | + |
|
| 28 | + /** |
|
| 29 | + * Sets up the plugin. |
|
| 30 | + */ |
|
| 31 | + public function initialize(Server $server) |
|
| 32 | + { |
|
| 33 | + $this->server = $server; |
|
| 34 | + $server->on('method:GET', [$this, 'httpGet']); |
|
| 35 | + $server->on('method:OPTIONS', [$this, 'httpOptions']); |
|
| 36 | + $server->on('method:HEAD', [$this, 'httpHead']); |
|
| 37 | + $server->on('method:DELETE', [$this, 'httpDelete']); |
|
| 38 | + $server->on('method:PROPFIND', [$this, 'httpPropFind']); |
|
| 39 | + $server->on('method:PROPPATCH', [$this, 'httpPropPatch']); |
|
| 40 | + $server->on('method:PUT', [$this, 'httpPut']); |
|
| 41 | + $server->on('method:MKCOL', [$this, 'httpMkcol']); |
|
| 42 | + $server->on('method:MOVE', [$this, 'httpMove']); |
|
| 43 | + $server->on('method:COPY', [$this, 'httpCopy']); |
|
| 44 | + $server->on('method:REPORT', [$this, 'httpReport']); |
|
| 45 | + |
|
| 46 | + $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90); |
|
| 47 | + $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200); |
|
| 48 | + $server->on('propFind', [$this, 'propFind']); |
|
| 49 | + $server->on('propFind', [$this, 'propFindNode'], 120); |
|
| 50 | + $server->on('propFind', [$this, 'propFindLate'], 200); |
|
| 51 | + |
|
| 52 | + $server->on('exception', [$this, 'exception']); |
|
| 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 | + * This is the default implementation for the GET method. |
|
| 70 | + * |
|
| 71 | + * @return bool |
|
| 72 | + */ |
|
| 73 | + public function httpGet(RequestInterface $request, ResponseInterface $response) |
|
| 74 | + { |
|
| 75 | + $path = $request->getPath(); |
|
| 76 | + $node = $this->server->tree->getNodeForPath($path); |
|
| 77 | + |
|
| 78 | + if (!$node instanceof IFile) { |
|
| 79 | + return; |
|
| 80 | + } |
|
| 81 | + |
|
| 82 | + if ('HEAD' === $request->getHeader('X-Sabre-Original-Method')) { |
|
| 83 | + $body = ''; |
|
| 84 | + } else { |
|
| 85 | + $body = $node->get(); |
|
| 86 | + |
|
| 87 | + // Converting string into stream, if needed. |
|
| 88 | + if (is_string($body)) { |
|
| 89 | + $stream = fopen('php://temp', 'r+'); |
|
| 90 | + fwrite($stream, $body); |
|
| 91 | + rewind($stream); |
|
| 92 | + $body = $stream; |
|
| 93 | + } |
|
| 94 | + } |
|
| 95 | + |
|
| 96 | + /* |
|
| 97 | 97 | * TODO: getetag, getlastmodified, getsize should also be used using |
| 98 | 98 | * this method |
| 99 | 99 | */ |
| 100 | - $httpHeaders = $this->server->getHTTPHeaders($path); |
|
| 100 | + $httpHeaders = $this->server->getHTTPHeaders($path); |
|
| 101 | 101 | |
| 102 | - /* ContentType needs to get a default, because many webservers will otherwise |
|
| 102 | + /* ContentType needs to get a default, because many webservers will otherwise |
|
| 103 | 103 | * default to text/html, and we don't want this for security reasons. |
| 104 | 104 | */ |
| 105 | - if (!isset($httpHeaders['Content-Type'])) { |
|
| 106 | - $httpHeaders['Content-Type'] = 'application/octet-stream'; |
|
| 107 | - } |
|
| 108 | - |
|
| 109 | - if (isset($httpHeaders['Content-Length'])) { |
|
| 110 | - $nodeSize = $httpHeaders['Content-Length']; |
|
| 111 | - |
|
| 112 | - // Need to unset Content-Length, because we'll handle that during figuring out the range |
|
| 113 | - unset($httpHeaders['Content-Length']); |
|
| 114 | - } else { |
|
| 115 | - $nodeSize = null; |
|
| 116 | - } |
|
| 117 | - |
|
| 118 | - $response->addHeaders($httpHeaders); |
|
| 119 | - |
|
| 120 | - $range = $this->server->getHTTPRange(); |
|
| 121 | - $ifRange = $request->getHeader('If-Range'); |
|
| 122 | - $ignoreRangeHeader = false; |
|
| 123 | - |
|
| 124 | - // If ifRange is set, and range is specified, we first need to check |
|
| 125 | - // the precondition. |
|
| 126 | - if ($nodeSize && $range && $ifRange) { |
|
| 127 | - // if IfRange is parsable as a date we'll treat it as a DateTime |
|
| 128 | - // otherwise, we must treat it as an etag. |
|
| 129 | - try { |
|
| 130 | - $ifRangeDate = new \DateTime($ifRange); |
|
| 131 | - |
|
| 132 | - // It's a date. We must check if the entity is modified since |
|
| 133 | - // the specified date. |
|
| 134 | - if (!isset($httpHeaders['Last-Modified'])) { |
|
| 135 | - $ignoreRangeHeader = true; |
|
| 136 | - } else { |
|
| 137 | - $modified = new \DateTime($httpHeaders['Last-Modified']); |
|
| 138 | - if ($modified > $ifRangeDate) { |
|
| 139 | - $ignoreRangeHeader = true; |
|
| 140 | - } |
|
| 141 | - } |
|
| 142 | - } catch (\Exception $e) { |
|
| 143 | - // It's an entity. We can do a simple comparison. |
|
| 144 | - if (!isset($httpHeaders['ETag'])) { |
|
| 145 | - $ignoreRangeHeader = true; |
|
| 146 | - } elseif ($httpHeaders['ETag'] !== $ifRange) { |
|
| 147 | - $ignoreRangeHeader = true; |
|
| 148 | - } |
|
| 149 | - } |
|
| 150 | - } |
|
| 151 | - |
|
| 152 | - // We're only going to support HTTP ranges if the backend provided a filesize |
|
| 153 | - if (!$ignoreRangeHeader && $nodeSize && $range) { |
|
| 154 | - // Determining the exact byte offsets |
|
| 155 | - if (!is_null($range[0])) { |
|
| 156 | - $start = $range[0]; |
|
| 157 | - $end = $range[1] ? $range[1] : $nodeSize - 1; |
|
| 158 | - if ($start >= $nodeSize) { |
|
| 159 | - throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$range[0].') exceeded the size of the entity ('.$nodeSize.')'); |
|
| 160 | - } |
|
| 161 | - if ($end < $start) { |
|
| 162 | - throw new Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[1].') is lower than the start offset ('.$range[0].')'); |
|
| 163 | - } |
|
| 164 | - if ($end >= $nodeSize) { |
|
| 165 | - $end = $nodeSize - 1; |
|
| 166 | - } |
|
| 167 | - } else { |
|
| 168 | - $start = $nodeSize - $range[1]; |
|
| 169 | - $end = $nodeSize - 1; |
|
| 170 | - |
|
| 171 | - if ($start < 0) { |
|
| 172 | - $start = 0; |
|
| 173 | - } |
|
| 174 | - } |
|
| 175 | - |
|
| 176 | - // Streams may advertise themselves as seekable, but still not |
|
| 177 | - // actually allow fseek. We'll manually go forward in the stream |
|
| 178 | - // if fseek failed. |
|
| 179 | - if (!stream_get_meta_data($body)['seekable'] || -1 === fseek($body, $start, SEEK_SET)) { |
|
| 180 | - $consumeBlock = 8192; |
|
| 181 | - for ($consumed = 0; $start - $consumed > 0;) { |
|
| 182 | - if (feof($body)) { |
|
| 183 | - throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$start.') exceeded the size of the entity ('.$consumed.')'); |
|
| 184 | - } |
|
| 185 | - $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock))); |
|
| 186 | - } |
|
| 187 | - } |
|
| 188 | - |
|
| 189 | - $response->setHeader('Content-Length', $end - $start + 1); |
|
| 190 | - $response->setHeader('Content-Range', 'bytes '.$start.'-'.$end.'/'.$nodeSize); |
|
| 191 | - $response->setStatus(206); |
|
| 192 | - $response->setBody($body); |
|
| 193 | - } else { |
|
| 194 | - if ($nodeSize) { |
|
| 195 | - $response->setHeader('Content-Length', $nodeSize); |
|
| 196 | - } |
|
| 197 | - $response->setStatus(200); |
|
| 198 | - $response->setBody($body); |
|
| 199 | - } |
|
| 200 | - // Sending back false will interrupt the event chain and tell the server |
|
| 201 | - // we've handled this method. |
|
| 202 | - return false; |
|
| 203 | - } |
|
| 204 | - |
|
| 205 | - /** |
|
| 206 | - * HTTP OPTIONS. |
|
| 207 | - * |
|
| 208 | - * @return bool |
|
| 209 | - */ |
|
| 210 | - public function httpOptions(RequestInterface $request, ResponseInterface $response) |
|
| 211 | - { |
|
| 212 | - $methods = $this->server->getAllowedMethods($request->getPath()); |
|
| 213 | - |
|
| 214 | - $response->setHeader('Allow', strtoupper(implode(', ', $methods))); |
|
| 215 | - $features = ['1', '3', 'extended-mkcol']; |
|
| 216 | - |
|
| 217 | - foreach ($this->server->getPlugins() as $plugin) { |
|
| 218 | - $features = array_merge($features, $plugin->getFeatures()); |
|
| 219 | - } |
|
| 220 | - |
|
| 221 | - $response->setHeader('DAV', implode(', ', $features)); |
|
| 222 | - $response->setHeader('MS-Author-Via', 'DAV'); |
|
| 223 | - $response->setHeader('Accept-Ranges', 'bytes'); |
|
| 224 | - $response->setHeader('Content-Length', '0'); |
|
| 225 | - $response->setStatus(200); |
|
| 226 | - |
|
| 227 | - // Sending back false will interrupt the event chain and tell the server |
|
| 228 | - // we've handled this method. |
|
| 229 | - return false; |
|
| 230 | - } |
|
| 231 | - |
|
| 232 | - /** |
|
| 233 | - * HTTP HEAD. |
|
| 234 | - * |
|
| 235 | - * This method is normally used to take a peak at a url, and only get the |
|
| 236 | - * HTTP response headers, without the body. This is used by clients to |
|
| 237 | - * determine if a remote file was changed, so they can use a local cached |
|
| 238 | - * version, instead of downloading it again |
|
| 239 | - * |
|
| 240 | - * @return bool |
|
| 241 | - */ |
|
| 242 | - public function httpHead(RequestInterface $request, ResponseInterface $response) |
|
| 243 | - { |
|
| 244 | - // This is implemented by changing the HEAD request to a GET request, |
|
| 245 | - // and telling the request handler that is doesn't need to create the body. |
|
| 246 | - $subRequest = clone $request; |
|
| 247 | - $subRequest->setMethod('GET'); |
|
| 248 | - $subRequest->setHeader('X-Sabre-Original-Method', 'HEAD'); |
|
| 249 | - |
|
| 250 | - try { |
|
| 251 | - $this->server->invokeMethod($subRequest, $response, false); |
|
| 252 | - } catch (Exception\NotImplemented $e) { |
|
| 253 | - // Some clients may do HEAD requests on collections, however, GET |
|
| 254 | - // requests and HEAD requests _may_ not be defined on a collection, |
|
| 255 | - // which would trigger a 501. |
|
| 256 | - // This breaks some clients though, so we're transforming these |
|
| 257 | - // 501s into 200s. |
|
| 258 | - $response->setStatus(200); |
|
| 259 | - $response->setBody(''); |
|
| 260 | - $response->setHeader('Content-Type', 'text/plain'); |
|
| 261 | - $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode()); |
|
| 262 | - } |
|
| 263 | - |
|
| 264 | - // Sending back false will interrupt the event chain and tell the server |
|
| 265 | - // we've handled this method. |
|
| 266 | - return false; |
|
| 267 | - } |
|
| 268 | - |
|
| 269 | - /** |
|
| 270 | - * HTTP Delete. |
|
| 271 | - * |
|
| 272 | - * The HTTP delete method, deletes a given uri |
|
| 273 | - */ |
|
| 274 | - public function httpDelete(RequestInterface $request, ResponseInterface $response) |
|
| 275 | - { |
|
| 276 | - $path = $request->getPath(); |
|
| 277 | - |
|
| 278 | - if (!$this->server->emit('beforeUnbind', [$path])) { |
|
| 279 | - return false; |
|
| 280 | - } |
|
| 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 interrupt the event chain and tell the server |
|
| 288 | - // we've handled this method. |
|
| 289 | - return false; |
|
| 290 | - } |
|
| 291 | - |
|
| 292 | - /** |
|
| 293 | - * WebDAV PROPFIND. |
|
| 294 | - * |
|
| 295 | - * This WebDAV method requests information about an uri resource, or a list of resources |
|
| 296 | - * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value |
|
| 297 | - * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory) |
|
| 298 | - * |
|
| 299 | - * The request body contains an XML data structure that has a list of properties the client understands |
|
| 300 | - * The response body is also an xml document, containing information about every uri resource and the requested properties |
|
| 301 | - * |
|
| 302 | - * It has to return a HTTP 207 Multi-status status code |
|
| 303 | - */ |
|
| 304 | - public function httpPropFind(RequestInterface $request, ResponseInterface $response) |
|
| 305 | - { |
|
| 306 | - $path = $request->getPath(); |
|
| 307 | - |
|
| 308 | - $requestBody = $request->getBodyAsString(); |
|
| 309 | - if (strlen($requestBody)) { |
|
| 310 | - try { |
|
| 311 | - $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody); |
|
| 312 | - } catch (ParseException $e) { |
|
| 313 | - throw new BadRequest($e->getMessage(), 0, $e); |
|
| 314 | - } |
|
| 315 | - } else { |
|
| 316 | - $propFindXml = new Xml\Request\PropFind(); |
|
| 317 | - $propFindXml->allProp = true; |
|
| 318 | - $propFindXml->properties = []; |
|
| 319 | - } |
|
| 320 | - |
|
| 321 | - $depth = $this->server->getHTTPDepth(1); |
|
| 322 | - // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled |
|
| 323 | - if (!$this->server->enablePropfindDepthInfinity && 0 != $depth) { |
|
| 324 | - $depth = 1; |
|
| 325 | - } |
|
| 326 | - |
|
| 327 | - $newProperties = $this->server->getPropertiesIteratorForPath($path, $propFindXml->properties, $depth); |
|
| 328 | - |
|
| 329 | - // This is a multi-status response |
|
| 330 | - $response->setStatus(207); |
|
| 331 | - $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 332 | - $response->setHeader('Vary', 'Brief,Prefer'); |
|
| 333 | - |
|
| 334 | - // Normally this header is only needed for OPTIONS responses, however.. |
|
| 335 | - // iCal seems to also depend on these being set for PROPFIND. Since |
|
| 336 | - // this is not harmful, we'll add it. |
|
| 337 | - $features = ['1', '3', 'extended-mkcol']; |
|
| 338 | - foreach ($this->server->getPlugins() as $plugin) { |
|
| 339 | - $features = array_merge($features, $plugin->getFeatures()); |
|
| 340 | - } |
|
| 341 | - $response->setHeader('DAV', implode(', ', $features)); |
|
| 342 | - |
|
| 343 | - $prefer = $this->server->getHTTPPrefer(); |
|
| 344 | - $minimal = 'minimal' === $prefer['return']; |
|
| 345 | - |
|
| 346 | - $data = $this->server->generateMultiStatus($newProperties, $minimal); |
|
| 347 | - $response->setBody($data); |
|
| 348 | - |
|
| 349 | - // Sending back false will interrupt the event chain and tell the server |
|
| 350 | - // we've handled this method. |
|
| 351 | - return false; |
|
| 352 | - } |
|
| 353 | - |
|
| 354 | - /** |
|
| 355 | - * WebDAV PROPPATCH. |
|
| 356 | - * |
|
| 357 | - * This method is called to update properties on a Node. The request is an XML body with all the mutations. |
|
| 358 | - * In this XML body it is specified which properties should be set/updated and/or deleted |
|
| 359 | - * |
|
| 360 | - * @return bool |
|
| 361 | - */ |
|
| 362 | - public function httpPropPatch(RequestInterface $request, ResponseInterface $response) |
|
| 363 | - { |
|
| 364 | - $path = $request->getPath(); |
|
| 365 | - |
|
| 366 | - try { |
|
| 367 | - $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody()); |
|
| 368 | - } catch (ParseException $e) { |
|
| 369 | - throw new BadRequest($e->getMessage(), 0, $e); |
|
| 370 | - } |
|
| 371 | - $newProperties = $propPatch->properties; |
|
| 372 | - |
|
| 373 | - $result = $this->server->updateProperties($path, $newProperties); |
|
| 374 | - |
|
| 375 | - $prefer = $this->server->getHTTPPrefer(); |
|
| 376 | - $response->setHeader('Vary', 'Brief,Prefer'); |
|
| 377 | - |
|
| 378 | - if ('minimal' === $prefer['return']) { |
|
| 379 | - // If return-minimal is specified, we only have to check if the |
|
| 380 | - // request was successful, and don't need to return the |
|
| 381 | - // multi-status. |
|
| 382 | - $ok = true; |
|
| 383 | - foreach ($result as $prop => $code) { |
|
| 384 | - if ((int) $code > 299) { |
|
| 385 | - $ok = false; |
|
| 386 | - } |
|
| 387 | - } |
|
| 388 | - |
|
| 389 | - if ($ok) { |
|
| 390 | - $response->setStatus(204); |
|
| 391 | - |
|
| 392 | - return false; |
|
| 393 | - } |
|
| 394 | - } |
|
| 395 | - |
|
| 396 | - $response->setStatus(207); |
|
| 397 | - $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 398 | - |
|
| 399 | - // Reorganizing the result for generateMultiStatus |
|
| 400 | - $multiStatus = []; |
|
| 401 | - foreach ($result as $propertyName => $code) { |
|
| 402 | - if (isset($multiStatus[$code])) { |
|
| 403 | - $multiStatus[$code][$propertyName] = null; |
|
| 404 | - } else { |
|
| 405 | - $multiStatus[$code] = [$propertyName => null]; |
|
| 406 | - } |
|
| 407 | - } |
|
| 408 | - $multiStatus['href'] = $path; |
|
| 409 | - |
|
| 410 | - $response->setBody( |
|
| 411 | - $this->server->generateMultiStatus([$multiStatus]) |
|
| 412 | - ); |
|
| 413 | - |
|
| 414 | - // Sending back false will interrupt the event chain and tell the server |
|
| 415 | - // we've handled this method. |
|
| 416 | - return false; |
|
| 417 | - } |
|
| 418 | - |
|
| 419 | - /** |
|
| 420 | - * HTTP PUT method. |
|
| 421 | - * |
|
| 422 | - * This HTTP method updates a file, or creates a new one. |
|
| 423 | - * |
|
| 424 | - * 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 |
|
| 425 | - * |
|
| 426 | - * @return bool |
|
| 427 | - */ |
|
| 428 | - public function httpPut(RequestInterface $request, ResponseInterface $response) |
|
| 429 | - { |
|
| 430 | - $body = $request->getBodyAsStream(); |
|
| 431 | - $path = $request->getPath(); |
|
| 432 | - |
|
| 433 | - // Intercepting Content-Range |
|
| 434 | - if ($request->getHeader('Content-Range')) { |
|
| 435 | - /* |
|
| 105 | + if (!isset($httpHeaders['Content-Type'])) { |
|
| 106 | + $httpHeaders['Content-Type'] = 'application/octet-stream'; |
|
| 107 | + } |
|
| 108 | + |
|
| 109 | + if (isset($httpHeaders['Content-Length'])) { |
|
| 110 | + $nodeSize = $httpHeaders['Content-Length']; |
|
| 111 | + |
|
| 112 | + // Need to unset Content-Length, because we'll handle that during figuring out the range |
|
| 113 | + unset($httpHeaders['Content-Length']); |
|
| 114 | + } else { |
|
| 115 | + $nodeSize = null; |
|
| 116 | + } |
|
| 117 | + |
|
| 118 | + $response->addHeaders($httpHeaders); |
|
| 119 | + |
|
| 120 | + $range = $this->server->getHTTPRange(); |
|
| 121 | + $ifRange = $request->getHeader('If-Range'); |
|
| 122 | + $ignoreRangeHeader = false; |
|
| 123 | + |
|
| 124 | + // If ifRange is set, and range is specified, we first need to check |
|
| 125 | + // the precondition. |
|
| 126 | + if ($nodeSize && $range && $ifRange) { |
|
| 127 | + // if IfRange is parsable as a date we'll treat it as a DateTime |
|
| 128 | + // otherwise, we must treat it as an etag. |
|
| 129 | + try { |
|
| 130 | + $ifRangeDate = new \DateTime($ifRange); |
|
| 131 | + |
|
| 132 | + // It's a date. We must check if the entity is modified since |
|
| 133 | + // the specified date. |
|
| 134 | + if (!isset($httpHeaders['Last-Modified'])) { |
|
| 135 | + $ignoreRangeHeader = true; |
|
| 136 | + } else { |
|
| 137 | + $modified = new \DateTime($httpHeaders['Last-Modified']); |
|
| 138 | + if ($modified > $ifRangeDate) { |
|
| 139 | + $ignoreRangeHeader = true; |
|
| 140 | + } |
|
| 141 | + } |
|
| 142 | + } catch (\Exception $e) { |
|
| 143 | + // It's an entity. We can do a simple comparison. |
|
| 144 | + if (!isset($httpHeaders['ETag'])) { |
|
| 145 | + $ignoreRangeHeader = true; |
|
| 146 | + } elseif ($httpHeaders['ETag'] !== $ifRange) { |
|
| 147 | + $ignoreRangeHeader = true; |
|
| 148 | + } |
|
| 149 | + } |
|
| 150 | + } |
|
| 151 | + |
|
| 152 | + // We're only going to support HTTP ranges if the backend provided a filesize |
|
| 153 | + if (!$ignoreRangeHeader && $nodeSize && $range) { |
|
| 154 | + // Determining the exact byte offsets |
|
| 155 | + if (!is_null($range[0])) { |
|
| 156 | + $start = $range[0]; |
|
| 157 | + $end = $range[1] ? $range[1] : $nodeSize - 1; |
|
| 158 | + if ($start >= $nodeSize) { |
|
| 159 | + throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$range[0].') exceeded the size of the entity ('.$nodeSize.')'); |
|
| 160 | + } |
|
| 161 | + if ($end < $start) { |
|
| 162 | + throw new Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[1].') is lower than the start offset ('.$range[0].')'); |
|
| 163 | + } |
|
| 164 | + if ($end >= $nodeSize) { |
|
| 165 | + $end = $nodeSize - 1; |
|
| 166 | + } |
|
| 167 | + } else { |
|
| 168 | + $start = $nodeSize - $range[1]; |
|
| 169 | + $end = $nodeSize - 1; |
|
| 170 | + |
|
| 171 | + if ($start < 0) { |
|
| 172 | + $start = 0; |
|
| 173 | + } |
|
| 174 | + } |
|
| 175 | + |
|
| 176 | + // Streams may advertise themselves as seekable, but still not |
|
| 177 | + // actually allow fseek. We'll manually go forward in the stream |
|
| 178 | + // if fseek failed. |
|
| 179 | + if (!stream_get_meta_data($body)['seekable'] || -1 === fseek($body, $start, SEEK_SET)) { |
|
| 180 | + $consumeBlock = 8192; |
|
| 181 | + for ($consumed = 0; $start - $consumed > 0;) { |
|
| 182 | + if (feof($body)) { |
|
| 183 | + throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$start.') exceeded the size of the entity ('.$consumed.')'); |
|
| 184 | + } |
|
| 185 | + $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock))); |
|
| 186 | + } |
|
| 187 | + } |
|
| 188 | + |
|
| 189 | + $response->setHeader('Content-Length', $end - $start + 1); |
|
| 190 | + $response->setHeader('Content-Range', 'bytes '.$start.'-'.$end.'/'.$nodeSize); |
|
| 191 | + $response->setStatus(206); |
|
| 192 | + $response->setBody($body); |
|
| 193 | + } else { |
|
| 194 | + if ($nodeSize) { |
|
| 195 | + $response->setHeader('Content-Length', $nodeSize); |
|
| 196 | + } |
|
| 197 | + $response->setStatus(200); |
|
| 198 | + $response->setBody($body); |
|
| 199 | + } |
|
| 200 | + // Sending back false will interrupt the event chain and tell the server |
|
| 201 | + // we've handled this method. |
|
| 202 | + return false; |
|
| 203 | + } |
|
| 204 | + |
|
| 205 | + /** |
|
| 206 | + * HTTP OPTIONS. |
|
| 207 | + * |
|
| 208 | + * @return bool |
|
| 209 | + */ |
|
| 210 | + public function httpOptions(RequestInterface $request, ResponseInterface $response) |
|
| 211 | + { |
|
| 212 | + $methods = $this->server->getAllowedMethods($request->getPath()); |
|
| 213 | + |
|
| 214 | + $response->setHeader('Allow', strtoupper(implode(', ', $methods))); |
|
| 215 | + $features = ['1', '3', 'extended-mkcol']; |
|
| 216 | + |
|
| 217 | + foreach ($this->server->getPlugins() as $plugin) { |
|
| 218 | + $features = array_merge($features, $plugin->getFeatures()); |
|
| 219 | + } |
|
| 220 | + |
|
| 221 | + $response->setHeader('DAV', implode(', ', $features)); |
|
| 222 | + $response->setHeader('MS-Author-Via', 'DAV'); |
|
| 223 | + $response->setHeader('Accept-Ranges', 'bytes'); |
|
| 224 | + $response->setHeader('Content-Length', '0'); |
|
| 225 | + $response->setStatus(200); |
|
| 226 | + |
|
| 227 | + // Sending back false will interrupt the event chain and tell the server |
|
| 228 | + // we've handled this method. |
|
| 229 | + return false; |
|
| 230 | + } |
|
| 231 | + |
|
| 232 | + /** |
|
| 233 | + * HTTP HEAD. |
|
| 234 | + * |
|
| 235 | + * This method is normally used to take a peak at a url, and only get the |
|
| 236 | + * HTTP response headers, without the body. This is used by clients to |
|
| 237 | + * determine if a remote file was changed, so they can use a local cached |
|
| 238 | + * version, instead of downloading it again |
|
| 239 | + * |
|
| 240 | + * @return bool |
|
| 241 | + */ |
|
| 242 | + public function httpHead(RequestInterface $request, ResponseInterface $response) |
|
| 243 | + { |
|
| 244 | + // This is implemented by changing the HEAD request to a GET request, |
|
| 245 | + // and telling the request handler that is doesn't need to create the body. |
|
| 246 | + $subRequest = clone $request; |
|
| 247 | + $subRequest->setMethod('GET'); |
|
| 248 | + $subRequest->setHeader('X-Sabre-Original-Method', 'HEAD'); |
|
| 249 | + |
|
| 250 | + try { |
|
| 251 | + $this->server->invokeMethod($subRequest, $response, false); |
|
| 252 | + } catch (Exception\NotImplemented $e) { |
|
| 253 | + // Some clients may do HEAD requests on collections, however, GET |
|
| 254 | + // requests and HEAD requests _may_ not be defined on a collection, |
|
| 255 | + // which would trigger a 501. |
|
| 256 | + // This breaks some clients though, so we're transforming these |
|
| 257 | + // 501s into 200s. |
|
| 258 | + $response->setStatus(200); |
|
| 259 | + $response->setBody(''); |
|
| 260 | + $response->setHeader('Content-Type', 'text/plain'); |
|
| 261 | + $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode()); |
|
| 262 | + } |
|
| 263 | + |
|
| 264 | + // Sending back false will interrupt the event chain and tell the server |
|
| 265 | + // we've handled this method. |
|
| 266 | + return false; |
|
| 267 | + } |
|
| 268 | + |
|
| 269 | + /** |
|
| 270 | + * HTTP Delete. |
|
| 271 | + * |
|
| 272 | + * The HTTP delete method, deletes a given uri |
|
| 273 | + */ |
|
| 274 | + public function httpDelete(RequestInterface $request, ResponseInterface $response) |
|
| 275 | + { |
|
| 276 | + $path = $request->getPath(); |
|
| 277 | + |
|
| 278 | + if (!$this->server->emit('beforeUnbind', [$path])) { |
|
| 279 | + return false; |
|
| 280 | + } |
|
| 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 interrupt the event chain and tell the server |
|
| 288 | + // we've handled this method. |
|
| 289 | + return false; |
|
| 290 | + } |
|
| 291 | + |
|
| 292 | + /** |
|
| 293 | + * WebDAV PROPFIND. |
|
| 294 | + * |
|
| 295 | + * This WebDAV method requests information about an uri resource, or a list of resources |
|
| 296 | + * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value |
|
| 297 | + * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory) |
|
| 298 | + * |
|
| 299 | + * The request body contains an XML data structure that has a list of properties the client understands |
|
| 300 | + * The response body is also an xml document, containing information about every uri resource and the requested properties |
|
| 301 | + * |
|
| 302 | + * It has to return a HTTP 207 Multi-status status code |
|
| 303 | + */ |
|
| 304 | + public function httpPropFind(RequestInterface $request, ResponseInterface $response) |
|
| 305 | + { |
|
| 306 | + $path = $request->getPath(); |
|
| 307 | + |
|
| 308 | + $requestBody = $request->getBodyAsString(); |
|
| 309 | + if (strlen($requestBody)) { |
|
| 310 | + try { |
|
| 311 | + $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody); |
|
| 312 | + } catch (ParseException $e) { |
|
| 313 | + throw new BadRequest($e->getMessage(), 0, $e); |
|
| 314 | + } |
|
| 315 | + } else { |
|
| 316 | + $propFindXml = new Xml\Request\PropFind(); |
|
| 317 | + $propFindXml->allProp = true; |
|
| 318 | + $propFindXml->properties = []; |
|
| 319 | + } |
|
| 320 | + |
|
| 321 | + $depth = $this->server->getHTTPDepth(1); |
|
| 322 | + // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled |
|
| 323 | + if (!$this->server->enablePropfindDepthInfinity && 0 != $depth) { |
|
| 324 | + $depth = 1; |
|
| 325 | + } |
|
| 326 | + |
|
| 327 | + $newProperties = $this->server->getPropertiesIteratorForPath($path, $propFindXml->properties, $depth); |
|
| 328 | + |
|
| 329 | + // This is a multi-status response |
|
| 330 | + $response->setStatus(207); |
|
| 331 | + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 332 | + $response->setHeader('Vary', 'Brief,Prefer'); |
|
| 333 | + |
|
| 334 | + // Normally this header is only needed for OPTIONS responses, however.. |
|
| 335 | + // iCal seems to also depend on these being set for PROPFIND. Since |
|
| 336 | + // this is not harmful, we'll add it. |
|
| 337 | + $features = ['1', '3', 'extended-mkcol']; |
|
| 338 | + foreach ($this->server->getPlugins() as $plugin) { |
|
| 339 | + $features = array_merge($features, $plugin->getFeatures()); |
|
| 340 | + } |
|
| 341 | + $response->setHeader('DAV', implode(', ', $features)); |
|
| 342 | + |
|
| 343 | + $prefer = $this->server->getHTTPPrefer(); |
|
| 344 | + $minimal = 'minimal' === $prefer['return']; |
|
| 345 | + |
|
| 346 | + $data = $this->server->generateMultiStatus($newProperties, $minimal); |
|
| 347 | + $response->setBody($data); |
|
| 348 | + |
|
| 349 | + // Sending back false will interrupt the event chain and tell the server |
|
| 350 | + // we've handled this method. |
|
| 351 | + return false; |
|
| 352 | + } |
|
| 353 | + |
|
| 354 | + /** |
|
| 355 | + * WebDAV PROPPATCH. |
|
| 356 | + * |
|
| 357 | + * This method is called to update properties on a Node. The request is an XML body with all the mutations. |
|
| 358 | + * In this XML body it is specified which properties should be set/updated and/or deleted |
|
| 359 | + * |
|
| 360 | + * @return bool |
|
| 361 | + */ |
|
| 362 | + public function httpPropPatch(RequestInterface $request, ResponseInterface $response) |
|
| 363 | + { |
|
| 364 | + $path = $request->getPath(); |
|
| 365 | + |
|
| 366 | + try { |
|
| 367 | + $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody()); |
|
| 368 | + } catch (ParseException $e) { |
|
| 369 | + throw new BadRequest($e->getMessage(), 0, $e); |
|
| 370 | + } |
|
| 371 | + $newProperties = $propPatch->properties; |
|
| 372 | + |
|
| 373 | + $result = $this->server->updateProperties($path, $newProperties); |
|
| 374 | + |
|
| 375 | + $prefer = $this->server->getHTTPPrefer(); |
|
| 376 | + $response->setHeader('Vary', 'Brief,Prefer'); |
|
| 377 | + |
|
| 378 | + if ('minimal' === $prefer['return']) { |
|
| 379 | + // If return-minimal is specified, we only have to check if the |
|
| 380 | + // request was successful, and don't need to return the |
|
| 381 | + // multi-status. |
|
| 382 | + $ok = true; |
|
| 383 | + foreach ($result as $prop => $code) { |
|
| 384 | + if ((int) $code > 299) { |
|
| 385 | + $ok = false; |
|
| 386 | + } |
|
| 387 | + } |
|
| 388 | + |
|
| 389 | + if ($ok) { |
|
| 390 | + $response->setStatus(204); |
|
| 391 | + |
|
| 392 | + return false; |
|
| 393 | + } |
|
| 394 | + } |
|
| 395 | + |
|
| 396 | + $response->setStatus(207); |
|
| 397 | + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 398 | + |
|
| 399 | + // Reorganizing the result for generateMultiStatus |
|
| 400 | + $multiStatus = []; |
|
| 401 | + foreach ($result as $propertyName => $code) { |
|
| 402 | + if (isset($multiStatus[$code])) { |
|
| 403 | + $multiStatus[$code][$propertyName] = null; |
|
| 404 | + } else { |
|
| 405 | + $multiStatus[$code] = [$propertyName => null]; |
|
| 406 | + } |
|
| 407 | + } |
|
| 408 | + $multiStatus['href'] = $path; |
|
| 409 | + |
|
| 410 | + $response->setBody( |
|
| 411 | + $this->server->generateMultiStatus([$multiStatus]) |
|
| 412 | + ); |
|
| 413 | + |
|
| 414 | + // Sending back false will interrupt the event chain and tell the server |
|
| 415 | + // we've handled this method. |
|
| 416 | + return false; |
|
| 417 | + } |
|
| 418 | + |
|
| 419 | + /** |
|
| 420 | + * HTTP PUT method. |
|
| 421 | + * |
|
| 422 | + * This HTTP method updates a file, or creates a new one. |
|
| 423 | + * |
|
| 424 | + * 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 |
|
| 425 | + * |
|
| 426 | + * @return bool |
|
| 427 | + */ |
|
| 428 | + public function httpPut(RequestInterface $request, ResponseInterface $response) |
|
| 429 | + { |
|
| 430 | + $body = $request->getBodyAsStream(); |
|
| 431 | + $path = $request->getPath(); |
|
| 432 | + |
|
| 433 | + // Intercepting Content-Range |
|
| 434 | + if ($request->getHeader('Content-Range')) { |
|
| 435 | + /* |
|
| 436 | 436 | An origin server that allows PUT on a given target resource MUST send |
| 437 | 437 | a 400 (Bad Request) response to a PUT request that contains a |
| 438 | 438 | Content-Range header field. |
| 439 | 439 | |
| 440 | 440 | Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4 |
| 441 | 441 | */ |
| 442 | - throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.'); |
|
| 443 | - } |
|
| 442 | + throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.'); |
|
| 443 | + } |
|
| 444 | 444 | |
| 445 | - // Intercepting the Finder problem |
|
| 446 | - if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) { |
|
| 447 | - /* |
|
| 445 | + // Intercepting the Finder problem |
|
| 446 | + if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) { |
|
| 447 | + /* |
|
| 448 | 448 | Many webservers will not cooperate well with Finder PUT requests, |
| 449 | 449 | because it uses 'Chunked' transfer encoding for the request body. |
| 450 | 450 | |
@@ -465,443 +465,443 @@ discard block |
||
| 465 | 465 | protect the end-user. |
| 466 | 466 | */ |
| 467 | 467 | |
| 468 | - // Only reading first byte |
|
| 469 | - $firstByte = fread($body, 1); |
|
| 470 | - if (1 !== strlen($firstByte)) { |
|
| 471 | - throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.'); |
|
| 472 | - } |
|
| 473 | - |
|
| 474 | - // The body needs to stay intact, so we copy everything to a |
|
| 475 | - // temporary stream. |
|
| 476 | - |
|
| 477 | - $newBody = fopen('php://temp', 'r+'); |
|
| 478 | - fwrite($newBody, $firstByte); |
|
| 479 | - stream_copy_to_stream($body, $newBody); |
|
| 480 | - rewind($newBody); |
|
| 481 | - |
|
| 482 | - $body = $newBody; |
|
| 483 | - } |
|
| 484 | - |
|
| 485 | - if ($this->server->tree->nodeExists($path)) { |
|
| 486 | - $node = $this->server->tree->getNodeForPath($path); |
|
| 487 | - |
|
| 488 | - // If the node is a collection, we'll deny it |
|
| 489 | - if (!($node instanceof IFile)) { |
|
| 490 | - throw new Exception\Conflict('PUT is not allowed on non-files.'); |
|
| 491 | - } |
|
| 492 | - if (!$this->server->updateFile($path, $body, $etag)) { |
|
| 493 | - return false; |
|
| 494 | - } |
|
| 495 | - |
|
| 496 | - $response->setHeader('Content-Length', '0'); |
|
| 497 | - if ($etag) { |
|
| 498 | - $response->setHeader('ETag', $etag); |
|
| 499 | - } |
|
| 500 | - $response->setStatus(204); |
|
| 501 | - } else { |
|
| 502 | - $etag = null; |
|
| 503 | - // If we got here, the resource didn't exist yet. |
|
| 504 | - if (!$this->server->createFile($path, $body, $etag)) { |
|
| 505 | - // For one reason or another the file was not created. |
|
| 506 | - return false; |
|
| 507 | - } |
|
| 508 | - |
|
| 509 | - $response->setHeader('Content-Length', '0'); |
|
| 510 | - if ($etag) { |
|
| 511 | - $response->setHeader('ETag', $etag); |
|
| 512 | - } |
|
| 513 | - $response->setStatus(201); |
|
| 514 | - } |
|
| 515 | - |
|
| 516 | - // Sending back false will interrupt the event chain and tell the server |
|
| 517 | - // we've handled this method. |
|
| 518 | - return false; |
|
| 519 | - } |
|
| 520 | - |
|
| 521 | - /** |
|
| 522 | - * WebDAV MKCOL. |
|
| 523 | - * |
|
| 524 | - * The MKCOL method is used to create a new collection (directory) on the server |
|
| 525 | - * |
|
| 526 | - * @return bool |
|
| 527 | - */ |
|
| 528 | - public function httpMkcol(RequestInterface $request, ResponseInterface $response) |
|
| 529 | - { |
|
| 530 | - $requestBody = $request->getBodyAsString(); |
|
| 531 | - $path = $request->getPath(); |
|
| 532 | - |
|
| 533 | - if ($requestBody) { |
|
| 534 | - $contentType = $request->getHeader('Content-Type'); |
|
| 535 | - if (null === $contentType || (0 !== strpos($contentType, 'application/xml') && 0 !== strpos($contentType, 'text/xml'))) { |
|
| 536 | - // We must throw 415 for unsupported mkcol bodies |
|
| 537 | - throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type'); |
|
| 538 | - } |
|
| 539 | - |
|
| 540 | - try { |
|
| 541 | - $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody); |
|
| 542 | - } catch (\Sabre\Xml\ParseException $e) { |
|
| 543 | - throw new Exception\BadRequest($e->getMessage(), 0, $e); |
|
| 544 | - } |
|
| 545 | - |
|
| 546 | - $properties = $mkcol->getProperties(); |
|
| 547 | - |
|
| 548 | - if (!isset($properties['{DAV:}resourcetype'])) { |
|
| 549 | - throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property'); |
|
| 550 | - } |
|
| 551 | - $resourceType = $properties['{DAV:}resourcetype']->getValue(); |
|
| 552 | - unset($properties['{DAV:}resourcetype']); |
|
| 553 | - } else { |
|
| 554 | - $properties = []; |
|
| 555 | - $resourceType = ['{DAV:}collection']; |
|
| 556 | - } |
|
| 557 | - |
|
| 558 | - $mkcol = new MkCol($resourceType, $properties); |
|
| 559 | - |
|
| 560 | - $result = $this->server->createCollection($path, $mkcol); |
|
| 561 | - |
|
| 562 | - if (is_array($result)) { |
|
| 563 | - $response->setStatus(207); |
|
| 564 | - $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 565 | - |
|
| 566 | - $response->setBody( |
|
| 567 | - $this->server->generateMultiStatus([$result]) |
|
| 568 | - ); |
|
| 569 | - } else { |
|
| 570 | - $response->setHeader('Content-Length', '0'); |
|
| 571 | - $response->setStatus(201); |
|
| 572 | - } |
|
| 573 | - |
|
| 574 | - // Sending back false will interrupt the event chain and tell the server |
|
| 575 | - // we've handled this method. |
|
| 576 | - return false; |
|
| 577 | - } |
|
| 578 | - |
|
| 579 | - /** |
|
| 580 | - * WebDAV HTTP MOVE method. |
|
| 581 | - * |
|
| 582 | - * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo |
|
| 583 | - * |
|
| 584 | - * @return bool |
|
| 585 | - */ |
|
| 586 | - public function httpMove(RequestInterface $request, ResponseInterface $response) |
|
| 587 | - { |
|
| 588 | - $path = $request->getPath(); |
|
| 589 | - |
|
| 590 | - $moveInfo = $this->server->getCopyAndMoveInfo($request); |
|
| 591 | - |
|
| 592 | - if ($moveInfo['destinationExists']) { |
|
| 593 | - if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) { |
|
| 594 | - return false; |
|
| 595 | - } |
|
| 596 | - } |
|
| 597 | - if (!$this->server->emit('beforeUnbind', [$path])) { |
|
| 598 | - return false; |
|
| 599 | - } |
|
| 600 | - if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) { |
|
| 601 | - return false; |
|
| 602 | - } |
|
| 603 | - if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) { |
|
| 604 | - return false; |
|
| 605 | - } |
|
| 606 | - |
|
| 607 | - if ($moveInfo['destinationExists']) { |
|
| 608 | - $this->server->tree->delete($moveInfo['destination']); |
|
| 609 | - $this->server->emit('afterUnbind', [$moveInfo['destination']]); |
|
| 610 | - } |
|
| 611 | - |
|
| 612 | - $this->server->tree->move($path, $moveInfo['destination']); |
|
| 613 | - |
|
| 614 | - // Its important afterMove is called before afterUnbind, because it |
|
| 615 | - // allows systems to transfer data from one path to another. |
|
| 616 | - // PropertyStorage uses this. If afterUnbind was first, it would clean |
|
| 617 | - // up all the properties before it has a chance. |
|
| 618 | - $this->server->emit('afterMove', [$path, $moveInfo['destination']]); |
|
| 619 | - $this->server->emit('afterUnbind', [$path]); |
|
| 620 | - $this->server->emit('afterBind', [$moveInfo['destination']]); |
|
| 621 | - |
|
| 622 | - // If a resource was overwritten we should send a 204, otherwise a 201 |
|
| 623 | - $response->setHeader('Content-Length', '0'); |
|
| 624 | - $response->setStatus($moveInfo['destinationExists'] ? 204 : 201); |
|
| 625 | - |
|
| 626 | - // Sending back false will interrupt the event chain and tell the server |
|
| 627 | - // we've handled this method. |
|
| 628 | - return false; |
|
| 629 | - } |
|
| 630 | - |
|
| 631 | - /** |
|
| 632 | - * WebDAV HTTP COPY method. |
|
| 633 | - * |
|
| 634 | - * This method copies one uri to a different uri, and works much like the MOVE request |
|
| 635 | - * A lot of the actual request processing is done in getCopyMoveInfo |
|
| 636 | - * |
|
| 637 | - * @return bool |
|
| 638 | - */ |
|
| 639 | - public function httpCopy(RequestInterface $request, ResponseInterface $response) |
|
| 640 | - { |
|
| 641 | - $path = $request->getPath(); |
|
| 642 | - |
|
| 643 | - $copyInfo = $this->server->getCopyAndMoveInfo($request); |
|
| 644 | - |
|
| 645 | - if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) { |
|
| 646 | - return false; |
|
| 647 | - } |
|
| 648 | - if (!$this->server->emit('beforeCopy', [$path, $copyInfo['destination']])) { |
|
| 649 | - return false; |
|
| 650 | - } |
|
| 651 | - |
|
| 652 | - if ($copyInfo['destinationExists']) { |
|
| 653 | - if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) { |
|
| 654 | - return false; |
|
| 655 | - } |
|
| 656 | - $this->server->tree->delete($copyInfo['destination']); |
|
| 657 | - } |
|
| 658 | - |
|
| 659 | - $this->server->tree->copy($path, $copyInfo['destination']); |
|
| 660 | - $this->server->emit('afterCopy', [$path, $copyInfo['destination']]); |
|
| 661 | - $this->server->emit('afterBind', [$copyInfo['destination']]); |
|
| 662 | - |
|
| 663 | - // If a resource was overwritten we should send a 204, otherwise a 201 |
|
| 664 | - $response->setHeader('Content-Length', '0'); |
|
| 665 | - $response->setStatus($copyInfo['destinationExists'] ? 204 : 201); |
|
| 666 | - |
|
| 667 | - // Sending back false will interrupt the event chain and tell the server |
|
| 668 | - // we've handled this method. |
|
| 669 | - return false; |
|
| 670 | - } |
|
| 671 | - |
|
| 672 | - /** |
|
| 673 | - * HTTP REPORT method implementation. |
|
| 674 | - * |
|
| 675 | - * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) |
|
| 676 | - * It's used in a lot of extensions, so it made sense to implement it into the core. |
|
| 677 | - * |
|
| 678 | - * @return bool |
|
| 679 | - */ |
|
| 680 | - public function httpReport(RequestInterface $request, ResponseInterface $response) |
|
| 681 | - { |
|
| 682 | - $path = $request->getPath(); |
|
| 683 | - |
|
| 684 | - $result = $this->server->xml->parse( |
|
| 685 | - $request->getBody(), |
|
| 686 | - $request->getUrl(), |
|
| 687 | - $rootElementName |
|
| 688 | - ); |
|
| 689 | - |
|
| 690 | - if ($this->server->emit('report', [$rootElementName, $result, $path])) { |
|
| 691 | - // If emit returned true, it means the report was not supported |
|
| 692 | - throw new Exception\ReportNotSupported(); |
|
| 693 | - } |
|
| 694 | - |
|
| 695 | - // Sending back false will interrupt the event chain and tell the server |
|
| 696 | - // we've handled this method. |
|
| 697 | - return false; |
|
| 698 | - } |
|
| 699 | - |
|
| 700 | - /** |
|
| 701 | - * This method is called during property updates. |
|
| 702 | - * |
|
| 703 | - * Here we check if a user attempted to update a protected property and |
|
| 704 | - * ensure that the process fails if this is the case. |
|
| 705 | - * |
|
| 706 | - * @param string $path |
|
| 707 | - */ |
|
| 708 | - public function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) |
|
| 709 | - { |
|
| 710 | - // Comparing the mutation list to the list of protected properties. |
|
| 711 | - $mutations = $propPatch->getMutations(); |
|
| 712 | - |
|
| 713 | - $protected = array_intersect( |
|
| 714 | - $this->server->protectedProperties, |
|
| 715 | - array_keys($mutations) |
|
| 716 | - ); |
|
| 717 | - |
|
| 718 | - if ($protected) { |
|
| 719 | - $propPatch->setResultCode($protected, 403); |
|
| 720 | - } |
|
| 721 | - } |
|
| 722 | - |
|
| 723 | - /** |
|
| 724 | - * This method is called during property updates. |
|
| 725 | - * |
|
| 726 | - * Here we check if a node implements IProperties and let the node handle |
|
| 727 | - * updating of (some) properties. |
|
| 728 | - * |
|
| 729 | - * @param string $path |
|
| 730 | - */ |
|
| 731 | - public function propPatchNodeUpdate($path, PropPatch $propPatch) |
|
| 732 | - { |
|
| 733 | - // This should trigger a 404 if the node doesn't exist. |
|
| 734 | - $node = $this->server->tree->getNodeForPath($path); |
|
| 735 | - |
|
| 736 | - if ($node instanceof IProperties) { |
|
| 737 | - $node->propPatch($propPatch); |
|
| 738 | - } |
|
| 739 | - } |
|
| 740 | - |
|
| 741 | - /** |
|
| 742 | - * This method is called when properties are retrieved. |
|
| 743 | - * |
|
| 744 | - * Here we add all the default properties. |
|
| 745 | - */ |
|
| 746 | - public function propFind(PropFind $propFind, INode $node) |
|
| 747 | - { |
|
| 748 | - $propFind->handle('{DAV:}getlastmodified', function () use ($node) { |
|
| 749 | - $lm = $node->getLastModified(); |
|
| 750 | - if ($lm) { |
|
| 751 | - return new Xml\Property\GetLastModified($lm); |
|
| 752 | - } |
|
| 753 | - }); |
|
| 754 | - |
|
| 755 | - if ($node instanceof IFile) { |
|
| 756 | - $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']); |
|
| 757 | - $propFind->handle('{DAV:}getetag', [$node, 'getETag']); |
|
| 758 | - $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']); |
|
| 759 | - } |
|
| 760 | - |
|
| 761 | - if ($node instanceof IQuota) { |
|
| 762 | - $quotaInfo = null; |
|
| 763 | - $propFind->handle('{DAV:}quota-used-bytes', function () use (&$quotaInfo, $node) { |
|
| 764 | - $quotaInfo = $node->getQuotaInfo(); |
|
| 765 | - |
|
| 766 | - return $quotaInfo[0]; |
|
| 767 | - }); |
|
| 768 | - $propFind->handle('{DAV:}quota-available-bytes', function () use (&$quotaInfo, $node) { |
|
| 769 | - if (!$quotaInfo) { |
|
| 770 | - $quotaInfo = $node->getQuotaInfo(); |
|
| 771 | - } |
|
| 772 | - |
|
| 773 | - return $quotaInfo[1]; |
|
| 774 | - }); |
|
| 775 | - } |
|
| 776 | - |
|
| 777 | - $propFind->handle('{DAV:}supported-report-set', function () use ($propFind) { |
|
| 778 | - $reports = []; |
|
| 779 | - foreach ($this->server->getPlugins() as $plugin) { |
|
| 780 | - $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath())); |
|
| 781 | - } |
|
| 782 | - |
|
| 783 | - return new Xml\Property\SupportedReportSet($reports); |
|
| 784 | - }); |
|
| 785 | - $propFind->handle('{DAV:}resourcetype', function () use ($node) { |
|
| 786 | - return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node)); |
|
| 787 | - }); |
|
| 788 | - $propFind->handle('{DAV:}supported-method-set', function () use ($propFind) { |
|
| 789 | - return new Xml\Property\SupportedMethodSet( |
|
| 790 | - $this->server->getAllowedMethods($propFind->getPath()) |
|
| 791 | - ); |
|
| 792 | - }); |
|
| 793 | - } |
|
| 794 | - |
|
| 795 | - /** |
|
| 796 | - * Fetches properties for a node. |
|
| 797 | - * |
|
| 798 | - * This event is called a bit later, so plugins have a chance first to |
|
| 799 | - * populate the result. |
|
| 800 | - */ |
|
| 801 | - public function propFindNode(PropFind $propFind, INode $node) |
|
| 802 | - { |
|
| 803 | - if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) { |
|
| 804 | - $nodeProperties = $node->getProperties($propertyNames); |
|
| 805 | - foreach ($nodeProperties as $propertyName => $propertyValue) { |
|
| 806 | - $propFind->set($propertyName, $propertyValue, 200); |
|
| 807 | - } |
|
| 808 | - } |
|
| 809 | - } |
|
| 810 | - |
|
| 811 | - /** |
|
| 812 | - * This method is called when properties are retrieved. |
|
| 813 | - * |
|
| 814 | - * This specific handler is called very late in the process, because we |
|
| 815 | - * want other systems to first have a chance to handle the properties. |
|
| 816 | - */ |
|
| 817 | - public function propFindLate(PropFind $propFind, INode $node) |
|
| 818 | - { |
|
| 819 | - $propFind->handle('{http://calendarserver.org/ns/}getctag', function () use ($propFind) { |
|
| 820 | - // If we already have a sync-token from the current propFind |
|
| 821 | - // request, we can re-use that. |
|
| 822 | - $val = $propFind->get('{http://sabredav.org/ns}sync-token'); |
|
| 823 | - if ($val) { |
|
| 824 | - return $val; |
|
| 825 | - } |
|
| 826 | - |
|
| 827 | - $val = $propFind->get('{DAV:}sync-token'); |
|
| 828 | - if ($val && is_scalar($val)) { |
|
| 829 | - return $val; |
|
| 830 | - } |
|
| 831 | - if ($val && $val instanceof Xml\Property\Href) { |
|
| 832 | - return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); |
|
| 833 | - } |
|
| 834 | - |
|
| 835 | - // If we got here, the earlier two properties may simply not have |
|
| 836 | - // been part of the earlier request. We're going to fetch them. |
|
| 837 | - $result = $this->server->getProperties($propFind->getPath(), [ |
|
| 838 | - '{http://sabredav.org/ns}sync-token', |
|
| 839 | - '{DAV:}sync-token', |
|
| 840 | - ]); |
|
| 841 | - |
|
| 842 | - if (isset($result['{http://sabredav.org/ns}sync-token'])) { |
|
| 843 | - return $result['{http://sabredav.org/ns}sync-token']; |
|
| 844 | - } |
|
| 845 | - if (isset($result['{DAV:}sync-token'])) { |
|
| 846 | - $val = $result['{DAV:}sync-token']; |
|
| 847 | - if (is_scalar($val)) { |
|
| 848 | - return $val; |
|
| 849 | - } elseif ($val instanceof Xml\Property\Href) { |
|
| 850 | - return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); |
|
| 851 | - } |
|
| 852 | - } |
|
| 853 | - }); |
|
| 854 | - } |
|
| 855 | - |
|
| 856 | - /** |
|
| 857 | - * Listens for exception events, and automatically logs them. |
|
| 858 | - * |
|
| 859 | - * @param Exception $e |
|
| 860 | - */ |
|
| 861 | - public function exception($e) |
|
| 862 | - { |
|
| 863 | - $logLevel = \Psr\Log\LogLevel::CRITICAL; |
|
| 864 | - if ($e instanceof \Sabre\DAV\Exception) { |
|
| 865 | - // If it's a standard sabre/dav exception, it means we have a http |
|
| 866 | - // status code available. |
|
| 867 | - $code = $e->getHTTPCode(); |
|
| 868 | - |
|
| 869 | - if ($code >= 400 && $code < 500) { |
|
| 870 | - // user error |
|
| 871 | - $logLevel = \Psr\Log\LogLevel::INFO; |
|
| 872 | - } else { |
|
| 873 | - // Server-side error. We mark it's as an error, but it's not |
|
| 874 | - // critical. |
|
| 875 | - $logLevel = \Psr\Log\LogLevel::ERROR; |
|
| 876 | - } |
|
| 877 | - } |
|
| 878 | - |
|
| 879 | - $this->server->getLogger()->log( |
|
| 880 | - $logLevel, |
|
| 881 | - 'Uncaught exception', |
|
| 882 | - [ |
|
| 883 | - 'exception' => $e, |
|
| 884 | - ] |
|
| 885 | - ); |
|
| 886 | - } |
|
| 887 | - |
|
| 888 | - /** |
|
| 889 | - * Returns a bunch of meta-data about the plugin. |
|
| 890 | - * |
|
| 891 | - * Providing this information is optional, and is mainly displayed by the |
|
| 892 | - * Browser plugin. |
|
| 893 | - * |
|
| 894 | - * The description key in the returned array may contain html and will not |
|
| 895 | - * be sanitized. |
|
| 896 | - * |
|
| 897 | - * @return array |
|
| 898 | - */ |
|
| 899 | - public function getPluginInfo() |
|
| 900 | - { |
|
| 901 | - return [ |
|
| 902 | - 'name' => $this->getPluginName(), |
|
| 903 | - '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.', |
|
| 904 | - 'link' => null, |
|
| 905 | - ]; |
|
| 906 | - } |
|
| 468 | + // Only reading first byte |
|
| 469 | + $firstByte = fread($body, 1); |
|
| 470 | + if (1 !== strlen($firstByte)) { |
|
| 471 | + throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.'); |
|
| 472 | + } |
|
| 473 | + |
|
| 474 | + // The body needs to stay intact, so we copy everything to a |
|
| 475 | + // temporary stream. |
|
| 476 | + |
|
| 477 | + $newBody = fopen('php://temp', 'r+'); |
|
| 478 | + fwrite($newBody, $firstByte); |
|
| 479 | + stream_copy_to_stream($body, $newBody); |
|
| 480 | + rewind($newBody); |
|
| 481 | + |
|
| 482 | + $body = $newBody; |
|
| 483 | + } |
|
| 484 | + |
|
| 485 | + if ($this->server->tree->nodeExists($path)) { |
|
| 486 | + $node = $this->server->tree->getNodeForPath($path); |
|
| 487 | + |
|
| 488 | + // If the node is a collection, we'll deny it |
|
| 489 | + if (!($node instanceof IFile)) { |
|
| 490 | + throw new Exception\Conflict('PUT is not allowed on non-files.'); |
|
| 491 | + } |
|
| 492 | + if (!$this->server->updateFile($path, $body, $etag)) { |
|
| 493 | + return false; |
|
| 494 | + } |
|
| 495 | + |
|
| 496 | + $response->setHeader('Content-Length', '0'); |
|
| 497 | + if ($etag) { |
|
| 498 | + $response->setHeader('ETag', $etag); |
|
| 499 | + } |
|
| 500 | + $response->setStatus(204); |
|
| 501 | + } else { |
|
| 502 | + $etag = null; |
|
| 503 | + // If we got here, the resource didn't exist yet. |
|
| 504 | + if (!$this->server->createFile($path, $body, $etag)) { |
|
| 505 | + // For one reason or another the file was not created. |
|
| 506 | + return false; |
|
| 507 | + } |
|
| 508 | + |
|
| 509 | + $response->setHeader('Content-Length', '0'); |
|
| 510 | + if ($etag) { |
|
| 511 | + $response->setHeader('ETag', $etag); |
|
| 512 | + } |
|
| 513 | + $response->setStatus(201); |
|
| 514 | + } |
|
| 515 | + |
|
| 516 | + // Sending back false will interrupt the event chain and tell the server |
|
| 517 | + // we've handled this method. |
|
| 518 | + return false; |
|
| 519 | + } |
|
| 520 | + |
|
| 521 | + /** |
|
| 522 | + * WebDAV MKCOL. |
|
| 523 | + * |
|
| 524 | + * The MKCOL method is used to create a new collection (directory) on the server |
|
| 525 | + * |
|
| 526 | + * @return bool |
|
| 527 | + */ |
|
| 528 | + public function httpMkcol(RequestInterface $request, ResponseInterface $response) |
|
| 529 | + { |
|
| 530 | + $requestBody = $request->getBodyAsString(); |
|
| 531 | + $path = $request->getPath(); |
|
| 532 | + |
|
| 533 | + if ($requestBody) { |
|
| 534 | + $contentType = $request->getHeader('Content-Type'); |
|
| 535 | + if (null === $contentType || (0 !== strpos($contentType, 'application/xml') && 0 !== strpos($contentType, 'text/xml'))) { |
|
| 536 | + // We must throw 415 for unsupported mkcol bodies |
|
| 537 | + throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type'); |
|
| 538 | + } |
|
| 539 | + |
|
| 540 | + try { |
|
| 541 | + $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody); |
|
| 542 | + } catch (\Sabre\Xml\ParseException $e) { |
|
| 543 | + throw new Exception\BadRequest($e->getMessage(), 0, $e); |
|
| 544 | + } |
|
| 545 | + |
|
| 546 | + $properties = $mkcol->getProperties(); |
|
| 547 | + |
|
| 548 | + if (!isset($properties['{DAV:}resourcetype'])) { |
|
| 549 | + throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property'); |
|
| 550 | + } |
|
| 551 | + $resourceType = $properties['{DAV:}resourcetype']->getValue(); |
|
| 552 | + unset($properties['{DAV:}resourcetype']); |
|
| 553 | + } else { |
|
| 554 | + $properties = []; |
|
| 555 | + $resourceType = ['{DAV:}collection']; |
|
| 556 | + } |
|
| 557 | + |
|
| 558 | + $mkcol = new MkCol($resourceType, $properties); |
|
| 559 | + |
|
| 560 | + $result = $this->server->createCollection($path, $mkcol); |
|
| 561 | + |
|
| 562 | + if (is_array($result)) { |
|
| 563 | + $response->setStatus(207); |
|
| 564 | + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); |
|
| 565 | + |
|
| 566 | + $response->setBody( |
|
| 567 | + $this->server->generateMultiStatus([$result]) |
|
| 568 | + ); |
|
| 569 | + } else { |
|
| 570 | + $response->setHeader('Content-Length', '0'); |
|
| 571 | + $response->setStatus(201); |
|
| 572 | + } |
|
| 573 | + |
|
| 574 | + // Sending back false will interrupt the event chain and tell the server |
|
| 575 | + // we've handled this method. |
|
| 576 | + return false; |
|
| 577 | + } |
|
| 578 | + |
|
| 579 | + /** |
|
| 580 | + * WebDAV HTTP MOVE method. |
|
| 581 | + * |
|
| 582 | + * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo |
|
| 583 | + * |
|
| 584 | + * @return bool |
|
| 585 | + */ |
|
| 586 | + public function httpMove(RequestInterface $request, ResponseInterface $response) |
|
| 587 | + { |
|
| 588 | + $path = $request->getPath(); |
|
| 589 | + |
|
| 590 | + $moveInfo = $this->server->getCopyAndMoveInfo($request); |
|
| 591 | + |
|
| 592 | + if ($moveInfo['destinationExists']) { |
|
| 593 | + if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) { |
|
| 594 | + return false; |
|
| 595 | + } |
|
| 596 | + } |
|
| 597 | + if (!$this->server->emit('beforeUnbind', [$path])) { |
|
| 598 | + return false; |
|
| 599 | + } |
|
| 600 | + if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) { |
|
| 601 | + return false; |
|
| 602 | + } |
|
| 603 | + if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) { |
|
| 604 | + return false; |
|
| 605 | + } |
|
| 606 | + |
|
| 607 | + if ($moveInfo['destinationExists']) { |
|
| 608 | + $this->server->tree->delete($moveInfo['destination']); |
|
| 609 | + $this->server->emit('afterUnbind', [$moveInfo['destination']]); |
|
| 610 | + } |
|
| 611 | + |
|
| 612 | + $this->server->tree->move($path, $moveInfo['destination']); |
|
| 613 | + |
|
| 614 | + // Its important afterMove is called before afterUnbind, because it |
|
| 615 | + // allows systems to transfer data from one path to another. |
|
| 616 | + // PropertyStorage uses this. If afterUnbind was first, it would clean |
|
| 617 | + // up all the properties before it has a chance. |
|
| 618 | + $this->server->emit('afterMove', [$path, $moveInfo['destination']]); |
|
| 619 | + $this->server->emit('afterUnbind', [$path]); |
|
| 620 | + $this->server->emit('afterBind', [$moveInfo['destination']]); |
|
| 621 | + |
|
| 622 | + // If a resource was overwritten we should send a 204, otherwise a 201 |
|
| 623 | + $response->setHeader('Content-Length', '0'); |
|
| 624 | + $response->setStatus($moveInfo['destinationExists'] ? 204 : 201); |
|
| 625 | + |
|
| 626 | + // Sending back false will interrupt the event chain and tell the server |
|
| 627 | + // we've handled this method. |
|
| 628 | + return false; |
|
| 629 | + } |
|
| 630 | + |
|
| 631 | + /** |
|
| 632 | + * WebDAV HTTP COPY method. |
|
| 633 | + * |
|
| 634 | + * This method copies one uri to a different uri, and works much like the MOVE request |
|
| 635 | + * A lot of the actual request processing is done in getCopyMoveInfo |
|
| 636 | + * |
|
| 637 | + * @return bool |
|
| 638 | + */ |
|
| 639 | + public function httpCopy(RequestInterface $request, ResponseInterface $response) |
|
| 640 | + { |
|
| 641 | + $path = $request->getPath(); |
|
| 642 | + |
|
| 643 | + $copyInfo = $this->server->getCopyAndMoveInfo($request); |
|
| 644 | + |
|
| 645 | + if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) { |
|
| 646 | + return false; |
|
| 647 | + } |
|
| 648 | + if (!$this->server->emit('beforeCopy', [$path, $copyInfo['destination']])) { |
|
| 649 | + return false; |
|
| 650 | + } |
|
| 651 | + |
|
| 652 | + if ($copyInfo['destinationExists']) { |
|
| 653 | + if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) { |
|
| 654 | + return false; |
|
| 655 | + } |
|
| 656 | + $this->server->tree->delete($copyInfo['destination']); |
|
| 657 | + } |
|
| 658 | + |
|
| 659 | + $this->server->tree->copy($path, $copyInfo['destination']); |
|
| 660 | + $this->server->emit('afterCopy', [$path, $copyInfo['destination']]); |
|
| 661 | + $this->server->emit('afterBind', [$copyInfo['destination']]); |
|
| 662 | + |
|
| 663 | + // If a resource was overwritten we should send a 204, otherwise a 201 |
|
| 664 | + $response->setHeader('Content-Length', '0'); |
|
| 665 | + $response->setStatus($copyInfo['destinationExists'] ? 204 : 201); |
|
| 666 | + |
|
| 667 | + // Sending back false will interrupt the event chain and tell the server |
|
| 668 | + // we've handled this method. |
|
| 669 | + return false; |
|
| 670 | + } |
|
| 671 | + |
|
| 672 | + /** |
|
| 673 | + * HTTP REPORT method implementation. |
|
| 674 | + * |
|
| 675 | + * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) |
|
| 676 | + * It's used in a lot of extensions, so it made sense to implement it into the core. |
|
| 677 | + * |
|
| 678 | + * @return bool |
|
| 679 | + */ |
|
| 680 | + public function httpReport(RequestInterface $request, ResponseInterface $response) |
|
| 681 | + { |
|
| 682 | + $path = $request->getPath(); |
|
| 683 | + |
|
| 684 | + $result = $this->server->xml->parse( |
|
| 685 | + $request->getBody(), |
|
| 686 | + $request->getUrl(), |
|
| 687 | + $rootElementName |
|
| 688 | + ); |
|
| 689 | + |
|
| 690 | + if ($this->server->emit('report', [$rootElementName, $result, $path])) { |
|
| 691 | + // If emit returned true, it means the report was not supported |
|
| 692 | + throw new Exception\ReportNotSupported(); |
|
| 693 | + } |
|
| 694 | + |
|
| 695 | + // Sending back false will interrupt the event chain and tell the server |
|
| 696 | + // we've handled this method. |
|
| 697 | + return false; |
|
| 698 | + } |
|
| 699 | + |
|
| 700 | + /** |
|
| 701 | + * This method is called during property updates. |
|
| 702 | + * |
|
| 703 | + * Here we check if a user attempted to update a protected property and |
|
| 704 | + * ensure that the process fails if this is the case. |
|
| 705 | + * |
|
| 706 | + * @param string $path |
|
| 707 | + */ |
|
| 708 | + public function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) |
|
| 709 | + { |
|
| 710 | + // Comparing the mutation list to the list of protected properties. |
|
| 711 | + $mutations = $propPatch->getMutations(); |
|
| 712 | + |
|
| 713 | + $protected = array_intersect( |
|
| 714 | + $this->server->protectedProperties, |
|
| 715 | + array_keys($mutations) |
|
| 716 | + ); |
|
| 717 | + |
|
| 718 | + if ($protected) { |
|
| 719 | + $propPatch->setResultCode($protected, 403); |
|
| 720 | + } |
|
| 721 | + } |
|
| 722 | + |
|
| 723 | + /** |
|
| 724 | + * This method is called during property updates. |
|
| 725 | + * |
|
| 726 | + * Here we check if a node implements IProperties and let the node handle |
|
| 727 | + * updating of (some) properties. |
|
| 728 | + * |
|
| 729 | + * @param string $path |
|
| 730 | + */ |
|
| 731 | + public function propPatchNodeUpdate($path, PropPatch $propPatch) |
|
| 732 | + { |
|
| 733 | + // This should trigger a 404 if the node doesn't exist. |
|
| 734 | + $node = $this->server->tree->getNodeForPath($path); |
|
| 735 | + |
|
| 736 | + if ($node instanceof IProperties) { |
|
| 737 | + $node->propPatch($propPatch); |
|
| 738 | + } |
|
| 739 | + } |
|
| 740 | + |
|
| 741 | + /** |
|
| 742 | + * This method is called when properties are retrieved. |
|
| 743 | + * |
|
| 744 | + * Here we add all the default properties. |
|
| 745 | + */ |
|
| 746 | + public function propFind(PropFind $propFind, INode $node) |
|
| 747 | + { |
|
| 748 | + $propFind->handle('{DAV:}getlastmodified', function () use ($node) { |
|
| 749 | + $lm = $node->getLastModified(); |
|
| 750 | + if ($lm) { |
|
| 751 | + return new Xml\Property\GetLastModified($lm); |
|
| 752 | + } |
|
| 753 | + }); |
|
| 754 | + |
|
| 755 | + if ($node instanceof IFile) { |
|
| 756 | + $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']); |
|
| 757 | + $propFind->handle('{DAV:}getetag', [$node, 'getETag']); |
|
| 758 | + $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']); |
|
| 759 | + } |
|
| 760 | + |
|
| 761 | + if ($node instanceof IQuota) { |
|
| 762 | + $quotaInfo = null; |
|
| 763 | + $propFind->handle('{DAV:}quota-used-bytes', function () use (&$quotaInfo, $node) { |
|
| 764 | + $quotaInfo = $node->getQuotaInfo(); |
|
| 765 | + |
|
| 766 | + return $quotaInfo[0]; |
|
| 767 | + }); |
|
| 768 | + $propFind->handle('{DAV:}quota-available-bytes', function () use (&$quotaInfo, $node) { |
|
| 769 | + if (!$quotaInfo) { |
|
| 770 | + $quotaInfo = $node->getQuotaInfo(); |
|
| 771 | + } |
|
| 772 | + |
|
| 773 | + return $quotaInfo[1]; |
|
| 774 | + }); |
|
| 775 | + } |
|
| 776 | + |
|
| 777 | + $propFind->handle('{DAV:}supported-report-set', function () use ($propFind) { |
|
| 778 | + $reports = []; |
|
| 779 | + foreach ($this->server->getPlugins() as $plugin) { |
|
| 780 | + $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath())); |
|
| 781 | + } |
|
| 782 | + |
|
| 783 | + return new Xml\Property\SupportedReportSet($reports); |
|
| 784 | + }); |
|
| 785 | + $propFind->handle('{DAV:}resourcetype', function () use ($node) { |
|
| 786 | + return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node)); |
|
| 787 | + }); |
|
| 788 | + $propFind->handle('{DAV:}supported-method-set', function () use ($propFind) { |
|
| 789 | + return new Xml\Property\SupportedMethodSet( |
|
| 790 | + $this->server->getAllowedMethods($propFind->getPath()) |
|
| 791 | + ); |
|
| 792 | + }); |
|
| 793 | + } |
|
| 794 | + |
|
| 795 | + /** |
|
| 796 | + * Fetches properties for a node. |
|
| 797 | + * |
|
| 798 | + * This event is called a bit later, so plugins have a chance first to |
|
| 799 | + * populate the result. |
|
| 800 | + */ |
|
| 801 | + public function propFindNode(PropFind $propFind, INode $node) |
|
| 802 | + { |
|
| 803 | + if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) { |
|
| 804 | + $nodeProperties = $node->getProperties($propertyNames); |
|
| 805 | + foreach ($nodeProperties as $propertyName => $propertyValue) { |
|
| 806 | + $propFind->set($propertyName, $propertyValue, 200); |
|
| 807 | + } |
|
| 808 | + } |
|
| 809 | + } |
|
| 810 | + |
|
| 811 | + /** |
|
| 812 | + * This method is called when properties are retrieved. |
|
| 813 | + * |
|
| 814 | + * This specific handler is called very late in the process, because we |
|
| 815 | + * want other systems to first have a chance to handle the properties. |
|
| 816 | + */ |
|
| 817 | + public function propFindLate(PropFind $propFind, INode $node) |
|
| 818 | + { |
|
| 819 | + $propFind->handle('{http://calendarserver.org/ns/}getctag', function () use ($propFind) { |
|
| 820 | + // If we already have a sync-token from the current propFind |
|
| 821 | + // request, we can re-use that. |
|
| 822 | + $val = $propFind->get('{http://sabredav.org/ns}sync-token'); |
|
| 823 | + if ($val) { |
|
| 824 | + return $val; |
|
| 825 | + } |
|
| 826 | + |
|
| 827 | + $val = $propFind->get('{DAV:}sync-token'); |
|
| 828 | + if ($val && is_scalar($val)) { |
|
| 829 | + return $val; |
|
| 830 | + } |
|
| 831 | + if ($val && $val instanceof Xml\Property\Href) { |
|
| 832 | + return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); |
|
| 833 | + } |
|
| 834 | + |
|
| 835 | + // If we got here, the earlier two properties may simply not have |
|
| 836 | + // been part of the earlier request. We're going to fetch them. |
|
| 837 | + $result = $this->server->getProperties($propFind->getPath(), [ |
|
| 838 | + '{http://sabredav.org/ns}sync-token', |
|
| 839 | + '{DAV:}sync-token', |
|
| 840 | + ]); |
|
| 841 | + |
|
| 842 | + if (isset($result['{http://sabredav.org/ns}sync-token'])) { |
|
| 843 | + return $result['{http://sabredav.org/ns}sync-token']; |
|
| 844 | + } |
|
| 845 | + if (isset($result['{DAV:}sync-token'])) { |
|
| 846 | + $val = $result['{DAV:}sync-token']; |
|
| 847 | + if (is_scalar($val)) { |
|
| 848 | + return $val; |
|
| 849 | + } elseif ($val instanceof Xml\Property\Href) { |
|
| 850 | + return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); |
|
| 851 | + } |
|
| 852 | + } |
|
| 853 | + }); |
|
| 854 | + } |
|
| 855 | + |
|
| 856 | + /** |
|
| 857 | + * Listens for exception events, and automatically logs them. |
|
| 858 | + * |
|
| 859 | + * @param Exception $e |
|
| 860 | + */ |
|
| 861 | + public function exception($e) |
|
| 862 | + { |
|
| 863 | + $logLevel = \Psr\Log\LogLevel::CRITICAL; |
|
| 864 | + if ($e instanceof \Sabre\DAV\Exception) { |
|
| 865 | + // If it's a standard sabre/dav exception, it means we have a http |
|
| 866 | + // status code available. |
|
| 867 | + $code = $e->getHTTPCode(); |
|
| 868 | + |
|
| 869 | + if ($code >= 400 && $code < 500) { |
|
| 870 | + // user error |
|
| 871 | + $logLevel = \Psr\Log\LogLevel::INFO; |
|
| 872 | + } else { |
|
| 873 | + // Server-side error. We mark it's as an error, but it's not |
|
| 874 | + // critical. |
|
| 875 | + $logLevel = \Psr\Log\LogLevel::ERROR; |
|
| 876 | + } |
|
| 877 | + } |
|
| 878 | + |
|
| 879 | + $this->server->getLogger()->log( |
|
| 880 | + $logLevel, |
|
| 881 | + 'Uncaught exception', |
|
| 882 | + [ |
|
| 883 | + 'exception' => $e, |
|
| 884 | + ] |
|
| 885 | + ); |
|
| 886 | + } |
|
| 887 | + |
|
| 888 | + /** |
|
| 889 | + * Returns a bunch of meta-data about the plugin. |
|
| 890 | + * |
|
| 891 | + * Providing this information is optional, and is mainly displayed by the |
|
| 892 | + * Browser plugin. |
|
| 893 | + * |
|
| 894 | + * The description key in the returned array may contain html and will not |
|
| 895 | + * be sanitized. |
|
| 896 | + * |
|
| 897 | + * @return array |
|
| 898 | + */ |
|
| 899 | + public function getPluginInfo() |
|
| 900 | + { |
|
| 901 | + return [ |
|
| 902 | + 'name' => $this->getPluginName(), |
|
| 903 | + '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.', |
|
| 904 | + 'link' => null, |
|
| 905 | + ]; |
|
| 906 | + } |
|
| 907 | 907 | } |