blob: fb6bee1ebd4bb058031f431d96d7f0834eaf3a5b [file] [log] [blame]
<?php
/**
* File containing the abstract ezcWebdavSimpleBackend class.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package Webdav
* @version //autogentag//
* @copyright Copyright (C) 2005-2010 eZ Systems AS. All rights reserved.
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
*/
/**
* Abstract base class for common backend operations.
*
* This base backend provides the generic handling of requests and dispatches the
* required actions to some basic manipulation methods, which you are required
* to implement, when extending this base class.
*
* This backend does not provide support for extended Webdav features, like
* compression, or lock handling by the backend, therefore the {@link
* getFeatures()} method is final. If you want to develop a backend which is
* capable of manual handling those features directly extend from {@link
* ezcWebdavBackend}.
*
* @version //autogentag//
* @package Webdav
* @mainclass
*/
abstract class ezcWebdavSimpleBackend extends ezcWebdavBackend implements ezcWebdavBackendPut, ezcWebdavBackendChange, ezcWebdavBackendMakeCollection
{
/**
* Create a new collection.
*
* Creates a new collection at the given $path.
*
* @param string $path
* @return void
*/
abstract protected function createCollection( $path );
/**
* Create a new resource.
*
* Creates a new resource at the given $path, optionally with the given
* $content.
*
* @param string $path
* @param string $content
* @return void
*/
abstract protected function createResource( $path, $content = null );
/**
* Changes contents of a resource.
*
* This method is used to change the contents of the resource identified by
* $path to the given $content.
*
* @param string $path
* @param string $content
* @return void
*/
abstract protected function setResourceContents( $path, $content );
/**
* Returns the content of a resource.
*
* Returns the content of the resource identified by $path.
*
* @param string $path
* @return string
*/
abstract protected function getResourceContents( $path );
/**
* Manually sets a property on a resource.
*
* Sets the given $propertyBackup for the resource identified by $path.
*
* @param string $path
* @param ezcWebdavProperty $property
* @return bool
*/
abstract public function setProperty( $path, ezcWebdavProperty $property );
/**
* Manually removes a property from a resource.
*
* Removes the given $property form the resource identified by $path.
*
* @param string $path
* @param ezcWebdavProperty $property
* @return bool
*/
abstract public function removeProperty( $path, ezcWebdavProperty $property );
/**
* Resets the property storage for a resource.
*
* Discardes the current {@link ezcWebdavPropertyStorage} of the resource
* identified by $path and replaces it with the given $properties.
*
* @param string $path
* @param ezcWebdavPropertyStorage $properties
* @return bool
*/
abstract public function resetProperties( $path, ezcWebdavPropertyStorage $properties );
/**
* Returns a property of a resource.
*
* Returns the property with the given $propertyName, from the resource
* identified by $path. You may optionally define a $namespace to receive
* the property from.
*
* @param string $path
* @param string $propertyName
* @param string $namespace
* @return ezcWebdavProperty
*/
abstract public function getProperty( $path, $propertyName, $namespace = 'DAV:' );
/**
* Returns all properties for a resource.
*
* Returns all properties for the resource identified by $path as a {@link
* ezcWebdavBasicPropertyStorage}.
*
* @param string $path
* @return ezcWebdavPropertyStorage
*/
abstract public function getAllProperties( $path );
/**
* Copies resources recursively from one path to another.
*
* Copies the resourced identified by $fromPath recursively to $toPath with
* the given $depth, where $depth is one of {@link
* ezcWebdavRequest::DEPTH_ZERO}, {@link ezcWebdavRequest::DEPTH_ONE},
* {@link ezcWebdavRequest::DEPTH_INFINITY}.
*
* Returns an array with {@link ezcWebdavErrorResponse}s for all subtrees,
* where the copy operation failed. Errors for subsequent resources in a
* subtree should be ommitted.
*
* If an empty array is return, the operation has been completed
* successfully.
*
* @param string $fromPath
* @param string $toPath
* @param int $depth
* @return array(ezcWebdavErrorResponse)
*/
abstract protected function performCopy( $fromPath, $toPath, $depth = ezcWebdavRequest::DEPTH_INFINITY );
/**
* Deletes everything below a path.
*
* Deletes the resource identified by $path recursively. Returns an
* instance of {@link ezcWebdavMultistatusResponse} if the deletion failed,
* and null on success.
*
* @param string $path
* @return ezcWebdavMultitstatusResponse|null
*/
abstract protected function performDelete( $path );
/**
* Returns if a resource exists.
*
* Returns if a the resource identified by $path exists.
*
* @param string $path
* @return bool
*/
abstract protected function nodeExists( $path );
/**
* Returns if resource is a collection.
*
* Returns if the resource identified by $path is a collection resource
* (true) or a non-collection one (false).
*
* @param string $path
* @return bool
*/
abstract protected function isCollection( $path );
/**
* Returns members of collection.
*
* Returns an array with the members of the collection identified by $path.
* The returned array can contain {@link ezcWebdavCollection}, and {@link
* ezcWebdavResource} instances and might also be empty, if the collection
* has no members.
*
* @param string $path
* @return array(ezcWebdavResource|ezcWebdavCollection)
*/
abstract protected function getCollectionMembers( $path );
/**
* Returns additional features supported by the backend.
*
* Returns a bitmap of additional features supported by the backend, referenced
* by constants from the basic {@link ezcWebdavBackend} class.
*
* @return int
*/
public final function getFeatures()
{
return 0;
}
/**
* Serves GET requests.
*
* The method receives a {@link ezcWebdavGetRequest} object containing all
* relevant information obout the clients request and will return an {@link
* ezcWebdavErrorResponse} instance on error or an instance of {@link
* ezcWebdavGetResourceResponse} or {@link ezcWebdavGetCollectionResponse}
* on success, depending on the type of resource that is referenced by the
* request.
*
* @param ezcWebdavGetRequest $request
* @return ezcWebdavResponse
*/
public function get( ezcWebdavGetRequest $request )
{
$source = $request->requestUri;
// Check authorization
if ( !ezcWebdavServer::getInstance()->isAuthorized( $source, $request->getHeader( 'Authorization' ) ) )
{
return $this->createUnauthorizedResponse(
$source,
$request->getHeader( 'Authorization' )
);
}
// Check if resource is available
if ( !$this->nodeExists( $source ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_404,
$source
);
}
// Verify If-[None-]Match headers
if ( ( $res = $this->checkIfMatchHeaders( $request, $source ) ) !== null )
{
return $res;
}
$res = null; // Init
if ( !$this->isCollection( $source ) )
{
// Just deliver file
$res = new ezcWebdavGetResourceResponse(
new ezcWebdavResource(
$source,
$this->getAllProperties( $source ),
$this->getResourceContents( $source )
)
);
}
else
{
// Return collection with contained children
$res = new ezcWebdavGetCollectionResponse(
new ezcWebdavCollection(
$source,
$this->getAllProperties( $source ),
$this->getCollectionMembers( $source )
)
);
}
// Add ETag header
$res->setHeader( 'ETag', $this->getETag( $source ) );
// Deliver response
return $res;
}
/**
* Serves HEAD requests.
*
* The method receives a {@link ezcWebdavHeadRequest} object containing all
* relevant information obout the clients request and will return an {@link
* ezcWebdavErrorResponse} instance on error or an instance of {@link
* ezcWebdavHeadResponse} on success.
*
* @param ezcWebdavHeadRequest $request
* @return ezcWebdavResponse
*/
public function head( ezcWebdavHeadRequest $request )
{
$source = $request->requestUri;
// Check authorization
if ( !ezcWebdavServer::getInstance()->isAuthorized( $source, $request->getHeader( 'Authorization' ) ) )
{
return $this->createUnauthorizedResponse(
$source,
$request->getHeader( 'Authorization' )
);
}
// Check if resource is available
if ( !$this->nodeExists( $source ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_404,
$source
);
}
$res = null; // Init
if ( !$this->isCollection( $source ) )
{
// Just deliver file without contents
$res = new ezcWebdavHeadResponse(
new ezcWebdavResource(
$source,
$this->getAllProperties( $source )
)
);
}
else
{
// Just deliver collection without children
$res = new ezcWebdavHeadResponse(
new ezcWebdavCollection(
$source,
$this->getAllProperties( $source )
)
);
}
// Add ETag header
$res->setHeader( 'ETag', $this->getETag( $source ) );
// Deliver response
return $res;
}
/**
* Returns all child nodes.
*
* Get all nodes from the resource identified by $source up to the given
* depth. Reuses the method {@link getCollectionMembers()}, but you may
* want to overwrite this implementation by somethings which fits better
* with your backend.
*
* @param string $source
* @param int $depth
* @return array(ezcWebdavResource|ezcWebdavCollection)
*/
protected function getNodes( $source, $depth )
{
// No special handling for plain resources
if ( !$this->isCollection( $source ) )
{
return array( new ezcWebdavResource( $source ) );
}
// For zero depth just return the collection
if ( $depth === ezcWebdavRequest::DEPTH_ZERO )
{
return array( new ezcWebdavCollection( $source ) );
}
$nodes = array( new ezcWebdavCollection( $source ) );
$recurseCollections = array( $source );
// Collect children for all collections listed in $recurseCollections.
for ( $i = 0; $i < count( $recurseCollections ); ++$i )
{
$source = $recurseCollections[$i];
$children = $this->getCollectionMembers( $source );
foreach ( $children as $child )
{
$nodes[] = $child;
// Check if we should recurse deeper, and add collections to
// processing list in this case.
if ( ( $child instanceof ezcWebdavCollection ) &&
( $depth === ezcWebdavRequest::DEPTH_INFINITY ) )
{
$recurseCollections[] = $child->path;
}
}
}
return $nodes;
}
/**
* Returns properties, fetched by name.
*
* Fetch properties as defined by the passed $request for the resource
* referenced. Properties are fetched by their names.
*
* This method checks also for each of the nodes affected by the request if
* authorization suceeds.
*
* @param ezcWebdavPropFindRequest $request
* @return ezcWebdavResponse
*/
protected function fetchProperties( ezcWebdavPropFindRequest $request )
{
$source = $request->requestUri;
// Get list of all affected node, depeding on source and depth
$nodes = $this->getNodes( $source, $request->getHeader( 'Depth' ) );
// Pathes which were already determined as unauthorized
$unauthorizedPaths = array();
$server = ezcWebdavServer::getInstance();
$performAuth = ( $server->auth !== null && $server->auth instanceof ezcWebdavAuthorizer );
// Get requested properties for all files
$responses = array();
foreach ( $nodes as $node )
{
// Responses for the current node
$nodeResponses = array();
// Authorization
$authorized = true;
if ( $performAuth )
{
$nodePath = $node->path;
foreach ( $unauthorizedPaths as $unauthorizedPath )
{
// Check if a parent path was already determined as unauthorized
if ( strpos( $nodePath, $unauthorizedPath ) === 0 )
{
// Skip this node completely, since we already have a
// parent node with 403
continue 2;
}
}
// Check authorization
if ( !ezcWebdavServer::getInstance()->isAuthorized( $nodePath, $request->getHeader( 'Authorization' ) ) )
{
$authorized = false;
$unauthorizedPaths[] = $nodePath;
}
}
if ( !$authorized )
{
$nodeResponses[] = new ezcWebdavPropStatResponse(
$request->prop,
// We send 403 Forbidden here. Hope that's correct? RFC
// does not state anything...
ezcWebdavResponse::STATUS_403
);
}
else
{
// Get all properties form node ...
$nodeProperties = $this->getAllProperties( $node->path );
// ... and diff the with the requested properties.
$notFound = $request->prop->diff( $nodeProperties );
$valid = $nodeProperties->intersect( $request->prop );
// Add propstat sub response for valid responses
if ( count( $valid ) )
{
$nodeResponses[] = new ezcWebdavPropStatResponse( $valid );
}
// Only create error response, when some properties could not be
// found.
if ( count( $notFound ) )
{
$nodeResponses[] = new ezcWebdavPropStatResponse(
$notFound,
ezcWebdavResponse::STATUS_404
);
}
}
// Create response
$responses[] = new ezcWebdavPropFindResponse(
$node,
$nodeResponses
);
}
return new ezcWebdavMultistatusResponse( $responses );
}
/**
* Returns names of all available properties for a resource.
*
* Fetches the names of all properties assigned to the reosource referenced
* in $request and, if the resozurce is a collection, also returns property
* names for its children, depending on the depth header of the $request.
*
* @param ezcWebdavPropFindRequest $request
* @return ezcWebdavResponse
*/
protected function fetchPropertyNames( ezcWebdavPropFindRequest $request )
{
$source = $request->requestUri;
// Get list of all affected node, depeding on source and depth
$nodes = $this->getNodes( $source, $request->getHeader( 'Depth' ) );
// Pathes which were already determined as unauthorized
$unauthorizedPaths = array();
$server = ezcWebdavServer::getInstance();
$performAuth = ( $server->auth !== null && $server->auth instanceof ezcWebdavAuthorizer );
// Get requested properties for all files
$responses = array();
foreach ( $nodes as $node )
{
if ( $performAuth )
{
$nodePath = $node->path;
foreach ( $unauthorizedPaths as $unauthorizedPath )
{
// Check if a parent path was already determined as unauthorized
if ( substr( $nodePath, $unauthorizedPath ) === 0 )
{
// Skip this node completely, since we already have a
// parent node with error response
continue 2;
}
}
// Check authorization
if ( !ezcWebdavServer::getInstance()->isAuthorized( $nodePath, $request->getHeader( 'Authorization' ) ) )
{
$unauthorizedPaths[] = $nodePath;
// Silently exclude unauthorized properties.
$responses[] = new ezcWebdavPropFindResponse(
$node,
new ezcWebdavPropStatResponse( new ezcWebdavBasicPropertyStorage() )
);
// Skip further processing of this node
continue;
}
}
// Get all properties form node ...
$nodeProperties = $this->getAllProperties( $node->path );
// ... and clear and add them to the property name storage.
$propertyNames = new ezcWebdavBasicPropertyStorage();
foreach ( $nodeProperties->getAllProperties() as $namespace => $properties )
{
foreach ( $properties as $name => $property )
{
// Clear property, because the client only want the names
// of the available properties.
$property = clone $property;
$property->clear();
$propertyNames->attach( $property );
}
}
// Add response
$responses[] = new ezcWebdavPropFindResponse(
$node,
new ezcWebdavPropStatResponse( $propertyNames )
);
}
return new ezcWebdavMultistatusResponse( $responses );
}
/**
* Returns all available properties for a resource.
*
* Fetches all available properties assigned to the reosource referenced in
* $request and, if the resource is a collection, also returns properties
* for its children, depending on the depth header of the $request. The
* instances of {@link ezcWebdavPropFindResponse} generated by this method
* are encapsulated in a {@link ezcWebdavMultistatusResponse} object.
*
* @param ezcWebdavPropFindRequest $request
* @return ezcWebdavMultistatusResponse
*/
protected function fetchAllProperties( ezcWebdavPropFindRequest $request )
{
$source = $request->requestUri;
// Get list of all affected node, depeding on source and depth
$nodes = $this->getNodes( $source, $request->getHeader( 'Depth' ) );
// Pathes which were already determined as unauthorized
$unauthorizedPaths = array();
$server = ezcWebdavServer::getInstance();
$performAuth = ( $server->auth !== null && $server->auth instanceof ezcWebdavAuthorizer );
// Get requested properties for all files
$responses = array();
foreach ( $nodes as $node )
{
if ( $performAuth )
{
foreach ( $unauthorizedPaths as $unauthorizedPath )
{
// Check if a parent path was already determined as unauthorized
if ( substr_compare( $node->path, $unauthorizedPath, 0, strlen( $unauthorizedPath ) ) === 0 )
{
// Skip this node completely, since we already have a
// parent node with 403
continue 2;
}
}
$nodePath = $node->path;
// Check authorization
if ( !ezcWebdavServer::getInstance()->isAuthorized( $nodePath, $request->getHeader( 'Authorization' ) ) )
{
$responses[] = $this->createUnauthorizedResponse(
$nodePath,
$request->getHeader( 'Authorization' )
);
$unauthorizedPaths[] = $nodePath;
// Skip further processing of node
continue;
}
}
// Just create response from properties
$responses[] = new ezcWebdavPropFindResponse(
$node,
new ezcWebdavPropStatResponse(
$this->getAllProperties( $node->path )
)
);
}
return new ezcWebdavMultistatusResponse( $responses );
}
/**
* Serves PROPFIND requests.
*
* The method receives a {@link ezcWebdavPropFindRequest} object containing
* all relevant information obout the clients request and will either
* return an instance of {@link ezcWebdavErrorResponse} to indicate an error
* or a {@link ezcWebdavPropFindResponse} on success. If the referenced
* resource is a collection or if some properties produced errors, an
* instance of {@link ezcWebdavMultistatusResponse} may be returned.
*
* The {@link ezcWebdavPropFindRequest} object contains a definition to
* find one or more properties of a given collection or non-collection
* resource.
*
* @param ezcWebdavPropFindRequest $request
* @return ezcWebdavResponse
*/
public function propFind( ezcWebdavPropFindRequest $request )
{
$source = $request->requestUri;
if ( !ezcWebdavServer::getInstance()->isAuthorized( $source, $request->getHeader( 'Authorization' ) ) )
{
// Globally issue a 401, if the user does not have access to the
// requested resource itself.
return $this->createUnauthorizedResponse(
$source,
$request->getHeader( 'Authorization' )
);
// Multistatus with 403 will be issued for nested resources in the
// specific methods.
}
// Check if resource is available
if ( !$this->nodeExists( $source ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_404,
$source
);
}
// Verify If-[None-]Match headers
$res = $this->checkIfMatchHeadersRecursive(
$request,
$source,
$request->getHeader( 'Depth' )
);
if ( $res !== null )
{
return $res;
}
// Check the exact type of propfind request and dispatch to
// corresponding method.
switch ( true )
{
case $request->prop:
return $this->fetchProperties( $request );
case $request->propName:
return $this->fetchPropertyNames( $request );
case $request->allProp:
return $this->fetchAllProperties( $request );
}
// This should really never happen, because the request class itself
// should have ensured, that on of those options is set. Untestable.
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_500
);
}
/**
* Serves PROPPATCH requests.
*
* The method receives a {@link ezcWebdavPropPatchRequest} object
* containing all relevant information obout the clients request and will
* return an instance of {@link ezcWebdavErrorResponse} on error or a
* {@link ezcWebdavPropPatchResponse} response on success. If the
* referenced resource is a collection or if only some properties produced
* errors, an instance of {@link ezcWebdavMultistatusResponse} may be
* returned.
*
* @param ezcWebdavPropPatchRequest $request
* @return ezcWebdavResponse
*/
public function propPatch( ezcWebdavPropPatchRequest $request )
{
$source = $request->requestUri;
// Check authorization
// Need to do this before checking of node existence is checked, to
// avoid leaking information
if ( !ezcWebdavServer::getInstance()->isAuthorized( $source, $request->getHeader( 'Authorization' ), ezcWebdavAuthorizer::ACCESS_WRITE ) )
{
return $this->createUnauthorizedResponse(
$source,
$request->getHeader( 'Authorization' )
);
}
// Check if resource is available
if ( !$this->nodeExists( $source ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_404,
$source
);
}
// Store proeprties, to be able to revert all changes later
$propertyBackup = clone $this->getAllProperties( $source );
$errors = array(
ezcWebdavResponse::STATUS_403 => new ezcWebdavBasicPropertyStorage(),
ezcWebdavResponse::STATUS_409 => new ezcWebdavBasicPropertyStorage(),
ezcWebdavResponse::STATUS_424 => new ezcWebdavBasicPropertyStorage(),
);
$errnous = false;
// Update properties, like requested
foreach ( $request->updates as $property )
{
// If there already has been some error, issue failed
// dependency errors for everything else.
if ( $errnous )
{
$errors[ezcWebdavResponse::STATUS_424]->attach( $property );
continue;
}
// Check for property validation errors and add a 409 for this.
if ( $property->hasError )
{
$errors[ezcWebdavResponse::STATUS_409]->attach( $property );
$errnous = true;
continue;
}
switch ( $request->updates->getFlag( $property->name, $property->namespace ) )
{
case ezcWebdavPropPatchRequest::REMOVE:
if ( !$this->removeProperty( $source, $property ) )
{
// If update failed, we assume the access has been denied.
$errors[ezcWebdavResponse::STATUS_403]->attach( $property );
$errnous = true;
}
break;
case ezcWebdavPropPatchRequest::SET:
if ( !$this->setProperty( $source, $property ) )
{
// If update failed, we assume the access has been denied.
//
// @todo: This assumptions is not particular correct.
// In case of live properties, which were tried to
// update a 409 error would be correct.
$errors[ezcWebdavResponse::STATUS_403]->attach( $property );
$errnous = true;
}
break;
default:
// This may happen, when a broken flag has been assigned
// during request generation. This SHOULD never happen.
$this->resetProperties( $source, $propertyBackup );
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_500
);
}
}
// Create node from source for response
if ( $this->isCollection( $source ) )
{
$node = new ezcWebdavCollection( $source );
}
else
{
$node = new ezcWebdavResource( $source );
}
if ( $errnous )
{
// Revert all changes
$this->resetProperties( $source, $propertyBackup );
// Create response
return new ezcWebdavMultistatusResponse(
new ezcWebdavPropPatchResponse(
$node,
new ezcWebdavPropStatResponse(
$errors[ezcWebdavResponse::STATUS_403],
ezcWebdavResponse::STATUS_403
),
new ezcWebdavPropStatResponse(
$errors[ezcWebdavResponse::STATUS_409],
ezcWebdavResponse::STATUS_409
),
new ezcWebdavPropStatResponse(
$errors[ezcWebdavResponse::STATUS_424],
ezcWebdavResponse::STATUS_424
)
)
);
}
// Verify If-[None-]Match headers.
// Done in this place to ensure that PROPPATCH would succeed otherwise.
// Reset of properties to orgiginal state is performed if ETag check
// fails.
if ( ( $res = $this->checkIfMatchHeaders( $request, $source ) ) !== null )
{
$this->resetProperties( $source, $propertyBackup );
return $res;
}
$successProps = new ezcWebdavBasicPropertyStorage();
foreach( $request->updates as $updatedProperty )
{
$successProp = clone $updatedProperty;
$successProp->clear();
$successProps->attach( $successProp );
}
// RFC update requires multi-status even if everything worked properly
return new ezcWebdavPropPatchResponse(
$node,
new ezcWebdavPropStatResponse( $successProps )
);
}
/**
* Serves PUT requests.
*
* The method receives a {@link ezcWebdavPutRequest} objects containing all
* relevant information obout the clients request and will return an
* instance of {@link ezcWebdavErrorResponse} on error or {@link
* ezcWebdavPutResponse} on success.
*
* @param ezcWebdavPutRequest $request
* @return ezcWebdavResponse
*/
public function put( ezcWebdavPutRequest $request )
{
$source = $request->requestUri;
// Check authorization
// Need to do this before checking of node existence is checked, to
// avoid leaking information
if ( !ezcWebdavServer::getInstance()->isAuthorized( $source, $request->getHeader( 'Authorization' ), ezcWebdavAuthorizer::ACCESS_WRITE ) )
{
return $this->createUnauthorizedResponse(
$source,
$request->getHeader( 'Authorization' )
);
}
// Check if parent node exists and throw a 409 otherwise
if ( !$this->nodeExists( dirname( $source ) ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_409,
$source
);
}
// Check if parent node is a collection, and throw a 409 otherwise
if ( !$this->isCollection( dirname( $source ) ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_409,
$source
);
}
// Check if resource to be updated or created does not exists already
// AND is a collection
if ( $this->nodeExists( $source ) && $this->isCollection( $source ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_409,
$source
);
}
// @todo: RFC2616 Section 9.6 PUT requires us to send 501 on all
// Content-* we don't support.
// Verify If-[None-]Match headers
if ( $this->nodeExists( $source ) && ( $res = $this->checkIfMatchHeaders( $request, $source ) ) !== null )
{
return $res;
}
// Everything is OK, create or update resource.
if ( !$this->nodeExists( $source ) )
{
$this->createResource( $source );
}
$this->setResourceContents( $source, $request->body );
$res = new ezcWebdavPutResponse(
$source
);
// Add ETag header
$res->setHeader( 'ETag', $this->getETag( $source ) );
// Deliver response
return $res;
}
/**
* Serves DELETE requests.
*
* The method receives a {@link ezcWebdavDeleteRequest} objects containing
* all relevant information obout the clients request and will return an
* instance of {@link ezcWebdavErrorResponse} on error or {@link
* ezcWebdavDeleteResponse} on success.
*
* @param ezcWebdavDeleteRequest $request
* @return ezcWebdavResponse
*/
public function delete( ezcWebdavDeleteRequest $request )
{
$source = $request->requestUri;
// Check authorization
// Need to do this before checking of node existence is checked, to
// avoid leaking information
$authState = $this->recursiveAuthCheck(
$request,
$source,
ezcWebdavAuthorizer::ACCESS_WRITE,
true
);
if ( count( $authState['errors'] ) !== 0 )
{
return $authState['errors'][0];
}
// Check if resource is available
if ( !$this->nodeExists( $source ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_404,
$source
);
}
// Verify If-[None-]Match headers
// @todo: Does this make sense for PROPFIND requests?
$res = $this->checkIfMatchHeadersRecursive(
$request,
$source,
ezcWebdavRequest::DEPTH_INFINITY
);
if ( $res !== null )
{
return $res;
}
// Delete
$deletion = $this->performDelete( $source );
if ( $deletion !== null )
{
// ezcWebdavMultistatusResponse
return $deletion;
}
// Send proper response on success
return new ezcWebdavDeleteResponse(
$source
);
}
/**
* Serves COPY requests.
*
* The method receives a {@link ezcWebdavCopyRequest} objects containing
* all relevant information obout the clients request and will return an
* instance of {@link ezcWebdavErrorResponse} on error or {@link
* ezcWebdavCopyResponse} on success. If only some operations failed, this
* method may return an instance of {@link ezcWebdavMultistatusResponse}.
*
* @param ezcWebdavCopyRequest $request
* @return ezcWebdavResponse
*/
public function copy( ezcWebdavCopyRequest $request )
{
// Indicates wheather a destiantion resource has been replaced or not.
// The success response code depends on this.
$replaced = false;
// Extract paths from request
$source = $request->requestUri;
$dest = $request->getHeader( 'Destination' );
// Check authorization
// Need to do this before checking of node existence is checked, to
// avoid leaking information
if ( !ezcWebdavServer::getInstance()->isAuthorized( $source, $request->getHeader( 'Authorization' ) ) )
{
return $this->createUnauthorizedResponse(
$source,
$request->getHeader( 'Authorization' )
);
}
if ( !ezcWebdavServer::getInstance()->isAuthorized( $dest, $request->getHeader( 'Authorization' ), ezcWebdavAuthorizer::ACCESS_WRITE ) )
{
return $this->createUnauthorizedResponse(
$dest,
$request->getHeader( 'Authorization' )
);
}
// Check if resource is available
if ( !$this->nodeExists( $source ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_404,
$source
);
}
// If source and destination are equal, the request should always fail.
if ( $source === $dest )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_403,
$source
);
}
// Check if destination resource exists and throw error, when
// overwrite header is F
if ( ( $request->getHeader( 'Overwrite' ) === 'F' ) &&
$this->nodeExists( $dest ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_412,
$dest
);
}
// Check if the destination parent directory already exists, otherwise
// bail out.
if ( !$this->nodeExists( $destDir = dirname( $dest ) ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_409,
$dest
);
}
// Verify If-[None-]Match headers on the $source
$res = $this->checkIfMatchHeadersRecursive(
$request,
$source,
$request->getHeader( 'Depth' )
);
if ( $res !== null )
{
return $res;
}
// Verify If-[None-]Match headers on the $dest if it exists
if ( $this->nodeExists( $dest ) &&
( $res = $this->checkIfMatchHeaders( $request, $dest ) ) !== null
)
{
return $res;
}
// Verify If-[None-]Match headers on the on $dests parent dir, if it
// does not exist
elseif ( ( $res = $this->checkIfMatchHeaders( $request, $destDir ) ) !== null )
{
return $res;
}
// The destination resource should be deleted if it exists and the
// overwrite headers is T
if ( ( $request->getHeader( 'Overwrite' ) === 'T' ) &&
$this->nodeExists( $dest ) )
{
// Check sub-sequent authorization on destination
$authState = $this->recursiveAuthCheck(
$request,
$dest,
ezcWebdavAuthorizer::ACCESS_WRITE,
true
);
if ( count( $authState['errors'] ) !== 0 )
{
// Permission denied on deleting destination
return $authState['errors'][0];
}
// Perform delete
// @todo: This method might return errors. If it does, the delete
// was not successful and therefore no copy should happen! (see:
// move()).
$replaced = true;
$this->performDelete( $dest );
}
$errors = array();
$copyPaths = array();
if ( $request->getHeader( 'Depth' ) === ezcWebdavRequest::DEPTH_INFINITY )
{
$authState = $this->recursiveAuthCheck( $request, $source );
$errors = $authState['errors'];
$copyPaths = $authState['paths'];
}
else
{
// Non recursive auth check necessary, plain check on $source
// already performed
$copyPaths = array( $source => ezcWebdavRequest::DEPTH_ZERO );
}
// Recursively copy paths that should be copied
foreach ( $copyPaths as $copySource => $copyDepth )
{
// Build destination path fur descendants
$copyDest = $dest . (string) substr( $copySource, strlen( $source ) );
// Perform copy and collect additional errors.
$errors = array_merge(
$errors,
// @todo: handle keepalive setting somehow - even the RFC is quite
// vague how to handle them exactly.
$this->performCopy( $copySource, $copyDest, $copyDepth )
);
}
if ( !count( $errors ) )
{
// No errors occured during copy. Just response with success.
return new ezcWebdavCopyResponse(
$replaced
);
}
// Send proper response on success
return new ezcWebdavMultistatusResponse( $errors );
}
/**
* Serves MOVE requests.
*
* The method receives a {@link ezcWebdavMoveRequest} objects containing
* all relevant information obout the clients request and will return an
* instance of {@link ezcWebdavErrorResponse} on error or {@link
* ezcWebdavMoveResponse} on success. If only some operations failed, this
* method may return an instance of {@link ezcWebdavMultistatusResponse}.
*
* @param ezcWebdavMoveRequest $request
* @return ezcWebdavResponse
*/
public function move( ezcWebdavMoveRequest $request )
{
// Indicates wheather a destiantion resource has been replaced or not.
// The success response code depends on this.
$replaced = false;
// Extract paths from request
$source = $request->requestUri;
$dest = $request->getHeader( 'Destination' );
// Check authorization
// Need to do this before checking of node existence is checked, to
// avoid leaking information
$authState = $this->recursiveAuthCheck(
$request,
$dest,
ezcWebdavAuthorizer::ACCESS_WRITE,
true
);
if ( count( $authState['errors'] ) !== 0 )
{
// Source permission denied
return $authState['errors'][0];
}
if ( !ezcWebdavServer::getInstance()->isAuthorized( $dest, $request->getHeader( 'Authorization' ), ezcWebdavAuthorizer::ACCESS_WRITE ) )
{
return $this->createUnauthorizedResponse(
$dest,
$request->getHeader( 'Authorization' )
);
}
// Check if resource is available
if ( !$this->nodeExists( $source ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_404,
$source
);
}
// If source and destination are equal, the request should always fail.
if ( $source === $dest )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_403,
$source
);
}
// Check if destination resource exists and throw error, when
// overwrite header is F
if ( ( $request->getHeader( 'Overwrite' ) === 'F' ) &&
$this->nodeExists( $dest ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_412,
$dest
);
}
// Check if the destination parent directory already exists, otherwise
// bail out.
if ( !$this->nodeExists( $destDir = dirname( $dest ) ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_409,
$dest
);
}
// Verify If-[None-]Match headers on the $source
$res = $this->checkIfMatchHeadersRecursive(
$request,
$source,
// We move, not copy!
ezcWebdavRequest::DEPTH_INFINITY
);
if ( $res !== null )
{
return $res;
}
// Verify If-[None-]Match headers on the $dest if it exists
if ( $this->nodeExists( $dest ) &&
( $res = $this->checkIfMatchHeaders( $request, $dest ) ) !== null
)
{
return $res;
}
// Verify If-[None-]Match headers on the on $dests parent dir, if it
// does not exist
elseif ( ( $res = $this->checkIfMatchHeaders( $request, $destDir ) ) !== null )
{
return $res;
}
// The destination resource should be deleted if it exists and the
// overwrite headers is T
if ( ( $request->getHeader( 'Overwrite' ) === 'T' ) &&
$this->nodeExists( $dest ) )
{
// Check sub-sequent authorization on destination
$authState = $this->recursiveAuthCheck(
$request,
$dest,
ezcWebdavAuthorizer::ACCESS_WRITE,
true
);
if ( count( $authState['errors'] ) !== 0 )
{
// Permission denied on deleting destination
return $authState['errors'][0];
}
$replaced = true;
if ( count( $delteErrors = $this->performDelete( $dest ) ) > 0 )
{
return new ezcWebdavMultistatusResponse( $delteErrors );
}
}
// All checks are passed, we can actuall copy now.
//
// MOVEd contents should always be copied using infinity depth.
//
// @todo: handle keepalive setting somehow - even the RFC is quite
// vague how to handle them exactly.
$errors = $this->performCopy( $source, $dest, ezcWebdavRequest::DEPTH_INFINITY );
// If an error occured we skip deletion of source.
//
// @IMPORTANT: This is a definition / assumption made by us, because it
// is not defined in the RFC how to handle such a case.
if ( count( $errors ) )
{
// We need a multistatus response, because some errors occured for some
// of the resources.
return new ezcWebdavMultistatusResponse( $errors );
}
// Delete the source, COPY has been successful
$deletion = $this->performDelete( $source );
// If deletion failed, this has again been caused by the automatic
// error causing facilities of the backend. Send 423 by choice.
//
// @todo: The error generated here should depend on the actual backend
// implementation and not be generated guessing what may fit.
if ( count( $deletion ) > 0 )
{
return new ezcWebdavMultistatusResponse( $deletion );
}
// Send proper response on success
return new ezcWebdavMoveResponse(
$replaced
);
}
/**
* Serves MKCOL (make collection) requests.
*
* The method receives a {@link ezcWebdavMakeCollectionRequest} objects
* containing all relevant information obout the clients request and will
* return an instance of {@link ezcWebdavErrorResponse} on error or {@link
* ezcWebdavMakeCollectionResponse} on success.
*
* @param ezcWebdavMakeCollectionRequest $request
* @return ezcWebdavResponse
*/
public function makeCollection( ezcWebdavMakeCollectionRequest $request )
{
$collection = $request->requestUri;
// Check authorization
// Need to do this before checking of node existence is checked, to
// avoid leaking information
if ( !ezcWebdavServer::getInstance()->isAuthorized( $collection, $request->getHeader( 'Authorization' ), ezcWebdavAuthorizer::ACCESS_WRITE ) )
{
return $this->createUnauthorizedResponse(
$collection,
$request->getHeader( 'Authorization' )
);
}
// If resource already exists, the collection cannot be created and a
// 405 is thrown.
if ( $this->nodeExists( $collection ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_405,
$collection
);
}
// Check if the parent node already exists, otherwise throw a 409
// error.
if ( !$this->nodeExists( dirname( $collection ) ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_409,
$collection
);
}
// If the parent node exists, but is a resource, which obviously can
// not accept any members, throw a 403 error.
if ( !$this->isCollection( $destDir = dirname( $collection ) ) )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_403,
$collection
);
}
// Verify If-[None-]Match headers on the on $dests parent dir
if ( ( $res = $this->checkIfMatchHeaders( $request, $destDir ) ) !== null )
{
return $res;
}
// As the handling of request bodies is not described in RFC 2518, we
// skip their handling and always return a 415 error.
if ( $request->body )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_415,
$collection
);
}
// Cause error, if requested?
// All checks passed, we can create the collection
$this->createCollection( $collection );
// Return success
return new ezcWebdavMakeCollectionResponse(
$collection
);
}
/**
* Handles the OPTIONS request.
*
* Applies authorization checking to the OPTIONS request and returns the
* parent response.
*
* @param ezcWebdavOptionsRequest $request
* @return ezcWebdavOptionsResponse
*/
public function options( ezcWebdavOptionsRequest $request )
{
// Check authorization
if ( !ezcWebdavServer::getInstance()->isAuthorized( $request->requestUri, $request->getHeader( 'Authorization' ) ) )
{
return $this->createUnauthorizedResponse(
$request->requestUri,
$request->getHeader( 'Authorization' )
);
}
return parent::options( $request );
}
/**
* Returns the etag representing the current state of $path.
*
* Calculates and returns the ETag for the resource represented by $path.
* The ETag is calculated from the $path itself and the following
* properties, which are concatenated and md5 hashed:
*
* <ul>
* <li>getcontentlength</li>
* <li>getlastmodified</li>
* </ul>
*
* This method can be overwritten in custom backend implementations to
* access the information needed directly without using the way around
* properties.
*
* Custom backend implementations are encouraged to use the same mechanism
* (or this method itself) to determine and generate ETags.
*
* @param mixed $path
* @return void
*/
protected function getETag( $path )
{
$contentLength = $this->getProperty( $path, 'getcontentlength' )->length;
$lastModified = $this->getProperty( $path, 'getlastmodified' )->date->format( 'c' );
return md5( $path . $contentLength . $lastModified );
}
/**
* Checks If-[Match-]Headers recursively on $path with $depth.
*
* Performs a recursive check using {@link checkIfMatchHeaders()} on $path.
* $depth can be any of the {@link ezcWebdavRequest} DEPTH_* constants.
*
* Returns {@link ezcWebdavErrorResponse} if any ETag check failed, null if
* everything went allright.
*
* @param ezcWebdavRequest $req
* @param string $path
* @param int $depth
* @return ezcWebdavErrorResponse|null
*/
private function checkIfMatchHeadersRecursive( ezcWebdavRequest $req, $path, $depth )
{
// Stop checking if non-collection is reached or depth is 0.
if ( !$this->isCollection( $path ) || $depth === ezcWebdavRequest::DEPTH_ZERO )
{
return $this->checkIfMatchHeaders( $req, $path );
}
// Check collection ETag
if ( ( $res = $this->checkIfMatchHeaders( $req, $path ) ) !== null )
{
return $res;
}
// $path is a collection, depth is > 0: Recurse.
$newDepth = $depth === ezcWebdavRequest::DEPTH_ONE
? ezcWebdavRequest::DEPTH_ZERO
: ezcWebdavRequest::DEPTH_INFINITY;
foreach ( $this->getCollectionMembers( $path ) as $member )
{
// If-[None-]Match header check produced error.
if ( ( $res = $this->checkIfMatchHeadersRecursive( $req, $member->path, $newDepth ) ) !== null )
{
return $res;
}
}
return null;
}
/**
* Checks the If-Match and If-None-Match headers.
*
* Checks if the If-Match and If-None-Match headers (potentially) provided
* by $req are valid for the resource identified by $path.
*
* Returns null if everything is alright, ezcWebdavErrorResponse if any
* ETag check failed.
*
* @param ezcWebdavRequest $req
* @param string $path
* @return ezcWebdavErrorResponse|null
*/
protected function checkIfMatchHeaders( ezcWebdavRequest $req, $path )
{
if ( ( $res = $this->checkIfMatchHeader( $req, $path ) ) !== null )
{
return $res;
}
if ( ( $res = $this->checkIfNoneMatchHeader( $req, $path ) ) !== null )
{
return $res;
}
return null;
}
/**
* Checks the If-Match-Header on $path, if present in $req.
*
* Returns ezcWebdavErrorResponse on failure, otherwise null.
*
* @param ezcWebdavRequest $req
* @param string $path
* @return ezcWebdavErrorResponse|null
*/
private function checkIfMatchHeader( ezcWebdavRequest $req, $path )
{
if ( ( $matches = $req->getHeader( 'If-Match' ) ) !== null )
{
$etag = $this->getETag( $path );
if ( $this->checkMatchHeader( $matches, $etag ) === false )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_412,
$path,
'If-Match header check failed.'
);
}
}
return null;
}
/**
* Checks the If-Match-Header on $path, if present in $req.
*
* Returns ezcWebdavErrorResponse on failure, otherwise null.
*
* @param ezcWebdavRequest $req
* @param string $path
* @return ezcWebdavErrorResponse|null
*/
private function checkIfNoneMatchHeader( ezcWebdavRequest $req, $path )
{
if ( ( $matches = $req->getHeader( 'If-None-Match' ) ) !== null )
{
$etag = $this->getETag( $path );
if ( $this->checkMatchHeader( $matches, $etag ) === true )
{
return new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_412,
$path,
'If-None-Match header check failed.'
);
}
}
return null;
}
/**
* Checks the If-[None-]Match header values against an $etag.
*
* Returns in any of the ETags given in $matches euqlauls to $etag.
*
* This is used in {@link checkIfMatchHeader()} and {@link
* checkIfNoneMatchHeader()}. The {@link checkIfMatchHeader()} method
* expects true as a good result, while {@link checkIfNoneMatchHeader()}
* desires false.
*
* @param array(string) $matches
* @param string $etag
* @return bool
*/
private function checkMatchHeader( $matches, $etag )
{
if ( $matches === true )
{
return true;
}
foreach ( $matches as $testEtag )
{
if ( $etag === $testEtag )
{
return true;
}
}
return false;
}
/**
* Recursively checks authorization for the COPY, MOVE and other requests.
*
* This method performs a recursive authorization check on the given $path
* using the credentials provided in $request. It returns a
* multidimensional array, indicating the authorization errors occurred and
* the paths that may by copied.
*
* The structure looks like this:
* <code>
* array(
* 'errors' => array(
* ezcWebdavErrorResponse(),
* ezcWebdavErrorResponse(),
* // ...
* )
* 'paths' => array(
* '/some/path' => ezcWebdavRequest::DEPTH_INFINITY,
* '/some/other/path' => ezcWebdavRequest::DEPTH_ZERO,
* // ...
* )
* )
* </code>
*
* The 'errors' key is assigned to an array of authorization error
* responses that will be merged to the ezcWebdavMultistatusResponse
* returned by the copy() method. The 'paths' array contains all paths that
* may be copied by the method. A path is assigned to the depth that it
* might be copied. The depth can be {@link
* ezcWebdavRequest::DEPTH_INFINITY} to indicate that a complete sub tree
* is save for copying, or {@link ezcWebdavRequest::DEPTH_ZERO}, to
* indicate that only the path itself may be copied, but none of its
* descendants.
*
* The $access parameter specifies which permission is to be checked {@link
* ezcWebdavAuthorizer::ACCESS_READ} is the default, {@link
* ezcWebdavAuthorizer::ACCESS_WRITE} may be set to indicate write
* permissions.
*
* If the $breakOnError parameter is set to true, no further checks will be
* applied to sibling resources, but the method will instantly return. This
* parameter is set to true for the MOVE request, since this request must
* be processed completly or not at all. The COPY request in contrast may
* also be processed partially, so this parameter is left as is.
*
* @param ezcWebdavRequest $request
* @param string $path
* @param int $access
* @param bool $breakOnError
* @return array
*
* @todo Mark protected as soon as API is final.
*/
private function recursiveAuthCheck( ezcWebdavRequest $request, $path, $access = ezcWebdavAuthorizer::ACCESS_WRITE, $breakOnError = false )
{
$result = array(
'errors' => array(),
'paths' => array(),
);
// Check auth for collections and resources equally
if ( !ezcWebdavServer::getInstance()->isAuthorized( $path, $request->getHeader( 'Authorization' ), $access ) )
{
$result['errors'][] = $this->createUnauthorizedResponse(
$path,
$request->getHeader( 'Authorization' )
);
}
else
{
if ( $this->isCollection( $path ) )
{
foreach ( $this->getCollectionMembers( $path ) as $member )
{
$tmpRes = $this->recursiveAuthCheck( $request, $member->path, $access );
if ( count( $tmpRes['errors'] ) !== 0 )
{
if ( $breakOnError )
{
return $tmpRes;
}
$result['errors'] = array_merge( $result['errors'], $tmpRes['errors'] );
$result['paths'] = array_merge( $result['paths'], $tmpRes['paths'] );
}
}
$result['paths'][$path] = ( count( $result['errors'] ) ? ezcWebdavRequest::DEPTH_ZERO : ezcWebdavRequest::DEPTH_INFINITY );
}
else
{
// Only a resource, so depth infinity does not make sense
$result['paths'][$path] = ezcWebdavRequest::DEPTH_ZERO;
}
}
return $result;
}
/**
* Returns an error response to indicate failed authorization.
*
* This method returns an instance of {@link ezcWebdavErrorResponse} with a
* corresponding status code, indicating that the request to $path was not
* authorized. In case the user did not provide authentication at all, the
* status code 401 (Unauthorized) is used to give the possibility of
* authenticating. Otherwise 403 (Forbidden) is used, since the
* authenticated user simply does not have access.
*
* @param string $path
* @param ezcWebdavAuthBasic|ezcWebdavAúthDigest $authHeader
* @return ezcWebdavErrorResponse
*/
private function createUnauthorizedResponse( $path, $authHeader = null )
{
// Check for anonymous auth
if ( $authHeader === null || $authHeader->username === '' )
{
return ezcWebdavServer::getInstance()->createUnauthenticatedResponse(
$path,
'Authorization failed.'
);
}
// Authenticated user does not have access
return ezcWebdavServer::getInstance()->createUnauthorizedResponse(
$path,
'Authorization failed.'
);
}
}
?>