* $backend = new ezcWebdavFileBackend( * 'directory/' * ); * * * Live properties are partly determined from the file systems itself (like * {@link ezcWebdavGetContentLengthProperty}), others need to be stored like * dead properties. This backend uses a special path for each resource to store * this information in its XML representation. * * @version 1.1alpha1 * @package Webdav * @mainclass */ class ezcWebdavFileBackend extends ezcWebdavSimpleBackend implements ezcWebdavLockBackend { /** * Options. * * @var ezcWebdavFileBackendOptions */ protected $options; /** * Root directory to serve content from. All paths are seen relatively to this one. * * @var string */ protected $root; /** * Keeps track of the lock level. * * Each time the lock() method is called, this counter is raised by 1. if * it was 0 before, the actual locking mechanism gets into action, * otherwise just the counter is raised. The lock is physically only freed, * if this counter is 0. * * This mechanism allows nested locking, as it is necessary, if the lock * plugin locks this backend external, but interal locking needs still to * be supported. * * @var int */ protected $lockLevel = 0; /** * Names of live properties from the DAV: namespace which will be handled * live, and should not be stored like dead properties. * * @var array(int=>string) */ protected $handledLiveProperties = array( 'getcontentlength', 'getlastmodified', 'creationdate', 'displayname', 'getetag', 'getcontenttype', 'resourcetype', 'supportedlock', 'lockdiscovery', ); /** * Creates a new backend instance. * * Creates a new backend to server WebDAV content from the file system path * identified by $root. If the given path does not exist or is not a * directory, an exception will be thrown. * * @param string $root * @return void * * @throws ezcBaseFileNotFoundException * if the given $root does not exist or is not a directory. * @throws ezcBaseFilePermissionException * if the given $root is not readable. */ public function __construct( $root ) { if ( !is_dir( $root ) ) { throw new ezcBaseFileNotFoundException( $root ); } if ( !is_readable( $root ) ) { throw new ezcBaseFilePermissionException( $root, ezcBaseFileException::READ ); } $this->root = realpath( $root ); $this->options = new ezcWebdavFileBackendOptions(); } /** * Locks the backend. * * Tries to lock the backend. If the lock is already owned by this process, * locking is successful. If $timeout is reached before a lock could be * acquired, an {@link ezcWebdavLockTimeoutException} is thrown. Waits * $waitTime microseconds between attempts to lock the backend. * * @param int $waitTime * @param int $timeout * @return void */ public function lock( $waitTime, $timeout ) { // Check and raise lockLevel counter if ( $this->lockLevel > 0 ) { // Lock already acquired ++$this->lockLevel; return; } $lockStart = microtime( true ); $lockFileName = $this->root . '/' . $this->options->lockFileName; // 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( $lockFileName, 'x' ) ) === false ) { // This is untestable. if ( microtime( true ) - $lockStart > $timeout ) { // Release timed out lock unlink( $lockFileName ); $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 ); // Add first lock ++$this->lockLevel; } /** * Removes the lock. * * @return void */ public function unlock() { if ( --$this->lockLevel === 0 ) { // Remove the lock file $lockFileName = $this->root . '/' . $this->options->lockFileName; unlink( $lockFileName ); } } /** * Property get access. * Simply returns a given property. * * @throws ezcBasePropertyNotFoundException * If a the value for the property propertys is not an instance of * @param string $name The name of the property to get. * @return mixed The property value. * * @ignore * * @throws ezcBasePropertyNotFoundException * if the given property does not exist. * @throws ezcBasePropertyPermissionException * if the property to be set is a write-only property. */ public function __get( $name ) { switch ( $name ) { case 'options': return $this->$name; default: throw new ezcBasePropertyNotFoundException( $name ); } } /** * Sets a property. * This method is called when an property is to be set. * * @param string $name The name of the property to set. * @param mixed $value The property value. * @return void * @ignore * * @throws ezcBasePropertyNotFoundException * if the given property does not exist. * @throws ezcBaseValueException * if the value to be assigned to a property is invalid. * @throws ezcBasePropertyPermissionException * if the property to be set is a read-only property. */ public function __set( $name, $value ) { switch ( $name ) { case 'options': if ( ! $value instanceof ezcWebdavFileBackendOptions ) { throw new ezcBaseValueException( $name, $value, 'ezcWebdavFileBackendOptions' ); } $this->$name = $value; break; default: throw new ezcBasePropertyNotFoundException( $name ); } } /** * Wait and get lock for complete directory tree. * * Acquire lock for the complete tree for read or write operations. This * does not implement any priorities for operations, or check if several * read operation may run in parallel. The plain locking should / could be * extended by something more sophisticated. * * If the tree already has been locked, the method waits until the lock can * be acquired. * * The optional second parameter $readOnly indicates wheather a read only * lock should be acquired. This may be used by extended implementations, * but it is not used in this implementation. * * @param bool $readOnly * @return void * * @todo The locking mechanism affects the ETag of the base collection. The * ETag is different on each request, which might result in problems * for clients that make extensive use of If-* headers. No client is * known so far, if problems occur here we need to find a solution * for this. */ protected function acquireLock( $readOnly = false ) { if ( $this->options->noLock ) { return true; } try { $this->lock( $this->options->waitForLock, 2000000 ); } catch ( ezcWebdavLockTimeoutException $e ) { return false; } return true; } /** * Free lock. * * Frees the lock after the operation has been finished. * * @return void */ protected function freeLock() { if ( $this->options->noLock ) { return true; } $this->unlock(); } /** * Returns the mime type of a resource. * * Return the mime type of the resource identified by $path. If a mime type * extension is available it will be used to read the real mime type, * otherwise the original mime type passed by the client when uploading the * file will be returned. If no mimetype has ever been associated with the * file, the method will just return 'application/octet-stream'. * * @param string $path * @return string */ protected function getMimeType( $path ) { // Check if extension pecl/fileinfo is usable. if ( $this->options->useMimeExts && ezcBaseFeatures::hasExtensionSupport( 'fileinfo' ) ) { $fInfo = new fInfo( FILEINFO_MIME ); $mimeType = $fInfo->file( $this->root . $path ); // The documentation tells to do this, but it does not work with a // current version of pecl/fileinfo // $fInfo->close(); return $mimeType; } // Check if extension ext/mime-magic is usable. if ( $this->options->useMimeExts && ezcBaseFeatures::hasExtensionSupport( 'mime_magic' ) && ( $mimeType = mime_content_type( $this->root . $path ) ) !== false ) { return $mimeType; } // Check if some browser submitted mime type is available. $storage = $this->getPropertyStorage( $path ); $properties = $storage->getAllProperties(); if ( isset( $properties['DAV:']['getcontenttype'] ) ) { return $properties['DAV:']['getcontenttype']->mime; } // Default to 'application/octet-stream' if nothing else is available. return 'application/octet-stream'; } /** * Creates a new collection. * * Creates a new collection at the given $path. * * @param string $path * @return void */ protected function createCollection( $path ) { mkdir( $this->root . $path ); chmod( $this->root . $path, $this->options->directoryMode ); // This automatically creates the property storage $storage = $this->getPropertyStoragePath( $path . '/foo' ); } /** * Creates a new resource. * * Creates a new resource at the given $path, optionally with the given * content. If $content is empty, an empty resource will be created. * * @param string $path * @param string $content * @return void */ protected function createResource( $path, $content = null ) { file_put_contents( $this->root . $path, $content ); chmod( $this->root . $path, $this->options->fileMode ); // This automatically creates the property storage if missing $storage = $this->getPropertyStoragePath( $path ); } /** * Sets the contents of a resource. * * This method replaces the content of the resource identified by $path * with the submitted $content. * * @param string $path * @param string $content * @return void */ protected function setResourceContents( $path, $content ) { file_put_contents( $this->root . $path, $content ); chmod( $this->root . $path, $this->options->fileMode ); } /** * Returns the contents of a resource. * * This method returns the content of the resource identified by $path as a * string. * * @param string $path * @return string */ protected function getResourceContents( $path ) { return file_get_contents( $this->root . $path ); } /** * Returns the storage path for a property. * * Returns the file systems path where properties are stored for the * resource identified by $path. This depends on the name of the resource. * * @param string $path * @return string */ protected function getPropertyStoragePath( $path ) { // Get storage path for properties depending on the type of the // resource. $storagePath = realpath( $this->root . dirname( $path ) ) . '/' . $this->options->propertyStoragePath . '/' . basename( $path ) . '.xml'; // Create property storage if it does not exist yet if ( !is_dir( dirname( $storagePath ) ) ) { mkdir( dirname( $storagePath ), $this->options->directoryMode ); } // Append name of namespace to property storage path return $storagePath; } /** * Returns the property storage for a resource. * * Returns the {@link ezcWebdavPropertyStorage} instance containing the * properties for the resource identified by $path. * * @param string $path * @return ezcWebdavBasicPropertyStorage */ protected function getPropertyStorage( $path ) { $storagePath = $this->getPropertyStoragePath( $path ); // If no properties has been stored yet, just return an empty property // storage. if ( !is_file( $storagePath ) ) { return new ezcWebdavBasicPropertyStorage(); } // Create handler structure to read properties $handler = new ezcWebdavPropertyHandler( $xml = new ezcWebdavXmlTool() ); $storage = new ezcWebdavBasicPropertyStorage(); // Read document try { $doc = $xml->createDom( file_get_contents( $storagePath ) ); } catch ( ezcWebdavInvalidXmlException $e ) { throw new ezcWebdavFileBackendBrokenStorageException( "Could not open XML as DOMDocument: '{$storage}'." ); } // Get property node from document $properties = $doc->getElementsByTagname( 'properties' )->item( 0 )->childNodes; // Extract and return properties $handler->extractProperties( $properties, $storage ); return $storage; } /** * Stores properties for a resource. * * Creates a new property storage file and stores the properties given for * the resource identified by $path. This depends on the affected resource * and the actual properties in the property storage. * * @param string $path * @param ezcWebdavBasicPropertyStorage $storage * @return void */ protected function storeProperties( $path, ezcWebdavBasicPropertyStorage $storage ) { $storagePath = $this->getPropertyStoragePath( $path ); // Create handler structure to read properties $handler = new ezcWebdavPropertyHandler( $xml = new ezcWebdavXmlTool() ); // Create new dom document with property storage for one namespace $doc = new DOMDocument( '1.0' ); $properties = $doc->createElement( 'properties' ); $doc->appendChild( $properties ); // Store and store properties $handler->serializeProperties( $storage, $properties ); return $doc->save( $storagePath ); } /** * 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 */ public function setProperty( $path, ezcWebdavProperty $property ) { // Check if property is a self handled live property and return an // error in this case. if ( ( $property->namespace === 'DAV:' ) && in_array( $property->name, $this->handledLiveProperties, true ) && ( $property->name !== 'getcontenttype' ) && ( $property->name !== 'lockdiscovery' ) ) { return false; } // Get namespace property storage $storage = $this->getPropertyStorage( $path ); // Attach property to store $storage->attach( $property ); // Store document back $this->storeProperties( $path, $storage ); return true; } /** * 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 */ public function removeProperty( $path, ezcWebdavProperty $property ) { // Live properties may not be removed. if ( $property instanceof ezcWebdavLiveProperty ) { return false; } // Get namespace property storage $storage = $this->getPropertyStorage( $path ); // Attach property to store $storage->detach( $property->name, $property->namespace ); // Store document back $this->storeProperties( $path, $storage ); return true; } /** * 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 $storage * @return bool */ public function resetProperties( $path, ezcWebdavPropertyStorage $storage ) { $this->storeProperties( $path, $storage ); } /** * 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 */ public function getProperty( $path, $propertyName, $namespace = 'DAV:' ) { $storage = $this->getPropertyStorage( $path ); // Handle dead propreties if ( $namespace !== 'DAV:' ) { $properties = $storage->getAllProperties(); return $properties[$namespace][$name]; } // Handle live properties switch ( $propertyName ) { case 'getcontentlength': $property = new ezcWebdavGetContentLengthProperty(); $property->length = $this->getContentLength( $path ); return $property; case 'getlastmodified': $property = new ezcWebdavGetLastModifiedProperty(); $property->date = new ezcWebdavDateTime( '@' . filemtime( $this->root . $path ) ); return $property; case 'creationdate': $property = new ezcWebdavCreationDateProperty(); $property->date = new ezcWebdavDateTime( '@' . filectime( $this->root . $path ) ); return $property; case 'displayname': $property = new ezcWebdavDisplayNameProperty(); $property->displayName = basename( $path ); return $property; case 'getcontenttype': $property = new ezcWebdavGetContentTypeProperty( $this->getMimeType( $path ) ); return $property; case 'getetag': $property = new ezcWebdavGetEtagProperty(); $property->etag = $this->getETag( $path ); return $property; case 'resourcetype': $property = new ezcWebdavResourceTypeProperty(); $property->type = $this->isCollection( $path ) ? ezcWebdavResourceTypeProperty::TYPE_COLLECTION : ezcWebdavResourceTypeProperty::TYPE_RESOURCE; return $property; case 'supportedlock': $property = new ezcWebdavSupportedLockProperty(); return $property; case 'lockdiscovery': $property = new ezcWebdavLockDiscoveryProperty(); return $property; default: // Handle all other live properties like dead properties $properties = $storage->getAllProperties(); return $properties[$namespace][$name]; } } /** * Returns the content length. * * Returns the content length (filesize) of the resource identified by * $path. * * @param string $path * @return string The content length. */ private function getContentLength( $path ) { $length = ezcWebdavGetContentLengthProperty::COLLECTION; if ( !$this->isCollection( $path ) ) { $length = (string) filesize( $this->root . $path ); } return $length; } /** * 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: * *