├── .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 | --------------------------------------------------------------------------------