blob: e600b5eae8f974e5b8ffcc24b6d161b0fc40b4b0 [file]
<?php
/**
* File containing the ezcWebdavMemoryBackend 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//
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
* @access private
*/
/**
* Backend that only resides in memory.
*
* Memory backend to serve some virtual content tree, offering options to cause
* failures in operations, mainly for testing the webdav server.
*
* The content of the backend is constructed from a multidimentional array
* structure representing the collections and files. The metadata may only be
* set by appropriate requests to the backend. No information is stored
* anywhere, so that every reinitialisations gives you a fresh backend.
*
* <code>
* $backend = new ezcWebdavMemoryBackend();
* $backend->addContents(
* array(
* 'foo' => 'bar', // File with content "bar"
* 'bar' => array( // Collection "bar"
* 'blubb' => 'Some more content.'
* // File bar/blubb with some more content
* ),
* )
* );
* </code>
*
* This backend does not implement any special features to test the servers
* capabilities to work with those features.
*
* @version //autogentag//
* @package Webdav
* @access private
*/
class ezcWebdavMemoryBackend extends ezcWebdavSimpleBackend implements ezcWebdavLockBackend
{
/**
* Options of the memory backend
*
* @var ezcWebdavMemoryBackendOptions
*/
protected $options;
/**
* Content structure of memory backend
*
* @var array
*/
protected $content = array(
'/' => array(),
);
/**
* Properties for collections and resources.
*
* They are stored in an array of the following form reusing the initial
* content example:
*
* array(
* '/foo' => array(
* 'property name' => 'property value',
* ),
* '/bar' => array(),
* '/bar/blubb' => array(),
* ...
* )
*
* @var array
*/
protected $props = array();
/**
* Indicates wheather to fake live properties.
*
* @var bool
*/
protected $fakeLiveProperties;
/**
* Construct backend from a given path.
*
* @param bool $fakeLiveProperties
* @return void
*/
public function __construct( $fakeLiveProperties = true )
{
$this->options = new ezcWebdavMemoryBackendOptions();
$this->fakeLiveProperties = $fakeLiveProperties;
// Initialize properties for root
if ( $fakeLiveProperties )
{
$this->props['/'] = $this->initializeProperties( '/', true );
}
}
/**
* Acquire a backend lock.
*
* Locks the complete backend using the lock file specified in {@link
* ezcWebdavMemoryBackendOptions->$lockFile}.
*
* @param int $waitTime Microseconds.
* @param int $timeout Microseconds.
* @return void
*/
public function lock( $waitTime, $timeout )
{
$lockFile = $this->options->lockFile;
$lockStart = microtime( true );
// fopen in mode 'x' will only open the file, if it does not exist yet.
// Even this is is expected it will throw a warning, if the file
// exists, which we need to silence using the @
while ( ( $fp = @fopen( $lockFile, 'x' ) ) === false )
{
// This is untestable.
if ( microtime( true ) - $lockStart > $timeout )
{
// Release timed out lock
unlink( $lockFile );
$lockStart = microtime( true );
}
else
{
usleep( $waitTime );
}
}
// Store random bit in file ... the microtime for example - might prove
// useful some time.
fwrite( $fp, microtime() );
fclose( $fp );
}
/**
* Release the backend lock.
*
* Releases the lock acquired by {@link lock()}.
*
* @return void
*/
public function unlock()
{
// Silence since lock maybe released if request processing takes too
// long
@unlink( $this->options->lockFile );
}
/**
* Offer access to some of the server properties.
*
* @throws ezcBasePropertyNotFoundException
* If the property $name is not defined
* @param string $name
* @return mixed
* @ignore
*/
public function __get( $name )
{
switch ( $name )
{
case 'options':
return $this->$name;
default:
throw new ezcBasePropertyNotFoundException( $name );
}
}
/**
* Sets the option $name to $value.
*
* @throws ezcBasePropertyNotFoundException
* if the property $name is not defined
* @throws ezcBaseValueException
* if $value is not correct for the property $name
* @param string $name
* @param mixed $value
* @return void
* @ignore
*/
public function __set( $name, $value )
{
switch ( $name )
{
case 'options':
if ( ! $value instanceof ezcWebdavMemoryBackendOptions )
{
throw new ezcBaseValueException( $name, $value, 'ezcWebdavMemoryBackendOptions' );
}
$this->$name = $value;
break;
default:
throw new ezcBasePropertyNotFoundException( $name );
}
}
/**
* Return an initial set of properties for resources and collections.
*
* The second parameter indicates wheather the given resource is a
* collection. The returned properties are used to initialize the property
* arrays for the given content.
*
* @param string $name
* @param bool $isCollection
* @return array
*
* @access protected
*/
public function initializeProperties( $name, $isCollection = false )
{
if ( $this->fakeLiveProperties )
{
$propertyStorage = new ezcWebdavBasicPropertyStorage();
// Add default creation date
$propertyStorage->attach(
new ezcWebdavCreationDateProperty( new ezcWebdavDateTime( '@1054034820' ) )
);
// Define default display name
$propertyStorage->attach(
new ezcWebdavDisplayNameProperty( basename( urldecode( $name ) ) )
);
// Define default language
$propertyStorage->attach(
new ezcWebdavGetContentLanguageProperty( array( 'en' ) )
);
// Define default content type
$propertyStorage->attach(
new ezcWebdavGetContentTypeProperty(
$isCollection ? 'httpd/unix-directory' : 'application/octet-stream'
)
);
// Define default ETag
$propertyStorage->attach(
new ezcWebdavGetEtagProperty( $this->getETag( $name ) )
);
// Define default modification time
$propertyStorage->attach(
new ezcWebdavGetLastModifiedProperty( new ezcWebdavDateTime( '@1124118780' ) )
);
// Define content length if node is a resource.
$propertyStorage->attach(
new ezcWebdavGetContentLengthProperty(
$isCollection ?
ezcWebdavGetContentLengthProperty::COLLECTION :
(string) strlen( $this->content[$name] )
)
);
$propertyStorage->attach(
new ezcWebdavResourceTypeProperty(
( $isCollection === true ?
ezcWebdavResourceTypeProperty::TYPE_COLLECTION :
ezcWebdavResourceTypeProperty::TYPE_RESOURCE
)
)
);
}
else
{
$propertyStorage = new ezcWebdavBasicPropertyStorage();
}
return $propertyStorage;
}
/**
* Clones the given $fromStorage for $toPath.
*
* Initializes a new property storage for $toPath with new live properties
* and clones all non exsitent properties from $fromStorage to it.
*
* @param string $toPath
* @param bool $isCollection
* @param ezcWebdavBasicPropertyStorage $fromStorage
* @return void
*/
private function cloneProperties( $toPath, $isCollection, ezcWebdavBasicPropertyStorage $fromStorage )
{
$toStorage = $this->initializeProperties( $toPath, $isCollection );
foreach ( $fromStorage as $prop )
{
if ( !$toStorage->contains( $prop->name, $prop->namespace ) )
{
$toStorage->attach( clone $prop );
}
}
$this->props[$toPath] = $toStorage;
}
/**
* Overwrites ETag generation from simple backend.
*
* Generates an ETag based on $path and the content of $path (if available
* and not a collection).
*
* @param string $path
* @return string
*/
protected function getETag( $path )
{
return ( md5( $path ) );
}
/**
* Read valid data from given content array and initialize property
* storage.
*
* @param array $contents
* @param string $path
* @return void
*/
public function addContents( array $contents, $path = '/' )
{
foreach ( $contents as $name => $content )
{
if ( !is_string( $name ) )
{
// Ignore elements which do not have a string key
continue;
}
// Full path to resource
$resourcePath = $path . $name;
if ( is_array( $content ) )
{
// Content is a collection
$this->content[$resourcePath] = array();
$this->props[$resourcePath] = $this->initializeProperties(
$resourcePath,
true
);
// Recurse
$this->addContents( $content, $resourcePath . '/' );
}
elseif ( is_string( $content ) )
{
// Content is a file
$this->content[$resourcePath] = $content;
$this->props[$resourcePath] = $this->initializeProperties(
$resourcePath
);
}
else
{
// Ignore everything else...
continue;
}
// Add contents to parent directory
$parent = ( $path === '/' ? '/' : substr( $path, 0, -1 ) );
$this->content[$parent][] = $resourcePath;
}
}
/**
* Create a new collection.
*
* Creates a new collection at the given path.
*
* @param string $path
* @return void
*/
protected function createCollection( $path )
{
// Create collection
$this->content[$path] = array();
// Add collection to parent node
$this->content[dirname( $path )][] = $path;
// Set initial metadata for collection
$this->props[$path] = $this->initializeProperties( $path, true );
}
/**
* 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
*/
protected function createResource( $path, $content = null )
{
// Create resource
$this->content[$path] = $content;
// Add resource to parent node
$this->content[dirname( $path )][] = $path;
// Set initial metadata for collection
$this->props[$path] = $this->initializeProperties( $path, false );
}
/**
* Set contents of a resource.
*
* Change the contents of the given resource to the given content.
*
* @param string $path
* @param string $content
* @return void
*/
protected function setResourceContents( $path, $content )
{
$this->content[$path] = $content;
}
/**
* Get contents of a resource.
*
* @param string $path
* @return string
*/
protected function getResourceContents( $path )
{
return $this->content[$path];
}
/**
* Manually set a property on a resource to request it later.
*
* @param string $resource
* @param ezcWebdavProperty $property
* @return bool
*/
public function setProperty( $resource, ezcWebdavProperty $property )
{
// Live properties may not be updated by a client.
if ( $this->options->failingOperations & ezcWebdavMemoryBackendOptions::REQUEST_PROPPATCH )
{
return false;
}
// Bail out, if the resource is not known yet.
if ( !array_key_exists( $resource, $this->props ) )
{
return false;
}
$this->props[$resource]->attach( $property );
return true;
}
/**
* Manually remove a property from a resource.
*
* @param string $resource
* @param ezcWebdavProperty $property
* @return bool
*/
public function removeProperty( $resource, ezcWebdavProperty $property )
{
// Live properties may not be removed.
if ( $property instanceof ezcWebdavLiveProperty )
{
return false;
}
$this->props[$resource]->detach( $property->name, $property->namespace );
return true;
}
/**
* Reset property storage for a resource.
*
* @param string $resource
* @param ezcWebdavPropertyStorage $properties
* @return bool
*/
public function resetProperties( $resource, ezcWebdavPropertyStorage $properties )
{
$this->props[$resource] = $properties;
}
/**
* Manually get a property on a resource.
*
* Get the property with the given name from the given resource. You may
* optionally define a namespace to receive the property from.
*
* @param string $resource
* @param string $propertyName
* @param string $namespace
* @return ezcWebdavProperty
*/
public function getProperty( $resource, $propertyName, $namespace = 'DAV:' )
{
return $this->props[$resource]->get( $propertyName, $namespace );
}
/**
* Manually get a property on a resource.
*
* Get all properties for the given resource as a {@link
* ezcWebdavPropertyStorage}
*
* @param string $resource
* @return ezcWebdavBasicPropertyStorage
*/
public function getAllProperties( $resource )
{
return $this->props[$resource];
}
/**
* Copy resources recursively from one path to another.
*
* Returns an array with {@link ezcWebdavErrorResponse}s for all subtree,
* where the copy operation failed. Errors subsequent nodes 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)
*/
protected function performCopy( $fromPath, $toPath, $depth = ezcWebdavRequest::DEPTH_INFINITY )
{
$causeErrors = (bool) ( $this->options->failingOperations & ( ezcWebdavMemoryBackendOptions::REQUEST_COPY | ezcWebdavMemoryBackendOptions::REQUEST_MOVE ) );
$errors = array();
if ( !is_array( $this->content[$fromPath] ) ||
( is_array( $this->content[$fromPath] ) && ( $depth === ezcWebdavRequest::DEPTH_ZERO ) ) )
{
// Copy a resource, or a collection, but the depth header told not
// to recurse into collections
if ( $causeErrors && preg_match( $this->options->failForRegexp, $fromPath ) > 0 )
{
// Completely abort with error
return array( ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_423,
$fromPath
) );
}
if ( $causeErrors && preg_match( $this->options->failForRegexp, $toPath ) > 0 )
{
// Completely abort with error
return array( ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_412,
$toPath
) );
}
// Perform copy operation
if ( is_array( $this->content[$fromPath] ) )
{
// Create a new empty collection
$this->content[$toPath] = array();
}
else
{
// Copy file content
$this->content[$toPath] = $this->content[$fromPath];
}
// Copy properties
$this->cloneProperties(
$toPath,
is_array( $this->content[$toPath] ),
$this->props[$fromPath]
);
// Add to parent node
$this->content[dirname( $toPath )][] = $toPath;
}
else
{
// Copy a collection
$errnousSubtrees = array();
// Array of copied collections, where the child names are required
// to be modified depending on the success of the copy operation.
$copiedCollections = array();
// Check all nodes, if they math the fromPath
foreach ( $this->content as $resource => $content )
{
if ( strpos( $resource, $fromPath ) !== 0 )
{
// This resource is not affected by the copy operation
continue;
}
// Check if this resource should be skipped, because
// already one of the parent nodes caused an error.
foreach ( $errnousSubtrees as $subtree )
{
if ( strpos( $resource, $subtree ) )
{
// Skip resource, then.
continue 2;
}
}
// Check if this resource should cause an error
if ( $causeErrors && preg_match( $this->options->failForRegexp, $resource ) )
{
// Cause an error and skip resource
$errors[] = new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_423,
$resource
);
continue;
}
// To actually perform the copy operation, modify the
// destination resource name
$newResourceName = preg_replace( '(^' . preg_quote( $fromPath ) . ')', $toPath, $resource );
// Check if this resource should cause an error
if ( $causeErrors && preg_match( $this->options->failForRegexp, $newResourceName ) )
{
// Cause an error and skip resource
$errors[] = new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_412,
$newResourceName
);
continue;
}
// Add collection to collection child recalculation array
if ( is_array( $this->content[$resource] ) )
{
$copiedCollections[] = $newResourceName;
}
// Actually copy
$this->content[$newResourceName] = $this->content[$resource];
// Copy properties
$this->cloneProperties(
$newResourceName,
is_array( $this->content[$resource] ),
$this->props[$resource]
);
// Add to parent node
$this->content[dirname( $newResourceName )][] = $newResourceName;
}
// Iterate over all copied collections and update the child
// references
foreach ( $copiedCollections as $collection )
{
foreach ( $this->content[$collection] as $nr => $child )
{
foreach ( $errnousSubtrees as $subtree )
{
if ( strpos( $child, $subtree ) )
{
// If child caused an error, it has not been
// copied, so we remove it.
unset( $this->content[$collection][$nr] );
continue 2;
}
}
// Also remove all references to old children, new children
// have already been added during the last step.
if ( preg_match( '(^' . preg_quote( $fromPath ) . ')', $child ) )
{
unset( $this->content[$collection][$nr] );
}
}
$this->content[$collection] = array_values( $this->content[$collection] );
}
}
return $errors;
}
/**
* Delete everything below this path.
*
* Returns an error response if the deletion failed, and null on success.
*
* @param string $path
* @return ezcWebdavErrorResponse
*/
protected function performDelete( $path )
{
// Check if any errors would occur during deletion process
$error = array();
foreach ( $this->content as $name => $content )
{
if ( strpos( $name, $path ) === 0 && ( substr( $name, strlen( $path ), 1 ) === '/' || $name === $path ) )
{
// Check if we want to cause some errors here.
if ( $this->options->failingOperations & ezcWebdavMemoryBackendOptions::REQUEST_DELETE && preg_match( $this->options->failForRegexp, $name ) > 0 )
{
$error[] = new ezcWebdavErrorResponse(
ezcWebdavResponse::STATUS_423,
$name
);
}
}
}
// If errors occured, return them
if ( count( $error ) )
{
return new ezcWebdavMultistatusResponse(
$error
);
}
// Remove all content nodes starting with requested path
foreach ( $this->content as $name => $content )
{
if ( strpos( $name, $path ) === 0 && ( substr( $name, strlen( $path ), 1 ) === '/' || $name === $path ) )
{
unset( $this->content[$name] );
unset( $this->props[$name] );
}
}
// Remove parent node assignement to removed node
$id = array_search( $path, $this->content[$parent = dirname( $path )] );
if ( $id !== false )
{
unset( $this->content[$parent][$id] );
$this->content[$parent] = array_values( $this->content[$parent] );
}
return null;
}
/**
* Check if node exists.
*
* Check if a node exists with the given path.
*
* @param string $path
* @return bool
*
* @access protected
*/
public function nodeExists( $path )
{
return isset( $this->content[$path] );
}
/**
* Check if node is a collection.
*
* Check if the node behind the given path is a collection.
*
* @param string $path
* @return bool
*
* @access protected
*/
public function isCollection( $path )
{
return $this->nodeExists( $path ) && is_array( $this->content[$path] );
}
/**
* Get members of collection.
*
* Returns an array with the members of the collection given by the path of
* the collection.
*
* The returned array holds elements which are either ezcWebdavCollection,
* or ezcWebdavResource.
*
* @param string $path
* @return array
*/
protected function getCollectionMembers( $path )
{
$contents = array();
foreach ( $this->content[$path] as $child )
{
if ( is_array( $this->content[$child] ) )
{
// Add collection without any children
$contents[] = new ezcWebdavCollection(
$child
);
}
else
{
// Add files without content
$contents[] = new ezcWebdavResource(
$child
);
}
}
return $contents;
}
/**
* Clones the memory backend deeply.
*
* @return void
*/
public function __clone()
{
foreach ( $this->props as $path => $propStorage )
{
$this->props[$path] = clone $propStorage;
}
}
}
?>