1: <?php
2:
3: /**
4: * Jyxo PHP Library
5: *
6: * LICENSE
7: *
8: * This source file is subject to the new BSD license that is bundled
9: * with this package in the file license.txt.
10: * It is also available through the world-wide-web at this URL:
11: * https://github.com/jyxo/php/blob/master/license.txt
12: */
13:
14: /**
15: * Client for work with WebDav. Uses the http PHP extension.
16: *
17: * @category Jyxo
18: * @package Jyxo_Webdav
19: * @copyright Copyright (c) 2005-2011 Jyxo, s.r.o.
20: * @license https://github.com/jyxo/php/blob/master/license.txt
21: * @author Jaroslav HanslĂk
22: */
23: class Jyxo_Webdav_Client
24: {
25: /**
26: * Servers list.
27: *
28: * @var array
29: */
30: private $servers = array();
31:
32: /**
33: * Connection options.
34: *
35: * @var array
36: */
37: private $options = array(
38: 'connecttimeout' => 1,
39: 'timeout' => 30
40: );
41:
42: /**
43: * Request pool.
44: *
45: * @var HttpRequestPool
46: */
47: private $pool = null;
48:
49: /**
50: * Constructor.
51: *
52: * @param array $servers
53: */
54: public function __construct(array $servers)
55: {
56: $this->servers = $servers;
57:
58: $this->pool = new HttpRequestPool();
59: }
60:
61: /**
62: * Sets an option.
63: *
64: * @param string $name Option name
65: * @param mixed $value Option value
66: */
67: public function setOption($name, $value)
68: {
69: $this->options[(string) $name] = $value;
70: }
71:
72: /**
73: * Checks if a file exists.
74: *
75: * @param string $path File path
76: * @return boolean
77: * @throws Jyxo_Webdav_Exception On error
78: */
79: public function exists($path)
80: {
81: $response = $this->sendRequest($this->getFilePath($path), HttpRequest::METH_HEAD);
82: return (200 === $response->getResponseCode());
83: }
84:
85: /**
86: * Returns file contents.
87: *
88: * @param string $path File path
89: * @return string
90: * @throws Jyxo_Webdav_FileNotExistException If the file does not exist
91: * @throws Jyxo_Webdav_Exception On error
92: */
93: public function get($path)
94: {
95: // Asking random server
96: $path = $this->getFilePath($path);
97: $response = $this->sendRequest($path, HttpRequest::METH_GET);
98:
99: if (200 !== $response->getResponseCode()) {
100: throw new Jyxo_Webdav_FileNotExistException(sprintf('File %s does not exist.', $path));
101: }
102:
103: return $response->getBody();
104: }
105:
106: /**
107: * Returns a file property.
108: * If no particular property is set, all properties are returned.
109: *
110: * @param string $path File path
111: * @param string $property Property name
112: * @return mixed
113: * @throws Jyxo_Webdav_FileNotExistException If the file does not exist
114: * @throws Jyxo_Webdav_Exception On error
115: */
116: public function getProperty($path, $property = '')
117: {
118: // Asking random server
119: $path = $this->getFilePath($path);
120: $response = $this->sendRequest($path, HttpRequest::METH_PROPFIND, array('Depth' => '0'));
121:
122: if (207 !== $response->getResponseCode()) {
123: throw new Jyxo_Webdav_FileNotExistException(sprintf('File %s does not exist.', $path));
124: }
125:
126: // Fetches file properties from the server
127: $properties = $this->getProperties($response);
128:
129: // Returns the requested property value
130: if (isset($properties[$property])) {
131: return $properties[$property];
132: }
133:
134: // Returns all properties
135: return $properties;
136: }
137:
138: /**
139: * Saves data to a remote file.
140: *
141: * @param string $path File path
142: * @param string $data Data
143: * @return boolean
144: * @throws Jyxo_Webdav_Exception On error
145: */
146: public function put($path, $data)
147: {
148: return $this->processPut($this->getFilePath($path), $data, false);
149: }
150:
151: /**
152: * Saves file contents to a remote file.
153: *
154: * @param string $path File path
155: * @param string $file Local file path
156: * @return boolean
157: * @throws Jyxo_Webdav_Exception On error
158: */
159: public function putFile($path, $file)
160: {
161: return $this->processPut($this->getFilePath($path), $file, true);
162: }
163:
164: /**
165: * Copies a file.
166: * Does not work on Lighttpd.
167: *
168: * @param string $pathFrom Source file path
169: * @param string $pathTo Target file path
170: * @return boolean
171: * @throws Jyxo_Webdav_Exception On error
172: */
173: public function copy($pathFrom, $pathTo)
174: {
175: $requestList = $this->getRequestList($this->getFilePath($pathFrom), HttpRequest::METH_COPY);
176: foreach ($requestList as $server => $request) {
177: $request->addHeaders(array('Destination' => $server . $this->getFilePath($pathTo)));
178: }
179:
180: foreach ($this->sendPool($requestList) as $request) {
181: // 201 means copied
182: if (201 !== $request->getResponseCode()) {
183: return false;
184: }
185: }
186:
187: return true;
188: }
189:
190: /**
191: * Renames a file.
192: * Does not work on Lighttpd.
193: *
194: * @param string $pathFrom Original file name
195: * @param string $pathTo New file name
196: * @return boolean
197: * @throws Jyxo_Webdav_Exception On error
198: */
199: public function rename($pathFrom, $pathTo)
200: {
201: $requestList = $this->getRequestList($this->getFilePath($pathFrom), HttpRequest::METH_MOVE);
202: foreach ($requestList as $server => $request) {
203: $request->addHeaders(array('Destination' => $server . $this->getFilePath($pathTo)));
204: }
205:
206: foreach ($this->sendPool($requestList) as $request) {
207: // 201 means renamed
208: if (201 !== $request->getResponseCode()) {
209: return false;
210: }
211: }
212:
213: return true;
214: }
215:
216: /**
217: * Deletes a file.
218: * Contains a check preventing from deleting directories.
219: *
220: * @param string $path Directory path
221: * @return boolean
222: * @throws Jyxo_Webdav_Exception On error
223: */
224: public function unlink($path)
225: {
226: // We do not delete directories
227: if ($this->isDir($path)) {
228: return false;
229: }
230:
231: foreach ($this->send($this->getFilePath($path), HttpRequest::METH_DELETE) as $request) {
232: // 204 means deleted
233: if (204 !== $request->getResponseCode()) {
234: return false;
235: }
236: }
237:
238: return true;
239: }
240:
241: /**
242: * Checks if a directory exists.
243: *
244: * @param string $dir Directory path
245: * @return boolean
246: * @throws Jyxo_Webdav_Exception On error
247: */
248: public function isDir($dir)
249: {
250: // Asking random server
251: $response = $this->sendRequest($this->getDirPath($dir), HttpRequest::METH_PROPFIND, array('Depth' => '0'));
252:
253: // The directory does not exist
254: if (207 !== $response->getResponseCode()) {
255: return false;
256: }
257:
258: // Fetches properties from the server
259: $properties = $this->getProperties($response);
260:
261: // Checks if it is a directory
262: return isset($properties['getcontenttype']) && ('httpd/unix-directory' === $properties['getcontenttype']);
263: }
264:
265: /**
266: * Creates a directory.
267: *
268: * @param string $dir Directory path
269: * @param boolean $recursive Create directories recursively?
270: * @return boolean
271: * @throws Jyxo_Webdav_Exception On error
272: */
273: public function mkdir($dir, $recursive = true)
274: {
275: // If creating directories recursively, create the parent directory first
276: $dir = trim($dir, '/');
277: if ($recursive) {
278: $dirs = explode('/', $dir);
279: } else {
280: $dirs = array($dir);
281: }
282:
283: $path = '';
284: foreach ($dirs as $dir) {
285: $path .= rtrim($dir);
286: $path = $this->getDirPath($path);
287:
288: foreach ($this->send($path, HttpRequest::METH_MKCOL) as $request) {
289: switch ($request->getResponseCode()) {
290: // The directory was created
291: case 201:
292: break;
293: // The directory already exists
294: case 405:
295: break;
296: // The directory could not be created
297: default:
298: return false;
299: }
300: }
301: }
302:
303: // The directory was created
304: return true;
305: }
306:
307: /**
308: * Deletes a directory.
309: *
310: * @param string $dir Directory path
311: * @return boolean
312: * @throws Jyxo_Webdav_Exception On error
313: */
314: public function rmdir($dir)
315: {
316: foreach ($this->send($this->getDirPath($dir), HttpRequest::METH_DELETE) as $request) {
317: // 204 means deleted
318: if (204 !== $request->getResponseCode()) {
319: return false;
320: }
321: }
322:
323: return true;
324: }
325:
326: /**
327: * Processes a PUT request.
328: *
329: * @param string $path File path
330: * @param string $data Data
331: * @param boolean $isFile Determines if $data is a file name or actual data
332: * @return boolean
333: * @throws Jyxo_Webdav_Exception On error
334: */
335: private function processPut($path, $data, $isFile)
336: {
337: $success = true;
338: foreach ($this->sendPut($path, $data, $isFile) as $request) {
339: switch ($request->getResponseCode()) {
340: // Saved
341: case 200:
342: case 201:
343: break;
344: // An existing file was modified
345: case 204:
346: break;
347: // The directory might not exist
348: case 403:
349: case 404:
350: case 409:
351: $success = false;
352: break;
353: // Could not save
354: default:
355: return false;
356: }
357: }
358:
359: // Saved
360: if ($success) {
361: return true;
362: }
363:
364: // Not saved, try creating the directory first
365: if (!$this->mkdir(dirname($path))) {
366: return false;
367: }
368:
369: // Try again
370: foreach ($this->sendPut($path, $data, $isFile) as $request) {
371: // 201 means saved
372: if (201 !== $request->getResponseCode()) {
373: return false;
374: }
375: }
376:
377: return true;
378: }
379:
380: /**
381: * Sends a PUT request.
382: *
383: * @param string $path File path
384: * @param string $data Data
385: * @param boolean $isFile Determines if $data is a file name or actual data
386: * @return HttpRequestPool
387: * @throws Jyxo_Webdav_Exception On error
388: */
389: private function sendPut($path, $data, $isFile)
390: {
391: $requestList = $this->getRequestList($path, HttpRequest::METH_PUT);
392: foreach ($requestList as $request) {
393: if ($isFile) {
394: $request->setPutFile($data);
395: } else {
396: $request->setPutData($data);
397: }
398: }
399:
400: return $this->sendPool($requestList);
401: }
402:
403: /**
404: * Creates a request pool and sends it.
405: *
406: * @param string $path Request path
407: * @param integer $method Request method
408: * @param array $headers Array of headers
409: * @return HttpRequestPool
410: */
411: private function send($path, $method, array $headers = array())
412: {
413: return $this->sendPool($this->getRequestList($path, $method, $headers));
414: }
415:
416: /**
417: * Sends a request pool.
418: *
419: * @param ArrayObject $requestList Request list
420: * @return HttpRequestPool
421: * @throws Jyxo_Webdav_Exception On error
422: */
423: private function sendPool(ArrayObject $requestList)
424: {
425: try {
426: // Clean the pool
427: $this->pool->reset();
428:
429: // Attach requests
430: foreach ($requestList as $request) {
431: $this->pool->attach($request);
432: }
433:
434: // Send
435: $this->pool->send();
436:
437: return $this->pool;
438: } catch (HttpException $e) {
439: // Find the innermost exception
440: $inner = $e;
441: while (null !== $inner->innerException) {
442: $inner = $inner->innerException;
443: }
444: throw new Jyxo_Webdav_Exception($inner->getMessage(), 0, $inner);
445: }
446: }
447:
448: /**
449: * Sends a request.
450: *
451: * @param string $path Request path
452: * @param integer $method Request method
453: * @param array $headers Array of headers
454: * @return HttpMessage
455: * @throws Jyxo_Webdav_Exception On error
456: */
457: private function sendRequest($path, $method, array $headers = array())
458: {
459: try {
460: // Send request to a random server
461: $request = $this->getRequest($this->servers[array_rand($this->servers)] . $path, $method, $headers);
462: return $request->send();
463: } catch (HttpException $e) {
464: throw new Jyxo_Webdav_Exception($e->getMessage(), 0, $e);
465: }
466: }
467:
468: /**
469: * Returns a list of requests; one for each server.
470: *
471: * @param string $path Request path
472: * @param integer $method Request method
473: * @param array $headers Array of headers
474: * @return ArrayObject
475: */
476: private function getRequestList($path, $method, array $headers = array())
477: {
478: $requestList = new ArrayObject();
479: foreach ($this->servers as $server) {
480: $requestList->offsetSet($server, $this->getRequest($server . $path, $method, $headers));
481: }
482: return $requestList;
483: }
484:
485: /**
486: * Creates a request.
487: *
488: * @param string $url Request URL
489: * @param integer $method Request method
490: * @param array $headers Array of headers
491: * @return HttpRequest
492: */
493: private function getRequest($url, $method, array $headers = array())
494: {
495: $request = new HttpRequest($url, $method, $this->options);
496: $request->setHeaders(array('Expect' => ''));
497: $request->addHeaders($headers);
498: return $request;
499: }
500:
501: /**
502: * Creates a file path without the trailing slash.
503: *
504: * @param string $path File path
505: * @return string
506: */
507: private function getFilePath($path)
508: {
509: return '/' . trim($path, '/');
510: }
511:
512: /**
513: * Creates a directory path with the trailing slash.
514: *
515: * @param string $path Directory path
516: * @return string
517: */
518: private function getDirPath($path)
519: {
520: return '/' . trim($path, '/') . '/';
521: }
522:
523: /**
524: * Fetches properties from the response.
525: *
526: * @param HttpMessage $response Response
527: * @return array
528: */
529: private function getProperties(HttpMessage $response)
530: {
531: // Process the XML with properties
532: $properties = array();
533: $reader = new Jyxo_XmlReader();
534: $reader->XML($response->getBody());
535:
536: // Ignore warnings
537: while (@$reader->read()) {
538: if ((XMLReader::ELEMENT === $reader->nodeType) && ('D:prop' === $reader->name)) {
539: while (@$reader->read()) {
540: // Element must not be empty and has to look something like <lp1:getcontentlength>13744</lp1:getcontentlength>
541: if ((XMLReader::ELEMENT === $reader->nodeType) && (!$reader->isEmptyElement)) {
542: if (preg_match('~^lp\d+:(.+)$~', $reader->name, $matches)) {
543: // Apache
544: $properties[$matches[1]] = $reader->getTextValue();
545: } elseif (preg_match('~^D:(.+)$~', $reader->name, $matches)) {
546: // Lighttpd
547: $properties[$matches[1]] = $reader->getTextValue();
548: }
549: } elseif ((XMLReader::END_ELEMENT === $reader->nodeType) && ('D:prop' === $reader->name)) {
550: break;
551: }
552: }
553: }
554: }
555:
556: return $properties;
557: }
558: }
559: