Viewing file: Stream.php (25.35 KB) -rw-rw-rw- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php require_once "HTTP/Request.php";
require "HTTP/WebDAV/Tools/_parse_propfind_response.php"; require "HTTP/WebDAV/Tools/_parse_lock_response.php";
// WebDAV defines some addition HTTP methods define('HTTP_REQUEST_METHOD_COPY', 'COPY', true); define('HTTP_REQUEST_METHOD_MOVE', 'MOVE', true); define('HTTP_REQUEST_METHOD_MKCOL', 'MKCOL', true); define('HTTP_REQUEST_METHOD_PROPFIND', 'PROPFIND', true); define('HTTP_REQUEST_METHOD_PROPPATCH', 'PROPPATCH', true); define('HTTP_REQUEST_METHOD_LOCK', 'LOCK', true); define('HTTP_REQUEST_METHOD_UNLOCK', 'UNLOCK', true);
/** * A stream wrapper class for WebDAV access * * @access public */ class HTTP_WebDAV_Client_Stream { /** * User-Agent: header string * * @access private * @var string */ var $userAgent = "PEAR::HTTP_WebDAV_Client";
/** * Content-type: header string * * @access private * @var string */ var $contentType = "application/octet-stream";
/** * The http or https resource URL * * @access private * @var string url */ var $url = false;
/** * The resource URL path * * @access private * @var string path */ var $path = false;
/** * File position indicator * * @access private * @var int offset in bytes */ var $position = 0;
/** * File status information cache * * @access private * @var array stat information */ var $stat = array();
/** * User name for authentication * * @access private * @var string name */ var $user = false;
/** * Password for authentication * * @access private * @var string password */ var $pass = false;
/** * WebDAV protocol levels supported by the server * * @access private * @var array level entries */ var $dav_level = array();
/** * HTTP methods supported by the server * * @access private * @var array method entries */ var $dav_allow = array();
/** * Directory content cache * * @access private * @var array filename entries */ var $dirfiles = false;
/** * Current readdir() position * * @access private * @var int */ var $dirpos = 0;
/** * Remember if end of file was reached * * @access private * @var bool */ var $eof = false;
/** * Lock token * * @access private * @var string */ var $locktoken = false;
/** * Stream wrapper interface open() method * * @access public * @var string resource URL * @var string mode flags * @var array not used here * @var string return real path here if suitable * @return bool true on success */ function stream_open($path, $mode, $options, &$opened_path) { // rewrite the request URL if (!$this->_parse_url($path)) return false;
// query server for WebDAV options if (!$this->_check_options()) return false;
// now get the file metadata // we only need type, size, creation and modification date $req = &$this->_startRequest(HTTP_REQUEST_METHOD_PROPFIND); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } $req->addHeader("Depth", "0"); $req->addHeader("Content-Type", "text/xml"); $req->addRawPostData('<?xml version="1.0" encoding="utf-8"?> <propfind xmlns="DAV:"> <prop> <resourcetype/> <getcontentlength/> <getlastmodified /> <creationdate/> </prop> </propfind> '); $req->sendRequest();
// check the response code, anything but 207 indicates a problem switch ($req->getResponseCode()) { case 207: // OK // now we have to parse the result to get the status info items $propinfo = &new HTTP_WebDAV_Client_parse_propfind_response($req->getResponseBody()); $this->stat = $propinfo->stat(); unset($propinfo); break;
case 404: // not found is ok in write modes if (preg_match('|[aw\+]|', $mode)) { break; // write } $this->eof = true; // else fallthru default: error_log("file not found: ".$req->getResponseCode()); return false; } // 'w' -> open for writing, truncate existing files if (strpos($mode, "w") !== false) { $req = &$this->_startRequest(HTTP_REQUEST_METHOD_PUT);
$req->addHeader('Content-length', 0);
if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); }
$req->sendRequest(); }
// 'a' -> open for appending if (strpos($mode, "a") !== false) { $this->position = $this->stat['size']; $this->eof = true; }
// we are done :) return true; }
/** * Streap wrapper interface close() method * * @access public */ function stream_close() { // unlock? if ($this->locktoken) { $this->stream_lock(LOCK_UN); }
// closing is simple as HTTP is stateless $this->url = false; }
/** * Stream wrapper interface stat() method * * @access public * @return array stat entries */ function stream_stat() { // we already have collected the needed information // in stream_open() :) return $this->stat; }
/** * Stream wrapper interface read() method * * @access public * @param int requested byte count * @return string read data */ function stream_read($count) { // do some math $start = $this->position; $end = $start + $count - 1;
// create a GET request with a range $req = &$this->_startRequest(HTTP_REQUEST_METHOD_GET); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } $req->addHeader("Range", "bytes=$start-$end");
// go! go! go! $req->sendRequest(); $data = $req->getResponseBody(); $len = strlen($data);
// lets see what happened switch ($req->getResponseCode()) { case 200: // server doesn't support range requests // TODO we should add some sort of cacheing here $data = substr($data, $start, $count); break;
case 206: // server supports range requests break;
case 416: // reading beyond end of file is not an error $data = ""; $len = 0; break;
default: return false; }
// no data indicates end of file if (!$len) { $this->eof = true; }
// update position $this->position += $len;
// thats it! return $data; }
/** * Stream wrapper interface write() method * * @access public * @param string data to write * @return int number of bytes actually written */ function stream_write($buffer) { // do some math $start = $this->position; $end = $this->position + strlen($buffer) - 1;
// create a partial PUT request $req = &$this->_startRequest(HTTP_REQUEST_METHOD_PUT); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } $req->addHeader("Content-Range", "bytes $start-$end/*"); if ($this->locktoken) { $req->addHeader("If", "(<{$this->locktoken}>)"); } $req->addRawPostData($buffer);
// go! go! go! $req->sendRequest();
// check result switch ($req->getResponseCode()) { case 200: case 201: case 204: $this->position += strlen($buffer); return 1 + $end - $start; default: return false; }
/* We do not cope with servers that do not support partial PUTs! And we do assume that a server does conform to the following rule from RFC 2616 Section 9.6:
"The recipient of the entity MUST NOT ignore any Content-* (e.g. Content-Range) headers that it does not understand or implement and MUST return a 501 (Not Implemented) response in such cases." So the worst case scenario with a compliant server not implementing partial PUTs should be a failed request. A server simply ignoring "Content-Range" would replace file contents with the request body instead of putting the data at the requested place but we can blame it for not being compliant in this case ;)
(TODO: maybe we should do a HTTP version check first?) we *could* emulate partial PUT support by adding local cacheing but for now we don't want to as it adds a lot of complexity and storage overhead to the client ... */ }
/** * Stream wrapper interface eof() method * * @access public * @return bool true if end of file was reached */ function stream_eof() { // another simple one return $this->eof; }
/** * Stream wrapper interface tell() method * * @access public * @return int current file position */ function stream_tell() { // just return the current position return $this->position; }
/** * Stream wrapper interface seek() method * * @access public * @param int position to seek to * @param int seek mode * @return bool true on success */ function stream_seek($pos, $whence) { switch ($whence) { case SEEK_SET: // absolute position $this->position = $pos; break; case SEEK_CUR: // relative position $this->position += $pos; break; case SEEK_END: // relative position form end $this->position = $this->stat['size'] + $pos; break; default: return false; }
// TODO: this is rather naive (check how libc handles this) $this->eof = false;
return true; }
/** * Stream wrapper interface URL stat() method * * @access public * @param string URL to get stat information for * @return array stat information */ function url_stat($url) { // we map this one to open()/stat()/close() // there won't be much gain in inlining this if (!$this->stream_open($url, "r", array(), $dummy)) { return false; } $stat = $this->stream_stat(); $this->stream_close();
return $stat; }
/** * Stream wrapper interface opendir() method * * @access public * @param string directory resource URL * @param array not used here * @return bool true on success */ function dir_opendir($path, $options) { // rewrite the request URL if (!$this->_parse_url($path)) return false;
// query server for WebDAV options if (!$this->_check_options()) return false;
if (!isset($this->dav_allow[HTTP_REQUEST_METHOD_PROPFIND])) { return false; }
// now read the directory $req = &$this->_startRequest(HTTP_REQUEST_METHOD_PROPFIND); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } $req->addHeader("Depth", "1"); $req->addHeader("Content-Type", "text/xml"); $req->addRawPostData('<?xml version="1.0" encoding="utf-8"?> <propfind xmlns="DAV:"> <prop> <resourcetype/> <getcontentlength/> <creationdate/> <getlastmodified/> </prop> </propfind> '); $req->sendRequest();
switch ($req->getResponseCode()) { case 207: // multistatus content $this->dirfiles = array(); $this->dirpos = 0;
// for all returned resource entries foreach (explode("\n", $req->getResponseBody()) as $line) { // Preg_match_all if the whole response is one line! if (preg_match_all("/href>([^<]*)/", $line, $matches)) { // skip the directory itself foreach ($matches[1] as $match){ // Compare to $this->url too if ($match == "" || $match == $this->path || $match == $this->url) { continue; } // just remember the basenames to return them later with readdir() $this->dirfiles[] = basename($match); } } } return true;
default: // any other response state indicates an error error_log("file not found"); return false; } }
/** * Stream wrapper interface readdir() method * * @access public * @return string filename */ function dir_readdir() { // bailout if directory is empty if (!is_array($this->dirfiles)) { return false; } // bailout if we already reached end of dir if ($this->dirpos >= count($this->dirfiles)) { return false; }
// return an entry and move on return $this->dirfiles[$this->dirpos++]; }
/** * Stream wrapper interface rewinddir() method * * @access public */ function dir_rewinddir() { // bailout if directory content info has already // been freed if (!is_array($this->dirfiles)) { return false; }
// rewind to first entry $this->dirpos = 0; }
/** * Stream wrapper interface closedir() method * * @access public */ function dir_closedir() { // free stored directory content if (is_array($this->dirfiles)) { $this->dirfiles = false; $this->dirpos = 0; } }
/** * Stream wrapper interface mkdir() method * * @access public * @param string collection URL to be created * @return bool true on access */ function mkdir($path) { // rewrite the request URL if (!$this->_parse_url($path)) return false;
// query server for WebDAV options if (!$this->_check_options()) return false;
$req = &$this->_startRequest(HTTP_REQUEST_METHOD_MKCOL); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } if ($this->locktoken) { $req->addHeader("If", "(<{$this->locktoken}>)"); } $req->sendRequest();
// check the response code, anything but 201 indicates a problem $stat = $req->getResponseCode(); switch ($stat) { case 201: return true; default: error_log("mkdir failed - ". $stat); return false; } }
/** * Stream wrapper interface rmdir() method * * @access public * @param string collection URL to be created * @return bool true on access */ function rmdir($path) { // TODO: this should behave like "rmdir", currently it is more like "rm -rf"
// rewrite the request URL if (!$this->_parse_url($path)) return false;
// query server for WebDAV options if (!$this->_check_options()) return false;
$req = &$this->_startRequest(HTTP_REQUEST_METHOD_DELETE); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } if ($this->locktoken) { $req->addHeader("If", "(<{$this->locktoken}>)"); } $req->sendRequest();
// check the response code, anything but 204 indicates a problem $stat = $req->getResponseCode(); switch ($stat) { case 204: return true; default: error_log("rmdir failed - ". $stat); return false; } }
/** * Stream wrapper interface rename() method * * @access public * @param string resource URL to be moved * @param string resource URL to move to * @return bool true on access */ function rename($path, $new_path) { // rewrite the request URL if (!$this->_parse_url($path)) return false;
// query server for WebDAV options if (!$this->_check_options()) return false;
$req = &$this->_startRequest(HTTP_REQUEST_METHOD_MOVE); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } if ($this->locktoken) { $req->addHeader("If", "(<{$this->locktoken}>)"); } if (!$this->_parse_url($new_path)) return false; $req->addHeader("Destination", $this->url); $req->sendRequest();
// check the response code, anything but 207 indicates a problem $stat = $req->getResponseCode(); switch ($stat) { case 201: case 204: return true; default: error_log("rename failed - ". $stat); return false; } }
/** * Stream wrapper interface unlink() method * * @access public * @param string resource URL to be removed * @return bool true on success */ function unlink($path) { // rewrite the request URL if (!$this->_parse_url($path)) return false;
// query server for WebDAV options if (!$this->_check_options()) return false;
// is DELETE supported? if (!isset($this->dav_allow[HTTP_REQUEST_METHOD_DELETE])) { return false; }
$req = &$this->_startRequest(HTTP_REQUEST_METHOD_DELETE); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } if ($this->locktoken) { $req->addHeader("If", "(<{$this->locktoken}>)"); } $req->sendRequest();
switch ($req->getResponseCode()) { case 204: // ok return true; default: return false; } }
/** * Static helper that registers the wrappers * * @access public, static * @return bool true on success (even if SSL doesn't work) */ function register() { // check that we have the required feature if (!function_exists("stream_register_wrapper")) { return false; }
// try to register the non-encrypted WebDAV wrapper if (!stream_register_wrapper("webdav", "HTTP_WebDAV_Client_Stream")) { return false; }
// now try to register the SSL protocol variant // it is not critical if this fails // TODO check whether SSL is possible with HTTP_Request stream_register_wrapper("webdavs", "HTTP_WebDAV_Client_Stream");
return true; }
/** * Helper function for URL analysis * * @access private * @param string original request URL * @return bool true on success else false */ function _parse_url($path) { // rewrite the WebDAV url as a plain HTTP url $url = parse_url($path);
// detect whether plain or SSL-encrypted transfer is requested $scheme = $url['scheme']; switch ($scheme) { case "webdav": $url['scheme'] = "http"; break; case "webdavs": $url['scheme'] = "https"; break; default: error_log("only 'webdav:' and 'webdavs:' are supported, not '$url[scheme]:'"); return false; }
if (isset($this->context)) { // extract settings from stream context $context = stream_context_get_options($this->context);
// User-Agent if (isset($context[$scheme]['user_agent'])) { $this->userAgent = $context[$scheme]['user_agent']; }
// Content-Type if (isset($context[$scheme]['content_type'])) { $this->contentType = $context[$scheme]['content_type']; } // TODO check whether to implement other HTTP specific // context settings from http://php.net/manual/en/context.http.php }
// if a TCP port is specified we have to add it after the host if (isset($url['port'])) { $url['host'] .= ":$url[port]"; }
// store the plain path for possible later use $this->path = $url["path"];
// now we can put together the new URL $this->url = "$url[scheme]://$url[host]$url[path]";
// extract authentication information if (isset($url['user'])) { $this->user = urldecode($url['user']); } if (isset($url['pass'])) { $this->pass = urldecode($url['pass']); }
return true; }
/** * Helper function for WebDAV OPTIONS detection * * @access private * @return bool true on success else false */ function _check_options() { // now check OPTIONS reply for WebDAV response headers $req = &$this->_startRequest(HTTP_REQUEST_METHOD_OPTIONS); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } $req->sendRequest(); if ($req->getResponseCode() != 200) { return false; }
// get the supported DAV levels and extensions $dav = $req->getResponseHeader("DAV"); $this->dav_level = array(); foreach (explode(",", $dav) as $level) { $this->dav_level[trim($level)] = true; } if (!isset($this->dav_level["1"])) { // we need at least DAV Level 1 conformance return false; } // get the supported HTTP methods // TODO these are not checked for WebDAV compliance yet $allow = $req->getResponseHeader("Allow"); $this->dav_allow = array(); foreach (explode(",", $allow) as $method) { $this->dav_allow[trim($method)] = true; }
// TODO check for required WebDAV methods
return true; }
/** * Stream handler interface lock() method (experimental ...) * * @access private * @return bool true on success else false */ function stream_lock($mode) { /* TODO: - think over how to refresh locks */ $ret = false;
// LOCK is only supported by DAV Level 2 if (!isset($this->dav_level["2"])) { return false; }
switch ($mode & ~LOCK_NB) { case LOCK_UN: if ($this->locktoken) { $req = &$this->_startRequest(HTTP_REQUEST_METHOD_UNLOCK); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } $req->addHeader("Lock-Token", "<{$this->locktoken}>"); $req->sendRequest();
$ret = $req->getResponseCode() == 204; } break;
case LOCK_SH: case LOCK_EX: $body = sprintf('<?xml version="1.0" encoding="utf-8" ?> <D:lockinfo xmlns:D="DAV:"> <D:lockscope><D:%s/></D:lockscope> <D:locktype><D:write/></D:locktype> <D:owner>%s</D:owner> </D:lockinfo>', ($mode & LOCK_SH) ? "shared" : "exclusive", get_class($this)); // TODO better owner string $req = &$this->_startRequest(HTTP_REQUEST_METHOD_LOCK); if (is_string($this->user)) { $req->setBasicAuth($this->user, @$this->pass); } if ($this->locktoken) { // needed for refreshing a lock $req->addHeader("Lock-Token", "<{$this->locktoken}>"); } $req->addHeader("Timeout", "Infinite, Second-4100000000"); $req->addHeader("Content-Type", 'text/xml; charset="utf-8"'); $req->addRawPostData($body); $req->sendRequest();
$ret = $req->getResponseCode() == 200;
if ($ret) { $propinfo = &new HTTP_WebDAV_Client_parse_lock_response($req->getResponseBody()); $this->locktoken = $propinfo->locktoken; // TODO deal with timeout } break; default: break; }
return $ret; }
function &_startRequest($method) { $req = &new HTTP_Request($this->url);
$req->addHeader('User-agent', $this->userAgent); $req->addHeader('Content-type', $this->contentType);
$req->setMethod($method);
return $req; } }
/* * Local variables: * tab-width: 4 * c-basic-offset: 4 * indent-tabs-mode:nil * End: */
|