<commit_before>eZ component: Webdav, Design, 1.1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Author: <NAME>
:Revision: $Rev$
:Date: $Date$
:Status: Draft

.. contents::

Scope
^^^^^

The scope of this document is to describe the enhancements for the Webdav
component version 1.2.

The general goal for this version is to support locking as described in RFC
2518. To achieve this, the if-header must be parsed and respected by the Webdav
component. This currently is not the case. The if-header must also be used to
respect entity tags, which is not an integral part of the locking feature.
However, the support of entity tags is part of this design, too. 

The following issues are covered in this design:

- Webdav does not provide ETags and does not honor the if-header with them
  (#12583).
- Lock support (#12286).

For locking, a plugin based approach has been favored while desiging and
creating the Webdav component version 1.0. However, it turned out that the
realization of lock support through a plugin is not as easy as it seemed to be.
Therefore this document describes 2 different design approaches for lock
related functionality: A plugin based one and an integrated one.

The document is devided into 3 parts. The first part deals with the realization
of functionality that is execlusively meant to support entity tags. The second
part describes features that are exclusively used to support locking features.
Since WebDAV mixes entity tags (originally HTTP/1.1) and locking in some
places, the third section describes the design for these common concerns.

Entity tag support
^^^^^^^^^^^^^^^^^^

This section describes the support of entity tags in the Webdav component, including
the usage of the if-header with entity tags and the generation and sending of the
ETag header.

The following design only refers to the support of entity tags as defined in
the HTTP/1.1 RFC. This includes the headers mentioned in the above RFC overview
and the generation and comparison of entity tags.

Entity tag generation
=====================

The generation of an entity tag requires uniqueness for the state of a resource
(the so-called entity), To achieve this, multiple data of the resource and be
involved and combined.

The last modification time in combination with the file size should be a
sufficient combination of data to generate an entity tag from. The Lighttpd
web server uses this information on a configurable basis, too. In addition it
can use the inode of the file for entity tag generation. Since inodes are
operating system dependent and only available for file system based back ends,
they will not be used in our entity tag generation scheme.

Since entity tags in WebDAV are also available through the getetag
live-property, a common way is needed to generate the entity tags for the
headers and the property. The ezcWebdavSimpleBackend will therefore request the
getetag property from the extending back end and use it's value for the headers
and validation.

The current generation of entity tags in ezcWebdavFileBackend will be replaced
by a mechanism that uses the last modification time and the size of the file.
It currently uses the path, which is not reliable and does not belong into the
entity tag. Entity tags are only valid for a single resource and only in
combination with its URI.

Header handling
===============

The handling of entity tag related headers must take place in several different
architecture levels of the Webdav component.

Transport layer
---------------

The transport layer needs to be able to parse the request headers and to
serialize the response headers back. Therefore the ezcWebdavHeaderHandler class
will be adjusted.

The class must parse the following new request headers:

- If-Match
- If-None-Match

ezcWebdavHeaderHandler will check if both of these headers are set. If this is
the case, both headers will be silently discarded, since such a combination is
undefined.

Both headers can contain a list of weak/non-weak entity tags or the "*" value,
to indicate that the resource must just exists, no matter in which state. To
represent this in PHP, the parsed headers will be represented either as an
array of string values or as the boolean value true. The headers will be parsed
into every request object automatically, if they are set.

The back end layer must take responsibility for interpreting the headers and
their values.

In addition, ezcWebdavHeaderHandler must take care of serializing an ETag
header, if this one is present in a response object. Since the ETag header may
only contain the string value of an entity tag, this mechanism is already
implemented in the response processing.

Back end layer
--------------

The interpretation of incoming If-Match and If-None-Match headers must be done
in the back end. The implementation will take place in the
ezcWebdavSimpleBackend class. With every incoming request, no matter which
request method is used, the If-Match and If-None-Match headers will be honored
in the following way:

If the back end determines that an If-Match header is set, it will:

- Check if all preconditions (except the If-Match) are fulfilled.

  - If not, return a corresponding error response.

- Check if the If-Match value is a boolean true.
  
  - Check if the requested path exists.
  
  - If it *does not*, cancel the method and return 412 (Precondition Failed).

- Check if the If-Match value is an array of strings.
  
  - Retrieve the affected resources getetag property and compare it with all
    strings in the array.
  
  - If *none* matches, cancel the method and return 412 (Precondition Failed).

- Process the method as if the If-Match header was not set.

If the back end determines that an If-None-Match header is set, it will:

- Check if all preconditions (except the If-Match) are fulfilled.
  
  - If not, return a corresponding error response.

- Check if the If-Match value is a boolean true.
  
  - Check if the requested path exists.
    
    - If it *does*, cancel the method and return 412 (Precondition Failed).

- Check if the If-Match value is an array of strings.
  
  - Retrieve the affected resources getetag property and compare it with all
    strings in the array.
    
    - If *any* matches, cancel the method and return 412 (Precondition Failed).

- Process the method as if the If-None-Match header was not set.

Since this behavior will be implemented within the ezcWebdavSimpleBackend
class, it automatically works with all extending back end classes. Back ends
that do not extend ezcWebdavSimpleBackend will have to take care for these
headers on their own.

Lock support
^^^^^^^^^^^^

Locking allows a WebDAV client to gain exclusive access to a resource
(collection or non-collection) to avoid the "lost update problem". This means
that a client can define to have exclusive access to a resource for a certain
time, where no other client may gain access to this resource.

The WebDAV RFC distinguishes locks by 2 essential properties: The scope of the
lock (shared vs. exclusive) and the type of the lock (write vs. read), while
for the type only a write lock is specified. Using an exclusive lock the client
ensures that he has absolutely exclusive access to the locked resource. With a
shared lock it is possible the multiple clients take part in one and the same
lock. More information on lock scopes are provided later.

============
Plugin based
============

This section describes a potential realization of locking through a plugin for
the Webdav component. This plugin will make use of the, not yet officially
released, plugin API.

Design goals
============

This design drafts goal is to build a lock plugin that is almost completely
independent from the used back end. In this sense, the requirements to be
fulfilled by a back end must be cut down as far as possible. Beside that, the
plugin should provide the largest possible compatibility to clients. However a
plugin that hooks into the parsing process cannot provide the same client
compatibility mechanisms as the base Webdav transport layer.

Hooks
=====

The plugin needs to assign to several hooks offered by the plugin API, which
will be described roughly in this section.

parseUnknownRequest (ezcWebdavTransport)
----------------------------------------

The requests LOCK and UNLOCK are dedicated to the locking mechanism. Therefore
the plugin needs to hook into parseUnknownRequest and parse the incoming
requests into (new) corresponding request objects.



===========
Open issues
===========

Authentication
==============

The problem
-----------

The WebDAV RFC specifies that lock tokens are not secure for authentication
since the can be obtained by every client with access to the repository through
the lockdiscovery property. Therefore, a server must provide its own
authentication scheme. We currently do not support any authentication mechanism
in the Webdav component. To realize locking, we need to support at least 1
authentication model in the component.

Solution attempt
----------------

The only possible authentication scheme provided through the HTTP protocol is
HTTP-Auth. Therefore the HTTP-Auth information should be integrated into every
request object, if provided. To check this information, an interface should be
provided that provides the following capabilities:

- Information of the assignment of a new lock token to a specific HTTP-Auth
  user.
- Removal of a lock token.
- Answer to the question if a certain user may perform an action with a given
  lock token.

In addition we should provide a basic implementation of this class, which does
actually not do anything but discard the given information and return true on
authentication question. This will not provide any security, but allow users to
plug their own authentication mechanisms into the lock facilities.

Further thoughts
----------------

In future it might be possibly that a WebdavAuthenticationTiein is implemented,
which might then be plugged into the described mechanism, too. This could in
addition be used for a "plugin" that performs authentication (and possibly
authorization?) in general on the transport/server level.

Lock null resources
===================

The problem
------------

For our special case this means, that we need to "physically" create
lock-null resources in the utilized back end, to make it persist during
requests, whenever a null-lock is acquired. This lock-null resource must
not appear as a real resource to the client (e.g. must not have a
correct content type and must not respond to GET requests). If the
resource is unlocked again, it must be removed from the back end again.

In our internal handling, that means, that we need to intervene any of
the operations not supported by a lock-null resource (e.g. COPY, MOVE,
PROPPATCH) requests to check is a lock-null resource is affected and
remove it potentially or forbid the operation. Furthermore, we need to
intervene any operation that is supported by a lock-null resource to
remove potential information from responses that must not be available
for lock-null resources. This will mean a lot of internally generated
requests to the back end and a lot of processing of the back end generated
responses.

For example, if a MOVE request is issued by a client, we need to check
the whole affected directory tree for lock-null resources and remove
those from the destination and send a special error code for them in the
response.

Atomicity of operations also comes into play here, since our internal
requests to the back end might interfere with other external requests. For
example, a null-lock might be acquired by the client while we are
internally checking the correctness of a MOVE request (race-condition).

Solution attempts
-----------------

I currently do not see a full solution for this problem, still there are
some ideas in my mind, that might be helpful. For lock-null resources we
can invent a special dead-property name space to indicate such resources.
If a special (for lock-null resources forbidden) operation occurs, we
can easily check for them using a PROPFIND request. Creating and
maintaining lock-null resources should then be not problem.

For the race-condition issue, we'll definitely need support in the
back end, because I see no other solution attempt here to ensure the
consistency.

Common concerns
^^^^^^^^^^^^^^^

This section describes the design to realize support for the If header, which
is used in WebDAV with combinations of entity tags and lock tokens.

============
Plugin based
============

This section tries to design the honoring of the If header in the Webdav
component using a plugin based approach for lock handling. This works
corresponding to the pluin approach described in `Plugin design draft`_.

It is not possibly to exctract all parts of locking support into the lock
plugin, since the If header must be parsed in one go. Since this can also
contain entity tags, which do belong into the main component, it cannot be
parsed exclusively in the lock plugin.

Transport layer
===============

The transport layer needs to parse the If header. Since the header can contain
entity tags and lock tokens, the parsing needs to take place inside
ezcWebdavHeaderHandler itself and cannot be part of the plugin. The If header
is the most complex header that needed to be parsed so far. All other parsed
headers either contained a string value or a simply to process other scalar
value. To encapsulate the If header correctly some new classes need to be
invented which are described in following:

::

    abstract class ezcWebdavIfHeaderList implements ArrayAccess
    {
        protected ezcWebdavIfHeaderListItems[] $items;
    }

    class ezcWebdavIfHeaderTaggedList extends ezcWebdavIfHeaderList
    {

    }

    class ezcWebdavIfHeaderNoTagList extends ezcWebdavIfHeaderList
    {

    }

These classes represent the lists provided in an If header. Both are accessed
through the ArrayAccess interface. The keys used to query the object are
resource pathes (not URIs!).

An ezcWebdavIfHeaderList object will return an array of
ezcWebdavIfHeaderListItem objects on read access through ArrayAccess. This
array represents the OR concatenation of the items. The item class realizes the
OR combination and is described further below.

The ezcWebdavIfHeaderTaggedList will return the list items defined for the resource
path given via ArrayAccess and only those for the given path (an empty array of
none were defined for this path. In contrast to that,
ezcWebdavIfHeaderNoTagList will return all list items for *every* resource
path, since the lists applies to all resources.

This way of abstracting tagged lists and no-tag lists allows a unified usage of
both classes in deeper layers of the Webdav component (plugin or backend).

::

    class ezcWebdavIfHeaderListItem
    {
        public function __construct(
            array $lockTokens = array(),
            array $eTags      = array(),
            bool $negated     = false
        );

        property-read array $lockTokens;
        property-read array $eTags;
        property-read bool  $negated;
    }

An instance of this class represents a list item, which can combines several
entity tags ($eTag) and lock tokens ($lockTokens). In addition it can be
defined to be negated ($negated === true). The combination represented by such
an object is a logical AND combination. 

Code examples
-------------

Some small code examples to illustrate the above class design will be showen
here.

::

   COPY /resource1 HTTP/1.1
   Host: www.foo.bar
   Destination: http://www.foo.bar/resource2
   If: <http://www.foo.bar/resource1> (<locktoken:a-write-lock-token>
   [W/"A weak ETag"]) (["strong ETag"])
   <http://www.bar.bar/random>(["another strong ETag"])

This example shows a tagged list in the If header, which will be parsed into an
instance of ezcWebdavIfHeaderTaggedList will be created from it in the
Transport layer. The access to this object in the backend or the lock plugin
will look as follows: ::

    $res1items = $ifHeader['/resource1'];

    $res2items = $ifHeader['/resource2'];

    $randomItems = $ifHeader['/random'];

The $res1items variable will contain an array reflecting the conditions
specified for http://www.foo.bar/resource1. The $res2items variable will
contain an empty array since no conditions were defined for this resource.
While the $randomItems variable should normally not be requested (since the
resource is not affected) it would contain the corresponding list items for the
http://www.bar.bar/random resource.

In contrast, the following reqeuest would return an instance of
ezcWebdavIfHeaderNoTagList for the contained If header: ::

   COPY /resource1 HTTP/1.1
   Host: www.foo.bar
   Destination: http://www.foo.bar/resource2
   If: (<locktoken:a-write-lock-token> [W/"A weak ETag"]) (["strong ETag"])
   (["another strong ETag"])

Taking the same accesses to the corresponding $ifHeader variable as shown above
will result in all 3 variables containing the same values: All 3 list items
will be contained, since the If header does not use tagging to specify which
resources are affected by the conditions.

Plugin layer
============

If the lock plugin is active, it needs to hook into every affected request (see
`Affected base methods`_) and check the If header conditions. The check does
not only affect the checking of locktoken conditions, but also the check of
etag validation, because both condition types are combineable.

The procedure (in pseudo code) is as follows: ::

    lock backend;
    foreach ( <range of affected resources> as <basse resource> )
    {
        create propfind request for <base resource>;
        set request properties to <getetag> and <lockdiscovery>;
        set depth according to incoming request;
        send request to backend;
        foreach ( <resources in response> as <resource> )
        {
            if ( <resource> does not conform to If header )
            {
                return <precondition failed response>;
            }
        }
    }
    discard If header;
    unlock backend;

The If header needs to be discarded after correct validation of all entity
tag/lock token conditions. This avoids that the backend checks for the entity
tag conditions a second time.

.. Note::
   The checking of both (lock token and entity tag) conditions is necessary in<commit_msg>- Added information about handling of directly lock affected methods.<commit_after>eZ component: Webdav, Design, 1.1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Author: <NAME>
:Revision: $Rev$
:Date: $Date$
:Status: Draft

.. contents::

Scope
^^^^^

The scope of this document is to describe the enhancements for the Webdav
component version 1.2.

The general goal for this version is to support locking as described in RFC
2518. To achieve this, the if-header must be parsed and respected by the Webdav
component. This currently is not the case. The if-header must also be used to
respect entity tags, which is not an integral part of the locking feature.
However, the support of entity tags is part of this design, too. 

The following issues are covered in this design:

- Webdav does not provide ETags and does not honor the if-header with them
  (#12583).
- Lock support (#12286).

For locking, a plugin based approach has been favored while desiging and
creating the Webdav component version 1.0. However, it turned out that the
realization of lock support through a plugin is not as easy as it seemed to be.
Therefore this document describes 2 different design approaches for lock
related functionality: A plugin based one and an integrated one.

The document is devided into 3 parts. The first part deals with the realization
of functionality that is execlusively meant to support entity tags. The second
part describes features that are exclusively used to support locking features.
Since WebDAV mixes entity tags (originally HTTP/1.1) and locking in some
places, the third section describes the design for these common concerns.

Entity tag support
^^^^^^^^^^^^^^^^^^

This section describes the support of entity tags in the Webdav component, including
the usage of the if-header with entity tags and the generation and sending of the
ETag header.

The following design only refers to the support of entity tags as defined in
the HTTP/1.1 RFC. This includes the headers mentioned in the above RFC overview
and the generation and comparison of entity tags.

Entity tag generation
=====================

The generation of an entity tag requires uniqueness for the state of a resource
(the so-called entity), To achieve this, multiple data of the resource and be
involved and combined.

The last modification time in combination with the file size should be a
sufficient combination of data to generate an entity tag from. The Lighttpd
web server uses this information on a configurable basis, too. In addition it
can use the inode of the file for entity tag generation. Since inodes are
operating system dependent and only available for file system based back ends,
they will not be used in our entity tag generation scheme.

Since entity tags in WebDAV are also available through the getetag
live-property, a common way is needed to generate the entity tags for the
headers and the property. The ezcWebdavSimpleBackend will therefore request the
getetag property from the extending back end and use it's value for the headers
and validation.

The current generation of entity tags in ezcWebdavFileBackend will be replaced
by a mechanism that uses the last modification time and the size of the file.
It currently uses the path, which is not reliable and does not belong into the
entity tag. Entity tags are only valid for a single resource and only in
combination with its URI.

Header handling
===============

The handling of entity tag related headers must take place in several different
architecture levels of the Webdav component.

Transport layer
---------------

The transport layer needs to be able to parse the request headers and to
serialize the response headers back. Therefore the ezcWebdavHeaderHandler class
will be adjusted.

The class must parse the following new request headers:

- If-Match
- If-None-Match

ezcWebdavHeaderHandler will check if both of these headers are set. If this is
the case, both headers will be silently discarded, since such a combination is
undefined.

Both headers can contain a list of weak/non-weak entity tags or the "*" value,
to indicate that the resource must just exists, no matter in which state. To
represent this in PHP, the parsed headers will be represented either as an
array of string values or as the boolean value true. The headers will be parsed
into every request object automatically, if they are set.

The back end layer must take responsibility for interpreting the headers and
their values.

In addition, ezcWebdavHeaderHandler must take care of serializing an ETag
header, if this one is present in a response object. Since the ETag header may
only contain the string value of an entity tag, this mechanism is already
implemented in the response processing.

Back end layer
--------------

The interpretation of incoming If-Match and If-None-Match headers must be done
in the back end. The implementation will take place in the
ezcWebdavSimpleBackend class. With every incoming request, no matter which
request method is used, the If-Match and If-None-Match headers will be honored
in the following way:

If the back end determines that an If-* header is set, it will (pseudo
code): ::

    if ( any precondition for the request fails except for the If-* header )
    {
        return <corresponding error response>;
    }
    if ( If-* header condition is not fulfilled )
    {
        return <error response 412  (Precondition Failed)>;
    }
    process <request> as if no If-* header was set;

Since this behavior will be implemented within the ezcWebdavSimpleBackend
class, it automatically works with all extending back end classes. Back ends
that do not extend ezcWebdavSimpleBackend will have to take care for these
headers on their own.

Lock support
^^^^^^^^^^^^

Locking allows a WebDAV client to gain exclusive access to a resource
(collection or non-collection) to avoid the "lost update problem". This means
that a client can define to have exclusive access to a resource for a certain
time, where no other client may gain access to this resource.

The WebDAV RFC distinguishes locks by 2 essential properties: The scope of the
lock (shared vs. exclusive) and the type of the lock (write vs. read), while
for the type only a write lock is specified. Using an exclusive lock the client
ensures that he has absolutely exclusive access to the locked resource. With a
shared lock it is possible the multiple clients take part in one and the same
lock. More information on lock scopes are provided later.

============
Plugin based
============

This section describes a potential realization of locking through a plugin for
the Webdav component. This plugin will make use of the, not yet officially
released, plugin API.

Design goals
============

This design drafts goal is to build a lock plugin that is almost completely
independent from the used back end. In this sense, the requirements to be
fulfilled by a back end must be cut down as far as possible. Beside that, the
plugin should provide the largest possible compatibility to clients. However a
plugin that hooks into the parsing process cannot provide the same client
compatibility mechanisms as the base Webdav transport layer.

Transport layer
===============

On the transport layer, the plugin needs to hook into many existing and 2 new
request methods to alter their behavior. This section only describes the
necessary functionality in the transport layer. Further functionality will be
added on the server layer, see below.

LOCK / UNLOCK
-------------

The LOCK request can not be parsed, yet. The ezcWebdavLockRequest class and
additional content classes already exists (@private) and need to be moved to
the lock plugin directories. To parse the LOCK request, the parseUnknownRequest
hook is used. The generated ezcWebdavLockRequest object is submitted further
on, to be handled on the server layer.

The same applies for the UNLOCK method.

For both requests the corresponding response objects still need to be created
and need to be serialzed by the handleUnknownResponse hook.

Properties
----------

The following live properties need to be added:

- lockdiscovery
- supportedlock

For both the property abstraction classes already exist (@private) and just
need to be moved to the lock plugin diretories. The parsing and serializing of
these properties needs to be implemented in the plugin. The hooks
extractUnkownLiveProperty and serializeUnknownLiveProperty will be used for this
purpose. This ensures that the properties can also be stored in the backend
properly.

.. Note::
   If the lock plugin is switched off later for such a repository, the
   specified properties should still work, since they are then simply
   considered dead properties.

The creation and removal of these properties is handled on the server layer by
the lock plugin.

Server layer
============

On the server layer, the hook receivedRequest will be used to intercept the
necessary requests. These are on the one hand the LOCK and UNLOCK methods,
which are exclusively handled by the lock plugin. On the other hand many other
methods must be intercepted to check for locks taking effect. The following
sections describe all necessary changes except the handling of the If header,
which is described in the main section `Common concerns`_.

LOCK
====

Objects of class ezcWebdavLockRequest must be handled completly by the lock
plugin. In case such a request is received, the following operations must be
performed (pseudo code): ::

    if ( <authentication> invalid )
    {
        // should probably be 401 Unauthorized?
        return <response 403 Forbidden>;
    }

    lock backend;
    
    generate <lock token>;
    
    create propfind request for <request path>;
    set Depth header of <propfind request> according to <lock request>;
    send <propfind request> to backend;
    
    if ( <resource> does not exist )
    {
        create lock-null resource;
    }
    else
    {
        foreach ( <resource> in <returned response> )
        {
            if ( <resource> is locked exclusively )
            {
                return <response 423 Locked>;
            }
            if ( <resource> is locked shared and <request> is exclusive )
            {
                return <response 412 Precondition Failed>;
            }
            add <lock token> to <lockdiscovery> property;
            add <lockdiscoveryproperty> to <properties to patch>;
        }
    }

    create <proppatch request> for <properties to patch>;
    send <proppatch request> to backend;

    
    unlock backend;
    
    return <corresponding response>;

.. Note::
   The code shown above does not include the handling of refreshing LOCK
   requests to reduce complexity. In case of a refresh, the lock token is
   already present in the request (If header) and the timeouts just need to be
   updated.

.. Note::
   The creation of a lock null resource is described in a dedicated section
   below.

The realization of the LOCK request via a plugin inquires several requests that
need to be created by the plugin and to be processed by the backend. In
addition, the plugin needs to process the returned response objects.

UNLOCK
======

The unlock operation contains exactly 1 lock token that is to be removed from
the resource(s) identified by the reqeust path. The following pseudo code
describes the operations to be used in the plugin to realize this: ::

    lock backend;

    create propfind request for <request path>;
    set Depth header to INFINITY;
    send <propfind request> to backend;

    foreach ( <resource> in <returned response> )
    {
        if ( <resource> is lock null resource> )
        {
            create delete request for <resource>;
            send delete request to backend;
        }
        else if ( <lock token> is assigned to <resource> )
        {
            remove <lock token> from <lockdiscovery> property;
            add <lockdiscoveryproperty> to <properties to patch>;
        }
    }

    create <proppatch request> for <properties to patch>;
    send <proppatch request> to backend;
    
    unlock backend;
    
    return <corresponding response>;

If the lock token could not be found in any of the resources returned by the
PROPFIND request, it is save to indicate success to the client. This case might
occur, if the lock expired right before the client sent the request. Anyway,
the lock is gone, as desired by the client.

PUT
===

The PUT method requires 2 additional checks if the If header check succeeded
(meaning that any send If header mached). The actions to take place before the
original processing of the PUT method can take place are described a follows
(pseudo code): ::

    lock backend;
    create propfind request for <resource>;
    set Depth header to 0;
    send <propfind request> to backend;
    if ( <resource> exists )
    {
        if ( <resource> is locked && <lock> is not mentioned in the If header )
        {
            return <response 423 Locked>;
        }
        if ( <resource> is a lock null resource )
        {
            create <proppatch request> for <resource> to make it a real resource;
            send <proppatch request> to backend;
        }
    }
    else
    {
        <parent> = get collection <resource>;
        create propfind request for <parent>;
        set Depth header to 0;
        send <propfind request> to backend;
        if ( <parent> is locked && <lock> is not mentioned in the If header )
        {
            return <response 423 Locked>;
        }
    }

    // Let backend perform PUT ...

    unlock backend;


POST
====

The POST request is not honored by the Webdav components and will therefore not
be included in lock handling.

PROPPATCH
=========

The PROPPATCH request is only allowed if the affected resource is not locked by
someone else. The necessery extensions are described below (pseudo code): ::
    
    lock backend;

    create propfind request for <resource>;
    set Depth header to 0;
    send <propfind request> to backend;
    if ( <resource> is locked && <lock> is not mentioned in the If header )
    {
        return <response 423 Locked>;
    }

    // Let backend perform PROPPATCH ...

    unlock backend;

MOVE
====

For the MOVE request 2 different resource trees need to be checked for locks.
The algorithm to do so is described below (pseudo code): ::

    lock backend;
    
    create propfind request for <request resource>;
    set Depth header to INFINITY;
    send <propfind request> to backend;
    foreach ( <resource> in <returned response> )
    {
        if ( <resource> is locked && <lock> is not mentioned in the If header )
        {
            return <response 423 Locked>;
        }
    }
    
    create propfind request for <destination resource>;
    set Depth header to 0;
    send <propfind request> to backend;

    if ( <destination resource> exists )
    {
        if ( <destination resource> is locked && <lock> is not mentioned in the If header )
        {
            return <response 423 Locked>;
        }
        if ( <destination resource> is a lock null resource )
        {
            delete <destination resource>;
        }
    }
    else
    {
        <parent> = get collection <destination resource>;
        create propfind request for <parent>;
        set Depth header to 0;
        send <propfind request> to backend;
        if ( <parent> is locked && <lock> is not mentioned in the If header )
        {
            return <response 423 Locked>;
        }
    }

    // Let backend perform MOVE ...

    unlock backend;

COPY
====

In contrast to the MOVE request, only the destination needs to be checked for
locks before copying. The algorithm looks as follows (pseudo code): ::
    
    lock backend;
    
    create propfind request for <destination resource>;
    set Depth header to 0;
    send <propfind request> to backend;

    if ( <destination resource> exists )
    {
        if ( <destination resource> is locked && <lock> is not mentioned in the If header )
        {
            return <response 423 Locked>;
        }
        if ( <destination resource> is a lock null resource )
        {
            delete <destination resource>;
        }
    }
    else
    {
        <parent> = get collection <destination resource>;
        create propfind request for <parent>;
        set Depth header to 0;
        send <propfind request> to backend;
        if ( <parent> is locked && <lock> is not mentioned in the If header )
        {
            return <response 423 Locked>;
        }
    }

    // Let backend perform COPY ...

    unlock backend;

DELETE
======

The DELETE method works on collection and non-collection resources and
therefore needs to check infinite depth for locks (pseudo code): ::
    
    lock backend;
    
    create propfind request for <resource>;
    set Depth header to INFINITY;
    send <propfind request> to backend;
    foreach ( <resource> in <returned response> )
    {
        if ( <resource> is locked && <lock> is not mentioned in the If header )
        {
            return <response 423 Locked>;
        }
    }

    // Let backend perform DELETE ...

    unlock backend;

MKCOL
=====

The MKCOL method is similar to the PUT request in terms of locking. The
following pseudo code shows how this works: ::

    lock backend;
    create propfind request for <resource>;
    set Depth header to 0;
    send <propfind request> to backend;
    if ( <resource> exists )
    {
        if ( <resource> is a lock null resource )
        {
            delete <resource>;
        }
        else if ( <resource> is locked && <lock> is not mentioned in the If header )
        {
            return <response 405 method not allowed>;
        }
    }
    else
    {
        <parent> = get collection <resource>;
        create propfind request for <parent>;
        set Depth header to 0;
        send <propfind request> to backend;
        if ( <parent> is locked && <lock> is not mentioned in the If header )
        {
            return <response 423 Locked>;
        }
    }

    // Let backend perform MKCOL ...

    unlock backend;
    

===========
Open issues
===========

Authentication
==============

The problem
-----------

The WebDAV RFC specifies that lock tokens are not secure for authentication
since the can be obtained by every client with access to the repository through
the lockdiscovery property. Therefore, a server must provide its own
authentication scheme. We currently do not support any authentication mechanism
in the Webdav component. To realize locking, we need to support at least 1
authentication model in the component.

Solution attempt
----------------

The only possible authentication scheme provided through the HTTP protocol is
HTTP-Auth. Therefore the HTTP-Auth information should be integrated into every
request object, if provided. To check this information, an interface should be
provided that provides the following capabilities:

- Information of the assignment of a new lock token to a specific HTTP-Auth
  user.
- Removal of a lock token.
- Answer to the question if a certain user may perform an action with a given
  lock token.

In addition we should provide a basic implementation of this class, which does
actually not do anything but discard the given information and return true on
authentication question. This will not provide any security, but allow users to
plug their own authentication mechanisms into the lock facilities.

Further thoughts
----------------

In future it might be possibly that a WebdavAuthenticationTiein is implemented,
which might then be plugged into the described mechanism, too. This could in
addition be used for a "plugin" that performs authentication (and possibly
authorization?) in general on the transport/server level.

Lock null resources
===================

The problem
------------

For our special case this means, that we need to "physically" create
lock-null resources in the utilized back end, to make it persist during
requests, whenever a null-lock is acquired. This lock-null resource must
not appear as a real resource to the client (e.g. must not have a
correct content type and must not respond to GET requests). If the
resource is unlocked again, it must be removed from the back end again.

In our internal handling, that means, that we need to intervene any of
the operations not supported by a lock-null resource (e.g. COPY, MOVE,
PROPPATCH) requests to check is a lock-null resource is affected and
remove it potentially or forbid the operation. Furthermore, we need to
intervene any operation that is supported by a lock-null resource to
remove potential information from responses that must not be available
for lock-null resources. This will mean a lot of internally generated
requests to the back end and a lot of processing of the back end generated
responses.

For example, if a MOVE request is issued by a client, we need to check
the whole affected directory tree for lock-null resources and remove
those from the destination and send a special error code for them in the
response.

Atomicity of operations also comes into play here, since our internal
requests to the back end might interfere with other external requests. For
example, a null-lock might be acquired by the client while we are
internally checking the correctness of a MOVE request (race-condition).

Solution attempts
-----------------

I currently do not see a full solution for this problem, still there are
some ideas in my mind, that might be helpful. For lock-null resources we
can invent a special dead-property name space to indicate such resources.
If a special (for lock-null resources forbidden) operation occurs, we
can easily check for them using a PROPFIND request. Creating and
maintaining lock-null resources should then be not problem.

For the race-condition issue, we'll definitely need support in the
back end, because I see no other solution attempt here to ensure the
consistency.

Common concerns
^^^^^^^^^^^^^^^

This section describes the design to realize support for the If header, which
is used in WebDAV with combinations of entity tags and lock tokens.

============
Plugin based
============

This section tries to design the honoring of the If header in the Webdav
component using a plugin based approach for lock handling. This works
corresponding to the pluin approach described in `Plugin design draft`_.

It is not possibly to exctract all parts of locking support into the lock
plugin, since the If header must be parsed in one go. Since this can also
contain entity tags, which do belong into the main component, it cannot be
parsed exclusively in the lock plugin.

Transport layer
===============

The transport layer needs to parse the If header.

.. Note::
   Since the header can contain entity tags and lock tokens, the parsing needs
   to take place inside ezcWebdavHeaderHandler itself and cannot be part of the
   plugin.
   
The If header is the most complex header that needed to be parsed so far. All
other parsed headers either contained a string value or a simply to process
other scalar value. To encapsulate the If header correctly some new classes
need to be invented which are described in following:

::

    abstract class ezcWebdavIfHeaderList implements ArrayAccess
    {
        protected ezcWebdavIfHeaderListItems[] $items;
    }

    class ezcWebdavIfHeaderTaggedList extends ezcWebdavIfHeaderList
    {

    }

    class ezcWebdavIfHeaderNoTagList extends ezcWebdavIfHeaderList
    {

    }

These classes represent the lists provided in an If header. Both are accessed
through the ArrayAccess interface. The keys used to query the object are
resource pathes (not URIs!).

An ezcWebdavIfHeaderList object will return an array of
ezcWebdavIfHeaderListItem objects on read access through ArrayAccess. This
array represents the OR concatenation of the items. The item class realizes the
OR combination and is described further below.

The ezcWebdavIfHeaderTaggedList will return the list items defined for the resource
path given via ArrayAccess and only those for the given path (an empty array of
none were defined for this path. In contrast to that,
ezcWebdavIfHeaderNoTagList will return all list items for *every* resource
path, since the lists applies to all resources.

This way of abstracting tagged lists and no-tag lists allows a unified usage of
both classes in deeper layers of the Webdav component (plugin or backend).

::

    class ezcWebdavIfHeaderListItem
    {
        public function __construct(
            array $lockTokens = array(),
            array $eTags      = array(),
            bool $negated     = false
        );

        property-read array $lockTokens;
        property-read array $eTags;
        property-read bool  $negated;
    }

An instance of this class represents a list item, which can combines several
entity tags ($eTag) and lock tokens ($lockTokens). In addition it can be
defined to be negated ($negated === true). The combination represented by such
an object is a logical AND combination. 

Code examples
-------------

Some small code examples to illustrate the above class design will be showen
here.

::

   COPY /resource1 HTTP/1.1
   Host: www.foo.bar
   Destination: http://www.foo.bar/resource2
   If: <http://www.foo.bar/resource1> (<locktoken:a-write-lock-token>
   [W/"A weak ETag"]) (["strong ETag"])
   <http://www.bar.bar/random>(["another strong ETag"])

This example shows a tagged list in the If header, which will be parsed into an
instance of ezcWebdavIfHeaderTaggedList will be created from it in the
Transport layer. The access to this object in the backend or the lock plugin
will look as follows: ::

    $res1items = $ifHeader['/resource1'];

    $res2items = $ifHeader['/resource2'];

    $randomItems = $ifHeader['/random'];

The $res1items variable will contain an array reflecting the conditions
specified for http://www.foo.bar/resource1. The $res2items variable will
contain an empty array since no conditions were defined for this resource.
While the $randomItems variable should normally not be requested (since the
resource is not affected) it would contain the corresponding list items for the
http://www.bar.bar/random resource.

In contrast, the following reqeuest would return an instance of
ezcWebdavIfHeaderNoTagList for the contained If header: ::

   COPY /resource1 HTTP/1.1
   Host: www.foo.bar
   Destination: http://www.foo.bar/resource2
   If: (<locktoken:a-write-lock-token> [W/"A weak ETag"]) (["strong ETag"])
   (["another strong ETag"])

Taking the same accesses to the corresponding $ifHeader variable as shown above
will result in all 3 variables containing the same values: All 3 list items
will be contained, since the If header does not use tagging to specify which
resources are affected by the conditions.

Server layer
============

If the lock plugin is active, it needs to hook into every affected request (see
`Affected base methods`_) and check the If header conditions. The check does
not only affect the checking of locktoken conditions, but also the check of
etag validation, because both condition types are combineable. The checks are
performed via the receivedRequest hook of the plugin API.

The procedure (in pseudo code) is as follows: ::

    lock backend;
    foreach ( <range of affected resources> as <basse resource> )
    {
        create propfind request for <base resource>;
        set request properties to <getetag> and <lockdiscovery>;
        set depth according to incoming request;
        send request to backend;
        foreach ( <resources in response> as <resource> )
        {
            if ( <resource> does not conform to If header )
            {
                return <precondition failed response>;
            }
        }
    }
    discard If header;
    unlock backend;

The If header needs to be discarded after correct validation of all entity
tag/lock token conditions. This avoids that the backend checks for the entity
tag conditions a second time.

.. Note::
   The checking of both (lock token and entity tag) conditions is necessary in
{'fraction_non_alphanumeric': 0.06033185745604608, 'fraction_numerical': 0.0035464041611142157, 'mean_word_length': 3.5023224308109153, 'pattern_counts': {'":': 0, '<': 154, '<?xml version=': 0, '>': 155, 'https://': 0, 'lorem ipsum': 0, 'www.': 16, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5963975', 'n_tokens_mistral': 11509, 'n_tokens_neox': 10982, 'n_words': 6616}