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