PageRenderTime 133ms CodeModel.GetById 39ms RepoModel.GetById 21ms app.codeStats 1ms

/lib/webdavlib.php

https://bitbucket.org/moodle/moodle
PHP | 1754 lines | 934 code | 157 blank | 663 comment | 189 complexity | 94fc805f97c9532b5616e51420d7c004 MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * A Moodle-modified WebDAV client, based on
  18. * webdav_client v0.1.5, a php based webdav client class.
  19. * class webdav client. a php based nearly RFC 2518 conforming client.
  20. *
  21. * This class implements methods to get access to an webdav server.
  22. * Most of the methods are returning boolean false on error, an integer status (http response status) on success
  23. * or an array in case of a multistatus response (207) from the webdav server. Look at the code which keys are used in arrays.
  24. * It's your responsibility to handle the webdav server responses in an proper manner.
  25. * Please notice that all Filenames coming from or going to the webdav server should be UTF-8 encoded (see RFC 2518).
  26. * This class tries to convert all you filenames into utf-8 when it's needed.
  27. *
  28. * Moodle modifications:
  29. * * Moodle 3.4: Add support for OAuth 2 bearer token-based authentication
  30. *
  31. * @package moodlecore
  32. * @author Christian Juerges <christian.juerges@xwave.ch>, Xwave GmbH, Josefstr. 92, 8005 Zuerich - Switzerland
  33. * @copyright (C) 2003/2004, Christian Juerges
  34. * @license http://opensource.org/licenses/lgpl-license.php GNU Lesser General Public License
  35. */
  36. class webdav_client {
  37. /**#@+
  38. * @access private
  39. * @var string
  40. */
  41. private $_debug = false;
  42. private $sock;
  43. private $_server;
  44. private $_protocol = 'HTTP/1.1';
  45. private $_port = 80;
  46. private $_socket = '';
  47. private $_path ='/';
  48. private $_auth = false;
  49. private $_user;
  50. private $_pass;
  51. private $_socket_timeout = 5;
  52. private $_errno;
  53. private $_errstr;
  54. private $_user_agent = 'Moodle WebDav Client';
  55. private $_crlf = "\r\n";
  56. private $_req;
  57. private $_resp_status;
  58. private $_parser;
  59. private $_parserid;
  60. private $_xmltree;
  61. private $_tree;
  62. private $_ls = array();
  63. private $_ls_ref;
  64. private $_ls_ref_cdata;
  65. private $_delete = array();
  66. private $_delete_ref;
  67. private $_delete_ref_cdata;
  68. private $_lock = array();
  69. private $_lock_ref;
  70. private $_lock_rec_cdata;
  71. private $_null = NULL;
  72. private $_header='';
  73. private $_body='';
  74. private $_connection_closed = false;
  75. private $_maxheaderlenth = 65536;
  76. private $_digestchallenge = null;
  77. private $_cnonce = '';
  78. private $_nc = 0;
  79. /**
  80. * OAuth token used for bearer auth.
  81. * @var string
  82. */
  83. private $oauthtoken;
  84. /**#@-*/
  85. /**
  86. * Constructor - Initialise class variables
  87. * @param string $server Hostname of the server to connect to
  88. * @param string $user Username (for basic/digest auth, see $auth)
  89. * @param string $pass Password (for basic/digest auth, see $auth)
  90. * @param bool $auth Authentication type; one of ['basic', 'digest', 'bearer']
  91. * @param string $socket Used protocol for fsockopen, usually: '' (empty) or 'ssl://'
  92. * @param string $oauthtoken OAuth 2 bearer token (for bearer auth, see $auth)
  93. */
  94. public function __construct($server = '', $user = '', $pass = '', $auth = false, $socket = '', $oauthtoken = '') {
  95. if (!empty($server)) {
  96. $this->_server = $server;
  97. }
  98. if (!empty($user) && !empty($pass)) {
  99. $this->user = $user;
  100. $this->pass = $pass;
  101. }
  102. $this->_auth = $auth;
  103. $this->_socket = $socket;
  104. if ($auth == 'bearer') {
  105. $this->oauthtoken = $oauthtoken;
  106. }
  107. }
  108. public function __set($key, $value) {
  109. $property = '_' . $key;
  110. $this->$property = $value;
  111. }
  112. /**
  113. * Set which HTTP protocol will be used.
  114. * Value 1 defines that HTTP/1.1 should be used (Keeps Connection to webdav server alive).
  115. * Otherwise HTTP/1.0 will be used.
  116. * @param int version
  117. */
  118. function set_protocol($version) {
  119. if ($version == 1) {
  120. $this->_protocol = 'HTTP/1.1';
  121. } else {
  122. $this->_protocol = 'HTTP/1.0';
  123. }
  124. }
  125. /**
  126. * Convert ISO 8601 Date and Time Profile used in RFC 2518 to an unix timestamp.
  127. * @access private
  128. * @param string iso8601
  129. * @return unixtimestamp on sucess. Otherwise false.
  130. */
  131. function iso8601totime($iso8601) {
  132. /*
  133. date-time = full-date "T" full-time
  134. full-date = date-fullyear "-" date-month "-" date-mday
  135. full-time = partial-time time-offset
  136. date-fullyear = 4DIGIT
  137. date-month = 2DIGIT ; 01-12
  138. date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
  139. month/year
  140. time-hour = 2DIGIT ; 00-23
  141. time-minute = 2DIGIT ; 00-59
  142. time-second = 2DIGIT ; 00-59, 00-60 based on leap second rules
  143. time-secfrac = "." 1*DIGIT
  144. time-numoffset = ("+" / "-") time-hour ":" time-minute
  145. time-offset = "Z" / time-numoffset
  146. partial-time = time-hour ":" time-minute ":" time-second
  147. [time-secfrac]
  148. */
  149. $regs = array();
  150. /* [1] [2] [3] [4] [5] [6] */
  151. if (preg_match('/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z$/', $iso8601, $regs)) {
  152. return mktime($regs[4],$regs[5], $regs[6], $regs[2], $regs[3], $regs[1]);
  153. }
  154. // to be done: regex for partial-time...apache webdav mod never returns partial-time
  155. return false;
  156. }
  157. /**
  158. * Open's a socket to a webdav server
  159. * @return bool true on success. Otherwise false.
  160. */
  161. function open() {
  162. // let's try to open a socket
  163. $this->_error_log('open a socket connection');
  164. $this->sock = fsockopen($this->_socket . $this->_server, $this->_port, $this->_errno, $this->_errstr, $this->_socket_timeout);
  165. core_php_time_limit::raise(30);
  166. if (is_resource($this->sock)) {
  167. socket_set_blocking($this->sock, true);
  168. $this->_connection_closed = false;
  169. $this->_error_log('socket is open: ' . $this->sock);
  170. return true;
  171. } else {
  172. $this->_error_log("$this->_errstr ($this->_errno)\n");
  173. return false;
  174. }
  175. }
  176. /**
  177. * Closes an open socket.
  178. */
  179. function close() {
  180. $this->_error_log('closing socket ' . $this->sock);
  181. $this->_connection_closed = true;
  182. fclose($this->sock);
  183. }
  184. /**
  185. * Check's if server is a webdav compliant server.
  186. * True if server returns a DAV Element in Header and when
  187. * schema 1,2 is supported.
  188. * @return bool true if server is webdav server. Otherwise false.
  189. */
  190. function check_webdav() {
  191. $resp = $this->options();
  192. if (!$resp) {
  193. return false;
  194. }
  195. $this->_error_log($resp['header']['DAV']);
  196. // check schema
  197. if (preg_match('/1,2/', $resp['header']['DAV'])) {
  198. return true;
  199. }
  200. // otherwise return false
  201. return false;
  202. }
  203. /**
  204. * Get options from webdav server.
  205. * @return array with all header fields returned from webdav server. false if server does not speak http.
  206. */
  207. function options() {
  208. $this->header_unset();
  209. $this->create_basic_request('OPTIONS');
  210. $this->send_request();
  211. $this->get_respond();
  212. $response = $this->process_respond();
  213. // validate the response ...
  214. // check http-version
  215. if ($response['status']['http-version'] == 'HTTP/1.1' ||
  216. $response['status']['http-version'] == 'HTTP/1.0') {
  217. return $response;
  218. }
  219. $this->_error_log('Response was not even http');
  220. return false;
  221. }
  222. /**
  223. * Public method mkcol
  224. *
  225. * Creates a new collection/directory on a webdav server
  226. * @param string path
  227. * @return int status code received as response from webdav server (see rfc 2518)
  228. */
  229. function mkcol($path) {
  230. $this->_path = $this->translate_uri($path);
  231. $this->header_unset();
  232. $this->create_basic_request('MKCOL');
  233. $this->send_request();
  234. $this->get_respond();
  235. $response = $this->process_respond();
  236. // validate the response ...
  237. // check http-version
  238. $http_version = $response['status']['http-version'];
  239. if ($http_version == 'HTTP/1.1' || $http_version == 'HTTP/1.0') {
  240. /** seems to be http ... proceed
  241. * just return what server gave us
  242. * rfc 2518 says:
  243. * 201 (Created) - The collection or structured resource was created in its entirety.
  244. * 403 (Forbidden) - This indicates at least one of two conditions:
  245. * 1) the server does not allow the creation of collections at the given location in its namespace, or
  246. * 2) the parent collection of the Request-URI exists but cannot accept members.
  247. * 405 (Method Not Allowed) - MKCOL can only be executed on a deleted/non-existent resource.
  248. * 409 (Conflict) - A collection cannot be made at the Request-URI until one or more intermediate
  249. * collections have been created.
  250. * 415 (Unsupported Media Type)- The server does not support the request type of the body.
  251. * 507 (Insufficient Storage) - The resource does not have sufficient space to record the state of the
  252. * resource after the execution of this method.
  253. */
  254. return $response['status']['status-code'];
  255. }
  256. }
  257. /**
  258. * Public method get
  259. *
  260. * Gets a file from a webdav collection.
  261. * @param string $path the path to the file on the webdav server
  262. * @param string &$buffer the buffer to store the data in
  263. * @param resource $fp optional if included, the data is written directly to this resource and not to the buffer
  264. * @return string|bool status code and &$buffer (by reference) with response data from server on success. False on error.
  265. */
  266. function get($path, &$buffer, $fp = null) {
  267. $this->_path = $this->translate_uri($path);
  268. $this->header_unset();
  269. $this->create_basic_request('GET');
  270. $this->send_request();
  271. $this->get_respond($fp);
  272. $response = $this->process_respond();
  273. $http_version = $response['status']['http-version'];
  274. // validate the response
  275. // check http-version
  276. if ($http_version == 'HTTP/1.1' || $http_version == 'HTTP/1.0') {
  277. // seems to be http ... proceed
  278. // We expect a 200 code
  279. if ($response['status']['status-code'] == 200 ) {
  280. if (!is_null($fp)) {
  281. $stat = fstat($fp);
  282. $this->_error_log('file created with ' . $stat['size'] . ' bytes.');
  283. } else {
  284. $this->_error_log('returning buffer with ' . strlen($response['body']) . ' bytes.');
  285. $buffer = $response['body'];
  286. }
  287. }
  288. return $response['status']['status-code'];
  289. }
  290. // ups: no http status was returned ?
  291. return false;
  292. }
  293. /**
  294. * Public method put
  295. *
  296. * Puts a file into a collection.
  297. * Data is putted as one chunk!
  298. * @param string path, string data
  299. * @return int status-code read from webdavserver. False on error.
  300. */
  301. function put($path, $data ) {
  302. $this->_path = $this->translate_uri($path);
  303. $this->header_unset();
  304. $this->create_basic_request('PUT');
  305. // add more needed header information ...
  306. $this->header_add('Content-length: ' . strlen($data));
  307. $this->header_add('Content-type: application/octet-stream');
  308. // send header
  309. $this->send_request();
  310. // send the rest (data)
  311. fputs($this->sock, $data);
  312. $this->get_respond();
  313. $response = $this->process_respond();
  314. // validate the response
  315. // check http-version
  316. if ($response['status']['http-version'] == 'HTTP/1.1' ||
  317. $response['status']['http-version'] == 'HTTP/1.0') {
  318. // seems to be http ... proceed
  319. // We expect a 200 or 204 status code
  320. // see rfc 2068 - 9.6 PUT...
  321. // print 'http ok<br>';
  322. return $response['status']['status-code'];
  323. }
  324. // ups: no http status was returned ?
  325. return false;
  326. }
  327. /**
  328. * Public method put_file
  329. *
  330. * Read a file as stream and puts it chunk by chunk into webdav server collection.
  331. *
  332. * Look at php documenation for legal filenames with fopen();
  333. * The filename will be translated into utf-8 if not allready in utf-8.
  334. *
  335. * @param string targetpath, string filename
  336. * @return int status code. False on error.
  337. */
  338. function put_file($path, $filename) {
  339. // try to open the file ...
  340. $handle = @fopen ($filename, 'r');
  341. if ($handle) {
  342. // $this->sock = pfsockopen ($this->_server, $this->_port, $this->_errno, $this->_errstr, $this->_socket_timeout);
  343. $this->_path = $this->translate_uri($path);
  344. $this->header_unset();
  345. $this->create_basic_request('PUT');
  346. // add more needed header information ...
  347. $this->header_add('Content-length: ' . filesize($filename));
  348. $this->header_add('Content-type: application/octet-stream');
  349. // send header
  350. $this->send_request();
  351. while (!feof($handle)) {
  352. fputs($this->sock,fgets($handle,4096));
  353. }
  354. fclose($handle);
  355. $this->get_respond();
  356. $response = $this->process_respond();
  357. // validate the response
  358. // check http-version
  359. if ($response['status']['http-version'] == 'HTTP/1.1' ||
  360. $response['status']['http-version'] == 'HTTP/1.0') {
  361. // seems to be http ... proceed
  362. // We expect a 200 or 204 status code
  363. // see rfc 2068 - 9.6 PUT...
  364. // print 'http ok<br>';
  365. return $response['status']['status-code'];
  366. }
  367. // ups: no http status was returned ?
  368. return false;
  369. } else {
  370. $this->_error_log('put_file: could not open ' . $filename);
  371. return false;
  372. }
  373. }
  374. /**
  375. * Public method get_file
  376. *
  377. * Gets a file from a collection into local filesystem.
  378. *
  379. * fopen() is used.
  380. * @param string $srcpath
  381. * @param string $localpath
  382. * @return bool true on success. false on error.
  383. */
  384. function get_file($srcpath, $localpath) {
  385. $localpath = $this->utf_decode_path($localpath);
  386. $handle = fopen($localpath, 'wb');
  387. if ($handle) {
  388. $unused = '';
  389. $ret = $this->get($srcpath, $unused, $handle);
  390. fclose($handle);
  391. if ($ret) {
  392. return true;
  393. }
  394. }
  395. return false;
  396. }
  397. /**
  398. * Public method copy_file
  399. *
  400. * Copies a file on a webdav server
  401. *
  402. * Duplicates a file on the webdav server (serverside).
  403. * All work is done on the webdav server. If you set param overwrite as true,
  404. * the target will be overwritten.
  405. *
  406. * @param string src_path, string dest_path, bool overwrite
  407. * @return int status code (look at rfc 2518). false on error.
  408. */
  409. function copy_file($src_path, $dst_path, $overwrite) {
  410. $this->_path = $this->translate_uri($src_path);
  411. $this->header_unset();
  412. $this->create_basic_request('COPY');
  413. $this->header_add(sprintf('Destination: http://%s%s', $this->_server, $this->translate_uri($dst_path)));
  414. if ($overwrite) {
  415. $this->header_add('Overwrite: T');
  416. } else {
  417. $this->header_add('Overwrite: F');
  418. }
  419. $this->header_add('');
  420. $this->send_request();
  421. $this->get_respond();
  422. $response = $this->process_respond();
  423. // validate the response ...
  424. // check http-version
  425. if ($response['status']['http-version'] == 'HTTP/1.1' ||
  426. $response['status']['http-version'] == 'HTTP/1.0') {
  427. /* seems to be http ... proceed
  428. just return what server gave us (as defined in rfc 2518) :
  429. 201 (Created) - The source resource was successfully copied. The copy operation resulted in the creation of a new resource.
  430. 204 (No Content) - The source resource was successfully copied to a pre-existing destination resource.
  431. 403 (Forbidden) - The source and destination URIs are the same.
  432. 409 (Conflict) - A resource cannot be created at the destination until one or more intermediate collections have been created.
  433. 412 (Precondition Failed) - The server was unable to maintain the liveness of the properties listed in the propertybehavior XML element
  434. or the Overwrite header is "F" and the state of the destination resource is non-null.
  435. 423 (Locked) - The destination resource was locked.
  436. 502 (Bad Gateway) - This may occur when the destination is on another server and the destination server refuses to accept the resource.
  437. 507 (Insufficient Storage) - The destination resource does not have sufficient space to record the state of the resource after the
  438. execution of this method.
  439. */
  440. return $response['status']['status-code'];
  441. }
  442. return false;
  443. }
  444. /**
  445. * Public method copy_coll
  446. *
  447. * Copies a collection on a webdav server
  448. *
  449. * Duplicates a collection on the webdav server (serverside).
  450. * All work is done on the webdav server. If you set param overwrite as true,
  451. * the target will be overwritten.
  452. *
  453. * @param string src_path, string dest_path, bool overwrite
  454. * @return int status code (look at rfc 2518). false on error.
  455. */
  456. function copy_coll($src_path, $dst_path, $overwrite) {
  457. $this->_path = $this->translate_uri($src_path);
  458. $this->header_unset();
  459. $this->create_basic_request('COPY');
  460. $this->header_add(sprintf('Destination: http://%s%s', $this->_server, $this->translate_uri($dst_path)));
  461. $this->header_add('Depth: Infinity');
  462. $xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n";
  463. $xml .= "<d:propertybehavior xmlns:d=\"DAV:\">\r\n";
  464. $xml .= " <d:keepalive>*</d:keepalive>\r\n";
  465. $xml .= "</d:propertybehavior>\r\n";
  466. $this->header_add('Content-length: ' . strlen($xml));
  467. $this->header_add('Content-type: application/xml');
  468. $this->send_request();
  469. // send also xml
  470. fputs($this->sock, $xml);
  471. $this->get_respond();
  472. $response = $this->process_respond();
  473. // validate the response ...
  474. // check http-version
  475. if ($response['status']['http-version'] == 'HTTP/1.1' ||
  476. $response['status']['http-version'] == 'HTTP/1.0') {
  477. /* seems to be http ... proceed
  478. just return what server gave us (as defined in rfc 2518) :
  479. 201 (Created) - The source resource was successfully copied. The copy operation resulted in the creation of a new resource.
  480. 204 (No Content) - The source resource was successfully copied to a pre-existing destination resource.
  481. 403 (Forbidden) - The source and destination URIs are the same.
  482. 409 (Conflict) - A resource cannot be created at the destination until one or more intermediate collections have been created.
  483. 412 (Precondition Failed) - The server was unable to maintain the liveness of the properties listed in the propertybehavior XML element
  484. or the Overwrite header is "F" and the state of the destination resource is non-null.
  485. 423 (Locked) - The destination resource was locked.
  486. 502 (Bad Gateway) - This may occur when the destination is on another server and the destination server refuses to accept the resource.
  487. 507 (Insufficient Storage) - The destination resource does not have sufficient space to record the state of the resource after the
  488. execution of this method.
  489. */
  490. return $response['status']['status-code'];
  491. }
  492. return false;
  493. }
  494. /**
  495. * Public method move
  496. *
  497. * Moves a file or collection on webdav server (serverside)
  498. *
  499. * If you set param overwrite as true, the target will be overwritten.
  500. *
  501. * @param string src_path, string dest_path, bool overwrite
  502. * @return int status code (look at rfc 2518). false on error.
  503. */
  504. // --------------------------------------------------------------------------
  505. // public method move
  506. // move/rename a file/collection on webdav server
  507. function move($src_path,$dst_path, $overwrite) {
  508. $this->_path = $this->translate_uri($src_path);
  509. $this->header_unset();
  510. $this->create_basic_request('MOVE');
  511. $this->header_add(sprintf('Destination: http://%s%s', $this->_server, $this->translate_uri($dst_path)));
  512. if ($overwrite) {
  513. $this->header_add('Overwrite: T');
  514. } else {
  515. $this->header_add('Overwrite: F');
  516. }
  517. $this->header_add('');
  518. $this->send_request();
  519. $this->get_respond();
  520. $response = $this->process_respond();
  521. // validate the response ...
  522. // check http-version
  523. if ($response['status']['http-version'] == 'HTTP/1.1' ||
  524. $response['status']['http-version'] == 'HTTP/1.0') {
  525. /* seems to be http ... proceed
  526. just return what server gave us (as defined in rfc 2518) :
  527. 201 (Created) - The source resource was successfully moved, and a new resource was created at the destination.
  528. 204 (No Content) - The source resource was successfully moved to a pre-existing destination resource.
  529. 403 (Forbidden) - The source and destination URIs are the same.
  530. 409 (Conflict) - A resource cannot be created at the destination until one or more intermediate collections have been created.
  531. 412 (Precondition Failed) - The server was unable to maintain the liveness of the properties listed in the propertybehavior XML element
  532. or the Overwrite header is "F" and the state of the destination resource is non-null.
  533. 423 (Locked) - The source or the destination resource was locked.
  534. 502 (Bad Gateway) - This may occur when the destination is on another server and the destination server refuses to accept the resource.
  535. 201 (Created) - The collection or structured resource was created in its entirety.
  536. 403 (Forbidden) - This indicates at least one of two conditions: 1) the server does not allow the creation of collections at the given
  537. location in its namespace, or 2) the parent collection of the Request-URI exists but cannot accept members.
  538. 405 (Method Not Allowed) - MKCOL can only be executed on a deleted/non-existent resource.
  539. 409 (Conflict) - A collection cannot be made at the Request-URI until one or more intermediate collections have been created.
  540. 415 (Unsupported Media Type)- The server does not support the request type of the body.
  541. 507 (Insufficient Storage) - The resource does not have sufficient space to record the state of the resource after the execution of this method.
  542. */
  543. return $response['status']['status-code'];
  544. }
  545. return false;
  546. }
  547. /**
  548. * Public method lock
  549. *
  550. * Locks a file or collection.
  551. *
  552. * Lock uses this->_user as lock owner.
  553. *
  554. * @param string path
  555. * @return int status code (look at rfc 2518). false on error.
  556. */
  557. function lock($path) {
  558. $this->_path = $this->translate_uri($path);
  559. $this->header_unset();
  560. $this->create_basic_request('LOCK');
  561. $this->header_add('Timeout: Infinite');
  562. $this->header_add('Content-type: text/xml');
  563. // create the xml request ...
  564. $xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n";
  565. $xml .= "<D:lockinfo xmlns:D='DAV:'\r\n>";
  566. $xml .= " <D:lockscope><D:exclusive/></D:lockscope>\r\n";
  567. $xml .= " <D:locktype><D:write/></D:locktype>\r\n";
  568. $xml .= " <D:owner>\r\n";
  569. $xml .= " <D:href>".($this->_user)."</D:href>\r\n";
  570. $xml .= " </D:owner>\r\n";
  571. $xml .= "</D:lockinfo>\r\n";
  572. $this->header_add('Content-length: ' . strlen($xml));
  573. $this->send_request();
  574. // send also xml
  575. fputs($this->sock, $xml);
  576. $this->get_respond();
  577. $response = $this->process_respond();
  578. // validate the response ... (only basic validation)
  579. // check http-version
  580. if ($response['status']['http-version'] == 'HTTP/1.1' ||
  581. $response['status']['http-version'] == 'HTTP/1.0') {
  582. /* seems to be http ... proceed
  583. rfc 2518 says:
  584. 200 (OK) - The lock request succeeded and the value of the lockdiscovery property is included in the body.
  585. 412 (Precondition Failed) - The included lock token was not enforceable on this resource or the server could not satisfy the
  586. request in the lockinfo XML element.
  587. 423 (Locked) - The resource is locked, so the method has been rejected.
  588. */
  589. switch($response['status']['status-code']) {
  590. case 200:
  591. // collection was successfully locked... see xml response to get lock token...
  592. if (strcmp($response['header']['Content-Type'], 'text/xml; charset="utf-8"') == 0) {
  593. // ok let's get the content of the xml stuff
  594. $this->_parser = xml_parser_create_ns();
  595. $this->_parserid = (int) $this->_parser;
  596. // forget old data...
  597. unset($this->_lock[$this->_parserid]);
  598. unset($this->_xmltree[$this->_parserid]);
  599. xml_parser_set_option($this->_parser,XML_OPTION_SKIP_WHITE,0);
  600. xml_parser_set_option($this->_parser,XML_OPTION_CASE_FOLDING,0);
  601. xml_set_object($this->_parser, $this);
  602. xml_set_element_handler($this->_parser, "_lock_startElement", "_endElement");
  603. xml_set_character_data_handler($this->_parser, "_lock_cdata");
  604. if (!xml_parse($this->_parser, $response['body'])) {
  605. die(sprintf("XML error: %s at line %d",
  606. xml_error_string(xml_get_error_code($this->_parser)),
  607. xml_get_current_line_number($this->_parser)));
  608. }
  609. // Free resources
  610. xml_parser_free($this->_parser);
  611. // add status code to array
  612. $this->_lock[$this->_parserid]['status'] = 200;
  613. return $this->_lock[$this->_parserid];
  614. } else {
  615. print 'Missing Content-Type: text/xml header in response.<br>';
  616. }
  617. return false;
  618. default:
  619. // hmm. not what we expected. Just return what we got from webdav server
  620. // someone else has to handle it.
  621. $this->_lock['status'] = $response['status']['status-code'];
  622. return $this->_lock;
  623. }
  624. }
  625. }
  626. /**
  627. * Public method unlock
  628. *
  629. * Unlocks a file or collection.
  630. *
  631. * @param string path, string locktoken
  632. * @return int status code (look at rfc 2518). false on error.
  633. */
  634. function unlock($path, $locktoken) {
  635. $this->_path = $this->translate_uri($path);
  636. $this->header_unset();
  637. $this->create_basic_request('UNLOCK');
  638. $this->header_add(sprintf('Lock-Token: <%s>', $locktoken));
  639. $this->send_request();
  640. $this->get_respond();
  641. $response = $this->process_respond();
  642. if ($response['status']['http-version'] == 'HTTP/1.1' ||
  643. $response['status']['http-version'] == 'HTTP/1.0') {
  644. /* seems to be http ... proceed
  645. rfc 2518 says:
  646. 204 (OK) - The 204 (No Content) status code is used instead of 200 (OK) because there is no response entity body.
  647. */
  648. return $response['status']['status-code'];
  649. }
  650. return false;
  651. }
  652. /**
  653. * Public method delete
  654. *
  655. * deletes a collection/directory on a webdav server
  656. * @param string path
  657. * @return int status code (look at rfc 2518). false on error.
  658. */
  659. function delete($path) {
  660. $this->_path = $this->translate_uri($path);
  661. $this->header_unset();
  662. $this->create_basic_request('DELETE');
  663. /* $this->header_add('Content-Length: 0'); */
  664. $this->header_add('');
  665. $this->send_request();
  666. $this->get_respond();
  667. $response = $this->process_respond();
  668. // validate the response ...
  669. // check http-version
  670. if ($response['status']['http-version'] == 'HTTP/1.1' ||
  671. $response['status']['http-version'] == 'HTTP/1.0') {
  672. // seems to be http ... proceed
  673. // We expect a 207 Multi-Status status code
  674. // print 'http ok<br>';
  675. switch ($response['status']['status-code']) {
  676. case 207:
  677. // collection was NOT deleted... see xml response for reason...
  678. // next there should be a Content-Type: text/xml; charset="utf-8" header line
  679. if (strcmp($response['header']['Content-Type'], 'text/xml; charset="utf-8"') == 0) {
  680. // ok let's get the content of the xml stuff
  681. $this->_parser = xml_parser_create_ns();
  682. $this->_parserid = (int) $this->_parser;
  683. // forget old data...
  684. unset($this->_delete[$this->_parserid]);
  685. unset($this->_xmltree[$this->_parserid]);
  686. xml_parser_set_option($this->_parser,XML_OPTION_SKIP_WHITE,0);
  687. xml_parser_set_option($this->_parser,XML_OPTION_CASE_FOLDING,0);
  688. xml_set_object($this->_parser, $this);
  689. xml_set_element_handler($this->_parser, "_delete_startElement", "_endElement");
  690. xml_set_character_data_handler($this->_parser, "_delete_cdata");
  691. if (!xml_parse($this->_parser, $response['body'])) {
  692. die(sprintf("XML error: %s at line %d",
  693. xml_error_string(xml_get_error_code($this->_parser)),
  694. xml_get_current_line_number($this->_parser)));
  695. }
  696. print "<br>";
  697. // Free resources
  698. xml_parser_free($this->_parser);
  699. $this->_delete[$this->_parserid]['status'] = $response['status']['status-code'];
  700. return $this->_delete[$this->_parserid];
  701. } else {
  702. print 'Missing Content-Type: text/xml header in response.<br>';
  703. }
  704. return false;
  705. default:
  706. // collection or file was successfully deleted
  707. $this->_delete['status'] = $response['status']['status-code'];
  708. return $this->_delete;
  709. }
  710. }
  711. }
  712. /**
  713. * Public method ls
  714. *
  715. * Get's directory information from webdav server into flat a array using PROPFIND
  716. *
  717. * All filenames are UTF-8 encoded.
  718. * Have a look at _propfind_startElement what keys are used in array returned.
  719. * @param string path
  720. * @return array dirinfo, false on error
  721. */
  722. function ls($path) {
  723. if (trim($path) == '') {
  724. $this->_error_log('Missing a path in method ls');
  725. return false;
  726. }
  727. $this->_path = $this->translate_uri($path);
  728. $this->header_unset();
  729. $this->create_basic_request('PROPFIND');
  730. $this->header_add('Depth: 1');
  731. $this->header_add('Content-type: application/xml');
  732. // create profind xml request...
  733. $xml = <<<EOD
  734. <?xml version="1.0" encoding="utf-8"?>
  735. <propfind xmlns="DAV:"><prop>
  736. <getcontentlength xmlns="DAV:"/>
  737. <getlastmodified xmlns="DAV:"/>
  738. <executable xmlns="http://apache.org/dav/props/"/>
  739. <resourcetype xmlns="DAV:"/>
  740. <checked-in xmlns="DAV:"/>
  741. <checked-out xmlns="DAV:"/>
  742. </prop></propfind>
  743. EOD;
  744. $this->header_add('Content-length: ' . strlen($xml));
  745. $this->send_request();
  746. $this->_error_log($xml);
  747. fputs($this->sock, $xml);
  748. $this->get_respond();
  749. $response = $this->process_respond();
  750. // validate the response ... (only basic validation)
  751. // check http-version
  752. if ($response['status']['http-version'] == 'HTTP/1.1' ||
  753. $response['status']['http-version'] == 'HTTP/1.0') {
  754. // seems to be http ... proceed
  755. // We expect a 207 Multi-Status status code
  756. // print 'http ok<br>';
  757. if (strcmp($response['status']['status-code'],'207') == 0 ) {
  758. // ok so far
  759. // next there should be a Content-Type: text/xml; charset="utf-8" header line
  760. if (preg_match('#(application|text)/xml;\s?charset=[\'\"]?utf-8[\'\"]?#i', $response['header']['Content-Type'])) {
  761. // ok let's get the content of the xml stuff
  762. $this->_parser = xml_parser_create_ns('UTF-8');
  763. $this->_parserid = (int) $this->_parser;
  764. // forget old data...
  765. unset($this->_ls[$this->_parserid]);
  766. unset($this->_xmltree[$this->_parserid]);
  767. xml_parser_set_option($this->_parser,XML_OPTION_SKIP_WHITE,0);
  768. xml_parser_set_option($this->_parser,XML_OPTION_CASE_FOLDING,0);
  769. // xml_parser_set_option($this->_parser,XML_OPTION_TARGET_ENCODING,'UTF-8');
  770. xml_set_object($this->_parser, $this);
  771. xml_set_element_handler($this->_parser, "_propfind_startElement", "_endElement");
  772. xml_set_character_data_handler($this->_parser, "_propfind_cdata");
  773. if (!xml_parse($this->_parser, $response['body'])) {
  774. die(sprintf("XML error: %s at line %d",
  775. xml_error_string(xml_get_error_code($this->_parser)),
  776. xml_get_current_line_number($this->_parser)));
  777. }
  778. // Free resources
  779. xml_parser_free($this->_parser);
  780. $arr = $this->_ls[$this->_parserid];
  781. return $arr;
  782. } else {
  783. $this->_error_log('Missing Content-Type: text/xml header in response!!');
  784. return false;
  785. }
  786. } else {
  787. // return status code ...
  788. return $response['status']['status-code'];
  789. }
  790. }
  791. // response was not http
  792. $this->_error_log('Ups in method ls: error in response from server');
  793. return false;
  794. }
  795. /**
  796. * Public method gpi
  797. *
  798. * Get's path information from webdav server for one element.
  799. *
  800. * @param string path
  801. * @return array dirinfo. false on error
  802. */
  803. function gpi($path) {
  804. // split path by last "/"
  805. $path = rtrim($path, "/");
  806. $item = basename($path);
  807. $dir = dirname($path);
  808. $list = $this->ls($dir);
  809. // be sure it is an array
  810. if (is_array($list)) {
  811. foreach($list as $e) {
  812. $fullpath = urldecode($e['href']);
  813. $filename = basename($fullpath);
  814. if ($filename == $item && $filename != "" and $fullpath != $dir."/") {
  815. return $e;
  816. }
  817. }
  818. }
  819. return false;
  820. }
  821. /**
  822. * Public method is_file
  823. *
  824. * Gathers whether a path points to a file or not.
  825. *
  826. * @param string path
  827. * @return bool true or false
  828. */
  829. function is_file($path) {
  830. $item = $this->gpi($path);
  831. if ($item === false) {
  832. return false;
  833. } else {
  834. return ($item['resourcetype'] != 'collection');
  835. }
  836. }
  837. /**
  838. * Public method is_dir
  839. *
  840. * Gather whether a path points to a directory
  841. * @param string path
  842. * return bool true or false
  843. */
  844. function is_dir($path) {
  845. // be sure path is utf-8
  846. $item = $this->gpi($path);
  847. if ($item === false) {
  848. return false;
  849. } else {
  850. return ($item['resourcetype'] == 'collection');
  851. }
  852. }
  853. /**
  854. * Public method mput
  855. *
  856. * Puts multiple files and/or directories onto a webdav server.
  857. *
  858. * Filenames should be allready UTF-8 encoded.
  859. * Param fileList must be in format array("localpath" => "destpath").
  860. *
  861. * @param array filelist
  862. * @return bool true on success. otherwise int status code on error
  863. */
  864. function mput($filelist) {
  865. $result = true;
  866. foreach ($filelist as $localpath => $destpath) {
  867. $localpath = rtrim($localpath, "/");
  868. $destpath = rtrim($destpath, "/");
  869. // attempt to create target path
  870. if (is_dir($localpath)) {
  871. $pathparts = explode("/", $destpath."/ "); // add one level, last level will be created as dir
  872. } else {
  873. $pathparts = explode("/", $destpath);
  874. }
  875. $checkpath = "";
  876. for ($i=1; $i<sizeof($pathparts)-1; $i++) {
  877. $checkpath .= "/" . $pathparts[$i];
  878. if (!($this->is_dir($checkpath))) {
  879. $result &= ($this->mkcol($checkpath) == 201 );
  880. }
  881. }
  882. if ($result) {
  883. // recurse directories
  884. if (is_dir($localpath)) {
  885. if (!$dp = opendir($localpath)) {
  886. $this->_error_log("Could not open localpath for reading");
  887. return false;
  888. }
  889. $fl = array();
  890. while($filename = readdir($dp)) {
  891. if ((is_file($localpath."/".$filename) || is_dir($localpath."/".$filename)) && $filename!="." && $filename != "..") {
  892. $fl[$localpath."/".$filename] = $destpath."/".$filename;
  893. }
  894. }
  895. $result &= $this->mput($fl);
  896. } else {
  897. $result &= ($this->put_file($destpath, $localpath) == 201);
  898. }
  899. }
  900. }
  901. return $result;
  902. }
  903. /**
  904. * Public method mget
  905. *
  906. * Gets multiple files and directories.
  907. *
  908. * FileList must be in format array("remotepath" => "localpath").
  909. * Filenames are UTF-8 encoded.
  910. *
  911. * @param array filelist
  912. * @return bool true on succes, other int status code on error
  913. */
  914. function mget($filelist) {
  915. $result = true;
  916. foreach ($filelist as $remotepath => $localpath) {
  917. $localpath = rtrim($localpath, "/");
  918. $remotepath = rtrim($remotepath, "/");
  919. // attempt to create local path
  920. if ($this->is_dir($remotepath)) {
  921. $pathparts = explode("/", $localpath."/ "); // add one level, last level will be created as dir
  922. } else {
  923. $pathparts = explode("/", $localpath);
  924. }
  925. $checkpath = "";
  926. for ($i=1; $i<sizeof($pathparts)-1; $i++) {
  927. $checkpath .= "/" . $pathparts[$i];
  928. if (!is_dir($checkpath)) {
  929. $result &= mkdir($checkpath);
  930. }
  931. }
  932. if ($result) {
  933. // recurse directories
  934. if ($this->is_dir($remotepath)) {
  935. $list = $this->ls($remotepath);
  936. $fl = array();
  937. foreach($list as $e) {
  938. $fullpath = urldecode($e['href']);
  939. $filename = basename($fullpath);
  940. if ($filename != '' and $fullpath != $remotepath . '/') {
  941. $fl[$remotepath."/".$filename] = $localpath."/".$filename;
  942. }
  943. }
  944. $result &= $this->mget($fl);
  945. } else {
  946. $result &= ($this->get_file($remotepath, $localpath));
  947. }
  948. }
  949. }
  950. return $result;
  951. }
  952. // --------------------------------------------------------------------------
  953. // private xml callback and helper functions starting here
  954. // --------------------------------------------------------------------------
  955. /**
  956. * Private method _endelement
  957. *
  958. * a generic endElement method (used for all xml callbacks).
  959. *
  960. * @param resource parser, string name
  961. * @access private
  962. */
  963. private function _endElement($parser, $name) {
  964. // end tag was found...
  965. $parserid = (int) $parser;
  966. $this->_xmltree[$parserid] = substr($this->_xmltree[$parserid],0, strlen($this->_xmltree[$parserid]) - (strlen($name) + 1));
  967. }
  968. /**
  969. * Private method _propfind_startElement
  970. *
  971. * Is needed by public method ls.
  972. *
  973. * Generic method will called by php xml_parse when a xml start element tag has been detected.
  974. * The xml tree will translated into a flat php array for easier access.
  975. * @param resource parser, string name, string attrs
  976. * @access private
  977. */
  978. private function _propfind_startElement($parser, $name, $attrs) {
  979. // lower XML Names... maybe break a RFC, don't know ...
  980. $parserid = (int) $parser;
  981. $propname = strtolower($name);
  982. if (!empty($this->_xmltree[$parserid])) {
  983. $this->_xmltree[$parserid] .= $propname . '_';
  984. } else {
  985. $this->_xmltree[$parserid] = $propname . '_';
  986. }
  987. // translate xml tree to a flat array ...
  988. switch($this->_xmltree[$parserid]) {
  989. case 'dav::multistatus_dav::response_':
  990. // new element in mu
  991. $this->_ls_ref =& $this->_ls[$parserid][];
  992. break;
  993. case 'dav::multistatus_dav::response_dav::href_':
  994. $this->_ls_ref_cdata = &$this->_ls_ref['href'];
  995. break;
  996. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::creationdate_':
  997. $this->_ls_ref_cdata = &$this->_ls_ref['creationdate'];
  998. break;
  999. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::getlastmodified_':
  1000. $this->_ls_ref_cdata = &$this->_ls_ref['lastmodified'];
  1001. break;
  1002. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::getcontenttype_':
  1003. $this->_ls_ref_cdata = &$this->_ls_ref['getcontenttype'];
  1004. break;
  1005. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::getcontentlength_':
  1006. $this->_ls_ref_cdata = &$this->_ls_ref['getcontentlength'];
  1007. break;
  1008. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::depth_':
  1009. $this->_ls_ref_cdata = &$this->_ls_ref['activelock_depth'];
  1010. break;
  1011. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::owner_dav::href_':
  1012. $this->_ls_ref_cdata = &$this->_ls_ref['activelock_owner'];
  1013. break;
  1014. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::owner_':
  1015. $this->_ls_ref_cdata = &$this->_ls_ref['activelock_owner'];
  1016. break;
  1017. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::timeout_':
  1018. $this->_ls_ref_cdata = &$this->_ls_ref['activelock_timeout'];
  1019. break;
  1020. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::locktoken_dav::href_':
  1021. $this->_ls_ref_cdata = &$this->_ls_ref['activelock_token'];
  1022. break;
  1023. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::lockdiscovery_dav::activelock_dav::locktype_dav::write_':
  1024. $this->_ls_ref_cdata = &$this->_ls_ref['activelock_type'];
  1025. $this->_ls_ref_cdata = 'write';
  1026. $this->_ls_ref_cdata = &$this->_null;
  1027. break;
  1028. case 'dav::multistatus_dav::response_dav::propstat_dav::prop_dav::resourcetype_dav::collection_':
  1029. $this->_ls_ref_cdata = &$this->_ls_ref['resourcetype'];
  1030. $this->_ls_ref_cdata = 'collection';
  1031. $this->_ls_ref_cdata = &$this->_null;
  1032. break;
  1033. case 'dav::multistatus_dav::response_dav::propstat_dav::status_':
  1034. $this->_ls_ref_cdata = &$this->_ls_ref['status'];
  1035. break;
  1036. default:
  1037. // handle unknown xml elements...
  1038. $this->_ls_ref_cdata = &$this->_ls_ref[$this->_xmltree[$parserid]];
  1039. }
  1040. }
  1041. /**
  1042. * Private method _propfind_cData
  1043. *
  1044. * Is needed by public method ls.
  1045. *
  1046. * Will be called by php xml_set_character_data_handler() when xml data has to be handled.
  1047. * Stores data found into class var _ls_ref_cdata
  1048. * @param resource parser, string cdata
  1049. * @access private
  1050. */
  1051. private function _propfind_cData($parser, $cdata) {
  1052. if (trim($cdata) <> '') {
  1053. // cdata must be appended, because sometimes the php xml parser makes multiple calls
  1054. // to _propfind_cData before the xml end tag was reached...
  1055. $this->_ls_ref_cdata .= $cdata;
  1056. } else {
  1057. // do nothing
  1058. }
  1059. }
  1060. /**
  1061. * Private method _delete_startElement
  1062. *
  1063. * Is used by public method delete.
  1064. *
  1065. * Will be called by php xml_parse.
  1066. * @param resource parser, string name, string attrs)
  1067. * @access private
  1068. */
  1069. private function _delete_startElement($parser, $name, $attrs) {
  1070. // lower XML Names... maybe break a RFC, don't know ...
  1071. $parserid = (int) $parser;
  1072. $propname = strtolower($name);
  1073. $this->_xmltree[$parserid] .= $propname . '_';
  1074. // translate xml tree to a flat array ...
  1075. switch($this->_xmltree[$parserid]) {
  1076. case 'dav::multistatus_dav::response_':
  1077. // new element in mu
  1078. $this->_delete_ref =& $this->_delete[$parserid][];
  1079. break;
  1080. case 'dav::multistatus_dav::response_dav::href_':
  1081. $this->_delete_ref_cdata = &$this->_ls_ref['href'];
  1082. break;
  1083. default:
  1084. // handle unknown xml elements...
  1085. $this->_delete_cdata = &$this->_delete_ref[$this->_xmltree[$parserid]];
  1086. }
  1087. }
  1088. /**
  1089. * Private method _delete_cData
  1090. *
  1091. * Is used by public method delete.
  1092. *
  1093. * Will be called by php xml_set_character_data_handler() when xml data has to be handled.
  1094. * Stores data found into class var _delete_ref_cdata
  1095. * @param resource parser, string cdata
  1096. * @access private
  1097. */
  1098. private function _delete_cData($parser, $cdata) {
  1099. if (trim($cdata) <> '') {
  1100. $this->_delete_ref_cdata .= $cdata;
  1101. } else {
  1102. // do nothing
  1103. }
  1104. }
  1105. /**
  1106. * Private method _lock_startElement
  1107. *
  1108. * Is needed by public method lock.
  1109. *
  1110. * Mmethod will called by php xml_parse when a xml start element tag has been detected.
  1111. * The xml tree will translated into a flat php array for easier access.
  1112. * @param resource parser, string name, string attrs
  1113. * @access private
  1114. */
  1115. private function _lock_startElement($parser, $name, $attrs) {
  1116. // lower XML Names... maybe break a RFC, don't know ...
  1117. $parserid = (int) $parser;
  1118. $propname = strtolower($name);
  1119. $this->_xmltree[$parserid] .= $propname . '_';
  1120. // translate xml tree to a flat array ...
  1121. /*
  1122. dav::prop_dav::lockdiscovery_d…

Large files files are truncated, but you can click here to view the full file