xref: /plugin/davcal/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
1*a1a3b679SAndreas Boehler<?php
2*a1a3b679SAndreas Boehler
3*a1a3b679SAndreas Boehlernamespace Sabre\CalDAV;
4*a1a3b679SAndreas Boehler
5*a1a3b679SAndreas Boehleruse DateTimeZone;
6*a1a3b679SAndreas Boehleruse Sabre\DAV;
7*a1a3b679SAndreas Boehleruse Sabre\VObject;
8*a1a3b679SAndreas Boehleruse Sabre\HTTP\RequestInterface;
9*a1a3b679SAndreas Boehleruse Sabre\HTTP\ResponseInterface;
10*a1a3b679SAndreas Boehleruse Sabre\DAV\Exception\BadRequest;
11*a1a3b679SAndreas Boehleruse DateTime;
12*a1a3b679SAndreas Boehler
13*a1a3b679SAndreas Boehler/**
14*a1a3b679SAndreas Boehler * ICS Exporter
15*a1a3b679SAndreas Boehler *
16*a1a3b679SAndreas Boehler * This plugin adds the ability to export entire calendars as .ics files.
17*a1a3b679SAndreas Boehler * This is useful for clients that don't support CalDAV yet. They often do
18*a1a3b679SAndreas Boehler * support ics files.
19*a1a3b679SAndreas Boehler *
20*a1a3b679SAndreas Boehler * To use this, point a http client to a caldav calendar, and add ?expand to
21*a1a3b679SAndreas Boehler * the url.
22*a1a3b679SAndreas Boehler *
23*a1a3b679SAndreas Boehler * Further options that can be added to the url:
24*a1a3b679SAndreas Boehler *   start=123456789 - Only return events after the given unix timestamp
25*a1a3b679SAndreas Boehler *   end=123245679   - Only return events from before the given unix timestamp
26*a1a3b679SAndreas Boehler *   expand=1        - Strip timezone information and expand recurring events.
27*a1a3b679SAndreas Boehler *                     If you'd like to expand, you _must_ also specify start
28*a1a3b679SAndreas Boehler *                     and end.
29*a1a3b679SAndreas Boehler *
30*a1a3b679SAndreas Boehler * By default this plugin returns data in the text/calendar format (iCalendar
31*a1a3b679SAndreas Boehler * 2.0). If you'd like to receive jCal data instead, you can use an Accept
32*a1a3b679SAndreas Boehler * header:
33*a1a3b679SAndreas Boehler *
34*a1a3b679SAndreas Boehler * Accept: application/calendar+json
35*a1a3b679SAndreas Boehler *
36*a1a3b679SAndreas Boehler * Alternatively, you can also specify this in the url using
37*a1a3b679SAndreas Boehler * accept=application/calendar+json, or accept=jcal for short. If the url
38*a1a3b679SAndreas Boehler * parameter and Accept header is specified, the url parameter wins.
39*a1a3b679SAndreas Boehler *
40*a1a3b679SAndreas Boehler * Note that specifying a start or end data implies that only events will be
41*a1a3b679SAndreas Boehler * returned. VTODO and VJOURNAL will be stripped.
42*a1a3b679SAndreas Boehler *
43*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
44*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/)
45*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License
46*a1a3b679SAndreas Boehler */
47*a1a3b679SAndreas Boehlerclass ICSExportPlugin extends DAV\ServerPlugin {
48*a1a3b679SAndreas Boehler
49*a1a3b679SAndreas Boehler    /**
50*a1a3b679SAndreas Boehler     * Reference to Server class
51*a1a3b679SAndreas Boehler     *
52*a1a3b679SAndreas Boehler     * @var \Sabre\DAV\Server
53*a1a3b679SAndreas Boehler     */
54*a1a3b679SAndreas Boehler    protected $server;
55*a1a3b679SAndreas Boehler
56*a1a3b679SAndreas Boehler    /**
57*a1a3b679SAndreas Boehler     * Initializes the plugin and registers event handlers
58*a1a3b679SAndreas Boehler     *
59*a1a3b679SAndreas Boehler     * @param \Sabre\DAV\Server $server
60*a1a3b679SAndreas Boehler     * @return void
61*a1a3b679SAndreas Boehler     */
62*a1a3b679SAndreas Boehler    function initialize(DAV\Server $server) {
63*a1a3b679SAndreas Boehler
64*a1a3b679SAndreas Boehler        $this->server = $server;
65*a1a3b679SAndreas Boehler        $server->on('method:GET', [$this, 'httpGet'], 90);
66*a1a3b679SAndreas Boehler        $server->on('browserButtonActions', function($path, $node, &$actions) {
67*a1a3b679SAndreas Boehler            if ($node instanceof ICalendar) {
68*a1a3b679SAndreas Boehler                $actions .= '<a href="' . htmlspecialchars($path, ENT_QUOTES, 'UTF-8') . '?export"><span class="oi" data-glyph="calendar"></span></a>';
69*a1a3b679SAndreas Boehler            }
70*a1a3b679SAndreas Boehler        });
71*a1a3b679SAndreas Boehler
72*a1a3b679SAndreas Boehler    }
73*a1a3b679SAndreas Boehler
74*a1a3b679SAndreas Boehler    /**
75*a1a3b679SAndreas Boehler     * Intercepts GET requests on calendar urls ending with ?export.
76*a1a3b679SAndreas Boehler     *
77*a1a3b679SAndreas Boehler     * @param RequestInterface $request
78*a1a3b679SAndreas Boehler     * @param ResponseInterface $response
79*a1a3b679SAndreas Boehler     * @return bool
80*a1a3b679SAndreas Boehler     */
81*a1a3b679SAndreas Boehler    function httpGet(RequestInterface $request, ResponseInterface $response) {
82*a1a3b679SAndreas Boehler
83*a1a3b679SAndreas Boehler        $queryParams = $request->getQueryParameters();
84*a1a3b679SAndreas Boehler        if (!array_key_exists('export', $queryParams)) return;
85*a1a3b679SAndreas Boehler
86*a1a3b679SAndreas Boehler        $path = $request->getPath();
87*a1a3b679SAndreas Boehler
88*a1a3b679SAndreas Boehler        $node = $this->server->getProperties($path, [
89*a1a3b679SAndreas Boehler            '{DAV:}resourcetype',
90*a1a3b679SAndreas Boehler            '{DAV:}displayname',
91*a1a3b679SAndreas Boehler            '{http://sabredav.org/ns}sync-token',
92*a1a3b679SAndreas Boehler            '{DAV:}sync-token',
93*a1a3b679SAndreas Boehler            '{http://apple.com/ns/ical/}calendar-color',
94*a1a3b679SAndreas Boehler        ]);
95*a1a3b679SAndreas Boehler
96*a1a3b679SAndreas Boehler        if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{' . Plugin::NS_CALDAV . '}calendar')) {
97*a1a3b679SAndreas Boehler            return;
98*a1a3b679SAndreas Boehler        }
99*a1a3b679SAndreas Boehler        // Marking the transactionType, for logging purposes.
100*a1a3b679SAndreas Boehler        $this->server->transactionType = 'get-calendar-export';
101*a1a3b679SAndreas Boehler
102*a1a3b679SAndreas Boehler        $properties = $node;
103*a1a3b679SAndreas Boehler
104*a1a3b679SAndreas Boehler        $start = null;
105*a1a3b679SAndreas Boehler        $end = null;
106*a1a3b679SAndreas Boehler        $expand = false;
107*a1a3b679SAndreas Boehler        $componentType = false;
108*a1a3b679SAndreas Boehler        if (isset($queryParams['start'])) {
109*a1a3b679SAndreas Boehler            if (!ctype_digit($queryParams['start'])) {
110*a1a3b679SAndreas Boehler                throw new BadRequest('The start= parameter must contain a unix timestamp');
111*a1a3b679SAndreas Boehler            }
112*a1a3b679SAndreas Boehler            $start = DateTime::createFromFormat('U', $queryParams['start']);
113*a1a3b679SAndreas Boehler        }
114*a1a3b679SAndreas Boehler        if (isset($queryParams['end'])) {
115*a1a3b679SAndreas Boehler            if (!ctype_digit($queryParams['end'])) {
116*a1a3b679SAndreas Boehler                throw new BadRequest('The end= parameter must contain a unix timestamp');
117*a1a3b679SAndreas Boehler            }
118*a1a3b679SAndreas Boehler            $end = DateTime::createFromFormat('U', $queryParams['end']);
119*a1a3b679SAndreas Boehler        }
120*a1a3b679SAndreas Boehler        if (isset($queryParams['expand']) && !!$queryParams['expand']) {
121*a1a3b679SAndreas Boehler            if (!$start || !$end) {
122*a1a3b679SAndreas Boehler                throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.');
123*a1a3b679SAndreas Boehler            }
124*a1a3b679SAndreas Boehler            $expand = true;
125*a1a3b679SAndreas Boehler            $componentType = 'VEVENT';
126*a1a3b679SAndreas Boehler        }
127*a1a3b679SAndreas Boehler        if (isset($queryParams['componentType'])) {
128*a1a3b679SAndreas Boehler            if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) {
129*a1a3b679SAndreas Boehler                throw new BadRequest('You are not allowed to search for components of type: ' . $queryParams['componentType'] . ' here');
130*a1a3b679SAndreas Boehler            }
131*a1a3b679SAndreas Boehler            $componentType = $queryParams['componentType'];
132*a1a3b679SAndreas Boehler        }
133*a1a3b679SAndreas Boehler
134*a1a3b679SAndreas Boehler        $format = \Sabre\HTTP\Util::Negotiate(
135*a1a3b679SAndreas Boehler            $request->getHeader('Accept'),
136*a1a3b679SAndreas Boehler            [
137*a1a3b679SAndreas Boehler                'text/calendar',
138*a1a3b679SAndreas Boehler                'application/calendar+json',
139*a1a3b679SAndreas Boehler            ]
140*a1a3b679SAndreas Boehler        );
141*a1a3b679SAndreas Boehler
142*a1a3b679SAndreas Boehler        if (isset($queryParams['accept'])) {
143*a1a3b679SAndreas Boehler            if ($queryParams['accept'] === 'application/calendar+json' || $queryParams['accept'] === 'jcal') {
144*a1a3b679SAndreas Boehler                $format = 'application/calendar+json';
145*a1a3b679SAndreas Boehler            }
146*a1a3b679SAndreas Boehler        }
147*a1a3b679SAndreas Boehler        if (!$format) {
148*a1a3b679SAndreas Boehler            $format = 'text/calendar';
149*a1a3b679SAndreas Boehler        }
150*a1a3b679SAndreas Boehler
151*a1a3b679SAndreas Boehler        $this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response);
152*a1a3b679SAndreas Boehler
153*a1a3b679SAndreas Boehler        // Returning false to break the event chain
154*a1a3b679SAndreas Boehler        return false;
155*a1a3b679SAndreas Boehler
156*a1a3b679SAndreas Boehler    }
157*a1a3b679SAndreas Boehler
158*a1a3b679SAndreas Boehler    /**
159*a1a3b679SAndreas Boehler     * This method is responsible for generating the actual, full response.
160*a1a3b679SAndreas Boehler     *
161*a1a3b679SAndreas Boehler     * @param string $path
162*a1a3b679SAndreas Boehler     * @param DateTime|null $start
163*a1a3b679SAndreas Boehler     * @param DateTime|null $end
164*a1a3b679SAndreas Boehler     * @param bool $expand
165*a1a3b679SAndreas Boehler     * @param string $componentType
166*a1a3b679SAndreas Boehler     * @param string $format
167*a1a3b679SAndreas Boehler     * @param array $properties
168*a1a3b679SAndreas Boehler     * @param ResponseInterface $response
169*a1a3b679SAndreas Boehler     */
170*a1a3b679SAndreas Boehler    protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) {
171*a1a3b679SAndreas Boehler
172*a1a3b679SAndreas Boehler        $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
173*a1a3b679SAndreas Boehler
174*a1a3b679SAndreas Boehler        $blobs = [];
175*a1a3b679SAndreas Boehler        if ($start || $end || $componentType) {
176*a1a3b679SAndreas Boehler
177*a1a3b679SAndreas Boehler            // If there was a start or end filter, we need to enlist
178*a1a3b679SAndreas Boehler            // calendarQuery for speed.
179*a1a3b679SAndreas Boehler            $calendarNode = $this->server->tree->getNodeForPath($path);
180*a1a3b679SAndreas Boehler            $queryResult = $calendarNode->calendarQuery([
181*a1a3b679SAndreas Boehler                'name'         => 'VCALENDAR',
182*a1a3b679SAndreas Boehler                'comp-filters' => [
183*a1a3b679SAndreas Boehler                    [
184*a1a3b679SAndreas Boehler                        'name'           => $componentType,
185*a1a3b679SAndreas Boehler                        'comp-filters'   => [],
186*a1a3b679SAndreas Boehler                        'prop-filters'   => [],
187*a1a3b679SAndreas Boehler                        'is-not-defined' => false,
188*a1a3b679SAndreas Boehler                        'time-range'     => [
189*a1a3b679SAndreas Boehler                            'start' => $start,
190*a1a3b679SAndreas Boehler                            'end'   => $end,
191*a1a3b679SAndreas Boehler                        ],
192*a1a3b679SAndreas Boehler                    ],
193*a1a3b679SAndreas Boehler                ],
194*a1a3b679SAndreas Boehler                'prop-filters'   => [],
195*a1a3b679SAndreas Boehler                'is-not-defined' => false,
196*a1a3b679SAndreas Boehler                'time-range'     => null,
197*a1a3b679SAndreas Boehler            ]);
198*a1a3b679SAndreas Boehler
199*a1a3b679SAndreas Boehler            // queryResult is just a list of base urls. We need to prefix the
200*a1a3b679SAndreas Boehler            // calendar path.
201*a1a3b679SAndreas Boehler            $queryResult = array_map(
202*a1a3b679SAndreas Boehler                function($item) use ($path) {
203*a1a3b679SAndreas Boehler                    return $path . '/' . $item;
204*a1a3b679SAndreas Boehler                },
205*a1a3b679SAndreas Boehler                $queryResult
206*a1a3b679SAndreas Boehler            );
207*a1a3b679SAndreas Boehler            $nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]);
208*a1a3b679SAndreas Boehler            unset($queryResult);
209*a1a3b679SAndreas Boehler
210*a1a3b679SAndreas Boehler        } else {
211*a1a3b679SAndreas Boehler            $nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1);
212*a1a3b679SAndreas Boehler        }
213*a1a3b679SAndreas Boehler
214*a1a3b679SAndreas Boehler        // Flattening the arrays
215*a1a3b679SAndreas Boehler        foreach ($nodes as $node) {
216*a1a3b679SAndreas Boehler            if (isset($node[200][$calDataProp])) {
217*a1a3b679SAndreas Boehler                $blobs[$node['href']] = $node[200][$calDataProp];
218*a1a3b679SAndreas Boehler            }
219*a1a3b679SAndreas Boehler        }
220*a1a3b679SAndreas Boehler        unset($nodes);
221*a1a3b679SAndreas Boehler
222*a1a3b679SAndreas Boehler        $mergedCalendar = $this->mergeObjects(
223*a1a3b679SAndreas Boehler            $properties,
224*a1a3b679SAndreas Boehler            $blobs
225*a1a3b679SAndreas Boehler        );
226*a1a3b679SAndreas Boehler
227*a1a3b679SAndreas Boehler        if ($expand) {
228*a1a3b679SAndreas Boehler            $calendarTimeZone = null;
229*a1a3b679SAndreas Boehler            // We're expanding, and for that we need to figure out the
230*a1a3b679SAndreas Boehler            // calendar's timezone.
231*a1a3b679SAndreas Boehler            $tzProp = '{' . Plugin::NS_CALDAV . '}calendar-timezone';
232*a1a3b679SAndreas Boehler            $tzResult = $this->server->getProperties($path, [$tzProp]);
233*a1a3b679SAndreas Boehler            if (isset($tzResult[$tzProp])) {
234*a1a3b679SAndreas Boehler                // This property contains a VCALENDAR with a single
235*a1a3b679SAndreas Boehler                // VTIMEZONE.
236*a1a3b679SAndreas Boehler                $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
237*a1a3b679SAndreas Boehler                $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
238*a1a3b679SAndreas Boehler                unset($vtimezoneObj);
239*a1a3b679SAndreas Boehler            } else {
240*a1a3b679SAndreas Boehler                // Defaulting to UTC.
241*a1a3b679SAndreas Boehler                $calendarTimeZone = new DateTimeZone('UTC');
242*a1a3b679SAndreas Boehler            }
243*a1a3b679SAndreas Boehler
244*a1a3b679SAndreas Boehler            $mergedCalendar->expand($start, $end, $calendarTimeZone);
245*a1a3b679SAndreas Boehler        }
246*a1a3b679SAndreas Boehler
247*a1a3b679SAndreas Boehler        $response->setHeader('Content-Type', $format);
248*a1a3b679SAndreas Boehler
249*a1a3b679SAndreas Boehler        switch ($format) {
250*a1a3b679SAndreas Boehler            case 'text/calendar' :
251*a1a3b679SAndreas Boehler                $mergedCalendar = $mergedCalendar->serialize();
252*a1a3b679SAndreas Boehler                break;
253*a1a3b679SAndreas Boehler            case 'application/calendar+json' :
254*a1a3b679SAndreas Boehler                $mergedCalendar = json_encode($mergedCalendar->jsonSerialize());
255*a1a3b679SAndreas Boehler                break;
256*a1a3b679SAndreas Boehler        }
257*a1a3b679SAndreas Boehler
258*a1a3b679SAndreas Boehler        $response->setStatus(200);
259*a1a3b679SAndreas Boehler        $response->setBody($mergedCalendar);
260*a1a3b679SAndreas Boehler
261*a1a3b679SAndreas Boehler    }
262*a1a3b679SAndreas Boehler
263*a1a3b679SAndreas Boehler    /**
264*a1a3b679SAndreas Boehler     * Merges all calendar objects, and builds one big iCalendar blob.
265*a1a3b679SAndreas Boehler     *
266*a1a3b679SAndreas Boehler     * @param array $properties Some CalDAV properties
267*a1a3b679SAndreas Boehler     * @param array $inputObjects
268*a1a3b679SAndreas Boehler     * @return VObject\Component\VCalendar
269*a1a3b679SAndreas Boehler     */
270*a1a3b679SAndreas Boehler    function mergeObjects(array $properties, array $inputObjects) {
271*a1a3b679SAndreas Boehler
272*a1a3b679SAndreas Boehler        $calendar = new VObject\Component\VCalendar();
273*a1a3b679SAndreas Boehler        $calendar->version = '2.0';
274*a1a3b679SAndreas Boehler        if (DAV\Server::$exposeVersion) {
275*a1a3b679SAndreas Boehler            $calendar->prodid = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
276*a1a3b679SAndreas Boehler        } else {
277*a1a3b679SAndreas Boehler            $calendar->prodid = '-//SabreDAV//SabreDAV//EN';
278*a1a3b679SAndreas Boehler        }
279*a1a3b679SAndreas Boehler        if (isset($properties['{DAV:}displayname'])) {
280*a1a3b679SAndreas Boehler            $calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname'];
281*a1a3b679SAndreas Boehler        }
282*a1a3b679SAndreas Boehler        if (isset($properties['{http://apple.com/ns/ical/}calendar-color'])) {
283*a1a3b679SAndreas Boehler            $calendar->{'X-APPLE-CALENDAR-COLOR'} = $properties['{http://apple.com/ns/ical/}calendar-color'];
284*a1a3b679SAndreas Boehler        }
285*a1a3b679SAndreas Boehler
286*a1a3b679SAndreas Boehler        $collectedTimezones = [];
287*a1a3b679SAndreas Boehler
288*a1a3b679SAndreas Boehler        $timezones = [];
289*a1a3b679SAndreas Boehler        $objects = [];
290*a1a3b679SAndreas Boehler
291*a1a3b679SAndreas Boehler        foreach ($inputObjects as $href => $inputObject) {
292*a1a3b679SAndreas Boehler
293*a1a3b679SAndreas Boehler            $nodeComp = VObject\Reader::read($inputObject);
294*a1a3b679SAndreas Boehler
295*a1a3b679SAndreas Boehler            foreach ($nodeComp->children() as $child) {
296*a1a3b679SAndreas Boehler
297*a1a3b679SAndreas Boehler                switch ($child->name) {
298*a1a3b679SAndreas Boehler                    case 'VEVENT' :
299*a1a3b679SAndreas Boehler                    case 'VTODO' :
300*a1a3b679SAndreas Boehler                    case 'VJOURNAL' :
301*a1a3b679SAndreas Boehler                        $objects[] = $child;
302*a1a3b679SAndreas Boehler                        break;
303*a1a3b679SAndreas Boehler
304*a1a3b679SAndreas Boehler                    // VTIMEZONE is special, because we need to filter out the duplicates
305*a1a3b679SAndreas Boehler                    case 'VTIMEZONE' :
306*a1a3b679SAndreas Boehler                        // Naively just checking tzid.
307*a1a3b679SAndreas Boehler                        if (in_array((string)$child->TZID, $collectedTimezones)) continue;
308*a1a3b679SAndreas Boehler
309*a1a3b679SAndreas Boehler                        $timezones[] = $child;
310*a1a3b679SAndreas Boehler                        $collectedTimezones[] = $child->TZID;
311*a1a3b679SAndreas Boehler                        break;
312*a1a3b679SAndreas Boehler
313*a1a3b679SAndreas Boehler                }
314*a1a3b679SAndreas Boehler
315*a1a3b679SAndreas Boehler            }
316*a1a3b679SAndreas Boehler
317*a1a3b679SAndreas Boehler        }
318*a1a3b679SAndreas Boehler
319*a1a3b679SAndreas Boehler        foreach ($timezones as $tz) $calendar->add($tz);
320*a1a3b679SAndreas Boehler        foreach ($objects as $obj) $calendar->add($obj);
321*a1a3b679SAndreas Boehler
322*a1a3b679SAndreas Boehler        return $calendar;
323*a1a3b679SAndreas Boehler
324*a1a3b679SAndreas Boehler    }
325*a1a3b679SAndreas Boehler
326*a1a3b679SAndreas Boehler    /**
327*a1a3b679SAndreas Boehler     * Returns a plugin name.
328*a1a3b679SAndreas Boehler     *
329*a1a3b679SAndreas Boehler     * Using this name other plugins will be able to access other plugins
330*a1a3b679SAndreas Boehler     * using \Sabre\DAV\Server::getPlugin
331*a1a3b679SAndreas Boehler     *
332*a1a3b679SAndreas Boehler     * @return string
333*a1a3b679SAndreas Boehler     */
334*a1a3b679SAndreas Boehler    function getPluginName() {
335*a1a3b679SAndreas Boehler
336*a1a3b679SAndreas Boehler        return 'ics-export';
337*a1a3b679SAndreas Boehler
338*a1a3b679SAndreas Boehler    }
339*a1a3b679SAndreas Boehler
340*a1a3b679SAndreas Boehler    /**
341*a1a3b679SAndreas Boehler     * Returns a bunch of meta-data about the plugin.
342*a1a3b679SAndreas Boehler     *
343*a1a3b679SAndreas Boehler     * Providing this information is optional, and is mainly displayed by the
344*a1a3b679SAndreas Boehler     * Browser plugin.
345*a1a3b679SAndreas Boehler     *
346*a1a3b679SAndreas Boehler     * The description key in the returned array may contain html and will not
347*a1a3b679SAndreas Boehler     * be sanitized.
348*a1a3b679SAndreas Boehler     *
349*a1a3b679SAndreas Boehler     * @return array
350*a1a3b679SAndreas Boehler     */
351*a1a3b679SAndreas Boehler    function getPluginInfo() {
352*a1a3b679SAndreas Boehler
353*a1a3b679SAndreas Boehler        return [
354*a1a3b679SAndreas Boehler            'name'        => $this->getPluginName(),
355*a1a3b679SAndreas Boehler            'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.',
356*a1a3b679SAndreas Boehler            'link'        => 'http://sabre.io/dav/ics-export-plugin/',
357*a1a3b679SAndreas Boehler        ];
358*a1a3b679SAndreas Boehler
359*a1a3b679SAndreas Boehler    }
360*a1a3b679SAndreas Boehler
361*a1a3b679SAndreas Boehler}
362