PageRenderTime 54ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Sabre/DAV/Client.php

https://github.com/KOLANICH/SabreDAV
PHP | 712 lines | 328 code | 131 blank | 253 comment | 39 complexity | ddaa0a27b3e8524bcd01e4119a9027ed MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. namespace Sabre\DAV;
  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. * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
  12. * @author Evert Pot (http://evertpot.com/)
  13. * @author KOLANICH
  14. * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
  15. *
  16. */
  17. class Client {
  18. protected static $defaultCurlSettings=array(
  19. CURLOPT_RETURNTRANSFER => true,
  20. // Return headers as part of the response
  21. CURLOPT_HEADER => true,
  22. // Automatically follow redirects
  23. CURLOPT_FOLLOWLOCATION => true,
  24. CURLOPT_MAXREDIRS => 5,
  25. /*CURLOPT_SSL_VERIFYHOST =>0,
  26. CURLOPT_SSL_VERIFYPEER =>0,*/
  27. );
  28. /**
  29. * The propertyMap is a key-value array.
  30. *
  31. * If you use the propertyMap, any {DAV:}multistatus responses with the
  32. * properties listed in this array, will automatically be mapped to a
  33. * respective class.
  34. *
  35. * The {DAV:}resourcetype property is automatically added. This maps to
  36. * Sabre\DAV\Property\ResourceType
  37. *
  38. * @var array
  39. */
  40. public $propertyMap = array();
  41. protected $baseUri;
  42. protected $ch=null;
  43. /**
  44. * Basic authentication
  45. */
  46. const AUTH_BASIC = 0b1;
  47. /**
  48. * Digest authentication
  49. */
  50. const AUTH_DIGEST = 0b10;
  51. /**
  52. * Default auth type
  53. */
  54. const AUTH_DEFAULT= 0b11;
  55. /**
  56. * Identity encoding, which basically does not nothing.
  57. */
  58. const ENCODING_IDENTITY = 0b001;
  59. /**
  60. * Deflate encoding
  61. */
  62. const ENCODING_DEFLATE = 0b010;
  63. /**
  64. * Gzip encoding
  65. */
  66. const ENCODING_GZIP = 0b100;
  67. /**
  68. * Sends all encoding headers.
  69. */
  70. const ENCODING_ALL = 0b111;
  71. /**
  72. * Default encoding.
  73. */
  74. const ENCODING_DEFAULT = self::ENCODING_IDENTITY;
  75. /**
  76. * Constructor
  77. *
  78. * Settings are provided through the 'settings' argument. The following
  79. * settings are supported:
  80. *
  81. * * baseUri
  82. * * userName (optional)
  83. * * password (optional)
  84. * * proxy (optional)
  85. * * authType (optional)
  86. * * encoding (optional)
  87. *
  88. * authType must be a bitmap, using self::AUTH_BASIC and
  89. * self::AUTH_DIGEST. If you know which authentication method will be
  90. * used, it's recommended to set it, as it will save a great deal of
  91. * requests to 'discover' this information.
  92. *
  93. * Encoding is a bitmap with one of the ENCODING constants.
  94. *
  95. * @param array $settings
  96. */
  97. public function __construct(array $settings) {
  98. if (!isset($settings['baseUri'])) {
  99. throw new \InvalidArgumentException('A baseUri must be provided');
  100. }
  101. $validSettings = array(
  102. 'baseUri',
  103. );
  104. foreach($validSettings as $validSetting) {
  105. if (isset($settings[$validSetting])) {
  106. $this->$validSetting = $settings[$validSetting];
  107. }
  108. }
  109. $this->propertyMap['{DAV:}resourcetype'] = 'Sabre\\DAV\\Property\\ResourceType';
  110. static::initCurl();
  111. if (isset($settings['encoding'])) {
  112. static::setEncodings($settings['encoding']);
  113. }else{
  114. static::setEncodings(self::ENCODING_DEFAULT);
  115. }
  116. if (isset($settings['proxy'])) {
  117. static::setProxy($settings['proxy']);
  118. }
  119. $authType=isset($settings['authType'])?$settings['authType']:self::AUTH_DEFAULT;
  120. if (isset($settings['userName'])) {
  121. static::setAuth($settings['userName'],$settings['password'],$authType);
  122. }
  123. if (isset($settings['verifyPeer'])) {
  124. $this->setVerifyPeer($settings['verifyPeer']);
  125. }
  126. if (isset($settings['cert'])) {
  127. $this->addTrustedCertificates($settings['cert']);
  128. }
  129. }
  130. public function __destruct() {
  131. if($this->ch)curl_close($this->ch);
  132. }
  133. /**
  134. * Initializes CURL handle
  135. * look for __construct docs
  136. * @param array $settings settings for CURL in format for curlopt_setopt_array
  137. */
  138. protected function initCurl(&$settings=null){
  139. $this->ch=curl_init();
  140. if (!$this->ch) {
  141. throw new Sabre_DAV_Exception('[CURL] unable to initialize curl handle');
  142. }
  143. $curlSettings = static::$defaultCurlSettings;
  144. if (isset($settings)&&is_array($settings)){
  145. $curlSettings+=$settings;
  146. unset($settings);
  147. }
  148. curl_setopt_array($this->ch, $curlSettings);
  149. unset($curlSettings);
  150. }
  151. /**
  152. * Used to set opts to "cURL "
  153. * @param integer $opt curl constant for option
  154. * @param mixed $val value
  155. * @return the same that cURL should return
  156. */
  157. protected function curlSetOpt($optName,$val){
  158. return curl_setopt($this->ch,$optName,$val);
  159. }
  160. /**
  161. * Add trusted root certificates to the webdav client.
  162. *
  163. * @param string $certificatesPath absolute path to a file which contains all trusted certificates
  164. */
  165. public function addTrustedCertificates($certificatesPath) {
  166. if(is_string($certificatesPath)){
  167. if(!file_exists($certificatesPath))throw new Exception('certificates path is not valid');
  168. static::setCertificates($certificatesPath);
  169. }else{
  170. throw new Exception('$certificates must be the absolute path of a file holding one or more certificates to verify the peer with.');
  171. }
  172. }
  173. /**
  174. * Used to set certificates file.
  175. * Not for usage by end user because addTrustedCertificates checks wheither file exist in call time but
  176. * this function will check this requirement during execution curl request.
  177. *
  178. * @param string $certificatesPath
  179. */
  180. protected function setCertificates($certificatesPath){
  181. static::curlSetOpt(CURLOPT_CAINFO,$certificatesPath);
  182. }
  183. /**
  184. * Enables/disables SSL peer verification
  185. *
  186. * @param boolean $shouldVerifyPeer
  187. */
  188. public function setVerifyPeer($shouldVerifyPeer){
  189. static::curlSetOpt(CURLOPT_SSL_VERIFYPEER,$shouldVerifyPeer);
  190. }
  191. /**
  192. * Used to set proxy
  193. *
  194. * @param string $proxyAddr address of proxy in format host:port
  195. */
  196. public function setProxy($proxyAddr) {
  197. static::curlSetOpt(CURLOPT_PROXY,$proxyAddr);
  198. }
  199. /**
  200. * Used to set auth type
  201. *
  202. * @param string $userName
  203. * @param string $password
  204. * @param integer $authType If DIGEST is used, the client makes 1 extra request per request, to get the authentication tokens.
  205. */
  206. public function setAuth($userName='',$password='',$authType=self::AUTH_DEFAULT) {
  207. if ($userName && $authType) {
  208. static::curlSetOpt(CURLOPT_USERPWD,$userName.':'.$password);
  209. }
  210. static::curlSetOpt(CURLOPT_HTTPAUTH,static::convertAuthTypeToInnerFormat($authType));
  211. }
  212. /** converts
  213. * @param number $authType bitwise OR of needed AUTH_* constants of this class
  214. * to format, suitable for CURL
  215. */
  216. protected static function convertAuthTypeToInnerFormat(&$authType){
  217. $curlAuthType = 0;
  218. if ($authType & self::AUTH_BASIC) {
  219. $curlAuthType |= CURLAUTH_BASIC;
  220. }
  221. if ($authType & self::AUTH_DIGEST) {
  222. $curlAuthType |= CURLAUTH_DIGEST;
  223. }
  224. return $curlAuthType;
  225. }
  226. /** converts
  227. * @param number $encodings bitwise OR of needed ENCODING_* constants of this class
  228. * to format, suitable for CURL
  229. */
  230. protected static function convertEncodingsToInnerFormat(&$encodings){
  231. $encodingsList = [];
  232. if ($encodings & self::ENCODING_IDENTITY) {
  233. $encodingsList[] = 'identity';
  234. }
  235. if ($encodings & self::ENCODING_DEFLATE) {
  236. $encodingsList[] = 'deflate';
  237. }
  238. if ($encodings & self::ENCODING_GZIP) {
  239. $encodingsList[] = 'gzip';
  240. }
  241. return implode(',', $encodingsList);
  242. }
  243. /**
  244. * Used to set enconings
  245. *
  246. * @param integer $encodings bitwise OR of needed ENCODING_* constants of this class
  247. */
  248. public function setEncodings($encodings=self::ENCODING_DEFAULT){
  249. static::curlSetOpt(CURLOPT_ENCODING,static::convertEncodingsToInnerFormat($encodings));
  250. }
  251. /**
  252. * Does a PROPFIND request
  253. *
  254. * The list of requested properties must be specified as an array, in clark
  255. * notation.
  256. *
  257. * The returned array will contain a list of filenames as keys, and
  258. * properties as values.
  259. *
  260. * The properties array will contain the list of properties. Only properties
  261. * that are actually returned from the server (without error) will be
  262. * returned, anything else is discarded.
  263. *
  264. * Depth should be either 0 or 1. A depth of 1 will cause a request to be
  265. * made to the server to also return all child resources.
  266. *
  267. * @param string $url
  268. * @param array $properties
  269. * @param int $depth
  270. * @return array
  271. */
  272. public function propFind($url, array $properties, $depth = 0) {
  273. $dom = new \DOMDocument('1.0', 'UTF-8');
  274. $dom->formatOutput = true;
  275. $root = $dom->createElementNS('DAV:', 'd:propfind');
  276. $prop = $dom->createElement('d:prop');
  277. foreach($properties as $property) {
  278. list(
  279. $namespace,
  280. $elementName
  281. ) = XMLUtil::parseClarkNotation($property);
  282. if ($namespace === 'DAV:') {
  283. $element = $dom->createElement('d:'.$elementName);
  284. } else {
  285. $element = $dom->createElementNS($namespace, 'x:'.$elementName);
  286. }
  287. $prop->appendChild( $element );
  288. }
  289. $dom->appendChild($root)->appendChild( $prop );
  290. $body = $dom->saveXML();
  291. $response = $this->request('PROPFIND', $url, $body, array(
  292. 'Depth' => $depth,
  293. 'Content-Type' => 'application/xml'
  294. ));
  295. $result = $this->parseMultiStatus($response['body']);
  296. // If depth was 0, we only return the top item
  297. if ($depth===0) {
  298. reset($result);
  299. $result = current($result);
  300. return isset($result[200])?$result[200]:array();
  301. }
  302. $newResult = array();
  303. foreach($result as $href => $statusList) {
  304. $newResult[$href] = isset($statusList[200])?$statusList[200]:array();
  305. }
  306. return $newResult;
  307. }
  308. /**
  309. * Updates a list of properties on the server
  310. *
  311. * The list of properties must have clark-notation properties for the keys,
  312. * and the actual (string) value for the value. If the value is null, an
  313. * attempt is made to delete the property.
  314. *
  315. * @param string $url
  316. * @param array $properties
  317. * @return void
  318. */
  319. public function propPatch($url, array $properties) {
  320. $dom = new \DOMDocument('1.0', 'UTF-8');
  321. $dom->formatOutput = true;
  322. $root = $dom->createElementNS('DAV:', 'd:propertyupdate');
  323. foreach($properties as $propName => $propValue) {
  324. list(
  325. $namespace,
  326. $elementName
  327. ) = XMLUtil::parseClarkNotation($propName);
  328. if ($propValue === null) {
  329. $remove = $dom->createElement('d:remove');
  330. $prop = $dom->createElement('d:prop');
  331. if ($namespace === 'DAV:') {
  332. $element = $dom->createElement('d:'.$elementName);
  333. } else {
  334. $element = $dom->createElementNS($namespace, 'x:'.$elementName);
  335. }
  336. $root->appendChild( $remove )->appendChild( $prop )->appendChild( $element );
  337. } else {
  338. $set = $dom->createElement('d:set');
  339. $prop = $dom->createElement('d:prop');
  340. if ($namespace === 'DAV:') {
  341. $element = $dom->createElement('d:'.$elementName);
  342. } else {
  343. $element = $dom->createElementNS($namespace, 'x:'.$elementName);
  344. }
  345. if ( $propValue instanceof Property ) {
  346. $propValue->serialize( new Server, $element );
  347. } else {
  348. $element->nodeValue = htmlspecialchars($propValue, ENT_NOQUOTES, 'UTF-8');
  349. }
  350. $root->appendChild( $set )->appendChild( $prop )->appendChild( $element );
  351. }
  352. }
  353. $dom->appendChild($root);
  354. $body = $dom->saveXML();
  355. $this->request('PROPPATCH', $url, $body, array(
  356. 'Content-Type' => 'application/xml'
  357. ));
  358. }
  359. /**
  360. * Performs an HTTP options request
  361. *
  362. * This method returns all the features from the 'DAV:' header as an array.
  363. * If there was no DAV header, or no contents this method will return an
  364. * empty array.
  365. *
  366. * @return array
  367. */
  368. public function options() {
  369. $result = $this->request('OPTIONS');
  370. if (!isset($result['headers']['dav'])) {
  371. return array();
  372. }
  373. $features = explode(',', $result['headers']['dav']);
  374. foreach($features as &$v) {
  375. $v = trim($v);
  376. }
  377. return $features;
  378. }
  379. /**
  380. * Performs an actual HTTP request, and returns the result.
  381. *
  382. * If the specified url is relative, it will be expanded based on the base
  383. * url.
  384. *
  385. * The returned array contains 3 keys:
  386. * * body - the response body
  387. * * httpCode - a HTTP code (200, 404, etc)
  388. * * headers - a list of response http headers. The header names have
  389. * been lowercased.
  390. *
  391. * For large uploads, it's highly recommended to specify body as a stream
  392. * resource. You can easily do this by simply passing the result of
  393. * fopen(..., 'r').
  394. *
  395. * @param string $method
  396. * @param string $url
  397. * @param string|resource|null $body
  398. * @param array $headers
  399. * @return array
  400. */
  401. public function request($method, $url = '', $body = null, $headers = array()) {
  402. $url = $this->getAbsoluteUrl($url);
  403. $curlSettings = array(
  404. CURLOPT_URL => $url,
  405. );
  406. if (is_null($body)) {
  407. $curlSettings[CURLOPT_POSTFIELDS] = '';
  408. } elseif (is_string($body)||is_array($body)) {
  409. $curlSettings[CURLOPT_POSTFIELDS] = $body;
  410. } elseif (is_resource($body)) {
  411. // This needs to be set to PUT, regardless of the actual method.
  412. $curlSettings[CURLOPT_PUT] = true;
  413. $curlSettings[CURLOPT_INFILE] = $body;
  414. }
  415. switch ($method) {
  416. case 'HEAD' :
  417. // do not read body with HEAD requests (this is necessary because cURL does not ignore the body with HEAD
  418. // requests when the Content-Length header is given - which in turn is perfectly valid according to HTTP
  419. // specs...) cURL does unfortunately return an error in this case ("transfer closed transfer closed with
  420. // ... bytes remaining to read") this can be circumvented by explicitly telling cURL to ignore the
  421. // response body
  422. $curlSettings[CURLOPT_NOBODY] = true;
  423. $curlSettings[CURLOPT_CUSTOMREQUEST] = 'HEAD';
  424. break;
  425. default:
  426. $curlSettings[CURLOPT_CUSTOMREQUEST] = $method;
  427. break;
  428. }
  429. // Adding HTTP headers
  430. $nHeaders = array();
  431. foreach($headers as $key=>$value) {
  432. $nHeaders[] = $key . ': ' . $value;
  433. }
  434. $curlSettings[CURLOPT_HTTPHEADER] = $nHeaders;
  435. list(
  436. $response,
  437. $curlInfo,
  438. $curlErrNo,
  439. $curlError
  440. ) = $this->curlRequest($curlSettings);
  441. $headerBlob = substr($response, 0, $curlInfo['header_size']);
  442. $response = substr($response, $curlInfo['header_size']);
  443. // In the case of 100 Continue, or redirects we'll have multiple lists
  444. // of headers for each separate HTTP response. We can easily split this
  445. // because they are separated by \r\n\r\n
  446. $headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n"));
  447. // We only care about the last set of headers
  448. $headerBlob = $headerBlob[count($headerBlob)-1];
  449. // Splitting headers
  450. $headerBlob = explode("\r\n", $headerBlob);
  451. $headers = array();
  452. foreach($headerBlob as $header) {
  453. $parts = explode(':', $header, 2);
  454. if (count($parts)==2) {
  455. $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
  456. }
  457. }
  458. $response = array(
  459. 'body' => $response,
  460. 'statusCode' => $curlInfo['http_code'],
  461. 'headers' => $headers
  462. );
  463. if ($curlErrNo) {
  464. throw new Exception('[CURL] Error while making request: ' . $curlError . ' (error code: ' . $curlErrNo . ')');
  465. }
  466. if ($response['statusCode']>=400) {
  467. switch ($response['statusCode']) {
  468. case 400 :
  469. throw new Exception\BadRequest('Bad request');
  470. case 401 :
  471. throw new Exception\NotAuthenticated('Not authenticated');
  472. case 402 :
  473. throw new Exception\PaymentRequired('Payment required');
  474. case 403 :
  475. throw new Exception\Forbidden('Forbidden');
  476. case 404:
  477. throw new Exception\NotFound('Resource not found.');
  478. case 405 :
  479. throw new Exception\MethodNotAllowed('Method not allowed');
  480. case 409 :
  481. throw new Exception\Conflict('Conflict');
  482. case 412 :
  483. throw new Exception\PreconditionFailed('Precondition failed');
  484. case 416 :
  485. throw new Exception\RequestedRangeNotSatisfiable('Requested Range Not Satisfiable');
  486. case 500 :
  487. throw new Exception('Internal server error');
  488. case 501 :
  489. throw new Exception\NotImplemented('Not Implemented');
  490. case 507 :
  491. throw new Exception\InsufficientStorage('Insufficient storage');
  492. default:
  493. throw new Exception('HTTP error response. (errorcode ' . $response['statusCode'] . ')');
  494. }
  495. }
  496. return $response;
  497. }
  498. /**
  499. * Wrapper for all curl functions.
  500. *
  501. * The only reason this was split out in a separate method, is so it
  502. * becomes easier to unittest.
  503. *
  504. * @param string $url
  505. * @param array $settings
  506. * @return array
  507. */
  508. // @codeCoverageIgnoreStart
  509. protected function curlRequest($settings) {
  510. curl_setopt_array($this->ch, $settings);
  511. return array(
  512. curl_exec($this->ch),
  513. curl_getinfo($this->ch),
  514. curl_errno($this->ch),
  515. curl_error($this->ch)
  516. );
  517. }
  518. // @codeCoverageIgnoreEnd
  519. /**
  520. * Returns the full url based on the given url (which may be relative). All
  521. * urls are expanded based on the base url as given by the server.
  522. *
  523. * @param string $url
  524. * @return string
  525. */
  526. protected function getAbsoluteUrl($url) {
  527. // If the url starts with http:// or https://, the url is already absolute.
  528. if (preg_match('/^http(s?):\/\//', $url)) {
  529. return $url;
  530. }
  531. // If the url starts with a slash, we must calculate the url based off
  532. // the root of the base url.
  533. if (strpos($url,'/') === 0) {
  534. $parts = parse_url($this->baseUri);
  535. return $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
  536. }
  537. // Otherwise...
  538. return $this->baseUri . $url;
  539. }
  540. /**
  541. * Parses a WebDAV multistatus response body
  542. *
  543. * This method returns an array with the following structure
  544. *
  545. * array(
  546. * 'url/to/resource' => array(
  547. * '200' => array(
  548. * '{DAV:}property1' => 'value1',
  549. * '{DAV:}property2' => 'value2',
  550. * ),
  551. * '404' => array(
  552. * '{DAV:}property1' => null,
  553. * '{DAV:}property2' => null,
  554. * ),
  555. * )
  556. * 'url/to/resource2' => array(
  557. * .. etc ..
  558. * )
  559. * )
  560. *
  561. *
  562. * @param string $body xml body
  563. * @return array
  564. */
  565. public function parseMultiStatus($body) {
  566. try {
  567. $dom = XMLUtil::loadDOMDocument($body);
  568. } catch (Exception\BadRequest $e) {
  569. throw new \InvalidArgumentException('The body passed to parseMultiStatus could not be parsed. Is it really xml?');
  570. }
  571. $responses = Property\ResponseList::unserialize(
  572. $dom->documentElement,
  573. $this->propertyMap
  574. );
  575. $result = array();
  576. foreach($responses->getResponses() as $response) {
  577. $result[$response->getHref()] = $response->getResponseProperties();
  578. }
  579. return $result;
  580. }
  581. }