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