Zeta Components - high quality PHP components

eZ Components - Webdav

Introduction

The Webdav component enables you to easily set up a WebDAV-enabled HTTP server. Users can then create and edit site content by uploading and downloading files to and from their desktop. The current implementation is compatible with RFC 2518 and realizes most parts of the changes defined in RFC 4918. It also supports clients that do not conform to the standard and provides an interface to support these clients.

The component is intended to support you by providing access to your data through the HTTP 1.1 protocol. The data can be stored in the file system, or any other custom data storage. It is then served as a virtual directory tree to the user.

Terms

There are some terms used in a WebDAV environment whose meanings differ slightly from the usage in similar environments.

Collection
When it comes to WebDAV, a collection means a set of files and other collections, which may be compared with directories in a normal file system.
Resource
A resource equals a file, but we use a different term here to differentiate between real files on the hard disk and the virtual resources (files) in a WebDAV share.
Properties
There are several default properties, like the modification time or file size of WebDAV resources, but you can also store and modify custom properties on all resources.

Setting up a WebDAV server

To set up a basic WebDAV server, you must consider two steps:

  1. You need to configure the WebDAV server to work correctly with the incoming requests from WebDAV clients. This means that you need to set up some rewriting for the request paths (which the client sends) to the paths that are used in the back-end.

  2. You need to set up the back-end, so that it points to the resources you want to share through WebDAV.

Path auto detection

Using the default path factory, which tries to auto detect your setup and map the paths accordingly, you need very little code to setup a WebDAV server.

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. $server ezcWebdavServer::getInstance();
  4. $backend = new ezcWebdavFileBackend(
  5.    dirname__FILE__ ) . '/backend'
  6. );
  7. $server->handle$backend ); 
  8. ?>

As you can see in the example, we first create a new WebDAV server instance. Then a file back-end is created, which just receives the directory as a parameter, where your contents are stored.

Finally we call the method handle() on ezcWebdavServer, which actually parses and responds to the request with the created back-end as a parameter.

Basic path factory

A custom path factory enables you to specify the request path mapping to the path of a resource in the repository. This can be used if the automatic detection does not work.

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. $server ezcWebdavServer::getInstance();
  4. $pathFactory = new ezcWebdavBasicPathFactory(
  5.     'http://example.com/webdav/index.php'
  6. );
  7. foreach ( $server->configurations as $conf )
  8. {
  9.     $conf->pathFactory $pathFactory;
  10. }
  11. $backend = new ezcWebdavFileBackend(
  12.    dirname__FILE__ ) . '/backend'
  13. );
  14. $server->handle$backend ); 
  15. ?>

When assigning the server configuration to the new ezcWebdavBasicPathFactory object, you provide the base URL, which will always be removed from the request URLs.

If you need more specialized mapping of request paths to repository paths, you can write your own path factory, by implementing the ezcWebdavPathFactory interface, or extending one of the existing path factories.

Testing the server

You can test the server directly with a WebDAV client of your choice. However, most WebDAV clients have very poor debugging capabilities.

The WebDAV client with the most verbose error reporting currently is the command line WebDAV client cadaver, where you might get more information than failed request notifications.

The second step you should take is to enable error logging, either by catching all exceptions from WebDAV and logging them to a file, or by simply enabling log_errors in php.ini.

You can also access the WebDAV server with a browser, since WebDAV is just an extension to the HTTP protocol. You should be able to get valid results out of this, and also see possible errors. Remember that collections (or directories), although they can contain other collections and resources, do not consist of any data themselves. Therefore, if everything is working properly, you will get a blank page when viewing collections in your browser. However, you should still be able to download resources (or files) in the WebDAV share.

Authentication and authorization

Since version 1.1, the Webdav component allows you to integrate your own authentication and authorization mechanisms into the server. To achieve this, you need to create a class which implements one of the following interfaces:

Each of the class extends the one listed before it. The anonymous authenticator provides a method to authenticate the anonymous user (who did not provide a user name or password at all). The second authorizer provides HTTP Basic authentication to clients. This might not be supported by some clients and is generally considered less secure than Digest. The third interface makes the server provide Basic and Digest authentication. To ease the implementation of Digest authentication, you can extend the abstract base class ezcWebdavDigestAuthenticatorBase.

While these interfaces only authenticate a user for general use of WebDAV, they do not provide authorization support. That means, every authenticated user has full write access to all resources. If you also want to have fine grained authorization support, the class must in addition implement ezcWebdavAuthorizer.

The following source shows an example implementation for Digest authentication with authorization support:

  1. <?php
  2. class myCustomAuth extends ezcWebdavDigestAuthenticatorBase
  3.                    implements ezcWebdavAuthorizer
  4. {
  5.     protected $credentials = array(
  6.         'some' => 'thing',
  7.     );
  8.     public function authenticateAnonymousezcWebdavAnonymousAuth $data )
  9.     {
  10.         return false;
  11.     }
  12.     public function authenticateBasicezcWebdavBasicAuth $data )
  13.     {
  14.         $username $data->username;
  15.         $password $data->password;
  16.         if ( !isset( $this->credentials[$username] ) )
  17.         {
  18.             return false;
  19.         }
  20.         return ( $this->credentials[$username] === $password );
  21.     }
  22.     public function authenticateDigestezcWebdavDigestAuth $data )
  23.     {
  24.         $username $data->username;
  25.         if ( !isset( $this->credentials[$username] ) )
  26.         {
  27.             return false;
  28.         }
  29.         return ( $this->checkDigest$data$this->credentials[$username] ) );
  30.     }
  31.     public function authorize$user$path$access ezcWebdavAuthorizer::ACCESS_READ )
  32.     {
  33.         if ( $access === ezcWebdavAuthorizer::ACCESS_READ )
  34.         {
  35.             return true;
  36.         }
  37.         if ( $user === 'some' && substr$path0) === '/some' )
  38.         {
  39.             return true;
  40.         }
  41.         return false;
  42.     }
  43. }
  44. ?>

The method authenticateAnonymous() is called, if the user did not provide any credentials or the server was not able to parse the provided authentication header. You need to decide here, if you want to grant anonymous access to your WebDAV server, which is generally considered a bad idea (except if you restrict authorization properly).

authenticateBasic() is called by the server, when Basic authentication is provided by the client.

If the client provides Digest authentication, the method authenticateDigest() is called instead. The given ezcWebdavDigestAuth contains all necessary information to calculate the digest. This task is performed by the method checkDigest() of the base class. It is recommended, that you implement the digest calculation yourself, if you have the capability to e.g. do it right in the database. This avoids the risk of copying the plain text password data into PHPs memory scope.

The auhtorize() method is called by the back-end, for each path that is affected by a request. The back-end provides the $username of the user who wishes to gain access and the type of access, which might be one of:

For recursive operations the back-end will call the authorize() method for each sub-sequent path, too, so you don't need to worry about this yourself. In addition it might happen that the back-end (or a plugin) calls the method for a certain path multiple times during a request. It is therefore recommended that you cache the data internally in your authorization object to avoid multiple accesses to your permission storage.

To activate authentication and authorization in the server you need to instantiate your class and assign the created object to the servers $auth property, like this:

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. require_once 'custom_auth.php';
  4. $server ezcWebdavServer::getInstance();
  5. $server->auth = new myCustomAuthClass();
  6. $backend = new ezcWebdavFileBackend(
  7.    dirname__FILE__ ) . '/backend'
  8. );
  9. $server->handle$backend ); 
  10. ?>

Locking

Since version 1.1 the Webdav component has support for locking (WebDAV compliance class 2). It is realized through a plugin, which means that you can integrate it into your own WebDAV environment easily, without changing your setup a lot.

Pre-conditions

To make the lock plugin work in your environment, you need to fulfill the following pre-conditions:

The ezcWebdavLockBackend interface requires your back-end to implement 2 new methods, that allow the lock plugin to gain an exclusive lock on the back-end. This is necessary to ensure that the lock plugins operations are atomic. Note that you don't need to support the LOCK and UNLOCK requests in your back-end. Both are completely handled by the lock plugin, which communicates with the back-end only by standard requests. For more information see Writing a custom back-end. The file back-end shipped with the Webdav component supports all requirements.

The ezcWebdavLockAuthorizer interface defines methods to let your authorization mechanism know about the assignment between users and locks. You need to store string lock tokens per user.

Lock authorization

To setup locking, you first need an authorization class, which implements the ezcWebdavLockAuthorizer interface. In this example, the class presented in Authentication and authorization is extended for that purpose.

  1. <?php
  2. require_once 'custom_auth.php';
  3. class myCustomLockAuth extends myCustomAuth
  4.                    implements ezcWebdavLockAuthorizer
  5. {
  6.     protected $tokens;
  7.     protected $storageFile;
  8.     public function __construct$storageFile )
  9.     {
  10.         $this->storageFile $storageFile;
  11.         $this->tokens      = array();
  12.         if ( file_exists$storageFile ) )
  13.         {
  14.             $this->tokens = include $storageFile;
  15.         }
  16.     }
  17.     public function __destruct()
  18.     {
  19.         if ( $this->tokens !== array() )
  20.         {
  21.             file_put_contents(
  22.                 $this->storageFile,
  23.                 "<?php\n\nreturn " var_export$this->tokenstrue ) . ";\n\n?>"
  24.             );
  25.         }
  26.     }
  27.     public function assignLock$user$lockToken )
  28.     {
  29.         if ( !isset( $this->tokens[$user] ) )
  30.         {
  31.             $this->tokens[$user] = array();
  32.         }
  33.         $this->tokens[$user][$lockToken] = true;
  34.     }
  35.     public function ownsLock$user$lockToken )
  36.     {
  37.         return ( isset( $this->tokens[$user][$lockToken] ) );
  38.     }
  39.     public function releaseLock$user$lockToken )
  40.     {
  41.         unset( $this->tokens[$user][$lockToken] );
  42.     }
  43. }
  44. ?>

The assignment between users and locks must persist between requests. For this reason, the example uses a simple PHP file that stores an array of lock assignments. The structure of this array looks a follows:

  1. <?php
  2.     array(
  3.         '<user_name_a>' => array(
  4.             '<lock_token_1>' => true,
  5.             '<lock_token_2>' => true,
  6.             // ...
  7.         ),
  8.         // ...
  9.     );
  10. ?>

A lock token is a string with length 52. You will most probably store this information in a database, depending on your authorization mechanism.

In the constructor, the contents of the token $storageFile (containing the array shown above, if it exists) is received. It is stored again in the destructor, to persist lock assignments.

The method ezcWebdavLockAuthorizer->assignLock() is called by the lock plugin to indicate that the user with name $user has achieved a new lock with the lock token $lockToken. You need to store this assignment, while making sure that a user can have an arbitrary number of lock tokens assigned.

Using ezcWebdavLockAuthorizer->ownsLock(), the lock plugin asks if a certain $user owns the given $lockToken. This is the authorization scheme itself. You need to make sure that this method only returns true if the given $lockToken has been assigned to the $user before, using ezcWebdavLockAuthorizer->assignLock().

In case the user performs an UNLOCK request, the plugin will call ezcWebdavLockAuthorizer->releaseLock(). You then need to remove the assignment between $user and $lockToken from your storage.

Setup

The following example shows a typical setup for a Webdav server with locking:

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. require_once 'custom_lock_auth.php';
  4. $server ezcWebdavServer::getInstance();
  5. $server->auth = new myCustomLockAuth(
  6.     // Some configuration directory here
  7.     dirname__FILE__ ) . '/tokens.php'
  8. );
  9. $server->pluginRegistry->registerPlugin(
  10.     new ezcWebdavLockPluginConfiguration()
  11. );
  12. $backend = new ezcWebdavFileBackend(
  13.     // Your WebDAV directory here
  14.     dirname__FILE__ ) . '/backend'
  15. );
  16. $server->handle$backend );
  17. ?>

An instance of your custom authentication/authorization class is assigned to the server. You will need to adjust the location of the token assignment file, if you want to get this example running. It must be writable by the user running your web server and should be located outside of your web root.

Lines 14-16 show how the lock plugin is activated. The ezcWebdavLockPluginConfiguration class receives optionally an instance of ezcWebdavLockPluginOptions, which you can use to adjust some settings.

For the back-end initialization you will need to adjust the path, too. The same rules as for the token.php file apply here:

To test your setup properly, you should first access the URL using a web browser. You should now see any exceptions, errors and warnings that might be generated. If you see a white page, everything seems correct. Now try with a WebDAV client.

Purging locks

Whenever a clients successfully acquires a lock, a timeout value is assigned to it. If the client does not access the lock for that number of seconds, it might be considered orphan and can safely be removed. To achieve this removal of locks, the Webdav component provides an API for you, that you might use in a CRON job or a similar mechanism, to purge all outdated locks.

  1. <?php
  2. require_once 'tutorial_autoload.php';
  3. require_once 'custom_lock_auth.php';
  4. $server ezcWebdavServer::getInstance();
  5. $server->pluginRegistry->registerPlugin(
  6.     new ezcWebdavLockPluginConfiguration()
  7. );
  8. $backend = new ezcWebdavFileBackend(
  9.     // Your WebDAV directory here
  10.     dirname__FILE__ ) . '/backend'
  11. );
  12. $administrator = new ezcWebdavLockAdministrator(
  13.     $backend
  14. );
  15. $administrator->purgeLocks();
  16. ?>

The setup for the lock administration should be identical to your web setup. Except, you might leave out the authentication instance, since it will be deactivated by the administrator object anyway. However, it does not hurt to have it in place.

The ezcWebdavLockAdministrator object is instantiated given the back-end it should work on. The call to ezcWebdavLockAdministrator->purgeLocks() optionally accepts a string parameter to indicate which paths should be searched for orphan locks. Omitting this parameter, as shown in the example, searches the complete back-end.

Note that the process of purging locks will lock your back-end completely, as the lock plugin does it during any request that needs to perform multiple requests.

Writing a custom back-end

The most common way of extending WebDAV is to provide a custom back-end to your data. A back-end receives ezcWebdavRequest objects and generates ezcWebdavResponse objects, which are displayed in a way that the current client will understand.

There are basically two ways for you to implement a custom back-end. You can implement all the request object handling yourself, by extending ezcWebdavBackend directly, or you can reuse the existing helper class ezcWebdavSimpleBackend.

The simple back-end

The simple back-end, defined in the ezcWebdavSimpleBackend class, already implements all request-to-response mapping, so you only need to implement several methods that directly access the data in your back-end (like the file back-end does).

If you need more fine-grained control, or optimizations, you will still need to extend the basic ezcWebdavBackend class directly. If you want to implement a custom back-end you could use the file back-end or the memory back-end (which as mainly intended for testing) as an implementation guide.

If you do not extend ezcWebdavSimpleBackend, be sure to implement authorization and to pay attention to the If-Match and If-None-Match headers. Both are already handled by ezcWebdavSimpleBackend, so you don't need to take care about that if you extend this class.

Backends for locking

If you want to use your custom back-end with the lock plugin, it needs to implement some more interfaces. First of all, you need to implement all of the basic back-end interfaces:

All of these are already implemented by ezcWebdavSimpleBackend. It is recommended that you use this one as the basis for your custom lock back-end, since it already handles authorization for you and processes the HTTP headers If-Match and If-None-Match. You will need to handle both on your own, if you do not extend ezcWebdavSimpleBackend.

No matter which way you choose, you need to implement ezcWebdavLockBackend. This interface requires you to implement 2 methods:

public function lock( $waitTime, $timeout );
This method is used by the lock plugin, whenever it needs to communicate with the back-end to process a request. In this case, the lock plugin will lock the back-end as soon as it receives a request it must react on. It will the process all necessary operations, make the back-end process them and unlock the back-end, when it created a response. The $waitTime parameter defines how long the back-end should wait between attempts to achieve the lock. The $timeout parameter defines after what time of trying to acquire a lock the back-end must release a pending lock and restart trying. These parameters are defined through ezcWebdavLockPluginOptions.
public function unlock();
The unlock() method is called when the lock plugin has finished all its operations. In case a fatal error occurs in the Webdav component or your back-end, a lock might not be released. For this case, the lock() method must remove a pending lock after $timeout.

The ezcWebdavFileBackend, which is shipped with the Webdav component, supports all of the features mentioned above.