You are viewing revision #4 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version.
- Short introduction to WebDAV
- Requirements
- Configuration
- Controller
- Database
- SabreDAV backends
- PHP client
This article will show you how to easily implement a WebDAV server in your project using SabreDAV.
Short introduction to WebDAV ¶
WebDAV server provides a remote filesystem in a tree structure, with locks, authentication and ACLs for authorization. As a storage backend, it can use an ordinary filesystem, storing data in a directory or some other data source, like a database. The tree structure doesn't have to exist, it could just represent some business logic of a custom project.
CalDAV and CardDAV are WebDAV extensions, adding special directories holding files representing calendar events and contact cards.
SabreDAV is a PHP implementation of a WebDAV server. It's designed to be integrated into other software. Thanks to this, a WebDAV server can be added to an existing projects utilizing existing:
- database structures
- business logic in ActiveRecords
- authentication mechanisms
Every WebDAV server uses following data models:
- principals - representing users and other resources
- groups of principals
- locks - optionally
CalDAV uses:
- calendars
- calendar objects (events)
CardDAV uses:
- addressbooks
- addressbooks entries (cards)
All of the above must be provided in classes extending base classes provided by SabreDAV.
Requirements ¶
Download a release zip.
Extract it to a temporary directory, then copy:
- SabreDAV/lib/Sabre into protected/vendors
- SabreDAV/vendor/sabre/vobject/lib/Sabre/VObject into protected/vendors/Sabre
Save the rest for future reference, as the examples and tests will provide useful code templates.
Configuration ¶
SabreDAV server provides its own url managment, so let's redirect all traffic prefixed with dav into one action running that server.
In protected/config/main.php adjust the rules in urlManager component:
'urlManager' => array(
'urlFormat' => 'path',
'rules' => array(
'dav*' => array('dav/index', 'parsingOnly'=>true),
// ... rest of your usual routes
'<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>',
'<controller:\w+>/<action:\w+>' => '<controller>/<action>',
),
'showScriptName' => false,
),
Controller ¶
SabreDAV classes are namespaced and they integrate with Yii's autoloader. Because a one controller's action is a single point of entry into all SabreDAVs code, we can create an alias for the autoloader directly in the controller's source file.
After configuring and starting the DAV server, it will take over and process the request further.
All project-specific classes are prefixed with My. They override classes provided by SabreDAV to adjust to a different database schema, use ActiveRecords instead of plain PDO and Yii's auth mechanisms.
This example starts a calendar server, but it's just a plain WebDAV server extended with some plugins.
<?php
Yii::setPathOfAlias('Sabre', Yii::getPathOfAlias('application.vendors.Sabre'));
use Sabre\DAV;
class DavController extends CController
{
public function actionIndex()
{
// only 3 simple classes must be implemented by extending SabreDAVs base classes
$principalBackend = new MyPrincipalBackend;
$calendarBackend = new MyCalendarBackend;
$authPlugin = new Sabre\DAV\Auth\Plugin(new MyAuth, 'example.com');
// this defines the root of the tree, here just principals and calendars are stored
$tree = array(
//new Sabre\DAVACL\PrincipalCollection($principalBackend),
new Sabre\CalDAV\Principal\Collection($principalBackend),
new Sabre\CalDAV\CalendarRootNode($principalBackend, $calendarBackend),
);
$server = new DAV\Server($tree);
$server->setBaseUri($this->createUrl('/dav/'));
$server->addPlugin($authPlugin);
$server->addPlugin(new Sabre\DAVACL\Plugin());
$server->addPlugin(new Sabre\CalDAV\Plugin());
// this is fun, try to open that action in a plain web browser
$server->addPlugin(new Sabre\DAV\Browser\Plugin());
$server->exec();
}
}
Database ¶
As stated in the introduction, following data models are needed, representing some database structures:
- principals (users) and groups
- locks - optionally
- calendars and calendar objects (events) - optionally, if building a CalDAV
- addressbooks and addressbooks entries (cards) - optionally, if building a CardDAV
You can use your existing database structures to store and read that data or create them from examples provided with SabreDAV.
Take a look at the extracted SabreDAV release zip, into the SabreDAV/examples/sql directory. Choose files for your database. Use them as a template for creating new tables or to get an idea of what is being used by provided example backends.
SabreDAV backends ¶
Auth ¶
Extending the AbstractBasic class provides us with a HTTP Basic auth mechanism for our DAV server.
To validate the username and password we can use the same UserIdentity class that is used to log into the Yii based project.
<?php
class MyAuth extends Sabre\DAV\Auth\Backend\AbstractBasic
{
protected $_identity;
protected function validateUserPass($username, $password)
{
if ($this->_identity === null) {
$this->_identity=new UserIdentity($username,$password);
}
return $this->_identity->authenticate() && $this->_identity->errorCode == UserIdentity::ERROR_NONE;
}
}
Principal ¶
Principal backend just performes searches and returns results as arrays of simple string properties.
Take a look at the class in Sabre\DAVACL\PrincipalBackend\PDO.
Now create a similar class extending Sabre\DAVACL\PrincipalBackend\AbstractBackend, but use your ActiveRecord models instead of performing raw SQL queries through PDO.
Info: as principals represent users, groups and other resources, more than one ActiveRecord could be used in the PrincipalBackend creating a data union
Below is a simple, stripped from comments example of a principal backend, along with three methods in the User model class.
use Sabre\DAV;
use Sabre\DAVACL;
class MyPrincipalBackend extends Sabre\DAVACL\PrincipalBackend\AbstractBackend {
/**
* ActiveRecord class name for 'principals' model.
* It must contain an 'uri' attribute, property or getter method.
* It must contain following methods:
* - getPrincipals() returning an array of property=>value.
* - setPrincipals() accepting an array of property=>value.
* - getPrincipalMap() returning an array of property=>attribute.
*
* @var string
*/
protected $userClass;
public function __construct($userClass = 'User') {
$this->userClass = $userClass;
}
public function getPrincipalsByPrefix($prefixPath) {
$models = CActiveRecord::model($this->userClass)->findAll();
$principals = array();
foreach($models as $model) {
list($rowPrefix) = DAV\URLUtil::splitPath($model->uri);
if ($rowPrefix !== $prefixPath) continue;
$principals[] = $model->getPrincipal();
}
return $principals;
}
public function getPrincipalByPath($path) {
$model = CActiveRecord::model($this->userClass)->findByAttributes(array('uri'=>$path));
return $model === null ? null : $model->getPrincipal();
}
public function updatePrincipal($path, $mutations) {
$model = CActiveRecord::model($this->userClass)->findByAttributes(array('uri'=>$path));
if ($model === null)
return false;
$result = $model->setPrincipal($mutations);
if (is_string($result)) {
$response = array(
403 => array($result => null),
424 => array(),
);
// Adding the rest to the response as a 424
foreach($mutations as $key=>$value) {
if ($key !== $result) {
$response[424][$key] = null;
}
}
return $response;
}
return $result;
}
public function searchPrincipals($prefixPath, array $searchProperties) {
$map = CActiveRecord::model($this->userClass)->getPrincipalMap();
$attributes = array();
// translate keys in $searchProperties from property names to attribute names
foreach($searchProperties as $property => $value) {
if (isset($map[$property])) $attributes[$map[$property]] = $value;
}
$models = CActiveRecord::model($this->userClass)->findByAttributes($attributes);
$principals = array();
foreach($models as $model) {
list($rowPrefix) = DAV\URLUtil::splitPath($model->uri);
if ($rowPrefix !== $prefixPath) continue;
$principals[] = $model->getPrincipal();
}
return $principals;
}
public function getGroupMemberSet($principal) {
// not implemented, this could return all principals for a share-all calendar server
return array();
}
public function getGroupMembership($principal) {
// not implemented, this could return a list of all principals
// with two subprincipals: calendar-proxy-read and calendar-proxy-write for a share-all calendar server
return array();
}
public function setGroupMemberSet($principal, array $members) {
throw new Exception\NotImplemented('Not Implemented');
}
}
Three extra methods in the User model class, moving more business logic from the principal backend to the models.
class User extends BaseUser
{
public static function model($className=__CLASS__) {
return parent::model($className);
}
public function getPrincipalMap() {
// adjust that to your User class
return array(
'{DAV:}displayname' => 'displayname',
'{http://sabredav.org/ns}vcard-url' => 'vcardurl',
'{http://sabredav.org/ns}email-address' => 'email',
);
}
public function getPrincipal() {
// maybe an uri getter method could generate it on the fly from primary key
$result = array(
'id' => $this->primaryKey,
'uri' => $this->uri,
);
foreach($this->getPrincipalMap() as $property=>$attribute) {
$result[$property] = $this->{$attribute};
}
return $result;
}
public function setPrincipal(array $principal) {
// validate that all $principal keys are known properties
$map = $this->getPrincipalMap();
$validProperties = array_keys($map);
foreach($principal as $property=>$value) {
if (!in_array($property, $validProperties)) {
return $property;
}
$this->{$map[$property]} = $value;
}
return $this->save();
}
}
Others ¶
Other backends, like Calendar or Card are more complex and tend to be very project-specific.
For a Calendar backend, look at Sabre\CalDAV\Backend\PDO and then create a class extending Sabre\CalDAV\Backend\AbstractBackend.
Do the same for others, if you need them.
Info: now the project specific business logic can be kept separated from the WebDAV implementation inside ActiveRecords as rules, scopes and scenarios
PHP client ¶
After playing around with your new calendar server using Thunderbird with the Lightning extension it's time to write your own client in Yii.
Backend ¶
SabreDAV again provides a basic example class that could be used as a starting point. Talking to a DAV server is just like any other REST service, using XML as the media type.
Info: if your DAV server is the same web application as the client, remember to stop the session before making a request to the DAV server. When a PHP process makes a request to the same server, the second process will wait for the first one to release the lock on the session file and the first one will wait for the request to complete, creating a deadlock.
Here's an example action fetching some calendar objects.
<?php
class ClientController extends Controller {
protected $davSettings = array(
'baseUri' => 'dav/',
// a special auth backend was provided for SabreDAV that authenticates users
// already logged into Yii, so no username or password is sent
'userName' => null,
//'password' => null,
);
protected function beforeAction($action) {
$this->davSettings['baseUri'] = $this->createAbsoluteUrl($this->davSettings['baseUri']) . '/';
return true;
}
public function actionIndex($start = null, $end = null) {
// we will be using same session to authenticate in SabreDAV
$headers = array('Cookie'=>Yii::app()->session->sessionName.'='.Yii::app()->session->sessionID);
Yii::app()->session->close();
$client = new Sabre\DAV\Client($this->davSettings);
// prepare request body
$doc = new DOMDocument('1.0', 'utf-8');
$doc->formatOutput = true;
$query = $doc->createElement('c:calendar-query');
$query->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:c', 'urn:ietf:params:xml:ns:caldav');
$query->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:d', 'DAV:');
$prop = $doc->createElement('d:prop');
$prop->appendChild($doc->createElement('d:getetag'));
$prop->appendChild($doc->createElement('c:calendar-data'));
$query->appendChild($prop);
$doc->appendChild($query);
$body = $doc->saveXML();
$headers = array_merge(array(
'Depth'=>'1',
'Prefer'=>'return-minimal',
'Content-Type'=>'application/xml; charset=utf-8',
), $headers);
unset($doc);
$response = $this->request('REPORT', 'calendars/somePrincipal', $body, $headers);
header("Content-type: application/json");
echo CJSON::encode($this->parseMultiStatus($response['body']));
}
}
Frontend ¶
A good starting point is to search the extensions catalog for wrappers for FullCalendar jQuery plugin.
Nice Tutorial!
Thanks for this excellent tutorial!
When I open the Client controller, I get the error:
exception 'CException' with message 'Neither ClientController nor attached Behavior have a scope "request".' in C:\Frameworks\yii-git\framework\base\CComponent.php:266 Stack trace: #0 [internal function]: CComponent->__call('request', Array) #1 C:\xampp\htdocs\appname\protected\controllers\ClientController.php(48): ClientController->request('REPORT', 'calendars/someP...', '<?xml version="...', Array)
Thanks + regards,
Joachim
client controller
There's a typo, change:
$this->request
to:
$client->request
I've prepared that example by extracting some code from my class overloading Sabre\DAV\Client.
Very nice....
Hey, super tutorial. Working right now through it.
One question: why is there the example.com
$authPlugin = new Sabre\DAV\Auth\Plugin(new MyAuth, 'example.com');
gb5256
example.com
I don't really remember, check out the source code. It's probably just the message displayed in basic auth when asking for username/password.
MyCalendarBackend.php
Hello,
could somebody share his MyCalendarBackend.php? I am a bit lost for that one...
Thanks,
gb5256
Implementation in Yii 2.0
Anyone looking to implement this in Yii 2.0; remember to disable CSRF validation for the controller. I.e.:
public function init() { $this->enableCsrfValidation = false; }
You'll also need an UrlManager rule. I.e. if you have a DavController with actionServer($path):
['pattern'=>'dav/<path:.+>', 'route'=>'dav/server', 'mode'=>\yii\web\UrlRule::PARSING_ONLY],
Has anyone, by any chance, integrated Sabre calDAV into Yii2 application?
I am strugling with this for couple of weeks with very small partial successes but I'm still far away from the complete working solution.
Any help would be appreciated.
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.