1<?php 2 3namespace Sabre\DAV; 4 5use Sabre\HTTP; 6use Sabre\Uri; 7 8/** 9 * SabreDAV DAV client 10 * 11 * This client wraps around Curl to provide a convenient API to a WebDAV 12 * server. 13 * 14 * NOTE: This class is experimental, it's api will likely change in the future. 15 * 16 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 17 * @author Evert Pot (http://evertpot.com/) 18 * @license http://sabre.io/license/ Modified BSD License 19 */ 20class Client extends HTTP\Client { 21 22 /** 23 * The xml service. 24 * 25 * Uset this service to configure the property and namespace maps. 26 * 27 * @var mixed 28 */ 29 public $xml; 30 31 /** 32 * The elementMap 33 * 34 * This property is linked via reference to $this->xml->elementMap. 35 * It's deprecated as of version 3.0.0, and should no longer be used. 36 * 37 * @deprecated 38 * @var array 39 */ 40 public $propertyMap = []; 41 42 /** 43 * Base URI 44 * 45 * This URI will be used to resolve relative urls. 46 * 47 * @var string 48 */ 49 protected $baseUri; 50 51 /** 52 * Basic authentication 53 */ 54 const AUTH_BASIC = 1; 55 56 /** 57 * Digest authentication 58 */ 59 const AUTH_DIGEST = 2; 60 61 /** 62 * NTLM authentication 63 */ 64 const AUTH_NTLM = 4; 65 66 /** 67 * Identity encoding, which basically does not nothing. 68 */ 69 const ENCODING_IDENTITY = 1; 70 71 /** 72 * Deflate encoding 73 */ 74 const ENCODING_DEFLATE = 2; 75 76 /** 77 * Gzip encoding 78 */ 79 const ENCODING_GZIP = 4; 80 81 /** 82 * Sends all encoding headers. 83 */ 84 const ENCODING_ALL = 7; 85 86 /** 87 * Content-encoding 88 * 89 * @var int 90 */ 91 protected $encoding = self::ENCODING_IDENTITY; 92 93 /** 94 * Constructor 95 * 96 * Settings are provided through the 'settings' argument. The following 97 * settings are supported: 98 * 99 * * baseUri 100 * * userName (optional) 101 * * password (optional) 102 * * proxy (optional) 103 * * authType (optional) 104 * * encoding (optional) 105 * 106 * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST 107 * and self::AUTH_NTLM. If you know which authentication method will be 108 * used, it's recommended to set it, as it will save a great deal of 109 * requests to 'discover' this information. 110 * 111 * Encoding is a bitmap with one of the ENCODING constants. 112 * 113 * @param array $settings 114 */ 115 function __construct(array $settings) { 116 117 if (!isset($settings['baseUri'])) { 118 throw new \InvalidArgumentException('A baseUri must be provided'); 119 } 120 121 parent::__construct(); 122 123 $this->baseUri = $settings['baseUri']; 124 125 if (isset($settings['proxy'])) { 126 $this->addCurlSetting(CURLOPT_PROXY, $settings['proxy']); 127 } 128 129 if (isset($settings['userName'])) { 130 $userName = $settings['userName']; 131 $password = isset($settings['password']) ? $settings['password'] : ''; 132 133 if (isset($settings['authType'])) { 134 $curlType = 0; 135 if ($settings['authType'] & self::AUTH_BASIC) { 136 $curlType |= CURLAUTH_BASIC; 137 } 138 if ($settings['authType'] & self::AUTH_DIGEST) { 139 $curlType |= CURLAUTH_DIGEST; 140 } 141 if ($settings['authType'] & self::AUTH_NTLM) { 142 $curlType |= CURLAUTH_NTLM; 143 } 144 } else { 145 $curlType = CURLAUTH_BASIC | CURLAUTH_DIGEST; 146 } 147 148 $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType); 149 $this->addCurlSetting(CURLOPT_USERPWD, $userName . ':' . $password); 150 151 } 152 153 if (isset($settings['encoding'])) { 154 $encoding = $settings['encoding']; 155 156 $encodings = []; 157 if ($encoding & self::ENCODING_IDENTITY) { 158 $encodings[] = 'identity'; 159 } 160 if ($encoding & self::ENCODING_DEFLATE) { 161 $encodings[] = 'deflate'; 162 } 163 if ($encoding & self::ENCODING_GZIP) { 164 $encodings[] = 'gzip'; 165 } 166 $this->addCurlSetting(CURLOPT_ENCODING, implode(',', $encodings)); 167 } 168 169 $this->addCurlSetting(CURLOPT_USERAGENT, 'sabre-dav/' . Version::VERSION . ' (http://sabre.io/)'); 170 171 $this->xml = new Xml\Service(); 172 // BC 173 $this->propertyMap = & $this->xml->elementMap; 174 175 } 176 177 /** 178 * Does a PROPFIND request 179 * 180 * The list of requested properties must be specified as an array, in clark 181 * notation. 182 * 183 * The returned array will contain a list of filenames as keys, and 184 * properties as values. 185 * 186 * The properties array will contain the list of properties. Only properties 187 * that are actually returned from the server (without error) will be 188 * returned, anything else is discarded. 189 * 190 * Depth should be either 0 or 1. A depth of 1 will cause a request to be 191 * made to the server to also return all child resources. 192 * 193 * @param string $url 194 * @param array $properties 195 * @param int $depth 196 * @return array 197 */ 198 function propFind($url, array $properties, $depth = 0) { 199 200 $dom = new \DOMDocument('1.0', 'UTF-8'); 201 $dom->formatOutput = true; 202 $root = $dom->createElementNS('DAV:', 'd:propfind'); 203 $prop = $dom->createElement('d:prop'); 204 205 foreach ($properties as $property) { 206 207 list( 208 $namespace, 209 $elementName 210 ) = \Sabre\Xml\Service::parseClarkNotation($property); 211 212 if ($namespace === 'DAV:') { 213 $element = $dom->createElement('d:' . $elementName); 214 } else { 215 $element = $dom->createElementNS($namespace, 'x:' . $elementName); 216 } 217 218 $prop->appendChild($element); 219 } 220 221 $dom->appendChild($root)->appendChild($prop); 222 $body = $dom->saveXML(); 223 224 $url = $this->getAbsoluteUrl($url); 225 226 $request = new HTTP\Request('PROPFIND', $url, [ 227 'Depth' => $depth, 228 'Content-Type' => 'application/xml' 229 ], $body); 230 231 $response = $this->send($request); 232 233 if ((int)$response->getStatus() >= 400) { 234 throw new HTTP\ClientHttpException($response); 235 } 236 237 $result = $this->parseMultiStatus($response->getBodyAsString()); 238 239 // If depth was 0, we only return the top item 240 if ($depth === 0) { 241 reset($result); 242 $result = current($result); 243 return isset($result[200]) ? $result[200] : []; 244 } 245 246 $newResult = []; 247 foreach ($result as $href => $statusList) { 248 249 $newResult[$href] = isset($statusList[200]) ? $statusList[200] : []; 250 251 } 252 253 return $newResult; 254 255 } 256 257 /** 258 * Updates a list of properties on the server 259 * 260 * The list of properties must have clark-notation properties for the keys, 261 * and the actual (string) value for the value. If the value is null, an 262 * attempt is made to delete the property. 263 * 264 * @param string $url 265 * @param array $properties 266 * @return bool 267 */ 268 function propPatch($url, array $properties) { 269 270 $propPatch = new Xml\Request\PropPatch(); 271 $propPatch->properties = $properties; 272 $xml = $this->xml->write( 273 '{DAV:}propertyupdate', 274 $propPatch 275 ); 276 277 $url = $this->getAbsoluteUrl($url); 278 $request = new HTTP\Request('PROPPATCH', $url, [ 279 'Content-Type' => 'application/xml', 280 ], $xml); 281 $response = $this->send($request); 282 283 if ($response->getStatus() >= 400) { 284 throw new HTTP\ClientHttpException($response); 285 } 286 287 if ($response->getStatus() === 207) { 288 // If it's a 207, the request could still have failed, but the 289 // information is hidden in the response body. 290 $result = $this->parseMultiStatus($response->getBodyAsString()); 291 292 $errorProperties = []; 293 foreach ($result as $href => $statusList) { 294 foreach ($statusList as $status => $properties) { 295 296 if ($status >= 400) { 297 foreach ($properties as $propName => $propValue) { 298 $errorProperties[] = $propName . ' (' . $status . ')'; 299 } 300 } 301 302 } 303 } 304 if ($errorProperties) { 305 306 throw new HTTP\ClientException('PROPPATCH failed. The following properties errored: ' . implode(', ', $errorProperties)); 307 } 308 } 309 return true; 310 311 } 312 313 /** 314 * Performs an HTTP options request 315 * 316 * This method returns all the features from the 'DAV:' header as an array. 317 * If there was no DAV header, or no contents this method will return an 318 * empty array. 319 * 320 * @return array 321 */ 322 function options() { 323 324 $request = new HTTP\Request('OPTIONS', $this->getAbsoluteUrl('')); 325 $response = $this->send($request); 326 327 $dav = $response->getHeader('Dav'); 328 if (!$dav) { 329 return []; 330 } 331 332 $features = explode(',', $dav); 333 foreach ($features as &$v) { 334 $v = trim($v); 335 } 336 return $features; 337 338 } 339 340 /** 341 * Performs an actual HTTP request, and returns the result. 342 * 343 * If the specified url is relative, it will be expanded based on the base 344 * url. 345 * 346 * The returned array contains 3 keys: 347 * * body - the response body 348 * * httpCode - a HTTP code (200, 404, etc) 349 * * headers - a list of response http headers. The header names have 350 * been lowercased. 351 * 352 * For large uploads, it's highly recommended to specify body as a stream 353 * resource. You can easily do this by simply passing the result of 354 * fopen(..., 'r'). 355 * 356 * This method will throw an exception if an HTTP error was received. Any 357 * HTTP status code above 399 is considered an error. 358 * 359 * Note that it is no longer recommended to use this method, use the send() 360 * method instead. 361 * 362 * @param string $method 363 * @param string $url 364 * @param string|resource|null $body 365 * @param array $headers 366 * @throws ClientException, in case a curl error occurred. 367 * @return array 368 */ 369 function request($method, $url = '', $body = null, array $headers = []) { 370 371 $url = $this->getAbsoluteUrl($url); 372 373 $response = $this->send(new HTTP\Request($method, $url, $headers, $body)); 374 return [ 375 'body' => $response->getBodyAsString(), 376 'statusCode' => (int)$response->getStatus(), 377 'headers' => array_change_key_case($response->getHeaders()), 378 ]; 379 380 } 381 382 /** 383 * Returns the full url based on the given url (which may be relative). All 384 * urls are expanded based on the base url as given by the server. 385 * 386 * @param string $url 387 * @return string 388 */ 389 function getAbsoluteUrl($url) { 390 391 return Uri\resolve( 392 $this->baseUri, 393 $url 394 ); 395 396 } 397 398 /** 399 * Parses a WebDAV multistatus response body 400 * 401 * This method returns an array with the following structure 402 * 403 * [ 404 * 'url/to/resource' => [ 405 * '200' => [ 406 * '{DAV:}property1' => 'value1', 407 * '{DAV:}property2' => 'value2', 408 * ], 409 * '404' => [ 410 * '{DAV:}property1' => null, 411 * '{DAV:}property2' => null, 412 * ], 413 * ], 414 * 'url/to/resource2' => [ 415 * .. etc .. 416 * ] 417 * ] 418 * 419 * 420 * @param string $body xml body 421 * @return array 422 */ 423 function parseMultiStatus($body) { 424 425 $multistatus = $this->xml->expect('{DAV:}multistatus', $body); 426 427 $result = []; 428 429 foreach ($multistatus->getResponses() as $response) { 430 431 $result[$response->getHref()] = $response->getResponseProperties(); 432 433 } 434 435 return $result; 436 437 } 438 439} 440