├── .gitignore
├── .travis.yml
├── README.md
├── composer.json
├── lib
└── Sabre
│ └── DAVClient
│ ├── Adapter
│ └── CardDAVAdapter.php
│ ├── Client.php
│ └── RequestBuilder
│ ├── AddressBookMultiGetRequestBuilder.php
│ ├── RequestBuilderInterface.php
│ └── SyncCollectionReportRequestBuilder.php
└── tests
├── Sabre
└── DAVClient
│ ├── ClientMock.php
│ └── ClientTest.php
├── bootstrap.php
└── phpunit.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | # composer
2 | vendor
3 | composer.lock
4 |
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - 5.4
4 | - 5.5
5 | - hhvm
6 |
7 | matrix:
8 | allow_failures:
9 | - php: hhvm
10 |
11 | before_script:
12 | - composer install --prefer-source
13 |
14 | script:
15 | - phpunit --configuration tests/phpunit.xml
16 |
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | sabre/davclient
2 | ===============
3 |
4 | A WebDAV, CalDAV and CardDAV client for PHP.
5 |
6 | Note that this library is completely non-functional at the moment ;)
7 |
8 | Installation
9 | ------------
10 |
11 | Make sure you have [Composer][3] installed. In your project directory, create,
12 | or edit a `composer.json` file, and make sure it contains something like this:
13 |
14 |
15 | ```json
16 | {
17 | "require" : {
18 | "sabre/davclient" : "dev-master"
19 | }
20 | }
21 | ```
22 |
23 | After that, just hit `composer install` and you should be rolling.
24 |
25 | Questions?
26 | ----------
27 |
28 | Head over to the [sabre/dav mailinglist][4], or you can also just open a ticket
29 | on [GitHub][5].
30 |
31 | Made at fruux
32 | -------------
33 |
34 | This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support.
35 |
36 | [3]: http://getcomposer.org/
37 | [4]: http://groups.google.com/group/sabredav-discuss
38 | [5]: https://github.com/fruux/sabre-davclient/issues/
39 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sabre/davclient",
3 | "description": "The sabre/davclient library provides an easy to use library for consuming Web-, Cal- and CardDAV services",
4 | "keywords": [ "WebDAV", "CalDAV", "CardDAV" ],
5 | "homepage": "https://github.com/fruux/sabre-davclient",
6 | "license": "BSD-3-Clause",
7 | "require": {
8 | "php": ">=5.4.1",
9 | "sabre/dav": "~1.9.0alpha2"
10 | },
11 | "require-dev" : {
12 | "phpunit/phpunit": "3.7.*"
13 | },
14 | "minimum-stability": "dev",
15 | "prefer-stable": true,
16 | "authors": [
17 | {
18 | "name": "Evert Pot",
19 | "email": "evert@rooftopsolutions.nl",
20 | "homepage": "http://evertpot.com/",
21 | "role": "Developer"
22 | }
23 | ],
24 | "support": {
25 | "forum": "https://groups.google.com/group/sabredav-discuss",
26 | "source": "https://github.com/fruux/sabre-davclient"
27 | },
28 | "autoload": {
29 | "psr-0": {
30 | "Sabre\\DAVClient": "lib/"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/Sabre/DAVClient/Adapter/CardDAVAdapter.php:
--------------------------------------------------------------------------------
1 | client = $client;
16 | }
17 |
18 | public function addressBookMultiGetReport($uri, $uids = [])
19 | {
20 | $builder = new RequestBuilder\AddressBookMultiGetRequestBuilder($uri, $uids);
21 |
22 | $request = $builder->build();
23 |
24 | return $this->client->send($request);
25 | }
26 |
27 | public function syncCollectionReport($uri, $sync_token = null, $sync_level = 1)
28 | {
29 | $builder = new RequestBuilder\SyncCollectionReportRequestBuilder($uri, $sync_token, $sync_level);
30 |
31 | $request = $builder->build();
32 |
33 | return $this->client->send($request);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/Sabre/DAVClient/Client.php:
--------------------------------------------------------------------------------
1 | baseUri = $settings['baseUri'];
137 |
138 | if (isset($settings['proxy'])) {
139 | $this->addCurlSetting(CURLOPT_PROXY, $settings['proxy']);
140 | }
141 |
142 | if (isset($settings['userName'])) {
143 | $userName = $settings['userName'];
144 | $password = isset($settings['password'])?$settings['password']:'';
145 |
146 | if (isset($settings['authType'])) {
147 | $curlType = 0;
148 |
149 | if ($settings['authType'] & self::AUTH_BASIC) {
150 | $curlType |= CURLAUTH_BASIC;
151 | }
152 |
153 | if ($settings['authType'] & self::AUTH_DIGEST) {
154 | $curlType |= CURLAUTH_DIGEST;
155 | }
156 | } else {
157 | $curlType = CURLAUTH_BASIC | CURLAUTH_DIGEST;
158 | }
159 |
160 | $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType);
161 | $this->addCurlSetting(CURLOPT_USERPWD, $userName . ':' . $password);
162 |
163 | }
164 |
165 | if (isset($settings['encoding'])) {
166 | $encoding = $settings['encoding'];
167 |
168 | $encodings = [];
169 |
170 | if ($encoding & self::ENCODING_IDENTITY) {
171 | $encodings[] = 'identity';
172 | }
173 |
174 | if ($encoding & self::ENCODING_DEFLATE) {
175 | $encodings[] = 'deflate';
176 | }
177 |
178 | if ($encoding & self::ENCODING_GZIP) {
179 | $encodings[] = 'gzip';
180 | }
181 |
182 | $this->addCurlSetting(CURLOPT_ENCODING, implode(',', $encodings));
183 | }
184 |
185 | $this->propertyMap['{DAV:}resourcetype'] = 'Sabre\\DAV\\Property\\ResourceType';
186 |
187 | $this->curlMultiHandle = curl_multi_init();
188 | }
189 |
190 |
191 | /**
192 | * Add trusted root certificates to the webdav client.
193 | *
194 | * The parameter certificates should be a absolute path to a file
195 | * which contains all trusted certificates
196 | *
197 | * @param string $certificates
198 | * @return void
199 | */
200 | public function addTrustedCertificates($certificates)
201 | {
202 | $this->addCurlSetting(CURLOPT_CAINFO, $certificates);
203 | }
204 |
205 | /**
206 | * Enables/disables SSL peer verification
207 | *
208 | * @param bool $value
209 | * @return void
210 | */
211 | public function setVerifyPeer($value)
212 | {
213 | $this->addCurlSetting(CURLOPT_SSL_VERIFYPEER, $value);
214 | }
215 |
216 | /**
217 | * Does a PROPFIND request
218 | *
219 | * The list of requested properties must be specified as an array, in clark
220 | * notation.
221 | *
222 | * The returned array will contain a list of filenames as keys, and
223 | * properties as values.
224 | *
225 | * The properties array will contain the list of properties. Only properties
226 | * that are actually returned from the server (without error) will be
227 | * returned, anything else is discarded.
228 | *
229 | * Depth should be either 0 or 1. A depth of 1 will cause a request to be
230 | * made to the server to also return all child resources.
231 | *
232 | * @param string $url
233 | * @param array $properties
234 | * @param int $depth
235 | * @return array
236 | */
237 | public function propFind($url, array $properties, $depth = 0)
238 | {
239 | $dom = new \DOMDocument('1.0', 'UTF-8');
240 | $dom->formatOutput = true;
241 | $root = $dom->createElementNS('DAV:', 'd:propfind');
242 | $prop = $dom->createElement('d:prop');
243 |
244 | foreach ($properties as $property) {
245 | list(
246 | $namespace,
247 | $elementName
248 | ) = XMLUtil::parseClarkNotation($property);
249 |
250 | if ($namespace === 'DAV:') {
251 | $element = $dom->createElement('d:'.$elementName);
252 | } else {
253 | $element = $dom->createElementNS($namespace, 'x:'.$elementName);
254 | }
255 |
256 | $prop->appendChild( $element );
257 | }
258 |
259 | $dom->appendChild($root)->appendChild( $prop );
260 | $body = $dom->saveXML();
261 |
262 | $url = $this->getAbsoluteUrl($url);
263 |
264 | $request = new HTTP\Request('PROPFIND', $url, [
265 | 'Depth' => $depth,
266 | 'Content-Type' => 'application/xml'
267 | ], $body);
268 |
269 | $response = $this->send($request);
270 |
271 | if ((int)$response->getStatus() >= 400) {
272 | throw new Exception('HTTP error: ' . $response->getStatus());
273 | }
274 |
275 | $result = $this->parseMultiStatus($response->getBody(true));
276 |
277 | // If depth was 0, we only return the top item
278 | if ($depth === 0) {
279 | reset($result);
280 | $result = current($result);
281 | return isset($result[200]) ? $result[200] : [];
282 | }
283 |
284 | $newResult = [];
285 |
286 | foreach ($result as $href => $statusList) {
287 | $newResult[$href] = isset($statusList[200]) ? $statusList[200] : [];
288 | }
289 |
290 | return $newResult;
291 | }
292 |
293 | /**
294 | * Updates a list of properties on the server
295 | *
296 | * The list of properties must have clark-notation properties for the keys,
297 | * and the actual (string) value for the value. If the value is null, an
298 | * attempt is made to delete the property.
299 | *
300 | * @param string $url
301 | * @param array $properties
302 | * @return void
303 | */
304 | public function propPatch($url, array $properties)
305 | {
306 | $dom = new \DOMDocument('1.0', 'UTF-8');
307 | $dom->formatOutput = true;
308 | $root = $dom->createElementNS('DAV:', 'd:propertyupdate');
309 |
310 | foreach ($properties as $propName => $propValue) {
311 | list(
312 | $namespace,
313 | $elementName
314 | ) = XMLUtil::parseClarkNotation($propName);
315 |
316 | if ($propValue === null) {
317 | $remove = $dom->createElement('d:remove');
318 | $prop = $dom->createElement('d:prop');
319 |
320 | if ($namespace === 'DAV:') {
321 | $element = $dom->createElement('d:'.$elementName);
322 | } else {
323 | $element = $dom->createElementNS($namespace, 'x:'.$elementName);
324 | }
325 |
326 | $root->appendChild( $remove )->appendChild( $prop )->appendChild( $element );
327 | } else {
328 |
329 | $set = $dom->createElement('d:set');
330 | $prop = $dom->createElement('d:prop');
331 |
332 | if ($namespace === 'DAV:') {
333 | $element = $dom->createElement('d:'.$elementName);
334 | } else {
335 | $element = $dom->createElementNS($namespace, 'x:'.$elementName);
336 | }
337 |
338 | if ( $propValue instanceof Property ) {
339 | $propValue->serialize( new Server, $element );
340 | } else {
341 | $element->nodeValue = htmlspecialchars($propValue, ENT_NOQUOTES, 'UTF-8');
342 | }
343 |
344 | $root->appendChild( $set )->appendChild( $prop )->appendChild( $element );
345 | }
346 | }
347 |
348 | $dom->appendChild($root);
349 | $body = $dom->saveXML();
350 |
351 | $url = $this->getAbsoluteUrl($url);
352 | $request = new HTTP\Request('PROPPATCH', $url, [
353 | 'Content-Type' => 'application/xml',
354 | ], $body);
355 | $response = $this->send($request);
356 |
357 | if ((int)$response->getStatus() >= 400) {
358 | throw new Exception('HTTP error: ' . $response->getStatus());
359 | }
360 | }
361 |
362 | /**
363 | * Performs an HTTP options request
364 | *
365 | * This method returns all the features from the 'DAV:' header as an array.
366 | * If there was no DAV header, or no contents this method will return an
367 | * empty array.
368 | *
369 | * @return array
370 | */
371 | public function options()
372 | {
373 | $request = new HTTP\Request('OPTIONS', $this->getAbsoluteUrl(''));
374 | $response = $this->send($request);
375 |
376 | $dav = $response->getHeader('Dav');
377 |
378 | if (!$dav) {
379 | return [];
380 | }
381 |
382 | $features = explode(',', $dav);
383 |
384 | foreach ($features as &$v) {
385 | $v = trim($v);
386 | }
387 |
388 | return $features;
389 | }
390 |
391 | /**
392 | * Performs an actual HTTP request, and returns the result.
393 | *
394 | * If the specified url is relative, it will be expanded based on the base
395 | * url.
396 | *
397 | * The returned array contains 3 keys:
398 | * * body - the response body
399 | * * httpCode - a HTTP code (200, 404, etc)
400 | * * headers - a list of response http headers. The header names have
401 | * been lowercased.
402 | *
403 | * For large uploads, it's highly recommended to specify body as a stream
404 | * resource. You can easily do this by simply passing the result of
405 | * fopen(..., 'r').
406 | *
407 | * This method will throw an exception if an HTTP error was received. Any
408 | * HTTP status code above 399 is considered an error.
409 | *
410 | * Note that it is no longer recommended to use this method, use the send()
411 | * method instead.
412 | *
413 | * @param string $method
414 | * @param string $url
415 | * @param string|resource|null $body
416 | * @param array $headers
417 | * @throws ClientException, in case a curl error occurred.
418 | * @return array
419 | */
420 | public function request($method, $url = '', $body = null, array $headers = [])
421 | {
422 | $response = $this->send(new HTTP\Request($method, $url, $headers, $body));
423 |
424 | return [
425 | 'body' => $response->getBody($asString = true),
426 | 'statusCode' => (int)$response->getStatus(),
427 | 'headers' => array_change_key_case($response->getHeaders()),
428 | ];
429 | }
430 |
431 | /**
432 | * Sends a request to a HTTP server, and returns a response.
433 | *
434 | * Switches request URL for absolute URL
435 | *
436 | * @param RequestInterface $request
437 | * @return ResponseInterface
438 | */
439 | public function send(HTTP\RequestInterface $request)
440 | {
441 | $url = $request->getUrl();
442 |
443 | $absoluteUrl = $this->getAbsoluteUrl($url);
444 |
445 | $request->setUrl($absoluteUrl);
446 |
447 | return parent::send($request);
448 | }
449 |
450 | /**
451 | * Returns the full url based on the given url (which may be relative). All
452 | * urls are expanded based on the base url as given by the server.
453 | *
454 | * @param string $url
455 | * @return string
456 | */
457 | public function getAbsoluteUrl($url)
458 | {
459 | // If the url starts with http:// or https://, the url is already absolute.
460 | if (preg_match('/^http(s?):\/\//', $url)) {
461 | return $url;
462 | }
463 |
464 | // If the url starts with a slash, we must calculate the url based off
465 | // the root of the base url.
466 | if (strpos($url,'/') === 0) {
467 | $parts = parse_url($this->baseUri);
468 | return $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
469 | }
470 |
471 | // Otherwise...
472 | return $this->baseUri . $url;
473 | }
474 |
475 | /**
476 | * Parses a WebDAV multistatus response body
477 | *
478 | * This method returns an array with the following structure
479 | *
480 | * array(
481 | * 'url/to/resource' => array(
482 | * '200' => array(
483 | * '{DAV:}property1' => 'value1',
484 | * '{DAV:}property2' => 'value2',
485 | * ),
486 | * '404' => array(
487 | * '{DAV:}property1' => null,
488 | * '{DAV:}property2' => null,
489 | * ),
490 | * )
491 | * 'url/to/resource2' => array(
492 | * .. etc ..
493 | * )
494 | * )
495 | *
496 | *
497 | * @param string $body xml body
498 | * @return array
499 | */
500 | public function parseMultiStatus($body)
501 | {
502 | try {
503 | $dom = XMLUtil::loadDOMDocument($body);
504 | } catch (Exception\BadRequest $e) {
505 | throw new \InvalidArgumentException('The body passed to parseMultiStatus could not be parsed. Is it really xml?');
506 | }
507 |
508 | $responses = Property\ResponseList::unserialize(
509 | $dom->documentElement,
510 | $this->propertyMap
511 | );
512 |
513 | $result = [];
514 |
515 | foreach ($responses->getResponses() as $response) {
516 | $result[$response->getHref()] = $response->getResponseProperties();
517 | }
518 |
519 | return $result;
520 | }
521 |
522 | public function getAdapter($adapterName, $adapterClass)
523 | {
524 | if (!array_key_exists($adapterName, $this->adapters)) {
525 | $this->registerAdapter($adapterName, $adapterClass);
526 | }
527 |
528 | return $this->adapters[$adapterName];
529 | }
530 |
531 | public function registerAdapter($adapterName, $adapterClass)
532 | {
533 | if (array_key_exists($adapterName, $this->adapters)) {
534 | throw Exceptions\AdapterAlreadyRegisteredException('An adapter by then name "' . $adapterName . '" is already registered.');
535 | }
536 |
537 | $this->adapters[$adapterName] = new $adapterClass($this);
538 |
539 | return $this;
540 | }
541 |
542 | public function registerAdapters($adapters)
543 | {
544 | foreach ($adapters as $adapterName => $adapterClass) {
545 | $this->registerAdapter($adapterName, $adapterClass);
546 | }
547 |
548 | return $this;
549 | }
550 |
551 | public function getCardDAVAdapter()
552 | {
553 | return $this->getAdapter('CardDAV', 'Sabre\\DAVClient\\Adapter\\CardDAVAdapter');
554 | }
555 | }
556 |
--------------------------------------------------------------------------------
/lib/Sabre/DAVClient/RequestBuilder/AddressBookMultiGetRequestBuilder.php:
--------------------------------------------------------------------------------
1 | 'text/xml'];
12 |
13 | protected $method = 'REPORT';
14 |
15 | protected $url;
16 |
17 | public function __construct($url, array $contacts)
18 | {
19 | $this->url = $url;
20 | $this->contacts = $contacts;
21 | }
22 |
23 | public function build()
24 | {
25 | return new HTTP\Request($this->method, $this->url, $this->headers, $this->writeXML());
26 | }
27 |
28 | protected function writeXML()
29 | {
30 | $xml = new \XMLWriter;
31 | $xml->openMemory();
32 | $xml->setIndent(4);
33 | $xml->startDocument('1.0', 'utf-8');
34 | $xml->startElement('a:addressbook-multiget');
35 | $xml->writeAttribute('xmlns:d', 'DAV:');
36 | $xml->writeAttribute('xmlns:a', 'urn:ietf:params:xml:ns:carddav');
37 | $xml->writeElement('d:sync-token');
38 | $xml->startElement('d:prop');
39 | $xml->writeElement('d:getetag');
40 | $xml->writeElement('a:address-data');
41 | $xml->endElement();
42 |
43 | foreach ($this->contacts as $contact) {
44 | $xml->writeElement('d:href', $contact);
45 | }
46 |
47 | $xml->endElement();
48 | $xml->endDocument();
49 |
50 | return $xml->outputMemory();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/Sabre/DAVClient/RequestBuilder/RequestBuilderInterface.php:
--------------------------------------------------------------------------------
1 | 'text/xml', 'Depth' => 1];
12 |
13 | protected $method = 'REPORT';
14 |
15 | protected $url;
16 |
17 | public function __construct($url, $sync_token = null, $sync_level = 1)
18 | {
19 | $this->url = $url;
20 | $this->sync_token = $sync_token;
21 | $this->sync_level = $sync_level;
22 | }
23 |
24 | public function build()
25 | {
26 | return new HTTP\Request($this->method, $this->url, $this->headers, $this->writeXML());
27 | }
28 |
29 | protected function writeXML()
30 | {
31 | $xml = new \XMLWriter;
32 | $xml->openMemory();
33 | $xml->setIndent(4);
34 | $xml->startDocument('1.0', 'utf-8');
35 | $xml->startElement('d:sync-collection');
36 | $xml->writeAttribute('xmlns:d', 'DAV:');
37 | $xml->writeElement('d:sync-token', $this->sync_token);
38 | $xml->writeElement('d:sync-level', $this->sync_level);
39 | $xml->startElement('d:prop');
40 | $xml->writeElement('d:getetag');
41 | $xml->endElement();
42 | $xml->endElement();
43 | $xml->endDocument();
44 |
45 | return $xml->outputMemory();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Sabre/DAVClient/ClientMock.php:
--------------------------------------------------------------------------------
1 | '/',
13 | ));
14 | $this->assertInstanceOf('Sabre\DAVClient\ClientMock', $client);
15 |
16 | }
17 |
18 | /**
19 | * @expectedException InvalidArgumentException
20 | */
21 | function testConstructNoBaseUri() {
22 |
23 | $client = new ClientMock(array());
24 |
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
9 |
10 | Sabre/DAVClient
11 |
12 |
13 |
14 |
15 | ../lib/
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------