Upgrading from SabreDAV 1.8 to 2.0

SabreDAV 2.0 has finally been released after almost 2 years of work.

New stuff

To read the full list of changes (there's quite a few!) read the changelogs:

API Changes and BC breaks

Now requires PHP 5.4

If you are still running PHP 5.3, you must not upgrade SabreDAV. SabreDAV 1.8 will be maintained for bugfixes for some time, but you should consider upgrading your PHP version to PHP 5.4, to take advantage of the latest and greatest, and get a big speed-up in the process.

Database changes for PDO backends

The PDO backends got a number of changes, mainly to speed things up, but also to provide support for new features.

This affects you if you were running either a CalDAV or a CardDAV server, with the default PDO backend.

Before you start the migration process, please make a backup first! No warranty is given for lost data.

To run the database upgrade, simply start ./bin/migrate20.php from the SabreDAV project. This script will give you more information about its arguments.

Taking advantage of WebDAV-sync

This affects you if you were running a Cal or CardDAV server with the default PDO backend.

This is an optional step, but after upgrading the database schema, you can add the following line to your server file to turn on WebDAV-sync:

$server->addPlugin(new \Sabre\DAV\Sync\Plugin());

The result is much faster synchronization with modern Cal and CardDAV clients. For high-load systems, you should see significant improvements in CPU, Memory and bandwidth usage.

Upgraded to sabre/vobject 3

If you've done any work with sabre/vobject, and you should also read the migration instructions from the vobject project page.

Since this SabreDAV version we are now shipping sabre/vobject 3, which has a large amount of improvements over sabre/vobject 2, but also breaks a few things.

Event / Plugin system got a massive overhaul

This affects you if you wrote your own plugins, or if you used subscribeEvent or broadcastEvent manually.

SabreDAV now externalized the event system into a separate project, namely sabre/event.

In simple terms this means that:

The arguments to these two functions have stayed the same, a bunch more event-related methods have been added, as documented on the sabre/event project page.

However, because we had to change this, we also took the chance to change the arguments of a lot of SabreDAV events, to make them much more effective.

A lot of events had a $method and a $uri argument. I was unhappy with this design, as for any other information related to the HTTP request, aside from the method or uri had to be grabbed from $server->httpRequest.

Similarly, any information sent back to the client always had to be done through $server->httpResponse.

Both these properties still exists, but it is not longer the recommended way to read information about http requests and change the http response. Instead, event handlers get a full $request and $response objects as their direct arguments.

This allows for much easier unit testing, as well as event handlers to do live-rewriting or sub-requests without affecting the global state.

The main events that have changed their behavior are 'beforeMethod' and 'unknownMethod'. 'unknownMethod' has been completely removed, and 'beforeMethod' has altered.

The implication is that every single plugin that implemented those events has now also changed. Besides using on instead of subscribeEvent, the bodies for the event handlers have been changed to use new arguments.

More on the method-handling process in the next chapter.

Switched to PSR-4

We changed out autoloading mechanism to switch from psr-0 to psr-4. If you used vendor/autoload.php this makes absolutely no difference, but if you manually set-up autoloading, be aware that the directory structure is now:

lib/
lib/DAV
lib/DAVACL
lib/CalDAV
lib/CardDAV

And no longer:

lib/
lib/Sabre
lib/Sabre/DAV
lib/Sabre/DAVACL
lib/Sabre/CalDAV
lib/Sabre/CardDAV

Removed deprecated autoloader script

lib/Sabre/autoload.php was already deprecated in version 1.7.0, but now it got removed. If you were still using this, switch to vendor/autoload.php.

The beforeMethod event has changed quite a bit, and the unknownMethod event had been completely removed.

When a HTTP method is executed on a SabreDAV server, for example the 'GET' method, the following events are triggered in order:

  1. beforeMethod:GET
  2. beforeMethod
  3. method:GET
  4. method
  5. afterMethod:GET
  6. afterMethod

By default the CorePlugin will register an event handler for method:GET. If you'd like to handle GET in certain cases, you can be first by registering your own GET method handler, with a lower priority.

The default priority for every method is 100, so by specifying 90 we guarantee that our event is always triggered first.

use
    Sabre\HTTP\RequestInterface,
    Sabre\HTTP\ResponseInterface;


$myGetHandler = function(RequestInterface $request, ResponseInterface $response) {

    // If the filename contains the word cat, we will return miaow instead of
    // the real response body.
    $path = $request->getPath();
    if (false === strpos($path, 'cat')) {
        // No cat = do nothing
        return;
    }

    // There was a cat in the url. Lets do weird stuff.
    $response->setBody('Miaow!');
    $response->setStatus(200); // 200 OK
    $response->setHeader('Content-Type', 'text/plain');

    // This ensures that the standard GET handler does not kick in.
    return false;

};

$server->on('beforeMethod:GET', $myGetHandler, 90);

Every method-related handlers get 2 arguments, the http request and the http response.

beforeMethod allows you to prevent a method from being executed. The ACL method uses this to ensure that methods are not handled if the user did not have permission to do so.

beforeMethod also allows you to rewrite the response body before its being handled, for whatever reason. You may want to convert the format of the response body to something else.

The method and method:GET events (for the PUT method, it would be method:PUT) are strictly reserved for actually handling the event. If you did handle the event, you must return false from your event handler to indicate that the method is handled, and no other system has to do any handling.

The afterMethod event is to do any post-processing. You could use this to log that the method has been executed, or re-write the response body after it has been generated.

CalDAV BackendInterface adds a new method

This affects you, if you wrote your own CalDAV backend.

The CalDAV BackendInterface now adds the getMultipleCalendarObjects method. Implementing this can result in significant speed-ups.

If you extended AbstractBackend, there already is a default implementation, that lazily falls back on calling getCalendarObject.

CardDAV BackendInterface adds a new method

This affects you, if you wrote your own CardDAV backend.

The CardDAV BackendInterface now adds the getMultipleCards method. Implementing this can result in significant speed-ups.

If you extended AbstractBackend, there already is a default implementation, that lazily falls back on calling getCard.

CalDAV and CardDAV PDO backends now have support for WebDAV-Sync

Support for WebDAV-Sync means that every change that's happening in any calendar or addressbook is now recorded.

This allows a client to simply ask for all modifications since the last sync, and greatly improves performance on a number of fronts.

If you made custom modifications to the PDO backends, keep in mind that because of this new functionality, some stuff may not work exactly liked it used to. If for example the methods that create or modify calendar objects don't register a change in the database, a client may never see this change.

Similarly, if you made modifications manually in the database, these may also be 'lost changes'. The best recommendation I can give is to have a good look at the new PDO backends and see what's going on there.

Property updating has changed.

This is relevant to you if you:

The way property updating was implemented, did not allow for certain types of operations. In particular, it was impossible to write a 'generic dead property' storage.

Before, any method that dealt with updating properties would simply receive an argument called $properties or sometimes $mutations that was a simple structure such as this:

array(
    '{DAV:}displayname' => 'new name!',
    '{DAV:}foo' => null,
);

This had to be changed, as different components may be responsible for handling storage of different properties, but each must successful and work together for the entire operation to be successful.

To do this, a new object is introduced, Sabre\DAV\PropPatch. This object holds the mutation list for properties, and provides a whole bunch of convenience methods for the statuses.

The full documentation can be read on Properties, but here's the gist of what has changed:

  1. Sabre\DAV\IProperties::updateProperties (now ::propPatch).
  2. Any principal backend implementing updatePrincipal.
  3. Any CalDAV backend implementing updateCalendar.
  4. Any CardDAV backend implementing updateAddressBook.

All of the previous methods received the $mutations or $properties array.

This is now the PropPatch object.

Here's a new example of a node storing {DAV:}displayname:

function propPatch(\Sabre\DAV\PropPatch $propPatch) {

    $propPatch->handle('{DAV:}displayname', function($value) {

        // Store the $value in the database here.

        // Return true if you were successful.
        return true;

    });

}

As you can see updating properties now happens in two steps:

  1. You tell the system you promise to handle storage of {DAV:}displayname.
  2. You do the actual storing (in the enclosed function).

The new PROPPATCH system now checks if every incoming property can be stored by some subsystem, and only then calls all callbacks.

If you want to store more than one property in one go, this can also be done using the handle method.

This is an example for a CalDAV backend that stores both the displayname, and the calendar description:

function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) {

    $properties = [
        '{DAV:}displayname',
        '{urn:ietf:params:xml:ns:caldav}calendar-description',
    ];

    $propPatch->handle($properties, function($mutations) {

        // $mutations contains the list of properties we need to update.

        // Return true if you were successful.
        return true;

    });

}

As you can see in the previous example, the handle() method can be used to do multiple properties at once by supplying the list of properties as an array.

The updateProperties event is no more

The updateProperties event on the server is now called propPatch, and looks like this:

$server->on('propPatch', function($path, \Sabre\DAV\PropPatch $propPatch) {


});

beforeGetProperties and afterGetProperties and updateProperties events have been removed.

These three events are now replaced by two new events:

Usage instructions can be found on the writing plugins documentation.

Sabre\DAV\IProperties has a new API

If you implement Sabre\DAV\IProperties or ever created a tree node that implemented an updateProperties() method, pay attention.

The updateProperties() method has been removed in favor of a new propPatch() method.

This was needed to allow the creation of a generic property storage module.

The old method signature:

function updateProperties(array $mutations) {

    // ...

}

The new method signature:

function propPatch(\Sabre\DAV\PropPatch $propPatch) {

    // ...

}

Instead of a simple array with to-be-updated properties, the method now receives an instance of Sabre\DAV\PropPatch.

Instead of having to loop through the old array and figure out what properties you support and store, there's a bunch of convenience methods that will help with this.

In particular, the handle() method allows you to tell PropFind "I can handle this property and I promise to store it somewhere".

For example, if your node supports storing the {DAV:}displayname property, this is how that method may look like:

function propPatch(\Sabre\DAV\PropPatch $propPatch) {

    $propPatch->handle('{DAV:}displayname', function($value) {

        // Store $value somewhere. Note that if value is null, you should
        // delete the property.
        //
        // You must return true or false depending on if the operation was
        // successful.

    });

}

Updated updateCalendar and updateAddressBook in CalDAV and CardDAV backends.

Due to the upgraded property system, both updateCalendar and updateAddressBook got similar changes. Each now receive a propPatch object instead of an array with mutations.

Updates in the sabre/http API

The lib/Sabre/HTTP portion of the library moved to its own package, and got an almost 100% rewrite.

If you did anything directly with Sabre\HTTP, it's likely that things have changed. This is most likely true if you ever created an instance of a class in the Sabre\HTTP namespace, you did something with Authentication or something with the Request and Response objects.

The Request object

Old syntax:

$request = new \Sabre\HTTP\Request();

new syntax:

$request = \Sabre\HTTP\Sapi::getRequest();

If you ever created a 'fake' or 'mock' http request, you probably used a syntax such as the following:

$request = new \Sabre\HTTP\Request([
    'REQUEST_URI' => '/foo',
    'REQUEST_METHOD' => 'POST',
    'HTTP_CONTENT_TYPE' => 'application/xml',
]);

The new syntax looks like this:

$request = \Sabre\HTTP\Sapi::createFromServerArray([
    'REQUEST_URI' => '/foo',
    'REQUEST_METHOD' => 'POST',
    'HTTP_CONTENT_TYPE' => 'application/xml',
]);

But.. this is still a bit ugly, as it requires you to manually re-construct the cgi variables from $_SERVER. The new, improved syntax looks like this:

$request = new Request(
  'POST',
  '/foo',
  ['Content-Type' => 'application/xml']
);

The Response object

Before, the response object would automatically call header() and send the response body as soon as possible.

This is no longer true, and some methods also have changed.

Before you would do something like this:

$response = new \Sabre\HTTP\Response();
$response->setHeader('Content-Type', 'application/xml'); // This would already call php's header() !
$response->sendStatus(201); // This also calls php's header()
$response->sendBody('<?xml version="1.0"?>...etc'); // This calls fpassthru/echo

The new response object doesn't send anything right away. Everything is buffered, until sendResponse() is called. In addition, sendStatus is now setStatus, and sendBody is setBody.

$response = new \Sabre\HTTP\Response();
$response->setHeader('Content-Type', 'application/xml');
$response->setStatus(201);
$response->setBody('<?xml version="1.0"?>...etc');

\Sabre\HTTP\Sapi::sendResponse($response);

Authentication

The authentication-related classes have moved into their own namespace. The new class names are:

Furthermore, the constructor arguments have changed for all of these. The following 3 arguments are now required:

$realm is just a single string. $request must be a Request object, and $response and instance of Response.

Custom WebDAV serialization slightly changed

If you implemented your own custom, complex WebDAV properties, using Sabre\DAV\Property as a base-class, this affects you.

The unserialize method now got an extra argument. The full signature changed from static function unserialize(\DOMElement) to static function unserialize(\DOMElement, array $propertyMap). The new $propertyMap argument contains the list of WebDAV properties and which classes they should be mapped to.

All you likely have to do, is to just add that new argument to the method declaration, to ensure PHP doesn't emit any warnings.

Functionality moved from the Server class to CorePlugin

This version of SabreDAV adds a 'CorePlugin', which uses the plugin system to provide basic functionality. The plugin is automatically added.

It does mean that a number of functions have moved from Sabre\DAV\Server to this plugin.

If you sub-classed Sabre\DAV\Server, or called any of these methods manually for some reason, your code will break.

The relevant methods are :

Sabre\DAV\Version::STABILITY no longer exists. The stability is now incorporated in the VERSION constant.

Furthermore, the following classes have been removed, as they mostly contained redundant information:

Changed Sabre\DAV\Server::getCopyMoveInfo

This method now requires a RequestInterface object as its first argument. This object will be used to figure out all the relevant information, rather than the global SabreDAV state.

Sabre\DAV\URLUtil has moved

The new classname for this utility is Sabre\HTTP\URLUtil.

The old class still exists, but will be removed in a future version.

Sabre\DAV\Server::NODE constants are removed.

The Sabre\DAV\Server::NODE_FILE and Sabre\DAV\Server::NODE_COLLECTION no longer exist.