Source for file Server.php
Documentation is available at Server.php
+----------------------------------------------------------------------+
| Copyright (c) 2002-2007 Christian Stocker, Hartmut Holzgraefe |
| Redistribution and use in source and binary forms, with or without |
| modification, are permitted provided that the following conditions |
| 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 |
| 3. The names of the authors may not be used to endorse or promote |
| products derived from this software without specific prior |
| 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. |
+----------------------------------------------------------------------+
require_once "HTTP/WebDAV/Tools/_parse_propfind.php";
require_once "HTTP/WebDAV/Tools/_parse_proppatch.php";
require_once "HTTP/WebDAV/Tools/_parse_lockinfo.php";
* Virtual base class for implementing WebDAV servers
* WebDAV server base class, needs to be extended to do useful work
* @package HTTP_WebDAV_Server
* @author Hartmut Holzgraefe <hholzgra@php.net>
* @version @package_version@
* complete URI for this request
* base URI for this request
* URI path for this request
* Realm string to be used in authentification popups
* String to be used in "X-Dav-Powered-By" header
* Remember parsed If: (RFC2518/9.4) header conditions
var $_if_header_uris = array ();
* HTTP response status/message
var $_http_status = "200 OK";
* encoding of property values passed in
var $_prop_encoding = "utf-8";
* Copy of $_SERVER superglobal array
* Derived classes may extend the constructor to
// PHP messages destroy XML output -> switch them off
// copy $_SERVER variables to local _SERVER array
// so that derived classes can simply modify these
$this->_SERVER = $_SERVER;
* Serve WebDAV HTTP request
* dispatch WebDAV HTTP request to the apropriate method handler
// prevent warning in litmus check 'delete_fragment'
if (strstr($this->_SERVER["REQUEST_URI"], '#')) {
// default uri is the complete request uri
if (isset ($this->_SERVER["HTTPS"]) && $this->_SERVER["HTTPS"] === "on") {
$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 ...
$this->uri = $uri . $path_info;
$this->path = $this->_urldecode ($path_info);
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 ...
// if a WebDAV client didn't give a path we just assume '/'
// 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
// Windows seems to require this being the last header sent
// (changed according to PECL bug #3138)
if (! $this->_check_if_header_conditions ()) {
// detect requested method names
$method = strtolower($this->_SERVER["REQUEST_METHOD"]);
$wrapper = "http_". $method;
// activate HEAD emulation by GET if no HEAD method found
$this->$wrapper(); // call method by name
} else { // method not found/implemented
if ($this->_SERVER["REQUEST_METHOD"] == "LOCK") {
header("Allow: ". join(", ", $this->_allow ())); // tell client what's allowed
// {{{ abstract WebDAV methods
* overload this method to retrieve resources from your server
* @param array &$params Array of input and output parameters
* @returns int HTTP-Statuscode
// dummy entry for PHPDoc
* @returns int HTTP-Statuscode
// dummy entry for PHPDoc
* @returns int HTTP-Statuscode
// dummy entry for PHPDoc
* @returns int HTTP-Statuscode
// dummy entry for PHPDoc
* @returns int HTTP-Statuscode
// dummy entry for PHPDoc
* PROPFIND implementation
* PROPFIND implementation
* @returns int HTTP-Statuscode
// dummy entry for PHPDoc
* PROPPATCH implementation
* PROPPATCH implementation
* @returns int HTTP-Statuscode
// dummy entry for PHPDoc
* @returns int HTTP-Statuscode
// dummy entry for PHPDoc
* @returns int HTTP-Statuscode
// dummy entry for PHPDoc
// {{{ other abstract methods
* overload this method to retrieve and confirm authentication information
* @param string type Authentication type, e.g. "basic" or "digest"
* @param string username Transmitted username
* @param string passwort Transmitted password
* @returns bool Authentication status
function checkAuth($type, $username, $password)
// dummy entry for PHPDoc
* check lock status for a resource
* overload this method to return shared and exclusive locks
* active for this resource
* @param string resource Resource path to check
* @returns array An array of lock entries each consisting
* of 'type' ('shared'/'exclusive'), 'token' and 'timeout'
function checklock($resource)
// dummy entry for PHPDoc
// {{{ WebDAV HTTP method wrappers
* The OPTIONS method handler creates a valid OPTIONS reply
* including Dav: and Allowed: headers
* based on the implemented methods found in the actual instance
// Microsoft clients default to the Frontpage protocol
// unless we tell them to use WebDAV
$allow = $this->_allow ();
$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
* PROPFIND method handler
$options["path"] = $this->path;
// search depth from header (default is "infinity)
if (isset ($this->_SERVER['HTTP_DEPTH'])) {
$options["depth"] = $this->_SERVER["HTTP_DEPTH"];
$options["depth"] = "infinity";
// analyze request payload
if (!$propinfo->success ) {
$options['props'] = $propinfo->props;
if (!$this->PROPFIND ($options, $files)) {
$files = array ("files" => array ());
$lock = $this->checkLock ($this->path);
$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'])) {
// collect namespaces here
// 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"])) {
// 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']) {
// only the names of all existing properties were requested
// so we remove all values
unset ($files["files"][$filekey]["props"][$key]["val"]);
// search property name in requested properties
foreach ((array) $options["props"] as $reqprop) {
if (!isset ($reqprop["xmlns"])) {
if ( $reqprop["name"] == $prop["name"]
&& $reqprop["xmlns"] == $prop["ns"]) {
// unset property and continue with next one if not found/requested
$files["files"][$filekey]["props"][$key]= "";
if (empty ($prop["ns"])) continue; // no namespace
if ($ns == "DAV:") continue; // default namespace
if (isset ($ns_hash[$ns])) continue; // already known
$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
foreach ($options["props"] as $reqprop) {
if ($reqprop['name']== "") continue; // skip empty entries
if (!isset ($reqprop["xmlns"])) {
// check if property exists in result
foreach ($file["props"] as $prop) {
if ( $reqprop["name"] == $prop["name"]
&& $reqprop["xmlns"] == $prop["ns"]) {
if ($reqprop["xmlns"]=== "DAV:" && $reqprop["name"]=== "lockdiscovery") {
// lockdiscovery is handled by the base class
$files["files"][$filekey]["props"][]
// 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 ...
header('Content-Type: text/xml; charset="utf-8"');
echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
echo "<D:multistatus xmlns:D=\"DAV:\">\n";
foreach ($files["files"] as $file) {
// ignore empty or incomplete entries
if (!is_array($file) || empty ($file) || !isset ($file["path"])) continue;
if (!is_string($path) || $path=== "") continue;
echo " <D:response $ns_defs>\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 " <D:href>$href</D:href>\n";
// report all found properties and their values (if any)
if (isset ($file["props"]) && is_array($file["props"])) {
foreach ($file["props"] as $key => $prop) {
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 " <D:$prop[name]/>\n";
} else if (!empty ($prop["ns"])) {
echo " <". $ns_hash[$prop["ns"]]." :$prop[name]/>\n";
echo " <$prop[name] xmlns=\"\"/>";
} else if ($prop["ns"] == "DAV:") {
// some WebDAV properties need special treatment
echo " <D:creationdate ns0:dt=\"dateTime.tz\">"
. gmdate("Y-m-d\\TH:i:s\\Z", $prop['val'])
echo " <D:getlastmodified ns0:dt=\"dateTime.rfc1123\">"
. gmdate("D, d M Y H:i:s ", $prop['val'])
. "GMT</D:getlastmodified>\n";
echo " <D:resourcetype><D:$prop[val]/></D:resourcetype>\n";
echo " <D:supportedlock>$prop[val]</D:supportedlock>\n";
echo " <D:lockdiscovery>\n";
echo " </D:lockdiscovery>\n";
// the following are non-standard Microsoft extensions to the DAV namespace
echo " <D:lastaccessed ns0:dt=\"dateTime.rfc1123\">"
. gmdate("D, d M Y H:i:s ", $prop['val'])
. "GMT</D:lastaccessed>\n";
. is_string($prop['val']) ? $prop['val'] : ($prop['val'] ? 'true' : 'false')
// properties from namespaces != "DAV:" or without any namespace
echo " <" . $ns_hash[$prop["ns"]] . " :$prop[name]>"
. "</" . $ns_hash[$prop["ns"]] . " :$prop[name]>\n";
echo " <$prop[name] xmlns=\"\">"
echo " <D:status>HTTP/1.1 200 OK</D:status>\n";
// now report all properties requested but not found
if (isset ($file["noprops"])) {
foreach ($file["noprops"] as $key => $prop) {
if ($prop["ns"] == "DAV:") {
echo " <D:$prop[name]/>\n";
} else if ($prop["ns"] == "") {
echo " <$prop[name] xmlns=\"\"/>\n";
echo " <" . $ns_hash[$prop["ns"]] . " :$prop[name]/>\n";
echo " <D:status>HTTP/1.1 404 Not Found</D:status>\n";
echo "</D:multistatus>\n";
* PROPPATCH method handler
if ($this->_check_lock_status ($this->path)) {
$options["path"] = $this->path;
if (!$propinfo->success ) {
$options['props'] = $propinfo->props;
$responsedescr = $this->PROPPATCH ($options);
header('Content-Type: text/xml; charset="utf-8"');
echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
echo "<D:multistatus xmlns:D=\"DAV:\">\n";
echo " <D:href>". $this->_urlencode ($this->_mergePaths ($this->_SERVER["SCRIPT_NAME"], $this->path)). "</D:href>\n";
foreach ($options["props"] as $prop) {
echo " <D:prop><$prop[name] xmlns=\"$prop[ns]\"/></D:prop>\n";
echo " <D:status>HTTP/1.1 $prop[status]</D:status>\n";
echo " <D:responsedescription>".
"</D:responsedescription>\n";
echo "</D:multistatus>\n";
$options["path"] = $this->path;
$stat = $this->MKCOL ($options);
// TODO check for invalid stream
$options["path"] = $this->path;
$this->_get_ranges ($options);
if (true === ($status = $this->GET ($options))) {
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");
if (isset ($range['end'])) {
$size = $range['end']- $range['start']+1;
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);
if (isset ($options['size'])) {
header("Content-length: ". ($options['size'] - $range['start']));
header("Content-range: ". $range['start']. "-". $range['end']. "/"
. (isset ($options['size']) ? $options['size'] : "*"));
header("Content-length: ". $range['last']);
fseek($options['stream'], - $range['last'], SEEK_END );
$this->_multipart_byterange_header (); // init multipart
foreach ($options['ranges'] as $range) {
// TODO what if size unknown? 500?
if (isset ($range['start'])) {
$to = !empty ($range['end']) ? $range['end'] : $options['size']-1;
$from = $options['size'] - $range['last']-1;
$to = $options['size'] -1;
$total = isset ($options['size']) ? $options['size'] : "*";
$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);
$this->_multipart_byterange_header (); // end multipart
// normal request or stream isn't seekable, return full content
if (isset ($options['size'])) {
header("Content-length: ". $options['size']);
return; // no more headers
} elseif (isset ($options['data'])) {
// reply to partial request
header("Content-length: ". $this->bytes($options['data']));
// TODO: check setting of headers in various code paths above
* parse HTTP Range: header
* @param array options array to store result in
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 ("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 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)) {
// a little naive, this sequence *might* be part of the content
// but it's really not likely and rather expensive to check
header("Content-type: multipart/byteranges; boundary=". $this->multipart_separator);
// generate closing multipart sequence
echo " \n--{$this->multipart_separator}-- ";
// 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);
$options["path"] = $this->path;
$status = $this->head($options);
$status = $this->GET($options);
if (!isset($options['size'])) {
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";
if ($this->_check_lock_status($this->path)) {
$options["path"] = $this->path;
$options["content_length"] = $this->_SERVER["CONTENT_LENGTH"];
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)) {
echo "The service does not support mulipart PUT requests";
$options["content_type"] = $this->_SERVER["CONTENT_TYPE"];
// 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;
case 'HTTP_CONTENT_ENCODING': // RFC 2616 14.11
// TODO support this if ext/zlib filters are available
echo "The service does not support ' $val' content encoding" ;
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;
case 'HTTP_CONTENT_LENGTH':
// defined on IIS and has the same value as CONTENT_LENGTH
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. */
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)) {
echo "The service does only support single byte ranges";
$range = array("start" => $matches[1], "end" => $matches[2]);
$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 ...
case 'HTTP_CONTENT_TYPE':
// defined on IIS and has the same value as CONTENT_TYPE
case 'HTTP_CONTENT_MD5': // RFC 2616 14.15
// TODO: maybe we can just pretend here?
echo "The service does not support content MD5 checksum verification";
// any other unknown Content-* headers
echo "The service does not support ' $key'" ;
$options["stream"] = fopen("php://input", "r");
$stat = $this->PUT($options);
$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'])) {
$data = fread($options['stream'], $length);
$data = fread($options['stream'], 8192);
$stat = "400 Bad request";
if (false === fwrite($stream, $data)) {
while (!feof($options["stream"])) {
if (false === fwrite($stream, fread($options["stream"], 8192))) {
// check RFC 2518 Section 9.2, last paragraph
if (isset($this->_SERVER["HTTP_DEPTH"])) {
if ($this->_SERVER["HTTP_DEPTH"] != "infinity") {
if ($this->_check_lock_status($this->path)) {
$options["path"] = $this->path;
$stat = $this->DELETE($options);
// no need to check source lock status here
// destination lock status is always checked by the helper method
$this->_copymove("copy");
if ($this->_check_lock_status($this->path)) {
// destination lock status is always checked by the helper method
$this->_copymove("move");
$options["path"] = $this->path;
if (isset($this->_SERVER['HTTP_DEPTH'])) {
$options["depth"] = $this->_SERVER["HTTP_DEPTH"];
$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)) {
$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);
// extract lock request information from request XML payload
$lockinfo = new <a href="../HTTP_WebDAV_Server/_parse_lockinfo.html">_parse_lockinfo</a>("php://input");
if (!$lockinfo->success) {
// check if locking is possible
if (!$this->_check_lock_status($this->path, $lockinfo->lockscope === "shared")) {
$options["scope"] = $lockinfo->lockscope;
$options["type"] = $lockinfo->locktype;
$options["owner"] = $lockinfo->owner;
$options["locktoken"] = $this->_new_locktoken();
$stat = $this->LOCK($options);
$http_stat = $stat ? "200 OK" : "423 Locked";
$http_stat = (string)$stat;
if ($http_stat{0} == 2) { // 2xx states are ok
if ($options["timeout"]) {
// if multiple timeout values were given we take the first only
reset($options["timeout"]);
$options["timeout"] = current($options["timeout"]);
// if the timeout is numeric only we need to reformat it
// 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());
$timeout = "Second- $options[timeout ]" ;
// 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"];
header('Content-Type: text/xml; charset="utf-8"');
header("Lock-Token: < $options[locktoken ]>" );
echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
echo "<D:prop xmlns:D=\"DAV:\">\n";
echo " <D:lockdiscovery>\n";
echo " <D:activelock>\n";
echo " <D:lockscope><D: $options[scope ]/></D:lockscope>\n" ;
echo " <D:locktype><D: $options[type ]/></D:locktype>\n" ;
echo " <D:depth> $options[depth ]</D:depth>\n" ;
echo " <D:owner> $options[owner ]</D:owner>\n" ;
echo " <D:timeout> $timeout</D:timeout>\n" ;
echo " <D:locktoken><D:href> $options[locktoken ]</D:href></D:locktoken>\n" ;
echo " </D:activelock>\n";
echo " </D:lockdiscovery>\n";
$options["path"] = $this->path;
if (isset($this->_SERVER['HTTP_DEPTH'])) {
$options["depth"] = $this->_SERVER["HTTP_DEPTH"];
$options["depth"] = "infinity";
$options["token"] = substr(trim($this->_SERVER["HTTP_LOCK_TOKEN"]), 1, -1);
$stat = $this->UNLOCK($options);
function _copymove($what)
$options["path"] = $this->path;
if (isset($this->_SERVER["HTTP_DEPTH"])) {
$options["depth"] = $this->_SERVER["HTTP_DEPTH"];
$options["depth"] = "infinity";
$http_header_host = preg_replace("/:80$/", "", $this->_SERVER["HTTP_HOST"]);
$url = parse_url($this->_SERVER["HTTP_DESTINATION"]);
if (isset($url["host"])) {
// TODO check url scheme, too
$http_host = $url["host"];
if (isset($url["port"]) && $url["port"] != 80)
$http_host.= ":".$url["port"];
// 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"])) {
$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";
$options["overwrite"] = true;
$stat = $this->$what($options);
* check for implemented HTTP methods
* @return array something
// 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
if (!strncmp("http_", $method, 5)) {
$allow[$method] = $method;
// we can emulate a missing HEAD implemetation using GET
if (isset($allow["GET"]))
// no LOCK without checklok()
* helper for property element creation
* @param string XML namespace (optional)
* @param string property name
* @param string property value
* @return array property array
return array("ns" => $args[0],
return array("ns" => "DAV:",
* check authentication if check is implemented
* @return bool true if authentication succeded or not necessary
$auth_type = isset($this->_SERVER["AUTH_TYPE"])
? $this->_SERVER["AUTH_TYPE"]
$auth_user = isset($this->_SERVER["PHP_AUTH_USER"])
? $this->_SERVER["PHP_AUTH_USER"]
$auth_pw = isset($this->_SERVER["PHP_AUTH_PW"])
? $this->_SERVER["PHP_AUTH_PW"]
// PEAR style method name
return $this->checkAuth($auth_type, $auth_user, $auth_pw);
// old (pre 1.0) method name
return $this->check_auth($auth_type, $auth_user, $auth_pw);
// no method found -> no authentication required
* generate Unique Universal IDentifier for lock token
* @return string a new UUID
// use uuid extension from PECL if available
// set variant and version fields for 'true' random uuid
$n = 8 + (ord($uuid{16}) & 3);
$hex = "0123456789abcdef";
return substr($uuid, 0, 8)."-"
* create a new opaque lock token as defined in RFC2518
* @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)
// already at end of string?
if (strlen($string) <= $pos) {
// now it depends on what we found
// URIs are enclosed in <...>
$pos2 = strpos($string, ">", $pos);
$uri = substr($string, $pos, $pos2 - $pos);
return array("URI", $uri);
//Etags are enclosed in [...]
if ($string{$pos} == "W") {
$pos2 = strpos($string, "]", $pos);
$etag = substr($string, $pos + 1, $pos2 - $pos - 2);
return array($type, $etag);
// "N" indicates negation
return array("NOT", "Not");
// anything else is passed verbatim char by char
return array("CHAR", $c);
* @param string header string
* @return array URIs and their conditions
function _if_header_parser($str)
$token = $this->_if_header_lexer($str, $pos);
if ($token[0] == "URI") {
$uri = $token[1]; // remember URI
$token = $this->_if_header_lexer($str, $pos); // get next token
if ($token[0] != "CHAR" || $token[1] != "(") {
$token = $this->_if_header_lexer($str, $pos);
if ($token[0] == "NOT") {
$list[] = $not."< $token[1 ]>" ;
$list[] = $not."[W/' $token[1 ]']>" ;
$list[] = $not."[' $token[1 ]']>" ;
if (isset($uris[$uri]) && is_array($uris[$uri])) {
* 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
function _check_if_header_conditions()
if (isset($this->_SERVER["HTTP_IF"])) {
$this->_if_header_parser($this->_SERVER["HTTP_IF"]);
foreach ($this->_if_header_uris as $uri => $conditions) {
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, "<opaquelocktoken:", strlen("<opaquelocktoken"))) {
if (!preg_match('/^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$/', $condition)) {
if (!$this->_check_uri_condition($uri, $condition)) {
* Check a single URI condition parsed from an if-header
* Check a single URI condition parsed from an if-header
* @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("<DAV:", $condition, 5)) {
* @param string path of resource to check
* @param bool exclusive lock?
function _check_lock_status($path, $exclusive_only = false)
// FIXME depth -> ignored for now
$lock = $this->checkLock($path);
// ... and lock is not owned?
// 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"))
* Generate lockdiscovery reply from checklock() result
* @param string resource path to check
* @return string lockdiscovery response
// no lock support without checklock() method
$lock = $this->checklock($path);
// generate <activelock> block for returned data
// check for 'timeout' or 'expires'
if (!empty($lock["expires"])) {
$timeout = "Second-".($lock["expires"] - time());
} else if (!empty($lock["timeout"])) {
$timeout = "Second- $lock[timeout ]" ;
// genreate response block
<D:lockscope><D: $lock[scope ]/></D:lockscope>
<D:locktype><D: $lock[type ]/></D:locktype>
<D:depth> $lock[depth ]</D:depth>
<D:owner> $lock[owner ]</D:owner>
<D:timeout> $timeout</D:timeout>
<D:locktoken><D:href> $lock[token ]</D:href></D:locktoken>
// return generated response
* set HTTP return status and mirror it in a private header
* @param string status code and message
function http_status($status)
// simplified success case
$this->_http_status = $status;
// generate HTTP status response
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",
* private version of PHP urldecode
* not really needed but added for completenes
* @param string URL to decode
* @return string decoded URL
function _urldecode($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)
* 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] != '/') {
* 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] == '/') {
* 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)
return $this->_unslashify($parent).$child;
return $this->_slashify($parent).$child;
* mbstring.func_overload save strlen version: counting the bytes not the chars
Documentation generated on Mon, 11 Mar 2019 15:50:56 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.
|