xref: /plugin/davcal/calendarBackendDokuwiki.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
1*a1a3b679SAndreas Boehler<?php
2*a1a3b679SAndreas Boehler
3*a1a3b679SAndreas Boehleruse \Sabre\VObject;
4*a1a3b679SAndreas Boehleruse \Sabre\CalDAV;
5*a1a3b679SAndreas Boehleruse \Sabre\DAV;
6*a1a3b679SAndreas Boehleruse \Sabre\DAV\Exception\Forbidden;
7*a1a3b679SAndreas Boehler/**
8*a1a3b679SAndreas Boehler * PDO CalDAV backend for DokuWiki - based on Sabre's CalDAV backend
9*a1a3b679SAndreas Boehler *
10*a1a3b679SAndreas Boehler * This backend is used to store calendar-data in a PDO database, such as
11*a1a3b679SAndreas Boehler * sqlite or MySQL
12*a1a3b679SAndreas Boehler *
13*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
14*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/)
15*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License
16*a1a3b679SAndreas Boehler */
17*a1a3b679SAndreas Boehlerclass DokuWikiSabreCalendarBackend extends \Sabre\CalDAV\Backend\AbstractBackend {
18*a1a3b679SAndreas Boehler
19*a1a3b679SAndreas Boehler    /**
20*a1a3b679SAndreas Boehler     * We need to specify a max date, because we need to stop *somewhere*
21*a1a3b679SAndreas Boehler     *
22*a1a3b679SAndreas Boehler     * On 32 bit system the maximum for a signed integer is 2147483647, so
23*a1a3b679SAndreas Boehler     * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
24*a1a3b679SAndreas Boehler     * in 2038-01-19 to avoid problems when the date is converted
25*a1a3b679SAndreas Boehler     * to a unix timestamp.
26*a1a3b679SAndreas Boehler     */
27*a1a3b679SAndreas Boehler    const MAX_DATE = '2038-01-01';
28*a1a3b679SAndreas Boehler
29*a1a3b679SAndreas Boehler    /**
30*a1a3b679SAndreas Boehler     * pdo
31*a1a3b679SAndreas Boehler     *
32*a1a3b679SAndreas Boehler     * @var \PDO
33*a1a3b679SAndreas Boehler     */
34*a1a3b679SAndreas Boehler    protected $pdo;
35*a1a3b679SAndreas Boehler
36*a1a3b679SAndreas Boehler    /**
37*a1a3b679SAndreas Boehler     * DokuWiki PlugIn Helper
38*a1a3b679SAndreas Boehler     */
39*a1a3b679SAndreas Boehler    protected $hlp = null;
40*a1a3b679SAndreas Boehler    /**
41*a1a3b679SAndreas Boehler     * The table name that will be used for calendars
42*a1a3b679SAndreas Boehler     *
43*a1a3b679SAndreas Boehler     * @var string
44*a1a3b679SAndreas Boehler     */
45*a1a3b679SAndreas Boehler    public $calendarTableName = 'calendars';
46*a1a3b679SAndreas Boehler
47*a1a3b679SAndreas Boehler
48*a1a3b679SAndreas Boehler    /**
49*a1a3b679SAndreas Boehler     * The table name that will be used for DokuWiki Calendar-Page mapping
50*a1a3b679SAndreas Boehler     *
51*a1a3b679SAndreas Boehler     * @var string
52*a1a3b679SAndreas Boehler     */
53*a1a3b679SAndreas Boehler    public $calendarDokuwikiMappingTableName = 'pagetocalendarmapping';
54*a1a3b679SAndreas Boehler    /**
55*a1a3b679SAndreas Boehler     * The table name that will be used for calendar objects
56*a1a3b679SAndreas Boehler     *
57*a1a3b679SAndreas Boehler     * @var string
58*a1a3b679SAndreas Boehler     */
59*a1a3b679SAndreas Boehler    public $calendarObjectTableName = 'calendarobjects';
60*a1a3b679SAndreas Boehler
61*a1a3b679SAndreas Boehler    /**
62*a1a3b679SAndreas Boehler     * The table name that will be used for tracking changes in calendars.
63*a1a3b679SAndreas Boehler     *
64*a1a3b679SAndreas Boehler     * @var string
65*a1a3b679SAndreas Boehler     */
66*a1a3b679SAndreas Boehler    public $calendarChangesTableName = 'calendarchanges';
67*a1a3b679SAndreas Boehler
68*a1a3b679SAndreas Boehler    /**
69*a1a3b679SAndreas Boehler     * The table name that will be used inbox items.
70*a1a3b679SAndreas Boehler     *
71*a1a3b679SAndreas Boehler     * @var string
72*a1a3b679SAndreas Boehler     */
73*a1a3b679SAndreas Boehler    public $schedulingObjectTableName = 'schedulingobjects';
74*a1a3b679SAndreas Boehler
75*a1a3b679SAndreas Boehler    /**
76*a1a3b679SAndreas Boehler     * The table name that will be used for calendar subscriptions.
77*a1a3b679SAndreas Boehler     *
78*a1a3b679SAndreas Boehler     * @var string
79*a1a3b679SAndreas Boehler     */
80*a1a3b679SAndreas Boehler    public $calendarSubscriptionsTableName = 'calendarsubscriptions';
81*a1a3b679SAndreas Boehler
82*a1a3b679SAndreas Boehler    /**
83*a1a3b679SAndreas Boehler     * List of CalDAV properties, and how they map to database fieldnames
84*a1a3b679SAndreas Boehler     * Add your own properties by simply adding on to this array.
85*a1a3b679SAndreas Boehler     *
86*a1a3b679SAndreas Boehler     * Note that only string-based properties are supported here.
87*a1a3b679SAndreas Boehler     *
88*a1a3b679SAndreas Boehler     * @var array
89*a1a3b679SAndreas Boehler     */
90*a1a3b679SAndreas Boehler    public $propertyMap = [
91*a1a3b679SAndreas Boehler        '{DAV:}displayname'                                   => 'displayname',
92*a1a3b679SAndreas Boehler        '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
93*a1a3b679SAndreas Boehler        '{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
94*a1a3b679SAndreas Boehler        '{http://apple.com/ns/ical/}calendar-order'           => 'calendarorder',
95*a1a3b679SAndreas Boehler        '{http://apple.com/ns/ical/}calendar-color'           => 'calendarcolor',
96*a1a3b679SAndreas Boehler    ];
97*a1a3b679SAndreas Boehler
98*a1a3b679SAndreas Boehler    /**
99*a1a3b679SAndreas Boehler     * List of subscription properties, and how they map to database fieldnames.
100*a1a3b679SAndreas Boehler     *
101*a1a3b679SAndreas Boehler     * @var array
102*a1a3b679SAndreas Boehler     */
103*a1a3b679SAndreas Boehler    public $subscriptionPropertyMap = [
104*a1a3b679SAndreas Boehler        '{DAV:}displayname'                                           => 'displayname',
105*a1a3b679SAndreas Boehler        '{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
106*a1a3b679SAndreas Boehler        '{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
107*a1a3b679SAndreas Boehler        '{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
108*a1a3b679SAndreas Boehler        '{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
109*a1a3b679SAndreas Boehler        '{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
110*a1a3b679SAndreas Boehler        '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
111*a1a3b679SAndreas Boehler    ];
112*a1a3b679SAndreas Boehler
113*a1a3b679SAndreas Boehler    /**
114*a1a3b679SAndreas Boehler     * Creates the backend
115*a1a3b679SAndreas Boehler     *
116*a1a3b679SAndreas Boehler     * @param \PDO $pdo
117*a1a3b679SAndreas Boehler     */
118*a1a3b679SAndreas Boehler    function __construct(\PDO $pdo) {
119*a1a3b679SAndreas Boehler
120*a1a3b679SAndreas Boehler        $this->pdo = $pdo;
121*a1a3b679SAndreas Boehler        $this->hlp = plugin_load('helper', 'davcal');
122*a1a3b679SAndreas Boehler
123*a1a3b679SAndreas Boehler    }
124*a1a3b679SAndreas Boehler
125*a1a3b679SAndreas Boehler    /**
126*a1a3b679SAndreas Boehler     * Returns a list of calendars for a principal.
127*a1a3b679SAndreas Boehler     *
128*a1a3b679SAndreas Boehler     * Every project is an array with the following keys:
129*a1a3b679SAndreas Boehler     *  * id, a unique id that will be used by other functions to modify the
130*a1a3b679SAndreas Boehler     *    calendar. This can be the same as the uri or a database key.
131*a1a3b679SAndreas Boehler     *  * uri. This is just the 'base uri' or 'filename' of the calendar.
132*a1a3b679SAndreas Boehler     *  * principaluri. The owner of the calendar. Almost always the same as
133*a1a3b679SAndreas Boehler     *    principalUri passed to this method.
134*a1a3b679SAndreas Boehler     *
135*a1a3b679SAndreas Boehler     * Furthermore it can contain webdav properties in clark notation. A very
136*a1a3b679SAndreas Boehler     * common one is '{DAV:}displayname'.
137*a1a3b679SAndreas Boehler     *
138*a1a3b679SAndreas Boehler     * Many clients also require:
139*a1a3b679SAndreas Boehler     * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
140*a1a3b679SAndreas Boehler     * For this property, you can just return an instance of
141*a1a3b679SAndreas Boehler     * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet.
142*a1a3b679SAndreas Boehler     *
143*a1a3b679SAndreas Boehler     * If you return {http://sabredav.org/ns}read-only and set the value to 1,
144*a1a3b679SAndreas Boehler     * ACL will automatically be put in read-only mode.
145*a1a3b679SAndreas Boehler     *
146*a1a3b679SAndreas Boehler     * @param string $principalUri
147*a1a3b679SAndreas Boehler     * @return array
148*a1a3b679SAndreas Boehler     */
149*a1a3b679SAndreas Boehler    function getCalendarsForUser($principalUri) {
150*a1a3b679SAndreas Boehler
151*a1a3b679SAndreas Boehler        $fields = array_values($this->propertyMap);
152*a1a3b679SAndreas Boehler        $fields[] = 'id';
153*a1a3b679SAndreas Boehler        $fields[] = 'uri';
154*a1a3b679SAndreas Boehler        $fields[] = 'synctoken';
155*a1a3b679SAndreas Boehler        $fields[] = 'components';
156*a1a3b679SAndreas Boehler        $fields[] = 'principaluri';
157*a1a3b679SAndreas Boehler        $fields[] = 'transparent';
158*a1a3b679SAndreas Boehler
159*a1a3b679SAndreas Boehler        $idInfo = $this->hlp->getCalendarIdsForUser($principalUri);
160*a1a3b679SAndreas Boehler        $idFilter = array_keys($idInfo);
161*a1a3b679SAndreas Boehler        // Making fields a comma-delimited list
162*a1a3b679SAndreas Boehler        $fields = implode(', ', $fields);
163*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarTableName . " ORDER BY calendarorder ASC");
164*a1a3b679SAndreas Boehler        $stmt->execute();
165*a1a3b679SAndreas Boehler
166*a1a3b679SAndreas Boehler        $calendars = [];
167*a1a3b679SAndreas Boehler        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
168*a1a3b679SAndreas Boehler            // Filter Calendars by the array returned by the DokuWiki Auth system
169*a1a3b679SAndreas Boehler            if(array_search($row['id'], $idFilter) === false)
170*a1a3b679SAndreas Boehler                continue;
171*a1a3b679SAndreas Boehler            $components = [];
172*a1a3b679SAndreas Boehler            if ($row['components']) {
173*a1a3b679SAndreas Boehler                $components = explode(',', $row['components']);
174*a1a3b679SAndreas Boehler            }
175*a1a3b679SAndreas Boehler
176*a1a3b679SAndreas Boehler            $calendar = [
177*a1a3b679SAndreas Boehler                'id'                                                                 => $row['id'],
178*a1a3b679SAndreas Boehler                'uri'                                                                => $row['uri'],
179*a1a3b679SAndreas Boehler                'principaluri'                                                       => $principalUri,//$row['principaluri'], // Overwrite principaluri from database, we actually don't need it.
180*a1a3b679SAndreas Boehler                '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}getctag'                  => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ? $row['synctoken'] : '0'),
181*a1a3b679SAndreas Boehler                '{http://sabredav.org/ns}sync-token'                                 => $row['synctoken'] ? $row['synctoken'] : '0',
182*a1a3b679SAndreas Boehler                '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components),
183*a1a3b679SAndreas Boehler                '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp'         => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
184*a1a3b679SAndreas Boehler            ];
185*a1a3b679SAndreas Boehler            if($idInfo[$row['id']]['readonly'] === true)
186*a1a3b679SAndreas Boehler                $calendar['{http://sabredav.org/ns}read-only'] = '1';
187*a1a3b679SAndreas Boehler
188*a1a3b679SAndreas Boehler
189*a1a3b679SAndreas Boehler            foreach ($this->propertyMap as $xmlName => $dbName) {
190*a1a3b679SAndreas Boehler                $calendar[$xmlName] = $row[$dbName];
191*a1a3b679SAndreas Boehler            }
192*a1a3b679SAndreas Boehler
193*a1a3b679SAndreas Boehler            $calendars[] = $calendar;
194*a1a3b679SAndreas Boehler
195*a1a3b679SAndreas Boehler        }
196*a1a3b679SAndreas Boehler
197*a1a3b679SAndreas Boehler        return $calendars;
198*a1a3b679SAndreas Boehler
199*a1a3b679SAndreas Boehler    }
200*a1a3b679SAndreas Boehler
201*a1a3b679SAndreas Boehler    /**
202*a1a3b679SAndreas Boehler     * Creates a new calendar for a principal.
203*a1a3b679SAndreas Boehler     *
204*a1a3b679SAndreas Boehler     * If the creation was a success, an id must be returned that can be used
205*a1a3b679SAndreas Boehler     * to reference this calendar in other methods, such as updateCalendar.
206*a1a3b679SAndreas Boehler     *
207*a1a3b679SAndreas Boehler     * @param string $principalUri
208*a1a3b679SAndreas Boehler     * @param string $calendarUri
209*a1a3b679SAndreas Boehler     * @param array $properties
210*a1a3b679SAndreas Boehler     * @return string
211*a1a3b679SAndreas Boehler     */
212*a1a3b679SAndreas Boehler    function createCalendar($principalUri, $calendarUri, array $properties) {
213*a1a3b679SAndreas Boehler
214*a1a3b679SAndreas Boehler        return false;
215*a1a3b679SAndreas Boehler        /*
216*a1a3b679SAndreas Boehler        $fieldNames = [
217*a1a3b679SAndreas Boehler            'principaluri',
218*a1a3b679SAndreas Boehler            'uri',
219*a1a3b679SAndreas Boehler            'synctoken',
220*a1a3b679SAndreas Boehler            'transparent',
221*a1a3b679SAndreas Boehler        ];
222*a1a3b679SAndreas Boehler        $values = [
223*a1a3b679SAndreas Boehler            ':principaluri' => $principalUri,
224*a1a3b679SAndreas Boehler            ':uri'          => $calendarUri,
225*a1a3b679SAndreas Boehler            ':synctoken'    => 1,
226*a1a3b679SAndreas Boehler            ':transparent'  => 0,
227*a1a3b679SAndreas Boehler        ];
228*a1a3b679SAndreas Boehler
229*a1a3b679SAndreas Boehler        // Default value
230*a1a3b679SAndreas Boehler        $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
231*a1a3b679SAndreas Boehler        $fieldNames[] = 'components';
232*a1a3b679SAndreas Boehler        if (!isset($properties[$sccs])) {
233*a1a3b679SAndreas Boehler            $values[':components'] = 'VEVENT,VTODO';
234*a1a3b679SAndreas Boehler        } else {
235*a1a3b679SAndreas Boehler            if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) {
236*a1a3b679SAndreas Boehler                throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet');
237*a1a3b679SAndreas Boehler            }
238*a1a3b679SAndreas Boehler            $values[':components'] = implode(',', $properties[$sccs]->getValue());
239*a1a3b679SAndreas Boehler        }
240*a1a3b679SAndreas Boehler        $transp = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp';
241*a1a3b679SAndreas Boehler        if (isset($properties[$transp])) {
242*a1a3b679SAndreas Boehler            $values[':transparent'] = $properties[$transp]->getValue() === 'transparent';
243*a1a3b679SAndreas Boehler        }
244*a1a3b679SAndreas Boehler
245*a1a3b679SAndreas Boehler        foreach ($this->propertyMap as $xmlName => $dbName) {
246*a1a3b679SAndreas Boehler            if (isset($properties[$xmlName])) {
247*a1a3b679SAndreas Boehler
248*a1a3b679SAndreas Boehler                $values[':' . $dbName] = $properties[$xmlName];
249*a1a3b679SAndreas Boehler                $fieldNames[] = $dbName;
250*a1a3b679SAndreas Boehler            }
251*a1a3b679SAndreas Boehler        }
252*a1a3b679SAndreas Boehler
253*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")");
254*a1a3b679SAndreas Boehler        $stmt->execute($values);
255*a1a3b679SAndreas Boehler
256*a1a3b679SAndreas Boehler        return $this->pdo->lastInsertId();
257*a1a3b679SAndreas Boehler        */
258*a1a3b679SAndreas Boehler    }
259*a1a3b679SAndreas Boehler
260*a1a3b679SAndreas Boehler    /**
261*a1a3b679SAndreas Boehler     * Updates properties for a calendar.
262*a1a3b679SAndreas Boehler     *
263*a1a3b679SAndreas Boehler     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
264*a1a3b679SAndreas Boehler     * To do the actual updates, you must tell this object which properties
265*a1a3b679SAndreas Boehler     * you're going to process with the handle() method.
266*a1a3b679SAndreas Boehler     *
267*a1a3b679SAndreas Boehler     * Calling the handle method is like telling the PropPatch object "I
268*a1a3b679SAndreas Boehler     * promise I can handle updating this property".
269*a1a3b679SAndreas Boehler     *
270*a1a3b679SAndreas Boehler     * Read the PropPatch documenation for more info and examples.
271*a1a3b679SAndreas Boehler     *
272*a1a3b679SAndreas Boehler     * @param string $calendarId
273*a1a3b679SAndreas Boehler     * @param \Sabre\DAV\PropPatch $propPatch
274*a1a3b679SAndreas Boehler     * @return void
275*a1a3b679SAndreas Boehler     */
276*a1a3b679SAndreas Boehler    function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) {
277*a1a3b679SAndreas Boehler
278*a1a3b679SAndreas Boehler        $supportedProperties = array_keys($this->propertyMap);
279*a1a3b679SAndreas Boehler        $supportedProperties[] = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp';
280*a1a3b679SAndreas Boehler
281*a1a3b679SAndreas Boehler        $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
282*a1a3b679SAndreas Boehler            $newValues = [];
283*a1a3b679SAndreas Boehler            foreach ($mutations as $propertyName => $propertyValue) {
284*a1a3b679SAndreas Boehler
285*a1a3b679SAndreas Boehler                switch ($propertyName) {
286*a1a3b679SAndreas Boehler                    case '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' :
287*a1a3b679SAndreas Boehler                        $fieldName = 'transparent';
288*a1a3b679SAndreas Boehler                        $newValues[$fieldName] = $propertyValue->getValue() === 'transparent';
289*a1a3b679SAndreas Boehler                        break;
290*a1a3b679SAndreas Boehler                    default :
291*a1a3b679SAndreas Boehler                        $fieldName = $this->propertyMap[$propertyName];
292*a1a3b679SAndreas Boehler                        $newValues[$fieldName] = $propertyValue;
293*a1a3b679SAndreas Boehler                        break;
294*a1a3b679SAndreas Boehler                }
295*a1a3b679SAndreas Boehler
296*a1a3b679SAndreas Boehler            }
297*a1a3b679SAndreas Boehler            $valuesSql = [];
298*a1a3b679SAndreas Boehler            foreach ($newValues as $fieldName => $value) {
299*a1a3b679SAndreas Boehler                $valuesSql[] = $fieldName . ' = ?';
300*a1a3b679SAndreas Boehler            }
301*a1a3b679SAndreas Boehler
302*a1a3b679SAndreas Boehler            $stmt = $this->pdo->prepare("UPDATE " . $this->calendarTableName . " SET " . implode(', ', $valuesSql) . " WHERE id = ?");
303*a1a3b679SAndreas Boehler            $newValues['id'] = $calendarId;
304*a1a3b679SAndreas Boehler            $stmt->execute(array_values($newValues));
305*a1a3b679SAndreas Boehler
306*a1a3b679SAndreas Boehler            $this->addChange($calendarId, "", 2);
307*a1a3b679SAndreas Boehler
308*a1a3b679SAndreas Boehler            return true;
309*a1a3b679SAndreas Boehler
310*a1a3b679SAndreas Boehler        });
311*a1a3b679SAndreas Boehler
312*a1a3b679SAndreas Boehler    }
313*a1a3b679SAndreas Boehler
314*a1a3b679SAndreas Boehler    /**
315*a1a3b679SAndreas Boehler     * Delete a calendar and all it's objects
316*a1a3b679SAndreas Boehler     *
317*a1a3b679SAndreas Boehler     * @param string $calendarId
318*a1a3b679SAndreas Boehler     * @return void
319*a1a3b679SAndreas Boehler     */
320*a1a3b679SAndreas Boehler    function deleteCalendar($calendarId) {
321*a1a3b679SAndreas Boehler
322*a1a3b679SAndreas Boehler        /*
323*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?');
324*a1a3b679SAndreas Boehler        $stmt->execute([$calendarId]);
325*a1a3b679SAndreas Boehler
326*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarTableName . ' WHERE id = ?');
327*a1a3b679SAndreas Boehler        $stmt->execute([$calendarId]);
328*a1a3b679SAndreas Boehler
329*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarChangesTableName . ' WHERE calendarid = ?');
330*a1a3b679SAndreas Boehler        $stmt->execute([$calendarId]);
331*a1a3b679SAndreas Boehler         */
332*a1a3b679SAndreas Boehler
333*a1a3b679SAndreas Boehler    }
334*a1a3b679SAndreas Boehler
335*a1a3b679SAndreas Boehler    /**
336*a1a3b679SAndreas Boehler     * Returns all calendar objects within a calendar.
337*a1a3b679SAndreas Boehler     *
338*a1a3b679SAndreas Boehler     * Every item contains an array with the following keys:
339*a1a3b679SAndreas Boehler     *   * calendardata - The iCalendar-compatible calendar data
340*a1a3b679SAndreas Boehler     *   * uri - a unique key which will be used to construct the uri. This can
341*a1a3b679SAndreas Boehler     *     be any arbitrary string, but making sure it ends with '.ics' is a
342*a1a3b679SAndreas Boehler     *     good idea. This is only the basename, or filename, not the full
343*a1a3b679SAndreas Boehler     *     path.
344*a1a3b679SAndreas Boehler     *   * lastmodified - a timestamp of the last modification time
345*a1a3b679SAndreas Boehler     *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
346*a1a3b679SAndreas Boehler     *   '  "abcdef"')
347*a1a3b679SAndreas Boehler     *   * size - The size of the calendar objects, in bytes.
348*a1a3b679SAndreas Boehler     *   * component - optional, a string containing the type of object, such
349*a1a3b679SAndreas Boehler     *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
350*a1a3b679SAndreas Boehler     *     the Content-Type header.
351*a1a3b679SAndreas Boehler     *
352*a1a3b679SAndreas Boehler     * Note that the etag is optional, but it's highly encouraged to return for
353*a1a3b679SAndreas Boehler     * speed reasons.
354*a1a3b679SAndreas Boehler     *
355*a1a3b679SAndreas Boehler     * The calendardata is also optional. If it's not returned
356*a1a3b679SAndreas Boehler     * 'getCalendarObject' will be called later, which *is* expected to return
357*a1a3b679SAndreas Boehler     * calendardata.
358*a1a3b679SAndreas Boehler     *
359*a1a3b679SAndreas Boehler     * If neither etag or size are specified, the calendardata will be
360*a1a3b679SAndreas Boehler     * used/fetched to determine these numbers. If both are specified the
361*a1a3b679SAndreas Boehler     * amount of times this is needed is reduced by a great degree.
362*a1a3b679SAndreas Boehler     *
363*a1a3b679SAndreas Boehler     * @param string $calendarId
364*a1a3b679SAndreas Boehler     * @return array
365*a1a3b679SAndreas Boehler     */
366*a1a3b679SAndreas Boehler    function getCalendarObjects($calendarId) {
367*a1a3b679SAndreas Boehler
368*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?');
369*a1a3b679SAndreas Boehler        $stmt->execute([$calendarId]);
370*a1a3b679SAndreas Boehler
371*a1a3b679SAndreas Boehler        $result = [];
372*a1a3b679SAndreas Boehler        foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
373*a1a3b679SAndreas Boehler            $result[] = [
374*a1a3b679SAndreas Boehler                'id'           => $row['id'],
375*a1a3b679SAndreas Boehler                'uri'          => $row['uri'],
376*a1a3b679SAndreas Boehler                'lastmodified' => $row['lastmodified'],
377*a1a3b679SAndreas Boehler                'etag'         => '"' . $row['etag'] . '"',
378*a1a3b679SAndreas Boehler                'calendarid'   => $row['calendarid'],
379*a1a3b679SAndreas Boehler                'size'         => (int)$row['size'],
380*a1a3b679SAndreas Boehler                'component'    => strtolower($row['componenttype']),
381*a1a3b679SAndreas Boehler            ];
382*a1a3b679SAndreas Boehler        }
383*a1a3b679SAndreas Boehler
384*a1a3b679SAndreas Boehler        return $result;
385*a1a3b679SAndreas Boehler
386*a1a3b679SAndreas Boehler    }
387*a1a3b679SAndreas Boehler
388*a1a3b679SAndreas Boehler    /**
389*a1a3b679SAndreas Boehler     * Returns information from a single calendar object, based on it's object
390*a1a3b679SAndreas Boehler     * uri.
391*a1a3b679SAndreas Boehler     *
392*a1a3b679SAndreas Boehler     * The object uri is only the basename, or filename and not a full path.
393*a1a3b679SAndreas Boehler     *
394*a1a3b679SAndreas Boehler     * The returned array must have the same keys as getCalendarObjects. The
395*a1a3b679SAndreas Boehler     * 'calendardata' object is required here though, while it's not required
396*a1a3b679SAndreas Boehler     * for getCalendarObjects.
397*a1a3b679SAndreas Boehler     *
398*a1a3b679SAndreas Boehler     * This method must return null if the object did not exist.
399*a1a3b679SAndreas Boehler     *
400*a1a3b679SAndreas Boehler     * @param string $calendarId
401*a1a3b679SAndreas Boehler     * @param string $objectUri
402*a1a3b679SAndreas Boehler     * @return array|null
403*a1a3b679SAndreas Boehler     */
404*a1a3b679SAndreas Boehler    function getCalendarObject($calendarId, $objectUri) {
405*a1a3b679SAndreas Boehler
406*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?');
407*a1a3b679SAndreas Boehler        $stmt->execute([$calendarId, $objectUri]);
408*a1a3b679SAndreas Boehler        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
409*a1a3b679SAndreas Boehler
410*a1a3b679SAndreas Boehler        if (!$row) return null;
411*a1a3b679SAndreas Boehler
412*a1a3b679SAndreas Boehler        return [
413*a1a3b679SAndreas Boehler            'id'            => $row['id'],
414*a1a3b679SAndreas Boehler            'uri'           => $row['uri'],
415*a1a3b679SAndreas Boehler            'lastmodified'  => $row['lastmodified'],
416*a1a3b679SAndreas Boehler            'etag'          => '"' . $row['etag'] . '"',
417*a1a3b679SAndreas Boehler            'calendarid'    => $row['calendarid'],
418*a1a3b679SAndreas Boehler            'size'          => (int)$row['size'],
419*a1a3b679SAndreas Boehler            'calendardata'  => $row['calendardata'],
420*a1a3b679SAndreas Boehler            'component'     => strtolower($row['componenttype']),
421*a1a3b679SAndreas Boehler         ];
422*a1a3b679SAndreas Boehler
423*a1a3b679SAndreas Boehler    }
424*a1a3b679SAndreas Boehler
425*a1a3b679SAndreas Boehler    /**
426*a1a3b679SAndreas Boehler     * Returns a list of calendar objects.
427*a1a3b679SAndreas Boehler     *
428*a1a3b679SAndreas Boehler     * This method should work identical to getCalendarObject, but instead
429*a1a3b679SAndreas Boehler     * return all the calendar objects in the list as an array.
430*a1a3b679SAndreas Boehler     *
431*a1a3b679SAndreas Boehler     * If the backend supports this, it may allow for some speed-ups.
432*a1a3b679SAndreas Boehler     *
433*a1a3b679SAndreas Boehler     * @param mixed $calendarId
434*a1a3b679SAndreas Boehler     * @param array $uris
435*a1a3b679SAndreas Boehler     * @return array
436*a1a3b679SAndreas Boehler     */
437*a1a3b679SAndreas Boehler    function getMultipleCalendarObjects($calendarId, array $uris) {
438*a1a3b679SAndreas Boehler
439*a1a3b679SAndreas Boehler        $query = 'SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri IN (';
440*a1a3b679SAndreas Boehler        // Inserting a whole bunch of question marks
441*a1a3b679SAndreas Boehler        $query .= implode(',', array_fill(0, count($uris), '?'));
442*a1a3b679SAndreas Boehler        $query .= ')';
443*a1a3b679SAndreas Boehler
444*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare($query);
445*a1a3b679SAndreas Boehler        $stmt->execute(array_merge([$calendarId], $uris));
446*a1a3b679SAndreas Boehler
447*a1a3b679SAndreas Boehler        $result = [];
448*a1a3b679SAndreas Boehler        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
449*a1a3b679SAndreas Boehler
450*a1a3b679SAndreas Boehler            $result[] = [
451*a1a3b679SAndreas Boehler                'id'           => $row['id'],
452*a1a3b679SAndreas Boehler                'uri'          => $row['uri'],
453*a1a3b679SAndreas Boehler                'lastmodified' => $row['lastmodified'],
454*a1a3b679SAndreas Boehler                'etag'         => '"' . $row['etag'] . '"',
455*a1a3b679SAndreas Boehler                'calendarid'   => $row['calendarid'],
456*a1a3b679SAndreas Boehler                'size'         => (int)$row['size'],
457*a1a3b679SAndreas Boehler                'calendardata' => $row['calendardata'],
458*a1a3b679SAndreas Boehler                'component'    => strtolower($row['componenttype']),
459*a1a3b679SAndreas Boehler            ];
460*a1a3b679SAndreas Boehler
461*a1a3b679SAndreas Boehler        }
462*a1a3b679SAndreas Boehler        return $result;
463*a1a3b679SAndreas Boehler
464*a1a3b679SAndreas Boehler    }
465*a1a3b679SAndreas Boehler
466*a1a3b679SAndreas Boehler
467*a1a3b679SAndreas Boehler    /**
468*a1a3b679SAndreas Boehler     * Creates a new calendar object.
469*a1a3b679SAndreas Boehler     *
470*a1a3b679SAndreas Boehler     * The object uri is only the basename, or filename and not a full path.
471*a1a3b679SAndreas Boehler     *
472*a1a3b679SAndreas Boehler     * It is possible return an etag from this function, which will be used in
473*a1a3b679SAndreas Boehler     * the response to this PUT request. Note that the ETag must be surrounded
474*a1a3b679SAndreas Boehler     * by double-quotes.
475*a1a3b679SAndreas Boehler     *
476*a1a3b679SAndreas Boehler     * However, you should only really return this ETag if you don't mangle the
477*a1a3b679SAndreas Boehler     * calendar-data. If the result of a subsequent GET to this object is not
478*a1a3b679SAndreas Boehler     * the exact same as this request body, you should omit the ETag.
479*a1a3b679SAndreas Boehler     *
480*a1a3b679SAndreas Boehler     * @param mixed $calendarId
481*a1a3b679SAndreas Boehler     * @param string $objectUri
482*a1a3b679SAndreas Boehler     * @param string $calendarData
483*a1a3b679SAndreas Boehler     * @return string|null
484*a1a3b679SAndreas Boehler     */
485*a1a3b679SAndreas Boehler    function createCalendarObject($calendarId, $objectUri, $calendarData) {
486*a1a3b679SAndreas Boehler
487*a1a3b679SAndreas Boehler        $extraData = $this->getDenormalizedData($calendarData);
488*a1a3b679SAndreas Boehler
489*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarObjectTableName . ' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)');
490*a1a3b679SAndreas Boehler        $stmt->execute([
491*a1a3b679SAndreas Boehler            $calendarId,
492*a1a3b679SAndreas Boehler            $objectUri,
493*a1a3b679SAndreas Boehler            $calendarData,
494*a1a3b679SAndreas Boehler            time(),
495*a1a3b679SAndreas Boehler            $extraData['etag'],
496*a1a3b679SAndreas Boehler            $extraData['size'],
497*a1a3b679SAndreas Boehler            $extraData['componentType'],
498*a1a3b679SAndreas Boehler            $extraData['firstOccurence'],
499*a1a3b679SAndreas Boehler            $extraData['lastOccurence'],
500*a1a3b679SAndreas Boehler            $extraData['uid'],
501*a1a3b679SAndreas Boehler        ]);
502*a1a3b679SAndreas Boehler        $this->addChange($calendarId, $objectUri, 1);
503*a1a3b679SAndreas Boehler
504*a1a3b679SAndreas Boehler        return '"' . $extraData['etag'] . '"';
505*a1a3b679SAndreas Boehler
506*a1a3b679SAndreas Boehler    }
507*a1a3b679SAndreas Boehler
508*a1a3b679SAndreas Boehler    /**
509*a1a3b679SAndreas Boehler     * Updates an existing calendarobject, based on it's uri.
510*a1a3b679SAndreas Boehler     *
511*a1a3b679SAndreas Boehler     * The object uri is only the basename, or filename and not a full path.
512*a1a3b679SAndreas Boehler     *
513*a1a3b679SAndreas Boehler     * It is possible return an etag from this function, which will be used in
514*a1a3b679SAndreas Boehler     * the response to this PUT request. Note that the ETag must be surrounded
515*a1a3b679SAndreas Boehler     * by double-quotes.
516*a1a3b679SAndreas Boehler     *
517*a1a3b679SAndreas Boehler     * However, you should only really return this ETag if you don't mangle the
518*a1a3b679SAndreas Boehler     * calendar-data. If the result of a subsequent GET to this object is not
519*a1a3b679SAndreas Boehler     * the exact same as this request body, you should omit the ETag.
520*a1a3b679SAndreas Boehler     *
521*a1a3b679SAndreas Boehler     * @param mixed $calendarId
522*a1a3b679SAndreas Boehler     * @param string $objectUri
523*a1a3b679SAndreas Boehler     * @param string $calendarData
524*a1a3b679SAndreas Boehler     * @return string|null
525*a1a3b679SAndreas Boehler     */
526*a1a3b679SAndreas Boehler    function updateCalendarObject($calendarId, $objectUri, $calendarData) {
527*a1a3b679SAndreas Boehler
528*a1a3b679SAndreas Boehler        $extraData = $this->getDenormalizedData($calendarData);
529*a1a3b679SAndreas Boehler
530*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarObjectTableName . ' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?');
531*a1a3b679SAndreas Boehler        $stmt->execute([$calendarData, time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'], $extraData['uid'], $calendarId, $objectUri]);
532*a1a3b679SAndreas Boehler
533*a1a3b679SAndreas Boehler        $this->addChange($calendarId, $objectUri, 2);
534*a1a3b679SAndreas Boehler
535*a1a3b679SAndreas Boehler        return '"' . $extraData['etag'] . '"';
536*a1a3b679SAndreas Boehler
537*a1a3b679SAndreas Boehler    }
538*a1a3b679SAndreas Boehler
539*a1a3b679SAndreas Boehler    /**
540*a1a3b679SAndreas Boehler     * Parses some information from calendar objects, used for optimized
541*a1a3b679SAndreas Boehler     * calendar-queries.
542*a1a3b679SAndreas Boehler     *
543*a1a3b679SAndreas Boehler     * Returns an array with the following keys:
544*a1a3b679SAndreas Boehler     *   * etag - An md5 checksum of the object without the quotes.
545*a1a3b679SAndreas Boehler     *   * size - Size of the object in bytes
546*a1a3b679SAndreas Boehler     *   * componentType - VEVENT, VTODO or VJOURNAL
547*a1a3b679SAndreas Boehler     *   * firstOccurence
548*a1a3b679SAndreas Boehler     *   * lastOccurence
549*a1a3b679SAndreas Boehler     *   * uid - value of the UID property
550*a1a3b679SAndreas Boehler     *
551*a1a3b679SAndreas Boehler     * @param string $calendarData
552*a1a3b679SAndreas Boehler     * @return array
553*a1a3b679SAndreas Boehler     */
554*a1a3b679SAndreas Boehler    protected function getDenormalizedData($calendarData) {
555*a1a3b679SAndreas Boehler
556*a1a3b679SAndreas Boehler        $vObject = VObject\Reader::read($calendarData);
557*a1a3b679SAndreas Boehler        $componentType = null;
558*a1a3b679SAndreas Boehler        $component = null;
559*a1a3b679SAndreas Boehler        $firstOccurence = null;
560*a1a3b679SAndreas Boehler        $lastOccurence = null;
561*a1a3b679SAndreas Boehler        $uid = null;
562*a1a3b679SAndreas Boehler        foreach ($vObject->getComponents() as $component) {
563*a1a3b679SAndreas Boehler            if ($component->name !== 'VTIMEZONE') {
564*a1a3b679SAndreas Boehler                $componentType = $component->name;
565*a1a3b679SAndreas Boehler                $uid = (string)$component->UID;
566*a1a3b679SAndreas Boehler                break;
567*a1a3b679SAndreas Boehler            }
568*a1a3b679SAndreas Boehler        }
569*a1a3b679SAndreas Boehler        if (!$componentType) {
570*a1a3b679SAndreas Boehler            throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
571*a1a3b679SAndreas Boehler        }
572*a1a3b679SAndreas Boehler        if ($componentType === 'VEVENT') {
573*a1a3b679SAndreas Boehler            $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
574*a1a3b679SAndreas Boehler            // Finding the last occurence is a bit harder
575*a1a3b679SAndreas Boehler            if (!isset($component->RRULE)) {
576*a1a3b679SAndreas Boehler                if (isset($component->DTEND)) {
577*a1a3b679SAndreas Boehler                    $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
578*a1a3b679SAndreas Boehler                } elseif (isset($component->DURATION)) {
579*a1a3b679SAndreas Boehler                    $endDate = clone $component->DTSTART->getDateTime();
580*a1a3b679SAndreas Boehler                    $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue()));
581*a1a3b679SAndreas Boehler                    $lastOccurence = $endDate->getTimeStamp();
582*a1a3b679SAndreas Boehler                } elseif (!$component->DTSTART->hasTime()) {
583*a1a3b679SAndreas Boehler                    $endDate = clone $component->DTSTART->getDateTime();
584*a1a3b679SAndreas Boehler                    $endDate->modify('+1 day');
585*a1a3b679SAndreas Boehler                    $lastOccurence = $endDate->getTimeStamp();
586*a1a3b679SAndreas Boehler                } else {
587*a1a3b679SAndreas Boehler                    $lastOccurence = $firstOccurence;
588*a1a3b679SAndreas Boehler                }
589*a1a3b679SAndreas Boehler            } else {
590*a1a3b679SAndreas Boehler                $it = new VObject\Recur\EventIterator($vObject, (string)$component->UID);
591*a1a3b679SAndreas Boehler                $maxDate = new \DateTime(self::MAX_DATE);
592*a1a3b679SAndreas Boehler                if ($it->isInfinite()) {
593*a1a3b679SAndreas Boehler                    $lastOccurence = $maxDate->getTimeStamp();
594*a1a3b679SAndreas Boehler                } else {
595*a1a3b679SAndreas Boehler                    $end = $it->getDtEnd();
596*a1a3b679SAndreas Boehler                    while ($it->valid() && $end < $maxDate) {
597*a1a3b679SAndreas Boehler                        $end = $it->getDtEnd();
598*a1a3b679SAndreas Boehler                        $it->next();
599*a1a3b679SAndreas Boehler
600*a1a3b679SAndreas Boehler                    }
601*a1a3b679SAndreas Boehler                    $lastOccurence = $end->getTimeStamp();
602*a1a3b679SAndreas Boehler                }
603*a1a3b679SAndreas Boehler
604*a1a3b679SAndreas Boehler            }
605*a1a3b679SAndreas Boehler        }
606*a1a3b679SAndreas Boehler
607*a1a3b679SAndreas Boehler        return [
608*a1a3b679SAndreas Boehler            'etag'           => md5($calendarData),
609*a1a3b679SAndreas Boehler            'size'           => strlen($calendarData),
610*a1a3b679SAndreas Boehler            'componentType'  => $componentType,
611*a1a3b679SAndreas Boehler            'firstOccurence' => $firstOccurence,
612*a1a3b679SAndreas Boehler            'lastOccurence'  => $lastOccurence,
613*a1a3b679SAndreas Boehler            'uid'            => $uid,
614*a1a3b679SAndreas Boehler        ];
615*a1a3b679SAndreas Boehler
616*a1a3b679SAndreas Boehler    }
617*a1a3b679SAndreas Boehler
618*a1a3b679SAndreas Boehler    /**
619*a1a3b679SAndreas Boehler     * Deletes an existing calendar object.
620*a1a3b679SAndreas Boehler     *
621*a1a3b679SAndreas Boehler     * The object uri is only the basename, or filename and not a full path.
622*a1a3b679SAndreas Boehler     *
623*a1a3b679SAndreas Boehler     * @param string $calendarId
624*a1a3b679SAndreas Boehler     * @param string $objectUri
625*a1a3b679SAndreas Boehler     * @return void
626*a1a3b679SAndreas Boehler     */
627*a1a3b679SAndreas Boehler    function deleteCalendarObject($calendarId, $objectUri) {
628*a1a3b679SAndreas Boehler
629*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?');
630*a1a3b679SAndreas Boehler        $stmt->execute([$calendarId, $objectUri]);
631*a1a3b679SAndreas Boehler
632*a1a3b679SAndreas Boehler        $this->addChange($calendarId, $objectUri, 3);
633*a1a3b679SAndreas Boehler
634*a1a3b679SAndreas Boehler    }
635*a1a3b679SAndreas Boehler
636*a1a3b679SAndreas Boehler    /**
637*a1a3b679SAndreas Boehler     * Performs a calendar-query on the contents of this calendar.
638*a1a3b679SAndreas Boehler     *
639*a1a3b679SAndreas Boehler     * The calendar-query is defined in RFC4791 : CalDAV. Using the
640*a1a3b679SAndreas Boehler     * calendar-query it is possible for a client to request a specific set of
641*a1a3b679SAndreas Boehler     * object, based on contents of iCalendar properties, date-ranges and
642*a1a3b679SAndreas Boehler     * iCalendar component types (VTODO, VEVENT).
643*a1a3b679SAndreas Boehler     *
644*a1a3b679SAndreas Boehler     * This method should just return a list of (relative) urls that match this
645*a1a3b679SAndreas Boehler     * query.
646*a1a3b679SAndreas Boehler     *
647*a1a3b679SAndreas Boehler     * The list of filters are specified as an array. The exact array is
648*a1a3b679SAndreas Boehler     * documented by \Sabre\CalDAV\CalendarQueryParser.
649*a1a3b679SAndreas Boehler     *
650*a1a3b679SAndreas Boehler     * Note that it is extremely likely that getCalendarObject for every path
651*a1a3b679SAndreas Boehler     * returned from this method will be called almost immediately after. You
652*a1a3b679SAndreas Boehler     * may want to anticipate this to speed up these requests.
653*a1a3b679SAndreas Boehler     *
654*a1a3b679SAndreas Boehler     * This method provides a default implementation, which parses *all* the
655*a1a3b679SAndreas Boehler     * iCalendar objects in the specified calendar.
656*a1a3b679SAndreas Boehler     *
657*a1a3b679SAndreas Boehler     * This default may well be good enough for personal use, and calendars
658*a1a3b679SAndreas Boehler     * that aren't very large. But if you anticipate high usage, big calendars
659*a1a3b679SAndreas Boehler     * or high loads, you are strongly adviced to optimize certain paths.
660*a1a3b679SAndreas Boehler     *
661*a1a3b679SAndreas Boehler     * The best way to do so is override this method and to optimize
662*a1a3b679SAndreas Boehler     * specifically for 'common filters'.
663*a1a3b679SAndreas Boehler     *
664*a1a3b679SAndreas Boehler     * Requests that are extremely common are:
665*a1a3b679SAndreas Boehler     *   * requests for just VEVENTS
666*a1a3b679SAndreas Boehler     *   * requests for just VTODO
667*a1a3b679SAndreas Boehler     *   * requests with a time-range-filter on a VEVENT.
668*a1a3b679SAndreas Boehler     *
669*a1a3b679SAndreas Boehler     * ..and combinations of these requests. It may not be worth it to try to
670*a1a3b679SAndreas Boehler     * handle every possible situation and just rely on the (relatively
671*a1a3b679SAndreas Boehler     * easy to use) CalendarQueryValidator to handle the rest.
672*a1a3b679SAndreas Boehler     *
673*a1a3b679SAndreas Boehler     * Note that especially time-range-filters may be difficult to parse. A
674*a1a3b679SAndreas Boehler     * time-range filter specified on a VEVENT must for instance also handle
675*a1a3b679SAndreas Boehler     * recurrence rules correctly.
676*a1a3b679SAndreas Boehler     * A good example of how to interprete all these filters can also simply
677*a1a3b679SAndreas Boehler     * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
678*a1a3b679SAndreas Boehler     * as possible, so it gives you a good idea on what type of stuff you need
679*a1a3b679SAndreas Boehler     * to think of.
680*a1a3b679SAndreas Boehler     *
681*a1a3b679SAndreas Boehler     * This specific implementation (for the PDO) backend optimizes filters on
682*a1a3b679SAndreas Boehler     * specific components, and VEVENT time-ranges.
683*a1a3b679SAndreas Boehler     *
684*a1a3b679SAndreas Boehler     * @param string $calendarId
685*a1a3b679SAndreas Boehler     * @param array $filters
686*a1a3b679SAndreas Boehler     * @return array
687*a1a3b679SAndreas Boehler     */
688*a1a3b679SAndreas Boehler    function calendarQuery($calendarId, array $filters) {
689*a1a3b679SAndreas Boehler
690*a1a3b679SAndreas Boehler        $componentType = null;
691*a1a3b679SAndreas Boehler        $requirePostFilter = true;
692*a1a3b679SAndreas Boehler        $timeRange = null;
693*a1a3b679SAndreas Boehler
694*a1a3b679SAndreas Boehler        // if no filters were specified, we don't need to filter after a query
695*a1a3b679SAndreas Boehler        if (!$filters['prop-filters'] && !$filters['comp-filters']) {
696*a1a3b679SAndreas Boehler            $requirePostFilter = false;
697*a1a3b679SAndreas Boehler        }
698*a1a3b679SAndreas Boehler
699*a1a3b679SAndreas Boehler        // Figuring out if there's a component filter
700*a1a3b679SAndreas Boehler        if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
701*a1a3b679SAndreas Boehler            $componentType = $filters['comp-filters'][0]['name'];
702*a1a3b679SAndreas Boehler
703*a1a3b679SAndreas Boehler            // Checking if we need post-filters
704*a1a3b679SAndreas Boehler            if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
705*a1a3b679SAndreas Boehler                $requirePostFilter = false;
706*a1a3b679SAndreas Boehler            }
707*a1a3b679SAndreas Boehler            // There was a time-range filter
708*a1a3b679SAndreas Boehler            if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
709*a1a3b679SAndreas Boehler                $timeRange = $filters['comp-filters'][0]['time-range'];
710*a1a3b679SAndreas Boehler
711*a1a3b679SAndreas Boehler                // If start time OR the end time is not specified, we can do a
712*a1a3b679SAndreas Boehler                // 100% accurate mysql query.
713*a1a3b679SAndreas Boehler                if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
714*a1a3b679SAndreas Boehler                    $requirePostFilter = false;
715*a1a3b679SAndreas Boehler                }
716*a1a3b679SAndreas Boehler            }
717*a1a3b679SAndreas Boehler
718*a1a3b679SAndreas Boehler        }
719*a1a3b679SAndreas Boehler
720*a1a3b679SAndreas Boehler        if ($requirePostFilter) {
721*a1a3b679SAndreas Boehler            $query = "SELECT uri, calendardata FROM " . $this->calendarObjectTableName . " WHERE calendarid = :calendarid";
722*a1a3b679SAndreas Boehler        } else {
723*a1a3b679SAndreas Boehler            $query = "SELECT uri FROM " . $this->calendarObjectTableName . " WHERE calendarid = :calendarid";
724*a1a3b679SAndreas Boehler        }
725*a1a3b679SAndreas Boehler
726*a1a3b679SAndreas Boehler        $values = [
727*a1a3b679SAndreas Boehler            'calendarid' => $calendarId,
728*a1a3b679SAndreas Boehler        ];
729*a1a3b679SAndreas Boehler
730*a1a3b679SAndreas Boehler        if ($componentType) {
731*a1a3b679SAndreas Boehler            $query .= " AND componenttype = :componenttype";
732*a1a3b679SAndreas Boehler            $values['componenttype'] = $componentType;
733*a1a3b679SAndreas Boehler        }
734*a1a3b679SAndreas Boehler
735*a1a3b679SAndreas Boehler        if ($timeRange && $timeRange['start']) {
736*a1a3b679SAndreas Boehler            $query .= " AND lastoccurence > :startdate";
737*a1a3b679SAndreas Boehler            $values['startdate'] = $timeRange['start']->getTimeStamp();
738*a1a3b679SAndreas Boehler        }
739*a1a3b679SAndreas Boehler        if ($timeRange && $timeRange['end']) {
740*a1a3b679SAndreas Boehler            $query .= " AND firstoccurence < :enddate";
741*a1a3b679SAndreas Boehler            $values['enddate'] = $timeRange['end']->getTimeStamp();
742*a1a3b679SAndreas Boehler        }
743*a1a3b679SAndreas Boehler
744*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare($query);
745*a1a3b679SAndreas Boehler        $stmt->execute($values);
746*a1a3b679SAndreas Boehler
747*a1a3b679SAndreas Boehler        $result = [];
748*a1a3b679SAndreas Boehler        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
749*a1a3b679SAndreas Boehler            if ($requirePostFilter) {
750*a1a3b679SAndreas Boehler                if (!$this->validateFilterForObject($row, $filters)) {
751*a1a3b679SAndreas Boehler                    continue;
752*a1a3b679SAndreas Boehler                }
753*a1a3b679SAndreas Boehler            }
754*a1a3b679SAndreas Boehler            $result[] = $row['uri'];
755*a1a3b679SAndreas Boehler
756*a1a3b679SAndreas Boehler        }
757*a1a3b679SAndreas Boehler
758*a1a3b679SAndreas Boehler        return $result;
759*a1a3b679SAndreas Boehler
760*a1a3b679SAndreas Boehler    }
761*a1a3b679SAndreas Boehler
762*a1a3b679SAndreas Boehler    /**
763*a1a3b679SAndreas Boehler     * Searches through all of a users calendars and calendar objects to find
764*a1a3b679SAndreas Boehler     * an object with a specific UID.
765*a1a3b679SAndreas Boehler     *
766*a1a3b679SAndreas Boehler     * This method should return the path to this object, relative to the
767*a1a3b679SAndreas Boehler     * calendar home, so this path usually only contains two parts:
768*a1a3b679SAndreas Boehler     *
769*a1a3b679SAndreas Boehler     * calendarpath/objectpath.ics
770*a1a3b679SAndreas Boehler     *
771*a1a3b679SAndreas Boehler     * If the uid is not found, return null.
772*a1a3b679SAndreas Boehler     *
773*a1a3b679SAndreas Boehler     * This method should only consider * objects that the principal owns, so
774*a1a3b679SAndreas Boehler     * any calendars owned by other principals that also appear in this
775*a1a3b679SAndreas Boehler     * collection should be ignored.
776*a1a3b679SAndreas Boehler     *
777*a1a3b679SAndreas Boehler     * @param string $principalUri
778*a1a3b679SAndreas Boehler     * @param string $uid
779*a1a3b679SAndreas Boehler     * @return string|null
780*a1a3b679SAndreas Boehler     */
781*a1a3b679SAndreas Boehler    function getCalendarObjectByUID($principalUri, $uid) {
782*a1a3b679SAndreas Boehler
783*a1a3b679SAndreas Boehler        $query = <<<SQL
784*a1a3b679SAndreas BoehlerSELECT
785*a1a3b679SAndreas Boehler    calendars.uri AS calendaruri, calendarobjects.uri as objecturi
786*a1a3b679SAndreas BoehlerFROM
787*a1a3b679SAndreas Boehler    $this->calendarObjectTableName AS calendarobjects
788*a1a3b679SAndreas BoehlerLEFT JOIN
789*a1a3b679SAndreas Boehler    $this->calendarTableName AS calendars
790*a1a3b679SAndreas Boehler    ON calendarobjects.calendarid = calendars.id
791*a1a3b679SAndreas BoehlerWHERE
792*a1a3b679SAndreas Boehler    calendars.principaluri = ?
793*a1a3b679SAndreas Boehler    AND
794*a1a3b679SAndreas Boehler    calendarobjects.uid = ?
795*a1a3b679SAndreas BoehlerSQL;
796*a1a3b679SAndreas Boehler
797*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare($query);
798*a1a3b679SAndreas Boehler        $stmt->execute([$principalUri, $uid]);
799*a1a3b679SAndreas Boehler
800*a1a3b679SAndreas Boehler        if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
801*a1a3b679SAndreas Boehler            return $row['calendaruri'] . '/' . $row['objecturi'];
802*a1a3b679SAndreas Boehler        }
803*a1a3b679SAndreas Boehler
804*a1a3b679SAndreas Boehler    }
805*a1a3b679SAndreas Boehler
806*a1a3b679SAndreas Boehler    /**
807*a1a3b679SAndreas Boehler     * The getChanges method returns all the changes that have happened, since
808*a1a3b679SAndreas Boehler     * the specified syncToken in the specified calendar.
809*a1a3b679SAndreas Boehler     *
810*a1a3b679SAndreas Boehler     * This function should return an array, such as the following:
811*a1a3b679SAndreas Boehler     *
812*a1a3b679SAndreas Boehler     * [
813*a1a3b679SAndreas Boehler     *   'syncToken' => 'The current synctoken',
814*a1a3b679SAndreas Boehler     *   'added'   => [
815*a1a3b679SAndreas Boehler     *      'new.txt',
816*a1a3b679SAndreas Boehler     *   ],
817*a1a3b679SAndreas Boehler     *   'modified'   => [
818*a1a3b679SAndreas Boehler     *      'modified.txt',
819*a1a3b679SAndreas Boehler     *   ],
820*a1a3b679SAndreas Boehler     *   'deleted' => [
821*a1a3b679SAndreas Boehler     *      'foo.php.bak',
822*a1a3b679SAndreas Boehler     *      'old.txt'
823*a1a3b679SAndreas Boehler     *   ]
824*a1a3b679SAndreas Boehler     * ];
825*a1a3b679SAndreas Boehler     *
826*a1a3b679SAndreas Boehler     * The returned syncToken property should reflect the *current* syncToken
827*a1a3b679SAndreas Boehler     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
828*a1a3b679SAndreas Boehler     * property this is needed here too, to ensure the operation is atomic.
829*a1a3b679SAndreas Boehler     *
830*a1a3b679SAndreas Boehler     * If the $syncToken argument is specified as null, this is an initial
831*a1a3b679SAndreas Boehler     * sync, and all members should be reported.
832*a1a3b679SAndreas Boehler     *
833*a1a3b679SAndreas Boehler     * The modified property is an array of nodenames that have changed since
834*a1a3b679SAndreas Boehler     * the last token.
835*a1a3b679SAndreas Boehler     *
836*a1a3b679SAndreas Boehler     * The deleted property is an array with nodenames, that have been deleted
837*a1a3b679SAndreas Boehler     * from collection.
838*a1a3b679SAndreas Boehler     *
839*a1a3b679SAndreas Boehler     * The $syncLevel argument is basically the 'depth' of the report. If it's
840*a1a3b679SAndreas Boehler     * 1, you only have to report changes that happened only directly in
841*a1a3b679SAndreas Boehler     * immediate descendants. If it's 2, it should also include changes from
842*a1a3b679SAndreas Boehler     * the nodes below the child collections. (grandchildren)
843*a1a3b679SAndreas Boehler     *
844*a1a3b679SAndreas Boehler     * The $limit argument allows a client to specify how many results should
845*a1a3b679SAndreas Boehler     * be returned at most. If the limit is not specified, it should be treated
846*a1a3b679SAndreas Boehler     * as infinite.
847*a1a3b679SAndreas Boehler     *
848*a1a3b679SAndreas Boehler     * If the limit (infinite or not) is higher than you're willing to return,
849*a1a3b679SAndreas Boehler     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
850*a1a3b679SAndreas Boehler     *
851*a1a3b679SAndreas Boehler     * If the syncToken is expired (due to data cleanup) or unknown, you must
852*a1a3b679SAndreas Boehler     * return null.
853*a1a3b679SAndreas Boehler     *
854*a1a3b679SAndreas Boehler     * The limit is 'suggestive'. You are free to ignore it.
855*a1a3b679SAndreas Boehler     *
856*a1a3b679SAndreas Boehler     * @param string $calendarId
857*a1a3b679SAndreas Boehler     * @param string $syncToken
858*a1a3b679SAndreas Boehler     * @param int $syncLevel
859*a1a3b679SAndreas Boehler     * @param int $limit
860*a1a3b679SAndreas Boehler     * @return array
861*a1a3b679SAndreas Boehler     */
862*a1a3b679SAndreas Boehler    function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
863*a1a3b679SAndreas Boehler
864*a1a3b679SAndreas Boehler        // Current synctoken
865*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('SELECT synctoken FROM ' . $this->calendarTableName . ' WHERE id = ?');
866*a1a3b679SAndreas Boehler        $stmt->execute([ $calendarId ]);
867*a1a3b679SAndreas Boehler        $currentToken = $stmt->fetchColumn(0);
868*a1a3b679SAndreas Boehler
869*a1a3b679SAndreas Boehler        if (is_null($currentToken)) return null;
870*a1a3b679SAndreas Boehler
871*a1a3b679SAndreas Boehler        $result = [
872*a1a3b679SAndreas Boehler            'syncToken' => $currentToken,
873*a1a3b679SAndreas Boehler            'added'     => [],
874*a1a3b679SAndreas Boehler            'modified'  => [],
875*a1a3b679SAndreas Boehler            'deleted'   => [],
876*a1a3b679SAndreas Boehler        ];
877*a1a3b679SAndreas Boehler
878*a1a3b679SAndreas Boehler        if ($syncToken) {
879*a1a3b679SAndreas Boehler
880*a1a3b679SAndreas Boehler            $query = "SELECT uri, operation FROM " . $this->calendarChangesTableName . " WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken";
881*a1a3b679SAndreas Boehler            if ($limit > 0) $query .= " LIMIT " . (int)$limit;
882*a1a3b679SAndreas Boehler
883*a1a3b679SAndreas Boehler            // Fetching all changes
884*a1a3b679SAndreas Boehler            $stmt = $this->pdo->prepare($query);
885*a1a3b679SAndreas Boehler            $stmt->execute([$syncToken, $currentToken, $calendarId]);
886*a1a3b679SAndreas Boehler
887*a1a3b679SAndreas Boehler            $changes = [];
888*a1a3b679SAndreas Boehler
889*a1a3b679SAndreas Boehler            // This loop ensures that any duplicates are overwritten, only the
890*a1a3b679SAndreas Boehler            // last change on a node is relevant.
891*a1a3b679SAndreas Boehler            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
892*a1a3b679SAndreas Boehler
893*a1a3b679SAndreas Boehler                $changes[$row['uri']] = $row['operation'];
894*a1a3b679SAndreas Boehler
895*a1a3b679SAndreas Boehler            }
896*a1a3b679SAndreas Boehler
897*a1a3b679SAndreas Boehler            foreach ($changes as $uri => $operation) {
898*a1a3b679SAndreas Boehler
899*a1a3b679SAndreas Boehler                switch ($operation) {
900*a1a3b679SAndreas Boehler                    case 1 :
901*a1a3b679SAndreas Boehler                        $result['added'][] = $uri;
902*a1a3b679SAndreas Boehler                        break;
903*a1a3b679SAndreas Boehler                    case 2 :
904*a1a3b679SAndreas Boehler                        $result['modified'][] = $uri;
905*a1a3b679SAndreas Boehler                        break;
906*a1a3b679SAndreas Boehler                    case 3 :
907*a1a3b679SAndreas Boehler                        $result['deleted'][] = $uri;
908*a1a3b679SAndreas Boehler                        break;
909*a1a3b679SAndreas Boehler                }
910*a1a3b679SAndreas Boehler
911*a1a3b679SAndreas Boehler            }
912*a1a3b679SAndreas Boehler        } else {
913*a1a3b679SAndreas Boehler            // No synctoken supplied, this is the initial sync.
914*a1a3b679SAndreas Boehler            $query = "SELECT uri FROM " . $this->calendarObjectTableName . " WHERE calendarid = ?";
915*a1a3b679SAndreas Boehler            $stmt = $this->pdo->prepare($query);
916*a1a3b679SAndreas Boehler            $stmt->execute([$calendarId]);
917*a1a3b679SAndreas Boehler
918*a1a3b679SAndreas Boehler            $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
919*a1a3b679SAndreas Boehler        }
920*a1a3b679SAndreas Boehler        return $result;
921*a1a3b679SAndreas Boehler
922*a1a3b679SAndreas Boehler    }
923*a1a3b679SAndreas Boehler
924*a1a3b679SAndreas Boehler    /**
925*a1a3b679SAndreas Boehler     * Adds a change record to the calendarchanges table.
926*a1a3b679SAndreas Boehler     *
927*a1a3b679SAndreas Boehler     * @param mixed $calendarId
928*a1a3b679SAndreas Boehler     * @param string $objectUri
929*a1a3b679SAndreas Boehler     * @param int $operation 1 = add, 2 = modify, 3 = delete.
930*a1a3b679SAndreas Boehler     * @return void
931*a1a3b679SAndreas Boehler     */
932*a1a3b679SAndreas Boehler    protected function addChange($calendarId, $objectUri, $operation) {
933*a1a3b679SAndreas Boehler
934*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarChangesTableName . ' (uri, synctoken, calendarid, operation) SELECT ?, synctoken, ?, ? FROM ' . $this->calendarTableName . ' WHERE id = ?');
935*a1a3b679SAndreas Boehler        $stmt->execute([
936*a1a3b679SAndreas Boehler            $objectUri,
937*a1a3b679SAndreas Boehler            $calendarId,
938*a1a3b679SAndreas Boehler            $operation,
939*a1a3b679SAndreas Boehler            $calendarId
940*a1a3b679SAndreas Boehler        ]);
941*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarTableName . ' SET synctoken = synctoken + 1 WHERE id = ?');
942*a1a3b679SAndreas Boehler        $stmt->execute([
943*a1a3b679SAndreas Boehler            $calendarId
944*a1a3b679SAndreas Boehler        ]);
945*a1a3b679SAndreas Boehler
946*a1a3b679SAndreas Boehler    }
947*a1a3b679SAndreas Boehler
948*a1a3b679SAndreas Boehler    /**
949*a1a3b679SAndreas Boehler     * Returns a list of subscriptions for a principal.
950*a1a3b679SAndreas Boehler     *
951*a1a3b679SAndreas Boehler     * Every subscription is an array with the following keys:
952*a1a3b679SAndreas Boehler     *  * id, a unique id that will be used by other functions to modify the
953*a1a3b679SAndreas Boehler     *    subscription. This can be the same as the uri or a database key.
954*a1a3b679SAndreas Boehler     *  * uri. This is just the 'base uri' or 'filename' of the subscription.
955*a1a3b679SAndreas Boehler     *  * principaluri. The owner of the subscription. Almost always the same as
956*a1a3b679SAndreas Boehler     *    principalUri passed to this method.
957*a1a3b679SAndreas Boehler     *  * source. Url to the actual feed
958*a1a3b679SAndreas Boehler     *
959*a1a3b679SAndreas Boehler     * Furthermore, all the subscription info must be returned too:
960*a1a3b679SAndreas Boehler     *
961*a1a3b679SAndreas Boehler     * 1. {DAV:}displayname
962*a1a3b679SAndreas Boehler     * 2. {http://apple.com/ns/ical/}refreshrate
963*a1a3b679SAndreas Boehler     * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
964*a1a3b679SAndreas Boehler     *    should not be stripped).
965*a1a3b679SAndreas Boehler     * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
966*a1a3b679SAndreas Boehler     *    should not be stripped).
967*a1a3b679SAndreas Boehler     * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
968*a1a3b679SAndreas Boehler     *    attachments should not be stripped).
969*a1a3b679SAndreas Boehler     * 7. {http://apple.com/ns/ical/}calendar-color
970*a1a3b679SAndreas Boehler     * 8. {http://apple.com/ns/ical/}calendar-order
971*a1a3b679SAndreas Boehler     * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
972*a1a3b679SAndreas Boehler     *    (should just be an instance of
973*a1a3b679SAndreas Boehler     *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
974*a1a3b679SAndreas Boehler     *    default components).
975*a1a3b679SAndreas Boehler     *
976*a1a3b679SAndreas Boehler     * @param string $principalUri
977*a1a3b679SAndreas Boehler     * @return array
978*a1a3b679SAndreas Boehler     */
979*a1a3b679SAndreas Boehler    function getSubscriptionsForUser($principalUri) {
980*a1a3b679SAndreas Boehler
981*a1a3b679SAndreas Boehler        $fields = array_values($this->subscriptionPropertyMap);
982*a1a3b679SAndreas Boehler        $fields[] = 'id';
983*a1a3b679SAndreas Boehler        $fields[] = 'uri';
984*a1a3b679SAndreas Boehler        $fields[] = 'source';
985*a1a3b679SAndreas Boehler        $fields[] = 'principaluri';
986*a1a3b679SAndreas Boehler        $fields[] = 'lastmodified';
987*a1a3b679SAndreas Boehler
988*a1a3b679SAndreas Boehler        // Making fields a comma-delimited list
989*a1a3b679SAndreas Boehler        $fields = implode(', ', $fields);
990*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarSubscriptionsTableName . " WHERE principaluri = ? ORDER BY calendarorder ASC");
991*a1a3b679SAndreas Boehler        $stmt->execute([$principalUri]);
992*a1a3b679SAndreas Boehler
993*a1a3b679SAndreas Boehler        $subscriptions = [];
994*a1a3b679SAndreas Boehler        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
995*a1a3b679SAndreas Boehler
996*a1a3b679SAndreas Boehler            $subscription = [
997*a1a3b679SAndreas Boehler                'id'           => $row['id'],
998*a1a3b679SAndreas Boehler                'uri'          => $row['uri'],
999*a1a3b679SAndreas Boehler                'principaluri' => $row['principaluri'],
1000*a1a3b679SAndreas Boehler                'source'       => $row['source'],
1001*a1a3b679SAndreas Boehler                'lastmodified' => $row['lastmodified'],
1002*a1a3b679SAndreas Boehler
1003*a1a3b679SAndreas Boehler                '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
1004*a1a3b679SAndreas Boehler            ];
1005*a1a3b679SAndreas Boehler
1006*a1a3b679SAndreas Boehler            foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
1007*a1a3b679SAndreas Boehler                if (!is_null($row[$dbName])) {
1008*a1a3b679SAndreas Boehler                    $subscription[$xmlName] = $row[$dbName];
1009*a1a3b679SAndreas Boehler                }
1010*a1a3b679SAndreas Boehler            }
1011*a1a3b679SAndreas Boehler
1012*a1a3b679SAndreas Boehler            $subscriptions[] = $subscription;
1013*a1a3b679SAndreas Boehler
1014*a1a3b679SAndreas Boehler        }
1015*a1a3b679SAndreas Boehler
1016*a1a3b679SAndreas Boehler        return $subscriptions;
1017*a1a3b679SAndreas Boehler
1018*a1a3b679SAndreas Boehler    }
1019*a1a3b679SAndreas Boehler
1020*a1a3b679SAndreas Boehler    /**
1021*a1a3b679SAndreas Boehler     * Creates a new subscription for a principal.
1022*a1a3b679SAndreas Boehler     *
1023*a1a3b679SAndreas Boehler     * If the creation was a success, an id must be returned that can be used to reference
1024*a1a3b679SAndreas Boehler     * this subscription in other methods, such as updateSubscription.
1025*a1a3b679SAndreas Boehler     *
1026*a1a3b679SAndreas Boehler     * @param string $principalUri
1027*a1a3b679SAndreas Boehler     * @param string $uri
1028*a1a3b679SAndreas Boehler     * @param array $properties
1029*a1a3b679SAndreas Boehler     * @return mixed
1030*a1a3b679SAndreas Boehler     */
1031*a1a3b679SAndreas Boehler    function createSubscription($principalUri, $uri, array $properties) {
1032*a1a3b679SAndreas Boehler
1033*a1a3b679SAndreas Boehler        $fieldNames = [
1034*a1a3b679SAndreas Boehler            'principaluri',
1035*a1a3b679SAndreas Boehler            'uri',
1036*a1a3b679SAndreas Boehler            'source',
1037*a1a3b679SAndreas Boehler            'lastmodified',
1038*a1a3b679SAndreas Boehler        ];
1039*a1a3b679SAndreas Boehler
1040*a1a3b679SAndreas Boehler        if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
1041*a1a3b679SAndreas Boehler            throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
1042*a1a3b679SAndreas Boehler        }
1043*a1a3b679SAndreas Boehler
1044*a1a3b679SAndreas Boehler        $values = [
1045*a1a3b679SAndreas Boehler            ':principaluri' => $principalUri,
1046*a1a3b679SAndreas Boehler            ':uri'          => $uri,
1047*a1a3b679SAndreas Boehler            ':source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
1048*a1a3b679SAndreas Boehler            ':lastmodified' => time(),
1049*a1a3b679SAndreas Boehler        ];
1050*a1a3b679SAndreas Boehler
1051*a1a3b679SAndreas Boehler        foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
1052*a1a3b679SAndreas Boehler            if (isset($properties[$xmlName])) {
1053*a1a3b679SAndreas Boehler
1054*a1a3b679SAndreas Boehler                $values[':' . $dbName] = $properties[$xmlName];
1055*a1a3b679SAndreas Boehler                $fieldNames[] = $dbName;
1056*a1a3b679SAndreas Boehler            }
1057*a1a3b679SAndreas Boehler        }
1058*a1a3b679SAndreas Boehler
1059*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarSubscriptionsTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")");
1060*a1a3b679SAndreas Boehler        $stmt->execute($values);
1061*a1a3b679SAndreas Boehler
1062*a1a3b679SAndreas Boehler        return $this->pdo->lastInsertId();
1063*a1a3b679SAndreas Boehler
1064*a1a3b679SAndreas Boehler    }
1065*a1a3b679SAndreas Boehler
1066*a1a3b679SAndreas Boehler    /**
1067*a1a3b679SAndreas Boehler     * Updates a subscription
1068*a1a3b679SAndreas Boehler     *
1069*a1a3b679SAndreas Boehler     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
1070*a1a3b679SAndreas Boehler     * To do the actual updates, you must tell this object which properties
1071*a1a3b679SAndreas Boehler     * you're going to process with the handle() method.
1072*a1a3b679SAndreas Boehler     *
1073*a1a3b679SAndreas Boehler     * Calling the handle method is like telling the PropPatch object "I
1074*a1a3b679SAndreas Boehler     * promise I can handle updating this property".
1075*a1a3b679SAndreas Boehler     *
1076*a1a3b679SAndreas Boehler     * Read the PropPatch documenation for more info and examples.
1077*a1a3b679SAndreas Boehler     *
1078*a1a3b679SAndreas Boehler     * @param mixed $subscriptionId
1079*a1a3b679SAndreas Boehler     * @param \Sabre\DAV\PropPatch $propPatch
1080*a1a3b679SAndreas Boehler     * @return void
1081*a1a3b679SAndreas Boehler     */
1082*a1a3b679SAndreas Boehler    function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) {
1083*a1a3b679SAndreas Boehler
1084*a1a3b679SAndreas Boehler        $supportedProperties = array_keys($this->subscriptionPropertyMap);
1085*a1a3b679SAndreas Boehler        $supportedProperties[] = '{http://calendarserver.org/ns/}source';
1086*a1a3b679SAndreas Boehler
1087*a1a3b679SAndreas Boehler        $propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
1088*a1a3b679SAndreas Boehler
1089*a1a3b679SAndreas Boehler            $newValues = [];
1090*a1a3b679SAndreas Boehler
1091*a1a3b679SAndreas Boehler            foreach ($mutations as $propertyName => $propertyValue) {
1092*a1a3b679SAndreas Boehler
1093*a1a3b679SAndreas Boehler                if ($propertyName === '{http://calendarserver.org/ns/}source') {
1094*a1a3b679SAndreas Boehler                    $newValues['source'] = $propertyValue->getHref();
1095*a1a3b679SAndreas Boehler                } else {
1096*a1a3b679SAndreas Boehler                    $fieldName = $this->subscriptionPropertyMap[$propertyName];
1097*a1a3b679SAndreas Boehler                    $newValues[$fieldName] = $propertyValue;
1098*a1a3b679SAndreas Boehler                }
1099*a1a3b679SAndreas Boehler
1100*a1a3b679SAndreas Boehler            }
1101*a1a3b679SAndreas Boehler
1102*a1a3b679SAndreas Boehler            // Now we're generating the sql query.
1103*a1a3b679SAndreas Boehler            $valuesSql = [];
1104*a1a3b679SAndreas Boehler            foreach ($newValues as $fieldName => $value) {
1105*a1a3b679SAndreas Boehler                $valuesSql[] = $fieldName . ' = ?';
1106*a1a3b679SAndreas Boehler            }
1107*a1a3b679SAndreas Boehler
1108*a1a3b679SAndreas Boehler            $stmt = $this->pdo->prepare("UPDATE " . $this->calendarSubscriptionsTableName . " SET " . implode(', ', $valuesSql) . ", lastmodified = ? WHERE id = ?");
1109*a1a3b679SAndreas Boehler            $newValues['lastmodified'] = time();
1110*a1a3b679SAndreas Boehler            $newValues['id'] = $subscriptionId;
1111*a1a3b679SAndreas Boehler            $stmt->execute(array_values($newValues));
1112*a1a3b679SAndreas Boehler
1113*a1a3b679SAndreas Boehler            return true;
1114*a1a3b679SAndreas Boehler
1115*a1a3b679SAndreas Boehler        });
1116*a1a3b679SAndreas Boehler
1117*a1a3b679SAndreas Boehler    }
1118*a1a3b679SAndreas Boehler
1119*a1a3b679SAndreas Boehler    /**
1120*a1a3b679SAndreas Boehler     * Deletes a subscription
1121*a1a3b679SAndreas Boehler     *
1122*a1a3b679SAndreas Boehler     * @param mixed $subscriptionId
1123*a1a3b679SAndreas Boehler     * @return void
1124*a1a3b679SAndreas Boehler     */
1125*a1a3b679SAndreas Boehler    function deleteSubscription($subscriptionId) {
1126*a1a3b679SAndreas Boehler
1127*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarSubscriptionsTableName . ' WHERE id = ?');
1128*a1a3b679SAndreas Boehler        $stmt->execute([$subscriptionId]);
1129*a1a3b679SAndreas Boehler
1130*a1a3b679SAndreas Boehler    }
1131*a1a3b679SAndreas Boehler
1132*a1a3b679SAndreas Boehler    /**
1133*a1a3b679SAndreas Boehler     * Returns a single scheduling object.
1134*a1a3b679SAndreas Boehler     *
1135*a1a3b679SAndreas Boehler     * The returned array should contain the following elements:
1136*a1a3b679SAndreas Boehler     *   * uri - A unique basename for the object. This will be used to
1137*a1a3b679SAndreas Boehler     *           construct a full uri.
1138*a1a3b679SAndreas Boehler     *   * calendardata - The iCalendar object
1139*a1a3b679SAndreas Boehler     *   * lastmodified - The last modification date. Can be an int for a unix
1140*a1a3b679SAndreas Boehler     *                    timestamp, or a PHP DateTime object.
1141*a1a3b679SAndreas Boehler     *   * etag - A unique token that must change if the object changed.
1142*a1a3b679SAndreas Boehler     *   * size - The size of the object, in bytes.
1143*a1a3b679SAndreas Boehler     *
1144*a1a3b679SAndreas Boehler     * @param string $principalUri
1145*a1a3b679SAndreas Boehler     * @param string $objectUri
1146*a1a3b679SAndreas Boehler     * @return array
1147*a1a3b679SAndreas Boehler     */
1148*a1a3b679SAndreas Boehler    function getSchedulingObject($principalUri, $objectUri) {
1149*a1a3b679SAndreas Boehler
1150*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('SELECT uri, calendardata, lastmodified, etag, size FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ? AND uri = ?');
1151*a1a3b679SAndreas Boehler        $stmt->execute([$principalUri, $objectUri]);
1152*a1a3b679SAndreas Boehler        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
1153*a1a3b679SAndreas Boehler
1154*a1a3b679SAndreas Boehler        if (!$row) return null;
1155*a1a3b679SAndreas Boehler
1156*a1a3b679SAndreas Boehler        return [
1157*a1a3b679SAndreas Boehler            'uri'          => $row['uri'],
1158*a1a3b679SAndreas Boehler            'calendardata' => $row['calendardata'],
1159*a1a3b679SAndreas Boehler            'lastmodified' => $row['lastmodified'],
1160*a1a3b679SAndreas Boehler            'etag'         => '"' . $row['etag'] . '"',
1161*a1a3b679SAndreas Boehler            'size'         => (int)$row['size'],
1162*a1a3b679SAndreas Boehler         ];
1163*a1a3b679SAndreas Boehler
1164*a1a3b679SAndreas Boehler    }
1165*a1a3b679SAndreas Boehler
1166*a1a3b679SAndreas Boehler    /**
1167*a1a3b679SAndreas Boehler     * Returns all scheduling objects for the inbox collection.
1168*a1a3b679SAndreas Boehler     *
1169*a1a3b679SAndreas Boehler     * These objects should be returned as an array. Every item in the array
1170*a1a3b679SAndreas Boehler     * should follow the same structure as returned from getSchedulingObject.
1171*a1a3b679SAndreas Boehler     *
1172*a1a3b679SAndreas Boehler     * The main difference is that 'calendardata' is optional.
1173*a1a3b679SAndreas Boehler     *
1174*a1a3b679SAndreas Boehler     * @param string $principalUri
1175*a1a3b679SAndreas Boehler     * @return array
1176*a1a3b679SAndreas Boehler     */
1177*a1a3b679SAndreas Boehler    function getSchedulingObjects($principalUri) {
1178*a1a3b679SAndreas Boehler
1179*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('SELECT id, calendardata, uri, lastmodified, etag, size FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ?');
1180*a1a3b679SAndreas Boehler        $stmt->execute([$principalUri]);
1181*a1a3b679SAndreas Boehler
1182*a1a3b679SAndreas Boehler        $result = [];
1183*a1a3b679SAndreas Boehler        foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1184*a1a3b679SAndreas Boehler            $result[] = [
1185*a1a3b679SAndreas Boehler                'calendardata' => $row['calendardata'],
1186*a1a3b679SAndreas Boehler                'uri'          => $row['uri'],
1187*a1a3b679SAndreas Boehler                'lastmodified' => $row['lastmodified'],
1188*a1a3b679SAndreas Boehler                'etag'         => '"' . $row['etag'] . '"',
1189*a1a3b679SAndreas Boehler                'size'         => (int)$row['size'],
1190*a1a3b679SAndreas Boehler            ];
1191*a1a3b679SAndreas Boehler        }
1192*a1a3b679SAndreas Boehler
1193*a1a3b679SAndreas Boehler        return $result;
1194*a1a3b679SAndreas Boehler
1195*a1a3b679SAndreas Boehler    }
1196*a1a3b679SAndreas Boehler
1197*a1a3b679SAndreas Boehler    /**
1198*a1a3b679SAndreas Boehler     * Deletes a scheduling object
1199*a1a3b679SAndreas Boehler     *
1200*a1a3b679SAndreas Boehler     * @param string $principalUri
1201*a1a3b679SAndreas Boehler     * @param string $objectUri
1202*a1a3b679SAndreas Boehler     * @return void
1203*a1a3b679SAndreas Boehler     */
1204*a1a3b679SAndreas Boehler    function deleteSchedulingObject($principalUri, $objectUri) {
1205*a1a3b679SAndreas Boehler
1206*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ? AND uri = ?');
1207*a1a3b679SAndreas Boehler        $stmt->execute([$principalUri, $objectUri]);
1208*a1a3b679SAndreas Boehler
1209*a1a3b679SAndreas Boehler    }
1210*a1a3b679SAndreas Boehler
1211*a1a3b679SAndreas Boehler    /**
1212*a1a3b679SAndreas Boehler     * Creates a new scheduling object. This should land in a users' inbox.
1213*a1a3b679SAndreas Boehler     *
1214*a1a3b679SAndreas Boehler     * @param string $principalUri
1215*a1a3b679SAndreas Boehler     * @param string $objectUri
1216*a1a3b679SAndreas Boehler     * @param string $objectData
1217*a1a3b679SAndreas Boehler     * @return void
1218*a1a3b679SAndreas Boehler     */
1219*a1a3b679SAndreas Boehler    function createSchedulingObject($principalUri, $objectUri, $objectData) {
1220*a1a3b679SAndreas Boehler
1221*a1a3b679SAndreas Boehler        $stmt = $this->pdo->prepare('INSERT INTO ' . $this->schedulingObjectTableName . ' (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)');
1222*a1a3b679SAndreas Boehler        $stmt->execute([$principalUri, $objectData, $objectUri, time(), md5($objectData), strlen($objectData) ]);
1223*a1a3b679SAndreas Boehler
1224*a1a3b679SAndreas Boehler    }
1225*a1a3b679SAndreas Boehler
1226*a1a3b679SAndreas Boehler}
1227