| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886 |
- <?php
- declare(strict_types=1);
- namespace Sabre\CardDAV;
- use Sabre\DAV;
- use Sabre\DAV\Exception\ReportNotSupported;
- use Sabre\DAV\Xml\Property\LocalHref;
- use Sabre\DAVACL;
- use Sabre\HTTP;
- use Sabre\HTTP\RequestInterface;
- use Sabre\HTTP\ResponseInterface;
- use Sabre\Uri;
- use Sabre\VObject;
- /**
- * CardDAV plugin.
- *
- * The CardDAV plugin adds CardDAV functionality to the WebDAV server
- *
- * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
- * @author Evert Pot (http://evertpot.com/)
- * @license http://sabre.io/license/ Modified BSD License
- */
- class Plugin extends DAV\ServerPlugin
- {
- /**
- * Url to the addressbooks.
- */
- const ADDRESSBOOK_ROOT = 'addressbooks';
- /**
- * xml namespace for CardDAV elements.
- */
- const NS_CARDDAV = 'urn:ietf:params:xml:ns:carddav';
- /**
- * Add urls to this property to have them automatically exposed as
- * 'directories' to the user.
- *
- * @var array
- */
- public $directories = [];
- /**
- * Server class.
- *
- * @var DAV\Server
- */
- protected $server;
- /**
- * The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data,
- * which can hold up to 2^24 = 16777216 bytes. This is plenty. We're
- * capping it to 10M here.
- */
- protected $maxResourceSize = 10000000;
- /**
- * Initializes the plugin.
- */
- public function initialize(DAV\Server $server)
- {
- /* Events */
- $server->on('propFind', [$this, 'propFindEarly']);
- $server->on('propFind', [$this, 'propFindLate'], 150);
- $server->on('report', [$this, 'report']);
- $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
- $server->on('beforeWriteContent', [$this, 'beforeWriteContent']);
- $server->on('beforeCreateFile', [$this, 'beforeCreateFile']);
- $server->on('afterMethod:GET', [$this, 'httpAfterGet']);
- $server->xml->namespaceMap[self::NS_CARDDAV] = 'card';
- $server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-query'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport';
- $server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-multiget'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport';
- /* Mapping Interfaces to {DAV:}resourcetype values */
- $server->resourceTypeMapping['Sabre\\CardDAV\\IAddressBook'] = '{'.self::NS_CARDDAV.'}addressbook';
- $server->resourceTypeMapping['Sabre\\CardDAV\\IDirectory'] = '{'.self::NS_CARDDAV.'}directory';
- /* Adding properties that may never be changed */
- $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-address-data';
- $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}max-resource-size';
- $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}addressbook-home-set';
- $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-collation-set';
- $server->xml->elementMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre\\DAV\\Xml\\Property\\Href';
- $this->server = $server;
- }
- /**
- * Returns a list of supported features.
- *
- * This is used in the DAV: header in the OPTIONS and PROPFIND requests.
- *
- * @return array
- */
- public function getFeatures()
- {
- return ['addressbook'];
- }
- /**
- * Returns a list of reports this plugin supports.
- *
- * This will be used in the {DAV:}supported-report-set property.
- * Note that you still need to subscribe to the 'report' event to actually
- * implement them
- *
- * @param string $uri
- *
- * @return array
- */
- public function getSupportedReportSet($uri)
- {
- $node = $this->server->tree->getNodeForPath($uri);
- if ($node instanceof IAddressBook || $node instanceof ICard) {
- return [
- '{'.self::NS_CARDDAV.'}addressbook-multiget',
- '{'.self::NS_CARDDAV.'}addressbook-query',
- ];
- }
- return [];
- }
- /**
- * Adds all CardDAV-specific properties.
- */
- public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node)
- {
- $ns = '{'.self::NS_CARDDAV.'}';
- if ($node instanceof IAddressBook) {
- $propFind->handle($ns.'max-resource-size', $this->maxResourceSize);
- $propFind->handle($ns.'supported-address-data', function () {
- return new Xml\Property\SupportedAddressData();
- });
- $propFind->handle($ns.'supported-collation-set', function () {
- return new Xml\Property\SupportedCollationSet();
- });
- }
- if ($node instanceof DAVACL\IPrincipal) {
- $path = $propFind->getPath();
- $propFind->handle('{'.self::NS_CARDDAV.'}addressbook-home-set', function () use ($path) {
- return new LocalHref($this->getAddressBookHomeForPrincipal($path).'/');
- });
- if ($this->directories) {
- $propFind->handle('{'.self::NS_CARDDAV.'}directory-gateway', function () {
- return new LocalHref($this->directories);
- });
- }
- }
- if ($node instanceof ICard) {
- // The address-data property is not supposed to be a 'real'
- // property, but in large chunks of the spec it does act as such.
- // Therefore we simply expose it as a property.
- $propFind->handle('{'.self::NS_CARDDAV.'}address-data', function () use ($node) {
- $val = $node->get();
- if (is_resource($val)) {
- $val = stream_get_contents($val);
- }
- return $val;
- });
- }
- }
- /**
- * This functions handles REPORT requests specific to CardDAV.
- *
- * @param string $reportName
- * @param \DOMNode $dom
- * @param mixed $path
- *
- * @return bool
- */
- public function report($reportName, $dom, $path)
- {
- switch ($reportName) {
- case '{'.self::NS_CARDDAV.'}addressbook-multiget':
- $this->server->transactionType = 'report-addressbook-multiget';
- $this->addressbookMultiGetReport($dom);
- return false;
- case '{'.self::NS_CARDDAV.'}addressbook-query':
- $this->server->transactionType = 'report-addressbook-query';
- $this->addressBookQueryReport($dom);
- return false;
- default:
- return;
- }
- }
- /**
- * Returns the addressbook home for a given principal.
- *
- * @param string $principal
- *
- * @return string
- */
- protected function getAddressbookHomeForPrincipal($principal)
- {
- list(, $principalId) = Uri\split($principal);
- return self::ADDRESSBOOK_ROOT.'/'.$principalId;
- }
- /**
- * This function handles the addressbook-multiget REPORT.
- *
- * This report is used by the client to fetch the content of a series
- * of urls. Effectively avoiding a lot of redundant requests.
- *
- * @param Xml\Request\AddressBookMultiGetReport $report
- */
- public function addressbookMultiGetReport($report)
- {
- $contentType = $report->contentType;
- $version = $report->version;
- if ($version) {
- $contentType .= '; version='.$version;
- }
- $vcardType = $this->negotiateVCard(
- $contentType
- );
- $propertyList = [];
- $paths = array_map(
- [$this->server, 'calculateUri'],
- $report->hrefs
- );
- foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $props) {
- if (isset($props['200']['{'.self::NS_CARDDAV.'}address-data'])) {
- $props['200']['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard(
- $props[200]['{'.self::NS_CARDDAV.'}address-data'],
- $vcardType
- );
- }
- $propertyList[] = $props;
- }
- $prefer = $this->server->getHTTPPrefer();
- $this->server->httpResponse->setStatus(207);
- $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
- $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
- $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return']));
- }
- /**
- * This method is triggered before a file gets updated with new content.
- *
- * This plugin uses this method to ensure that Card nodes receive valid
- * vcard data.
- *
- * @param string $path
- * @param resource $data
- * @param bool $modified should be set to true, if this event handler
- * changed &$data
- */
- public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified)
- {
- if (!$node instanceof ICard) {
- return;
- }
- $this->validateVCard($data, $modified);
- }
- /**
- * This method is triggered before a new file is created.
- *
- * This plugin uses this method to ensure that Card nodes receive valid
- * vcard data.
- *
- * @param string $path
- * @param resource $data
- * @param bool $modified should be set to true, if this event handler
- * changed &$data
- */
- public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified)
- {
- if (!$parentNode instanceof IAddressBook) {
- return;
- }
- $this->validateVCard($data, $modified);
- }
- /**
- * Checks if the submitted iCalendar data is in fact, valid.
- *
- * An exception is thrown if it's not.
- *
- * @param resource|string $data
- * @param bool $modified should be set to true, if this event handler
- * changed &$data
- */
- protected function validateVCard(&$data, &$modified)
- {
- // If it's a stream, we convert it to a string first.
- if (is_resource($data)) {
- $data = stream_get_contents($data);
- }
- $before = $data;
- try {
- // If the data starts with a [, we can reasonably assume we're dealing
- // with a jCal object.
- if ('[' === substr($data, 0, 1)) {
- $vobj = VObject\Reader::readJson($data);
- // Converting $data back to iCalendar, as that's what we
- // technically support everywhere.
- $data = $vobj->serialize();
- $modified = true;
- } else {
- $vobj = VObject\Reader::read($data);
- }
- } catch (VObject\ParseException $e) {
- throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vCard or jCard data. Parse error: '.$e->getMessage());
- }
- if ('VCARD' !== $vobj->name) {
- throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
- }
- $options = VObject\Node::PROFILE_CARDDAV;
- $prefer = $this->server->getHTTPPrefer();
- if ('strict' !== $prefer['handling']) {
- $options |= VObject\Node::REPAIR;
- }
- $messages = $vobj->validate($options);
- $highestLevel = 0;
- $warningMessage = null;
- // $messages contains a list of problems with the vcard, along with
- // their severity.
- foreach ($messages as $message) {
- if ($message['level'] > $highestLevel) {
- // Recording the highest reported error level.
- $highestLevel = $message['level'];
- $warningMessage = $message['message'];
- }
- switch ($message['level']) {
- case 1:
- // Level 1 means that there was a problem, but it was repaired.
- $modified = true;
- break;
- case 2:
- // Level 2 means a warning, but not critical
- break;
- case 3:
- // Level 3 means a critical error
- throw new DAV\Exception\UnsupportedMediaType('Validation error in vCard: '.$message['message']);
- }
- }
- if ($warningMessage) {
- $this->server->httpResponse->setHeader(
- 'X-Sabre-Ew-Gross',
- 'vCard validation warning: '.$warningMessage
- );
- // Re-serializing object.
- $data = $vobj->serialize();
- if (!$modified && 0 !== strcmp($data, $before)) {
- // This ensures that the system does not send an ETag back.
- $modified = true;
- }
- }
- // Destroy circular references to PHP will GC the object.
- $vobj->destroy();
- }
- /**
- * This function handles the addressbook-query REPORT.
- *
- * This report is used by the client to filter an addressbook based on a
- * complex query.
- *
- * @param Xml\Request\AddressBookQueryReport $report
- */
- protected function addressbookQueryReport($report)
- {
- $depth = $this->server->getHTTPDepth(0);
- if (0 == $depth) {
- $candidateNodes = [
- $this->server->tree->getNodeForPath($this->server->getRequestUri()),
- ];
- if (!$candidateNodes[0] instanceof ICard) {
- throw new ReportNotSupported('The addressbook-query report is not supported on this url with Depth: 0');
- }
- } else {
- $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
- }
- $contentType = $report->contentType;
- if ($report->version) {
- $contentType .= '; version='.$report->version;
- }
- $vcardType = $this->negotiateVCard(
- $contentType
- );
- $validNodes = [];
- foreach ($candidateNodes as $node) {
- if (!$node instanceof ICard) {
- continue;
- }
- $blob = $node->get();
- if (is_resource($blob)) {
- $blob = stream_get_contents($blob);
- }
- if (!$this->validateFilters($blob, $report->filters, $report->test)) {
- continue;
- }
- $validNodes[] = $node;
- if ($report->limit && $report->limit <= count($validNodes)) {
- // We hit the maximum number of items, we can stop now.
- break;
- }
- }
- $result = [];
- foreach ($validNodes as $validNode) {
- if (0 == $depth) {
- $href = $this->server->getRequestUri();
- } else {
- $href = $this->server->getRequestUri().'/'.$validNode->getName();
- }
- list($props) = $this->server->getPropertiesForPath($href, $report->properties, 0);
- if (isset($props[200]['{'.self::NS_CARDDAV.'}address-data'])) {
- $props[200]['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard(
- $props[200]['{'.self::NS_CARDDAV.'}address-data'],
- $vcardType,
- $report->addressDataProperties
- );
- }
- $result[] = $props;
- }
- $prefer = $this->server->getHTTPPrefer();
- $this->server->httpResponse->setStatus(207);
- $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
- $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
- $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return']));
- }
- /**
- * Validates if a vcard makes it throught a list of filters.
- *
- * @param string $vcardData
- * @param string $test anyof or allof (which means OR or AND)
- *
- * @return bool
- */
- public function validateFilters($vcardData, array $filters, $test)
- {
- if (!$filters) {
- return true;
- }
- $vcard = VObject\Reader::read($vcardData);
- foreach ($filters as $filter) {
- $isDefined = isset($vcard->{$filter['name']});
- if ($filter['is-not-defined']) {
- if ($isDefined) {
- $success = false;
- } else {
- $success = true;
- }
- } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
- // We only need to check for existence
- $success = $isDefined;
- } else {
- $vProperties = $vcard->select($filter['name']);
- $results = [];
- if ($filter['param-filters']) {
- $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
- }
- if ($filter['text-matches']) {
- $texts = [];
- foreach ($vProperties as $vProperty) {
- $texts[] = $vProperty->getValue();
- }
- $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
- }
- if (1 === count($results)) {
- $success = $results[0];
- } else {
- if ('anyof' === $filter['test']) {
- $success = $results[0] || $results[1];
- } else {
- $success = $results[0] && $results[1];
- }
- }
- } // else
- // There are two conditions where we can already determine whether
- // or not this filter succeeds.
- if ('anyof' === $test && $success) {
- // Destroy circular references to PHP will GC the object.
- $vcard->destroy();
- return true;
- }
- if ('allof' === $test && !$success) {
- // Destroy circular references to PHP will GC the object.
- $vcard->destroy();
- return false;
- }
- } // foreach
- // Destroy circular references to PHP will GC the object.
- $vcard->destroy();
- // If we got all the way here, it means we haven't been able to
- // determine early if the test failed or not.
- //
- // This implies for 'anyof' that the test failed, and for 'allof' that
- // we succeeded. Sounds weird, but makes sense.
- return 'allof' === $test;
- }
- /**
- * Validates if a param-filter can be applied to a specific property.
- *
- * @todo currently we're only validating the first parameter of the passed
- * property. Any subsequence parameters with the same name are
- * ignored.
- *
- * @param string $test
- *
- * @return bool
- */
- protected function validateParamFilters(array $vProperties, array $filters, $test)
- {
- foreach ($filters as $filter) {
- $isDefined = false;
- foreach ($vProperties as $vProperty) {
- $isDefined = isset($vProperty[$filter['name']]);
- if ($isDefined) {
- break;
- }
- }
- if ($filter['is-not-defined']) {
- if ($isDefined) {
- $success = false;
- } else {
- $success = true;
- }
- // If there's no text-match, we can just check for existence
- } elseif (!$filter['text-match'] || !$isDefined) {
- $success = $isDefined;
- } else {
- $success = false;
- foreach ($vProperties as $vProperty) {
- // If we got all the way here, we'll need to validate the
- // text-match filter.
- if (isset($vProperty[$filter['name']])) {
- $success = DAV\StringUtil::textMatch(
- $vProperty[$filter['name']]->getValue(),
- $filter['text-match']['value'],
- $filter['text-match']['collation'],
- $filter['text-match']['match-type']
- );
- if ($filter['text-match']['negate-condition']) {
- $success = !$success;
- }
- }
- if ($success) {
- break;
- }
- }
- } // else
- // There are two conditions where we can already determine whether
- // or not this filter succeeds.
- if ('anyof' === $test && $success) {
- return true;
- }
- if ('allof' === $test && !$success) {
- return false;
- }
- }
- // If we got all the way here, it means we haven't been able to
- // determine early if the test failed or not.
- //
- // This implies for 'anyof' that the test failed, and for 'allof' that
- // we succeeded. Sounds weird, but makes sense.
- return 'allof' === $test;
- }
- /**
- * Validates if a text-filter can be applied to a specific property.
- *
- * @param string $test
- *
- * @return bool
- */
- protected function validateTextMatches(array $texts, array $filters, $test)
- {
- foreach ($filters as $filter) {
- $success = false;
- foreach ($texts as $haystack) {
- $success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
- if ($filter['negate-condition']) {
- $success = !$success;
- }
- // Breaking on the first match
- if ($success) {
- break;
- }
- }
- if ($success && 'anyof' === $test) {
- return true;
- }
- if (!$success && 'allof' == $test) {
- return false;
- }
- }
- // If we got all the way here, it means we haven't been able to
- // determine early if the test failed or not.
- //
- // This implies for 'anyof' that the test failed, and for 'allof' that
- // we succeeded. Sounds weird, but makes sense.
- return 'allof' === $test;
- }
- /**
- * This event is triggered when fetching properties.
- *
- * This event is scheduled late in the process, after most work for
- * propfind has been done.
- */
- public function propFindLate(DAV\PropFind $propFind, DAV\INode $node)
- {
- // If the request was made using the SOGO connector, we must rewrite
- // the content-type property. By default SabreDAV will send back
- // text/x-vcard; charset=utf-8, but for SOGO we must strip that last
- // part.
- if (false === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird')) {
- return;
- }
- $contentType = $propFind->get('{DAV:}getcontenttype');
- if (null !== $contentType) {
- list($part) = explode(';', $contentType);
- if ('text/x-vcard' === $part || 'text/vcard' === $part) {
- $propFind->set('{DAV:}getcontenttype', 'text/x-vcard');
- }
- }
- }
- /**
- * This method is used to generate HTML output for the
- * Sabre\DAV\Browser\Plugin. This allows us to generate an interface users
- * can use to create new addressbooks.
- *
- * @param string $output
- *
- * @return bool
- */
- public function htmlActionsPanel(DAV\INode $node, &$output)
- {
- if (!$node instanceof AddressBookHome) {
- return;
- }
- $output .= '<tr><td colspan="2"><form method="post" action="">
- <h3>Create new address book</h3>
- <input type="hidden" name="sabreAction" value="mkcol" />
- <input type="hidden" name="resourceType" value="{DAV:}collection,{'.self::NS_CARDDAV.'}addressbook" />
- <label>Name (uri):</label> <input type="text" name="name" /><br />
- <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
- <input type="submit" value="create" />
- </form>
- </td></tr>';
- return false;
- }
- /**
- * This event is triggered after GET requests.
- *
- * This is used to transform data into jCal, if this was requested.
- */
- public function httpAfterGet(RequestInterface $request, ResponseInterface $response)
- {
- $contentType = $response->getHeader('Content-Type');
- if (null === $contentType || false === strpos($contentType, 'text/vcard')) {
- return;
- }
- $target = $this->negotiateVCard($request->getHeader('Accept'), $mimeType);
- $newBody = $this->convertVCard(
- $response->getBody(),
- $target
- );
- $response->setBody($newBody);
- $response->setHeader('Content-Type', $mimeType.'; charset=utf-8');
- $response->setHeader('Content-Length', strlen($newBody));
- }
- /**
- * This helper function performs the content-type negotiation for vcards.
- *
- * It will return one of the following strings:
- * 1. vcard3
- * 2. vcard4
- * 3. jcard
- *
- * It defaults to vcard3.
- *
- * @param string $input
- * @param string $mimeType
- *
- * @return string
- */
- protected function negotiateVCard($input, &$mimeType = null)
- {
- $result = HTTP\negotiateContentType(
- $input,
- [
- // Most often used mime-type. Version 3
- 'text/x-vcard',
- // The correct standard mime-type. Defaults to version 3 as
- // well.
- 'text/vcard',
- // vCard 4
- 'text/vcard; version=4.0',
- // vCard 3
- 'text/vcard; version=3.0',
- // jCard
- 'application/vcard+json',
- ]
- );
- $mimeType = $result;
- switch ($result) {
- default:
- case 'text/x-vcard':
- case 'text/vcard':
- case 'text/vcard; version=3.0':
- $mimeType = 'text/vcard';
- return 'vcard3';
- case 'text/vcard; version=4.0':
- return 'vcard4';
- case 'application/vcard+json':
- return 'jcard';
- // @codeCoverageIgnoreStart
- }
- // @codeCoverageIgnoreEnd
- }
- /**
- * Converts a vcard blob to a different version, or jcard.
- *
- * @param string|resource $data
- * @param string $target
- * @param array $propertiesFilter
- *
- * @return string
- */
- protected function convertVCard($data, $target, array $propertiesFilter = null)
- {
- if (is_resource($data)) {
- $data = stream_get_contents($data);
- }
- $input = VObject\Reader::read($data);
- if (!empty($propertiesFilter)) {
- $propertiesFilter = array_merge(['UID', 'VERSION', 'FN'], $propertiesFilter);
- $keys = array_unique(array_map(function ($child) {
- return $child->name;
- }, $input->children()));
- $keys = array_diff($keys, $propertiesFilter);
- foreach ($keys as $key) {
- unset($input->$key);
- }
- $data = $input->serialize();
- }
- $output = null;
- try {
- switch ($target) {
- default:
- case 'vcard3':
- if (VObject\Document::VCARD30 === $input->getDocumentType()) {
- // Do nothing
- return $data;
- }
- $output = $input->convert(VObject\Document::VCARD30);
- return $output->serialize();
- case 'vcard4':
- if (VObject\Document::VCARD40 === $input->getDocumentType()) {
- // Do nothing
- return $data;
- }
- $output = $input->convert(VObject\Document::VCARD40);
- return $output->serialize();
- case 'jcard':
- $output = $input->convert(VObject\Document::VCARD40);
- return json_encode($output);
- }
- } finally {
- // Destroy circular references to PHP will GC the object.
- $input->destroy();
- if (!is_null($output)) {
- $output->destroy();
- }
- }
- }
- /**
- * Returns a plugin name.
- *
- * Using this name other plugins will be able to access other plugins
- * using DAV\Server::getPlugin
- *
- * @return string
- */
- public function getPluginName()
- {
- return 'carddav';
- }
- /**
- * Returns a bunch of meta-data about the plugin.
- *
- * Providing this information is optional, and is mainly displayed by the
- * Browser plugin.
- *
- * The description key in the returned array may contain html and will not
- * be sanitized.
- *
- * @return array
- */
- public function getPluginInfo()
- {
- return [
- 'name' => $this->getPluginName(),
- 'description' => 'Adds support for CardDAV (rfc6352)',
- 'link' => 'http://sabre.io/dav/carddav/',
- ];
- }
- }
|