"Fossies" - the Fresh Open Source Software Archive

Member "dav-4.0.3/lib/CalDAV/Backend/PDO.php" (10 Jan 2020, 55333 Bytes) of package /linux/www/dav-4.0.3.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "PDO.php" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 4.0.2_vs_4.0.3.

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