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