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