1*a1a3b679SAndreas Boehler<?php 2*a1a3b679SAndreas Boehler 3*a1a3b679SAndreas Boehlernamespace Sabre\CardDAV; 4*a1a3b679SAndreas Boehler 5*a1a3b679SAndreas Boehleruse Sabre\DAV; 6*a1a3b679SAndreas Boehleruse Sabre\DAV\Exception\ReportNotSupported; 7*a1a3b679SAndreas Boehleruse Sabre\DAV\Xml\Property\Href; 8*a1a3b679SAndreas Boehleruse Sabre\DAVACL; 9*a1a3b679SAndreas Boehleruse Sabre\HTTP; 10*a1a3b679SAndreas Boehleruse Sabre\HTTP\RequestInterface; 11*a1a3b679SAndreas Boehleruse Sabre\HTTP\ResponseInterface; 12*a1a3b679SAndreas Boehleruse Sabre\VObject; 13*a1a3b679SAndreas Boehler 14*a1a3b679SAndreas Boehler/** 15*a1a3b679SAndreas Boehler * CardDAV plugin 16*a1a3b679SAndreas Boehler * 17*a1a3b679SAndreas Boehler * The CardDAV plugin adds CardDAV functionality to the WebDAV server 18*a1a3b679SAndreas Boehler * 19*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). 20*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/) 21*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License 22*a1a3b679SAndreas Boehler */ 23*a1a3b679SAndreas Boehlerclass Plugin extends DAV\ServerPlugin { 24*a1a3b679SAndreas Boehler 25*a1a3b679SAndreas Boehler /** 26*a1a3b679SAndreas Boehler * Url to the addressbooks 27*a1a3b679SAndreas Boehler */ 28*a1a3b679SAndreas Boehler const ADDRESSBOOK_ROOT = 'addressbooks'; 29*a1a3b679SAndreas Boehler 30*a1a3b679SAndreas Boehler /** 31*a1a3b679SAndreas Boehler * xml namespace for CardDAV elements 32*a1a3b679SAndreas Boehler */ 33*a1a3b679SAndreas Boehler const NS_CARDDAV = 'urn:ietf:params:xml:ns:carddav'; 34*a1a3b679SAndreas Boehler 35*a1a3b679SAndreas Boehler /** 36*a1a3b679SAndreas Boehler * Add urls to this property to have them automatically exposed as 37*a1a3b679SAndreas Boehler * 'directories' to the user. 38*a1a3b679SAndreas Boehler * 39*a1a3b679SAndreas Boehler * @var array 40*a1a3b679SAndreas Boehler */ 41*a1a3b679SAndreas Boehler public $directories = []; 42*a1a3b679SAndreas Boehler 43*a1a3b679SAndreas Boehler /** 44*a1a3b679SAndreas Boehler * Server class 45*a1a3b679SAndreas Boehler * 46*a1a3b679SAndreas Boehler * @var Sabre\DAV\Server 47*a1a3b679SAndreas Boehler */ 48*a1a3b679SAndreas Boehler protected $server; 49*a1a3b679SAndreas Boehler 50*a1a3b679SAndreas Boehler /** 51*a1a3b679SAndreas Boehler * The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data, 52*a1a3b679SAndreas Boehler * which can hold up to 2^24 = 16777216 bytes. This is plenty. We're 53*a1a3b679SAndreas Boehler * capping it to 10M here. 54*a1a3b679SAndreas Boehler */ 55*a1a3b679SAndreas Boehler protected $maxResourceSize = 10000000; 56*a1a3b679SAndreas Boehler 57*a1a3b679SAndreas Boehler /** 58*a1a3b679SAndreas Boehler * Initializes the plugin 59*a1a3b679SAndreas Boehler * 60*a1a3b679SAndreas Boehler * @param DAV\Server $server 61*a1a3b679SAndreas Boehler * @return void 62*a1a3b679SAndreas Boehler */ 63*a1a3b679SAndreas Boehler function initialize(DAV\Server $server) { 64*a1a3b679SAndreas Boehler 65*a1a3b679SAndreas Boehler /* Events */ 66*a1a3b679SAndreas Boehler $server->on('propFind', [$this, 'propFindEarly']); 67*a1a3b679SAndreas Boehler $server->on('propFind', [$this, 'propFindLate'], 150); 68*a1a3b679SAndreas Boehler $server->on('report', [$this, 'report']); 69*a1a3b679SAndreas Boehler $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); 70*a1a3b679SAndreas Boehler $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); 71*a1a3b679SAndreas Boehler $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); 72*a1a3b679SAndreas Boehler $server->on('afterMethod:GET', [$this, 'httpAfterGet']); 73*a1a3b679SAndreas Boehler 74*a1a3b679SAndreas Boehler $server->xml->namespaceMap[self::NS_CARDDAV] = 'card'; 75*a1a3b679SAndreas Boehler 76*a1a3b679SAndreas Boehler $server->xml->elementMap['{' . self::NS_CARDDAV . '}addressbook-query'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport'; 77*a1a3b679SAndreas Boehler $server->xml->elementMap['{' . self::NS_CARDDAV . '}addressbook-multiget'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport'; 78*a1a3b679SAndreas Boehler 79*a1a3b679SAndreas Boehler /* Mapping Interfaces to {DAV:}resourcetype values */ 80*a1a3b679SAndreas Boehler $server->resourceTypeMapping['Sabre\\CardDAV\\IAddressBook'] = '{' . self::NS_CARDDAV . '}addressbook'; 81*a1a3b679SAndreas Boehler $server->resourceTypeMapping['Sabre\\CardDAV\\IDirectory'] = '{' . self::NS_CARDDAV . '}directory'; 82*a1a3b679SAndreas Boehler 83*a1a3b679SAndreas Boehler /* Adding properties that may never be changed */ 84*a1a3b679SAndreas Boehler $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}supported-address-data'; 85*a1a3b679SAndreas Boehler $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}max-resource-size'; 86*a1a3b679SAndreas Boehler $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}addressbook-home-set'; 87*a1a3b679SAndreas Boehler $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}supported-collation-set'; 88*a1a3b679SAndreas Boehler 89*a1a3b679SAndreas Boehler $server->xml->elementMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre\\DAV\\Xml\\Property\\Href'; 90*a1a3b679SAndreas Boehler 91*a1a3b679SAndreas Boehler $this->server = $server; 92*a1a3b679SAndreas Boehler 93*a1a3b679SAndreas Boehler } 94*a1a3b679SAndreas Boehler 95*a1a3b679SAndreas Boehler /** 96*a1a3b679SAndreas Boehler * Returns a list of supported features. 97*a1a3b679SAndreas Boehler * 98*a1a3b679SAndreas Boehler * This is used in the DAV: header in the OPTIONS and PROPFIND requests. 99*a1a3b679SAndreas Boehler * 100*a1a3b679SAndreas Boehler * @return array 101*a1a3b679SAndreas Boehler */ 102*a1a3b679SAndreas Boehler function getFeatures() { 103*a1a3b679SAndreas Boehler 104*a1a3b679SAndreas Boehler return ['addressbook']; 105*a1a3b679SAndreas Boehler 106*a1a3b679SAndreas Boehler } 107*a1a3b679SAndreas Boehler 108*a1a3b679SAndreas Boehler /** 109*a1a3b679SAndreas Boehler * Returns a list of reports this plugin supports. 110*a1a3b679SAndreas Boehler * 111*a1a3b679SAndreas Boehler * This will be used in the {DAV:}supported-report-set property. 112*a1a3b679SAndreas Boehler * Note that you still need to subscribe to the 'report' event to actually 113*a1a3b679SAndreas Boehler * implement them 114*a1a3b679SAndreas Boehler * 115*a1a3b679SAndreas Boehler * @param string $uri 116*a1a3b679SAndreas Boehler * @return array 117*a1a3b679SAndreas Boehler */ 118*a1a3b679SAndreas Boehler function getSupportedReportSet($uri) { 119*a1a3b679SAndreas Boehler 120*a1a3b679SAndreas Boehler $node = $this->server->tree->getNodeForPath($uri); 121*a1a3b679SAndreas Boehler if ($node instanceof IAddressBook || $node instanceof ICard) { 122*a1a3b679SAndreas Boehler return [ 123*a1a3b679SAndreas Boehler '{' . self::NS_CARDDAV . '}addressbook-multiget', 124*a1a3b679SAndreas Boehler '{' . self::NS_CARDDAV . '}addressbook-query', 125*a1a3b679SAndreas Boehler ]; 126*a1a3b679SAndreas Boehler } 127*a1a3b679SAndreas Boehler return []; 128*a1a3b679SAndreas Boehler 129*a1a3b679SAndreas Boehler } 130*a1a3b679SAndreas Boehler 131*a1a3b679SAndreas Boehler 132*a1a3b679SAndreas Boehler /** 133*a1a3b679SAndreas Boehler * Adds all CardDAV-specific properties 134*a1a3b679SAndreas Boehler * 135*a1a3b679SAndreas Boehler * @param DAV\PropFind $propFind 136*a1a3b679SAndreas Boehler * @param DAV\INode $node 137*a1a3b679SAndreas Boehler * @return void 138*a1a3b679SAndreas Boehler */ 139*a1a3b679SAndreas Boehler function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) { 140*a1a3b679SAndreas Boehler 141*a1a3b679SAndreas Boehler $ns = '{' . self::NS_CARDDAV . '}'; 142*a1a3b679SAndreas Boehler 143*a1a3b679SAndreas Boehler if ($node instanceof IAddressBook) { 144*a1a3b679SAndreas Boehler 145*a1a3b679SAndreas Boehler $propFind->handle($ns . 'max-resource-size', $this->maxResourceSize); 146*a1a3b679SAndreas Boehler $propFind->handle($ns . 'supported-address-data', function() { 147*a1a3b679SAndreas Boehler return new Xml\Property\SupportedAddressData(); 148*a1a3b679SAndreas Boehler }); 149*a1a3b679SAndreas Boehler $propFind->handle($ns . 'supported-collation-set', function() { 150*a1a3b679SAndreas Boehler return new Xml\Property\SupportedCollationSet(); 151*a1a3b679SAndreas Boehler }); 152*a1a3b679SAndreas Boehler 153*a1a3b679SAndreas Boehler } 154*a1a3b679SAndreas Boehler if ($node instanceof DAVACL\IPrincipal) { 155*a1a3b679SAndreas Boehler 156*a1a3b679SAndreas Boehler $path = $propFind->getPath(); 157*a1a3b679SAndreas Boehler 158*a1a3b679SAndreas Boehler $propFind->handle('{' . self::NS_CARDDAV . '}addressbook-home-set', function() use ($path) { 159*a1a3b679SAndreas Boehler return new Href($this->getAddressBookHomeForPrincipal($path) . '/'); 160*a1a3b679SAndreas Boehler }); 161*a1a3b679SAndreas Boehler 162*a1a3b679SAndreas Boehler if ($this->directories) $propFind->handle('{' . self::NS_CARDDAV . '}directory-gateway', function() { 163*a1a3b679SAndreas Boehler return new Href($this->directories); 164*a1a3b679SAndreas Boehler }); 165*a1a3b679SAndreas Boehler 166*a1a3b679SAndreas Boehler } 167*a1a3b679SAndreas Boehler 168*a1a3b679SAndreas Boehler if ($node instanceof ICard) { 169*a1a3b679SAndreas Boehler 170*a1a3b679SAndreas Boehler // The address-data property is not supposed to be a 'real' 171*a1a3b679SAndreas Boehler // property, but in large chunks of the spec it does act as such. 172*a1a3b679SAndreas Boehler // Therefore we simply expose it as a property. 173*a1a3b679SAndreas Boehler $propFind->handle('{' . self::NS_CARDDAV . '}address-data', function() use ($node) { 174*a1a3b679SAndreas Boehler $val = $node->get(); 175*a1a3b679SAndreas Boehler if (is_resource($val)) 176*a1a3b679SAndreas Boehler $val = stream_get_contents($val); 177*a1a3b679SAndreas Boehler 178*a1a3b679SAndreas Boehler return $val; 179*a1a3b679SAndreas Boehler 180*a1a3b679SAndreas Boehler }); 181*a1a3b679SAndreas Boehler 182*a1a3b679SAndreas Boehler } 183*a1a3b679SAndreas Boehler 184*a1a3b679SAndreas Boehler } 185*a1a3b679SAndreas Boehler 186*a1a3b679SAndreas Boehler /** 187*a1a3b679SAndreas Boehler * This functions handles REPORT requests specific to CardDAV 188*a1a3b679SAndreas Boehler * 189*a1a3b679SAndreas Boehler * @param string $reportName 190*a1a3b679SAndreas Boehler * @param \DOMNode $dom 191*a1a3b679SAndreas Boehler * @return bool 192*a1a3b679SAndreas Boehler */ 193*a1a3b679SAndreas Boehler function report($reportName, $dom) { 194*a1a3b679SAndreas Boehler 195*a1a3b679SAndreas Boehler switch ($reportName) { 196*a1a3b679SAndreas Boehler case '{' . self::NS_CARDDAV . '}addressbook-multiget' : 197*a1a3b679SAndreas Boehler $this->server->transactionType = 'report-addressbook-multiget'; 198*a1a3b679SAndreas Boehler $this->addressbookMultiGetReport($dom); 199*a1a3b679SAndreas Boehler return false; 200*a1a3b679SAndreas Boehler case '{' . self::NS_CARDDAV . '}addressbook-query' : 201*a1a3b679SAndreas Boehler $this->server->transactionType = 'report-addressbook-query'; 202*a1a3b679SAndreas Boehler $this->addressBookQueryReport($dom); 203*a1a3b679SAndreas Boehler return false; 204*a1a3b679SAndreas Boehler default : 205*a1a3b679SAndreas Boehler return; 206*a1a3b679SAndreas Boehler 207*a1a3b679SAndreas Boehler } 208*a1a3b679SAndreas Boehler 209*a1a3b679SAndreas Boehler 210*a1a3b679SAndreas Boehler } 211*a1a3b679SAndreas Boehler 212*a1a3b679SAndreas Boehler /** 213*a1a3b679SAndreas Boehler * Returns the addressbook home for a given principal 214*a1a3b679SAndreas Boehler * 215*a1a3b679SAndreas Boehler * @param string $principal 216*a1a3b679SAndreas Boehler * @return string 217*a1a3b679SAndreas Boehler */ 218*a1a3b679SAndreas Boehler protected function getAddressbookHomeForPrincipal($principal) { 219*a1a3b679SAndreas Boehler 220*a1a3b679SAndreas Boehler list(, $principalId) = \Sabre\HTTP\URLUtil::splitPath($principal); 221*a1a3b679SAndreas Boehler return self::ADDRESSBOOK_ROOT . '/' . $principalId; 222*a1a3b679SAndreas Boehler 223*a1a3b679SAndreas Boehler } 224*a1a3b679SAndreas Boehler 225*a1a3b679SAndreas Boehler 226*a1a3b679SAndreas Boehler /** 227*a1a3b679SAndreas Boehler * This function handles the addressbook-multiget REPORT. 228*a1a3b679SAndreas Boehler * 229*a1a3b679SAndreas Boehler * This report is used by the client to fetch the content of a series 230*a1a3b679SAndreas Boehler * of urls. Effectively avoiding a lot of redundant requests. 231*a1a3b679SAndreas Boehler * 232*a1a3b679SAndreas Boehler * @param Xml\Request\AddressBookMultiGetReport $report 233*a1a3b679SAndreas Boehler * @return void 234*a1a3b679SAndreas Boehler */ 235*a1a3b679SAndreas Boehler function addressbookMultiGetReport($report) { 236*a1a3b679SAndreas Boehler 237*a1a3b679SAndreas Boehler $contentType = $report->contentType; 238*a1a3b679SAndreas Boehler $version = $report->version; 239*a1a3b679SAndreas Boehler if ($version) { 240*a1a3b679SAndreas Boehler $contentType .= '; version=' . $version; 241*a1a3b679SAndreas Boehler } 242*a1a3b679SAndreas Boehler 243*a1a3b679SAndreas Boehler $vcardType = $this->negotiateVCard( 244*a1a3b679SAndreas Boehler $contentType 245*a1a3b679SAndreas Boehler ); 246*a1a3b679SAndreas Boehler 247*a1a3b679SAndreas Boehler $propertyList = []; 248*a1a3b679SAndreas Boehler $paths = array_map( 249*a1a3b679SAndreas Boehler [$this->server, 'calculateUri'], 250*a1a3b679SAndreas Boehler $report->hrefs 251*a1a3b679SAndreas Boehler ); 252*a1a3b679SAndreas Boehler foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $props) { 253*a1a3b679SAndreas Boehler 254*a1a3b679SAndreas Boehler if (isset($props['200']['{' . self::NS_CARDDAV . '}address-data'])) { 255*a1a3b679SAndreas Boehler 256*a1a3b679SAndreas Boehler $props['200']['{' . self::NS_CARDDAV . '}address-data'] = $this->convertVCard( 257*a1a3b679SAndreas Boehler $props[200]['{' . self::NS_CARDDAV . '}address-data'], 258*a1a3b679SAndreas Boehler $vcardType 259*a1a3b679SAndreas Boehler ); 260*a1a3b679SAndreas Boehler 261*a1a3b679SAndreas Boehler } 262*a1a3b679SAndreas Boehler $propertyList[] = $props; 263*a1a3b679SAndreas Boehler 264*a1a3b679SAndreas Boehler } 265*a1a3b679SAndreas Boehler 266*a1a3b679SAndreas Boehler $prefer = $this->server->getHTTPPrefer(); 267*a1a3b679SAndreas Boehler 268*a1a3b679SAndreas Boehler $this->server->httpResponse->setStatus(207); 269*a1a3b679SAndreas Boehler $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); 270*a1a3b679SAndreas Boehler $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); 271*a1a3b679SAndreas Boehler $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, $prefer['return'] === 'minimal')); 272*a1a3b679SAndreas Boehler 273*a1a3b679SAndreas Boehler } 274*a1a3b679SAndreas Boehler 275*a1a3b679SAndreas Boehler /** 276*a1a3b679SAndreas Boehler * This method is triggered before a file gets updated with new content. 277*a1a3b679SAndreas Boehler * 278*a1a3b679SAndreas Boehler * This plugin uses this method to ensure that Card nodes receive valid 279*a1a3b679SAndreas Boehler * vcard data. 280*a1a3b679SAndreas Boehler * 281*a1a3b679SAndreas Boehler * @param string $path 282*a1a3b679SAndreas Boehler * @param DAV\IFile $node 283*a1a3b679SAndreas Boehler * @param resource $data 284*a1a3b679SAndreas Boehler * @param bool $modified Should be set to true, if this event handler 285*a1a3b679SAndreas Boehler * changed &$data. 286*a1a3b679SAndreas Boehler * @return void 287*a1a3b679SAndreas Boehler */ 288*a1a3b679SAndreas Boehler function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) { 289*a1a3b679SAndreas Boehler 290*a1a3b679SAndreas Boehler if (!$node instanceof ICard) 291*a1a3b679SAndreas Boehler return; 292*a1a3b679SAndreas Boehler 293*a1a3b679SAndreas Boehler $this->validateVCard($data, $modified); 294*a1a3b679SAndreas Boehler 295*a1a3b679SAndreas Boehler } 296*a1a3b679SAndreas Boehler 297*a1a3b679SAndreas Boehler /** 298*a1a3b679SAndreas Boehler * This method is triggered before a new file is created. 299*a1a3b679SAndreas Boehler * 300*a1a3b679SAndreas Boehler * This plugin uses this method to ensure that Card nodes receive valid 301*a1a3b679SAndreas Boehler * vcard data. 302*a1a3b679SAndreas Boehler * 303*a1a3b679SAndreas Boehler * @param string $path 304*a1a3b679SAndreas Boehler * @param resource $data 305*a1a3b679SAndreas Boehler * @param DAV\ICollection $parentNode 306*a1a3b679SAndreas Boehler * @param bool $modified Should be set to true, if this event handler 307*a1a3b679SAndreas Boehler * changed &$data. 308*a1a3b679SAndreas Boehler * @return void 309*a1a3b679SAndreas Boehler */ 310*a1a3b679SAndreas Boehler function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) { 311*a1a3b679SAndreas Boehler 312*a1a3b679SAndreas Boehler if (!$parentNode instanceof IAddressBook) 313*a1a3b679SAndreas Boehler return; 314*a1a3b679SAndreas Boehler 315*a1a3b679SAndreas Boehler $this->validateVCard($data, $modified); 316*a1a3b679SAndreas Boehler 317*a1a3b679SAndreas Boehler } 318*a1a3b679SAndreas Boehler 319*a1a3b679SAndreas Boehler /** 320*a1a3b679SAndreas Boehler * Checks if the submitted iCalendar data is in fact, valid. 321*a1a3b679SAndreas Boehler * 322*a1a3b679SAndreas Boehler * An exception is thrown if it's not. 323*a1a3b679SAndreas Boehler * 324*a1a3b679SAndreas Boehler * @param resource|string $data 325*a1a3b679SAndreas Boehler * @param bool $modified Should be set to true, if this event handler 326*a1a3b679SAndreas Boehler * changed &$data. 327*a1a3b679SAndreas Boehler * @return void 328*a1a3b679SAndreas Boehler */ 329*a1a3b679SAndreas Boehler protected function validateVCard(&$data, &$modified) { 330*a1a3b679SAndreas Boehler 331*a1a3b679SAndreas Boehler // If it's a stream, we convert it to a string first. 332*a1a3b679SAndreas Boehler if (is_resource($data)) { 333*a1a3b679SAndreas Boehler $data = stream_get_contents($data); 334*a1a3b679SAndreas Boehler } 335*a1a3b679SAndreas Boehler 336*a1a3b679SAndreas Boehler $before = md5($data); 337*a1a3b679SAndreas Boehler 338*a1a3b679SAndreas Boehler // Converting the data to unicode, if needed. 339*a1a3b679SAndreas Boehler $data = DAV\StringUtil::ensureUTF8($data); 340*a1a3b679SAndreas Boehler 341*a1a3b679SAndreas Boehler if (md5($data) !== $before) $modified = true; 342*a1a3b679SAndreas Boehler 343*a1a3b679SAndreas Boehler try { 344*a1a3b679SAndreas Boehler 345*a1a3b679SAndreas Boehler // If the data starts with a [, we can reasonably assume we're dealing 346*a1a3b679SAndreas Boehler // with a jCal object. 347*a1a3b679SAndreas Boehler if (substr($data, 0, 1) === '[') { 348*a1a3b679SAndreas Boehler $vobj = VObject\Reader::readJson($data); 349*a1a3b679SAndreas Boehler 350*a1a3b679SAndreas Boehler // Converting $data back to iCalendar, as that's what we 351*a1a3b679SAndreas Boehler // technically support everywhere. 352*a1a3b679SAndreas Boehler $data = $vobj->serialize(); 353*a1a3b679SAndreas Boehler $modified = true; 354*a1a3b679SAndreas Boehler } else { 355*a1a3b679SAndreas Boehler $vobj = VObject\Reader::read($data); 356*a1a3b679SAndreas Boehler } 357*a1a3b679SAndreas Boehler 358*a1a3b679SAndreas Boehler } catch (VObject\ParseException $e) { 359*a1a3b679SAndreas Boehler 360*a1a3b679SAndreas Boehler throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vCard or jCard data. Parse error: ' . $e->getMessage()); 361*a1a3b679SAndreas Boehler 362*a1a3b679SAndreas Boehler } 363*a1a3b679SAndreas Boehler 364*a1a3b679SAndreas Boehler if ($vobj->name !== 'VCARD') { 365*a1a3b679SAndreas Boehler throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.'); 366*a1a3b679SAndreas Boehler } 367*a1a3b679SAndreas Boehler 368*a1a3b679SAndreas Boehler if (!isset($vobj->UID)) { 369*a1a3b679SAndreas Boehler // No UID in vcards is invalid, but we'll just add it in anyway. 370*a1a3b679SAndreas Boehler $vobj->add('UID', DAV\UUIDUtil::getUUID()); 371*a1a3b679SAndreas Boehler $data = $vobj->serialize(); 372*a1a3b679SAndreas Boehler $modified = true; 373*a1a3b679SAndreas Boehler } 374*a1a3b679SAndreas Boehler 375*a1a3b679SAndreas Boehler } 376*a1a3b679SAndreas Boehler 377*a1a3b679SAndreas Boehler 378*a1a3b679SAndreas Boehler /** 379*a1a3b679SAndreas Boehler * This function handles the addressbook-query REPORT 380*a1a3b679SAndreas Boehler * 381*a1a3b679SAndreas Boehler * This report is used by the client to filter an addressbook based on a 382*a1a3b679SAndreas Boehler * complex query. 383*a1a3b679SAndreas Boehler * 384*a1a3b679SAndreas Boehler * @param Xml\Request\AddressBookQueryReport $report 385*a1a3b679SAndreas Boehler * @return void 386*a1a3b679SAndreas Boehler */ 387*a1a3b679SAndreas Boehler protected function addressbookQueryReport($report) { 388*a1a3b679SAndreas Boehler 389*a1a3b679SAndreas Boehler $depth = $this->server->getHTTPDepth(0); 390*a1a3b679SAndreas Boehler 391*a1a3b679SAndreas Boehler if ($depth == 0) { 392*a1a3b679SAndreas Boehler $candidateNodes = [ 393*a1a3b679SAndreas Boehler $this->server->tree->getNodeForPath($this->server->getRequestUri()) 394*a1a3b679SAndreas Boehler ]; 395*a1a3b679SAndreas Boehler if (!$candidateNodes[0] instanceof ICard) { 396*a1a3b679SAndreas Boehler throw new ReportNotSupported('The addressbook-query report is not supported on this url with Depth: 0'); 397*a1a3b679SAndreas Boehler } 398*a1a3b679SAndreas Boehler } else { 399*a1a3b679SAndreas Boehler $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri()); 400*a1a3b679SAndreas Boehler } 401*a1a3b679SAndreas Boehler 402*a1a3b679SAndreas Boehler $contentType = $report->contentType; 403*a1a3b679SAndreas Boehler if ($report->version) { 404*a1a3b679SAndreas Boehler $contentType .= '; version=' . $report->version; 405*a1a3b679SAndreas Boehler } 406*a1a3b679SAndreas Boehler 407*a1a3b679SAndreas Boehler $vcardType = $this->negotiateVCard( 408*a1a3b679SAndreas Boehler $contentType 409*a1a3b679SAndreas Boehler ); 410*a1a3b679SAndreas Boehler 411*a1a3b679SAndreas Boehler $validNodes = []; 412*a1a3b679SAndreas Boehler foreach ($candidateNodes as $node) { 413*a1a3b679SAndreas Boehler 414*a1a3b679SAndreas Boehler if (!$node instanceof ICard) 415*a1a3b679SAndreas Boehler continue; 416*a1a3b679SAndreas Boehler 417*a1a3b679SAndreas Boehler $blob = $node->get(); 418*a1a3b679SAndreas Boehler if (is_resource($blob)) { 419*a1a3b679SAndreas Boehler $blob = stream_get_contents($blob); 420*a1a3b679SAndreas Boehler } 421*a1a3b679SAndreas Boehler 422*a1a3b679SAndreas Boehler if (!$this->validateFilters($blob, $report->filters, $report->test)) { 423*a1a3b679SAndreas Boehler continue; 424*a1a3b679SAndreas Boehler } 425*a1a3b679SAndreas Boehler 426*a1a3b679SAndreas Boehler $validNodes[] = $node; 427*a1a3b679SAndreas Boehler 428*a1a3b679SAndreas Boehler if ($report->limit && $report->limit <= count($validNodes)) { 429*a1a3b679SAndreas Boehler // We hit the maximum number of items, we can stop now. 430*a1a3b679SAndreas Boehler break; 431*a1a3b679SAndreas Boehler } 432*a1a3b679SAndreas Boehler 433*a1a3b679SAndreas Boehler } 434*a1a3b679SAndreas Boehler 435*a1a3b679SAndreas Boehler $result = []; 436*a1a3b679SAndreas Boehler foreach ($validNodes as $validNode) { 437*a1a3b679SAndreas Boehler 438*a1a3b679SAndreas Boehler if ($depth == 0) { 439*a1a3b679SAndreas Boehler $href = $this->server->getRequestUri(); 440*a1a3b679SAndreas Boehler } else { 441*a1a3b679SAndreas Boehler $href = $this->server->getRequestUri() . '/' . $validNode->getName(); 442*a1a3b679SAndreas Boehler } 443*a1a3b679SAndreas Boehler 444*a1a3b679SAndreas Boehler list($props) = $this->server->getPropertiesForPath($href, $report->properties, 0); 445*a1a3b679SAndreas Boehler 446*a1a3b679SAndreas Boehler if (isset($props[200]['{' . self::NS_CARDDAV . '}address-data'])) { 447*a1a3b679SAndreas Boehler 448*a1a3b679SAndreas Boehler $props[200]['{' . self::NS_CARDDAV . '}address-data'] = $this->convertVCard( 449*a1a3b679SAndreas Boehler $props[200]['{' . self::NS_CARDDAV . '}address-data'], 450*a1a3b679SAndreas Boehler $vcardType 451*a1a3b679SAndreas Boehler ); 452*a1a3b679SAndreas Boehler 453*a1a3b679SAndreas Boehler } 454*a1a3b679SAndreas Boehler $result[] = $props; 455*a1a3b679SAndreas Boehler 456*a1a3b679SAndreas Boehler } 457*a1a3b679SAndreas Boehler 458*a1a3b679SAndreas Boehler $prefer = $this->server->getHTTPPrefer(); 459*a1a3b679SAndreas Boehler 460*a1a3b679SAndreas Boehler $this->server->httpResponse->setStatus(207); 461*a1a3b679SAndreas Boehler $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); 462*a1a3b679SAndreas Boehler $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); 463*a1a3b679SAndreas Boehler $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return'] === 'minimal')); 464*a1a3b679SAndreas Boehler 465*a1a3b679SAndreas Boehler } 466*a1a3b679SAndreas Boehler 467*a1a3b679SAndreas Boehler /** 468*a1a3b679SAndreas Boehler * Validates if a vcard makes it throught a list of filters. 469*a1a3b679SAndreas Boehler * 470*a1a3b679SAndreas Boehler * @param string $vcardData 471*a1a3b679SAndreas Boehler * @param array $filters 472*a1a3b679SAndreas Boehler * @param string $test anyof or allof (which means OR or AND) 473*a1a3b679SAndreas Boehler * @return bool 474*a1a3b679SAndreas Boehler */ 475*a1a3b679SAndreas Boehler function validateFilters($vcardData, array $filters, $test) { 476*a1a3b679SAndreas Boehler 477*a1a3b679SAndreas Boehler $vcard = VObject\Reader::read($vcardData); 478*a1a3b679SAndreas Boehler 479*a1a3b679SAndreas Boehler if (!$filters) return true; 480*a1a3b679SAndreas Boehler 481*a1a3b679SAndreas Boehler foreach ($filters as $filter) { 482*a1a3b679SAndreas Boehler 483*a1a3b679SAndreas Boehler $isDefined = isset($vcard->{$filter['name']}); 484*a1a3b679SAndreas Boehler if ($filter['is-not-defined']) { 485*a1a3b679SAndreas Boehler if ($isDefined) { 486*a1a3b679SAndreas Boehler $success = false; 487*a1a3b679SAndreas Boehler } else { 488*a1a3b679SAndreas Boehler $success = true; 489*a1a3b679SAndreas Boehler } 490*a1a3b679SAndreas Boehler } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) { 491*a1a3b679SAndreas Boehler 492*a1a3b679SAndreas Boehler // We only need to check for existence 493*a1a3b679SAndreas Boehler $success = $isDefined; 494*a1a3b679SAndreas Boehler 495*a1a3b679SAndreas Boehler } else { 496*a1a3b679SAndreas Boehler 497*a1a3b679SAndreas Boehler $vProperties = $vcard->select($filter['name']); 498*a1a3b679SAndreas Boehler 499*a1a3b679SAndreas Boehler $results = []; 500*a1a3b679SAndreas Boehler if ($filter['param-filters']) { 501*a1a3b679SAndreas Boehler $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']); 502*a1a3b679SAndreas Boehler } 503*a1a3b679SAndreas Boehler if ($filter['text-matches']) { 504*a1a3b679SAndreas Boehler $texts = []; 505*a1a3b679SAndreas Boehler foreach ($vProperties as $vProperty) 506*a1a3b679SAndreas Boehler $texts[] = $vProperty->getValue(); 507*a1a3b679SAndreas Boehler 508*a1a3b679SAndreas Boehler $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']); 509*a1a3b679SAndreas Boehler } 510*a1a3b679SAndreas Boehler 511*a1a3b679SAndreas Boehler if (count($results) === 1) { 512*a1a3b679SAndreas Boehler $success = $results[0]; 513*a1a3b679SAndreas Boehler } else { 514*a1a3b679SAndreas Boehler if ($filter['test'] === 'anyof') { 515*a1a3b679SAndreas Boehler $success = $results[0] || $results[1]; 516*a1a3b679SAndreas Boehler } else { 517*a1a3b679SAndreas Boehler $success = $results[0] && $results[1]; 518*a1a3b679SAndreas Boehler } 519*a1a3b679SAndreas Boehler } 520*a1a3b679SAndreas Boehler 521*a1a3b679SAndreas Boehler } // else 522*a1a3b679SAndreas Boehler 523*a1a3b679SAndreas Boehler // There are two conditions where we can already determine whether 524*a1a3b679SAndreas Boehler // or not this filter succeeds. 525*a1a3b679SAndreas Boehler if ($test === 'anyof' && $success) { 526*a1a3b679SAndreas Boehler return true; 527*a1a3b679SAndreas Boehler } 528*a1a3b679SAndreas Boehler if ($test === 'allof' && !$success) { 529*a1a3b679SAndreas Boehler return false; 530*a1a3b679SAndreas Boehler } 531*a1a3b679SAndreas Boehler 532*a1a3b679SAndreas Boehler } // foreach 533*a1a3b679SAndreas Boehler 534*a1a3b679SAndreas Boehler // If we got all the way here, it means we haven't been able to 535*a1a3b679SAndreas Boehler // determine early if the test failed or not. 536*a1a3b679SAndreas Boehler // 537*a1a3b679SAndreas Boehler // This implies for 'anyof' that the test failed, and for 'allof' that 538*a1a3b679SAndreas Boehler // we succeeded. Sounds weird, but makes sense. 539*a1a3b679SAndreas Boehler return $test === 'allof'; 540*a1a3b679SAndreas Boehler 541*a1a3b679SAndreas Boehler } 542*a1a3b679SAndreas Boehler 543*a1a3b679SAndreas Boehler /** 544*a1a3b679SAndreas Boehler * Validates if a param-filter can be applied to a specific property. 545*a1a3b679SAndreas Boehler * 546*a1a3b679SAndreas Boehler * @todo currently we're only validating the first parameter of the passed 547*a1a3b679SAndreas Boehler * property. Any subsequence parameters with the same name are 548*a1a3b679SAndreas Boehler * ignored. 549*a1a3b679SAndreas Boehler * @param array $vProperties 550*a1a3b679SAndreas Boehler * @param array $filters 551*a1a3b679SAndreas Boehler * @param string $test 552*a1a3b679SAndreas Boehler * @return bool 553*a1a3b679SAndreas Boehler */ 554*a1a3b679SAndreas Boehler protected function validateParamFilters(array $vProperties, array $filters, $test) { 555*a1a3b679SAndreas Boehler 556*a1a3b679SAndreas Boehler foreach ($filters as $filter) { 557*a1a3b679SAndreas Boehler 558*a1a3b679SAndreas Boehler $isDefined = false; 559*a1a3b679SAndreas Boehler foreach ($vProperties as $vProperty) { 560*a1a3b679SAndreas Boehler $isDefined = isset($vProperty[$filter['name']]); 561*a1a3b679SAndreas Boehler if ($isDefined) break; 562*a1a3b679SAndreas Boehler } 563*a1a3b679SAndreas Boehler 564*a1a3b679SAndreas Boehler if ($filter['is-not-defined']) { 565*a1a3b679SAndreas Boehler if ($isDefined) { 566*a1a3b679SAndreas Boehler $success = false; 567*a1a3b679SAndreas Boehler } else { 568*a1a3b679SAndreas Boehler $success = true; 569*a1a3b679SAndreas Boehler } 570*a1a3b679SAndreas Boehler 571*a1a3b679SAndreas Boehler // If there's no text-match, we can just check for existence 572*a1a3b679SAndreas Boehler } elseif (!$filter['text-match'] || !$isDefined) { 573*a1a3b679SAndreas Boehler 574*a1a3b679SAndreas Boehler $success = $isDefined; 575*a1a3b679SAndreas Boehler 576*a1a3b679SAndreas Boehler } else { 577*a1a3b679SAndreas Boehler 578*a1a3b679SAndreas Boehler $success = false; 579*a1a3b679SAndreas Boehler foreach ($vProperties as $vProperty) { 580*a1a3b679SAndreas Boehler // If we got all the way here, we'll need to validate the 581*a1a3b679SAndreas Boehler // text-match filter. 582*a1a3b679SAndreas Boehler $success = DAV\StringUtil::textMatch($vProperty[$filter['name']]->getValue(), $filter['text-match']['value'], $filter['text-match']['collation'], $filter['text-match']['match-type']); 583*a1a3b679SAndreas Boehler if ($success) break; 584*a1a3b679SAndreas Boehler } 585*a1a3b679SAndreas Boehler if ($filter['text-match']['negate-condition']) { 586*a1a3b679SAndreas Boehler $success = !$success; 587*a1a3b679SAndreas Boehler } 588*a1a3b679SAndreas Boehler 589*a1a3b679SAndreas Boehler } // else 590*a1a3b679SAndreas Boehler 591*a1a3b679SAndreas Boehler // There are two conditions where we can already determine whether 592*a1a3b679SAndreas Boehler // or not this filter succeeds. 593*a1a3b679SAndreas Boehler if ($test === 'anyof' && $success) { 594*a1a3b679SAndreas Boehler return true; 595*a1a3b679SAndreas Boehler } 596*a1a3b679SAndreas Boehler if ($test === 'allof' && !$success) { 597*a1a3b679SAndreas Boehler return false; 598*a1a3b679SAndreas Boehler } 599*a1a3b679SAndreas Boehler 600*a1a3b679SAndreas Boehler } 601*a1a3b679SAndreas Boehler 602*a1a3b679SAndreas Boehler // If we got all the way here, it means we haven't been able to 603*a1a3b679SAndreas Boehler // determine early if the test failed or not. 604*a1a3b679SAndreas Boehler // 605*a1a3b679SAndreas Boehler // This implies for 'anyof' that the test failed, and for 'allof' that 606*a1a3b679SAndreas Boehler // we succeeded. Sounds weird, but makes sense. 607*a1a3b679SAndreas Boehler return $test === 'allof'; 608*a1a3b679SAndreas Boehler 609*a1a3b679SAndreas Boehler } 610*a1a3b679SAndreas Boehler 611*a1a3b679SAndreas Boehler /** 612*a1a3b679SAndreas Boehler * Validates if a text-filter can be applied to a specific property. 613*a1a3b679SAndreas Boehler * 614*a1a3b679SAndreas Boehler * @param array $texts 615*a1a3b679SAndreas Boehler * @param array $filters 616*a1a3b679SAndreas Boehler * @param string $test 617*a1a3b679SAndreas Boehler * @return bool 618*a1a3b679SAndreas Boehler */ 619*a1a3b679SAndreas Boehler protected function validateTextMatches(array $texts, array $filters, $test) { 620*a1a3b679SAndreas Boehler 621*a1a3b679SAndreas Boehler foreach ($filters as $filter) { 622*a1a3b679SAndreas Boehler 623*a1a3b679SAndreas Boehler $success = false; 624*a1a3b679SAndreas Boehler foreach ($texts as $haystack) { 625*a1a3b679SAndreas Boehler $success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']); 626*a1a3b679SAndreas Boehler 627*a1a3b679SAndreas Boehler // Breaking on the first match 628*a1a3b679SAndreas Boehler if ($success) break; 629*a1a3b679SAndreas Boehler } 630*a1a3b679SAndreas Boehler if ($filter['negate-condition']) { 631*a1a3b679SAndreas Boehler $success = !$success; 632*a1a3b679SAndreas Boehler } 633*a1a3b679SAndreas Boehler 634*a1a3b679SAndreas Boehler if ($success && $test === 'anyof') 635*a1a3b679SAndreas Boehler return true; 636*a1a3b679SAndreas Boehler 637*a1a3b679SAndreas Boehler if (!$success && $test == 'allof') 638*a1a3b679SAndreas Boehler return false; 639*a1a3b679SAndreas Boehler 640*a1a3b679SAndreas Boehler 641*a1a3b679SAndreas Boehler } 642*a1a3b679SAndreas Boehler 643*a1a3b679SAndreas Boehler // If we got all the way here, it means we haven't been able to 644*a1a3b679SAndreas Boehler // determine early if the test failed or not. 645*a1a3b679SAndreas Boehler // 646*a1a3b679SAndreas Boehler // This implies for 'anyof' that the test failed, and for 'allof' that 647*a1a3b679SAndreas Boehler // we succeeded. Sounds weird, but makes sense. 648*a1a3b679SAndreas Boehler return $test === 'allof'; 649*a1a3b679SAndreas Boehler 650*a1a3b679SAndreas Boehler } 651*a1a3b679SAndreas Boehler 652*a1a3b679SAndreas Boehler /** 653*a1a3b679SAndreas Boehler * This event is triggered when fetching properties. 654*a1a3b679SAndreas Boehler * 655*a1a3b679SAndreas Boehler * This event is scheduled late in the process, after most work for 656*a1a3b679SAndreas Boehler * propfind has been done. 657*a1a3b679SAndreas Boehler * 658*a1a3b679SAndreas Boehler * @param DAV\PropFind $propFind 659*a1a3b679SAndreas Boehler * @param DAV\INode $node 660*a1a3b679SAndreas Boehler * @return void 661*a1a3b679SAndreas Boehler */ 662*a1a3b679SAndreas Boehler function propFindLate(DAV\PropFind $propFind, DAV\INode $node) { 663*a1a3b679SAndreas Boehler 664*a1a3b679SAndreas Boehler // If the request was made using the SOGO connector, we must rewrite 665*a1a3b679SAndreas Boehler // the content-type property. By default SabreDAV will send back 666*a1a3b679SAndreas Boehler // text/x-vcard; charset=utf-8, but for SOGO we must strip that last 667*a1a3b679SAndreas Boehler // part. 668*a1a3b679SAndreas Boehler if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird') === false) { 669*a1a3b679SAndreas Boehler return; 670*a1a3b679SAndreas Boehler } 671*a1a3b679SAndreas Boehler $contentType = $propFind->get('{DAV:}getcontenttype'); 672*a1a3b679SAndreas Boehler list($part) = explode(';', $contentType); 673*a1a3b679SAndreas Boehler if ($part === 'text/x-vcard' || $part === 'text/vcard') { 674*a1a3b679SAndreas Boehler $propFind->set('{DAV:}getcontenttype', 'text/x-vcard'); 675*a1a3b679SAndreas Boehler } 676*a1a3b679SAndreas Boehler 677*a1a3b679SAndreas Boehler } 678*a1a3b679SAndreas Boehler 679*a1a3b679SAndreas Boehler /** 680*a1a3b679SAndreas Boehler * This method is used to generate HTML output for the 681*a1a3b679SAndreas Boehler * Sabre\DAV\Browser\Plugin. This allows us to generate an interface users 682*a1a3b679SAndreas Boehler * can use to create new addressbooks. 683*a1a3b679SAndreas Boehler * 684*a1a3b679SAndreas Boehler * @param DAV\INode $node 685*a1a3b679SAndreas Boehler * @param string $output 686*a1a3b679SAndreas Boehler * @return bool 687*a1a3b679SAndreas Boehler */ 688*a1a3b679SAndreas Boehler function htmlActionsPanel(DAV\INode $node, &$output) { 689*a1a3b679SAndreas Boehler 690*a1a3b679SAndreas Boehler if (!$node instanceof AddressBookHome) 691*a1a3b679SAndreas Boehler return; 692*a1a3b679SAndreas Boehler 693*a1a3b679SAndreas Boehler $output .= '<tr><td colspan="2"><form method="post" action=""> 694*a1a3b679SAndreas Boehler <h3>Create new address book</h3> 695*a1a3b679SAndreas Boehler <input type="hidden" name="sabreAction" value="mkcol" /> 696*a1a3b679SAndreas Boehler <input type="hidden" name="resourceType" value="{DAV:}collection,{' . self::NS_CARDDAV . '}addressbook" /> 697*a1a3b679SAndreas Boehler <label>Name (uri):</label> <input type="text" name="name" /><br /> 698*a1a3b679SAndreas Boehler <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br /> 699*a1a3b679SAndreas Boehler <input type="submit" value="create" /> 700*a1a3b679SAndreas Boehler </form> 701*a1a3b679SAndreas Boehler </td></tr>'; 702*a1a3b679SAndreas Boehler 703*a1a3b679SAndreas Boehler return false; 704*a1a3b679SAndreas Boehler 705*a1a3b679SAndreas Boehler } 706*a1a3b679SAndreas Boehler 707*a1a3b679SAndreas Boehler /** 708*a1a3b679SAndreas Boehler * This event is triggered after GET requests. 709*a1a3b679SAndreas Boehler * 710*a1a3b679SAndreas Boehler * This is used to transform data into jCal, if this was requested. 711*a1a3b679SAndreas Boehler * 712*a1a3b679SAndreas Boehler * @param RequestInterface $request 713*a1a3b679SAndreas Boehler * @param ResponseInterface $response 714*a1a3b679SAndreas Boehler * @return void 715*a1a3b679SAndreas Boehler */ 716*a1a3b679SAndreas Boehler function httpAfterGet(RequestInterface $request, ResponseInterface $response) { 717*a1a3b679SAndreas Boehler 718*a1a3b679SAndreas Boehler if (strpos($response->getHeader('Content-Type'), 'text/vcard') === false) { 719*a1a3b679SAndreas Boehler return; 720*a1a3b679SAndreas Boehler } 721*a1a3b679SAndreas Boehler 722*a1a3b679SAndreas Boehler $target = $this->negotiateVCard($request->getHeader('Accept'), $mimeType); 723*a1a3b679SAndreas Boehler 724*a1a3b679SAndreas Boehler $newBody = $this->convertVCard( 725*a1a3b679SAndreas Boehler $response->getBody(), 726*a1a3b679SAndreas Boehler $target 727*a1a3b679SAndreas Boehler ); 728*a1a3b679SAndreas Boehler 729*a1a3b679SAndreas Boehler $response->setBody($newBody); 730*a1a3b679SAndreas Boehler $response->setHeader('Content-Type', $mimeType . '; charset=utf-8'); 731*a1a3b679SAndreas Boehler $response->setHeader('Content-Length', strlen($newBody)); 732*a1a3b679SAndreas Boehler 733*a1a3b679SAndreas Boehler } 734*a1a3b679SAndreas Boehler 735*a1a3b679SAndreas Boehler /** 736*a1a3b679SAndreas Boehler * This helper function performs the content-type negotiation for vcards. 737*a1a3b679SAndreas Boehler * 738*a1a3b679SAndreas Boehler * It will return one of the following strings: 739*a1a3b679SAndreas Boehler * 1. vcard3 740*a1a3b679SAndreas Boehler * 2. vcard4 741*a1a3b679SAndreas Boehler * 3. jcard 742*a1a3b679SAndreas Boehler * 743*a1a3b679SAndreas Boehler * It defaults to vcard3. 744*a1a3b679SAndreas Boehler * 745*a1a3b679SAndreas Boehler * @param string $input 746*a1a3b679SAndreas Boehler * @param string $mimeType 747*a1a3b679SAndreas Boehler * @return string 748*a1a3b679SAndreas Boehler */ 749*a1a3b679SAndreas Boehler protected function negotiateVCard($input, &$mimeType = null) { 750*a1a3b679SAndreas Boehler 751*a1a3b679SAndreas Boehler $result = HTTP\Util::negotiate( 752*a1a3b679SAndreas Boehler $input, 753*a1a3b679SAndreas Boehler [ 754*a1a3b679SAndreas Boehler // Most often used mime-type. Version 3 755*a1a3b679SAndreas Boehler 'text/x-vcard', 756*a1a3b679SAndreas Boehler // The correct standard mime-type. Defaults to version 3 as 757*a1a3b679SAndreas Boehler // well. 758*a1a3b679SAndreas Boehler 'text/vcard', 759*a1a3b679SAndreas Boehler // vCard 4 760*a1a3b679SAndreas Boehler 'text/vcard; version=4.0', 761*a1a3b679SAndreas Boehler // vCard 3 762*a1a3b679SAndreas Boehler 'text/vcard; version=3.0', 763*a1a3b679SAndreas Boehler // jCard 764*a1a3b679SAndreas Boehler 'application/vcard+json', 765*a1a3b679SAndreas Boehler ] 766*a1a3b679SAndreas Boehler ); 767*a1a3b679SAndreas Boehler 768*a1a3b679SAndreas Boehler $mimeType = $result; 769*a1a3b679SAndreas Boehler switch ($result) { 770*a1a3b679SAndreas Boehler 771*a1a3b679SAndreas Boehler default : 772*a1a3b679SAndreas Boehler case 'text/x-vcard' : 773*a1a3b679SAndreas Boehler case 'text/vcard' : 774*a1a3b679SAndreas Boehler case 'text/vcard; version=3.0' : 775*a1a3b679SAndreas Boehler $mimeType = 'text/vcard'; 776*a1a3b679SAndreas Boehler return 'vcard3'; 777*a1a3b679SAndreas Boehler case 'text/vcard; version=4.0' : 778*a1a3b679SAndreas Boehler return 'vcard4'; 779*a1a3b679SAndreas Boehler case 'application/vcard+json' : 780*a1a3b679SAndreas Boehler return 'jcard'; 781*a1a3b679SAndreas Boehler 782*a1a3b679SAndreas Boehler // @codeCoverageIgnoreStart 783*a1a3b679SAndreas Boehler } 784*a1a3b679SAndreas Boehler // @codeCoverageIgnoreEnd 785*a1a3b679SAndreas Boehler 786*a1a3b679SAndreas Boehler } 787*a1a3b679SAndreas Boehler 788*a1a3b679SAndreas Boehler /** 789*a1a3b679SAndreas Boehler * Converts a vcard blob to a different version, or jcard. 790*a1a3b679SAndreas Boehler * 791*a1a3b679SAndreas Boehler * @param string $data 792*a1a3b679SAndreas Boehler * @param string $target 793*a1a3b679SAndreas Boehler * @return string 794*a1a3b679SAndreas Boehler */ 795*a1a3b679SAndreas Boehler protected function convertVCard($data, $target) { 796*a1a3b679SAndreas Boehler 797*a1a3b679SAndreas Boehler $data = VObject\Reader::read($data); 798*a1a3b679SAndreas Boehler switch ($target) { 799*a1a3b679SAndreas Boehler default : 800*a1a3b679SAndreas Boehler case 'vcard3' : 801*a1a3b679SAndreas Boehler $data = $data->convert(VObject\Document::VCARD30); 802*a1a3b679SAndreas Boehler return $data->serialize(); 803*a1a3b679SAndreas Boehler case 'vcard4' : 804*a1a3b679SAndreas Boehler $data = $data->convert(VObject\Document::VCARD40); 805*a1a3b679SAndreas Boehler return $data->serialize(); 806*a1a3b679SAndreas Boehler case 'jcard' : 807*a1a3b679SAndreas Boehler $data = $data->convert(VObject\Document::VCARD40); 808*a1a3b679SAndreas Boehler return json_encode($data->jsonSerialize()); 809*a1a3b679SAndreas Boehler 810*a1a3b679SAndreas Boehler // @codeCoverageIgnoreStart 811*a1a3b679SAndreas Boehler } 812*a1a3b679SAndreas Boehler // @codeCoverageIgnoreEnd 813*a1a3b679SAndreas Boehler 814*a1a3b679SAndreas Boehler } 815*a1a3b679SAndreas Boehler 816*a1a3b679SAndreas Boehler /** 817*a1a3b679SAndreas Boehler * Returns a plugin name. 818*a1a3b679SAndreas Boehler * 819*a1a3b679SAndreas Boehler * Using this name other plugins will be able to access other plugins 820*a1a3b679SAndreas Boehler * using DAV\Server::getPlugin 821*a1a3b679SAndreas Boehler * 822*a1a3b679SAndreas Boehler * @return string 823*a1a3b679SAndreas Boehler */ 824*a1a3b679SAndreas Boehler function getPluginName() { 825*a1a3b679SAndreas Boehler 826*a1a3b679SAndreas Boehler return 'carddav'; 827*a1a3b679SAndreas Boehler 828*a1a3b679SAndreas Boehler } 829*a1a3b679SAndreas Boehler 830*a1a3b679SAndreas Boehler /** 831*a1a3b679SAndreas Boehler * Returns a bunch of meta-data about the plugin. 832*a1a3b679SAndreas Boehler * 833*a1a3b679SAndreas Boehler * Providing this information is optional, and is mainly displayed by the 834*a1a3b679SAndreas Boehler * Browser plugin. 835*a1a3b679SAndreas Boehler * 836*a1a3b679SAndreas Boehler * The description key in the returned array may contain html and will not 837*a1a3b679SAndreas Boehler * be sanitized. 838*a1a3b679SAndreas Boehler * 839*a1a3b679SAndreas Boehler * @return array 840*a1a3b679SAndreas Boehler */ 841*a1a3b679SAndreas Boehler function getPluginInfo() { 842*a1a3b679SAndreas Boehler 843*a1a3b679SAndreas Boehler return [ 844*a1a3b679SAndreas Boehler 'name' => $this->getPluginName(), 845*a1a3b679SAndreas Boehler 'description' => 'Adds support for CardDAV (rfc6352)', 846*a1a3b679SAndreas Boehler 'link' => 'http://sabre.io/dav/carddav/', 847*a1a3b679SAndreas Boehler ]; 848*a1a3b679SAndreas Boehler 849*a1a3b679SAndreas Boehler } 850*a1a3b679SAndreas Boehler 851*a1a3b679SAndreas Boehler} 852