6 * The CardDAV plugin adds CardDAV functionality to the WebDAV server
10 * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
11 * @author Evert Pot (http://www.rooftopsolutions.nl/)
12 * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
14 class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin {
17 * Url to the addressbooks
19 const ADDRESSBOOK_ROOT = 'addressbooks';
22 * xml namespace for CardDAV elements
24 const NS_CARDDAV = 'urn:ietf:params:xml:ns:carddav';
27 * Add urls to this property to have them automatically exposed as
28 * 'directories' to the user.
32 public $directories = array();
37 * @var Sabre_DAV_Server
42 * Initializes the plugin
44 * @param Sabre_DAV_Server $server
47 public function initialize(Sabre_DAV_Server $server) {
50 $server->subscribeEvent('beforeGetProperties', array($this, 'beforeGetProperties'));
51 $server->subscribeEvent('afterGetProperties', array($this, 'afterGetProperties'));
52 $server->subscribeEvent('updateProperties', array($this, 'updateProperties'));
53 $server->subscribeEvent('report', array($this,'report'));
54 $server->subscribeEvent('onHTMLActionsPanel', array($this,'htmlActionsPanel'));
55 $server->subscribeEvent('onBrowserPostAction', array($this,'browserPostAction'));
56 $server->subscribeEvent('beforeWriteContent', array($this, 'beforeWriteContent'));
57 $server->subscribeEvent('beforeCreateFile', array($this, 'beforeCreateFile'));
60 $server->xmlNamespaces[self::NS_CARDDAV] = 'card';
62 /* Mapping Interfaces to {DAV:}resourcetype values */
63 $server->resourceTypeMapping['Sabre_CardDAV_IAddressBook'] = '{' . self::NS_CARDDAV . '}addressbook';
64 $server->resourceTypeMapping['Sabre_CardDAV_IDirectory'] = '{' . self::NS_CARDDAV . '}directory';
66 /* Adding properties that may never be changed */
67 $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}supported-address-data';
68 $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}max-resource-size';
69 $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}addressbook-home-set';
70 $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}supported-collation-set';
72 $server->propertyMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre_DAV_Property_Href';
74 $this->server = $server;
79 * Returns a list of supported features.
81 * This is used in the DAV: header in the OPTIONS and PROPFIND requests.
85 public function getFeatures() {
87 return array('addressbook');
92 * Returns a list of reports this plugin supports.
94 * This will be used in the {DAV:}supported-report-set property.
95 * Note that you still need to subscribe to the 'report' event to actually
101 public function getSupportedReportSet($uri) {
103 $node = $this->server->tree->getNodeForPath($uri);
104 if ($node instanceof Sabre_CardDAV_IAddressBook || $node instanceof Sabre_CardDAV_ICard) {
106 '{' . self::NS_CARDDAV . '}addressbook-multiget',
107 '{' . self::NS_CARDDAV . '}addressbook-query',
116 * Adds all CardDAV-specific properties
118 * @param string $path
119 * @param Sabre_DAV_INode $node
120 * @param array $requestedProperties
121 * @param array $returnedProperties
124 public function beforeGetProperties($path, Sabre_DAV_INode $node, array &$requestedProperties, array &$returnedProperties) {
126 if ($node instanceof Sabre_DAVACL_IPrincipal) {
128 // calendar-home-set property
129 $addHome = '{' . self::NS_CARDDAV . '}addressbook-home-set';
130 if (in_array($addHome,$requestedProperties)) {
131 $principalId = $node->getName();
132 $addressbookHomePath = self::ADDRESSBOOK_ROOT . '/' . $principalId . '/';
133 unset($requestedProperties[array_search($addHome, $requestedProperties)]);
134 $returnedProperties[200][$addHome] = new Sabre_DAV_Property_Href($addressbookHomePath);
137 $directories = '{' . self::NS_CARDDAV . '}directory-gateway';
138 if ($this->directories && in_array($directories, $requestedProperties)) {
139 unset($requestedProperties[array_search($directories, $requestedProperties)]);
140 $returnedProperties[200][$directories] = new Sabre_DAV_Property_HrefList($this->directories);
145 if ($node instanceof Sabre_CardDAV_ICard) {
147 // The address-data property is not supposed to be a 'real'
148 // property, but in large chunks of the spec it does act as such.
149 // Therefore we simply expose it as a property.
150 $addressDataProp = '{' . self::NS_CARDDAV . '}address-data';
151 if (in_array($addressDataProp, $requestedProperties)) {
152 unset($requestedProperties[$addressDataProp]);
154 if (is_resource($val))
155 $val = stream_get_contents($val);
157 $returnedProperties[200][$addressDataProp] = $val;
162 if ($node instanceof Sabre_CardDAV_UserAddressBooks) {
164 $meCardProp = '{http://calendarserver.org/ns/}me-card';
165 if (in_array($meCardProp, $requestedProperties)) {
167 $props = $this->server->getProperties($node->getOwner(), array('{http://sabredav.org/ns}vcard-url'));
168 if (isset($props['{http://sabredav.org/ns}vcard-url'])) {
170 $returnedProperties[200][$meCardProp] = new Sabre_DAV_Property_Href(
171 $props['{http://sabredav.org/ns}vcard-url']
173 $pos = array_search($meCardProp, $requestedProperties);
174 unset($requestedProperties[$pos]);
185 * This event is triggered when a PROPPATCH method is executed
187 * @param array $mutations
188 * @param array $result
189 * @param Sabre_DAV_INode $node
192 public function updateProperties(&$mutations, &$result, $node) {
194 if (!$node instanceof Sabre_CardDAV_UserAddressBooks) {
198 $meCard = '{http://calendarserver.org/ns/}me-card';
200 // The only property we care about
201 if (!isset($mutations[$meCard]))
204 $value = $mutations[$meCard];
205 unset($mutations[$meCard]);
207 if ($value instanceof Sabre_DAV_Property_IHref) {
208 $value = $value->getHref();
209 $value = $this->server->calculateUri($value);
210 } elseif (!is_null($value)) {
211 $result[400][$meCard] = null;
215 $innerResult = $this->server->updateProperties(
218 '{http://sabredav.org/ns}vcard-url' => $value,
222 $closureResult = false;
223 foreach($innerResult as $status => $props) {
224 if (is_array($props) && array_key_exists('{http://sabredav.org/ns}vcard-url', $props)) {
225 $result[$status][$meCard] = null;
226 $closureResult = ($status>=200 && $status<300);
236 * This functions handles REPORT requests specific to CardDAV
238 * @param string $reportName
239 * @param DOMNode $dom
242 public function report($reportName,$dom) {
244 switch($reportName) {
245 case '{'.self::NS_CARDDAV.'}addressbook-multiget' :
246 $this->addressbookMultiGetReport($dom);
248 case '{'.self::NS_CARDDAV.'}addressbook-query' :
249 $this->addressBookQueryReport($dom);
260 * This function handles the addressbook-multiget REPORT.
262 * This report is used by the client to fetch the content of a series
263 * of urls. Effectively avoiding a lot of redundant requests.
265 * @param DOMNode $dom
268 public function addressbookMultiGetReport($dom) {
270 $properties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild));
272 $hrefElems = $dom->getElementsByTagNameNS('DAV:','href');
273 $propertyList = array();
275 foreach($hrefElems as $elem) {
277 $uri = $this->server->calculateUri($elem->nodeValue);
278 list($propertyList[]) = $this->server->getPropertiesForPath($uri,$properties);
282 $this->server->httpResponse->sendStatus(207);
283 $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
284 $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList));
289 * This method is triggered before a file gets updated with new content.
291 * This plugin uses this method to ensure that Card nodes receive valid
294 * @param string $path
295 * @param Sabre_DAV_IFile $node
296 * @param resource $data
299 public function beforeWriteContent($path, Sabre_DAV_IFile $node, &$data) {
301 if (!$node instanceof Sabre_CardDAV_ICard)
304 $this->validateVCard($data);
309 * This method is triggered before a new file is created.
311 * This plugin uses this method to ensure that Card nodes receive valid
314 * @param string $path
315 * @param resource $data
316 * @param Sabre_DAV_ICollection $parentNode
319 public function beforeCreateFile($path, &$data, Sabre_DAV_ICollection $parentNode) {
321 if (!$parentNode instanceof Sabre_CardDAV_IAddressBook)
324 $this->validateVCard($data);
329 * Checks if the submitted iCalendar data is in fact, valid.
331 * An exception is thrown if it's not.
333 * @param resource|string $data
336 protected function validateVCard(&$data) {
338 // If it's a stream, we convert it to a string first.
339 if (is_resource($data)) {
340 $data = stream_get_contents($data);
343 // Converting the data to unicode, if needed.
344 $data = Sabre_DAV_StringUtil::ensureUTF8($data);
348 $vobj = Sabre_VObject_Reader::read($data);
350 } catch (Sabre_VObject_ParseException $e) {
352 throw new Sabre_DAV_Exception_UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage());
356 if ($vobj->name !== 'VCARD') {
357 throw new Sabre_DAV_Exception_UnsupportedMediaType('This collection can only support vcard objects.');
360 if (!isset($vobj->UID)) {
361 throw new Sabre_DAV_Exception_BadRequest('Every vcard must have an UID.');
368 * This function handles the addressbook-query REPORT
370 * This report is used by the client to filter an addressbook based on a
373 * @param DOMNode $dom
376 protected function addressbookQueryReport($dom) {
378 $query = new Sabre_CardDAV_AddressBookQueryParser($dom);
381 $depth = $this->server->getHTTPDepth(0);
384 $candidateNodes = array(
385 $this->server->tree->getNodeForPath($this->server->getRequestUri())
388 $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
391 $validNodes = array();
392 foreach($candidateNodes as $node) {
394 if (!$node instanceof Sabre_CardDAV_ICard)
397 $blob = $node->get();
398 if (is_resource($blob)) {
399 $blob = stream_get_contents($blob);
402 if (!$this->validateFilters($blob, $query->filters, $query->test)) {
406 $validNodes[] = $node;
408 if ($query->limit && $query->limit <= count($validNodes)) {
409 // We hit the maximum number of items, we can stop now.
416 foreach($validNodes as $validNode) {
419 $href = $this->server->getRequestUri();
421 $href = $this->server->getRequestUri() . '/' . $validNode->getName();
424 list($result[]) = $this->server->getPropertiesForPath($href, $query->requestedProperties, 0);
428 $this->server->httpResponse->sendStatus(207);
429 $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
430 $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result));
435 * Validates if a vcard makes it throught a list of filters.
437 * @param string $vcardData
438 * @param array $filters
439 * @param string $test anyof or allof (which means OR or AND)
442 public function validateFilters($vcardData, array $filters, $test) {
444 $vcard = Sabre_VObject_Reader::read($vcardData);
446 if (!$filters) return true;
448 foreach($filters as $filter) {
450 $isDefined = isset($vcard->{$filter['name']});
451 if ($filter['is-not-defined']) {
457 } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
459 // We only need to check for existence
460 $success = $isDefined;
464 $vProperties = $vcard->select($filter['name']);
467 if ($filter['param-filters']) {
468 $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
470 if ($filter['text-matches']) {
472 foreach($vProperties as $vProperty)
473 $texts[] = $vProperty->value;
475 $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
478 if (count($results)===1) {
479 $success = $results[0];
481 if ($filter['test'] === 'anyof') {
482 $success = $results[0] || $results[1];
484 $success = $results[0] && $results[1];
490 // There are two conditions where we can already determine whether
491 // or not this filter succeeds.
492 if ($test==='anyof' && $success) {
495 if ($test==='allof' && !$success) {
501 // If we got all the way here, it means we haven't been able to
502 // determine early if the test failed or not.
504 // This implies for 'anyof' that the test failed, and for 'allof' that
505 // we succeeded. Sounds weird, but makes sense.
506 return $test==='allof';
511 * Validates if a param-filter can be applied to a specific property.
513 * @todo currently we're only validating the first parameter of the passed
514 * property. Any subsequence parameters with the same name are
516 * @param array $vProperties
517 * @param array $filters
518 * @param string $test
521 protected function validateParamFilters(array $vProperties, array $filters, $test) {
523 foreach($filters as $filter) {
526 foreach($vProperties as $vProperty) {
527 $isDefined = isset($vProperty[$filter['name']]);
528 if ($isDefined) break;
531 if ($filter['is-not-defined']) {
538 // If there's no text-match, we can just check for existence
539 } elseif (!$filter['text-match'] || !$isDefined) {
541 $success = $isDefined;
546 foreach($vProperties as $vProperty) {
547 // If we got all the way here, we'll need to validate the
548 // text-match filter.
549 $success = Sabre_DAV_StringUtil::textMatch($vProperty[$filter['name']]->value, $filter['text-match']['value'], $filter['text-match']['collation'], $filter['text-match']['match-type']);
552 if ($filter['text-match']['negate-condition']) {
553 $success = !$success;
558 // There are two conditions where we can already determine whether
559 // or not this filter succeeds.
560 if ($test==='anyof' && $success) {
563 if ($test==='allof' && !$success) {
569 // If we got all the way here, it means we haven't been able to
570 // determine early if the test failed or not.
572 // This implies for 'anyof' that the test failed, and for 'allof' that
573 // we succeeded. Sounds weird, but makes sense.
574 return $test==='allof';
579 * Validates if a text-filter can be applied to a specific property.
581 * @param array $texts
582 * @param array $filters
583 * @param string $test
586 protected function validateTextMatches(array $texts, array $filters, $test) {
588 foreach($filters as $filter) {
591 foreach($texts as $haystack) {
592 $success = Sabre_DAV_StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
594 // Breaking on the first match
597 if ($filter['negate-condition']) {
598 $success = !$success;
601 if ($success && $test==='anyof')
604 if (!$success && $test=='allof')
610 // If we got all the way here, it means we haven't been able to
611 // determine early if the test failed or not.
613 // This implies for 'anyof' that the test failed, and for 'allof' that
614 // we succeeded. Sounds weird, but makes sense.
615 return $test==='allof';
620 * This event is triggered after webdav-properties have been retrieved.
624 public function afterGetProperties($uri, &$properties) {
626 // If the request was made using the SOGO connector, we must rewrite
627 // the content-type property. By default SabreDAV will send back
628 // text/x-vcard; charset=utf-8, but for SOGO we must strip that last
630 if (!isset($properties[200]['{DAV:}getcontenttype']))
633 if (strpos($this->server->httpRequest->getHeader('User-Agent'),'Thunderbird')===false) {
637 if (strpos($properties[200]['{DAV:}getcontenttype'],'text/x-vcard')===0) {
638 $properties[200]['{DAV:}getcontenttype'] = 'text/x-vcard';
644 * This method is used to generate HTML output for the
645 * Sabre_DAV_Browser_Plugin. This allows us to generate an interface users
646 * can use to create new calendars.
648 * @param Sabre_DAV_INode $node
649 * @param string $output
652 public function htmlActionsPanel(Sabre_DAV_INode $node, &$output) {
654 if (!$node instanceof Sabre_CardDAV_UserAddressBooks)
657 $output.= '<tr><td colspan="2"><form method="post" action="">
658 <h3>Create new address book</h3>
659 <input type="hidden" name="sabreAction" value="mkaddressbook" />
660 <label>Name (uri):</label> <input type="text" name="name" /><br />
661 <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
662 <input type="submit" value="create" />
671 * This method allows us to intercept the 'mkcalendar' sabreAction. This
672 * action enables the user to create new calendars from the browser plugin.
675 * @param string $action
676 * @param array $postVars
679 public function browserPostAction($uri, $action, array $postVars) {
681 if ($action!=='mkaddressbook')
684 $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:carddav}addressbook');
685 $properties = array();
686 if (isset($postVars['{DAV:}displayname'])) {
687 $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
689 $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);