xref: /plugin/davcal/vendor/sabre/dav/lib/CardDAV/Plugin.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
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