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