This repository has been archived on 2024-12-02. You can view files and clone it, but cannot push or open issues or pull requests.
AbetoArmarios_Web/Source/gallery2/modules/webdav/lib/HTTP/WebDAV/Server.php

2545 lines
73 KiB
PHP
Raw Permalink Normal View History

<?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 15729 2007-01-27 05:53:15Z andy_st $
require_once(dirname(__FILE__) . '/Tools/_parse_propfind.php');
require_once(dirname(__FILE__) . '/Tools/_parse_proppatch.php');
require_once(dirname(__FILE__) . '/Tools/_parse_lockinfo.php');
define('HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE',
'urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882');
/**
* 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 0.99.1dev
*/
class HTTP_WebDAV_Server
{
// {{{ Member Variables
/**
* URL path for this request
*
* @var string
*/
var $path;
/**
* Base URL for this request
*
* See PHP parse_url structure
*
* @var array
*/
var $baseUrl;
/**
* 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 headers
*
* @var array
*/
var $headers = array();
/**
* Encoding of property values passed in
*
* @var string
*/
var $_prop_encoding = 'utf-8';
// }}}
// {{{ handleRequest
/**
* Handle WebDAV request
*
* Dispatch WebDAV request to the apropriate method wrapper
*
* @param void
* @return void
*/
function handleRequest()
{
// identify ourselves
if (empty($this->dav_powered_by)) {
$this->dav_powered_by = 'PHP class: ' . get_class($this);
}
$this->setResponseHeader('X-Dav-Powered-By: ' . $this->dav_powered_by);
// set path
if (empty($this->path)) {
$this->path = $this->_urldecode($_SERVER['PATH_INFO']);
$this->path = trim($this->path, '/');
}
if (ini_get('magic_quotes_gpc')) {
$this->path = stripslashes($this->path);
}
// set base URL
if (empty($this->baseUrl)) {
$this->baseUrl = parse_url(
"http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]");
$this->baseUrl['path'] = substr($this->baseUrl['path'], 0,
strlen($this->baseUrl['path']) - strlen($_SERVER['PATH_INFO']));
}
// check authentication
if (!$this->check_auth_wrapper()) {
// RFC2518 says we must use Digest instead of Basic but Microsoft
// clients do not support Digest and we don't support NTLM or
// Kerberos so we are stuck with Basic here
$this->setResponseHeader('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->setResponseStatus('401 Authentication Required');
return;
}
// check
if (!$this->_check_if_header_conditions()) {
$this->setResponseStatus('412 Precondition Failed');
return;
}
// detect requested method names
$method = strtolower($_SERVER['REQUEST_METHOD']);
$wrapper = $method . '_wrapper';
// emulate HEAD using GET if no HEAD method found
if ($wrapper == 'head_wrapper' &&
!method_exists($this, 'head')) {
$method = 'get';
}
if (method_exists($this, $method) &&
method_exists($this, $wrapper) ||
$method == 'options') {
$this->$wrapper();
return;
}
// method not found/implemented
if ($method == 'lock') {
$this->setResponseStatus('412 Precondition Failed');
return;
}
// tell client what's allowed
$this->setResponseStatus('405 Method Not Allowed');
$this->setResponseHeader('Allow: ' . implode(', ', $this->_allow()));
}
// }}}
// {{{ abstract WebDAV methods
// {{{ PROPFIND
/**
* PROPFIND implementation
*
* @abstract
* @param array &$params
* @returns int HTTP-Statuscode
*/
/* abstract
function propfind()
{
// dummy entry for PHPDoc
}
*/
// }}}
// {{{ PROPPATCH
/**
* PROPPATCH implementation
*
* @abstract
* @param array &$params
* @returns int HTTP-Statuscode
*/
/* abstract
function proppatch()
{
// dummy entry for PHPDoc
}
*/
// }}}
// {{{ MKCOL
/**
* MKCOL implementation
*
* @abstract
* @param array &$params
* @returns int HTTP-Statuscode
*/
/* abstract
function mkcol()
{
// dummy entry for PHPDoc
}
*/
// }}}
// {{{ GET
/**
* GET implementation
*
* Overload this method to retrieve resources from your server
*
* @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()
{
// dummy entry for PHPDoc
}
*/
// }}}
// {{{ DELETE
/**
* DELETE implementation
*
* @abstract
* @param array &$params
* @returns int HTTP-Statuscode
*/
/* abstract
function delete()
{
// dummy entry for PHPDoc
}
*/
// }}}
// {{{ PUT
/**
* PUT implementation
*
* @abstract
* @param array &$params
* @returns int HTTP-Statuscode
*/
/* abstract
function put()
{
// dummy entry for PHPDoc
}
*/
// }}}
// {{{ COPY
/**
* COPY implementation
*
* @abstract
* @param array &$params
* @returns int HTTP-Statuscode
*/
/* abstract
function copy()
{
// dummy entry for PHPDoc
}
*/
// }}}
// {{{ MOVE
/**
* MOVE implementation
*
* @abstract
* @param array &$params
* @returns int HTTP-Statuscode
*/
/* abstract
function move()
{
// dummy entry for PHPDoc
}
*/
// }}}
// {{{ LOCK
/**
* LOCK implementation
*
* @abstract
* @param array &$params
* @returns int HTTP-Statuscode
*/
/* abstract
function lock()
{
// dummy entry for PHPDoc
}
*/
// }}}
// {{{ UNLOCK
/**
* UNLOCK implementation
*
* @abstract
* @param array &$params
* @returns int HTTP-Statuscode
*/
/* abstract
function unlock()
{
// dummy entry for PHPDoc
}
*/
// }}}
// }}}
// {{{ other abstract methods
// {{{ checkAuth
/**
* 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
}
*/
// }}}
// {{{ getLocks
/**
* Get lock entries for a resource
*
* Overload this method to return shared and exclusive locks active for
* this resource
*
* @abstract
* @param string resource path to check
* @returns array of lock entries each consisting
* of 'type' ('shared'/'exclusive'), 'token' and 'timeout'
*/
/* abstract
function getLocks($path)
{
// dummy entry for PHPDoc
}
*/
// }}}
// }}}
// {{{ WebDAV HTTP method wrappers
// {{{ 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 options()
{
// get allowed methods
$allow = $this->_allow();
// dav header
$dav = array(1); // assume we are always dav class 1 compliant
if (in_array('LOCK', $allow) && in_array('UNLOCK', $allow)) {
$dav[] = 2; // dav class 2 requires that locking is supported
}
// tell clients what we found
$this->setResponseHeader('Allow: ' . implode(', ', $allow));
$this->setResponseHeader('DAV: ' . implode(',', $dav));
$this->setResponseHeader('Content-Length: 0');
// Microsoft clients default to the Frontpage protocol unless we tell
// them to use WebDAV
$this->setResponseHeader('MS-Author-Via: DAV');
$this->setResponseStatus('200 OK');
}
// }}}
// {{{ propfind_request_helper
/**
* PROPFIND request helper - prepares data-structures from PROPFIND requests
*
* @param options
* @return void
*/
function propfind_request_helper(&$options)
{
$options = array();
$options['path'] = $this->path;
// get depth from header (default is 'infinity')
$options['depth'] = 'infinity';
if (!empty($_SERVER['HTTP_DEPTH'])) {
$options['depth'] = $_SERVER['HTTP_DEPTH'];
}
// analyze request payload
$parser = new _parse_propfind($this->openRequestBody());
if (!$parser->success) {
$this->setResponseStatus('400 Bad Request');
return;
}
$options['props'] = $parser->props;
return true;
}
// }}}
// {{{ propfind_response_helper
/**
* PROPFIND response helper - format PROPFIND response
*
* @param options
* @param files
* @return void
*/
function propfind_response_helper($options, $files)
{
$responses = array();
// now loop over all returned files
foreach ($files as $file) {
$response = array();
if (empty($file['href'])) {
$response['href'] = $this->getHref($file['path']);
} else {
$response['href'] = $file['href'];
}
$response['propstat'] = array();
// collect namespaces here
$response['namespaces'] = array();
if (!empty($options['namespaces'])) {
$response['namespaces'] = $options['namespaces'];
}
// Microsoft needs this special namespace for date and time values
$response['namespaces'][HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE] =
'ns' . count($response['namespaces']);
if (is_array($options['props'])) {
// loop over all requested properties
foreach ($options['props'] as $reqprop) {
$status = '200 OK';
$prop = $this->getProp($reqprop, $file, $options);
if (!empty($prop['status'])) {
$status = $prop['status'];
}
if (empty($response['propstat'][$status])) {
$response['propstat'][$status] = array();
}
$response['propstat'][$status][] = $prop;
// namespace handling
if (empty($prop['ns']) || // empty namespace
$prop['ns'] == 'DAV:' || // default namespace
!empty($response['namespaces'][$prop['ns']])) { // already known
continue;
}
// register namespace
$response['namespaces'][$prop['ns']] = 'ns' . count($response['namespaces']);
}
} else if (is_array($file['props'])) {
// loop over all returned properties
foreach ($file['props'] as $prop) {
$status = '200 OK';
if (!empty($prop['status'])) {
$status = $prop['status'];
}
if (empty($response['propstat'][$status])) {
$response['propstat'][$status] = array();
}
if ($options['props'] == 'propname') {
// only names of all existing properties were requested
// so remove values
unset($prop['value']);
}
$response['propstat'][$status][] = $prop;
unset($prop['value']);
// namespace handling
if (empty($prop['ns']) || // empty namespace
$prop['ns'] == 'DAV:' || // default namespace
!empty($response['namespaces'][$prop['ns']])) { // already known
continue;
}
// register namespace
$response['namespaces'][$prop['ns']] = 'ns' . count($response['namespaces']);
}
}
$responses[] = $response;
}
$this->_multistatusResponseHelper($responses);
}
// }}}
// {{{ propfind_wrapper
/**
* PROPFIND method wrapper
*
* @param void
* @return void
*/
function propfind_wrapper()
{
// prepare data-structure from PROPFIND request
if (!$this->propfind_request_helper($options)) {
return;
}
// call user handler
if (!$this->propfind($options, $files)) {
return;
}
// format PROPFIND response
$this->propfind_response_helper($options, $files);
}
// }}}
// {{{ proppatch_request_helper
/**
* PROPPATCH request helper - prepares data-structures from PROPPATCH requests
*
* @param options
* @return void
*/
function proppatch_request_helper(&$options)
{
$options = array();
$options['path'] = $this->path;
$propinfo = new _parse_proppatch($this->openRequestBody());
if (!$propinfo->success) {
$this->setResponseStatus('400 Bad Request');
return;
}
$options['props'] = $propinfo->props;
return true;
}
// }}}
// {{{ proppatch_response_helper
/**
* PROPPATCH response helper - format PROPPATCH response
*
* @param options
* @param responsedescr
* @return void
*/
function proppatch_response_helper($options, $responsedescription=null)
{
$response = array();
if (empty($options['href'])) {
$response['href'] = $this->getHref($options['path']);
} else {
$response['href'] = $options['href'];
}
$response['propstat'] = array();
// collect namespaces here
$response['namespaces'] = array();
if (!empty($options['namespaces'])) {
$response['namespaces'] = $options['namespaces'];
}
if (!empty($options['props']) && is_array($options['props'])) {
foreach ($options['props'] as $prop) {
$status = '200 OK';
if (!empty($prop['status'])) {
$status = $prop['status'];
}
if (empty($response['propstat'][$status])) {
$response['propstat'][$status] = array();
}
$response['propstat'][$status][] = $prop;
// namespace handling
if (empty($prop['ns']) || // empty namespace
$prop['ns'] == 'DAV:' || // default namespace
!empty($response['namespaces'][$prop['ns']])) { // already known
continue;
}
// register namespace
$response['namespaces'][$prop['ns']] = 'ns' . count($response['namespaces']);
}
}
$response['responsedescription'] = $responsedescription;
$this->_multistatusResponseHelper(array($response));
}
// }}}
// {{{ proppatch_wrapper
/**
* PROPPATCH method wrapper
*
* @param void
* @return void
*/
function proppatch_wrapper()
{
// check resource is not locked
if (!$this->check_locks_wrapper($this->path)) {
$this->setResponseStatus('423 Locked');
return;
}
// perpare data-structure from PROPATCH request
if (!$this->proppatch_request_helper($options)) {
return;
}
// call user handler
$responsedescription = $this->proppatch($options);
// format PROPPATCH response
$this->proppatch_response_helper($options, $responsedescription);
}
// }}}
// {{{ mkcol_wrapper
/**
* MKCOL method wrapper
*
* @param void
* @return void
*/
function mkcol_wrapper()
{
$options = array();
$options['path'] = $this->path;
$status = $this->mkcol($options);
$this->setResponseStatus($status);
}
// }}}
// {{{ get_request_helper
/**
* GET request helper - prepares data-structures from GET requests
*
* @param options
* @return void
*/
function get_request_helper(&$options)
{
// TODO check for invalid stream
$options = array();
$options['path'] = $this->path;
$this->_get_ranges($options);
return true;
}
/**
* Parse HTTP Range: header
*
* @param array options array to store result in
* @return void
*/
function _get_ranges(&$options)
{
// process Range: header if present
if (!empty($_SERVER['HTTP_RANGE'])) {
// we only support standard 'bytes' range specifications for now
if (ereg('bytes[[:space:]]*=[[:space:]]*(.+)', $_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);
}
}
}
}
// }}}
// {{{ get_response_helper
/**
* GET response helper - format GET response
*
* @param options
* @param status
* @return void
*/
function get_response_helper($options, $status)
{
if (empty($status)) {
$status = '404 Not Found';
}
// set headers before we start printing
$this->setResponseStatus($status);
if ($status !== true) {
return;
}
if (empty($options['mimetype'])) {
$options['mimetype'] = 'application/octet-stream';
}
$this->setResponseHeader("Content-Type: $options[mimetype]");
if (!empty($options['mtime'])) {
$this->setResponseHeader('Last-Modified:'
. gmdate('D, d M Y H:i:s', $options['mtime']) . 'GMT');
}
if ($options['stream']) {
// GET handler returned a stream
if (!empty($options['ranges']) &&
(fseek($options['stream'], 0, SEEK_SET) === 0)) {
// partial request and stream is seekable
if (count($options['ranges']) === 1) {
$range = $options['ranges'][0];
if (!empty($range['start'])) {
fseek($options['stream'], $range['start'], SEEK_SET);
if (feof($options['stream'])) {
$this->setResponseStatus(
'416 Requested Range Not Satisfiable');
return;
}
if (!empty($range['end'])) {
$size = $range['end'] - $range['start'] + 1;
$this->setResponseStatus('206 Partial');
$this->setResponseHeader("Content-Length: $size");
$this->setResponseHeader(
"Content-Range: $range[start]-$range[end]/"
. (!empty($options['size']) ? $options['size'] : '*'));
while ($size && !feof($options['stream'])) {
$buffer = fread($options['stream'], 4096);
$size -= strlen($buffer);
echo $buffer;
}
} else {
$this->setResponseStatus('206 Partial');
if (!empty($options['size'])) {
$this->setResponseHeader("Content-Length: "
. ($options['size'] - $range['start']));
$this->setResponseHeader(
"Content-Range: $range[start]-$range[end]/"
. (!empty($options['size']) ? $options['size'] : '*'));
}
fpassthru($options['stream']);
}
} else {
$this->setResponseHeader("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 (!empty($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 = !empty($options['size']) ? $options['size'] : '*';
$size = $to - $from + 1;
$this->_multipart_byterange_header($options['mimetype'],
$from, $to, $total);
fseek($options['stream'], $start, SEEK_SET);
while ($size && !feof($options['stream'])) {
$buffer = fread($options['stream'], 4096);
$size -= strlen($buffer);
echo $buffer;
}
}
// end multipart
$this->_multipart_byterange_header();
}
} else {
// normal request or stream isn't seekable, return full content
if (!empty($options['size'])) {
$this->setResponseHeader("Content-Length: $options[size]");
}
fpassthru($options['stream']);
}
} else if (!empty($options['data'])) {
if (is_array($options['data'])) {
// reply to partial request
} else {
$this->setResponseHeader("Content-Length: "
. strlen($options['data']));
echo $options['data'];
}
}
}
/**
* 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 (empty($this->multipart_separator)) {
// init
// 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
$this->setResponseHeader(
'Content-Type: multipart/byteranges; boundary='
. $this->multipart_separator);
return;
}
// end
// generate closing multipart sequence
echo "\n--{$this->multipart_separator}--";
return;
}
// 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) . "\n\n";
}
// }}}
// {{{ get_wrapper
/**
* GET method wrapper
*
* @param void
* @return void
*/
function get_wrapper()
{
// perpare data-structure from GET request
if (!$this->get_request_helper($options)) {
return;
}
// call user handler
$status = $this->get($options);
// format GET response
$this->get_response_helper($options, $status);
}
// }}}
// {{{ head_response_helper
/**
* HEAD response helper - format HEAD response
*
* @param options
* @param status
* @return void
*/
function head_response_helper($options, $status)
{
if (empty($status)) {
$status = '404 Not Found';
}
// set headers before we start printing
$this->setResponseStatus($status);
if ($status !== true) {
return;
}
if (empty($options['mimetype'])) {
$options['mimetype'] = 'application/octet-stream';
}
$this->setResponseHeader("Content-Type: $options[mimetype]");
if (!empty($options['mtime'])) {
$this->setResponseHeader('Last-Modified:'
. gmdate('D, d M Y H:i:s', $options['mtime']) . 'GMT');
}
if (!empty($options['stream'])) {
// GET handler returned a stream
if (!empty($options['ranges'])
&& (fseek($options['stream'], 0, SEEK_SET) === 0)) {
// partial request and stream is seekable
if (count($options['ranges']) === 1) {
$range = $options['ranges'][0];
if (!empty($range['start'])) {
fseek($options['stream'], $range['start'], SEEK_SET);
if (feof($options['stream'])) {
$this->setResponseStatus(
'416 Requested Range Not Satisfiable');
return;
}
if (!empty($range['end'])) {
$size = $range['end'] - $range['start'] + 1;
$this->setResponseStatus('206 Partial');
$this->setResponseHeader("Content-Length: $size");
$this->setResponseHeader(
"Content-Range: $range[start]-$range[end]/"
. (!empty($options['size']) ? $options['size'] : '*'));
} else {
$this->setResponseStatus('206 Partial');
if (!empty($options['size'])) {
$this->setResponseHeader("Content-Length: "
. ($options['size'] - $range['start']));
$this->setResponseHeader(
"Content-Range: $start-$end/"
. (!empty($options['size']) ? $options['size'] : '*'));
}
}
} else {
$this->setResponseHeader(
"Content-Length: $range[last]");
fseek($options['stream'], -$range['last'], SEEK_END);
}
} else {
$this->_multipart_byterange_header(); // init multipart
foreach ($options['ranges'] as $range) {
// TODO what if size unknown? 500?
if (!empty($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 = !empty($options['size']) ? $options['size'] :
'*';
$size = $to - $from + 1;
$this->_multipart_byterange_header($options['mimetype'],
$from, $to, $total);
fseek($options['stream'], $start, SEEK_SET);
}
$this->_multipart_byterange_header(); // end multipart
}
} else {
// normal request or stream isn't seekable, return full content
if (!empty($options['size'])) {
$this->setResponseHeader("Content-Length: $options[size]");
}
}
} else if (!empty($options['data'])) {
if (is_array($options['data'])) {
// reply to partial request
} else {
$this->setResponseHeader("Content-Length: "
. strlen($options['data']));
}
}
}
// }}}
// {{{ head_wrapper
/**
* HEAD method wrapper
*
* @param void
* @return void
*/
function head_wrapper()
{
$options = array();
$options['path'] = $this->path;
// call user handler
if (method_exists($this, 'head')) {
$status = $this->head($options);
} else {
// can emulate HEAD using GET
ob_start();
$status = $this->get($options);
ob_end_clean();
}
// format HEAD response
$this->head_response_helper($options, $status);
}
// }}}
// {{{ put_request_helper
/**
* PUT request helper - prepares data-structures from PUT requests
*
* @param options
* @return void
*/
function put_request_helper(&$options)
{
$options = array();
$options['path'] = $this->path;
/* Content-Length may be zero */
if (!isset($_SERVER['CONTENT_LENGTH'])) {
return;
}
$options['content_length'] = $_SERVER['CONTENT_LENGTH'];
// default content type if none given
$options['content_type'] = 'application/unknown';
// get the content-type
if (!empty($_SERVER['CONTENT_TYPE'])) {
// for now we do not support any sort of multipart requests
if (!strncmp($_SERVER['CONTENT_TYPE'], 'multipart/', 10)) {
$this->setResponseStatus('501 Not Implemented');
echo 'The service does not support mulipart PUT requests';
return;
}
$options['content_type'] = $_SERVER['CONTENT_TYPE'];
}
// RFC2616 2.6: The recipient of the entity MUST NOT ignore any
// Content-* (e.g. Content-Range) headers that it does not understand
// or implement and MUST return a 501 (Not Implemented) response in
// such cases.
foreach ($_SERVER as $key => $value) {
if (strncmp($key, 'HTTP_CONTENT', 11)) {
continue;
}
switch ($key) {
case 'HTTP_CONTENT_ENCODING': // RFC2616 14.11
// TODO support this if ext/zlib filters are available
$this->setResponseStatus('501 Not Implemented');
echo "The service does not support '$value' content encoding";
return;
case 'HTTP_CONTENT_LANGUAGE': // RFC2616 14.12
// we assume it is not critical if this one is ignored in the
// actual PUT implementation
$options['content_language'] = $value;
break;
case 'HTTP_CONTENT_LENGTH':
// defined on IIS and has the same value as CONTENT_LENGTH
break;
case 'HTTP_CONTENT_LOCATION': // RFC2616 14.14
// 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': // RFC2616 14.16
// single byte range requests are supported
// the header format is also specified in RFC2616 14.16
// TODO we have to ensure that implementations support this or send 501 instead
if (!preg_match('@bytes\s+(\d+)-(\d+)/((\d+)|\*)@', $value, $matches)) {
$this->setResponseStatus('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_MD5': // RFC2616 14.15
// TODO maybe we can just pretend here?
$this->setResponseStatus('501 Not Implemented');
echo 'The service does not support content MD5 checksum verification';
return;
case 'HTTP_CONTENT_TYPE':
// defined on IIS and has the same value as CONTENT_TYPE
break;
default:
// any other unknown Content-* headers
$this->setResponseStatus('501 Not Implemented');
echo "The service does not support '$key'";
return;
}
}
$options['stream'] = $this->openRequestBody();
return true;
}
// }}}
// {{{ put_response_helper
/**
* PUT response helper - format PUT response
*
* @param options
* @param status
* @return void
*/
function put_response_helper($options, $status)
{
if (empty($status)) {
$status = '403 Forbidden';
} else if (is_resource($status)
&& get_resource_type($status) == 'stream') {
$stream = $status;
$status = isset($options['new']) && $options['new'] === false ? '204 No Content' : '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'])) {
$buf = fread($options['stream'], 4096);
if (fwrite($stream, $buf) != 4096) {
break;
}
}
}
fclose($stream);
}
$this->setResponseStatus($status);
}
// }}}
// {{{ put_wrapper
/**
* PUT method wrapper
*
* @param void
* @return void
*/
function put_wrapper()
{
// check resource is not locked
if (!$this->check_locks_wrapper($this->path)) {
$this->setResponseStatus('423 Locked');
return;
}
// perpare data-structure from PUT request
if (!$this->put_request_helper($options)) {
return;
}
// call user handler
$status = $this->put($options);
// format PUT response
$this->put_response_helper($options, $status);
}
// }}}
// {{{ delete_wrapper
/**
* DELETE method wrapper
*
* @param void
* @return void
*/
function delete_wrapper()
{
// RFC2518 9.2 last paragraph
if (!empty($_SERVER['HTTP_DEPTH'])
&& $_SERVER['HTTP_DEPTH'] != 'infinity') {
$this->setResponseStatus('400 Bad Request');
return;
}
// check resource is not locked
if (!$this->check_locks_wrapper($this->path)) {
$this->setResponseStatus('423 Locked');
return;
}
$options = array();
$options['path'] = $this->path;
// call user handler
$status = $this->delete($options);
if ($status === true) {
$status = '204 No Content';
}
$this->setResponseStatus($status);
}
// }}}
// {{{ copymove_request_helper
/**
* COPY/MOVE request helper - prepares data-structures from COPY/MOVE
* requests
*
* @param options
* @return void
*/
function copymove_request_helper(&$options)
{
$options = array();
$options['path'] = $this->path;
$options['depth'] = 'infinity';
if (!empty($_SERVER['HTTP_DEPTH'])) {
$options['depth'] = $_SERVER['HTTP_DEPTH'];
}
// RFC2518 9.6, 8.8.4 and 8.9.3
$options['overwrite'] = true;
if (!empty($_SERVER['HTTP_OVERWRITE'])) {
$options['overwrite'] = $_SERVER['HTTP_OVERWRITE'] == 'T';
}
$url = parse_url($_SERVER['HTTP_DESTINATION']);
// does the destination resource belong on this server?
if ($url['host'] == $this->baseUrl['host']
&& (empty($url['port']) ? 80 : $url['port']) == (empty($this->baseUrl['port']) ? 80 : $this->baseUrl['port'])
&& !strncmp($url['path'], $this->baseUrl['path'], strlen($this->baseUrl['path']))) {
if (!empty($this->baseurl['query'])) {
foreach (explode('&', $this->baseUrl['query']) as $queryComponent) {
if (!in_array($queryComponent, explode('&', $url['query']))) {
$options['dest_url'] = $_SERVER['HTTP_DESTINATION'];
return true;
}
}
}
$options['dest'] =
substr($url['path'], strlen($this->baseUrl['path']));
$options['dest'] = $this->_urldecode($options['dest']);
$options['dest'] = trim($options['dest'], '/');
// check source and destination are not the same - data could be lost
// if overwrite is true - RFC2518 8.8.5
if ($options['dest'] == $this->path) {
$this->setResponseStatus('403 Forbidden');
return;
}
return true;
}
$options['dest_url'] = $_SERVER['HTTP_DESTINATION'];
return true;
}
// }}}
// {{{ copy_wrapper
/**
* COPY method wrapper
*
* @param void
* @return void
*/
function copy_wrapper()
{
// no need to check source is not locked
// perpare data-structure from COPY request
if (!$this->copymove_request_helper($options)) {
return;
}
// check destination is not locked
if (!empty($options['dest']) &&
!$this->check_locks_wrapper($options['dest'])) {
$this->setResponseStatus('423 Locked');
return;
}
// call user handler
$status = $this->copy($options);
if ($status === true) {
$status = $options['new'] === false ? '204 No Content' :
'201 Created';
}
$this->setResponseStatus($status);
}
// }}}
// {{{ move_wrapper
/**
* MOVE method wrapper
*
* @param void
* @return void
*/
function move_wrapper()
{
// check resource is not locked
if (!$this->check_locks_wrapper($this->path)) {
$this->setResponseStatus('423 Locked');
return;
}
// perpare data-structure from MOVE request
if (!$this->copymove_request_helper($options)) {
return;
}
// check destination is not locked
if (!empty($options['dest']) &&
!$this->check_locks_wrapper($options['dest'])) {
$this->setResponseStatus('423 Locked');
return;
}
// call user handler
$status = $this->move($options);
if ($status === true) {
$status = $options['new'] === false ? '204 No Content' :
'201 Created';
}
$this->setResponseStatus($status);
}
// }}}
// {{{ lock_request_helper
/**
* LOCK request helper - prepares data-structures from LOCK requests
*
* @param options
* @return void
*/
function lock_request_helper(&$options)
{
$options = array();
$options['path'] = $this->path;
// a LOCK request with an If header but without a body is used to
// refresh a lock. Content-Lenght may be unset or zero.
if (empty($_SERVER['CONTENT_LENGTH']) && !empty($_SERVER['HTTP_IF'])) {
// FIXME: Refresh multiple locks?
$options['update'] = substr($_SERVER['HTTP_IF'], 2, -2);
return true;
}
$options['depth'] = 'infinity';
if (!empty($_SERVER['HTTP_DEPTH'])) {
$options['depth'] = $_SERVER['HTTP_DEPTH'];
}
if (!empty($_SERVER['HTTP_TIMEOUT'])) {
$options['timeout'] = explode(',', $_SERVER['HTTP_TIMEOUT']);
}
// extract lock request information from request XML payload
$lockinfo = new _parse_lockinfo($this->openRequestBody());
if (!$lockinfo->success) {
$this->setResponseStatus('400 Bad Request');
return;
}
// new lock
$options['scope'] = $lockinfo->lockscope;
$options['type'] = $lockinfo->locktype;
$options['owner'] = $lockinfo->owner;
$options['token'] = $this->_new_locktoken();
return true;
}
// }}}
// {{{ lock_response_helper
/**
* LOCK response helper - format LOCK response
*
* @param options
* @param status
* @return void
*/
function lock_response_helper($options, $status)
{
if (!empty($options['locks']) && is_array($options['locks'])) {
$this->setResponseStatus('409 Conflict');
$responses = array();
foreach ($options['locks'] as $lock) {
$response = array();
if (empty($lock['href'])) {
$response['href'] = $this->getHref($lock['path']);
} else {
$response['href'] = $lock['href'];
}
$response['status'] = '423 Locked';
$responses[] = $response;
}
$this->_multistatusResponseHelper($responses);
return;
}
if ($status === true) {
$status = '200 OK';
} else if ($status === false) {
$status = '423 Locked';
}
// set headers before we start printing
$this->setResponseStatus($status);
if ($status{0} == 2) { // 2xx states are ok
$this->setResponseHeader('Content-Type: text/xml; charset="utf-8"');
// RFC2518 8.10.1: In order to indicate the lock token associated
// with a newly created lock, a Lock-Token response header MUST be
// included in the response for every successful LOCK request for a
// new lock. Note that the Lock-Token header would not be returned
// in the response for a successful refresh LOCK request because a
// new lock was not created.
if (empty($options['update']) || !empty($options['token'])) {
$this->setResponseHeader("Lock-Token: <$options[token]>");
}
$lock = array();
foreach (array('scope', 'type', 'depth', 'owner') as $key) {
$lock[$key] = $options[$key];
}
if (!empty($options['expires'])) {
$lock['expires'] = $options['expires'];
} else {
$lock['timeout'] = $options['timeout'];
}
if (!empty($options['update'])) {
$lock['token'] = $options['update'];
} else {
$lock['token'] = $options['token'];
}
echo "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
echo "<D:prop xmlns:D=\"DAV:\">\n";
echo " <D:lockdiscovery>\n";
echo ' ' . $this->_activelocksResponseHelper(array($lock))
. "\n";
echo " </D:lockdiscovery>\n";
echo "</D:prop>\n";
}
}
// }}}
// {{{ lock_wrapper
/**
* LOCK method wrapper
*
* @param void
* @return void
*/
function lock_wrapper()
{
// perpare data-structure from LOCK request
if (!$this->lock_request_helper($options)) {
return;
}
// check resource is not locked
if (!empty($options['update'])
&& !$this->check_locks_wrapper(
$this->path, $options['scope'] == 'shared')) {
$this->setResponseStatus('423 Locked');
return;
}
$options['locks'] = $this->getDescendentsLocks($this->path);
if (empty($options['locks'])) {
// call user handler
$status = $this->lock($options);
}
// format LOCK response
$this->lock_response_helper($options, $status);
}
// }}}
// {{{ unlock_request_helper
/**
* UNLOCK request helper - prepares data-structures from UNLOCK requests
*
* @param options
* @return void
*/
function unlock_request_helper(&$options)
{
$options = array();
$options['path'] = $this->path;
if (empty($_SERVER['HTTP_LOCK_TOKEN'])) {
return;
}
// strip surrounding <>
$options['token'] = substr(trim($_SERVER['HTTP_LOCK_TOKEN']), 1, -1);
return true;
}
// }}}
// {{{ unlock_wrapper
/**
* UNLOCK method wrapper
*
* @param void
* @return void
*/
function unlock_wrapper()
{
// perpare data-structure from DELETE request
if (!$this->unlock_request_helper($options)) {
return;
}
// call user handler
$status = $this->unlock($options);
// RFC2518 8.11.1
if ($status === true) {
$status = '204 No Content';
}
$this->setResponseStatus($status);
}
// }}}
function _multistatusResponseHelper($responses)
{
// now we generate the response header...
$this->setResponseStatus('207 Multi-Status', false);
$this->setResponseHeader('Content-Type: text/xml; charset="utf-8"');
// ...& payload
echo "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
echo "<D:multistatus xmlns:D=\"DAV:\">\n";
foreach ($responses as $response) {
// ignore empty or incomplete entries
if (!is_array($response) || empty($response)) {
continue;
}
$namespaces = '';
if (!empty($response['namespaces'])) {
foreach ($response['namespaces'] as $name => $prefix) {
$namespaces .= " xmlns:$prefix=\"$name\"";
}
}
echo " <D:response$namespaces>\n";
echo " <D:href>$response[href]</D:href>\n";
// report all found properties and their values (if any)
// nothing to do if no properties were returend for a file
if (!empty($response['propstat']) &&
is_array($response['propstat'])) {
foreach ($response['propstat'] as $status => $props) {
echo " <D:propstat>\n";
echo " <D:prop>\n";
foreach ($props as $prop) {
if (!is_array($prop) || empty($prop['name'])) {
continue;
}
// empty properties (cannot use empty for check as '0'
// is a legal value here)
if (empty($prop['value']) && (!isset($prop['value'])
|| $prop['value'] !== 0)) {
if ($prop['ns'] == 'DAV:') {
echo " <D:$prop[name]/>\n";
continue;
}
if (!empty($prop['ns'])) {
echo ' <' . $response['namespaces'][$prop['ns']] . ":$prop[name]/>\n";
continue;
}
echo " <$prop[name] xmlns=\"\"/>";
continue;
}
// some WebDAV properties need special treatment
if ($prop['ns'] == 'DAV:') {
switch ($prop['name']) {
case 'creationdate':
echo ' <D:creationdate ' . $response['namespaces'][HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE] . ':dt="dateTime.tz">' . gmdate('Y-m-d\TH:i:s\Z', $prop['value']) . "</D:creationdate>\n";
break;
case 'getlastmodified':
echo ' <D:getlastmodified ' . $response['namespaces'][HTTP_WEBDAV_SERVER_DATATYPE_NAMESPACE] . ':dt="dateTime.rfc1123">' . gmdate('D, d M Y H:i:s', $prop['value']) . " UTC</D:getlastmodified>\n";
break;
case 'resourcetype':
echo " <D:resourcetype><D:$prop[value]/></D:resourcetype>\n";
break;
case 'supportedlock':
if (is_array($prop['value'])) {
$prop['value'] =
$this->_lockentriesResponseHelper(
$prop['value']);
}
echo " <D:supportedlock>\n";
echo " $prop[value]\n";
echo " </D:supportedlock>\n";
break;
case 'lockdiscovery':
if (is_array($prop['value'])) {
$prop['value'] =
$this->_activelocksResponseHelper(
$prop['value']);
}
echo " <D:lockdiscovery>\n";
echo " $prop[value]\n";
echo " </D:lockdiscovery>\n";
break;
default:
echo " <D:$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['value'])) . "</D:$prop[name]>\n";
}
continue;
}
if (!empty($prop['ns'])) {
echo ' <' . $response['namespaces'][$prop['ns']] . ":$prop[name]>" . $this->_prop_encode(htmlspecialchars($prop['value'])) . '</' . $response['namespaces'][$prop['ns']] . ":$prop[name]>\n";
continue;
}
echo " <$prop[name] xmlns=\"\">" . $this->_prop_encode(htmlspecialchars($prop['value'])) . "</$prop[name]>\n";
}
echo " </D:prop>\n";
echo " <D:status>HTTP/1.1 $status</D:status>\n";
echo " </D:propstat>\n";
}
}
if (!empty($response['status'])) {
echo " <D:status>HTTP/1.1 $response[status]</D:status>\n";
}
if (!empty($response['responsedescription'])) {
echo ' <D:responsedescription>' . $this->_prop_encode(htmlspecialchars($response['responsedescription'])) . "</D:responsedescription>\n";
}
echo " </D:response>\n";
}
echo "</D:multistatus>\n";
}
function _activelocksResponseHelper($locks)
{
if (!is_array($locks) || empty($locks)) {
return '';
}
foreach ($locks as $key => $lock) {
if (!is_array($lock) || empty($lock)) {
continue;
}
// check for 'timeout' or 'expires'
$timeout = 'Infinite';
if (!empty($lock['expires'])) {
$timeout = 'Second-' . ($lock['expires'] - time());
} else if (!empty($lock['timeout'])) {
// more than a million is considered an absolute timestamp
// less is more likely a relative value
$timeout = "Second-$lock[timeout]";
if ($lock['timeout'] > 1000000) {
$timeout = 'Second-' . ($lock['timeout'] - time());
}
}
// genreate response block
$locks[$key] = "<D:activelock>
<D:lockscope><D:$lock[scope]/></D:lockscope>
<D:locktype><D:$lock[type]/></D:locktype>
<D:depth>" . ($lock['depth'] == 'infinity' ? 'Infinity' : $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 implode('', $locks);
}
function _lockentriesResponseHelper($locks)
{
if (!is_array($locks) || empty($locks)) {
return '';
}
foreach ($locks as $key => $lock) {
if (!is_array($lock) || empty($lock)) {
continue;
}
$locks[$key] = "<D:lockentry>
<D:lockscope><D:$lock[scope]/></D:lockscope>
<D:locktype><D:$lock[type]/></D:locktype>
</D:lockentry>";
}
return implode('', $locks);
}
function getHref($path)
{
return $this->baseUrl['path'] . '/' . $path;
}
function getProp($reqprop, $file, $options)
{
// check if property exists in response
foreach ($file['props'] as $prop) {
if ($reqprop['name'] == $prop['name']
&& $reqprop['ns'] == $prop['ns']) {
return $prop;
}
}
if ($reqprop['name'] == 'lockdiscovery'
&& $reqprop['ns'] == 'DAV:'
&& method_exists($this, 'getLocks')) {
return $this->mkprop('DAV:', 'lockdiscovery',
$this->getLocks($file['path']));
}
// incase the requested property had a value, like calendar-data
unset($reqprop['value']);
$reqprop['status'] = '404 Not Found';
return $reqprop;
}
function getDescendentsLocks($path)
{
$options = array();
$options['path'] = $path;
$options['depth'] = 'infinity';
$options['props'] = array();
$options['props'][] = $this->mkprop('DAV:', 'lockdiscovery', null);
// call user handler
if (!$this->propfind($options, $files)) {
return;
}
return $files;
}
// {{{ _allow()
/**
* List implemented methods
*
* @param void
* @return array something
*/
function _allow()
{
// OPTIONS is always there
$allow = array('OPTIONS');
// all other methods need both a method_wrapper() and a method()
// implementation
// the base class defines only wrappers
foreach(get_class_methods($this) as $method) {
// strncmp breaks with negative len -
// http://bugs.php.net/bug.php?id=36944
//if (!strncmp('_wrapper', $method, -8)) {
if (!strcmp(substr($method, -8), '_wrapper')) {
$method = strtolower(substr($method, 0, -8));
if (method_exists($this, $method) &&
($method != 'lock' && $method != 'unlock' ||
method_exists($this, 'getLocks'))) {
$allow[] = $method;
}
}
}
// we can emulate a missing HEAD implemetation using GET
if (in_array('GET', $allow)) {
$allow[] = 'HEAD';
}
return $allow;
}
// }}}
// {{{ mkprop
/**
* Helper for property element creation
*
* @param string XML namespace (optional)
* @param string property name
* @param string property value
* @return array property array
*/
function mkprop()
{
$args = func_get_args();
$prop = array();
$prop['ns'] = 'DAV:';
if (count($args) > 2) {
$prop['ns'] = array_shift($args);
}
$prop['name'] = array_shift($args);
$prop['value'] = array_shift($args);
$prop['status'] = array_shift($args);
return $prop;
}
// }}}
// {{{ check_auth_wrapper
/**
* Check authentication if implemented
*
* @param void
* @return boolean true if authentication succeded or not necessary
*/
function check_auth_wrapper()
{
if (method_exists($this, 'checkAuth')) {
// PEAR style method name
return $this->checkAuth(@$_SERVER['AUTH_TYPE'],
@$_SERVER['PHP_AUTH_USER'],
@$_SERVER['PHP_AUTH_PW']);
}
if (method_exists($this, 'check_auth')) {
// old (pre 1.0) method name
return $this->check_auth(@$_SERVER['AUTH_TYPE'],
@$_SERVER['PHP_AUTH_USER'],
@$_SERVER['PHP_AUTH_PW']);
}
// no method found -> no authentication required
return true;
}
// }}}
// {{{ UUID stuff
/**
* Generate Unique Universal IDentifier for lock token
*
* @param void
* @return string a new UUID
*/
function _new_uuid()
{
// use uuid extension from PECL if available
if (function_exists('uuid_create')) {
return uuid_create();
}
// fallback
$uuid = md5(microtime() . getmypid()); // this should be random enough for now
// set variant and version fields for 'true' random uuid
$uuid{12} = '4';
$n = 8 + (ord($uuid{16}) & 3);
$hex = '0123456789abcdef';
$uuid{16} = $hex{$n};
// return formated uuid
return substr($uuid, 0, 8)
. '-' . substr($uuid, 8, 4)
. '-' . substr($uuid, 12, 4)
. '-' . substr($uuid, 16, 4)
. '-' . substr($uuid, 20);
}
/**
* Create a new opaque lock token as defined in RFC2518
*
* @param void
* @return string new RFC2518 opaque lock token
*/
function _new_locktoken()
{
return 'opaquelocktoken:' . $this->_new_uuid();
}
// }}}
// {{{ WebDAV If: header parsing
/**
*
*
* @param string header string to parse
* @param int current parsing position
* @return array next token (type and value)
*/
function _if_header_lexer($string, &$pos)
{
// skip whitespace
while (ctype_space($string{$pos})) {
++$pos;
}
// already at end of string?
if (strlen($string) <= $pos) {
return;
}
// get next character
$c = $string{$pos++};
// now it depends on what we found
switch ($c) {
// URLs are enclosed in <...>
case '<':
$pos2 = strpos($string, '>', $pos);
$uri = substr($string, $pos, $pos2 - $pos);
$pos = $pos2 + 1;
return array('URI', $uri);
// ETags are enclosed in [...]
case '[':
$type = 'ETAG_STRONG';
if ($string{$pos} == 'W') {
$type = 'ETAG_WEAK';
$pos += 2;
}
$pos2 = strpos($string, ']', $pos);
$etag = substr($string, $pos + 1, $pos2 - $pos - 2);
$pos = $pos2 + 1;
return array($type, $etag);
// 'N' indicates negation
case 'N':
$pos += 2;
return array('NOT', 'Not');
// anything else is passed verbatim char by char
default:
return array('CHAR', $c);
}
}
/**
* Parse If: header
*
* @param string header string
* @return array URLs 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 URL
$uri = '';
if ($token[0] == 'URI') {
$uri = $token[1]; // remember URL
$token = $this->_if_header_lexer($str, $pos); // get next token
}
// sanity check
if ($token[0] != 'CHAR' || $token[1] != '(') {
return;
}
$list = array();
$level = 1;
while ($level) {
$token = $this->_if_header_lexer($str, $pos);
$not = '';
if ($token[0] == 'NOT') {
$not = '!';
$token = $this->_if_header_lexer($str, $pos);
}
switch ($token[0]) {
case 'CHAR':
switch ($token[1]) {
case '(':
$level++;
break;
case ')':
$level--;
break;
default:
return;
}
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;
}
}
// RFC2518 9.4.1: The No-tag-list production describes a series of
// state tokens and ETags. If multiple No-tag-list productions are
// used then one only needs to match the state of the resource for
// the method to be allowed to continue.
//
// FIXME: Since only one list of conditions must be satisfied, it
// is a mistake to merge all lists of conditions. Instead, a URL
// should reference an array of arrays of conditions, the inner
// array representing a conjunction and the outer array
// representing a disjunction, or $uris shouldn't be associative,
// but be an array of productions array('href' => $href,
// 'conditions' => array($condition, ...))
if (!empty($uris[$uri])) {
$uris[$uri] = array_merge($uris[$uri], $list);
continue;
}
$uris[$uri] = $list;
}
return $uris;
}
/**
* Check if conditions from If: headers are met
*
* The If: header is an extension to HTTP/1.1 defined in RFC2518 9.4
*
* @param void
* @return boolean
*/
function _check_if_header_conditions()
{
if (empty($_SERVER['HTTP_IF'])) {
return true;
}
$this->_if_header_uris =
$this->_if_header_parser($_SERVER['HTTP_IF']);
// any match is ok
foreach ($this->_if_header_uris as $uri => $conditions) {
// RFC2518 9.4.1: If a method, due to the presence of a Depth or
// Destination header, is applied to multiple resources then the
// No-tag-list production MUST be applied to each resource the
// method is applied to.
if (empty($uri)) {
$uri = $this->getHref($this->path);
}
// all must match
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 (!ereg('^<opaquelocktoken:[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}>$', $condition)) {
return;
}
}
if (!$this->_check_uri_condition($uri, $condition)) {
continue 2;
}
}
return true;
}
}
/**
* Check a single URL condition parsed from an if-header
*
* @abstract
* @param string URL to check
* @param string condition to check for this URL
* @return boolean condition check result
*/
function _check_uri_condition($uri, $condition)
{
// not really implemented here,
// implementations must override
return true;
}
/**
* For each lock on the requested resource, check that the lock token is in
* the If header. If requesting a shared lock, check only exclusive locks
* on the requested resource.
*
* @param array of locks
* @param string path of resource to check
* @param boolean exclusive lock?
* @return boolean true if the request is allowed
*/
function check_locks_helper($locks, $path, $exclusive_only=false)
{
$conditions = array();
if (!empty($_SERVER['HTTP_IF'])) {
$conditions = $this->_if_header_parser($_SERVER['HTTP_IF']);
}
foreach ($locks as $lock) {
if ($exclusive_only && ($lock['scope'] == 'shared')) {
continue;
}
// for both Tagged-list and No-tag-list productions
foreach (array($this->getHref($path), '') as $href) {
if (!empty($conditions[$href])
&& in_array("<$lock[token]>", $conditions[$href])) {
continue 2;
}
}
return false;
}
return true;
}
/**
* @param string path of resource to check
* @param boolean exclusive lock?
*/
function check_locks_wrapper($path, $exclusive_only=false)
{
if (!method_exists($this, 'getLocks')) {
return true;
}
return $this->check_locks_helper(
$this->getLocks($path), $path, $exclusive_only);
}
// }}}
/**
* Open request body input stream
*
* Because it's not possible to write to php://input (unlike the potential
* to set request variables) and not possible until PHP 5.1 to register
* alternative stream wrappers with php:// URLs, this function enables
* sub-classes to override the request body. Gallery uses this for unit
* testing. This function also collects all instances of opening the
* request body in one place.
*
* @return resource a file descriptor
*/
function openRequestBody()
{
return fopen('php://input', 'rb');
}
/**
* Set HTTP response header
*
* This function enables sub-classes to override header-setting. Gallery
* uses this to avoid replacing headers elsewhere in the application, and
* for testability.
*
* @param string status code and message
* @return void
*/
function setResponseHeader($header, $replace=true)
{
$key = 'status';
if (strncasecmp($header, 'HTTP/', 5) !== 0) {
$key = strtolower(substr($header, 0, strpos($header, ':')));
}
if ($replace || empty($this->headers[$key])) {
header($header);
$this->headers[$key] = $header;
}
}
/**
* Set HTTP response status and mirror it in a private header
*
* @param string status code and message
* @return void
*/
function setResponseStatus($status, $replace=true)
{
// simplified success case
if ($status === true) {
$status = '200 OK';
}
// didn't set a more specific status code
if (empty($status)) {
$status = '500 Internal Server Error';
}
// generate HTTP status response
$this->setResponseHeader("HTTP/1.1 $status", $replace);
$this->setResponseHeader("X-WebDAV-Status: $status", $replace);
}
/**
* 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)
{
return strtr($url, array(' ' => '%20',
'&' => '%26',
'<' => '%3C',
'>' => '%3E'));
}
/**
* Private version of PHP urldecode
*
* Not really needed but added for completenes.
*
* @param string URL to decode
* @return string decoded URL
*/
function _urldecode($path)
{
return 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)
{
switch (strtolower($this->_prop_encoding)) {
case 'utf-8':
return $text;
case 'iso-8859-1':
case 'iso-8859-15':
case 'latin-1':
default:
return utf8_encode($text);
}
}
/**
* Make sure path ends in a slash
*
* @param string directory path
* @return string directory path with trailing slash
*/
function _slashify($path)
{
if (substr($path, -1) != '/') {
$path .= '/';
}
return $path;
}
}
// Local variables:
// tab-width: 4
// c-basic-offset: 4
// End:
?>