]> git.mxchange.org Git - friendica-addons.git/blob - dav/SabreDAV/lib/Sabre/DAV/Client.php
Merge remote branch 'upstream/master'
[friendica-addons.git] / dav / SabreDAV / lib / Sabre / DAV / Client.php
1 <?php
2
3 /**
4  * SabreDAV DAV client
5  *
6  * This client wraps around Curl to provide a convenient API to a WebDAV
7  * server.
8  *
9  * NOTE: This class is experimental, it's api will likely change in the future.
10  *
11  * @package Sabre
12  * @subpackage DAVClient
13  * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
14  * @author Evert Pot (http://www.rooftopsolutions.nl/)
15  * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
16  */
17 class Sabre_DAV_Client {
18
19     /**
20      * The propertyMap is a key-value array.
21      *
22      * If you use the propertyMap, any {DAV:}multistatus responses with the
23      * proeprties listed in this array, will automatically be mapped to a
24      * respective class.
25      *
26      * The {DAV:}resourcetype property is automatically added. This maps to
27      * Sabre_DAV_Property_ResourceType
28      *
29      * @var array
30      */
31     public $propertyMap = array();
32
33     protected $baseUri;
34     protected $userName;
35     protected $password;
36     protected $proxy;
37
38     /**
39      * Basic authentication
40      */
41     const AUTH_BASIC = 1;
42
43     /**
44      * Digest authentication
45      */
46     const AUTH_DIGEST = 2;
47
48     /**
49      * The authentication type we're using.
50      *
51      * This is a bitmask of AUTH_BASIC and AUTH_DIGEST.
52      *
53      * If DIGEST is used, the client makes 1 extra request per request, to get
54      * the authentication tokens.
55      *
56      * @var int
57      */
58     protected $authType;
59
60     /**
61      * Constructor
62      *
63      * Settings are provided through the 'settings' argument. The following
64      * settings are supported:
65      *
66      *   * baseUri
67      *   * userName (optional)
68      *   * password (optional)
69      *   * proxy (optional)
70      *
71      * @param array $settings
72      */
73     public function __construct(array $settings) {
74
75         if (!isset($settings['baseUri'])) {
76             throw new InvalidArgumentException('A baseUri must be provided');
77         }
78
79         $validSettings = array(
80             'baseUri',
81             'userName',
82             'password',
83             'proxy',
84         );
85
86         foreach($validSettings as $validSetting) {
87             if (isset($settings[$validSetting])) {
88                 $this->$validSetting = $settings[$validSetting];
89             }
90         }
91
92         if (isset($settings['authType'])) {
93             $this->authType = $settings['authType'];
94         } else {
95             $this->authType = self::AUTH_BASIC | self::AUTH_DIGEST;
96         }
97
98         $this->propertyMap['{DAV:}resourcetype'] = 'Sabre_DAV_Property_ResourceType';
99
100     }
101
102     /**
103      * Does a PROPFIND request
104      *
105      * The list of requested properties must be specified as an array, in clark
106      * notation.
107      *
108      * The returned array will contain a list of filenames as keys, and
109      * properties as values.
110      *
111      * The properties array will contain the list of properties. Only properties
112      * that are actually returned from the server (without error) will be
113      * returned, anything else is discarded.
114      *
115      * Depth should be either 0 or 1. A depth of 1 will cause a request to be
116      * made to the server to also return all child resources.
117      *
118      * @param string $url
119      * @param array $properties
120      * @param int $depth
121      * @return array
122      */
123     public function propFind($url, array $properties, $depth = 0) {
124
125         $body = '<?xml version="1.0"?>' . "\n";
126         $body.= '<d:propfind xmlns:d="DAV:">' . "\n";
127         $body.= '  <d:prop>' . "\n";
128
129         foreach($properties as $property) {
130
131             list(
132                 $namespace,
133                 $elementName
134             ) = Sabre_DAV_XMLUtil::parseClarkNotation($property);
135
136             if ($namespace === 'DAV:') {
137                 $body.='    <d:' . $elementName . ' />' . "\n";
138             } else {
139                 $body.="    <x:" . $elementName . " xmlns:x=\"" . $namespace . "\"/>\n";
140             }
141
142         }
143
144         $body.= '  </d:prop>' . "\n";
145         $body.= '</d:propfind>';
146
147         $response = $this->request('PROPFIND', $url, $body, array(
148             'Depth' => $depth,
149             'Content-Type' => 'application/xml'
150         ));
151
152         $result = $this->parseMultiStatus($response['body']);
153
154         // If depth was 0, we only return the top item
155         if ($depth===0) {
156             reset($result);
157             $result = current($result);
158             return $result[200];
159         }
160
161         $newResult = array();
162         foreach($result as $href => $statusList) {
163
164             $newResult[$href] = $statusList[200];
165
166         }
167
168         return $newResult;
169
170     }
171
172     /**
173      * Updates a list of properties on the server
174      *
175      * The list of properties must have clark-notation properties for the keys,
176      * and the actual (string) value for the value. If the value is null, an
177      * attempt is made to delete the property.
178      *
179      * @todo Must be building the request using the DOM, and does not yet
180      *       support complex properties.
181      * @param string $url
182      * @param array $properties
183      * @return void
184      */
185     public function propPatch($url, array $properties) {
186
187         $body = '<?xml version="1.0"?>' . "\n";
188         $body.= '<d:propertyupdate xmlns:d="DAV:">' . "\n";
189
190         foreach($properties as $propName => $propValue) {
191
192             list(
193                 $namespace,
194                 $elementName
195             ) = Sabre_DAV_XMLUtil::parseClarkNotation($propName);
196
197             if ($propValue === null) {
198
199                 $body.="<d:remove><d:prop>\n";
200
201                 if ($namespace === 'DAV:') {
202                     $body.='    <d:' . $elementName . ' />' . "\n";
203                 } else {
204                     $body.="    <x:" . $elementName . " xmlns:x=\"" . $namespace . "\"/>\n";
205                 }
206
207                 $body.="</d:prop></d:remove>\n";
208
209             } else {
210
211                 $body.="<d:set><d:prop>\n";
212                 if ($namespace === 'DAV:') {
213                     $body.='    <d:' . $elementName . '>';
214                 } else {
215                     $body.="    <x:" . $elementName . " xmlns:x=\"" . $namespace . "\">";
216                 }
217                 // Shitty.. i know
218                 $body.=htmlspecialchars($propValue, ENT_NOQUOTES, 'UTF-8');
219                 if ($namespace === 'DAV:') {
220                     $body.='</d:' . $elementName . '>' . "\n";
221                 } else {
222                     $body.="</x:" . $elementName . ">\n";
223                 }
224                 $body.="</d:prop></d:set>\n";
225
226             }
227
228         }
229
230         $body.= '</d:propertyupdate>';
231
232         $this->request('PROPPATCH', $url, $body, array(
233             'Content-Type' => 'application/xml'
234         ));
235
236     }
237
238     /**
239      * Performs an HTTP options request
240      *
241      * This method returns all the features from the 'DAV:' header as an array.
242      * If there was no DAV header, or no contents this method will return an
243      * empty array.
244      *
245      * @return array
246      */
247     public function options() {
248
249         $result = $this->request('OPTIONS');
250         if (!isset($result['headers']['dav'])) {
251             return array();
252         }
253
254         $features = explode(',', $result['headers']['dav']);
255         foreach($features as &$v) {
256             $v = trim($v);
257         }
258         return $features;
259
260     }
261
262     /**
263      * Performs an actual HTTP request, and returns the result.
264      *
265      * If the specified url is relative, it will be expanded based on the base
266      * url.
267      *
268      * The returned array contains 3 keys:
269      *   * body - the response body
270      *   * httpCode - a HTTP code (200, 404, etc)
271      *   * headers - a list of response http headers. The header names have
272      *     been lowercased.
273      *
274      * @param string $method
275      * @param string $url
276      * @param string $body
277      * @param array $headers
278      * @return array
279      */
280     public function request($method, $url = '', $body = null, $headers = array()) {
281
282         $url = $this->getAbsoluteUrl($url);
283
284         $curlSettings = array(
285             CURLOPT_RETURNTRANSFER => true,
286             // Return headers as part of the response
287             CURLOPT_HEADER => true,
288             CURLOPT_POSTFIELDS => $body,
289             // Automatically follow redirects
290             CURLOPT_FOLLOWLOCATION => true,
291             CURLOPT_MAXREDIRS => 5,
292         );
293
294         switch ($method) {
295             case 'HEAD' :
296
297                 // do not read body with HEAD requests (this is neccessary because cURL does not ignore the body with HEAD
298                 // requests when the Content-Length header is given - which in turn is perfectly valid according to HTTP
299                 // specs...) cURL does unfortunately return an error in this case ("transfer closed transfer closed with
300                 // ... bytes remaining to read") this can be circumvented by explicitly telling cURL to ignore the
301                 // response body
302                 $curlSettings[CURLOPT_NOBODY] = true;
303                 $curlSettings[CURLOPT_CUSTOMREQUEST] = 'HEAD';
304                 break;
305
306             default:
307                 $curlSettings[CURLOPT_CUSTOMREQUEST] = $method;
308                 break;
309
310         }
311
312         // Adding HTTP headers
313         $nHeaders = array();
314         foreach($headers as $key=>$value) {
315
316             $nHeaders[] = $key . ': ' . $value;
317
318         }
319         $curlSettings[CURLOPT_HTTPHEADER] = $nHeaders;
320
321         if ($this->proxy) {
322             $curlSettings[CURLOPT_PROXY] = $this->proxy;
323         }
324
325         if ($this->userName && $this->authType) {
326             $curlType = 0;
327             if ($this->authType & self::AUTH_BASIC) {
328                 $curlType |= CURLAUTH_BASIC;
329             }
330             if ($this->authType & self::AUTH_DIGEST) {
331                 $curlType |= CURLAUTH_DIGEST;
332             }
333             $curlSettings[CURLOPT_HTTPAUTH] = $curlType;
334             $curlSettings[CURLOPT_USERPWD] = $this->userName . ':' . $this->password;
335         }
336
337         list(
338             $response,
339             $curlInfo,
340             $curlErrNo,
341             $curlError
342         ) = $this->curlRequest($url, $curlSettings);
343
344         $headerBlob = substr($response, 0, $curlInfo['header_size']);
345         $response = substr($response, $curlInfo['header_size']);
346
347         // In the case of 100 Continue, or redirects we'll have multiple lists
348         // of headers for each separate HTTP response. We can easily split this
349         // because they are separated by \r\n\r\n
350         $headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n"));
351
352         // We only care about the last set of headers
353         $headerBlob = $headerBlob[count($headerBlob)-1];
354
355         // Splitting headers
356         $headerBlob = explode("\r\n", $headerBlob);
357
358         $headers = array();
359         foreach($headerBlob as $header) {
360             $parts = explode(':', $header, 2);
361             if (count($parts)==2) {
362                 $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
363             }
364         }
365
366         $response = array(
367             'body' => $response,
368             'statusCode' => $curlInfo['http_code'],
369             'headers' => $headers
370         );
371
372         if ($curlErrNo) {
373             throw new Sabre_DAV_Exception('[CURL] Error while making request: ' . $curlError . ' (error code: ' . $curlErrNo . ')');
374         }
375
376         if ($response['statusCode']>=400) {
377             switch ($response['statusCode']) {
378                 case 400 :
379                     throw new Sabre_DAV_Exception_BadRequest('Bad request');
380                 case 401 :
381                     throw new Sabre_DAV_Exception_NotAuthenticated('Not authenticated');
382                 case 402 :
383                     throw new Sabre_DAV_Exception_PaymentRequired('Payment required');
384                 case 403 :
385                     throw new Sabre_DAV_Exception_Forbidden('Forbidden');
386                 case 404:
387                     throw new Sabre_DAV_Exception_NotFound('Resource not found.');
388                 case 405 :
389                     throw new Sabre_DAV_Exception_MethodNotAllowed('Method not allowed');
390                 case 409 :
391                     throw new Sabre_DAV_Exception_Conflict('Conflict');
392                 case 412 :
393                     throw new Sabre_DAV_Exception_PreconditionFailed('Precondition failed');
394                 case 416 :
395                     throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('Requested Range Not Satisfiable');
396                 case 500 :
397                     throw new Sabre_DAV_Exception('Internal server error');
398                 case 501 :
399                     throw new Sabre_DAV_Exception_NotImplemented('Not Implemeneted');
400                 case 507 :
401                     throw new Sabre_DAV_Exception_InsufficientStorage('Insufficient storage');
402                 default:
403                     throw new Sabre_DAV_Exception('HTTP error response. (errorcode ' . $response['statusCode'] . ')');
404             }
405         }
406
407         return $response;
408
409     }
410
411     /**
412      * Wrapper for all curl functions.
413      *
414      * The only reason this was split out in a separate method, is so it
415      * becomes easier to unittest.
416      *
417      * @param string $url
418      * @param array $settings
419      * @return array
420      */
421     protected function curlRequest($url, $settings) {
422
423         $curl = curl_init($url);
424         curl_setopt_array($curl, $settings);
425
426         return array(
427             curl_exec($curl),
428             curl_getinfo($curl),
429             curl_errno($curl),
430             curl_error($curl)
431         );
432
433     }
434
435     /**
436      * Returns the full url based on the given url (which may be relative). All
437      * urls are expanded based on the base url as given by the server.
438      *
439      * @param string $url
440      * @return string
441      */
442     protected function getAbsoluteUrl($url) {
443
444         // If the url starts with http:// or https://, the url is already absolute.
445         if (preg_match('/^http(s?):\/\//', $url)) {
446             return $url;
447         }
448
449         // If the url starts with a slash, we must calculate the url based off
450         // the root of the base url.
451         if (strpos($url,'/') === 0) {
452             $parts = parse_url($this->baseUri);
453             return $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
454         }
455
456         // Otherwise...
457         return $this->baseUri . $url;
458
459     }
460
461     /**
462      * Parses a WebDAV multistatus response body
463      *
464      * This method returns an array with the following structure
465      *
466      * array(
467      *   'url/to/resource' => array(
468      *     '200' => array(
469      *        '{DAV:}property1' => 'value1',
470      *        '{DAV:}property2' => 'value2',
471      *     ),
472      *     '404' => array(
473      *        '{DAV:}property1' => null,
474      *        '{DAV:}property2' => null,
475      *     ),
476      *   )
477      *   'url/to/resource2' => array(
478      *      .. etc ..
479      *   )
480      * )
481      *
482      *
483      * @param string $body xml body
484      * @return array
485      */
486     public function parseMultiStatus($body) {
487
488         $responseXML = simplexml_load_string($body, null, LIBXML_NOBLANKS | LIBXML_NOCDATA);
489         if ($responseXML===false) {
490             throw new InvalidArgumentException('The passed data is not valid XML');
491         }
492
493         $responseXML->registerXPathNamespace('d', 'DAV:');
494
495         $propResult = array();
496
497         foreach($responseXML->xpath('d:response') as $response) {
498             $response->registerXPathNamespace('d', 'DAV:');
499             $href = $response->xpath('d:href');
500             $href = (string)$href[0];
501
502             $properties = array();
503
504             foreach($response->xpath('d:propstat') as $propStat) {
505
506                 $propStat->registerXPathNamespace('d', 'DAV:');
507                 $status = $propStat->xpath('d:status');
508                 list($httpVersion, $statusCode, $message) = explode(' ', (string)$status[0],3);
509
510                 $properties[$statusCode] = Sabre_DAV_XMLUtil::parseProperties(dom_import_simplexml($propStat), $this->propertyMap);
511
512             }
513
514             $propResult[$href] = $properties;
515
516         }
517
518         return $propResult;
519
520     }
521
522 }