PageRenderTime 208ms CodeModel.GetById 121ms app.highlight 46ms RepoModel.GetById 34ms app.codeStats 1ms

/lib/Sabre/CardDAV/Plugin.php

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