mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-27 10:23:28 +01:00
392c55f31b
Addressbook does NOT allow to specify the URL, unlike iCal which allows it after autodetection fails. This, some XML specifics set now for Apple addressbook user-agents and etags for addressbook collection itself allow now to use EGroupware with iPhone or Mac addressbook. The later was working before, if you edited the URL into a decompiled plist file, but failed now because of a new REPORT it tries on the principal, to find out shared addessbooks, which we not yet support, but failed to tell in the correct way (501 Not Implemented). Addressbook sync now the personal addressbook, because that is what we tell it as addressbook-home-set. We should add some configuration so user can choose what addressbook to set as addressbook-home-set, or to set the "all" addressbook (/addressbook). For the later we could add some prefs like SyncML to specify filters or eg. a distribution list.
2634 lines
84 KiB
PHP
2634 lines
84 KiB
PHP
<?php
|
|
//
|
|
// +----------------------------------------------------------------------+
|
|
// | PHP Version 4 |
|
|
// +----------------------------------------------------------------------+
|
|
// | Copyright (c) 1997-2003 The PHP Group |
|
|
// +----------------------------------------------------------------------+
|
|
// | This source file is subject to version 2.02 of the PHP license, |
|
|
// | that is bundled with this package in the file LICENSE, and is |
|
|
// | available at through the world-wide-web at |
|
|
// | http://www.php.net/license/2_02.txt. |
|
|
// | If you did not receive a copy of the PHP license and are unable to |
|
|
// | obtain it through the world-wide-web, please send a note to |
|
|
// | license@php.net so we can mail you a copy immediately. |
|
|
// +----------------------------------------------------------------------+
|
|
// | Authors: Hartmut Holzgraefe <hholzgra@php.net> |
|
|
// | Christian Stocker <chregu@bitflux.ch> |
|
|
// +----------------------------------------------------------------------+
|
|
//
|
|
// $Id: Server.php,v 1.56 2006/10/10 11:53:16 hholzgra Exp $
|
|
//
|
|
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@
|
|
*/
|
|
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;
|
|
|
|
/**
|
|
* Set if client requires <D:href> to be a url (true) or a path (false).
|
|
* RFC 4918 allows both: http://www.webdav.org/specs/rfc4918.html#ELEMENT_href
|
|
* But some clients can NOT deal with one or the other!
|
|
*
|
|
* @var boolean
|
|
*/
|
|
var $client_require_href_as_url;
|
|
|
|
/**
|
|
* Set if client requires does not allow namespace redundacy.
|
|
* The XML Namespace specification does allow both
|
|
* But some clients can NOT deal with one or the other!
|
|
*
|
|
* @var boolean (client_refuses_redundand_namespace_declarations)
|
|
* @var boolean (client_needs_redundand_namespace_declarations)
|
|
*/
|
|
var $crrnd = false;
|
|
var $cnrnd = false;
|
|
|
|
/**
|
|
|
|
|
|
/**
|
|
* 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 is currently to use just the path, extending class can set $this->client_require_href_as_url depending on user-agent
|
|
if ($this->client_require_href_as_url)
|
|
{
|
|
// default uri is the complete request uri
|
|
$uri = (@$this->_SERVER["HTTPS"] === "on" ? "https:" : "http:") . '//'.$this->_SERVER['HTTP_HOST'];
|
|
}
|
|
// we cant use SCRIPT_NAME, because it fails, if there's any url rewriting
|
|
//error_log("pathinfo:\n". $this->_urldecode($this->_SERVER['REQUEST_URI']).":\n".$this->_SERVER['PATH_INFO']);
|
|
$uri .= $this->_urldecode($this->_SERVER['REQUEST_URI']);
|
|
if (!empty($this->_SERVER["PATH_INFO"]))
|
|
{
|
|
$uri = substr($uri,0,-strlen($this->_SERVER["PATH_INFO"]));
|
|
}
|
|
|
|
$path_info = empty($this->_SERVER["PATH_INFO"]) ? "/" : $this->_SERVER["PATH_INFO"];
|
|
|
|
$this->base_uri = $uri;
|
|
$this->uri = $uri . $path_info;
|
|
// set path
|
|
// $_SERVER['PATH_INFO'] is already urldecoded
|
|
//$this->path = $this->_urldecode($path_info);
|
|
// quote '#' (e.g. OpenOffice uses this for lock-files)
|
|
$this->path = strtr($path_info,array(
|
|
'%' => '%25',
|
|
'#' => '%23',
|
|
'?' => '%3F',
|
|
));
|
|
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") {
|
|
$error = '412 Precondition failed';
|
|
;
|
|
} else {
|
|
$error = '405 Method not allowed';
|
|
header("Allow: ".join(", ", $this->_allow())); // tell client what's allowed
|
|
}
|
|
$this->http_status($error);
|
|
echo "<html><head><title>Error $error</title></head>\n";
|
|
echo "<body><h1>$error</h1>\n";
|
|
echo "The requested could not by handled by this server.\n";
|
|
echo '(URI ' . $this->_SERVER['REQUEST_URI'] . ")<br>\n<br>\n";
|
|
echo "</body></html>\n";
|
|
}
|
|
}
|
|
|
|
// }}}
|
|
|
|
// {{{ abstract WebDAV methods
|
|
|
|
// {{{ GET()
|
|
/**
|
|
* GET implementation
|
|
*
|
|
* overload this method to retrieve resources from your server
|
|
* <br>
|
|
*
|
|
*
|
|
* @abstract
|
|
* @param array &$params Array of input and output parameters
|
|
* <br><b>input</b><ul>
|
|
* <li> path -
|
|
* </ul>
|
|
* <br><b>output</b><ul>
|
|
* <li> size -
|
|
* </ul>
|
|
* @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
|
|
}
|
|
*/
|
|
// }}}
|
|
|
|
// {{{ ACL()
|
|
|
|
/**
|
|
* ACL implementation
|
|
*
|
|
* ACL implementation
|
|
*
|
|
* @abstract
|
|
* @param array &$params
|
|
* @returns int HTTP-Statuscode
|
|
*/
|
|
|
|
/* abstract
|
|
function ACL()
|
|
{
|
|
// 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: heaers
|
|
* 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
|
|
}
|
|
|
|
// allow extending class to modify DAV and Allow headers
|
|
if (method_exists($this,'OPTIONS')) {
|
|
$this->OPTIONS($this->path,$dav,$allow);
|
|
}
|
|
|
|
// 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 string $handler='PROPFIND' allows to use method eg. for CalDAV REPORT
|
|
* @return void
|
|
*/
|
|
function http_PROPFIND($handler='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['root'] = $propinfo->root;
|
|
$options['props'] = $propinfo->props;
|
|
if ($propinfo->filters)
|
|
$options['filters'] = $propinfo->filters;
|
|
if ($propinfo->other)
|
|
$options['other'] = $propinfo->other;
|
|
|
|
// call user handler
|
|
if (!($retval =$this->$handler($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;
|
|
}
|
|
}
|
|
|
|
// now we generate the reply header ...
|
|
if ($retval === true)
|
|
{
|
|
$this->http_status('207 Multi-Status');
|
|
}
|
|
elseif (is_string($retval))
|
|
{
|
|
$this->http_status($retval);
|
|
header('Content-Type: text/html');
|
|
echo "<html><head><title>Error $retval</title></head>\n";
|
|
echo "<body><h1>$retval</h1>\n";
|
|
switch (substr($retval, 0 ,3))
|
|
{
|
|
case '501': // Not Implemented
|
|
echo "The requested feature is not (yet) supported by this server.\n";
|
|
break;
|
|
default:
|
|
echo "The request could not be handled by this server.\n";
|
|
}
|
|
echo '(URI ' . $this->_SERVER['REQUEST_URI'] . ")<br>\n<br>\n";
|
|
echo "</body></html>\n";
|
|
return;
|
|
}
|
|
// dav header
|
|
$dav = array(1); // assume we are always dav class 1 compliant
|
|
$allow = false;
|
|
|
|
// allow extending class to modify DAV
|
|
if (method_exists($this,'OPTIONS')) {
|
|
$this->OPTIONS($this->path,$dav,$allow);
|
|
}
|
|
header("DAV: " .join(", ", $dav));
|
|
header('Content-Type: text/xml; charset="utf-8"');
|
|
|
|
// ... and payload
|
|
echo "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
|
echo $this->crrnd ? "<multistatus xmlns=\"DAV:\">\n" : "<D:multistatus xmlns:D=\"DAV:\">\n";
|
|
|
|
// using an ArrayIterator to prevent foreach from copying the array,
|
|
// as we cant loop by reference, when an iterator is given in $files['files']
|
|
if (is_array($files['files']))
|
|
{
|
|
$files['files'] = new ArrayIterator($files['files']);
|
|
}
|
|
// now we loop over all returned file entries
|
|
foreach ($files['files'] as $file) {
|
|
|
|
// collect namespaces here
|
|
$ns_hash = array('DAV:' => 'D');
|
|
|
|
// Microsoft Clients need this special namespace for date and time values
|
|
$ns_defs = 'xmlns:ns0="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/"';
|
|
|
|
// 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 &$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
|
|
|
|
// this can happen if we have allprop and prop in one propfind:
|
|
// <allprop /><prop><blah /></prop>, eg. blah is not automatic returned by allprop
|
|
switch(is_array($options['props']) ? $options['props'][0] : $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($prop["val"]);
|
|
break;
|
|
|
|
default:
|
|
$found = false;
|
|
|
|
// search property name in requested properties
|
|
foreach ((array)$options["props"] as $reqprop) {
|
|
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) {
|
|
$prop="";
|
|
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 (!is_array($reqprop) || $reqprop['name']=="") continue; // skip empty entries, or 'all' if <allprop /> used together with <prop>
|
|
|
|
$found = false;
|
|
|
|
// 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
|
|
$file["props"][]
|
|
= $this->mkprop("DAV:",
|
|
"lockdiscovery",
|
|
$this->lockdiscovery($file['path']));
|
|
// only collect $file['noprops'] if we have NO Brief: t HTTP Header
|
|
} elseif (!isset($this->_SERVER['HTTP_BRIEF']) || $this->_SERVER['HTTP_BRIEF'] != 't') {
|
|
// add empty value for this property
|
|
$file["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]\"";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// ignore empty or incomplete entries
|
|
if (!is_array($file) || empty($file) || !isset($file["path"])) continue;
|
|
$path = $file['path'];
|
|
if (!is_string($path) || $path==="") continue;
|
|
|
|
if ($this->crrnd)
|
|
{
|
|
echo " <response $ns_defs>\n";
|
|
}
|
|
else
|
|
{
|
|
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 */
|
|
// path needs to be urlencoded (only basic version of this class!)
|
|
$href = $this->_urlencode($this->_mergePathes($this->base_uri, $path));
|
|
|
|
if ($this->crrnd)
|
|
{
|
|
echo " <href>$href</href>\n";
|
|
}
|
|
else
|
|
{
|
|
echo " <D:href>$href</D:href>\n";
|
|
}
|
|
|
|
// report all found properties and their values (if any)
|
|
if (isset($file["props"]) && is_array($file["props"])) {
|
|
echo ' <'.($this->crrnd?'':'D:')."propstat>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."prop>\n";
|
|
|
|
foreach ($file["props"] as &$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 ' <'.($this->crrnd?'':'D:')."$prop[name]/>\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 ' <'.($this->crrnd?'':'D:')."creationdate ns0:dt=\"dateTime.tz\">"
|
|
. gmdate("Y-m-d\\TH:i:s\\Z", $prop['val'])
|
|
. '</'.($this->crrnd?'':'D:')."creationdate>\n";
|
|
break;
|
|
case "getlastmodified":
|
|
echo ' <'.($this->crrnd?'':'D:')."getlastmodified ns0:dt=\"dateTime.rfc1123\">"
|
|
. gmdate("D, d M Y H:i:s ", $prop['val'])
|
|
. "GMT</".($this->crrnd?'':'D:')."getlastmodified>\n";
|
|
break;
|
|
case "supportedlock":
|
|
echo ' <'.($this->crrnd?'':'D:')."supportedlock>$prop[val]</".($this->crrnd?'':'D:')."supportedlock>\n";
|
|
break;
|
|
case "lockdiscovery":
|
|
echo ' <'.($this->crrnd?'':'D:')."lockdiscovery>\n";
|
|
echo $prop["val"];
|
|
echo ' </'.($this->crrnd?'':'D:')."lockdiscovery>\n";
|
|
break;
|
|
default:
|
|
$ns_defs = '';
|
|
if (is_array($prop['val']))
|
|
{
|
|
$hns_hash = $ns_hash;
|
|
$val = $this->_hierarchical_prop_encode($prop['val'], 'DAV:', $ns_defs, $hns_hash);
|
|
} elseif (isset($prop['raw'])) {
|
|
$val = $this->_prop_encode('<![CDATA['.$prop['val'].']]>');
|
|
} else {
|
|
$val = $this->_prop_encode(htmlspecialchars($prop['val']));
|
|
}
|
|
echo ' <'.($this->crrnd?'':'D:')."$prop[name]$ns_defs>$val".
|
|
'</'.($this->crrnd?'':'D:')."$prop[name]>\n";
|
|
break;
|
|
}
|
|
} else {
|
|
// allow multiple values and attributes, required eg. for caldav:supported-calendar-component-set
|
|
if ($prop['ns'] && is_array($prop['val'])) {
|
|
if (!isset($ns_hash[$prop['ns']])) {
|
|
$ns_name = "ns".(count($ns_hash) + 1);
|
|
$ns_hash[$prop['ns']] = $ns_name;
|
|
}
|
|
$vals = $extra_ns = '';
|
|
foreach($prop['val'] as $subprop)
|
|
{
|
|
if ($subprop['ns'] && $subprop['ns'] != 'DAV:') {
|
|
// register property namespace if not known yet
|
|
if (!isset($ns_hash[$subprop['ns']])) {
|
|
$ns_name = "ns".(count($ns_hash) + 1);
|
|
$ns_hash[$subprop['ns']] = $ns_name;
|
|
} else {
|
|
$ns_name = $ns_hash[$subprop['ns']];
|
|
}
|
|
if (strchr($extra_ns,$extra=' xmlns:'.$ns_name.'="'.$subprop['ns'].'"') === false) {
|
|
$extra_ns .= $extra;
|
|
}
|
|
$ns_name .= ':';
|
|
} elseif ($subprop['ns'] == 'DAV:') {
|
|
$ns_name = 'D:';
|
|
} else {
|
|
$ns_name = '';
|
|
}
|
|
$vals .= "<$ns_name$subprop[name]";
|
|
if (is_array($subprop['val'])) // val contains only attributes, no value
|
|
{
|
|
foreach($subprop['val'] as $attr => $val)
|
|
{
|
|
$vals .= ' '.$attr.'="'.htmlspecialchars($val).'"';
|
|
}
|
|
$vals .= '/>';
|
|
}
|
|
else
|
|
{
|
|
$vals .= '>';
|
|
if (isset($subprop['raw'])) {
|
|
$vals .= '<![CDATA['.$subprop['val'].']]>';
|
|
} else {
|
|
$vals .= htmlspecialchars($subprop['val']);
|
|
}
|
|
$vals .= "</$ns_name$subprop[name]>";
|
|
}
|
|
}
|
|
echo ' <'.$ns_hash[$prop['ns']].":$prop[name]$extra_ns>$vals</".$ns_hash[$prop['ns']].":$prop[name]>\n";
|
|
} else {
|
|
if ($prop['raw'])
|
|
{
|
|
$val = '<![CDATA['.$prop['val'].']]>';
|
|
} else {
|
|
$val = htmlspecialchars($prop['val']);
|
|
}
|
|
$val = $this->_prop_encode($val);
|
|
// properties from namespaces != "DAV:" or without any namespace
|
|
if ($prop['ns']) {
|
|
if ($this->crrnd) {
|
|
echo " <$prop[name] xmlns=".'"'.$prop["ns"].'">'
|
|
. $val . "</$prop[name]>\n";
|
|
} else {
|
|
echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]>"
|
|
. $val . '</'.$ns_hash[$prop['ns']].":$prop[name]>\n";
|
|
}
|
|
} else {
|
|
echo " <$prop[name] xmlns=\"\">$val</$prop[name]>\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->crrnd)
|
|
{
|
|
echo " </prop>\n";
|
|
echo " <status>HTTP/1.1 200 OK</status>\n";
|
|
echo " </propstat>\n";
|
|
}
|
|
else
|
|
{
|
|
echo " </D:prop>\n";
|
|
echo " <D:status>HTTP/1.1 200 OK</D:status>\n";
|
|
echo " </D:propstat>\n";
|
|
}
|
|
}
|
|
|
|
// now report all properties requested but not found
|
|
if (isset($file["noprops"])) {
|
|
echo ' <'.($this->crrnd?'':'D:')."propstat>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."prop>\n";
|
|
|
|
foreach ($file["noprops"] as &$prop) {
|
|
if ($prop["ns"] == "DAV:") {
|
|
echo ' <'.($this->crrnd?'':'D:')."$prop[name]/>\n";
|
|
} else if ($prop["ns"] == "") {
|
|
echo " <$prop[name] xmlns=\"\"/>\n";
|
|
} else {
|
|
echo " <" . $ns_hash[$prop["ns"]] . ":$prop[name]/>\n";
|
|
}
|
|
}
|
|
|
|
if ($this->crrnd)
|
|
{
|
|
echo " </prop>\n";
|
|
echo " <status>HTTP/1.1 404 Not Found</status>\n";
|
|
echo " </propstat>\n";
|
|
}
|
|
else
|
|
{
|
|
echo " </D:prop>\n";
|
|
echo " <D:status>HTTP/1.1 404 Not Found</D:status>\n";
|
|
echo " </D:propstat>\n";
|
|
}
|
|
}
|
|
|
|
echo ' </'.($this->crrnd?'':'D:')."response>\n";
|
|
}
|
|
|
|
echo '</'.($this->crrnd?'':'D:')."multistatus>\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 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
|
|
|
echo "<D:multistatus xmlns:D=\"DAV:\">\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."response>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."href>".$this->_urlencode($this->_mergePathes($this->_SERVER["SCRIPT_NAME"], $this->path)).'</'.($this->crrnd?'':'D:')."href>\n";
|
|
|
|
foreach ($options["props"] as $prop) {
|
|
echo ' <'.($this->crrnd?'':'D:')."propstat>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."prop><$prop[name] xmlns=\"$prop[ns]\"/></".($this->crrnd?'':'D:')."prop>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."status>HTTP/1.1 $prop[status]</".($this->crrnd?'':'D:')."status>\n";
|
|
echo ' </'.($this->crrnd?'':'D:')."propstat>\n";
|
|
}
|
|
|
|
if ($responsedescr) {
|
|
echo ' <'.($this->crrnd?'':'D:')."responsedescription>".
|
|
$this->_prop_encode(htmlspecialchars($responsedescr)).
|
|
'</'.($this->crrnd?'':'D:')."responsedescription>\n";
|
|
}
|
|
|
|
echo ' </'.($this->crrnd?'':'D:')."response>\n";
|
|
echo '</'.($this->crrnd?'':'D:')."multistatus>\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
|
|
* @return 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";
|
|
}
|
|
// switching off zlib.output_compression for zip archives, as the double compression makes problems eg. with lighttpd
|
|
if ($options['mimetype'] == 'application/zip')
|
|
{
|
|
ini_set('zlib.output_compression',0);
|
|
}
|
|
header("Content-type: $options[mimetype]");
|
|
|
|
if (isset($options['mtime'])) {
|
|
header("Last-modified:".gmdate("D, d M Y H:i:s ", $options['mtime'])."GMT");
|
|
}
|
|
// fix for IE and https, thanks to rob_burcham@yahoo.com
|
|
// see http://us3.php.net/manual/en/function.header.php#83219
|
|
// and http://support.microsoft.com/kb/812935
|
|
header("Cache-Control: maxage=1"); //In seconds
|
|
header("Pragma: public");
|
|
|
|
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 pathes 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_POST()
|
|
|
|
/**
|
|
* POST method handler
|
|
*
|
|
* @param void
|
|
* @return void
|
|
*/
|
|
function http_POST()
|
|
{
|
|
$status = '405 Method not allowed';
|
|
$options = Array();
|
|
$options['path'] = $this->path;
|
|
|
|
error_log('WebDAV POST: ' . $this->path);
|
|
|
|
if (isset($this->_SERVER['CONTENT_LENGTH']))
|
|
{
|
|
$options['content_length'] = $this->_SERVER['CONTENT_LENGTH'];
|
|
}
|
|
elseif (isset($this->_SERVER['X-Expected-Entity-Length']))
|
|
{
|
|
// MacOS gives us that hint
|
|
$options['content_length'] = $this->_SERVER['X-Expected-Entity-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 POST 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 POST 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];
|
|
}
|
|
$option['ranges'][] = $range;
|
|
|
|
// TODO make sure the implementation supports partial POST
|
|
// 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');
|
|
|
|
if (method_exists($this, 'POST')) {
|
|
$status = $this->POST($options);
|
|
|
|
if ($status === false) {
|
|
$status = '400 Something went wrong';
|
|
} else if ($status === true) {
|
|
$status = '200 OK';
|
|
} else if (is_resource($status) && get_resource_type($status) == 'stream') {
|
|
$stream = $status;
|
|
|
|
$status = empty($options['new']) ? '200 OK' : '201 Created';
|
|
|
|
if (!empty($options['ranges'])) {
|
|
// TODO multipart support is missing (see also above)
|
|
if (0 == fseek($stream, $range[0]['start'], SEEK_SET)) {
|
|
$length = $range[0]['end']-$range[0]['start']+1;
|
|
if (!fwrite($stream, fread($options['stream'], $length))) {
|
|
$status = '403 Forbidden';
|
|
}
|
|
} else {
|
|
$status = '403 Forbidden';
|
|
}
|
|
} else {
|
|
while (!feof($options['stream'])) {
|
|
if (false === fwrite($stream, fread($options['stream'], 4096))) {
|
|
$status = '403 Forbidden';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
fclose($stream);
|
|
}
|
|
}
|
|
$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;
|
|
|
|
if (isset($this->_SERVER['CONTENT_LENGTH']))
|
|
{
|
|
$options['content_length'] = $this->_SERVER['CONTENT_LENGTH'];
|
|
}
|
|
elseif (isset($this->_SERVER['X-Expected-Entity-Length']))
|
|
{
|
|
// MacOS gives us that hint
|
|
$options['content_length'] = $this->_SERVER['X-Expected-Entity-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];
|
|
}
|
|
$option["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, $range[0]["start"], SEEK_SET)) {
|
|
$length = $range[0]["end"]-$range[0]["start"]+1;
|
|
if (!fwrite($stream, fread($options["stream"], $length))) {
|
|
$stat = "403 Forbidden";
|
|
}
|
|
} else {
|
|
$stat = "403 Forbidden";
|
|
}
|
|
} else {
|
|
while (!feof($options["stream"])) {
|
|
if (false === fwrite($stream, fread($options["stream"], 4096))) {
|
|
$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") {
|
|
if (stripos($_SERVER['HTTP_USER_AGENT'],'webdrive') !== false)
|
|
{
|
|
// pretend we didnt see it, as webdrive does not handle the depth parameter correctly while deleting collections
|
|
}
|
|
else
|
|
{
|
|
$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;
|
|
// Todo: lockinfo::owner still contains D:href opening and closing tags, maybe they should be removed here with strip_tags
|
|
$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 = $stat;
|
|
}
|
|
$this->http_status($http_stat);
|
|
|
|
if ($http_stat{0} == 2) { // 2xx states are ok
|
|
if ($options["timeout"]) {
|
|
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
|
|
{
|
|
$timeout = $options[timeout];
|
|
}
|
|
} else {
|
|
$timeout = "Infinite";
|
|
}
|
|
|
|
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 ' <'.($this->crrnd?'':'D:')."lockdiscovery>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."activelock>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."lockscope><D:$options[scope]/></".($this->crrnd?'':'D:')."lockscope>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."locktype><D:$options[type]/></".($this->crrnd?'':'D:')."locktype>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."depth>$options[depth]</".($this->crrnd?'':'D:')."depth>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."owner>$options[owner]</".($this->crrnd?'':'D:')."owner>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."timeout>$timeout</".($this->crrnd?'':'D:')."timeout>\n";
|
|
echo ' <'.($this->crrnd?'':'D:')."locktoken><D:href>$options[locktoken]</D:href></".($this->crrnd?'':'D:')."locktoken>\n";
|
|
echo ' </'.($this->crrnd?'':'D:')."activelock>\n";
|
|
echo ' </'.($this->crrnd?'':'D:')."lockdiscovery>\n";
|
|
echo '</'.($this->crrnd?'':'D:')."prop>\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);
|
|
}
|
|
|
|
// }}}
|
|
|
|
// {{{ http_ACL()
|
|
|
|
/**
|
|
* ACL method handler
|
|
*
|
|
* @param void
|
|
* @return void
|
|
*/
|
|
function http_ACL()
|
|
{
|
|
$options = Array();
|
|
$options['path'] = $this->path;
|
|
$options['errors'] = array();
|
|
|
|
if (isset($this->_SERVER['HTTP_DEPTH'])) {
|
|
$options['depth'] = $this->_SERVER['HTTP_DEPTH'];
|
|
} else {
|
|
$options['depth'] = 'infinity';
|
|
}
|
|
|
|
// call user method
|
|
$status = $this->ACL($options);
|
|
|
|
// now we generate the reply header ...
|
|
$this->http_status($status);
|
|
$content = '';
|
|
|
|
if (is_array($options['errors']) && count($options['errors'])) {
|
|
header('Content-Type: text/xml; charset="utf-8"');
|
|
// ... and payload
|
|
$content .= "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
|
$content .= "<D:error xmlns:D=\"DAV:\"> \n";
|
|
foreach ($options['errors'] as $violation) {
|
|
$content .= '<'.($this->crrnd?'':'D:')."$violation/>\n";
|
|
}
|
|
$content .= '</'.($this->crrnd?'':'D:')."error>\n";
|
|
}
|
|
header("Content-length: ".$this->bytes($content));
|
|
if ($content) echo $options['content'];
|
|
}
|
|
|
|
// }}}
|
|
|
|
// }}}
|
|
|
|
// {{{ _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";
|
|
}
|
|
|
|
extract(parse_url($this->_SERVER["HTTP_DESTINATION"]));
|
|
$path = urldecode($path);
|
|
$http_host = $host;
|
|
if (isset($port) && $port != 80)
|
|
$http_host.= ":$port";
|
|
|
|
$http_header_host = preg_replace("/:80$/", "", $this->_SERVER["HTTP_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
|
|
* @praram boolen property raw-flag
|
|
* @return array property array
|
|
*/
|
|
function mkprop()
|
|
{
|
|
$args = func_get_args();
|
|
switch (count($args)) {
|
|
case 4:
|
|
return array('ns' => $args[0],
|
|
'name' => $args[1],
|
|
'val' => $args[2],
|
|
'raw' => true);
|
|
case 3:
|
|
return array('ns' => $args[0],
|
|
'name' => $args[1],
|
|
'val' => $args[2]);
|
|
default:
|
|
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()
|
|
{
|
|
if (method_exists($this, "checkAuth")) {
|
|
// PEAR style method name
|
|
return $this->checkAuth(@$this->_SERVER["AUTH_TYPE"],
|
|
@$this->_SERVER["PHP_AUTH_USER"],
|
|
@$this->_SERVER["PHP_AUTH_PW"]);
|
|
} else if (method_exists($this, "check_auth")) {
|
|
// old (pre 1.0) method name
|
|
return $this->check_auth(@$this->_SERVER["AUTH_TYPE"],
|
|
@$this->_SERVER["PHP_AUTH_USER"],
|
|
@$this->_SERVER["PHP_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:".HTTP_WebDAV_Server::_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 (@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, "<opaquelocktoken:", strlen("<opaquelocktoken"))) {
|
|
if (!preg_match('/^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$/', $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("<DAV:", $condition, 5)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @param string path of resource to check
|
|
* @param bool exclusive lock?
|
|
*/
|
|
function _check_lock_status($path, $exclusive_only = false)
|
|
{
|
|
// FIXME depth -> 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 <activelock> 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
|
|
if ($this->crrnd)
|
|
{
|
|
$activelocks.= "
|
|
<activelock>
|
|
<lockscope><$lock[scope]/></lockscope>
|
|
<locktype><$lock[type]/></locktype>
|
|
<depth>$lock[depth]</depth>
|
|
<owner>$lock[owner]</owner>
|
|
<timeout>$timeout</timeout>
|
|
<locktoken><href>$lock[token]</href></locktoken>
|
|
</activelock>
|
|
";
|
|
}
|
|
else
|
|
{
|
|
$activelocks.= "
|
|
<D:activelock>
|
|
<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>
|
|
</D:activelock>
|
|
";
|
|
}
|
|
}
|
|
|
|
// return generated response
|
|
//error_log(__METHOD__."\n".print_r($activelocks,true));
|
|
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 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)
|
|
{
|
|
// cadaver (and probably all neon using agents) need a more complete url encoding
|
|
// otherwise special chars like "$,()'" in filenames do NOT work
|
|
if (strpos($_SERVER['HTTP_USER_AGENT'],'neon') !== false)
|
|
{
|
|
return strtr(rawurlencode($url),array(
|
|
'%2F' => '/',
|
|
'%3A' => ':',
|
|
));
|
|
}
|
|
//error_log( __METHOD__."\n" .print_r($url,true));
|
|
return strtr($url, array(' ' => '%20',
|
|
'&' => '%26',
|
|
'<' => '%3C',
|
|
'>' => '%3E',
|
|
'+' => '%2B',
|
|
));
|
|
}
|
|
|
|
/**
|
|
* 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 urldecode($path);
|
|
}
|
|
|
|
/**
|
|
* Encode a hierarchical properties like:
|
|
*
|
|
* <D:supported-report-set>
|
|
* <supported-report>
|
|
* <report>
|
|
* <addressbook-query xmlns='urn:ietf:params:xml:ns:carddav'/>
|
|
* </report>
|
|
* </supported-report>
|
|
* <supported-report>
|
|
* <report>
|
|
* <addressbook-multiget xmlns='urn:ietf:params:xml:ns:carddav'/>
|
|
* </report>
|
|
* </supported-report>
|
|
* </D:supported-report-set>
|
|
*
|
|
* @param array $props
|
|
* @param string $ns
|
|
* @param strin $ns_defs
|
|
* @param array $ns_hash
|
|
* @return string
|
|
*/
|
|
function _hierarchical_prop_encode(array $props, $ns, &$ns_defs, array &$ns_hash)
|
|
{
|
|
$ret = '';
|
|
|
|
//error_log(__METHOD__.'('.array2string($props).')');
|
|
if (isset($props['name'])) $props = array($props);
|
|
|
|
foreach($props as $prop)
|
|
{
|
|
if (!isset($ns_hash[$prop['ns']])) // unknown namespace
|
|
{
|
|
// register namespace
|
|
$ns_name = 'ns'.(count($ns_hash) + 1);
|
|
$ns_hash[$prop['ns']] = $ns_name;
|
|
$ns_defs .= ' xmlns:'.$ns_name.'="'.$prop['ns'].'"';
|
|
}
|
|
if (is_array($prop['val']))
|
|
{
|
|
$subprop = $prop['val'];
|
|
if (isset($subprop['ns']) || isset($subprop[0]['ns']))
|
|
{
|
|
$ret .= '<'.($prop['ns'] == $ns ? ($this->cnrnd ? $ns_hash[$ns].':' : '') : $ns_hash[$prop['ns']].':').$prop['name'].
|
|
(empty($prop['val']) ? '/>' : '>'.$this->_hierarchical_prop_encode($prop['val'], $prop['ns'], $ns_defs, $ns_hash).
|
|
'</'.($prop['ns'] == $ns ? ($this->cnrnd ? $ns_hash[$ns].':' : '') : ($this->crrnd ? '' : $ns_hash[$prop['ns']].':')).$prop['name'].'>');
|
|
}
|
|
else // val contains only attributes, no value
|
|
{
|
|
$vals = '';
|
|
|
|
foreach($subprop as $attr => $val)
|
|
{
|
|
$vals .= ' '.$attr.'="'.htmlspecialchars($val).'"';
|
|
}
|
|
|
|
$ret .= '<'.($prop['ns'] == $ns ? ($this->cnrnd ? $ns_hash[$ns].':' : '') : $ns_hash[$prop['ns']].':').$prop['name'].
|
|
$vals .'/>';
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (empty($prop['val']))
|
|
{
|
|
$val = '';
|
|
}
|
|
else
|
|
{
|
|
if(isset($prop['raw']))
|
|
{
|
|
$val = $this->_prop_encode('<![CDATA['.$prop['val'].']]>');
|
|
} else {
|
|
$val = $this->_prop_encode(htmlspecialchars($prop['val']));
|
|
} }
|
|
|
|
$ret .= '<'.($prop['ns'] == $ns ? ($this->cnrnd ? $ns_hash[$ns].':' : '') : $ns_hash[$prop['ns']].':').$prop['name'].
|
|
(empty($prop['val']) ? ' />' : '>'.$val.'</'.($prop['ns'] == $ns ? ($this->cnrnd ? $ns_hash[$ns].':' : '') : ($this->crrnd ? '' : $ns_hash[$prop['ns']].':')).$prop['name'].'>');
|
|
}
|
|
}
|
|
|
|
//error_log(__METHOD__.'('.array2string($props).') = '.array2string($ret));
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
//error_log( __METHOD__."\n" .print_r($text,true));
|
|
//error_log("prop-encode:" . print_r($this->_prop_encoding,true));
|
|
|
|
switch (strtolower($this->_prop_encoding)) {
|
|
case "utf-8":
|
|
//error_log( __METHOD__."allready encoded\n" .print_r($text,true));
|
|
return $text;
|
|
case "iso-8859-1":
|
|
case "iso-8859-15":
|
|
case "latin-1":
|
|
default:
|
|
error_log( __METHOD__."utf8 encode\n" .print_r(utf8_encode($text),true));
|
|
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)
|
|
{
|
|
//error_log(__METHOD__." called with $path");
|
|
if ($path[$this->bytes($path)-1] != '/') {
|
|
//error_log(__METHOD__." added slash at the end of path");
|
|
$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)
|
|
{
|
|
//error_log(__METHOD__." called with $path");
|
|
if ($path[$this->bytes($path)-1] == '/') {
|
|
$path = substr($path, 0, -1);
|
|
//error_log(__METHOD__." removed slash at the end of path");
|
|
}
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Merge two pathes, make sure there is exactly one slash between them
|
|
*
|
|
* @param string parent path
|
|
* @param string child path
|
|
* @return string merged path
|
|
*/
|
|
function _mergePathes($parent, $child)
|
|
{
|
|
//error_log("merge called :\n$parent \n$child\n" . function_backtrace());
|
|
//error_log("merge :\n".print_r($this->_mergePathes($this->_SERVER["SCRIPT_NAME"], $this->path)true));
|
|
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:
|
|
*/
|
|
?>
|