]> git.mxchange.org Git - friendica-addons.git/blob - dav/SabreDAV/lib/Sabre/CardDAV/Plugin.php
Move friendica-specific parts into an own subdirectory
[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                 $returnedProperties[200][$addressDataProp] = $val;
158
159             }
160         }
161
162         if ($node instanceof Sabre_CardDAV_UserAddressBooks) {
163
164             $meCardProp = '{http://calendarserver.org/ns/}me-card';
165             if (in_array($meCardProp, $requestedProperties)) {
166
167                 $props = $this->server->getProperties($node->getOwner(), array('{http://sabredav.org/ns}vcard-url'));
168                 if (isset($props['{http://sabredav.org/ns}vcard-url'])) {
169
170                     $returnedProperties[200][$meCardProp] = new Sabre_DAV_Property_Href(
171                         $props['{http://sabredav.org/ns}vcard-url']
172                     );
173                     $pos = array_search($meCardProp, $requestedProperties);
174                     unset($requestedProperties[$pos]);
175
176                 }
177
178             }
179
180         }
181
182     }
183
184     /**
185      * This event is triggered when a PROPPATCH method is executed
186      *
187      * @param array $mutations
188      * @param array $result
189      * @param Sabre_DAV_INode $node
190      * @return bool
191      */
192     public function updateProperties(&$mutations, &$result, $node) {
193
194         if (!$node instanceof Sabre_CardDAV_UserAddressBooks) {
195             return true;
196         }
197
198         $meCard = '{http://calendarserver.org/ns/}me-card';
199
200         // The only property we care about
201         if (!isset($mutations[$meCard]))
202             return true;
203
204         $value = $mutations[$meCard];
205         unset($mutations[$meCard]);
206
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;
212             return false;
213         }
214
215         $innerResult = $this->server->updateProperties(
216             $node->getOwner(),
217             array(
218                 '{http://sabredav.org/ns}vcard-url' => $value,
219             )
220         );
221
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);
227             }
228
229         }
230
231         return $result;
232
233     }
234
235     /**
236      * This functions handles REPORT requests specific to CardDAV
237      *
238      * @param string $reportName
239      * @param DOMNode $dom
240      * @return bool
241      */
242     public function report($reportName,$dom) {
243
244         switch($reportName) {
245             case '{'.self::NS_CARDDAV.'}addressbook-multiget' :
246                 $this->addressbookMultiGetReport($dom);
247                 return false;
248             case '{'.self::NS_CARDDAV.'}addressbook-query' :
249                 $this->addressBookQueryReport($dom);
250                 return false;
251             default :
252                 return;
253
254         }
255
256
257     }
258
259     /**
260      * This function handles the addressbook-multiget REPORT.
261      *
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.
264      *
265      * @param DOMNode $dom
266      * @return void
267      */
268     public function addressbookMultiGetReport($dom) {
269
270         $properties = array_keys(Sabre_DAV_XMLUtil::parseProperties($dom->firstChild));
271
272         $hrefElems = $dom->getElementsByTagNameNS('DAV:','href');
273         $propertyList = array();
274
275         foreach($hrefElems as $elem) {
276
277             $uri = $this->server->calculateUri($elem->nodeValue);
278             list($propertyList[]) = $this->server->getPropertiesForPath($uri,$properties);
279
280         }
281
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));
285
286     }
287
288     /**
289      * This method is triggered before a file gets updated with new content.
290      *
291      * This plugin uses this method to ensure that Card nodes receive valid
292      * vcard data.
293      *
294      * @param string $path
295      * @param Sabre_DAV_IFile $node
296      * @param resource $data
297      * @return void
298      */
299     public function beforeWriteContent($path, Sabre_DAV_IFile $node, &$data) {
300
301         if (!$node instanceof Sabre_CardDAV_ICard)
302             return;
303
304         $this->validateVCard($data);
305
306     }
307
308     /**
309      * This method is triggered before a new file is created.
310      *
311      * This plugin uses this method to ensure that Card nodes receive valid
312      * vcard data.
313      *
314      * @param string $path
315      * @param resource $data
316      * @param Sabre_DAV_ICollection $parentNode
317      * @return void
318      */
319     public function beforeCreateFile($path, &$data, Sabre_DAV_ICollection $parentNode) {
320
321         if (!$parentNode instanceof Sabre_CardDAV_IAddressBook)
322             return;
323
324         $this->validateVCard($data);
325
326     }
327
328     /**
329      * Checks if the submitted iCalendar data is in fact, valid.
330      *
331      * An exception is thrown if it's not.
332      *
333      * @param resource|string $data
334      * @return void
335      */
336     protected function validateVCard(&$data) {
337
338         // If it's a stream, we convert it to a string first.
339         if (is_resource($data)) {
340             $data = stream_get_contents($data);
341         }
342
343         // Converting the data to unicode, if needed.
344         $data = Sabre_DAV_StringUtil::ensureUTF8($data);
345
346         try {
347
348             $vobj = Sabre_VObject_Reader::read($data);
349
350         } catch (Sabre_VObject_ParseException $e) {
351
352             throw new Sabre_DAV_Exception_UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage());
353
354         }
355
356         if ($vobj->name !== 'VCARD') {
357             throw new Sabre_DAV_Exception_UnsupportedMediaType('This collection can only support vcard objects.');
358         }
359
360         if (!isset($vobj->UID)) {
361             throw new Sabre_DAV_Exception_BadRequest('Every vcard must have an UID.');
362         }
363
364     }
365
366
367     /**
368      * This function handles the addressbook-query REPORT
369      *
370      * This report is used by the client to filter an addressbook based on a
371      * complex query.
372      *
373      * @param DOMNode $dom
374      * @return void
375      */
376     protected function addressbookQueryReport($dom) {
377
378         $query = new Sabre_CardDAV_AddressBookQueryParser($dom);
379         $query->parse();
380
381         $depth = $this->server->getHTTPDepth(0);
382
383         if ($depth==0) {
384             $candidateNodes = array(
385                 $this->server->tree->getNodeForPath($this->server->getRequestUri())
386             );
387         } else {
388             $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
389         }
390
391         $validNodes = array();
392         foreach($candidateNodes as $node) {
393
394             if (!$node instanceof Sabre_CardDAV_ICard)
395                 continue;
396
397             $blob = $node->get();
398             if (is_resource($blob)) {
399                 $blob = stream_get_contents($blob);
400             }
401
402             if (!$this->validateFilters($blob, $query->filters, $query->test)) {
403                 continue;
404             }
405
406             $validNodes[] = $node;
407
408             if ($query->limit && $query->limit <= count($validNodes)) {
409                 // We hit the maximum number of items, we can stop now.
410                 break;
411             }
412
413         }
414
415         $result = array();
416         foreach($validNodes as $validNode) {
417
418             if ($depth==0) {
419                 $href = $this->server->getRequestUri();
420             } else {
421                 $href = $this->server->getRequestUri() . '/' . $validNode->getName();
422             }
423
424             list($result[]) = $this->server->getPropertiesForPath($href, $query->requestedProperties, 0);
425
426         }
427
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));
431
432     }
433
434     /**
435      * Validates if a vcard makes it throught a list of filters.
436      *
437      * @param string $vcardData
438      * @param array $filters
439      * @param string $test anyof or allof (which means OR or AND)
440      * @return bool
441      */
442     public function validateFilters($vcardData, array $filters, $test) {
443
444         $vcard = Sabre_VObject_Reader::read($vcardData);
445
446         if (!$filters) return true;
447
448         foreach($filters as $filter) {
449
450             $isDefined = isset($vcard->{$filter['name']});
451             if ($filter['is-not-defined']) {
452                 if ($isDefined) {
453                     $success = false;
454                 } else {
455                     $success = true;
456                 }
457             } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
458
459                 // We only need to check for existence
460                 $success = $isDefined;
461
462             } else {
463
464                 $vProperties = $vcard->select($filter['name']);
465
466                 $results = array();
467                 if ($filter['param-filters']) {
468                     $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
469                 }
470                 if ($filter['text-matches']) {
471                     $texts = array();
472                     foreach($vProperties as $vProperty)
473                         $texts[] = $vProperty->value;
474
475                     $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
476                 }
477
478                 if (count($results)===1) {
479                     $success = $results[0];
480                 } else {
481                     if ($filter['test'] === 'anyof') {
482                         $success = $results[0] || $results[1];
483                     } else {
484                         $success = $results[0] && $results[1];
485                     }
486                 }
487
488             } // else
489
490             // There are two conditions where we can already determine whether
491             // or not this filter succeeds.
492             if ($test==='anyof' && $success) {
493                 return true;
494             }
495             if ($test==='allof' && !$success) {
496                 return false;
497             }
498
499         } // foreach
500
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.
503         //
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';
507
508     }
509
510     /**
511      * Validates if a param-filter can be applied to a specific property.
512      *
513      * @todo currently we're only validating the first parameter of the passed
514      *       property. Any subsequence parameters with the same name are
515      *       ignored.
516      * @param array $vProperties
517      * @param array $filters
518      * @param string $test
519      * @return bool
520      */
521     protected function validateParamFilters(array $vProperties, array $filters, $test) {
522
523         foreach($filters as $filter) {
524
525             $isDefined = false;
526             foreach($vProperties as $vProperty) {
527                 $isDefined = isset($vProperty[$filter['name']]);
528                 if ($isDefined) break;
529             }
530
531             if ($filter['is-not-defined']) {
532                 if ($isDefined) {
533                     $success = false;
534                 } else {
535                     $success = true;
536                 }
537
538             // If there's no text-match, we can just check for existence
539             } elseif (!$filter['text-match'] || !$isDefined) {
540
541                 $success = $isDefined;
542
543             } else {
544
545                 $success = false;
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']);
550                     if ($success) break;
551                 }
552                 if ($filter['text-match']['negate-condition']) {
553                     $success = !$success;
554                 }
555
556             } // else
557
558             // There are two conditions where we can already determine whether
559             // or not this filter succeeds.
560             if ($test==='anyof' && $success) {
561                 return true;
562             }
563             if ($test==='allof' && !$success) {
564                 return false;
565             }
566
567         }
568
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.
571         //
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';
575
576     }
577
578     /**
579      * Validates if a text-filter can be applied to a specific property.
580      *
581      * @param array $texts
582      * @param array $filters
583      * @param string $test
584      * @return bool
585      */
586     protected function validateTextMatches(array $texts, array $filters, $test) {
587
588         foreach($filters as $filter) {
589
590             $success = false;
591             foreach($texts as $haystack) {
592                 $success = Sabre_DAV_StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
593
594                 // Breaking on the first match
595                 if ($success) break;
596             }
597             if ($filter['negate-condition']) {
598                 $success = !$success;
599             }
600
601             if ($success && $test==='anyof')
602                 return true;
603
604             if (!$success && $test=='allof')
605                 return false;
606
607
608         }
609
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.
612         //
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';
616
617     }
618
619     /**
620      * This event is triggered after webdav-properties have been retrieved.
621      *
622      * @return bool
623      */
624     public function afterGetProperties($uri, &$properties) {
625
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
629         // part.
630         if (!isset($properties[200]['{DAV:}getcontenttype']))
631             return;
632
633         if (strpos($this->server->httpRequest->getHeader('User-Agent'),'Thunderbird')===false) {
634             return;
635         }
636
637         if (strpos($properties[200]['{DAV:}getcontenttype'],'text/x-vcard')===0) {
638             $properties[200]['{DAV:}getcontenttype'] = 'text/x-vcard';
639         }
640
641     }
642
643     /**
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.
647      *
648      * @param Sabre_DAV_INode $node
649      * @param string $output
650      * @return bool
651      */
652     public function htmlActionsPanel(Sabre_DAV_INode $node, &$output) {
653
654         if (!$node instanceof Sabre_CardDAV_UserAddressBooks)
655             return;
656
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" />
663             </form>
664             </td></tr>';
665
666         return false;
667
668     }
669
670     /**
671      * This method allows us to intercept the 'mkcalendar' sabreAction. This
672      * action enables the user to create new calendars from the browser plugin.
673      *
674      * @param string $uri
675      * @param string $action
676      * @param array $postVars
677      * @return bool
678      */
679     public function browserPostAction($uri, $action, array $postVars) {
680
681         if ($action!=='mkaddressbook')
682             return;
683
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'];
688         }
689         $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);
690         return false;
691
692     }
693
694 }