]> git.mxchange.org Git - friendica-addons.git/blob - dav/SabreDAV/lib/Sabre/CardDAV/Plugin.php
Merge remote branch 'friendica/master'
[friendica-addons.git] / dav / SabreDAV / lib / Sabre / CardDAV / Plugin.php
1 <?php
2
3 /**
4  * CardDAV plugin
5  *
6  * The CardDAV plugin adds CardDAV functionality to the WebDAV server
7  *
8  * @package Sabre
9  * @subpackage CardDAV
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
13  */
14 class Sabre_CardDAV_Plugin extends Sabre_DAV_ServerPlugin {
15
16     /**
17      * Url to the addressbooks
18      */
19     const ADDRESSBOOK_ROOT = 'addressbooks';
20
21     /**
22      * xml namespace for CardDAV elements
23      */
24     const NS_CARDDAV = 'urn:ietf:params:xml:ns:carddav';
25
26     /**
27      * Add urls to this property to have them automatically exposed as
28      * 'directories' to the user.
29      *
30      * @var array
31      */
32     public $directories = array();
33
34     /**
35      * Server class
36      *
37      * @var Sabre_DAV_Server
38      */
39     protected $server;
40
41     /**
42      * Initializes the plugin
43      *
44      * @param Sabre_DAV_Server $server
45      * @return void
46      */
47     public function initialize(Sabre_DAV_Server $server) {
48
49         /* Events */
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'));
58
59         /* Namespaces */
60         $server->xmlNamespaces[self::NS_CARDDAV] = 'card';
61
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';
65
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';
71
72         $server->propertyMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre_DAV_Property_Href';
73
74         $this->server = $server;
75
76     }
77
78     /**
79      * Returns a list of supported features.
80      *
81      * This is used in the DAV: header in the OPTIONS and PROPFIND requests.
82      *
83      * @return array
84      */
85     public function getFeatures() {
86
87         return array('addressbook');
88
89     }
90
91     /**
92      * Returns a list of reports this plugin supports.
93      *
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
96      * implement them
97      *
98      * @param string $uri
99      * @return array
100      */
101     public function getSupportedReportSet($uri) {
102
103         $node = $this->server->tree->getNodeForPath($uri);
104         if ($node instanceof Sabre_CardDAV_IAddressBook || $node instanceof Sabre_CardDAV_ICard) {
105             return array(
106                  '{' . self::NS_CARDDAV . '}addressbook-multiget',
107                  '{' . self::NS_CARDDAV . '}addressbook-query',
108             );
109         }
110         return array();
111
112     }
113
114
115     /**
116      * Adds all CardDAV-specific properties
117      *
118      * @param string $path
119      * @param Sabre_DAV_INode $node
120      * @param array $requestedProperties
121      * @param array $returnedProperties
122      * @return void
123      */
124     public function beforeGetProperties($path, Sabre_DAV_INode $node, array &$requestedProperties, array &$returnedProperties) {
125
126         if ($node instanceof Sabre_DAVACL_IPrincipal) {
127
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);
135             }
136
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);
141             }
142
143         }
144
145         if ($node instanceof Sabre_CardDAV_ICard) {
146
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]);
153                 $val = $node->get();
154                 if (is_resource($val))
155                     $val = stream_get_contents($val);
156
157                 // Taking out \r to not screw up the xml output
158                 $returnedProperties[200][$addressDataProp] = str_replace("\r","", $val);
159
160             }
161         }
162
163         if ($node instanceof Sabre_CardDAV_UserAddressBooks) {
164
165             $meCardProp = '{http://calendarserver.org/ns/}me-card';
166             if (in_array($meCardProp, $requestedProperties)) {
167
168                 $props = $this->server->getProperties($node->getOwner(), array('{http://sabredav.org/ns}vcard-url'));
169                 if (isset($props['{http://sabredav.org/ns}vcard-url'])) {
170
171                     $returnedProperties[200][$meCardProp] = new Sabre_DAV_Property_Href(
172                         $props['{http://sabredav.org/ns}vcard-url']
173                     );
174                     $pos = array_search($meCardProp, $requestedProperties);
175                     unset($requestedProperties[$pos]);
176
177                 }
178
179             }
180
181         }
182
183     }
184
185     /**
186      * This event is triggered when a PROPPATCH method is executed
187      *
188      * @param array $mutations
189      * @param array $result
190      * @param Sabre_DAV_INode $node
191      * @return bool
192      */
193     public function updateProperties(&$mutations, &$result, $node) {
194
195         if (!$node instanceof Sabre_CardDAV_UserAddressBooks) {
196             return true;
197         }
198
199         $meCard = '{http://calendarserver.org/ns/}me-card';
200
201         // The only property we care about
202         if (!isset($mutations[$meCard]))
203             return true;
204
205         $value = $mutations[$meCard];
206         unset($mutations[$meCard]);
207
208         if ($value instanceof Sabre_DAV_Property_IHref) {
209             $value = $value->getHref();
210             $value = $this->server->calculateUri($value);
211         } elseif (!is_null($value)) {
212             $result[400][$meCard] = null;
213             return false;
214         }
215
216         $innerResult = $this->server->updateProperties(
217             $node->getOwner(),
218             array(
219                 '{http://sabredav.org/ns}vcard-url' => $value,
220             )
221         );
222
223         $closureResult = false;
224         foreach($innerResult as $status => $props) {
225             if (is_array($props) && array_key_exists('{http://sabredav.org/ns}vcard-url', $props)) {
226                 $result[$status][$meCard] = null;
227                 $closureResult = ($status>=200 && $status<300);
228             }
229
230         }
231
232         return $result;
233
234     }
235
236     /**
237      * This functions handles REPORT requests specific to CardDAV
238      *
239      * @param string $reportName
240      * @param DOMNode $dom
241      * @return bool
242      */
243     public function report($reportName,$dom) {
244
245         switch($reportName) {
246             case '{'.self::NS_CARDDAV.'}addressbook-multiget' :
247                 $this->addressbookMultiGetReport($dom);
248                 return false;
249             case '{'.self::NS_CARDDAV.'}addressbook-query' :
250                 $this->addressBookQueryReport($dom);
251                 return false;
252             default :
253                 return;
254
255         }
256
257
258     }
259
260     /**
261      * This function handles the addressbook-multiget REPORT.
262      *
263      * This report is used by the client to fetch the content of a series
264      * of urls. Effectively avoiding a lot of redundant requests.
265      *
266      * @param DOMNode $dom
267      * @return void
268      */
269     public function addressbookMultiGetReport($dom) {
270
271         $properties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild));
272
273         $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href');
274         $propertyList = array();
275
276         foreach($hrefElems as $elem) {
277
278             $uri = $this->server->calculateUri($elem->nodeValue);
279             list($propertyList[]) = $this->server->getPropertiesForPath($uri,$properties);
280
281         }
282
283         $this->server->httpResponse->sendStatus(207);
284         $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
285         $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList));
286
287     }
288
289     /**
290      * This method is triggered before a file gets updated with new content.
291      *
292      * This plugin uses this method to ensure that Card nodes receive valid
293      * vcard data.
294      *
295      * @param string $path
296      * @param Sabre_DAV_IFile $node
297      * @param resource $data
298      * @return void
299      */
300     public function beforeWriteContent($path, Sabre_DAV_IFile $node, &$data) {
301
302         if (!$node instanceof Sabre_CardDAV_ICard)
303             return;
304
305         $this->validateVCard($data);
306
307     }
308
309     /**
310      * This method is triggered before a new file is created.
311      *
312      * This plugin uses this method to ensure that Card nodes receive valid
313      * vcard data.
314      *
315      * @param string $path
316      * @param resource $data
317      * @param Sabre_DAV_ICollection $parentNode
318      * @return void
319      */
320     public function beforeCreateFile($path, &$data, Sabre_DAV_ICollection $parentNode) {
321
322         if (!$parentNode instanceof Sabre_CardDAV_IAddressBook)
323             return;
324
325         $this->validateVCard($data);
326
327     }
328
329     /**
330      * Checks if the submitted iCalendar data is in fact, valid.
331      *
332      * An exception is thrown if it's not.
333      *
334      * @param resource|string $data
335      * @return void
336      */
337     protected function validateVCard(&$data) {
338
339         // If it's a stream, we convert it to a string first.
340         if (is_resource($data)) {
341             $data = stream_get_contents($data);
342         }
343
344         // Converting the data to unicode, if needed.
345         $data = Sabre_DAV_StringUtil::ensureUTF8($data);
346
347         try {
348
349             $vobj = Sabre_VObject_Reader::read($data);
350
351         } catch (Sabre_VObject_ParseException $e) {
352
353             throw new Sabre_DAV_Exception_UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage());
354
355         }
356
357         if ($vobj->name !== 'VCARD') {
358             throw new Sabre_DAV_Exception_UnsupportedMediaType('This collection can only support vcard objects.');
359         }
360
361     }
362
363
364     /**
365      * This function handles the addressbook-query REPORT
366      *
367      * This report is used by the client to filter an addressbook based on a
368      * complex query.
369      *
370      * @param DOMNode $dom
371      * @return void
372      */
373     protected function addressbookQueryReport($dom) {
374
375         $query = new Sabre_CardDAV_AddressBookQueryParser($dom);
376         $query->parse();
377
378         $depth = $this->server->getHTTPDepth(0);
379
380         if ($depth==0) {
381             $candidateNodes = array(
382                 $this->server->tree->getNodeForPath($this->server->getRequestUri())
383             );
384         } else {
385             $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
386         }
387
388         $validNodes = array();
389         foreach($candidateNodes as $node) {
390
391             if (!$node instanceof Sabre_CardDAV_ICard)
392                 continue;
393
394             $blob = $node->get();
395             if (is_resource($blob)) {
396                 $blob = stream_get_contents($blob);
397             }
398
399             if (!$this->validateFilters($blob, $query->filters, $query->test)) {
400                 continue;
401             }
402
403             $validNodes[] = $node;
404
405             if ($query->limit && $query->limit <= count($validNodes)) {
406                 // We hit the maximum number of items, we can stop now.
407                 break;
408             }
409
410         }
411
412         $result = array();
413         foreach($validNodes as $validNode) {
414
415             if ($depth==0) {
416                 $href = $this->server->getRequestUri();
417             } else {
418                 $href = $this->server->getRequestUri() . '/' . $validNode->getName();
419             }
420
421             list($result[]) = $this->server->getPropertiesForPath($href, $query->requestedProperties, 0);
422
423         }
424
425         $this->server->httpResponse->sendStatus(207);
426         $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
427         $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result));
428
429     }
430
431     /**
432      * Validates if a vcard makes it throught a list of filters.
433      *
434      * @param string $vcardData
435      * @param array $filters
436      * @param string $test anyof or allof (which means OR or AND)
437      * @return bool
438      */
439     public function validateFilters($vcardData, array $filters, $test) {
440
441         $vcard = Sabre_VObject_Reader::read($vcardData);
442
443         foreach($filters as $filter) {
444
445             $isDefined = isset($vcard->{$filter['name']});
446             if ($filter['is-not-defined']) {
447                 if ($isDefined) {
448                     $success = false;
449                 } else {
450                     $success = true;
451                 }
452             } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
453
454                 // We only need to check for existence
455                 $success = $isDefined;
456
457             } else {
458
459                 $vProperties = $vcard->select($filter['name']);
460
461                 $results = array();
462                 if ($filter['param-filters']) {
463                     $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
464                 }
465                 if ($filter['text-matches']) {
466                     $texts = array();
467                     foreach($vProperties as $vProperty)
468                         $texts[] = $vProperty->value;
469
470                     $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
471                 }
472
473                 if (count($results)===1) {
474                     $success = $results[0];
475                 } else {
476                     if ($filter['test'] === 'anyof') {
477                         $success = $results[0] || $results[1];
478                     } else {
479                         $success = $results[0] && $results[1];
480                     }
481                 }
482
483             } // else
484
485             // There are two conditions where we can already determine whether
486             // or not this filter succeeds.
487             if ($test==='anyof' && $success) {
488                 return true;
489             }
490             if ($test==='allof' && !$success) {
491                 return false;
492             }
493
494         } // foreach
495
496         // If we got all the way here, it means we haven't been able to
497         // determine early if the test failed or not.
498         //
499         // This implies for 'anyof' that the test failed, and for 'allof' that
500         // we succeeded. Sounds weird, but makes sense.
501         return $test==='allof';
502
503     }
504
505     /**
506      * Validates if a param-filter can be applied to a specific property.
507      *
508      * @todo currently we're only validating the first parameter of the passed
509      *       property. Any subsequence parameters with the same name are
510      *       ignored.
511      * @param array $vProperties
512      * @param array $filters
513      * @param string $test
514      * @return bool
515      */
516     protected function validateParamFilters(array $vProperties, array $filters, $test) {
517
518         foreach($filters as $filter) {
519
520             $isDefined = false;
521             foreach($vProperties as $vProperty) {
522                 $isDefined = isset($vProperty[$filter['name']]);
523                 if ($isDefined) break;
524             }
525
526             if ($filter['is-not-defined']) {
527                 if ($isDefined) {
528                     $success = false;
529                 } else {
530                     $success = true;
531                 }
532
533             // If there's no text-match, we can just check for existence
534             } elseif (!$filter['text-match'] || !$isDefined) {
535
536                 $success = $isDefined;
537
538             } else {
539
540                 $success = false;
541                 foreach($vProperties as $vProperty) {
542                     // If we got all the way here, we'll need to validate the
543                     // text-match filter.
544                     $success = Sabre_DAV_StringUtil::textMatch($vProperty[$filter['name']]->value, $filter['text-match']['value'], $filter['text-match']['collation'], $filter['text-match']['match-type']);
545                     if ($success) break;
546                 }
547                 if ($filter['text-match']['negate-condition']) {
548                     $success = !$success;
549                 }
550
551             } // else
552
553             // There are two conditions where we can already determine whether
554             // or not this filter succeeds.
555             if ($test==='anyof' && $success) {
556                 return true;
557             }
558             if ($test==='allof' && !$success) {
559                 return false;
560             }
561
562         }
563
564         // If we got all the way here, it means we haven't been able to
565         // determine early if the test failed or not.
566         //
567         // This implies for 'anyof' that the test failed, and for 'allof' that
568         // we succeeded. Sounds weird, but makes sense.
569         return $test==='allof';
570
571     }
572
573     /**
574      * Validates if a text-filter can be applied to a specific property.
575      *
576      * @param array $texts
577      * @param array $filters
578      * @param string $test
579      * @return bool
580      */
581     protected function validateTextMatches(array $texts, array $filters, $test) {
582
583         foreach($filters as $filter) {
584
585             $success = false;
586             foreach($texts as $haystack) {
587                 $success = Sabre_DAV_StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
588
589                 // Breaking on the first match
590                 if ($success) break;
591             }
592             if ($filter['negate-condition']) {
593                 $success = !$success;
594             }
595
596             if ($success && $test==='anyof')
597                 return true;
598
599             if (!$success && $test=='allof')
600                 return false;
601
602
603         }
604
605         // If we got all the way here, it means we haven't been able to
606         // determine early if the test failed or not.
607         //
608         // This implies for 'anyof' that the test failed, and for 'allof' that
609         // we succeeded. Sounds weird, but makes sense.
610         return $test==='allof';
611
612     }
613
614     /**
615      * This event is triggered after webdav-properties have been retrieved.
616      *
617      * @return bool
618      */
619     public function afterGetProperties($uri, &$properties) {
620
621         // If the request was made using the SOGO connector, we must rewrite
622         // the content-type property. By default SabreDAV will send back
623         // text/x-vcard; charset=utf-8, but for SOGO we must strip that last
624         // part.
625         if (!isset($properties[200]['{DAV:}getcontenttype']))
626             return;
627
628         if (strpos($this->server->httpRequest->getHeader('User-Agent'),'Thunderbird')===false) {
629             return;
630         }
631
632         if (strpos($properties[200]['{DAV:}getcontenttype'],'text/x-vcard')===0) {
633             $properties[200]['{DAV:}getcontenttype'] = 'text/x-vcard';
634         }
635
636     }
637
638     /**
639      * This method is used to generate HTML output for the
640      * Sabre_DAV_Browser_Plugin. This allows us to generate an interface users
641      * can use to create new calendars.
642      *
643      * @param Sabre_DAV_INode $node
644      * @param string $output
645      * @return bool
646      */
647     public function htmlActionsPanel(Sabre_DAV_INode $node, &$output) {
648
649         if (!$node instanceof Sabre_CardDAV_UserAddressBooks)
650             return;
651
652         $output.= '<tr><td colspan="2"><form method="post" action="">
653             <h3>Create new address book</h3>
654             <input type="hidden" name="sabreAction" value="mkaddressbook" />
655             <label>Name (uri):</label> <input type="text" name="name" /><br />
656             <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
657             <input type="submit" value="create" />
658             </form>
659             </td></tr>';
660
661         return false;
662
663     }
664
665     /**
666      * This method allows us to intercept the 'mkcalendar' sabreAction. This
667      * action enables the user to create new calendars from the browser plugin.
668      *
669      * @param string $uri
670      * @param string $action
671      * @param array $postVars
672      * @return bool
673      */
674     public function browserPostAction($uri, $action, array $postVars) {
675
676         if ($action!=='mkaddressbook')
677             return;
678
679         $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:carddav}addressbook');
680         $properties = array();
681         if (isset($postVars['{DAV:}displayname'])) {
682             $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
683         }
684         $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);
685         return false;
686
687     }
688
689 }