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