package.xml0000644000175000017500000002415011577376005013774 0ustar clockwerxclockwerx HTTP_WebDAV_Server pear.php.net WebDAV Server Baseclass. RFC2518 compliant helper class for WebDAV server implementation. Hartmut Holzgraefe hholzgra hholzgra@php.net yes Christian Stocker chregu chregu@bitflux.ch yes 2011-06-19 1.0.0RC6 1.0.0RC5 beta beta New BSD License QA Release Fix dir structure Bug #14163 Content-range is not processed (Hiroaki Kawai) Bug #14242 Uploaded file will be broken (Hiroaki Kawai) 4.4 1.4.0b1 0.9 0.9 alpha alpha 2003-02-18 PHP usable (complies to RFC 2518 in all but shared locks afaik) but still in developement 0.9.1 0.9.1 alpha alpha 2003-05-28 PHP usable (complies to RFC 2518 in all but shared locks afaik) but still in developement 0.99 0.99 beta beta 2003-11-18 PHP almost all TODO features and issues for 1.0 are now implemented, there might be some additional API cleanups in PROPFIND and PROPPATCH and some small issues in the Fileserver example still exist as soon as this is done i think the packe is ready for a 1.0RC1 release Starting with this release it is now possible to return streams from GET and PUT. IF you return a readable stream from GET or a writable stream from PUT the base class will take care of any further action including HTTP header generation and handling of partial GETs and PUTs (if the returned streams are seekable). The only things you should return in addition to an appropriate open stream are the current size of the resource in $options['size'] for both GET and PUT and the mimetype in $options['mimetype'] and modification date in $options['mtime'] for GET. 0.99.1 0.99.1 beta beta 2004-04-22 PHP Some serious stuff showed up that needs to be added/fixed before we go for 1.0. This release doesn't really address any of these, it only fixes some small issues with the existing code and adds comments in various places. 1.0.0rc1 1.0.0rc1 beta beta 2005-07-05 PHP Preparing for 1.0 release ... 1.0.0rc2 1.0.0rc2 beta beta 2006-01-15 PHP Still preparing for 1.0 release after some bug fixes ... 1.0.0RC3 1.0.0RC3 beta beta 2006-03-03 PHP More bug fixes, getting nearer to 1.0 release ... - there are still known charset encoding issues - some litmus locking tests are not passed yet 1.0.0RC4 1.0.0RC4 beta beta 2006-11-10 PHP More bug fixes: - prevent warnings - fixed failing litmus tests: - lock_refresh - fail_cond_put_unlocked - fail hard on unimplemented recursive lock - $_SERVER contents are now copied to the private _SERVER array in the constructor, derived classes can extend the constructor to modify $_SERVER contents - some headers were missing from HEAD replies (Bug #7240) - fixed variable name typos (Bug #7328) - added support for configurable table name prefixes (Bug #8366) - use @package-version@ placeholder in class headers (Bug #8811) - PROPFIND now returns null resources for resources locked but not yet created (Bug #8570) 1.0.0RC5 1.0.0RC5 beta beta 2010-10-05 New BSD License QA Release Bug #1949 $_prop_encoding not used for $file['path'] hholzgra Bug #2464 Missing return in http_GET() hholzgra Bug #6189 Various international filename issues hholzgra Bug #9367 Example Fileserver shows wrong uri for folders hholzgra Bug #10107 WebDAV Server only works with mod_php hholzgra Bug #10238 DELETE method in Filesystem.php breaks when path contains space character hholzgra Bug #10614 $uri variable gets set wrongly hholzgra Bug #10632 support D:lastaccessed as date field hholzgra Bug #10637 HTTP_WebDav_Server is throwing E_NOTICE's jorrit Bug #11069 download broken if mbstring.func_overload & 2 and utf-8 charset hholzgra Bug #11070 broken GET with mbstring.func_overload & 2 hholzgra Bug #11816 Fatal error when locking hholzgra Bug #11902 Implement HEAD in Filesystem and Reuse in GET hholzgra Bug #11903 CVS revision in fileheader hholzgra Bug #12073 Issues with filenames containing spaces hholzgra Bug #12282 MOVE (rename) not properly urldecoded hholzgra Bug #12283 Double urldecode causes problems with filenames that have % or + hholzgra Bug #12500 Undefined variable: host in HTTP/WebDAV/Server.php line 1470 hholzgra Bug #12602 Irrelevant NA Misspellings in API: pathes > paths hholzgra Bug #13372 script name is appended to path hholzgra Bug #13809 Unhandled HTTP_CONTENT_* Headers hholzgra Bug #13920 http_LOCK() expects string, requires int hholzgra - License Change to the new BSD License - reverting wrong namespace default logic - whitespace fix in XML output - Getting rid of warning suppression by @ prefixes (PEAR Bug #10637) - added handling for Microsoft specific 'lastaccessed' and 'ishidden' - Fix for $uri variable gets set wrongly (PEAR Bug #10614) 1.0.0RC6 1.0.0RC5 beta beta 2011-06-19 New BSD License QA Release Fix dir structure Bug #14163 Content-range is not processed (Hiroaki Kawai) Bug #14242 Uploaded file will be broken (Hiroaki Kawai) HTTP_WebDAV_Server-1.0.0RC6/db/Fileserver.sql0000600000175000017500000000212711577376005021265 0ustar clockwerxclockwerx-- MySQL dump 9.06 -- -- Host: localhost Database: webdav --------------------------------------------------------- -- Server version 4.0.3-beta CREATE DATABASE webdav; USE webdav; -- -- Table structure for table 'locks' -- CREATE TABLE locks ( token varchar(255) NOT NULL default '', path varchar(200) NOT NULL default '', expires int(11) NOT NULL default '0', owner varchar(200) default NULL, recursive int(11) default '0', writelock int(11) default '0', exclusivelock int(11) NOT NULL default 0, created bigint(20) default 0, modified bigint(20) default 0, PRIMARY KEY (token), UNIQUE KEY token (token), KEY path (path), KEY path_2 (path), KEY path_3 (path,token), KEY expires (expires) ) TYPE=MyISAM; -- -- Dumping data for table 'locks' -- -- -- Table structure for table 'properties' -- CREATE TABLE properties ( path varchar(255) NOT NULL default '', name varchar(120) NOT NULL default '', ns varchar(120) NOT NULL default 'DAV:', value text, PRIMARY KEY (path,name,ns), KEY path (path) ) TYPE=MyISAM; -- -- Dumping data for table 'properties' -- HTTP_WebDAV_Server-1.0.0RC6/HTTP/WebDAV/Server.php0000600000175000017500000021074011577376005021661 0ustar clockwerxclockwerx * @version @package_version@ */ class HTTP_WebDAV_Server { // {{{ Member Variables /** * complete URI for this request * * @var string */ var $uri; /** * base URI for this request * * @var string */ var $base_uri; /** * URI path for this request * * @var string */ var $path; /** * Realm string to be used in authentification popups * * @var string */ var $http_auth_realm = "PHP WebDAV"; /** * String to be used in "X-Dav-Powered-By" header * * @var string */ var $dav_powered_by = ""; /** * Remember parsed If: (RFC2518/9.4) header conditions * * @var array */ var $_if_header_uris = array(); /** * HTTP response status/message * * @var string */ var $_http_status = "200 OK"; /** * encoding of property values passed in * * @var string */ var $_prop_encoding = "utf-8"; /** * Copy of $_SERVER superglobal array * * Derived classes may extend the constructor to * modify its contents * * @var array */ var $_SERVER; // }}} // {{{ Constructor /** * Constructor * * @param void */ function HTTP_WebDAV_Server() { // PHP messages destroy XML output -> switch them off ini_set("display_errors", 0); // copy $_SERVER variables to local _SERVER array // so that derived classes can simply modify these $this->_SERVER = $_SERVER; } // }}} // {{{ ServeRequest() /** * Serve WebDAV HTTP request * * dispatch WebDAV HTTP request to the apropriate method handler * * @param void * @return void */ function ServeRequest() { // prevent warning in litmus check 'delete_fragment' if (strstr($this->_SERVER["REQUEST_URI"], '#')) { $this->http_status("400 Bad Request"); return; } // default uri is the complete request uri $uri = "http"; if (isset($this->_SERVER["HTTPS"]) && $this->_SERVER["HTTPS"] === "on") { $uri = "https"; } $uri.= "://".$this->_SERVER["HTTP_HOST"].$this->_SERVER["SCRIPT_NAME"]; // WebDAV has no concept of a query string and clients (including cadaver) // seem to pass '?' unencoded, so we need to extract the path info out // of the request URI ourselves $path_info = substr($this->_SERVER["REQUEST_URI"], strlen($this->_SERVER["SCRIPT_NAME"])); // just in case the path came in empty ... if (empty($path_info)) { $path_info = "/"; } $this->base_uri = $uri; $this->uri = $uri . $path_info; // set path $this->path = $this->_urldecode($path_info); if (!strlen($this->path)) { if ($this->_SERVER["REQUEST_METHOD"] == "GET") { // redirect clients that try to GET a collection // WebDAV clients should never try this while // regular HTTP clients might ... header("Location: ".$this->base_uri."/"); return; } else { // if a WebDAV client didn't give a path we just assume '/' $this->path = "/"; } } if (ini_get("magic_quotes_gpc")) { $this->path = stripslashes($this->path); } // identify ourselves if (empty($this->dav_powered_by)) { header("X-Dav-Powered-By: PHP class: ".get_class($this)); } else { header("X-Dav-Powered-By: ".$this->dav_powered_by); } // check authentication // for the motivation for not checking OPTIONS requests on / see // http://pear.php.net/bugs/bug.php?id=5363 if ( ( !(($this->_SERVER['REQUEST_METHOD'] == 'OPTIONS') && ($this->path == "/"))) && (!$this->_check_auth())) { // RFC2518 says we must use Digest instead of Basic // but Microsoft Clients do not support Digest // and we don't support NTLM and Kerberos // so we are stuck with Basic here header('WWW-Authenticate: Basic realm="'.($this->http_auth_realm).'"'); // Windows seems to require this being the last header sent // (changed according to PECL bug #3138) $this->http_status('401 Unauthorized'); return; } // check if (! $this->_check_if_header_conditions()) { return; } // detect requested method names $method = strtolower($this->_SERVER["REQUEST_METHOD"]); $wrapper = "http_".$method; // activate HEAD emulation by GET if no HEAD method found if ($method == "head" && !method_exists($this, "head")) { $method = "get"; } if (method_exists($this, $wrapper) && ($method == "options" || method_exists($this, $method))) { $this->$wrapper(); // call method by name } else { // method not found/implemented if ($this->_SERVER["REQUEST_METHOD"] == "LOCK") { $this->http_status("412 Precondition failed"); } else { $this->http_status("405 Method not allowed"); header("Allow: ".join(", ", $this->_allow())); // tell client what's allowed } } } // }}} // {{{ abstract WebDAV methods // {{{ GET() /** * GET implementation * * overload this method to retrieve resources from your server *
* * * @abstract * @param array &$params Array of input and output parameters *
input *
output * @returns int HTTP-Statuscode */ /* abstract function GET(&$params) { // dummy entry for PHPDoc } */ // }}} // {{{ PUT() /** * PUT implementation * * PUT implementation * * @abstract * @param array &$params * @returns int HTTP-Statuscode */ /* abstract function PUT() { // dummy entry for PHPDoc } */ // }}} // {{{ COPY() /** * COPY implementation * * COPY implementation * * @abstract * @param array &$params * @returns int HTTP-Statuscode */ /* abstract function COPY() { // dummy entry for PHPDoc } */ // }}} // {{{ MOVE() /** * MOVE implementation * * MOVE implementation * * @abstract * @param array &$params * @returns int HTTP-Statuscode */ /* abstract function MOVE() { // dummy entry for PHPDoc } */ // }}} // {{{ DELETE() /** * DELETE implementation * * DELETE implementation * * @abstract * @param array &$params * @returns int HTTP-Statuscode */ /* abstract function DELETE() { // dummy entry for PHPDoc } */ // }}} // {{{ PROPFIND() /** * PROPFIND implementation * * PROPFIND implementation * * @abstract * @param array &$params * @returns int HTTP-Statuscode */ /* abstract function PROPFIND() { // dummy entry for PHPDoc } */ // }}} // {{{ PROPPATCH() /** * PROPPATCH implementation * * PROPPATCH implementation * * @abstract * @param array &$params * @returns int HTTP-Statuscode */ /* abstract function PROPPATCH() { // dummy entry for PHPDoc } */ // }}} // {{{ LOCK() /** * LOCK implementation * * LOCK implementation * * @abstract * @param array &$params * @returns int HTTP-Statuscode */ /* abstract function LOCK() { // dummy entry for PHPDoc } */ // }}} // {{{ UNLOCK() /** * UNLOCK implementation * * UNLOCK implementation * * @abstract * @param array &$params * @returns int HTTP-Statuscode */ /* abstract function UNLOCK() { // dummy entry for PHPDoc } */ // }}} // }}} // {{{ other abstract methods // {{{ check_auth() /** * check authentication * * overload this method to retrieve and confirm authentication information * * @abstract * @param string type Authentication type, e.g. "basic" or "digest" * @param string username Transmitted username * @param string passwort Transmitted password * @returns bool Authentication status */ /* abstract function checkAuth($type, $username, $password) { // dummy entry for PHPDoc } */ // }}} // {{{ checklock() /** * check lock status for a resource * * overload this method to return shared and exclusive locks * active for this resource * * @abstract * @param string resource Resource path to check * @returns array An array of lock entries each consisting * of 'type' ('shared'/'exclusive'), 'token' and 'timeout' */ /* abstract function checklock($resource) { // dummy entry for PHPDoc } */ // }}} // }}} // {{{ WebDAV HTTP method wrappers // {{{ http_OPTIONS() /** * OPTIONS method handler * * The OPTIONS method handler creates a valid OPTIONS reply * including Dav: and Allowed: headers * based on the implemented methods found in the actual instance * * @param void * @return void */ function http_OPTIONS() { // Microsoft clients default to the Frontpage protocol // unless we tell them to use WebDAV header("MS-Author-Via: DAV"); // get allowed methods $allow = $this->_allow(); // dav header $dav = array(1); // assume we are always dav class 1 compliant if (isset($allow['LOCK'])) { $dav[] = 2; // dav class 2 requires that locking is supported } // tell clients what we found $this->http_status("200 OK"); header("DAV: " .join(", ", $dav)); header("Allow: ".join(", ", $allow)); header("Content-length: 0"); } // }}} // {{{ http_PROPFIND() /** * PROPFIND method handler * * @param void * @return void */ function http_PROPFIND() { $options = Array(); $files = Array(); $options["path"] = $this->path; // search depth from header (default is "infinity) if (isset($this->_SERVER['HTTP_DEPTH'])) { $options["depth"] = $this->_SERVER["HTTP_DEPTH"]; } else { $options["depth"] = "infinity"; } // analyze request payload $propinfo = new _parse_propfind("php://input"); if (!$propinfo->success) { $this->http_status("400 Error"); return; } $options['props'] = $propinfo->props; // call user handler if (!$this->PROPFIND($options, $files)) { $files = array("files" => array()); if (method_exists($this, "checkLock")) { // is locked? $lock = $this->checkLock($this->path); if (is_array($lock) && count($lock)) { $created = isset($lock['created']) ? $lock['created'] : time(); $modified = isset($lock['modified']) ? $lock['modified'] : time(); $files['files'][] = array("path" => $this->_slashify($this->path), "props" => array($this->mkprop("displayname", $this->path), $this->mkprop("creationdate", $created), $this->mkprop("getlastmodified", $modified), $this->mkprop("resourcetype", ""), $this->mkprop("getcontenttype", ""), $this->mkprop("getcontentlength", 0)) ); } } if (empty($files['files'])) { $this->http_status("404 Not Found"); return; } } // collect namespaces here $ns_hash = array(); // Microsoft Clients need this special namespace for date and time values $ns_defs = "xmlns:ns0=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\""; // now we loop over all returned file entries foreach ($files["files"] as $filekey => $file) { // nothing to do if no properties were returend for a file if (!isset($file["props"]) || !is_array($file["props"])) { continue; } // now loop over all returned properties foreach ($file["props"] as $key => $prop) { // as a convenience feature we do not require that user handlers // restrict returned properties to the requested ones // here we strip all unrequested entries out of the response switch($options['props']) { case "all": // nothing to remove break; case "names": // only the names of all existing properties were requested // so we remove all values unset($files["files"][$filekey]["props"][$key]["val"]); break; default: $found = false; // search property name in requested properties foreach ((array)$options["props"] as $reqprop) { if (!isset($reqprop["xmlns"])) { $reqprop["xmlns"] = ""; } if ( $reqprop["name"] == $prop["name"] && $reqprop["xmlns"] == $prop["ns"]) { $found = true; break; } } // unset property and continue with next one if not found/requested if (!$found) { $files["files"][$filekey]["props"][$key]=""; continue(2); } break; } // namespace handling if (empty($prop["ns"])) continue; // no namespace $ns = $prop["ns"]; if ($ns == "DAV:") continue; // default namespace if (isset($ns_hash[$ns])) continue; // already known // register namespace $ns_name = "ns".(count($ns_hash) + 1); $ns_hash[$ns] = $ns_name; $ns_defs .= " xmlns:$ns_name=\"$ns\""; } // we also need to add empty entries for properties that were requested // but for which no values where returned by the user handler if (is_array($options['props'])) { foreach ($options["props"] as $reqprop) { if ($reqprop['name']=="") continue; // skip empty entries $found = false; if (!isset($reqprop["xmlns"])) { $reqprop["xmlns"] = ""; } // check if property exists in result foreach ($file["props"] as $prop) { if ( $reqprop["name"] == $prop["name"] && $reqprop["xmlns"] == $prop["ns"]) { $found = true; break; } } if (!$found) { if ($reqprop["xmlns"]==="DAV:" && $reqprop["name"]==="lockdiscovery") { // lockdiscovery is handled by the base class $files["files"][$filekey]["props"][] = $this->mkprop("DAV:", "lockdiscovery", $this->lockdiscovery($files["files"][$filekey]['path'])); } else { // add empty value for this property $files["files"][$filekey]["noprops"][] = $this->mkprop($reqprop["xmlns"], $reqprop["name"], ""); // register property namespace if not known yet if ($reqprop["xmlns"] != "DAV:" && !isset($ns_hash[$reqprop["xmlns"]])) { $ns_name = "ns".(count($ns_hash) + 1); $ns_hash[$reqprop["xmlns"]] = $ns_name; $ns_defs .= " xmlns:$ns_name=\"$reqprop[xmlns]\""; } } } } } } // now we generate the reply header ... $this->http_status("207 Multi-Status"); header('Content-Type: text/xml; charset="utf-8"'); // ... and payload echo "\n"; echo "\n"; foreach ($files["files"] as $file) { // ignore empty or incomplete entries if (!is_array($file) || empty($file) || !isset($file["path"])) continue; $path = $file['path']; if (!is_string($path) || $path==="") continue; echo " \n"; /* TODO right now the user implementation has to make sure collections end in a slash, this should be done in here by checking the resource attribute */ $href = $this->_mergePaths($this->_SERVER['SCRIPT_NAME'], $path); /* minimal urlencoding is needed for the resource path */ $href = $this->_urlencode($href); echo " $href\n"; // report all found properties and their values (if any) if (isset($file["props"]) && is_array($file["props"])) { echo " \n"; echo " \n"; foreach ($file["props"] as $key => $prop) { if (!is_array($prop)) continue; if (!isset($prop["name"])) continue; if (!isset($prop["val"]) || $prop["val"] === "" || $prop["val"] === false) { // empty properties (cannot use empty() for check as "0" is a legal value here) if ($prop["ns"]=="DAV:") { echo " \n"; } else if (!empty($prop["ns"])) { echo " <".$ns_hash[$prop["ns"]].":$prop[name]/>\n"; } else { echo " <$prop[name] xmlns=\"\"/>"; } } else if ($prop["ns"] == "DAV:") { // some WebDAV properties need special treatment switch ($prop["name"]) { case "creationdate": echo " " . gmdate("Y-m-d\\TH:i:s\\Z", $prop['val']) . "\n"; break; case "getlastmodified": echo " " . gmdate("D, d M Y H:i:s ", $prop['val']) . "GMT\n"; break; case "resourcetype": echo " \n"; break; case "supportedlock": echo " $prop[val]\n"; break; case "lockdiscovery": echo " \n"; echo $prop["val"]; echo " \n"; break; // the following are non-standard Microsoft extensions to the DAV namespace case "lastaccessed": echo " " . gmdate("D, d M Y H:i:s ", $prop['val']) . "GMT\n"; break; case "ishidden": echo " " . is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false') . "\n"; break; default: echo " " . $this->_prop_encode(htmlspecialchars($prop['val'])) . "\n"; break; } } else { // properties from namespaces != "DAV:" or without any namespace if ($prop["ns"]) { echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "\n"; } else { echo " <$prop[name] xmlns=\"\">" . $this->_prop_encode(htmlspecialchars($prop['val'])) . "\n"; } } } echo " \n"; echo " HTTP/1.1 200 OK\n"; echo " \n"; } // now report all properties requested but not found if (isset($file["noprops"])) { echo " \n"; echo " \n"; foreach ($file["noprops"] as $key => $prop) { if ($prop["ns"] == "DAV:") { echo " \n"; } else if ($prop["ns"] == "") { echo " <$prop[name] xmlns=\"\"/>\n"; } else { echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n"; } } echo " \n"; echo " HTTP/1.1 404 Not Found\n"; echo " \n"; } echo " \n"; } echo "\n"; } // }}} // {{{ http_PROPPATCH() /** * PROPPATCH method handler * * @param void * @return void */ function http_PROPPATCH() { if ($this->_check_lock_status($this->path)) { $options = Array(); $options["path"] = $this->path; $propinfo = new _parse_proppatch("php://input"); if (!$propinfo->success) { $this->http_status("400 Error"); return; } $options['props'] = $propinfo->props; $responsedescr = $this->PROPPATCH($options); $this->http_status("207 Multi-Status"); header('Content-Type: text/xml; charset="utf-8"'); echo "\n"; echo "\n"; echo " \n"; echo " ".$this->_urlencode($this->_mergePaths($this->_SERVER["SCRIPT_NAME"], $this->path))."\n"; foreach ($options["props"] as $prop) { echo " \n"; echo " <$prop[name] xmlns=\"$prop[ns]\"/>\n"; echo " HTTP/1.1 $prop[status]\n"; echo " \n"; } if ($responsedescr) { echo " ". $this->_prop_encode(htmlspecialchars($responsedescr)). "\n"; } echo " \n"; echo "\n"; } else { $this->http_status("423 Locked"); } } // }}} // {{{ http_MKCOL() /** * MKCOL method handler * * @param void * @return void */ function http_MKCOL() { $options = Array(); $options["path"] = $this->path; $stat = $this->MKCOL($options); $this->http_status($stat); } // }}} // {{{ http_GET() /** * GET method handler * * @param void * @returns void */ function http_GET() { // TODO check for invalid stream $options = Array(); $options["path"] = $this->path; $this->_get_ranges($options); if (true === ($status = $this->GET($options))) { if (!headers_sent()) { $status = "200 OK"; if (!isset($options['mimetype'])) { $options['mimetype'] = "application/octet-stream"; } header("Content-type: $options[mimetype]"); if (isset($options['mtime'])) { header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT"); } if (isset($options['stream'])) { // GET handler returned a stream if (!empty($options['ranges']) && (0===fseek($options['stream'], 0, SEEK_SET))) { // partial request and stream is seekable if (count($options['ranges']) === 1) { $range = $options['ranges'][0]; if (isset($range['start'])) { fseek($options['stream'], $range['start'], SEEK_SET); if (feof($options['stream'])) { $this->http_status("416 Requested range not satisfiable"); return; } if (isset($range['end'])) { $size = $range['end']-$range['start']+1; $this->http_status("206 partial"); header("Content-length: $size"); header("Content-range: $range[start]-$range[end]/" . (isset($options['size']) ? $options['size'] : "*")); while ($size && !feof($options['stream'])) { $buffer = fread($options['stream'], 4096); $size -= $this->bytes($buffer); echo $buffer; } } else { $this->http_status("206 partial"); if (isset($options['size'])) { header("Content-length: ".($options['size'] - $range['start'])); header("Content-range: ".$range['start']."-".$range['end']."/" . (isset($options['size']) ? $options['size'] : "*")); } fpassthru($options['stream']); } } else { header("Content-length: ".$range['last']); fseek($options['stream'], -$range['last'], SEEK_END); fpassthru($options['stream']); } } else { $this->_multipart_byterange_header(); // init multipart foreach ($options['ranges'] as $range) { // TODO what if size unknown? 500? if (isset($range['start'])) { $from = $range['start']; $to = !empty($range['end']) ? $range['end'] : $options['size']-1; } else { $from = $options['size'] - $range['last']-1; $to = $options['size'] -1; } $total = isset($options['size']) ? $options['size'] : "*"; $size = $to - $from + 1; $this->_multipart_byterange_header($options['mimetype'], $from, $to, $total); fseek($options['stream'], $from, SEEK_SET); while ($size && !feof($options['stream'])) { $buffer = fread($options['stream'], 4096); $size -= $this->bytes($buffer); echo $buffer; } } $this->_multipart_byterange_header(); // end multipart } } else { // normal request or stream isn't seekable, return full content if (isset($options['size'])) { header("Content-length: ".$options['size']); } fpassthru($options['stream']); return; // no more headers } } elseif (isset($options['data'])) { if (is_array($options['data'])) { // reply to partial request } else { header("Content-length: ".$this->bytes($options['data'])); echo $options['data']; } } } } if (!headers_sent()) { if (false === $status) { $this->http_status("404 not found"); } else { // TODO: check setting of headers in various code paths above $this->http_status("$status"); } } } /** * parse HTTP Range: header * * @param array options array to store result in * @return void */ function _get_ranges(&$options) { // process Range: header if present if (isset($this->_SERVER['HTTP_RANGE'])) { // we only support standard "bytes" range specifications for now if (preg_match('/bytes\s*=\s*(.+)/', $this->_SERVER['HTTP_RANGE'], $matches)) { $options["ranges"] = array(); // ranges are comma separated foreach (explode(",", $matches[1]) as $range) { // ranges are either from-to pairs or just end positions list($start, $end) = explode("-", $range); $options["ranges"][] = ($start==="") ? array("last"=>$end) : array("start"=>$start, "end"=>$end); } } } } /** * generate separator headers for multipart response * * first and last call happen without parameters to generate * the initial header and closing sequence, all calls inbetween * require content mimetype, start and end byte position and * optionaly the total byte length of the requested resource * * @param string mimetype * @param int start byte position * @param int end byte position * @param int total resource byte size */ function _multipart_byterange_header($mimetype = false, $from = false, $to=false, $total=false) { if ($mimetype === false) { if (!isset($this->multipart_separator)) { // initial // a little naive, this sequence *might* be part of the content // but it's really not likely and rather expensive to check $this->multipart_separator = "SEPARATOR_".md5(microtime()); // generate HTTP header header("Content-type: multipart/byteranges; boundary=".$this->multipart_separator); } else { // final // generate closing multipart sequence echo "\n--{$this->multipart_separator}--"; } } else { // generate separator and header for next part echo "\n--{$this->multipart_separator}\n"; echo "Content-type: $mimetype\n"; echo "Content-range: $from-$to/". ($total === false ? "*" : $total); echo "\n\n"; } } // }}} // {{{ http_HEAD() /** * HEAD method handler * * @param void * @return void */ function http_HEAD() { $status = false; $options = Array(); $options["path"] = $this->path; if (method_exists($this, "HEAD")) { $status = $this->head($options); } else if (method_exists($this, "GET")) { ob_start(); $status = $this->GET($options); if (!isset($options['size'])) { $options['size'] = ob_get_length(); } ob_end_clean(); } if (!isset($options['mimetype'])) { $options['mimetype'] = "application/octet-stream"; } header("Content-type: $options[mimetype]"); if (isset($options['mtime'])) { header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT"); } if (isset($options['size'])) { header("Content-length: ".$options['size']); } if ($status === true) $status = "200 OK"; if ($status === false) $status = "404 Not found"; $this->http_status($status); } // }}} // {{{ http_PUT() /** * PUT method handler * * @param void * @return void */ function http_PUT() { if ($this->_check_lock_status($this->path)) { $options = Array(); $options["path"] = $this->path; $options["content_length"] = $this->_SERVER["CONTENT_LENGTH"]; // get the Content-type if (isset($this->_SERVER["CONTENT_TYPE"])) { // for now we do not support any sort of multipart requests if (!strncmp($this->_SERVER["CONTENT_TYPE"], "multipart/", 10)) { $this->http_status("501 not implemented"); echo "The service does not support mulipart PUT requests"; return; } $options["content_type"] = $this->_SERVER["CONTENT_TYPE"]; } else { // default content type if none given $options["content_type"] = "application/octet-stream"; } /* RFC 2616 2.6 says: "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." */ foreach ($this->_SERVER as $key => $val) { if (strncmp($key, "HTTP_CONTENT", 11)) continue; switch ($key) { case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11 // TODO support this if ext/zlib filters are available $this->http_status("501 not implemented"); echo "The service does not support '$val' content encoding"; return; case 'HTTP_CONTENT_LANGUAGE': // RFC 2616 14.12 // we assume it is not critical if this one is ignored // in the actual PUT implementation ... $options["content_language"] = $val; break; case 'HTTP_CONTENT_LENGTH': // defined on IIS and has the same value as CONTENT_LENGTH break; case 'HTTP_CONTENT_LOCATION': // RFC 2616 14.14 /* The meaning of the Content-Location header in PUT or POST requests is undefined; servers are free to ignore it in those cases. */ break; case 'HTTP_CONTENT_RANGE': // RFC 2616 14.16 // single byte range requests are supported // the header format is also specified in RFC 2616 14.16 // TODO we have to ensure that implementations support this or send 501 instead if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $val, $matches)) { $this->http_status("400 bad request"); echo "The service does only support single byte ranges"; return; } $range = array("start" => $matches[1], "end" => $matches[2]); if (is_numeric($matches[3])) { $range["total_length"] = $matches[3]; } if (!isset($options['ranges'])) { $options['ranges'] = array(); } $options["ranges"][] = $range; // TODO make sure the implementation supports partial PUT // this has to be done in advance to avoid data being overwritten // on implementations that do not support this ... break; case 'HTTP_CONTENT_TYPE': // defined on IIS and has the same value as CONTENT_TYPE break; case 'HTTP_CONTENT_MD5': // RFC 2616 14.15 // TODO: maybe we can just pretend here? $this->http_status("501 not implemented"); echo "The service does not support content MD5 checksum verification"; return; default: // any other unknown Content-* headers $this->http_status("501 not implemented"); echo "The service does not support '$key'"; return; } } $options["stream"] = fopen("php://input", "r"); $stat = $this->PUT($options); if ($stat === false) { $stat = "403 Forbidden"; } else if (is_resource($stat) && get_resource_type($stat) == "stream") { $stream = $stat; $stat = $options["new"] ? "201 Created" : "204 No Content"; if (!empty($options["ranges"])) { // TODO multipart support is missing (see also above) if (0 == fseek($stream, $options['ranges'][0]["start"], SEEK_SET)) { $length = $options['ranges'][0]["end"] - $options['ranges'][0]["start"]+1; while (!feof($options['stream'])) { if ($length <= 0) { break; } if ($length <= 8192) { $data = fread($options['stream'], $length); } else { $data = fread($options['stream'], 8192); } if ($data === false) { $stat = "400 Bad request"; } elseif (strlen($data)) { if (false === fwrite($stream, $data)) { $stat = "403 Forbidden"; break; } $length -= strlen($data); } } } else { $stat = "403 Forbidden"; } } else { while (!feof($options["stream"])) { if (false === fwrite($stream, fread($options["stream"], 8192))) { $stat = "403 Forbidden"; break; } } } fclose($stream); } $this->http_status($stat); } else { $this->http_status("423 Locked"); } } // }}} // {{{ http_DELETE() /** * DELETE method handler * * @param void * @return void */ function http_DELETE() { // check RFC 2518 Section 9.2, last paragraph if (isset($this->_SERVER["HTTP_DEPTH"])) { if ($this->_SERVER["HTTP_DEPTH"] != "infinity") { $this->http_status("400 Bad Request"); return; } } // check lock status if ($this->_check_lock_status($this->path)) { // ok, proceed $options = Array(); $options["path"] = $this->path; $stat = $this->DELETE($options); $this->http_status($stat); } else { // sorry, its locked $this->http_status("423 Locked"); } } // }}} // {{{ http_COPY() /** * COPY method handler * * @param void * @return void */ function http_COPY() { // no need to check source lock status here // destination lock status is always checked by the helper method $this->_copymove("copy"); } // }}} // {{{ http_MOVE() /** * MOVE method handler * * @param void * @return void */ function http_MOVE() { if ($this->_check_lock_status($this->path)) { // destination lock status is always checked by the helper method $this->_copymove("move"); } else { $this->http_status("423 Locked"); } } // }}} // {{{ http_LOCK() /** * LOCK method handler * * @param void * @return void */ function http_LOCK() { $options = Array(); $options["path"] = $this->path; if (isset($this->_SERVER['HTTP_DEPTH'])) { $options["depth"] = $this->_SERVER["HTTP_DEPTH"]; } else { $options["depth"] = "infinity"; } if (isset($this->_SERVER["HTTP_TIMEOUT"])) { $options["timeout"] = explode(",", $this->_SERVER["HTTP_TIMEOUT"]); } if (empty($this->_SERVER['CONTENT_LENGTH']) && !empty($this->_SERVER['HTTP_IF'])) { // check if locking is possible if (!$this->_check_lock_status($this->path)) { $this->http_status("423 Locked"); return; } // refresh lock $options["locktoken"] = substr($this->_SERVER['HTTP_IF'], 2, -2); $options["update"] = $options["locktoken"]; // setting defaults for required fields, LOCK() SHOULD overwrite these $options['owner'] = "unknown"; $options['scope'] = "exclusive"; $options['type'] = "write"; $stat = $this->LOCK($options); } else { // extract lock request information from request XML payload $lockinfo = new _parse_lockinfo("php://input"); if (!$lockinfo->success) { $this->http_status("400 bad request"); } // check if locking is possible if (!$this->_check_lock_status($this->path, $lockinfo->lockscope === "shared")) { $this->http_status("423 Locked"); return; } // new lock $options["scope"] = $lockinfo->lockscope; $options["type"] = $lockinfo->locktype; $options["owner"] = $lockinfo->owner; $options["locktoken"] = $this->_new_locktoken(); $stat = $this->LOCK($options); } if (is_bool($stat)) { $http_stat = $stat ? "200 OK" : "423 Locked"; } else { $http_stat = (string)$stat; } $this->http_status($http_stat); if ($http_stat{0} == 2) { // 2xx states are ok if ($options["timeout"]) { // if multiple timeout values were given we take the first only if (is_array($options["timeout"])) { reset($options["timeout"]); $options["timeout"] = current($options["timeout"]); } // if the timeout is numeric only we need to reformat it if (is_numeric($options["timeout"])) { // more than a million is considered an absolute timestamp // less is more likely a relative value if ($options["timeout"]>1000000) { $timeout = "Second-".($options['timeout']-time()); } else { $timeout = "Second-$options[timeout]"; } } else { // non-numeric values are passed on verbatim, // no error checking is performed here in this case // TODO: send "Infinite" on invalid timeout strings? $timeout = $options["timeout"]; } } else { $timeout = "Infinite"; } header('Content-Type: text/xml; charset="utf-8"'); header("Lock-Token: <$options[locktoken]>"); echo "\n"; echo "\n"; echo " \n"; echo " \n"; echo " \n"; echo " \n"; echo " $options[depth]\n"; echo " $options[owner]\n"; echo " $timeout\n"; echo " $options[locktoken]\n"; echo " \n"; echo " \n"; echo "\n\n"; } } // }}} // {{{ http_UNLOCK() /** * UNLOCK method handler * * @param void * @return void */ function http_UNLOCK() { $options = Array(); $options["path"] = $this->path; if (isset($this->_SERVER['HTTP_DEPTH'])) { $options["depth"] = $this->_SERVER["HTTP_DEPTH"]; } else { $options["depth"] = "infinity"; } // strip surrounding <> $options["token"] = substr(trim($this->_SERVER["HTTP_LOCK_TOKEN"]), 1, -1); // call user method $stat = $this->UNLOCK($options); $this->http_status($stat); } // }}} // }}} // {{{ _copymove() function _copymove($what) { $options = Array(); $options["path"] = $this->path; if (isset($this->_SERVER["HTTP_DEPTH"])) { $options["depth"] = $this->_SERVER["HTTP_DEPTH"]; } else { $options["depth"] = "infinity"; } $http_header_host = preg_replace("/:80$/", "", $this->_SERVER["HTTP_HOST"]); $url = parse_url($this->_SERVER["HTTP_DESTINATION"]); $path = urldecode($url["path"]); if (isset($url["host"])) { // TODO check url scheme, too $http_host = $url["host"]; if (isset($url["port"]) && $url["port"] != 80) $http_host.= ":".$url["port"]; } else { // only path given, set host to self $http_host == $http_header_host; } if ($http_host == $http_header_host && !strncmp($this->_SERVER["SCRIPT_NAME"], $path, strlen($this->_SERVER["SCRIPT_NAME"]))) { $options["dest"] = substr($path, strlen($this->_SERVER["SCRIPT_NAME"])); if (!$this->_check_lock_status($options["dest"])) { $this->http_status("423 Locked"); return; } } else { $options["dest_url"] = $this->_SERVER["HTTP_DESTINATION"]; } // see RFC 2518 Sections 9.6, 8.8.4 and 8.9.3 if (isset($this->_SERVER["HTTP_OVERWRITE"])) { $options["overwrite"] = $this->_SERVER["HTTP_OVERWRITE"] == "T"; } else { $options["overwrite"] = true; } $stat = $this->$what($options); $this->http_status($stat); } // }}} // {{{ _allow() /** * check for implemented HTTP methods * * @param void * @return array something */ function _allow() { // OPTIONS is always there $allow = array("OPTIONS" =>"OPTIONS"); // all other METHODS need both a http_method() wrapper // and a method() implementation // the base class supplies wrappers only foreach (get_class_methods($this) as $method) { if (!strncmp("http_", $method, 5)) { $method = strtoupper(substr($method, 5)); if (method_exists($this, $method)) { $allow[$method] = $method; } } } // we can emulate a missing HEAD implemetation using GET if (isset($allow["GET"])) $allow["HEAD"] = "HEAD"; // no LOCK without checklok() if (!method_exists($this, "checklock")) { unset($allow["LOCK"]); unset($allow["UNLOCK"]); } return $allow; } // }}} /** * helper for property element creation * * @param string XML namespace (optional) * @param string property name * @param string property value * @return array property array */ function mkprop() { $args = func_get_args(); if (count($args) == 3) { return array("ns" => $args[0], "name" => $args[1], "val" => $args[2]); } else { return array("ns" => "DAV:", "name" => $args[0], "val" => $args[1]); } } // {{{ _check_auth /** * check authentication if check is implemented * * @param void * @return bool true if authentication succeded or not necessary */ function _check_auth() { $auth_type = isset($this->_SERVER["AUTH_TYPE"]) ? $this->_SERVER["AUTH_TYPE"] : null; $auth_user = isset($this->_SERVER["PHP_AUTH_USER"]) ? $this->_SERVER["PHP_AUTH_USER"] : null; $auth_pw = isset($this->_SERVER["PHP_AUTH_PW"]) ? $this->_SERVER["PHP_AUTH_PW"] : null; if (method_exists($this, "checkAuth")) { // PEAR style method name return $this->checkAuth($auth_type, $auth_user, $auth_pw); } else if (method_exists($this, "check_auth")) { // old (pre 1.0) method name return $this->check_auth($auth_type, $auth_user, $auth_pw); } else { // no method found -> no authentication required return true; } } // }}} // {{{ UUID stuff /** * generate Unique Universal IDentifier for lock token * * @param void * @return string a new UUID */ function _new_uuid() { // use uuid extension from PECL if available if (function_exists("uuid_create")) { return uuid_create(); } // fallback $uuid = md5(microtime().getmypid()); // this should be random enough for now // set variant and version fields for 'true' random uuid $uuid{12} = "4"; $n = 8 + (ord($uuid{16}) & 3); $hex = "0123456789abcdef"; $uuid{16} = $hex{$n}; // return formated uuid return substr($uuid, 0, 8)."-" . substr($uuid, 8, 4)."-" . substr($uuid, 12, 4)."-" . substr($uuid, 16, 4)."-" . substr($uuid, 20); } /** * create a new opaque lock token as defined in RFC2518 * * @param void * @return string new RFC2518 opaque lock token */ function _new_locktoken() { return "opaquelocktoken:".$this->_new_uuid(); } // }}} // {{{ WebDAV If: header parsing /** * * * @param string header string to parse * @param int current parsing position * @return array next token (type and value) */ function _if_header_lexer($string, &$pos) { // skip whitespace while (ctype_space($string{$pos})) { ++$pos; } // already at end of string? if (strlen($string) <= $pos) { return false; } // get next character $c = $string{$pos++}; // now it depends on what we found switch ($c) { case "<": // URIs are enclosed in <...> $pos2 = strpos($string, ">", $pos); $uri = substr($string, $pos, $pos2 - $pos); $pos = $pos2 + 1; return array("URI", $uri); case "[": //Etags are enclosed in [...] if ($string{$pos} == "W") { $type = "ETAG_WEAK"; $pos += 2; } else { $type = "ETAG_STRONG"; } $pos2 = strpos($string, "]", $pos); $etag = substr($string, $pos + 1, $pos2 - $pos - 2); $pos = $pos2 + 1; return array($type, $etag); case "N": // "N" indicates negation $pos += 2; return array("NOT", "Not"); default: // anything else is passed verbatim char by char return array("CHAR", $c); } } /** * parse If: header * * @param string header string * @return array URIs and their conditions */ function _if_header_parser($str) { $pos = 0; $len = strlen($str); $uris = array(); // parser loop while ($pos < $len) { // get next token $token = $this->_if_header_lexer($str, $pos); // check for URI if ($token[0] == "URI") { $uri = $token[1]; // remember URI $token = $this->_if_header_lexer($str, $pos); // get next token } else { $uri = ""; } // sanity check if ($token[0] != "CHAR" || $token[1] != "(") { return false; } $list = array(); $level = 1; $not = ""; while ($level) { $token = $this->_if_header_lexer($str, $pos); if ($token[0] == "NOT") { $not = "!"; continue; } switch ($token[0]) { case "CHAR": switch ($token[1]) { case "(": $level++; break; case ")": $level--; break; default: return false; } break; case "URI": $list[] = $not."<$token[1]>"; break; case "ETAG_WEAK": $list[] = $not."[W/'$token[1]']>"; break; case "ETAG_STRONG": $list[] = $not."['$token[1]']>"; break; default: return false; } $not = ""; } if (isset($uris[$uri]) && is_array($uris[$uri])) { $uris[$uri] = array_merge($uris[$uri], $list); } else { $uris[$uri] = $list; } } return $uris; } /** * check if conditions from "If:" headers are meat * * the "If:" header is an extension to HTTP/1.1 * defined in RFC 2518 section 9.4 * * @param void * @return void */ function _check_if_header_conditions() { if (isset($this->_SERVER["HTTP_IF"])) { $this->_if_header_uris = $this->_if_header_parser($this->_SERVER["HTTP_IF"]); foreach ($this->_if_header_uris as $uri => $conditions) { if ($uri == "") { $uri = $this->uri; } // all must match $state = true; foreach ($conditions as $condition) { // lock tokens may be free form (RFC2518 6.3) // but if opaquelocktokens are used (RFC2518 6.4) // we have to check the format (litmus tests this) if (!strncmp($condition, "$/', $condition)) { $this->http_status("423 Locked"); return false; } } if (!$this->_check_uri_condition($uri, $condition)) { $this->http_status("412 Precondition failed"); $state = false; break; } } // any match is ok if ($state == true) { return true; } } return false; } return true; } /** * Check a single URI condition parsed from an if-header * * Check a single URI condition parsed from an if-header * * @abstract * @param string $uri URI to check * @param string $condition Condition to check for this URI * @returns bool Condition check result */ function _check_uri_condition($uri, $condition) { // not really implemented here, // implementations must override // a lock token can never be from the DAV: scheme // litmus uses DAV:no-lock in some tests if (!strncmp(" ignored for now if (method_exists($this, "checkLock")) { // is locked? $lock = $this->checkLock($path); // ... and lock is not owned? if (is_array($lock) && count($lock)) { // FIXME doesn't check uri restrictions yet if (!isset($this->_SERVER["HTTP_IF"]) || !strstr($this->_SERVER["HTTP_IF"], $lock["token"])) { if (!$exclusive_only || ($lock["scope"] !== "shared")) return false; } } } return true; } // }}} /** * Generate lockdiscovery reply from checklock() result * * @param string resource path to check * @return string lockdiscovery response */ function lockdiscovery($path) { // no lock support without checklock() method if (!method_exists($this, "checklock")) { return ""; } // collect response here $activelocks = ""; // get checklock() reply $lock = $this->checklock($path); // generate block for returned data if (is_array($lock) && count($lock)) { // check for 'timeout' or 'expires' if (!empty($lock["expires"])) { $timeout = "Second-".($lock["expires"] - time()); } else if (!empty($lock["timeout"])) { $timeout = "Second-$lock[timeout]"; } else { $timeout = "Infinite"; } // genreate response block $activelocks.= " $lock[depth] $lock[owner] $timeout $lock[token] "; } // return generated response return $activelocks; } /** * set HTTP return status and mirror it in a private header * * @param string status code and message * @return void */ function http_status($status) { // simplified success case if ($status === true) { $status = "200 OK"; } // remember status $this->_http_status = $status; // generate HTTP status response header("HTTP/1.1 $status"); header("X-WebDAV-Status: $status", true); } /** * private minimalistic version of PHP urlencode() * * only blanks, percent and XML special chars must be encoded here * full urlencode() encoding confuses some clients ... * * @param string URL to encode * @return string encoded URL */ function _urlencode($url) { return strtr($url, array(" "=>"%20", "%"=>"%25", "&"=>"%26", "<"=>"%3C", ">"=>"%3E", )); } /** * private version of PHP urldecode * * not really needed but added for completenes * * @param string URL to decode * @return string decoded URL */ function _urldecode($path) { return rawurldecode($path); } /** * UTF-8 encode property values if not already done so * * @param string text to encode * @return string utf-8 encoded text */ function _prop_encode($text) { switch (strtolower($this->_prop_encoding)) { case "utf-8": return $text; case "iso-8859-1": case "iso-8859-15": case "latin-1": default: return utf8_encode($text); } } /** * Slashify - make sure path ends in a slash * * @param string directory path * @returns string directory path wiht trailing slash */ function _slashify($path) { if ($path[strlen($path)-1] != '/') { $path = $path."/"; } return $path; } /** * Unslashify - make sure path doesn't in a slash * * @param string directory path * @returns string directory path wihtout trailing slash */ function _unslashify($path) { if ($path[strlen($path)-1] == '/') { $path = substr($path, 0, strlen($path) -1); } return $path; } /** * Merge two paths, make sure there is exactly one slash between them * * @param string parent path * @param string child path * @return string merged path */ function _mergePaths($parent, $child) { if ($child{0} == '/') { return $this->_unslashify($parent).$child; } else { return $this->_slashify($parent).$child; } } /** * mbstring.func_overload save strlen version: counting the bytes not the chars * * @param string $str * @return int */ function bytes($str) { static $func_overload; if (is_null($func_overload)) { $func_overload = @extension_loaded('mbstring') ? ini_get('mbstring.func_overload') : 0; } return $func_overload & 2 ? mb_strlen($str,'ascii') : strlen($str); } } /* * Local variables: * tab-width: 4 * c-basic-offset: 4 * End: */ ?> HTTP_WebDAV_Server-1.0.0RC6/HTTP/WebDAV/Server/Filesystem.php0000600000175000017500000007137111577376005024012 0ustar clockwerxclockwerx * @version @package-version@ */ class HTTP_WebDAV_Server_Filesystem extends HTTP_WebDAV_Server { /** * Root directory for WebDAV access * * Defaults to webserver document root (set by ServeRequest) * * @access private * @var string */ var $base = ""; /** * MySQL Host where property and locking information is stored * * @access private * @var string */ var $db_host = "localhost"; /** * MySQL database for property/locking information storage * * @access private * @var string */ var $db_name = "webdav"; /** * MySQL table name prefix * * @access private * @var string */ var $db_prefix = ""; /** * MySQL user for property/locking db access * * @access private * @var string */ var $db_user = "root"; /** * MySQL password for property/locking db access * * @access private * @var string */ var $db_passwd = ""; /** * Serve a webdav request * * @access public * @param string */ function ServeRequest($base = false) { // special treatment for litmus compliance test // reply on its identifier header // not needed for the test itself but eases debugging if (isset($this->_SERVER['HTTP_X_LITMUS'])) { error_log("Litmus test ".$this->_SERVER['HTTP_X_LITMUS']); header("X-Litmus-reply: ".$this->_SERVER['HTTP_X_LITMUS']); } // set root directory, defaults to webserver document root if not set if ($base) { $this->base = realpath($base); // TODO throw if not a directory } else if (!$this->base) { $this->base = $this->_SERVER['DOCUMENT_ROOT']; } // establish connection to property/locking db mysql_connect($this->db_host, $this->db_user, $this->db_passwd) or die(mysql_error()); mysql_select_db($this->db_name) or die(mysql_error()); // TODO throw on connection problems // let the base class do all the work parent::ServeRequest(); } /** * No authentication is needed here * * @access private * @param string HTTP Authentication type (Basic, Digest, ...) * @param string Username * @param string Password * @return bool true on successful authentication */ function check_auth($type, $user, $pass) { return true; } /** * PROPFIND method handler * * @param array general parameter passing array * @param array return array for file properties * @return bool true on success */ function PROPFIND(&$options, &$files) { // get absolute fs path to requested resource $fspath = $this->base . $options["path"]; // sanity check if (!file_exists($fspath)) { return false; } // prepare property array $files["files"] = array(); // store information for the requested path itself $files["files"][] = $this->fileinfo($options["path"]); // information for contained resources requested? if (!empty($options["depth"]) && is_dir($fspath) && is_readable($fspath)) { // make sure path ends with '/' $options["path"] = $this->_slashify($options["path"]); // try to open directory $handle = opendir($fspath); if ($handle) { // ok, now get all its contents while ($filename = readdir($handle)) { if ($filename != "." && $filename != "..") { $files["files"][] = $this->fileinfo($options["path"].$filename); } } // TODO recursion needed if "Depth: infinite" } } // ok, all done return true; } /** * Get properties for a single file/resource * * @param string resource path * @return array resource properties */ function fileinfo($path) { // map URI path to filesystem path $fspath = $this->base . $path; // create result array $info = array(); // TODO remove slash append code when base clase is able to do it itself $info["path"] = is_dir($fspath) ? $this->_slashify($path) : $path; $info["props"] = array(); // no special beautified displayname here ... $info["props"][] = $this->mkprop("displayname", strtoupper($path)); // creation and modification time $info["props"][] = $this->mkprop("creationdate", filectime($fspath)); $info["props"][] = $this->mkprop("getlastmodified", filemtime($fspath)); // Microsoft extensions: last access time and 'hidden' status $info["props"][] = $this->mkprop("lastaccessed", fileatime($fspath)); $info["props"][] = $this->mkprop("ishidden", ('.' === substr(basename($fspath), 0, 1))); // type and size (caller already made sure that path exists) if (is_dir($fspath)) { // directory (WebDAV collection) $info["props"][] = $this->mkprop("resourcetype", "collection"); $info["props"][] = $this->mkprop("getcontenttype", "httpd/unix-directory"); } else { // plain file (WebDAV resource) $info["props"][] = $this->mkprop("resourcetype", ""); if (is_readable($fspath)) { $info["props"][] = $this->mkprop("getcontenttype", $this->_mimetype($fspath)); } else { $info["props"][] = $this->mkprop("getcontenttype", "application/x-non-readable"); } $info["props"][] = $this->mkprop("getcontentlength", filesize($fspath)); } // get additional properties from database $query = "SELECT ns, name, value FROM {$this->db_prefix}properties WHERE path = '$path'"; $res = mysql_query($query); while ($row = mysql_fetch_assoc($res)) { $info["props"][] = $this->mkprop($row["ns"], $row["name"], $row["value"]); } mysql_free_result($res); return $info; } /** * detect if a given program is found in the search PATH * * helper function used by _mimetype() to detect if the * external 'file' utility is available * * @param string program name * @param string optional search path, defaults to $PATH * @return bool true if executable program found in path */ function _can_execute($name, $path = false) { // path defaults to PATH from environment if not set if ($path === false) { $path = getenv("PATH"); } // check method depends on operating system if (!strncmp(PHP_OS, "WIN", 3)) { // on Windows an appropriate COM or EXE file needs to exist $exts = array(".exe", ".com"); $check_fn = "file_exists"; } else { // anywhere else we look for an executable file of that name $exts = array(""); $check_fn = "is_executable"; } // now check the directories in the path for the program foreach (explode(PATH_SEPARATOR, $path) as $dir) { // skip invalid path entries if (!file_exists($dir)) continue; if (!is_dir($dir)) continue; // and now look for the file foreach ($exts as $ext) { if ($check_fn("$dir/$name".$ext)) return true; } } return false; } /** * try to detect the mime type of a file * * @param string file path * @return string guessed mime type */ function _mimetype($fspath) { if (is_dir($fspath)) { // directories are easy return "httpd/unix-directory"; } else if (function_exists("mime_content_type")) { // use mime magic extension if available $mime_type = mime_content_type($fspath); } else if ($this->_can_execute("file")) { // it looks like we have a 'file' command, // lets see it it does have mime support $fp = popen("file -i '$fspath' 2>/dev/null", "r"); $reply = fgets($fp); pclose($fp); // popen will not return an error if the binary was not found // and find may not have mime support using "-i" // so we test the format of the returned string // the reply begins with the requested filename if (!strncmp($reply, "$fspath: ", strlen($fspath)+2)) { $reply = substr($reply, strlen($fspath)+2); // followed by the mime type (maybe including options) if (preg_match('|^[[:alnum:]_-]+/[[:alnum:]_-]+;?.*|', $reply, $matches)) { $mime_type = $matches[0]; } } } if (empty($mime_type)) { // Fallback solution: try to guess the type by the file extension // TODO: add more ... // TODO: it has been suggested to delegate mimetype detection // to apache but this has at least three issues: // - works only with apache // - needs file to be within the document tree // - requires apache mod_magic // TODO: can we use the registry for this on Windows? // OTOH if the server is Windos the clients are likely to // be Windows, too, and tend do ignore the Content-Type // anyway (overriding it with information taken from // the registry) // TODO: have a seperate PEAR class for mimetype detection? switch (strtolower(strrchr(basename($fspath), "."))) { case ".html": $mime_type = "text/html"; break; case ".gif": $mime_type = "image/gif"; break; case ".jpg": $mime_type = "image/jpeg"; break; default: $mime_type = "application/octet-stream"; break; } } return $mime_type; } /** * HEAD method handler * * @param array parameter passing array * @return bool true on success */ function HEAD(&$options) { // get absolute fs path to requested resource $fspath = $this->base . $options["path"]; // sanity check if (!file_exists($fspath)) return false; // detect resource type $options['mimetype'] = $this->_mimetype($fspath); // detect modification time // see rfc2518, section 13.7 // some clients seem to treat this as a reverse rule // requiering a Last-Modified header if the getlastmodified header was set $options['mtime'] = filemtime($fspath); // detect resource size $options['size'] = filesize($fspath); return true; } /** * GET method handler * * @param array parameter passing array * @return bool true on success */ function GET(&$options) { // get absolute fs path to requested resource $fspath = $this->base . $options["path"]; // is this a collection? if (is_dir($fspath)) { return $this->GetDir($fspath, $options); } // the header output is the same as for HEAD if (!$this->HEAD($options)) { return false; } // no need to check result here, it is handled by the base class $options['stream'] = fopen($fspath, "r"); return true; } /** * GET method handler for directories * * This is a very simple mod_index lookalike. * See RFC 2518, Section 8.4 on GET/HEAD for collections * * @param string directory path * @return void function has to handle HTTP response itself */ function GetDir($fspath, &$options) { $path = $this->_slashify($options["path"]); if ($path != $options["path"]) { header("Location: ".$this->base_uri.$path); exit; } // fixed width directory column format $format = "%15s %-19s %-s\n"; if (!is_readable($fspath)) { return false; } $handle = opendir($fspath); if (!$handle) { return false; } echo "Index of ".htmlspecialchars($options['path'])."\n"; echo "

Index of ".htmlspecialchars($options['path'])."

\n"; echo "
";
        printf($format, "Size", "Last modified", "Filename");
        echo "
"; while ($filename = readdir($handle)) { if ($filename != "." && $filename != "..") { $fullpath = $fspath."/".$filename; $name = htmlspecialchars($filename); printf($format, number_format(filesize($fullpath)), strftime("%Y-%m-%d %H:%M:%S", filemtime($fullpath)), "$name"); } } echo "
"; closedir($handle); echo "\n"; exit; } /** * PUT method handler * * @param array parameter passing array * @return bool true on success */ function PUT(&$options) { $fspath = $this->base . $options["path"]; $dir = dirname($fspath); if (!file_exists($dir) || !is_dir($dir)) { return "409 Conflict"; // TODO right status code for both? } $options["new"] = ! file_exists($fspath); if ($options["new"] && !is_writeable($dir)) { return "403 Forbidden"; } if (!$options["new"] && !is_writeable($fspath)) { return "403 Forbidden"; } if (!$options["new"] && is_dir($fspath)) { return "403 Forbidden"; } $fp = fopen($fspath, "w"); return $fp; } /** * MKCOL method handler * * @param array general parameter passing array * @return bool true on success */ function MKCOL($options) { $path = $this->base .$options["path"]; $parent = dirname($path); $name = basename($path); if (!file_exists($parent)) { return "409 Conflict"; } if (!is_dir($parent)) { return "403 Forbidden"; } if ( file_exists($parent."/".$name) ) { return "405 Method not allowed"; } if (!empty($this->_SERVER["CONTENT_LENGTH"])) { // no body parsing yet return "415 Unsupported media type"; } $stat = mkdir($parent."/".$name, 0777); if (!$stat) { return "403 Forbidden"; } return ("201 Created"); } /** * DELETE method handler * * @param array general parameter passing array * @return bool true on success */ function DELETE($options) { $path = $this->base . "/" .$options["path"]; if (!file_exists($path)) { return "404 Not found"; } if (is_dir($path)) { $query = "DELETE FROM {$this->db_prefix}properties WHERE path LIKE '".$this->_slashify($options["path"])."%'"; mysql_query($query); System::rm(array("-rf", $path)); } else { unlink($path); } $query = "DELETE FROM {$this->db_prefix}properties WHERE path = '$options[path]'"; mysql_query($query); return "204 No Content"; } /** * MOVE method handler * * @param array general parameter passing array * @return bool true on success */ function MOVE($options) { return $this->COPY($options, true); } /** * COPY method handler * * @param array general parameter passing array * @return bool true on success */ function COPY($options, $del=false) { // TODO Property updates still broken (Litmus should detect this?) if (!empty($this->_SERVER["CONTENT_LENGTH"])) { // no body parsing yet return "415 Unsupported media type"; } // no copying to different WebDAV Servers yet if (isset($options["dest_url"])) { return "502 bad gateway"; } $source = $this->base . $options["path"]; if (!file_exists($source)) { return "404 Not found"; } if (is_dir($source)) { // resource is a collection switch ($options["depth"]) { case "infinity": // valid break; case "0": // valid for COPY only if ($del) { // MOVE? return "400 Bad request"; } break; case "1": // invalid for both COPY and MOVE default: return "400 Bad request"; } } $dest = $this->base . $options["dest"]; $destdir = dirname($dest); if (!file_exists($destdir) || !is_dir($destdir)) { return "409 Conflict"; } $new = !file_exists($dest); $existing_col = false; if (!$new) { if ($del && is_dir($dest)) { if (!$options["overwrite"]) { return "412 precondition failed"; } $dest .= basename($source); if (file_exists($dest)) { $options["dest"] .= basename($source); } else { $new = true; $existing_col = true; } } } if (!$new) { if ($options["overwrite"]) { $stat = $this->DELETE(array("path" => $options["dest"])); if (($stat{0} != "2") && (substr($stat, 0, 3) != "404")) { return $stat; } } else { return "412 precondition failed"; } } if ($del) { if (!rename($source, $dest)) { return "500 Internal server error"; } $destpath = $this->_unslashify($options["dest"]); if (is_dir($source)) { $query = "UPDATE {$this->db_prefix}properties SET path = REPLACE(path, '".$options["path"]."', '".$destpath."') WHERE path LIKE '".$this->_slashify($options["path"])."%'"; mysql_query($query); } $query = "UPDATE {$this->db_prefix}properties SET path = '".$destpath."' WHERE path = '".$options["path"]."'"; mysql_query($query); } else { if (is_dir($source)) { $files = System::find($source); $files = array_reverse($files); } else { $files = array($source); } if (!is_array($files) || empty($files)) { return "500 Internal server error"; } foreach ($files as $file) { if (is_dir($file)) { $file = $this->_slashify($file); } $destfile = str_replace($source, $dest, $file); if (is_dir($file)) { if (!file_exists($destfile)) { if (!is_writeable(dirname($destfile))) { return "403 Forbidden"; } if (!mkdir($destfile)) { return "409 Conflict"; } } else if (!is_dir($destfile)) { return "409 Conflict"; } } else { if (!copy($file, $destfile)) { return "409 Conflict"; } } } $query = "INSERT INTO {$this->db_prefix}properties SELECT * FROM {$this->db_prefix}properties WHERE path = '".$options['path']."'"; } return ($new && !$existing_col) ? "201 Created" : "204 No Content"; } /** * PROPPATCH method handler * * @param array general parameter passing array * @return bool true on success */ function PROPPATCH(&$options) { global $prefs, $tab; $msg = ""; $path = $options["path"]; $dir = dirname($path)."/"; $base = basename($path); foreach ($options["props"] as $key => $prop) { if ($prop["ns"] == "DAV:") { $options["props"][$key]['status'] = "403 Forbidden"; } else { if (isset($prop["val"])) { $query = "REPLACE INTO {$this->db_prefix}properties SET path = '$options[path]' , name = '$prop[name]' , ns= '$prop[ns]' , value = '$prop[val]'"; } else { $query = "DELETE FROM {$this->db_prefix}properties WHERE path = '$options[path]' AND name = '$prop[name]' AND ns = '$prop[ns]'"; } mysql_query($query); } } return ""; } /** * LOCK method handler * * @param array general parameter passing array * @return bool true on success */ function LOCK(&$options) { // get absolute fs path to requested resource $fspath = $this->base . $options["path"]; // TODO recursive locks on directories not supported yet // makes litmus test "32. lock_collection" fail if (is_dir($fspath) && !empty($options["depth"])) { return "409 Conflict"; } $options["timeout"] = time()+300; // 5min. hardcoded if (isset($options["update"])) { // Lock Update $where = "WHERE path = '$options[path]' AND token = '$options[update]'"; $query = "SELECT owner, exclusivelock FROM {$this->db_prefix}locks $where"; $res = mysql_query($query); $row = mysql_fetch_assoc($res); mysql_free_result($res); if (is_array($row)) { $query = "UPDATE {$this->db_prefix}locks SET expires = '$options[timeout]' , modified = ".time()." $where"; mysql_query($query); $options['owner'] = $row['owner']; $options['scope'] = $row["exclusivelock"] ? "exclusive" : "shared"; $options['type'] = $row["exclusivelock"] ? "write" : "read"; return true; } else { return false; } } $query = "INSERT INTO {$this->db_prefix}locks SET token = '$options[locktoken]' , path = '$options[path]' , created = ".time()." , modified = ".time()." , owner = '$options[owner]' , expires = '$options[timeout]' , exclusivelock = " .($options['scope'] === "exclusive" ? "1" : "0") ; mysql_query($query); return mysql_affected_rows() ? "200 OK" : "409 Conflict"; } /** * UNLOCK method handler * * @param array general parameter passing array * @return bool true on success */ function UNLOCK(&$options) { $query = "DELETE FROM {$this->db_prefix}locks WHERE path = '$options[path]' AND token = '$options[token]'"; mysql_query($query); return mysql_affected_rows() ? "204 No Content" : "409 Conflict"; } /** * checkLock() helper * * @param string resource path to check for locks * @return bool true on success */ function checkLock($path) { $result = false; $query = "SELECT owner, token, created, modified, expires, exclusivelock FROM {$this->db_prefix}locks WHERE path = '$path' "; $res = mysql_query($query); if ($res) { $row = mysql_fetch_array($res); mysql_free_result($res); if ($row) { $result = array( "type" => "write", "scope" => $row["exclusivelock"] ? "exclusive" : "shared", "depth" => 0, "owner" => $row['owner'], "token" => $row['token'], "created" => $row['created'], "modified" => $row['modified'], "expires" => $row['expires'] ); } } return $result; } /** * create database tables for property and lock storage * * @param void * @return bool true on success */ function create_database() { // TODO return false; } } /* * Local variables: * tab-width: 4 * c-basic-offset: 4 * indent-tabs-mode:nil * End: */ HTTP_WebDAV_Server-1.0.0RC6/tests/split_log.php0000600000175000017500000000064511577376005021723 0ustar clockwerxclockwerx HTTP_WebDAV_Server-1.0.0RC6/tests/test.sh0000600000175000017500000000053711577376005020531 0ustar clockwerxclockwerx#!/bin/bash (cd ..; sudo pear install -f package.xml) rm -f *.log mysql -u root webdav -e "TRUNCATE TABLE locks" mysql -u root webdav -e "TRUNCATE TABLE properties" sudo rm -rf /usr/local/apache/htdocs/mod_dav/* sudo rm -rf /usr/local/apache/htdocs/litmus/* litmus -k http://localhost/file.php # add -k to continue on errors php -q split_log.php HTTP_WebDAV_Server-1.0.0RC6/Tools/_parse_lockinfo.php0000600000175000017500000001670111577376005023022 0ustar clockwerxclockwerx * @version @package-version@ */ class _parse_lockinfo { /** * success state flag * * @var bool * @access public */ var $success = false; /** * lock type, currently only "write" * * @var string * @access public */ var $locktype = ""; /** * lock scope, "shared" or "exclusive" * * @var string * @access public */ var $lockscope = ""; /** * lock owner information * * @var string * @access public */ var $owner = ""; /** * flag that is set during lock owner read * * @var bool * @access private */ var $collect_owner = false; /** * constructor * * @param string path of stream to read * @access public */ function _parse_lockinfo($path) { // we assume success unless problems occur $this->success = true; // remember if any input was parsed $had_input = false; // open stream $f_in = fopen($path, "r"); if (!$f_in) { $this->success = false; return; } // create namespace aware parser $xml_parser = xml_parser_create_ns("UTF-8", " "); // set tag and data handlers xml_set_element_handler($xml_parser, array(&$this, "_startElement"), array(&$this, "_endElement")); xml_set_character_data_handler($xml_parser, array(&$this, "_data")); // we want a case sensitive parser xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, false); // parse input while ($this->success && !feof($f_in)) { $line = fgets($f_in); if (is_string($line)) { $had_input = true; $this->success &= xml_parse($xml_parser, $line, false); } } // finish parsing if ($had_input) { $this->success &= xml_parse($xml_parser, "", true); } // check if required tags where found $this->success &= !empty($this->locktype); $this->success &= !empty($this->lockscope); // free parser resource xml_parser_free($xml_parser); // close input stream fclose($f_in); } /** * tag start handler * * @param resource parser * @param string tag name * @param array tag attributes * @return void * @access private */ function _startElement($parser, $name, $attrs) { // namespace handling if (strstr($name, " ")) { list($ns, $tag) = explode(" ", $name); } else { $ns = ""; $tag = $name; } if ($this->collect_owner) { // everything within the tag needs to be collected $ns_short = ""; $ns_attr = ""; if ($ns) { if ($ns == "DAV:") { $ns_short = "D:"; } else { $ns_attr = " xmlns='$ns'"; } } $this->owner .= "<$ns_short$tag$ns_attr>"; } else if ($ns == "DAV:") { // parse only the essential tags switch ($tag) { case "write": $this->locktype = $tag; break; case "exclusive": case "shared": $this->lockscope = $tag; break; case "owner": $this->collect_owner = true; break; } } } /** * data handler * * @param resource parser * @param string data * @return void * @access private */ function _data($parser, $data) { // only the tag has data content if ($this->collect_owner) { $this->owner .= $data; } } /** * tag end handler * * @param resource parser * @param string tag name * @return void * @access private */ function _endElement($parser, $name) { // namespace handling if (strstr($name, " ")) { list($ns, $tag) = explode(" ", $name); } else { $ns = ""; $tag = $name; } // finished? if (($ns == "DAV:") && ($tag == "owner")) { $this->collect_owner = false; } // within we have to collect everything if ($this->collect_owner) { $ns_short = ""; $ns_attr = ""; if ($ns) { if ($ns == "DAV:") { $ns_short = "D:"; } else { $ns_attr = " xmlns='$ns'"; } } $this->owner .= ""; } } } ?> HTTP_WebDAV_Server-1.0.0RC6/Tools/_parse_propfind.php0000600000175000017500000001355111577376005023037 0ustar clockwerxclockwerx * @version @package-version@ */ class _parse_propfind { /** * success state flag * * @var bool * @access public */ var $success = false; /** * found properties are collected here * * @var array * @access public */ var $props = false; /** * internal tag nesting depth counter * * @var int * @access private */ var $depth = 0; /** * constructor * * @access public */ function _parse_propfind($path) { // success state flag $this->success = true; // property storage array $this->props = array(); // internal tag depth counter $this->depth = 0; // remember if any input was parsed $had_input = false; // open input stream $f_in = fopen($path, "r"); if (!$f_in) { $this->success = false; return; } // create XML parser $xml_parser = xml_parser_create_ns("UTF-8", " "); // set tag and data handlers xml_set_element_handler($xml_parser, array(&$this, "_startElement"), array(&$this, "_endElement")); // we want a case sensitive parser xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, false); // parse input while ($this->success && !feof($f_in)) { $line = fgets($f_in); if (is_string($line)) { $had_input = true; $this->success &= xml_parse($xml_parser, $line, false); } } // finish parsing if ($had_input) { $this->success &= xml_parse($xml_parser, "", true); } // free parser xml_parser_free($xml_parser); // close input stream fclose($f_in); // if no input was parsed it was a request if(!count($this->props)) $this->props = "all"; // default } /** * start tag handler * * @access private * @param resource parser * @param string tag name * @param array tag attributes */ function _startElement($parser, $name, $attrs) { // name space handling if (strstr($name, " ")) { list($ns, $tag) = explode(" ", $name); if ($ns == "") $this->success = false; } else { $ns = ""; $tag = $name; } // special tags at level 1: and if ($this->depth == 1) { if ($tag == "allprop") $this->props = "all"; if ($tag == "propname") $this->props = "names"; } // requested properties are found at level 2 if ($this->depth == 2) { $prop = array("name" => $tag); if ($ns) $prop["xmlns"] = $ns; $this->props[] = $prop; } // increment depth count $this->depth++; } /** * end tag handler * * @access private * @param resource parser * @param string tag name */ function _endElement($parser, $name) { // here we only need to decrement the depth count $this->depth--; } } ?> HTTP_WebDAV_Server-1.0.0RC6/Tools/_parse_proppatch.php0000600000175000017500000001500711577376005023214 0ustar clockwerxclockwerx * @version @package-version@ */ class _parse_proppatch { /** * * * @var * @access */ var $success; /** * * * @var * @access */ var $props; /** * * * @var * @access */ var $depth; /** * * * @var * @access */ var $mode; /** * * * @var * @access */ var $current; /** * constructor * * @param string path of input stream * @access public */ function _parse_proppatch($path) { $this->success = true; $this->depth = 0; $this->props = array(); $had_input = false; $f_in = fopen($path, "r"); if (!$f_in) { $this->success = false; return; } $xml_parser = xml_parser_create_ns("UTF-8", " "); xml_set_element_handler($xml_parser, array(&$this, "_startElement"), array(&$this, "_endElement")); xml_set_character_data_handler($xml_parser, array(&$this, "_data")); xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, false); while($this->success && !feof($f_in)) { $line = fgets($f_in); if (is_string($line)) { $had_input = true; $this->success &= xml_parse($xml_parser, $line, false); } } if($had_input) { $this->success &= xml_parse($xml_parser, "", true); } xml_parser_free($xml_parser); fclose($f_in); } /** * tag start handler * * @param resource parser * @param string tag name * @param array tag attributes * @return void * @access private */ function _startElement($parser, $name, $attrs) { if (strstr($name, " ")) { list($ns, $tag) = explode(" ", $name); if ($ns == "") $this->success = false; } else { $ns = ""; $tag = $name; } if ($this->depth == 1) { $this->mode = $tag; } if ($this->depth == 3) { $prop = array("name" => $tag); $this->current = array("name" => $tag, "ns" => $ns, "status"=> 200); if ($this->mode == "set") { $this->current["val"] = ""; // default set val } } if ($this->depth >= 4) { $this->current["val"] .= "<$tag"; if (isset($attr)) { foreach ($attr as $key => $val) { $this->current["val"] .= ' '.$key.'="'.str_replace('"','"', $val).'"'; } } $this->current["val"] .= ">"; } $this->depth++; } /** * tag end handler * * @param resource parser * @param string tag name * @return void * @access private */ function _endElement($parser, $name) { if (strstr($name, " ")) { list($ns, $tag) = explode(" ", $name); if ($ns == "") $this->success = false; } else { $ns = ""; $tag = $name; } $this->depth--; if ($this->depth >= 4) { $this->current["val"] .= ""; } if ($this->depth == 3) { if (isset($this->current)) { $this->props[] = $this->current; unset($this->current); } } } /** * input data handler * * @param resource parser * @param string data * @return void * @access private */ function _data($parser, $data) { if (isset($this->current)) { $this->current["val"] .= $data; } } } /* * Local variables: * tab-width: 4 * c-basic-offset: 4 * indent-tabs-mode:nil * End: */ HTTP_WebDAV_Server-1.0.0RC6/AUTHORS0000600000175000017500000000014211577376005017114 0ustar clockwerxclockwerx Authors: Hartmut Holzgraefe Christian Stocker HTTP_WebDAV_Server-1.0.0RC6/COPYING0000600000175000017500000000375611577376005017115 0ustar clockwerxclockwerxCopyright (c) 2002-2007 Christian Stocker, Hartmut Holzgraefe All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The names of the authors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. HTTP_WebDAV_Server-1.0.0RC6/dav.txt0000600000175000017500000001777711577376005017405 0ustar clockwerxclockwerxThe HTTP_WebDAV_Server class provides a framwork for the implementation of customized WebDAV servers that can provide filesystem like access to almost any kind of hierachically stored data. The (abstract) server base class tries to encapsulate as much of the protocol details as possible. It takes care of the needed WebDAV header and XML payload parsing and generation (and knows about some of the problems with common clients and tries hard to work around them). WebDAV itself is an extension to the HTTP protocol. The HTTP specific parts of it are already taken care of by the web server. Any data needed by the server class is provided by the PHP SAPI interface of the server used. To create a working server from the base class you have to extend and add methods for the actual access, modification and access control of your own data. You may use the included HTTP_WebDAV_Server_Filesystem class as an example of how to create a working server. This sample implementation is used for testing the implementation of this package against the litmus WebDAV compliance test suite. (litmus is available on http://www.webdav.org/neon/litmus) The methods you can add in your extended class are mostly named after the WebDAV specific request methods (using upper case names). Methods you may implement are: * GET() get a resource from the server * HEAD() get resource headers only from the server * PUT() create or modify a resource on the server * COPY() copy a resource on the server * MOVE() move a resource on the server * DELETE() delete a resource on the server * MKCOL() create a new collection * PROPFIND() get property data for a resource * PROPPATCH() modify property data for a resource * LOCK() lock a resource * UNLOCK() unlock a locked resource * checklock() check whether a resource is locked * check_auth() check authentication You can think of WebDAV resources as files, collections as directories and properties as filesystem meta data (like size, creation date, ...). The base class is able identify which of the methods you have implemented and will create appropriate answers to OPTIONS requests that ask for the WebDAV standards compliance level and the allowed HTTP methods for you. For a minimal working test server you need to implement GET(), PUT() and PROPFIND() only. For a minimal (level 1) standards compliant server you also need to implement MKCOL(), DELETE(), and PROPPATCH(). The COPY(), MOVE() and HEAD() methods are emulated using GET(), PUT() and DELETE() if not implemented, but for performance reasons you should better implement them yourself. For a complete (level 2) RFC2518 compliand server you also have to provide locking support by implementing LOCK(), UNLOCK() and checklock(). Authentication is not really part of the WebDAV specification and should be handled on the HTTP level. You can do so by means of, for example, .htaccess files or similar services provided by your web server. But you can also make use of the authentication features offered by PHP by implementing the check_auth() method. Using the check_auth() method you can create a dynamic interface to any authentication system protecting the data you want to serve. the following reference information may be outdated and/or incomplete ... bool PROPINFO($options, &$files) options[path] - Resource-Path options[depth] - Depth of search requested: "0", "1", or "infinity" options[props] - "all", "names", or an arry of requested properties each property array element is either a string (which implies the default "DAV:" namespace) or an array with the two elements "name" and "xmlns" for the properties name and XML namespace &$files - storage array for property results with the following elements: "files" -> array of found properties forresources. elements are: "path" -> path of the resource "props" -> properties array each property array element is either a string (which implies the default "DAV:" namespace) or an array with the two elements "name" and "xmlns" for the properties name and XML namespace you should at least support the following list of properties from the "DAV:" namespave: - resourcetype: "collection" oder "" - creationdate: unix-timestamp - getcontentlength: integer - getlastmodified: unix-timestamp You may want to add support for these "DAV:" properties, too: - getcontenttype: mime-type - displayname: string for a compliant server you also have to be able to return any property from other namespaces that has been stored using PROPPATCH return-value: true / false string MKCOL($option) options[path] - path of the new collection to be created return-value: string HTTP status and status message, possible values are * 201 Success * 403 Forbidden * 405 Method not allowed * 409 Conflict * 415 Unsupported media type * 507 Insufficient Storage (see also RFC2518 8.3.2) string GET(&$options) $options['path'] - path to the requested resource $options['ranges'] - optional array of range specifications for partial access. range specs are arrays that consist of either a 'start' and 'end' element (where 'end' can be empty to indicate a request up to the actual end of the resource) or a 'last' element to access the last n bytes of a resource without knowing its actual size in advance Return-value: true bei Erfolg, false wenn not found (TODO: andere stati berücksichtigen) Content-Type, Content-Length header müssen von der Methode selbst erzeugt werden (TODO: outdated) string PUT($options) options[path] - path to the requested resource options[content_length] - size of request data in bytes options[stream] - a PHP stream providing the input data return-value: string HTTP status, possible values are: * 201 Created -> the resource did not exist before and has been successfully created * 204 No Content -> a previously existing resource has successfully been modified * 409 Conflict ... string COPY($options) options[path] - path to the resource to be copied options[depth] - "0" or "infinity" (applies only to directories) options[overwrite] - true / false options[dest] - path to the destination resource if local options[dest_url] - non-local destination path return-value: string HTTP status, see RFC2518 8.8.5 string MOVE($options) options[path] - path to the resource to be moved options[overwrite] - true / false options[dest] - path to the destination resource if local options[dest_url] - non-local destination path return-value: string HTTP status, see RFC2518 8.9.4 string DELETE($options) options[path] - path to the resource to be removed return-value: string HTTP status, see RFC2518 8.6.2 bool check_auth($type, $user, $passwd) $type: HTTP-Auth type, i.A. "Basic" $user: HTTP Username $passwd: HTTP Passwort return-value: true bei success, sonst false (ToDo: array mit Auth-Type und Realm String zulassen bei fehler)HTTP_WebDAV_Server-1.0.0RC6/EXPERIMENTAL0000600000175000017500000000045711577376005017675 0ustar clockwerxclockwerxTHE HTTP_WebDAV_Server CLASSES ARE EXPERIMENTAL! the functionalities they provide may (and most likely will) change names or the way they work or even cease to exists in future releases and they may not perform as expected or might even not work at all use it at your own risk YOU HAVE BEEN WARNED! HTTP_WebDAV_Server-1.0.0RC6/file.php0000600000175000017500000000044711577376005017504 0ustar clockwerxclockwerxServeRequest($_SERVER["DOCUMENT_ROOT"]); ?>HTTP_WebDAV_Server-1.0.0RC6/LICENSE0000600000175000017500000000250611577376005017057 0ustar clockwerxclockwerxRedistribution and use in source and binary forms, with or without modification , are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, th is list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/ or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WA RRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABIL ITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR C ONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOW EVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILI TY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE U SE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.HTTP_WebDAV_Server-1.0.0RC6/README0000600000175000017500000000076111577376005016733 0ustar clockwerxclockwerxThis code depends on code introduced into the developement branch for PHP 4.3, so it will not run with PHP releases before 4.3.0 preliminary documentation is available in the dav.txt file, although it is currently a little outdated ... Server/Filesystem.php contains a sample implementation for a simple file server (including property and lock info storage in a mySQL database, see db/Fileserver.sql). This sample should give you a good clue about how to use this class for your own purpose. HTTP_WebDAV_Server-1.0.0RC6/TODO0000600000175000017500000000200511577376005016534 0ustar clockwerxclockwerxShort term (1.0 show stoppers): - documentation - check url en-/decodings (all pending bug reports are about this) Medium term (nice to have in 1.1): - the following items affect the fileserver example only, these are not problems of the base class itself - "Depth: Infinite" not supported yet (in PROPFIND) - properties not always copied/moved (in COPY/MOVE) - no recursove locks (in LOCK, UNLOCK etc.) - API cleanups - ETags support ... - MIME-type detection should become a package of its own? - mimetype may be guessed for stream (mime_magic extension now supports this in HEAD) - provide helper classes that extend the Server class by adding prepared LOCKing functionality, eg.: - HTTP_WebDAV_Server_Lock_MySQL - HTTP_WebDAV_Server_Lock_dbm - HTTP_WebDAV_Server_Lock_PearDB - ... (have to dig out my design pattern book ...?) Long term (definetly not in 1.0 or even 1.x): - the XML parsing parts are *very* Q&D change to simpleXML and DOM (requires PHP5) - Versioning - DASL - ...