diff options
Diffstat (limited to 'vendor/sabre/dav/lib')
70 files changed, 3296 insertions, 2098 deletions
diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php b/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php index 7513fb60d..bd8ee7602 100644 --- a/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php +++ b/vendor/sabre/dav/lib/CalDAV/Backend/BackendInterface.php @@ -44,10 +44,12 @@ interface BackendInterface { * If the creation was a success, an id must be returned that can be used to * reference this calendar in other methods, such as updateCalendar. * + * The id can be any type, including ints, strings, objects or array. + * * @param string $principalUri * @param string $calendarUri * @param array $properties - * @return void + * @return mixed */ function createCalendar($principalUri, $calendarUri, array $properties); @@ -63,7 +65,7 @@ interface BackendInterface { * * Read the PropPatch documentation for more info and examples. * - * @param string $path + * @param mixed $calendarId * @param \Sabre\DAV\PropPatch $propPatch * @return void */ diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php b/vendor/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php index 19b9b22a7..9c00a89ef 100644 --- a/vendor/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php +++ b/vendor/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php @@ -43,4 +43,19 @@ interface NotificationSupport extends BackendInterface { */ function deleteNotification($principalUri, NotificationInterface $notification); + /** + * This method is called when a user replied to a request to share. + * + * If the user chose to accept the share, this method should return the + * newly created calendar url. + * + * @param string href The sharee who is replying (often a mailto: address) + * @param int status One of the SharingPlugin::STATUS_* constants + * @param string $calendarUri The url to the calendar thats being shared + * @param string $inReplyTo The unique id this message is a response to + * @param string $summary A description of the reply + * @return null|string + */ + function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null); + } diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/PDO.php b/vendor/sabre/dav/lib/CalDAV/Backend/PDO.php index 76b69dc6e..b1c013d62 100644 --- a/vendor/sabre/dav/lib/CalDAV/Backend/PDO.php +++ b/vendor/sabre/dav/lib/CalDAV/Backend/PDO.php @@ -2,10 +2,11 @@ namespace Sabre\CalDAV\Backend; -use Sabre\VObject; use Sabre\CalDAV; use Sabre\DAV; use Sabre\DAV\Exception\Forbidden; +use Sabre\VObject; +use Sabre\DAV\Xml\Element\Sharee; /** * PDO CalDAV backend @@ -17,7 +18,12 @@ use Sabre\DAV\Exception\Forbidden; * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License */ -class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport { +class PDO extends AbstractBackend + implements + SyncSupport, + SubscriptionSupport, + SchedulingSupport, + SharingSupport { /** * We need to specify a max date, because we need to stop *somewhere* @@ -44,6 +50,16 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S public $calendarTableName = 'calendars'; /** + * The table name that will be used for calendars instances. + * + * A single calendar can have multiple instances, if the calendar is + * shared. + * + * @var string + */ + public $calendarInstancesTableName = 'calendarinstances'; + + /** * The table name that will be used for calendar objects * * @var string @@ -140,16 +156,23 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S function getCalendarsForUser($principalUri) { $fields = array_values($this->propertyMap); - $fields[] = 'id'; + $fields[] = 'calendarid'; $fields[] = 'uri'; $fields[] = 'synctoken'; $fields[] = 'components'; $fields[] = 'principaluri'; $fields[] = 'transparent'; + $fields[] = 'access'; // Making fields a comma-delimited list $fields = implode(', ', $fields); - $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarTableName . " WHERE principaluri = ? ORDER BY calendarorder ASC"); + $stmt = $this->pdo->prepare(<<<SQL +SELECT {$this->calendarInstancesTableName}.id as id, $fields FROM {$this->calendarInstancesTableName} + LEFT JOIN {$this->calendarTableName} ON + {$this->calendarInstancesTableName}.calendarid = {$this->calendarTableName}.id +WHERE principaluri = ? ORDER BY calendarorder ASC +SQL + ); $stmt->execute([$principalUri]); $calendars = []; @@ -161,15 +184,27 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S } $calendar = [ - 'id' => $row['id'], + 'id' => [(int)$row['calendarid'], (int)$row['id']], 'uri' => $row['uri'], 'principaluri' => $row['principaluri'], '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ? $row['synctoken'] : '0'), '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components), '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'), + 'share-resource-uri' => '/ns/share/' . $row['calendarid'], ]; + $calendar['share-access'] = (int)$row['access']; + // 1 = owner, 2 = readonly, 3 = readwrite + if ($row['access'] > 1) { + // We need to find more information about the original owner. + //$stmt2 = $this->pdo->prepare('SELECT principaluri FROM ' . $this->calendarInstancesTableName . ' WHERE access = 1 AND id = ?'); + //$stmt2->execute([$row['id']]); + + // read-only is for backwards compatbility. Might go away in + // the future. + $calendar['read-only'] = (int)$row['access'] === \Sabre\DAV\Sharing\Plugin::ACCESS_READ; + } foreach ($this->propertyMap as $xmlName => $dbName) { $calendar[$xmlName] = $row[$dbName]; @@ -199,31 +234,38 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S $fieldNames = [ 'principaluri', 'uri', - 'synctoken', 'transparent', + 'calendarid', ]; $values = [ ':principaluri' => $principalUri, ':uri' => $calendarUri, - ':synctoken' => 1, ':transparent' => 0, ]; - // Default value + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; - $fieldNames[] = 'components'; if (!isset($properties[$sccs])) { - $values[':components'] = 'VEVENT,VTODO'; + // Default value + $components = 'VEVENT,VTODO'; } else { if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) { throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet'); } - $values[':components'] = implode(',', $properties[$sccs]->getValue()); + $components = implode(',', $properties[$sccs]->getValue()); } $transp = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp'; if (isset($properties[$transp])) { - $values[':transparent'] = $properties[$transp]->getValue() === 'transparent'; + $values[':transparent'] = $properties[$transp]->getValue() === 'transparent' ? 1 : 0; } + $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarTableName . " (synctoken, components) VALUES (1, ?)"); + $stmt->execute([$components]); + + $calendarId = $this->pdo->lastInsertId( + $this->calendarTableName . '_id_seq' + ); + + $values[':calendarid'] = $calendarId; foreach ($this->propertyMap as $xmlName => $dbName) { if (isset($properties[$xmlName])) { @@ -233,10 +275,14 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S } } - $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")"); + $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarInstancesTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")"); + $stmt->execute($values); - return $this->pdo->lastInsertId(); + return [ + $calendarId, + $this->pdo->lastInsertId($this->calendarInstancesTableName . '_id_seq') + ]; } @@ -252,16 +298,21 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S * * Read the PropPatch documenation for more info and examples. * - * @param string $calendarId + * @param mixed $calendarId * @param \Sabre\DAV\PropPatch $propPatch * @return void */ function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + $supportedProperties = array_keys($this->propertyMap); $supportedProperties[] = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp'; - $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) { + $propPatch->handle($supportedProperties, function($mutations) use ($calendarId, $instanceId) { $newValues = []; foreach ($mutations as $propertyName => $propertyValue) { @@ -282,8 +333,8 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S $valuesSql[] = $fieldName . ' = ?'; } - $stmt = $this->pdo->prepare("UPDATE " . $this->calendarTableName . " SET " . implode(', ', $valuesSql) . " WHERE id = ?"); - $newValues['id'] = $calendarId; + $stmt = $this->pdo->prepare("UPDATE " . $this->calendarInstancesTableName . " SET " . implode(', ', $valuesSql) . " WHERE id = ?"); + $newValues['id'] = $instanceId; $stmt->execute(array_values($newValues)); $this->addChange($calendarId, "", 2); @@ -297,19 +348,49 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S /** * Delete a calendar and all it's objects * - * @param string $calendarId + * @param mixed $calendarId * @return void */ function deleteCalendar($calendarId) { - $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?'); - $stmt->execute([$calendarId]); + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; - $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarTableName . ' WHERE id = ?'); - $stmt->execute([$calendarId]); + $stmt = $this->pdo->prepare('SELECT access FROM ' . $this->calendarInstancesTableName . ' where id = ?'); + $stmt->execute([$instanceId]); + $access = (int)$stmt->fetchColumn(); + + if ($access === \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER) { + + /** + * If the user is the owner of the calendar, we delete all data and all + * instances. + **/ + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarChangesTableName . ' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarInstancesTableName . ' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarTableName . ' WHERE id = ?'); + $stmt->execute([$calendarId]); + + } else { + + /** + * If it was an instance of a shared calendar, we only delete that + * instance. + */ + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarInstancesTableName . ' WHERE id = ?'); + $stmt->execute([$instanceId]); + + } - $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarChangesTableName . ' WHERE calendarid = ?'); - $stmt->execute([$calendarId]); } @@ -341,11 +422,16 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S * used/fetched to determine these numbers. If both are specified the * amount of times this is needed is reduced by a great degree. * - * @param string $calendarId + * @param mixed $calendarId * @return array */ function getCalendarObjects($calendarId) { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?'); $stmt->execute([$calendarId]); @@ -354,9 +440,8 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S $result[] = [ 'id' => $row['id'], 'uri' => $row['uri'], - 'lastmodified' => $row['lastmodified'], + 'lastmodified' => (int)$row['lastmodified'], 'etag' => '"' . $row['etag'] . '"', - 'calendarid' => $row['calendarid'], 'size' => (int)$row['size'], 'component' => strtolower($row['componenttype']), ]; @@ -378,12 +463,17 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S * * This method must return null if the object did not exist. * - * @param string $calendarId + * @param mixed $calendarId * @param string $objectUri * @return array|null */ function getCalendarObject($calendarId, $objectUri) { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?'); $stmt->execute([$calendarId, $objectUri]); $row = $stmt->fetch(\PDO::FETCH_ASSOC); @@ -393,9 +483,8 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S return [ 'id' => $row['id'], 'uri' => $row['uri'], - 'lastmodified' => $row['lastmodified'], + 'lastmodified' => (int)$row['lastmodified'], 'etag' => '"' . $row['etag'] . '"', - 'calendarid' => $row['calendarid'], 'size' => (int)$row['size'], 'calendardata' => $row['calendardata'], 'component' => strtolower($row['componenttype']), @@ -417,6 +506,11 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S */ function getMultipleCalendarObjects($calendarId, array $uris) { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + $query = 'SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri IN ('; // Inserting a whole bunch of question marks $query .= implode(',', array_fill(0, count($uris), '?')); @@ -431,9 +525,8 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S $result[] = [ 'id' => $row['id'], 'uri' => $row['uri'], - 'lastmodified' => $row['lastmodified'], + 'lastmodified' => (int)$row['lastmodified'], 'etag' => '"' . $row['etag'] . '"', - 'calendarid' => $row['calendarid'], 'size' => (int)$row['size'], 'calendardata' => $row['calendardata'], 'component' => strtolower($row['componenttype']), @@ -465,6 +558,11 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S */ function createCalendarObject($calendarId, $objectUri, $calendarData) { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + $extraData = $this->getDenormalizedData($calendarData); $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarObjectTableName . ' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)'); @@ -506,6 +604,11 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S */ function updateCalendarObject($calendarId, $objectUri, $calendarData) { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + $extraData = $this->getDenormalizedData($calendarData); $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarObjectTableName . ' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?'); @@ -583,6 +686,10 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S } } + + // Ensure Occurence values are positive + if ($firstOccurence < 0) $firstOccurence = 0; + if ($lastOccurence < 0) $lastOccurence = 0; } // Destroy circular references to PHP will GC the object. @@ -604,12 +711,17 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S * * The object uri is only the basename, or filename and not a full path. * - * @param string $calendarId + * @param mixed $calendarId * @param string $objectUri * @return void */ function deleteCalendarObject($calendarId, $objectUri) { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?'); $stmt->execute([$calendarId, $objectUri]); @@ -665,12 +777,17 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S * This specific implementation (for the PDO) backend optimizes filters on * specific components, and VEVENT time-ranges. * - * @param string $calendarId + * @param mixed $calendarId * @param array $filters * @return array */ function calendarQuery($calendarId, array $filters) { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + $componentType = null; $requirePostFilter = true; $timeRange = null; @@ -766,14 +883,14 @@ class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, S $query = <<<SQL SELECT - calendars.uri AS calendaruri, calendarobjects.uri as objecturi + calendar_instances.uri AS calendaruri, calendarobjects.uri as objecturi FROM $this->calendarObjectTableName AS calendarobjects LEFT JOIN - $this->calendarTableName AS calendars - ON calendarobjects.calendarid = calendars.id + $this->calendarInstancesTableName AS calendar_instances + ON calendarobjects.calendarid = calendar_instances.calendarid WHERE - calendars.principaluri = ? + calendar_instances.principaluri = ? AND calendarobjects.uid = ? SQL; @@ -837,7 +954,7 @@ SQL; * * The limit is 'suggestive'. You are free to ignore it. * - * @param string $calendarId + * @param mixed $calendarId * @param string $syncToken * @param int $syncLevel * @param int $limit @@ -845,6 +962,11 @@ SQL; */ function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + // Current synctoken $stmt = $this->pdo->prepare('SELECT synctoken FROM ' . $this->calendarTableName . ' WHERE id = ?'); $stmt->execute([ $calendarId ]); @@ -1043,7 +1165,9 @@ SQL; $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarSubscriptionsTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")"); $stmt->execute($values); - return $this->pdo->lastInsertId(); + return $this->pdo->lastInsertId( + $this->calendarSubscriptionsTableName . '_id_seq' + ); } @@ -1207,4 +1331,179 @@ SQL; } + /** + * Updates the list of shares. + * + * @param mixed $calendarId + * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees + * @return void + */ + function updateInvites($calendarId, array $sharees) { + + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + $currentInvites = $this->getInvites($calendarId); + list($calendarId, $instanceId) = $calendarId; + + $removeStmt = $this->pdo->prepare("DELETE FROM " . $this->calendarInstancesTableName . " WHERE calendarid = ? AND share_href = ? AND access IN (2,3)"); + $updateStmt = $this->pdo->prepare("UPDATE " . $this->calendarInstancesTableName . " SET access = ?, share_displayname = ?, share_invitestatus = ? WHERE calendarid = ? AND share_href = ?"); + + $insertStmt = $this->pdo->prepare(' +INSERT INTO ' . $this->calendarInstancesTableName . ' + ( + calendarid, + principaluri, + access, + displayname, + uri, + description, + calendarorder, + calendarcolor, + timezone, + transparent, + share_href, + share_displayname, + share_invitestatus + ) + SELECT + ?, + ?, + ?, + displayname, + ?, + description, + calendarorder, + calendarcolor, + timezone, + 1, + ?, + ?, + ? + FROM ' . $this->calendarInstancesTableName . ' WHERE id = ?'); + + foreach ($sharees as $sharee) { + + if ($sharee->access === \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS) { + // if access was set no NOACCESS, it means access for an + // existing sharee was removed. + $removeStmt->execute([$calendarId, $sharee->href]); + continue; + } + + if (is_null($sharee->principal)) { + // If the server could not determine the principal automatically, + // we will mark the invite status as invalid. + $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_INVALID; + } else { + // Because sabre/dav does not yet have an invitation system, + // every invite is automatically accepted for now. + $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED; + } + + foreach ($currentInvites as $oldSharee) { + + if ($oldSharee->href === $sharee->href) { + // This is an update + $sharee->properties = array_merge( + $oldSharee->properties, + $sharee->properties + ); + $updateStmt->execute([ + $sharee->access, + isset($sharee->properties['{DAV:}displayname']) ? $sharee->properties['{DAV:}displayname'] : null, + $sharee->inviteStatus ?: $oldSharee->inviteStatus, + $calendarId, + $sharee->href + ]); + continue 2; + } + + } + // If we got here, it means it was a new sharee + $insertStmt->execute([ + $calendarId, + $sharee->principal, + $sharee->access, + \Sabre\DAV\UUIDUtil::getUUID(), + $sharee->href, + isset($sharee->properties['{DAV:}displayname']) ? $sharee->properties['{DAV:}displayname'] : null, + $sharee->inviteStatus ?: \Sabre\DAV\Sharing\Plugin::INVITE_NORESPONSE, + $instanceId + ]); + + } + + } + + /** + * Returns the list of people whom a calendar is shared with. + * + * Every item in the returned list must be a Sharee object with at + * least the following properties set: + * $href + * $shareAccess + * $inviteStatus + * + * and optionally: + * $properties + * + * @param mixed $calendarId + * @return \Sabre\DAV\Xml\Element\Sharee[] + */ + function getInvites($calendarId) { + + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to getInvites() is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $query = <<<SQL +SELECT + principaluri, + access, + share_href, + share_displayname, + share_invitestatus +FROM {$this->calendarInstancesTableName} +WHERE + calendarid = ? +SQL; + + $stmt = $this->pdo->prepare($query); + $stmt->execute([$calendarId]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $result[] = new Sharee([ + 'href' => isset($row['share_href']) ? $row['share_href'] : \Sabre\HTTP\encodePath($row['principaluri']), + 'access' => (int)$row['access'], + /// Everyone is always immediately accepted, for now. + 'inviteStatus' => (int)$row['share_invitestatus'], + 'properties' => + !empty($row['share_displayname']) + ? [ '{DAV:}displayname' => $row['share_displayname'] ] + : [], + 'principal' => $row['principaluri'], + ]); + + } + return $result; + + } + + /** + * Publishes a calendar + * + * @param mixed $calendarId + * @param bool $value + * @return void + */ + function setPublishStatus($calendarId, $value) { + + throw new \Exception('Not implemented'); + + } + } diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/SharingSupport.php b/vendor/sabre/dav/lib/CalDAV/Backend/SharingSupport.php index 6a11b0ab1..8b6e074e0 100644 --- a/vendor/sabre/dav/lib/CalDAV/Backend/SharingSupport.php +++ b/vendor/sabre/dav/lib/CalDAV/Backend/SharingSupport.php @@ -5,231 +5,48 @@ namespace Sabre\CalDAV\Backend; /** * Adds support for sharing features to a CalDAV server. * - * Note: This feature is experimental, and may change in between different - * SabreDAV versions. + * CalDAV backends that implement this interface, must make the following + * modifications to getCalendarsForUser: * - * Early warning: Currently SabreDAV provides no implementation for this. This - * is, because in it's current state there is no elegant way to do this. - * The problem lies in the fact that a real CalDAV server with sharing support - * would first need email support (with invite notifications), and really also - * a browser-frontend that allows people to accept or reject these shares. - * - * In addition, the CalDAV backends are currently kept as independent as - * possible, and should not be aware of principals, email addresses or - * accounts. - * - * Adding an implementation for Sharing to standard-sabredav would contradict - * these goals, so for this reason this is currently not implemented, although - * it may very well in the future; but probably not before SabreDAV 2.0. - * - * The interface works however, so if you implement all this, and do it - * correctly sharing _will_ work. It's not particularly easy, and I _urge you_ - * to make yourself acquainted with the following document first: - * - * https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt - * - * An overview - * =========== - * - * Implementing this interface will allow a user to share his or her calendars - * to other users. Effectively, when a calendar is shared the calendar will - * show up in both the Sharer's and Sharee's calendar-home root. - * This interface adds a few methods that ensure that this happens, and there - * are also a number of new requirements in the base-class you must now follow. - * - * - * How it works - * ============ - * - * When a user shares a calendar, the updateShares() method will be called with - * a list of sharees that are now added, and a list of sharees that have been - * removed. - * Removal is instant, but when a sharee is added the sharee first gets a - * chance to accept or reject the invitation for a share. - * - * After a share is accepted, the calendar will be returned from - * getUserCalendars for both the sharer, and the sharee. - * - * If the sharee deletes the calendar, only their share gets deleted. When the - * owner deletes a calendar, it will be removed for everybody. - * - * - * Notifications - * ============= - * - * During all these sharing operations, a lot of notifications are sent back - * and forward. - * - * Whenever the list of sharees for a calendar has been changed (they have been - * added, removed or modified) all sharees should get a notification for this - * change. - * This notification is always represented by: - * - * Sabre\CalDAV\Notifications\Notification\Invite - * - * In the case of an invite, the sharee may reply with an 'accept' or - * 'decline'. These are always represented by: - * - * Sabre\CalDAV\Notifications\Notification\InviteReply - * - * - * Calendar access by sharees - * ========================== - * - * As mentioned earlier, shared calendars must now also be returned for - * getCalendarsForUser for sharees. A few things change though. - * - * The following properties must be specified: - * - * 1. {http://calendarserver.org/ns/}shared-url - * - * This property MUST contain the url to the original calendar, that is.. the - * path to the calendar from the owner. - * - * 2. {http://sabredav.org/ns}owner-principal - * - * This is a url to to the principal who is sharing the calendar. - * - * 3. {http://sabredav.org/ns}read-only - * - * This should be either 0 or 1, depending on if the user has read-only or - * read-write access to the calendar. - * - * Only when this is done, the calendar will correctly be marked as a calendar - * that's shared to him, thus allowing clients to display the correct interface - * and ACL enforcement. - * - * If a sharee deletes their calendar, only their instance of the calendar - * should be deleted, the original should still exists. - * Pretty much any 'dead' WebDAV properties on these shared calendars should be - * specific to a user. This means that if the displayname is changed by a - * sharee, the original is not affected. This is also true for: - * * The description - * * The color - * * The order - * * And any other dead properties. - * - * Properties like a ctag should not be different for multiple instances of the - * calendar. - * - * Lastly, objects *within* calendars should also have user-specific data. The - * two things that are user-specific are: - * * VALARM objects - * * The TRANSP property - * - * This _also_ implies that if a VALARM is deleted by a sharee for some event, - * this has no effect on the original VALARM. - * - * Understandably, the this last requirement is one of the hardest. - * Realisticly, I can see people ignoring this part of the spec, but that could - * cause a different set of issues. - * - * - * Publishing - * ========== - * - * When a user publishes a url, the server should generate a 'publish url'. - * This is a read-only url, anybody can use to consume the calendar feed. - * - * Calendars are in one of two states: - * * published - * * unpublished - * - * If a calendar is published, the following property should be returned - * for each calendar in getCalendarsForUser. - * - * {http://calendarserver.org/ns/}publish-url - * - * This element should contain a {DAV:}href element, which points to the - * public url that does not require authentication. Unlike every other href, - * this url must be absolute. - * - * Ideally, the following property is always returned - * - * {http://calendarserver.org/ns/}pre-publish-url - * - * This property should contain the url that the calendar _would_ have, if it - * were to be published. iCal uses this to display the url, before the user - * will actually publish it. - * - * - * Selectively disabling publish or share feature - * ============================================== - * - * If Sabre\CalDAV\Property\AllowedSharingModes is returned from - * getCalendarsForUser, this allows the server to specify whether either sharing, - * or publishing is supported. - * - * This allows a client to determine in advance which features are available, - * and update the interface appropriately. If this property is not returned by - * the backend, the SharingPlugin automatically injects it and assumes both - * features are available. + * 1. Return shared calendars for users. + * 2. For every calendar, return calendar-resource-uri. This strings is a URI or + * relative URI reference that must be unique for every calendar, but + * identical for every instance of the same shared calenar. + * 3. For every calenar, you must return a share-access element. This element + * should contain one of the Sabre\DAV\Sharing\Plugin:ACCESS_* contants and + * indicates the access level the user has. * * @copyright Copyright (C) fruux GmbH (https://fruux.com/) * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License */ -interface SharingSupport extends NotificationSupport { +interface SharingSupport extends BackendInterface { /** * Updates the list of shares. * - * The first array is a list of people that are to be added to the - * calendar. - * - * Every element in the add array has the following properties: - * * href - A url. Usually a mailto: address - * * commonName - Usually a first and last name, or false - * * summary - A description of the share, can also be false - * * readOnly - A boolean value - * - * Every element in the remove array is just the address string. - * - * Note that if the calendar is currently marked as 'not shared' by and - * this method is called, the calendar should be 'upgraded' to a shared - * calendar. - * * @param mixed $calendarId - * @param array $add - * @param array $remove + * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees * @return void */ - function updateShares($calendarId, array $add, array $remove); + function updateInvites($calendarId, array $sharees); /** * Returns the list of people whom this calendar is shared with. * - * Every element in this array should have the following properties: - * * href - Often a mailto: address - * * commonName - Optional, for example a first + last name - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. - * * readOnly - boolean - * * summary - Optional, a description for the share + * Every item in the returned list must be a Sharee object with at + * least the following properties set: + * $href + * $shareAccess + * $inviteStatus * - * This method may be called by either the original instance of the - * calendar, as well as the shared instances. In the case of the shared - * instances, it is perfectly acceptable to return an empty array in case - * there are privacy concerns. + * and optionally: + * $properties * * @param mixed $calendarId - * @return array - */ - function getShares($calendarId); - - /** - * This method is called when a user replied to a request to share. - * - * If the user chose to accept the share, this method should return the - * newly created calendar url. - * - * @param string href The sharee who is replying (often a mailto: address) - * @param int status One of the SharingPlugin::STATUS_* constants - * @param string $calendarUri The url to the calendar thats being shared - * @param string $inReplyTo The unique id this message is a response to - * @param string $summary A description of the reply - * @return null|string + * @return \Sabre\DAV\Xml\Element\Sharee[] */ - function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null); + function getInvites($calendarId); /** * Publishes a calendar diff --git a/vendor/sabre/dav/lib/CalDAV/Backend/SimplePDO.php b/vendor/sabre/dav/lib/CalDAV/Backend/SimplePDO.php new file mode 100644 index 000000000..f8238ea9a --- /dev/null +++ b/vendor/sabre/dav/lib/CalDAV/Backend/SimplePDO.php @@ -0,0 +1,296 @@ +<?php + +namespace Sabre\CalDAV\Backend; + +use Sabre\CalDAV; +use Sabre\DAV; + +/** + * Simple PDO CalDAV backend. + * + * This class is basically the most minmum example to get a caldav backend up + * and running. This class uses the following schema (MySQL example): + * + * CREATE TABLE simple_calendars ( + * id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + * uri VARBINARY(200) NOT NULL, + * principaluri VARBINARY(200) NOT NULL + * ); + * + * CREATE TABLE simple_calendarobjects ( + * id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + * calendarid INT UNSIGNED NOT NULL, + * uri VARBINARY(200) NOT NULL, + * calendardata MEDIUMBLOB + * ) + * + * To make this class work, you absolutely need to have the PropertyStorage + * plugin enabled. + * + * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class SimplePDO extends AbstractBackend { + + /** + * pdo + * + * @var \PDO + */ + protected $pdo; + + /** + * Creates the backend + * + * @param \PDO $pdo + */ + function __construct(\PDO $pdo) { + + $this->pdo = $pdo; + + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the calendar. + * * principaluri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * @return array + */ + function getCalendarsForUser($principalUri) { + + // Making fields a comma-delimited list + $stmt = $this->pdo->prepare("SELECT id, uri FROM simple_calendars WHERE principaluri = ? ORDER BY id ASC"); + $stmt->execute([$principalUri]); + + $calendars = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + + $calendars[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $principalUri, + ]; + + } + + return $calendars; + + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used + * to reference this calendar in other methods, such as updateCalendar. + * + * @param string $principalUri + * @param string $calendarUri + * @param array $properties + * @return string + */ + function createCalendar($principalUri, $calendarUri, array $properties) { + + $stmt = $this->pdo->prepare("INSERT INTO simple_calendars (principaluri, uri) VALUES (?, ?)"); + $stmt->execute([$principalUri, $calendarUri]); + + return $this->pdo->lastInsertId(); + + } + + /** + * Delete a calendar and all it's objects + * + * @param string $calendarId + * @return void + */ + function deleteCalendar($calendarId) { + + $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM simple_calendars WHERE id = ?'); + $stmt->execute([$calendarId]); + + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * ' "abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param string $calendarId + * @return array + */ + function getCalendarObjects($calendarId) { + + $stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'etag' => '"' . md5($row['calendardata']) . '"', + 'calendarid' => $calendarId, + 'size' => strlen($row['calendardata']), + 'calendardata' => $row['calendardata'], + ]; + } + + return $result; + + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param string $calendarId + * @param string $objectUri + * @return array|null + */ + function getCalendarObject($calendarId, $objectUri) { + + $stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) return null; + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'etag' => '"' . md5($row['calendardata']) . '"', + 'calendarid' => $calendarId, + 'size' => strlen($row['calendardata']), + 'calendardata' => $row['calendardata'], + ]; + + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * @return string|null + */ + function createCalendarObject($calendarId, $objectUri, $calendarData) { + + $stmt = $this->pdo->prepare('INSERT INTO simple_calendarobjects (calendarid, uri, calendardata) VALUES (?,?,?)'); + $stmt->execute([ + $calendarId, + $objectUri, + $calendarData + ]); + + return '"' . md5($calendarData) . '"'; + + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * @return string|null + */ + function updateCalendarObject($calendarId, $objectUri, $calendarData) { + + $stmt = $this->pdo->prepare('UPDATE simple_calendarobjects SET calendardata = ? WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarData, $calendarId, $objectUri]); + + return '"' . md5($calendarData) . '"'; + + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param string $calendarId + * @param string $objectUri + * @return void + */ + function deleteCalendarObject($calendarId, $objectUri) { + + $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + + } + +} diff --git a/vendor/sabre/dav/lib/CalDAV/Calendar.php b/vendor/sabre/dav/lib/CalDAV/Calendar.php index ff8e19b15..90ace0d21 100644 --- a/vendor/sabre/dav/lib/CalDAV/Calendar.php +++ b/vendor/sabre/dav/lib/CalDAV/Calendar.php @@ -18,6 +18,8 @@ use Sabre\DAV\PropPatch; */ class Calendar implements ICalendar, DAV\IProperties, DAV\Sync\ISyncCollection, DAV\IMultiGet { + use DAVACL\ACLTrait; + /** * This is an array with calendar information * @@ -86,7 +88,7 @@ class Calendar implements ICalendar, DAV\IProperties, DAV\Sync\ISyncCollection, foreach ($this->calendarInfo as $propName => $propValue) { - if ($propName[0] === '{') + if (!is_null($propValue) && $propName[0] === '{') $response[$propName] = $this->calendarInfo[$propName]; } @@ -227,7 +229,7 @@ class Calendar implements ICalendar, DAV\IProperties, DAV\Sync\ISyncCollection, /** * Returns the last modification date as a unix timestamp. * - * @return void + * @return null */ function getLastModified() { @@ -249,19 +251,6 @@ class Calendar implements ICalendar, DAV\IProperties, DAV\Sync\ISyncCollection, } /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** * Returns a list of ACE's for this node. * * Each ACE has the following properties: @@ -360,50 +349,6 @@ class Calendar implements ICalendar, DAV\IProperties, DAV\Sync\ISyncCollection, } - /** - * Updates the ACL - * - * This method will receive a list of new ACE's. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet(); - - // We need to inject 'read-free-busy' in the tree, aggregated under - // {DAV:}read. - foreach ($default['aggregates'] as &$agg) { - - if ($agg['privilege'] !== '{DAV:}read') continue; - - $agg['aggregates'][] = [ - 'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy', - ]; - - } - return $default; - - } /** * Performs a calendar-query on the contents of this calendar. diff --git a/vendor/sabre/dav/lib/CalDAV/CalendarHome.php b/vendor/sabre/dav/lib/CalDAV/CalendarHome.php index a53f829e2..0a4bfb68f 100644 --- a/vendor/sabre/dav/lib/CalDAV/CalendarHome.php +++ b/vendor/sabre/dav/lib/CalDAV/CalendarHome.php @@ -22,6 +22,8 @@ use Sabre\HTTP\URLUtil; */ class CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL { + use DAVACL\ACLTrait; + /** * CalDAV backend * @@ -147,11 +149,7 @@ class CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL { foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) { if ($calendar['uri'] === $name) { if ($this->caldavBackend instanceof Backend\SharingSupport) { - if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) { - return new SharedCalendar($this->caldavBackend, $calendar); - } else { - return new ShareableCalendar($this->caldavBackend, $calendar); - } + return new SharedCalendar($this->caldavBackend, $calendar); } else { return new Calendar($this->caldavBackend, $calendar); } @@ -198,11 +196,7 @@ class CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL { $objs = []; foreach ($calendars as $calendar) { if ($this->caldavBackend instanceof Backend\SharingSupport) { - if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) { - $objs[] = new SharedCalendar($this->caldavBackend, $calendar); - } else { - $objs[] = new ShareableCalendar($this->caldavBackend, $calendar); - } + $objs[] = new SharedCalendar($this->caldavBackend, $calendar); } else { $objs[] = new Calendar($this->caldavBackend, $calendar); } @@ -278,11 +272,9 @@ class CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL { } /** - * Returns the owner principal + * Returns the owner of the calendar home. * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null + * @return string */ function getOwner() { @@ -291,19 +283,6 @@ class CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL { } /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** * Returns a list of ACE's for this node. * * Each ACE has the following properties: @@ -348,37 +327,6 @@ class CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL { } - /** - * Updates the ACL - * - * This method will receive a list of new ACE's. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } /** * This method is called when a user replied to a request to share. diff --git a/vendor/sabre/dav/lib/CalDAV/CalendarObject.php b/vendor/sabre/dav/lib/CalDAV/CalendarObject.php index 393ca4cbd..001b35112 100644 --- a/vendor/sabre/dav/lib/CalDAV/CalendarObject.php +++ b/vendor/sabre/dav/lib/CalDAV/CalendarObject.php @@ -11,6 +11,8 @@ namespace Sabre\CalDAV; */ class CalendarObject extends \Sabre\DAV\File implements ICalendarObject, \Sabre\DAVACL\IACL { + use \Sabre\DAVACL\ACLTrait; + /** * Sabre\CalDAV\Backend\BackendInterface * @@ -192,19 +194,6 @@ class CalendarObject extends \Sabre\DAV\File implements ICalendarObject, \Sabre\ } /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** * Returns a list of ACE's for this node. * * Each ACE has the following properties: @@ -226,22 +215,12 @@ class CalendarObject extends \Sabre\DAV\File implements ICalendarObject, \Sabre\ // The default ACL return [ [ - 'privilege' => '{DAV:}read', - 'principal' => $this->calendarInfo['principaluri'], - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}write', + 'privilege' => '{DAV:}all', 'principal' => $this->calendarInfo['principaluri'], 'protected' => true, ], [ - 'privilege' => '{DAV:}read', - 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}write', + 'privilege' => '{DAV:}all', 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', 'protected' => true, ], @@ -255,36 +234,4 @@ class CalendarObject extends \Sabre\DAV\File implements ICalendarObject, \Sabre\ } - /** - * Updates the ACL - * - * This method will receive a list of new ACE's. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new \Sabre\DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php b/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php index 8c296d50f..a3a824c71 100644 --- a/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php +++ b/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php @@ -170,13 +170,13 @@ class ICSExportPlugin extends DAV\ServerPlugin { protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) { $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data'; + $calendarNode = $this->server->tree->getNodeForPath($path); $blobs = []; if ($start || $end || $componentType) { // If there was a start or end filter, we need to enlist // calendarQuery for speed. - $calendarNode = $this->server->tree->getNodeForPath($path); $queryResult = $calendarNode->calendarQuery([ 'name' => 'VCALENDAR', 'comp-filters' => [ @@ -246,17 +246,29 @@ class ICSExportPlugin extends DAV\ServerPlugin { $mergedCalendar = $mergedCalendar->expand($start, $end, $calendarTimeZone); } - $response->setHeader('Content-Type', $format); + $filenameExtension = '.ics'; switch ($format) { case 'text/calendar' : $mergedCalendar = $mergedCalendar->serialize(); + $filenameExtension = '.ics'; break; case 'application/calendar+json' : $mergedCalendar = json_encode($mergedCalendar->jsonSerialize()); + $filenameExtension = '.json'; break; } + $filename = preg_replace( + '/[^a-zA-Z0-9-_ ]/um', + '', + $calendarNode->getName() + ); + $filename .= '-' . date('Y-m-d') . $filenameExtension; + + $response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); + $response->setHeader('Content-Type', $format); + $response->setStatus(200); $response->setBody($mergedCalendar); @@ -272,11 +284,11 @@ class ICSExportPlugin extends DAV\ServerPlugin { function mergeObjects(array $properties, array $inputObjects) { $calendar = new VObject\Component\VCalendar(); - $calendar->version = '2.0'; + $calendar->VERSION = '2.0'; if (DAV\Server::$exposeVersion) { - $calendar->prodid = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN'; + $calendar->PRODID = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN'; } else { - $calendar->prodid = '-//SabreDAV//SabreDAV//EN'; + $calendar->PRODID = '-//SabreDAV//SabreDAV//EN'; } if (isset($properties['{DAV:}displayname'])) { $calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname']; diff --git a/vendor/sabre/dav/lib/CalDAV/IShareableCalendar.php b/vendor/sabre/dav/lib/CalDAV/IShareableCalendar.php deleted file mode 100644 index cfda91a55..000000000 --- a/vendor/sabre/dav/lib/CalDAV/IShareableCalendar.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php - -namespace Sabre\CalDAV; - -/** - * This interface represents a Calendar that can be shared with other users. - * - * @copyright Copyright (C) fruux GmbH (https://fruux.com/) - * @author Evert Pot (http://evertpot.com/) - * @license http://sabre.io/license/ Modified BSD License - */ -interface IShareableCalendar extends ICalendar { - - /** - * Updates the list of shares. - * - * The first array is a list of people that are to be added to the - * calendar. - * - * Every element in the add array has the following properties: - * * href - A url. Usually a mailto: address - * * commonName - Usually a first and last name, or false - * * summary - A description of the share, can also be false - * * readOnly - A boolean value - * - * Every element in the remove array is just the address string. - * - * @param array $add - * @param array $remove - * @return void - */ - function updateShares(array $add, array $remove); - - /** - * Returns the list of people whom this calendar is shared with. - * - * Every element in this array should have the following properties: - * * href - Often a mailto: address - * * commonName - Optional, for example a first + last name - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. - * * readOnly - boolean - * * summary - Optional, a description for the share - * - * @return array - */ - function getShares(); - -} diff --git a/vendor/sabre/dav/lib/CalDAV/ISharedCalendar.php b/vendor/sabre/dav/lib/CalDAV/ISharedCalendar.php index 84442ac21..15f3b5335 100644 --- a/vendor/sabre/dav/lib/CalDAV/ISharedCalendar.php +++ b/vendor/sabre/dav/lib/CalDAV/ISharedCalendar.php @@ -2,6 +2,8 @@ namespace Sabre\CalDAV; +use Sabre\DAV\Sharing\ISharedNode; + /** * This interface represents a Calendar that is shared by a different user. * @@ -9,28 +11,16 @@ namespace Sabre\CalDAV; * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License */ -interface ISharedCalendar extends ICalendar { - - /** - * This method should return the url of the owners' copy of the shared - * calendar. - * - * @return string - */ - function getSharedUrl(); +interface ISharedCalendar extends ISharedNode { /** - * Returns the list of people whom this calendar is shared with. + * Marks this calendar as published. * - * Every element in this array should have the following properties: - * * href - Often a mailto: address - * * commonName - Optional, for example a first + last name - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. - * * readOnly - boolean - * * summary - Optional, a description for the share + * Publishing a calendar should automatically create a read-only, public, + * subscribable calendar. * - * @return array + * @param bool $value + * @return void */ - function getShares(); - + function setPublishStatus($value); } diff --git a/vendor/sabre/dav/lib/CalDAV/Notifications/Collection.php b/vendor/sabre/dav/lib/CalDAV/Notifications/Collection.php index 1fcc1171c..5fda61dfa 100644 --- a/vendor/sabre/dav/lib/CalDAV/Notifications/Collection.php +++ b/vendor/sabre/dav/lib/CalDAV/Notifications/Collection.php @@ -22,6 +22,8 @@ use Sabre\DAVACL; */ class Collection extends DAV\Collection implements ICollection, DAVACL\IACL { + use DAVACL\ACLTrait; + /** * The notification backend * @@ -96,78 +98,4 @@ class Collection extends DAV\Collection implements ICollection, DAVACL\IACL { } - /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** - * Returns a list of ACE's for this node. - * - * Each ACE has the following properties: - * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are - * currently the only supported privileges - * * 'principal', a url to the principal who owns the node - * * 'protected' (optional), indicating that this ACE is not allowed to - * be updated. - * - * @return array - */ - function getACL() { - - return [ - [ - 'principal' => $this->getOwner(), - 'privilege' => '{DAV:}read', - 'protected' => true, - ], - [ - 'principal' => $this->getOwner(), - 'privilege' => '{DAV:}write', - 'protected' => true, - ] - ]; - - } - - /** - * Updates the ACL - * - * This method will receive a list of new ACE's as an array argument. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/CalDAV/Notifications/Node.php b/vendor/sabre/dav/lib/CalDAV/Notifications/Node.php index 47e78d5de..11df0c94b 100644 --- a/vendor/sabre/dav/lib/CalDAV/Notifications/Node.php +++ b/vendor/sabre/dav/lib/CalDAV/Notifications/Node.php @@ -20,6 +20,8 @@ use Sabre\DAVACL; */ class Node extends DAV\File implements INode, DAVACL\IACL { + use DAVACL\ACLTrait; + /** * The notification backend * @@ -116,78 +118,4 @@ class Node extends DAV\File implements INode, DAVACL\IACL { } - /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** - * Returns a list of ACE's for this node. - * - * Each ACE has the following properties: - * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are - * currently the only supported privileges - * * 'principal', a url to the principal who owns the node - * * 'protected' (optional), indicating that this ACE is not allowed to - * be updated. - * - * @return array - */ - function getACL() { - - return [ - [ - 'principal' => $this->getOwner(), - 'privilege' => '{DAV:}read', - 'protected' => true, - ], - [ - 'principal' => $this->getOwner(), - 'privilege' => '{DAV:}write', - 'protected' => true, - ] - ]; - - } - - /** - * Updates the ACL - * - * This method will receive a list of new ACE's as an array argument. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/CalDAV/Plugin.php b/vendor/sabre/dav/lib/CalDAV/Plugin.php index 663490023..71ba75206 100644 --- a/vendor/sabre/dav/lib/CalDAV/Plugin.php +++ b/vendor/sabre/dav/lib/CalDAV/Plugin.php @@ -5,8 +5,9 @@ namespace Sabre\CalDAV; use DateTimeZone; use Sabre\DAV; use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\INode; use Sabre\DAV\MkCol; -use Sabre\DAV\Xml\Property\Href; +use Sabre\DAV\Xml\Property\LocalHref; use Sabre\DAVACL; use Sabre\VObject; use Sabre\HTTP; @@ -186,6 +187,7 @@ class Plugin extends DAV\ServerPlugin { $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); $server->on('afterMethod:GET', [$this, 'httpAfterGET']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); $server->xml->namespaceMap[self::NS_CALDAV] = 'cal'; $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; @@ -233,9 +235,10 @@ class Plugin extends DAV\ServerPlugin { * * @param string $reportName * @param mixed $report + * @param mixed $path * @return bool */ - function report($reportName, $report) { + function report($reportName, $report, $path) { switch ($reportName) { case '{' . self::NS_CALDAV . '}calendar-multiget' : @@ -341,7 +344,7 @@ class Plugin extends DAV\ServerPlugin { $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl); if (is_null($calendarHomePath)) return null; - return new Href($calendarHomePath . '/'); + return new LocalHref($calendarHomePath . '/'); }); // The calendar-user-address-set property is basically mapped to @@ -349,7 +352,7 @@ class Plugin extends DAV\ServerPlugin { $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-address-set', function() use ($node) { $addresses = $node->getAlternateUriSet(); $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() . '/'; - return new Href($addresses, false); + return new LocalHref($addresses); }); // For some reason somebody thought it was a good idea to add // another one of these properties. We're supporting it too. @@ -394,8 +397,8 @@ class Plugin extends DAV\ServerPlugin { } - $propFind->set($propRead, new Href($readList)); - $propFind->set($propWrite, new Href($writeList)); + $propFind->set($propRead, new LocalHref($readList)); + $propFind->set($propWrite, new LocalHref($writeList)); } @@ -821,11 +824,7 @@ class Plugin extends DAV\ServerPlugin { $data = stream_get_contents($data); } - $before = md5($data); - // Converting the data to unicode, if needed. - $data = DAV\StringUtil::ensureUTF8($data); - - if ($before !== md5($data)) $modified = true; + $before = $data; try { @@ -865,7 +864,7 @@ class Plugin extends DAV\ServerPlugin { } $foundType = null; - $foundUID = null; + foreach ($vobj->getComponents() as $component) { switch ($component->name) { case 'VTIMEZONE' : @@ -873,31 +872,59 @@ class Plugin extends DAV\ServerPlugin { case 'VEVENT' : case 'VTODO' : case 'VJOURNAL' : - if (is_null($foundType)) { - $foundType = $component->name; - if (!in_array($foundType, $supportedComponents)) { - throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType); - } - if (!isset($component->UID)) { - throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID'); - } - $foundUID = (string)$component->UID; - } else { - if ($foundType !== $component->name) { - throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType); - } - if ($foundUID !== (string)$component->UID) { - throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs'); - } - } + $foundType = $component->name; break; - default : - throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here'); + } + + } + + if (!$foundType || !in_array($foundType, $supportedComponents)) { + throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type ' . implode(', ', $supportedComponents)); + } + + $options = VObject\Node::PROFILE_CALDAV; + $prefer = $this->server->getHTTPPrefer(); + + if ($prefer['handling'] !== 'strict') { + $options |= VObject\Node::REPAIR; + } + + $messages = $vobj->validate($options); + $highestLevel = 0; + $warningMessage = null; + + // $messages contains a list of problems with the vcard, along with + // their severity. + foreach ($messages as $message) { + + if ($message['level'] > $highestLevel) { + // Recording the highest reported error level. + $highestLevel = $message['level']; + $warningMessage = $message['message']; } + switch ($message['level']) { + + case 1 : + // Level 1 means that there was a problem, but it was repaired. + $modified = true; + break; + case 2 : + // Level 2 means a warning, but not critical + break; + case 3 : + // Level 3 means a critical error + throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: ' . $message['message']); + + } + + } + if ($warningMessage) { + $response->setHeader( + 'X-Sabre-Ew-Gross', + 'iCalendar validation warning: ' . $warningMessage + ); } - if (!$foundType) - throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL'); // We use an extra variable to allow event handles to tell us wether // the object was modified or not. @@ -917,12 +944,12 @@ class Plugin extends DAV\ServerPlugin { ] ); - if ($subModified) { + if ($modified || $subModified) { // An event handler told us that it modified the object. $data = $vobj->serialize(); // Using md5 to figure out if there was an *actual* change. - if (!$modified && $before !== md5($data)) { + if (!$modified && strcmp($data, $before) !== 0) { $modified = true; } @@ -933,6 +960,22 @@ class Plugin extends DAV\ServerPlugin { } + /** + * This method is triggered whenever a subsystem reqeuests the privileges + * that are supported on a particular node. + * + * @param INode $node + * @param array $supportedPrivilegeSet + */ + function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) { + + if ($node instanceof ICalendar) { + $supportedPrivilegeSet['{DAV:}read']['aggregates']['{' . self::NS_CALDAV . '}read-free-busy'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } /** * This method is used to generate HTML output for the diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php b/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php index 13212565e..6b374ea3f 100644 --- a/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/Inbox.php @@ -17,6 +17,8 @@ use Sabre\VObject; */ class Inbox extends DAV\Collection implements IInbox { + use DAVACL\ACLTrait; + /** * CalDAV backend * @@ -119,19 +121,6 @@ class Inbox extends DAV\Collection implements IInbox { } /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** * Returns a list of ACE's for this node. * * Each ACE has the following properties: @@ -167,58 +156,11 @@ class Inbox extends DAV\Collection implements IInbox { 'protected' => true, ], [ - 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver-invite', + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver', 'principal' => '{DAV:}authenticated', 'protected' => true, ], - [ - 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver-reply', - 'principal' => '{DAV:}authenticated', - 'protected' => true, - ], - ]; - - } - - /** - * Updates the ACL - * - * This method will receive a list of new ACE's. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - $ns = '{' . CalDAV\Plugin::NS_CALDAV . '}'; - - $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet(); - $default['aggregates'][] = [ - 'privilege' => $ns . 'schedule-deliver', - 'aggregates' => [ - ['privilege' => $ns . 'schedule-deliver-invite'], - ['privilege' => $ns . 'schedule-deliver-reply'], - ], ]; - return $default; } diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php b/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php index dabaee2ca..29eefa744 100644 --- a/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/Outbox.php @@ -19,6 +19,8 @@ use Sabre\DAVACL; */ class Outbox extends DAV\Collection implements IOutbox { + use DAVACL\ACLTrait; + /** * The principal Uri * @@ -75,19 +77,6 @@ class Outbox extends DAV\Collection implements IOutbox { } /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** * Returns a list of ACE's for this node. * * Each ACE has the following properties: @@ -103,12 +92,7 @@ class Outbox extends DAV\Collection implements IOutbox { return [ [ - 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy', - 'principal' => $this->getOwner(), - 'protected' => true, - ], - [ - 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent', + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-send', 'principal' => $this->getOwner(), 'protected' => true, ], @@ -118,12 +102,7 @@ class Outbox extends DAV\Collection implements IOutbox { 'protected' => true, ], [ - 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy', - 'principal' => $this->getOwner() . '/calendar-proxy-write', - 'protected' => true, - ], - [ - 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent', + 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-send', 'principal' => $this->getOwner() . '/calendar-proxy-write', 'protected' => true, ], @@ -141,44 +120,4 @@ class Outbox extends DAV\Collection implements IOutbox { } - /** - * Updates the ACL - * - * This method will receive a list of new ACE's. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet(); - $default['aggregates'][] = [ - 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy', - ]; - $default['aggregates'][] = [ - 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent', - ]; - - return $default; - - } - } diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php b/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php index 827d6209b..47511140f 100644 --- a/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/Plugin.php @@ -5,10 +5,12 @@ namespace Sabre\CalDAV\Schedule; use DateTimeZone; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Sharing; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; use Sabre\DAV\INode; use Sabre\DAV\Xml\Property\Href; +use Sabre\DAV\Xml\Property\LocalHref; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; use Sabre\VObject; @@ -100,12 +102,13 @@ class Plugin extends ServerPlugin { function initialize(Server $server) { $this->server = $server; - $server->on('method:POST', [$this, 'httpPost']); - $server->on('propFind', [$this, 'propFind']); - $server->on('propPatch', [$this, 'propPatch']); - $server->on('calendarObjectChange', [$this, 'calendarObjectChange']); - $server->on('beforeUnbind', [$this, 'beforeUnbind']); - $server->on('schedule', [$this, 'scheduleLocalDelivery']); + $server->on('method:POST', [$this, 'httpPost']); + $server->on('propFind', [$this, 'propFind']); + $server->on('propPatch', [$this, 'propPatch']); + $server->on('calendarObjectChange', [$this, 'calendarObjectChange']); + $server->on('beforeUnbind', [$this, 'beforeUnbind']); + $server->on('schedule', [$this, 'scheduleLocalDelivery']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); $ns = '{' . self::NS_CALDAV . '}'; @@ -215,7 +218,7 @@ class Plugin extends ServerPlugin { } $outboxPath = $calendarHomePath . '/outbox/'; - return new Href($outboxPath); + return new LocalHref($outboxPath); }); // schedule-inbox-URL property @@ -227,7 +230,7 @@ class Plugin extends ServerPlugin { } $inboxPath = $calendarHomePath . '/inbox/'; - return new Href($inboxPath); + return new LocalHref($inboxPath); }); @@ -245,18 +248,28 @@ class Plugin extends ServerPlugin { $result = $this->server->getPropertiesForPath($calendarHomePath, [ '{DAV:}resourcetype', + '{DAV:}share-access', $sccs, ], 1); foreach ($result as $child) { - if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{' . self::NS_CALDAV . '}calendar') || $child[200]['{DAV:}resourcetype']->is('{http://calendarserver.org/ns/}shared')) { - // Node is either not a calendar or a shared instance. + if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{' . self::NS_CALDAV . '}calendar')) { + // Node is either not a calendar continue; } + if (isset($child[200]['{DAV:}share-access'])) { + $shareAccess = $child[200]['{DAV:}share-access']->getValue(); + if ($shareAccess !== Sharing\Plugin::ACCESS_NOTSHARED && $shareAccess !== Sharing\Plugin::ACCESS_SHAREDOWNER) { + // Node is a shared node, not owned by the relevant + // user. + continue; + } + + } if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) { // Either there is no supported-calendar-component-set // (which is fine) or we found one that supports VEVENT. - return new Href($child['href']); + return new LocalHref($child['href']); } } @@ -492,7 +505,7 @@ class Plugin extends ServerPlugin { } if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS . $privilege, DAVACL\Plugin::R_PARENT, false)) { - $iTipMessage->scheduleStatus = '3.8;organizer did not have the ' . $privilege . ' privilege on the attendees inbox'; + $iTipMessage->scheduleStatus = '3.8;insufficient privileges: ' . $privilege . ' is required on the recipient schedule inbox.'; return; } @@ -561,6 +574,65 @@ class Plugin extends ServerPlugin { } /** + * This method is triggered whenever a subsystem requests the privileges + * that are supported on a particular node. + * + * We need to add a number of privileges for scheduling purposes. + * + * @param INode $node + * @param array $supportedPrivilegeSet + */ + function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) { + + $ns = '{' . self::NS_CALDAV . '}'; + if ($node instanceof IOutbox) { + $supportedPrivilegeSet[$ns . 'schedule-send'] = [ + 'abstract' => false, + 'aggregates' => [ + $ns . 'schedule-send-invite' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns . 'schedule-send-reply' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns . 'schedule-send-freebusy' => [ + 'abstract' => false, + 'aggregates' => [], + ], + // Privilege from an earlier scheduling draft, but still + // used by some clients. + $ns . 'schedule-post-vevent' => [ + 'abstract' => false, + 'aggregates' => [], + ], + ] + ]; + } + if ($node instanceof IInbox) { + $supportedPrivilegeSet[$ns . 'schedule-deliver'] = [ + 'abstract' => false, + 'aggregates' => [ + $ns . 'schedule-deliver-invite' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns . 'schedule-deliver-reply' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns . 'schedule-query-freebusy' => [ + 'abstract' => false, + 'aggregates' => [], + ], + ] + ]; + } + + } + + /** * This method looks at an old iCalendar object, a new iCalendar object and * starts sending scheduling messages based on the changes. * @@ -647,7 +719,7 @@ class Plugin extends ServerPlugin { /** * This method handles POST requests to the schedule-outbox. * - * Currently, two types of requests are support: + * Currently, two types of requests are supported: * * FREEBUSY requests from RFC 6638 * * Simple iTIP messages from draft-desruisseaux-caldav-sched-04 * @@ -699,7 +771,7 @@ class Plugin extends ServerPlugin { if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') { - $acl && $acl->checkPrivileges($outboxPath, '{' . self::NS_CALDAV . '}schedule-query-freebusy'); + $acl && $acl->checkPrivileges($outboxPath, '{' . self::NS_CALDAV . '}schedule-send-freebusy'); $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response); // Destroy circular references so PHP can GC the object. @@ -727,7 +799,7 @@ class Plugin extends ServerPlugin { protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response) { $vFreeBusy = $vObject->VFREEBUSY; - $organizer = $vFreeBusy->organizer; + $organizer = $vFreeBusy->ORGANIZER; $organizer = (string)$organizer; @@ -863,6 +935,9 @@ class Plugin extends ServerPlugin { $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref(); $inboxUrl = $result[0][200][$caldavNS . 'schedule-inbox-URL']->getHref(); + // Do we have permission? + $aclPlugin->checkPrivileges($inboxUrl, $caldavNS . 'schedule-query-freebusy'); + // Grabbing the calendar list $objects = []; $calendarTimeZone = new DateTimeZone('UTC'); @@ -882,8 +957,6 @@ class Plugin extends ServerPlugin { continue; } - $aclPlugin->checkPrivileges($homeSet . $node->getName(), $caldavNS . 'read-free-busy'); - if (isset($props[$ctz])) { $vtimezoneObj = VObject\Reader::read($props[$ctz]); $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); diff --git a/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php b/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php index a36646e6c..6d9d3d5ec 100644 --- a/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php +++ b/vendor/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php @@ -134,22 +134,12 @@ class SchedulingObject extends \Sabre\CalDAV\CalendarObject implements IScheduli // The default ACL return [ [ - 'privilege' => '{DAV:}read', - 'principal' => $this->objectData['principaluri'], - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}write', - 'principal' => $this->objectData['principaluri'], - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}read', - 'principal' => $this->objectData['principaluri'] . '/calendar-proxy-write', + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', 'protected' => true, ], [ - 'privilege' => '{DAV:}write', + 'privilege' => '{DAV:}all', 'principal' => $this->objectData['principaluri'] . '/calendar-proxy-write', 'protected' => true, ], diff --git a/vendor/sabre/dav/lib/CalDAV/ShareableCalendar.php b/vendor/sabre/dav/lib/CalDAV/ShareableCalendar.php deleted file mode 100644 index c11695d5f..000000000 --- a/vendor/sabre/dav/lib/CalDAV/ShareableCalendar.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php - -namespace Sabre\CalDAV; - -/** - * This object represents a CalDAV calendar that can be shared with other - * users. - * - * @copyright Copyright (C) fruux GmbH (https://fruux.com/) - * @author Evert Pot (http://evertpot.com/) - * @license http://sabre.io/license/ Modified BSD License - */ -class ShareableCalendar extends Calendar implements IShareableCalendar { - - /** - * Updates the list of shares. - * - * The first array is a list of people that are to be added to the - * calendar. - * - * Every element in the add array has the following properties: - * * href - A url. Usually a mailto: address - * * commonName - Usually a first and last name, or false - * * summary - A description of the share, can also be false - * * readOnly - A boolean value - * - * Every element in the remove array is just the address string. - * - * @param array $add - * @param array $remove - * @return void - */ - function updateShares(array $add, array $remove) { - - $this->caldavBackend->updateShares($this->calendarInfo['id'], $add, $remove); - - } - - /** - * Returns the list of people whom this calendar is shared with. - * - * Every element in this array should have the following properties: - * * href - Often a mailto: address - * * commonName - Optional, for example a first + last name - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. - * * readOnly - boolean - * * summary - Optional, a description for the share - * - * @return array - */ - function getShares() { - - return $this->caldavBackend->getShares($this->calendarInfo['id']); - - } - - /** - * Marks this calendar as published. - * - * Publishing a calendar should automatically create a read-only, public, - * subscribable calendar. - * - * @param bool $value - * @return void - */ - function setPublishStatus($value) { - - $this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value); - - } - -} diff --git a/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php b/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php index 7973eff9c..7a77616e3 100644 --- a/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php +++ b/vendor/sabre/dav/lib/CalDAV/SharedCalendar.php @@ -2,6 +2,8 @@ namespace Sabre\CalDAV; +use Sabre\DAV\Sharing\Plugin as SPlugin; + /** * This object represents a CalDAV calendar that is shared by a different user. * @@ -12,50 +14,84 @@ namespace Sabre\CalDAV; class SharedCalendar extends Calendar implements ISharedCalendar { /** - * Constructor + * Returns the 'access level' for the instance of this shared resource. + * + * The value should be one of the Sabre\DAV\Sharing\Plugin::ACCESS_ + * constants. * - * @param Backend\BackendInterface $caldavBackend - * @param array $calendarInfo + * @return int */ - function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) { - - $required = [ - '{http://calendarserver.org/ns/}shared-url', - '{http://sabredav.org/ns}owner-principal', - '{http://sabredav.org/ns}read-only', - ]; - foreach ($required as $r) { - if (!isset($calendarInfo[$r])) { - throw new \InvalidArgumentException('The ' . $r . ' property must be specified for SharedCalendar(s)'); - } - } + function getShareAccess() { - parent::__construct($caldavBackend, $calendarInfo); + return isset($this->calendarInfo['share-access']) ? $this->calendarInfo['share-access'] : SPlugin::ACCESS_NOTSHARED; } /** - * This method should return the url of the owners' copy of the shared - * calendar. + * This function must return a URI that uniquely identifies the shared + * resource. This URI should be identical across instances, and is + * also used in several other XML bodies to connect invites to + * resources. + * + * This may simply be a relative reference to the original shared instance, + * but it could also be a urn. As long as it's a valid URI and unique. * * @return string */ - function getSharedUrl() { + function getShareResourceUri() { + + return $this->calendarInfo['share-resource-uri']; + + } + + /** + * Updates the list of sharees. + * + * Every item must be a Sharee object. + * + * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees + * @return void + */ + function updateInvites(array $sharees) { - return $this->calendarInfo['{http://calendarserver.org/ns/}shared-url']; + $this->caldavBackend->updateInvites($this->calendarInfo['id'], $sharees); } /** - * Returns the owner principal + * Returns the list of people whom this resource is shared with. + * + * Every item in the returned array must be a Sharee object with + * at least the following properties set: + * + * * $href + * * $shareAccess + * * $inviteStatus * - * This must be a url to a principal, or null if there's no owner + * and optionally: * - * @return string|null + * * $properties + * + * @return \Sabre\DAV\Xml\Element\Sharee[] */ - function getOwner() { + function getInvites() { - return $this->calendarInfo['{http://sabredav.org/ns}owner-principal']; + return $this->caldavBackend->getInvites($this->calendarInfo['id']); + + } + + /** + * Marks this calendar as published. + * + * Publishing a calendar should automatically create a read-only, public, + * subscribable calendar. + * + * @param bool $value + * @return void + */ + function setPublishStatus($value) { + + $this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value); } @@ -73,32 +109,72 @@ class SharedCalendar extends Calendar implements ISharedCalendar { */ function getACL() { - // The top-level ACL only contains access information for the true - // owner of the calendar, so we need to add the information for the - // sharee. - $acl = parent::getACL(); - $acl[] = [ - 'privilege' => '{DAV:}read', - 'principal' => $this->calendarInfo['principaluri'], - 'protected' => true, - ]; - if ($this->calendarInfo['{http://sabredav.org/ns}read-only']) { - $acl[] = [ - 'privilege' => '{DAV:}write-properties', - 'principal' => $this->calendarInfo['principaluri'], - 'protected' => true, - ]; - } else { - $acl[] = [ - 'privilege' => '{DAV:}write', - 'principal' => $this->calendarInfo['principaluri'], - 'protected' => true, - ]; + $acl = []; + + switch ($this->getShareAccess()) { + case SPlugin::ACCESS_NOTSHARED : + case SPlugin::ACCESS_SHAREDOWNER : + $acl[] = [ + 'privilege' => '{DAV:}share', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}share', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', + 'protected' => true, + ]; + // No break intentional! + case SPlugin::ACCESS_READWRITE : + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', + 'protected' => true, + ]; + // No break intentional! + case SPlugin::ACCESS_READ : + $acl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-read', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ]; + break; } return $acl; } + /** * This method returns the ACL's for calendar objects in this calendar. * The result of this method automatically gets passed to the @@ -108,40 +184,45 @@ class SharedCalendar extends Calendar implements ISharedCalendar { */ function getChildACL() { - $acl = parent::getChildACL(); - $acl[] = [ - 'privilege' => '{DAV:}read', - 'principal' => $this->calendarInfo['principaluri'], - 'protected' => true, - ]; - - if (!$this->calendarInfo['{http://sabredav.org/ns}read-only']) { - $acl[] = [ - 'privilege' => '{DAV:}write', - 'principal' => $this->calendarInfo['principaluri'], - 'protected' => true, - ]; + $acl = []; + + switch ($this->getShareAccess()) { + case SPlugin::ACCESS_NOTSHARED : + // No break intentional + case SPlugin::ACCESS_SHAREDOWNER : + // No break intentional + case SPlugin::ACCESS_READWRITE: + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', + 'protected' => true, + ]; + // No break intentional + case SPlugin::ACCESS_READ: + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-read', + 'protected' => true, + ]; + break; } - return $acl; - - } - - /** - * Returns the list of people whom this calendar is shared with. - * - * Every element in this array should have the following properties: - * * href - Often a mailto: address - * * commonName - Optional, for example a first + last name - * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. - * * readOnly - boolean - * * summary - Optional, a description for the share - * - * @return array - */ - function getShares() { - - return $this->caldavBackend->getShares($this->calendarInfo['id']); + return $acl; } diff --git a/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php b/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php index 5154fb1de..6f7df02bc 100644 --- a/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php +++ b/vendor/sabre/dav/lib/CalDAV/SharingPlugin.php @@ -4,6 +4,7 @@ namespace Sabre\CalDAV; use Sabre\DAV; use Sabre\DAV\Xml\Property\Href; +use Sabre\DAV\Xml\Property\LocalHref; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; @@ -26,15 +27,6 @@ use Sabre\HTTP\ResponseInterface; class SharingPlugin extends DAV\ServerPlugin { /** - * These are the various status constants used by sharing-messages. - */ - const STATUS_ACCEPTED = 1; - const STATUS_DECLINED = 2; - const STATUS_DELETED = 3; - const STATUS_NORESPONSE = 4; - const STATUS_INVALID = 5; - - /** * Reference to SabreDAV server object. * * @var Sabre\DAV\Server @@ -83,7 +75,10 @@ class SharingPlugin extends DAV\ServerPlugin { function initialize(DAV\Server $server) { $this->server = $server; - $server->resourceTypeMapping['Sabre\\CalDAV\\ISharedCalendar'] = '{' . Plugin::NS_CALENDARSERVER . '}shared'; + + if (is_null($this->server->getPlugin('sharing'))) { + throw new \LogicException('The generic "sharing" plugin must be loaded before the caldav sharing plugin. Call $server->addPlugin(new \Sabre\DAV\Sharing\Plugin()); before this one.'); + } array_push( $this->server->protectedProperties, @@ -114,24 +109,8 @@ class SharingPlugin extends DAV\ServerPlugin { */ function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) { - if ($node instanceof IShareableCalendar) { - - $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) { - return new Xml\Property\Invite( - $node->getShares() - ); - }); - - } - if ($node instanceof ISharedCalendar) { - $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}shared-url', function() use ($node) { - return new Href( - $node->getSharedUrl() - ); - }); - $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) { // Fetching owner information @@ -158,7 +137,7 @@ class SharingPlugin extends DAV\ServerPlugin { } return new Xml\Property\Invite( - $node->getShares(), + $node->getInvites(), $ownerInfo ); @@ -179,10 +158,18 @@ class SharingPlugin extends DAV\ServerPlugin { */ function propFindLate(DAV\PropFind $propFind, DAV\INode $node) { - if ($node instanceof IShareableCalendar) { + if ($node instanceof ISharedCalendar) { + $shareAccess = $node->getShareAccess(); if ($rt = $propFind->get('{DAV:}resourcetype')) { - if (count($node->getShares()) > 0) { - $rt->add('{' . Plugin::NS_CALENDARSERVER . '}shared-owner'); + switch ($shareAccess) { + case \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER : + $rt->add('{' . Plugin::NS_CALENDARSERVER . '}shared-owner'); + break; + case \Sabre\DAV\Sharing\Plugin::ACCESS_READ : + case \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE : + $rt->add('{' . Plugin::NS_CALENDARSERVER . '}shared'); + break; + } } $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes', function() { @@ -211,21 +198,24 @@ class SharingPlugin extends DAV\ServerPlugin { function propPatch($path, DAV\PropPatch $propPatch) { $node = $this->server->tree->getNodeForPath($path); - if (!$node instanceof IShareableCalendar) + if (!$node instanceof ISharedCalendar) return; - $propPatch->handle('{DAV:}resourcetype', function($value) use ($node) { - if ($value->is('{' . Plugin::NS_CALENDARSERVER . '}shared-owner')) return false; - $shares = $node->getShares(); - $remove = []; - foreach ($shares as $share) { - $remove[] = $share['href']; - } - $node->updateShares([], $remove); + if ($node->getShareAccess() === \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER || $node->getShareAccess() === \Sabre\DAV\Sharing\Plugin::ACCESS_NOTSHARED) { - return true; + $propPatch->handle('{DAV:}resourcetype', function($value) use ($node) { + if ($value->is('{' . Plugin::NS_CALENDARSERVER . '}shared-owner')) return false; + $shares = $node->getInvites(); + foreach ($shares as $share) { + $share->access = DAV\Sharing\Plugin::ACCESS_NOACCESS; + } + $node->updateInvites($shares); - }); + return true; + + }); + + } } @@ -267,26 +257,12 @@ class SharingPlugin extends DAV\ServerPlugin { switch ($documentType) { - // Dealing with the 'share' document, which modified invitees on a - // calendar. + // Both the DAV:share-resource and CALENDARSERVER:share requests + // behave identically. case '{' . Plugin::NS_CALENDARSERVER . '}share' : - // We can only deal with IShareableCalendar objects - if (!$node instanceof IShareableCalendar) { - return; - } - - $this->server->transactionType = 'post-calendar-share'; - - // Getting ACL info - $acl = $this->server->getPlugin('acl'); - - // If there's no ACL support, we allow everything - if ($acl) { - $acl->checkPrivileges($path, '{DAV:}write'); - } - - $node->updateShares($message->set, $message->remove); + $sharingPlugin = $this->server->getPlugin('sharing'); + $sharingPlugin->shareResource($path, $message->sharees); $response->setStatus(200); // Adding this because sending a response body may cause issues, @@ -328,11 +304,11 @@ class SharingPlugin extends DAV\ServerPlugin { $response->setHeader('X-Sabre-Status', 'everything-went-well'); if ($url) { - $writer = $this->server->xml->getWriter($this->server->getBaseUri()); + $writer = $this->server->xml->getWriter(); $writer->openMemory(); $writer->startDocument(); $writer->startElement('{' . Plugin::NS_CALENDARSERVER . '}shared-as'); - $writer->write(new Href($url)); + $writer->write(new LocalHref($url)); $writer->endElement(); $response->setHeader('Content-Type', 'application/xml'); $response->setBody($writer->outputMemory()); @@ -345,7 +321,7 @@ class SharingPlugin extends DAV\ServerPlugin { case '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar' : // We can only deal with IShareableCalendar objects - if (!$node instanceof IShareableCalendar) { + if (!$node instanceof ISharedCalendar) { return; } $this->server->transactionType = 'post-publish-calendar'; @@ -355,7 +331,7 @@ class SharingPlugin extends DAV\ServerPlugin { // If there's no ACL support, we allow everything if ($acl) { - $acl->checkPrivileges($path, '{DAV:}write'); + $acl->checkPrivileges($path, '{DAV:}share'); } $node->setPublishStatus(true); @@ -373,7 +349,7 @@ class SharingPlugin extends DAV\ServerPlugin { case '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar' : // We can only deal with IShareableCalendar objects - if (!$node instanceof IShareableCalendar) { + if (!$node instanceof ISharedCalendar) { return; } $this->server->transactionType = 'post-unpublish-calendar'; @@ -383,7 +359,7 @@ class SharingPlugin extends DAV\ServerPlugin { // If there's no ACL support, we allow everything if ($acl) { - $acl->checkPrivileges($path, '{DAV:}write'); + $acl->checkPrivileges($path, '{DAV:}share'); } $node->setPublishStatus(false); diff --git a/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php b/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php index ee53da2c6..3bb3451f3 100644 --- a/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php +++ b/vendor/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php @@ -5,8 +5,8 @@ namespace Sabre\CalDAV\Subscriptions; use Sabre\DAV\Collection; use Sabre\DAV\Xml\Property\Href; use Sabre\DAV\PropPatch; -use Sabre\DAV\Exception\MethodNotAllowed; use Sabre\DAVACL\IACL; +use Sabre\DAVACL\ACLTrait; use Sabre\CalDAV\Backend\SubscriptionSupport; /** @@ -20,6 +20,8 @@ use Sabre\CalDAV\Backend\SubscriptionSupport; */ class Subscription extends Collection implements ISubscription, IACL { + use ACLTrait; + /** * caldavBackend * @@ -144,7 +146,7 @@ class Subscription extends Collection implements ISubscription, IACL { * The Server class will filter out the extra. * * @param array $properties - * @return void + * @return array */ function getProperties($properties) { @@ -154,7 +156,7 @@ class Subscription extends Collection implements ISubscription, IACL { switch ($prop) { case '{http://calendarserver.org/ns/}source' : - $r[$prop] = new Href($this->subscriptionInfo['source'], false); + $r[$prop] = new Href($this->subscriptionInfo['source']); break; default : if (array_key_exists($prop, $this->subscriptionInfo)) { @@ -183,19 +185,6 @@ class Subscription extends Collection implements ISubscription, IACL { } /** - * Returns a group principal. - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** * Returns a list of ACE's for this node. * * Each ACE has the following properties: @@ -211,22 +200,12 @@ class Subscription extends Collection implements ISubscription, IACL { return [ [ - 'privilege' => '{DAV:}read', - 'principal' => $this->getOwner(), - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}write', + 'privilege' => '{DAV:}all', 'principal' => $this->getOwner(), 'protected' => true, ], [ - 'privilege' => '{DAV:}read', - 'principal' => $this->getOwner() . '/calendar-proxy-write', - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}write', + 'privilege' => '{DAV:}all', 'principal' => $this->getOwner() . '/calendar-proxy-write', 'protected' => true, ], @@ -239,36 +218,4 @@ class Subscription extends Collection implements ISubscription, IACL { } - /** - * Updates the ACL. - * - * This method will receive a list of new ACE's. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new MethodNotAllowed('Changing ACL is not yet supported'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php index 7fb022e33..1ca64f3e8 100644 --- a/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php @@ -5,6 +5,7 @@ namespace Sabre\CalDAV\Xml\Notification; use Sabre\Xml\Writer; use Sabre\CalDAV\SharingPlugin as SharingPlugin; use Sabre\CalDAV; +use Sabre\DAV; /** * This class represents the cs:invite-notification notification element. @@ -210,16 +211,10 @@ class Invite implements NotificationInterface { switch ($this->type) { - case SharingPlugin::STATUS_ACCEPTED : + case DAV\Sharing\Plugin::INVITE_ACCEPTED : $writer->writeElement($cs . 'invite-accepted'); break; - case SharingPlugin::STATUS_DECLINED : - $writer->writeElement($cs . 'invite-declined'); - break; - case SharingPlugin::STATUS_DELETED : - $writer->writeElement($cs . 'invite-deleted'); - break; - case SharingPlugin::STATUS_NORESPONSE : + case DAV\Sharing\Plugin::INVITE_NORESPONSE : $writer->writeElement($cs . 'invite-noresponse'); break; diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php index 945323fed..51bfc178a 100644 --- a/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php @@ -5,6 +5,7 @@ namespace Sabre\CalDAV\Xml\Notification; use Sabre\Xml\Writer; use Sabre\CalDAV; use Sabre\CalDAV\SharingPlugin; +use Sabre\DAV; /** * This class represents the cs:invite-reply notification element. @@ -162,10 +163,10 @@ class InviteReply implements NotificationInterface { switch ($this->type) { - case SharingPlugin::STATUS_ACCEPTED : + case DAV\Sharing\Plugin::INVITE_ACCEPTED : $writer->writeElement($cs . 'invite-accepted'); break; - case SharingPlugin::STATUS_DECLINED : + case DAV\Sharing\Plugin::INVITE_DECLINED : $writer->writeElement($cs . 'invite-declined'); break; diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php b/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php index 3ee053214..40ff6b936 100644 --- a/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Property/Invite.php @@ -2,11 +2,10 @@ namespace Sabre\CalDAV\Xml\Property; -use Sabre\Xml\Element; -use Sabre\Xml\Reader; +use Sabre\Xml\XmlSerializable; use Sabre\Xml\Writer; use Sabre\CalDAV\Plugin; -use Sabre\CalDAV\SharingPlugin; +use Sabre\DAV; /** * Invite property @@ -20,53 +19,23 @@ use Sabre\CalDAV\SharingPlugin; * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License */ -class Invite implements Element { +class Invite implements XmlSerializable { /** * The list of users a calendar has been shared to. * - * @var array + * @var Sharee[] */ - protected $users; - - /** - * The organizer contains information about the person who shared the - * object. - * - * @var array - */ - protected $organizer; + protected $sharees; /** * Creates the property. * - * Users is an array. Each element of the array has the following - * properties: - * - * * href - Often a mailto: address - * * commonName - Optional, for example a first and lastname for a user. - * * status - One of the SharingPlugin::STATUS_* constants. - * * readOnly - true or false - * * summary - Optional, description of the share - * - * The organizer key is optional to specify. It's only useful when a - * 'sharee' requests the sharing information. - * - * The organizer may have the following properties: - * * href - Often a mailto: address. - * * commonName - Optional human-readable name. - * * firstName - Optional first name. - * * lastName - Optional last name. - * - * If you wonder why these two structures are so different, I guess a - * valid answer is that the current spec is still a draft. - * - * @param array $users + * @param Sharee[] $sharees */ - function __construct(array $users, array $organizer = null) { + function __construct(array $sharees) { - $this->users = $users; - $this->organizer = $organizer; + $this->sharees = $sharees; } @@ -77,7 +46,7 @@ class Invite implements Element { */ function getValue() { - return $this->users; + return $this->sharees; } @@ -104,149 +73,55 @@ class Invite implements Element { $cs = '{' . Plugin::NS_CALENDARSERVER . '}'; - if (!is_null($this->organizer)) { - - $writer->startElement($cs . 'organizer'); - $writer->writeElement('{DAV:}href', $this->organizer['href']); - - if (isset($this->organizer['commonName']) && $this->organizer['commonName']) { - $writer->writeElement($cs . 'common-name', $this->organizer['commonName']); - } - if (isset($this->organizer['firstName']) && $this->organizer['firstName']) { - $writer->writeElement($cs . 'first-name', $this->organizer['firstName']); - } - if (isset($this->organizer['lastName']) && $this->organizer['lastName']) { - $writer->writeElement($cs . 'last-name', $this->organizer['lastName']); - } - $writer->endElement(); // organizer + foreach ($this->sharees as $sharee) { - } - - foreach ($this->users as $user) { - - $writer->startElement($cs . 'user'); - $writer->writeElement('{DAV:}href', $user['href']); - if (isset($user['commonName']) && $user['commonName']) { - $writer->writeElement($cs . 'common-name', $user['commonName']); - } - switch ($user['status']) { - - case SharingPlugin::STATUS_ACCEPTED : - $writer->writeElement($cs . 'invite-accepted'); - break; - case SharingPlugin::STATUS_DECLINED : - $writer->writeElement($cs . 'invite-declined'); - break; - case SharingPlugin::STATUS_NORESPONSE : - $writer->writeElement($cs . 'invite-noresponse'); - break; - case SharingPlugin::STATUS_INVALID : - $writer->writeElement($cs . 'invite-invalid'); - break; - } - - $writer->startElement($cs . 'access'); - if ($user['readOnly']) { - $writer->writeElement($cs . 'read'); + if ($sharee->access === \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER) { + $writer->startElement($cs . 'organizer'); } else { - $writer->writeElement($cs . 'read-write'); - } - $writer->endElement(); // access - - if (isset($user['summary']) && $user['summary']) { - $writer->writeElement($cs . 'summary', $user['summary']); - } - - $writer->endElement(); //user - - } - - } - - /** - * The deserialize method is called during xml parsing. - * - * This method is called statictly, this is because in theory this method - * may be used as a type of constructor, or factory method. - * - * Often you want to return an instance of the current class, but you are - * free to return other data as well. - * - * You are responsible for advancing the reader to the next element. Not - * doing anything will result in a never-ending loop. - * - * If you just want to skip parsing for this element altogether, you can - * just call $reader->next(); - * - * $reader->parseInnerTree() will parse the entire sub-tree, and advance to - * the next element. - * - * @param Reader $reader - * @return mixed - */ - static function xmlDeserialize(Reader $reader) { - - $cs = '{' . Plugin::NS_CALENDARSERVER . '}'; - - $users = []; - - foreach ($reader->parseInnerTree() as $elem) { + $writer->startElement($cs . 'user'); - if ($elem['name'] !== $cs . 'user') - continue; - - $user = [ - 'href' => null, - 'commonName' => null, - 'readOnly' => null, - 'summary' => null, - 'status' => null, - ]; - - foreach ($elem['value'] as $userElem) { - - switch ($userElem['name']) { - case $cs . 'invite-accepted' : - $user['status'] = SharingPlugin::STATUS_ACCEPTED; - break; - case $cs . 'invite-declined' : - $user['status'] = SharingPlugin::STATUS_DECLINED; + switch ($sharee->inviteStatus) { + case DAV\Sharing\Plugin::INVITE_ACCEPTED : + $writer->writeElement($cs . 'invite-accepted'); break; - case $cs . 'invite-noresponse' : - $user['status'] = SharingPlugin::STATUS_NORESPONSE; + case DAV\Sharing\Plugin::INVITE_DECLINED : + $writer->writeElement($cs . 'invite-declined'); break; - case $cs . 'invite-invalid' : - $user['status'] = SharingPlugin::STATUS_INVALID; + case DAV\Sharing\Plugin::INVITE_NORESPONSE : + $writer->writeElement($cs . 'invite-noresponse'); break; - case '{DAV:}href' : - $user['href'] = $userElem['value']; + case DAV\Sharing\Plugin::INVITE_INVALID : + $writer->writeElement($cs . 'invite-invalid'); break; - case $cs . 'common-name' : - $user['commonName'] = $userElem['value']; - break; - case $cs . 'access' : - foreach ($userElem['value'] as $accessHref) { - if ($accessHref['name'] === $cs . 'read') { - $user['readOnly'] = true; - } - } + } + + $writer->startElement($cs . 'access'); + switch ($sharee->access) { + case DAV\Sharing\Plugin::ACCESS_READWRITE : + $writer->writeElement($cs . 'read-write'); break; - case $cs . 'summary' : - $user['summary'] = $userElem['value']; + case DAV\Sharing\Plugin::ACCESS_READ : + $writer->writeElement($cs . 'read'); break; } + $writer->endElement(); // access } - if (!$user['status']) { - throw new \InvalidArgumentException('Every user must have one of cs:invite-accepted, cs:invite-declined, cs:invite-noresponse or cs:invite-invalid'); - } - $users[] = $user; + $href = new \Sabre\DAV\Xml\Property\Href($sharee->href); + $href->xmlSerialize($writer); - } + if (isset($sharee->properties['{DAV:}displayname'])) { + $writer->writeElement($cs . 'common-name', $sharee->properties['{DAV:}displayname']); + } + if ($sharee->comment) { + $writer->writeElement($cs . 'summary', $sharee->comment); + } + $writer->endElement(); // organizer or user - return new self($users); + } } + } diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php b/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php index ec627156f..2ecf6c2bb 100644 --- a/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php @@ -2,12 +2,13 @@ namespace Sabre\CalDAV\Xml\Request; +use Sabre\CalDAV\Plugin; +use Sabre\CalDAV\SharingPlugin; +use Sabre\DAV; +use Sabre\DAV\Exception\BadRequest; use Sabre\Xml\Reader; use Sabre\Xml\XmlDeserializable; use Sabre\Xml\Element\KeyValue; -use Sabre\DAV\Exception\BadRequest; -use Sabre\CalDAV\Plugin; -use Sabre\CalDAV\SharingPlugin; /** * Invite-reply POST request parser @@ -121,10 +122,10 @@ class InviteReply implements XmlDeserializable { } break; case '{' . Plugin::NS_CALENDARSERVER . '}invite-accepted' : - $status = SharingPlugin::STATUS_ACCEPTED; + $status = DAV\Sharing\Plugin::INVITE_ACCEPTED; break; case '{' . Plugin::NS_CALENDARSERVER . '}invite-declined' : - $status = SharingPlugin::STATUS_DECLINED; + $status = DAV\Sharing\Plugin::INVITE_DECLINED; break; case '{' . Plugin::NS_CALENDARSERVER . '}in-reply-to' : $inReplyTo = $value; diff --git a/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php b/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php index dacc5dc94..b5d9a133c 100644 --- a/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php +++ b/vendor/sabre/dav/lib/CalDAV/Xml/Request/Share.php @@ -2,9 +2,10 @@ namespace Sabre\CalDAV\Xml\Request; +use Sabre\CalDAV\Plugin; +use Sabre\DAV\Xml\Element\Sharee; use Sabre\Xml\Reader; use Sabre\Xml\XmlDeserializable; -use Sabre\CalDAV\Plugin; /** * Share POST request parser @@ -20,37 +21,20 @@ use Sabre\CalDAV\Plugin; class Share implements XmlDeserializable { /** - * The list of new people added or updated. - * - * Every element has the following keys: - * 1. href - An email address - * 2. commonName - Some name - * 3. summary - An optional description of the share - * 4. readOnly - true or false - * - * @var array - */ - public $set = []; - - /** - * List of people removed from the share list. - * - * The list is a flat list of email addresses (including mailto:). + * The list of new people added or updated or removed from the share. * - * @var array + * @var Sharee[] */ - public $remove = []; + public $sharees = []; /** * Constructor * - * @param array $set - * @param array $remove + * @param Sharee[] $sharees */ - function __construct(array $set, array $remove) { + function __construct(array $sharees) { - $this->set = $set; - $this->remove = $remove; + $this->sharees = $sharees; } @@ -77,13 +61,12 @@ class Share implements XmlDeserializable { */ static function xmlDeserialize(Reader $reader) { - $elems = $reader->parseInnerTree([ + $elems = $reader->parseGetElements([ '{' . Plugin::NS_CALENDARSERVER . '}set' => 'Sabre\\Xml\\Element\\KeyValue', '{' . Plugin::NS_CALENDARSERVER . '}remove' => 'Sabre\\Xml\\Element\\KeyValue', ]); - $set = []; - $remove = []; + $sharees = []; foreach ($elems as $elem) { switch ($elem['name']) { @@ -94,22 +77,34 @@ class Share implements XmlDeserializable { $sumElem = '{' . Plugin::NS_CALENDARSERVER . '}summary'; $commonName = '{' . Plugin::NS_CALENDARSERVER . '}common-name'; - $set[] = [ + $properties = []; + if (isset($sharee[$commonName])) { + $properties['{DAV:}displayname'] = $sharee[$commonName]; + } + + $access = array_key_exists('{' . Plugin::NS_CALENDARSERVER . '}read-write', $sharee) + ? \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE + : \Sabre\DAV\Sharing\Plugin::ACCESS_READ; + + $sharees[] = new Sharee([ 'href' => $sharee['{DAV:}href'], - 'commonName' => isset($sharee[$commonName]) ? $sharee[$commonName] : null, - 'summary' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null, - 'readOnly' => !array_key_exists('{' . Plugin::NS_CALENDARSERVER . '}read-write', $sharee), - ]; + 'properties' => $properties, + 'access' => $access, + 'comment' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null + ]); break; case '{' . Plugin::NS_CALENDARSERVER . '}remove' : - $remove[] = $elem['value']['{DAV:}href']; + $sharees[] = new Sharee([ + 'href' => $elem['value']['{DAV:}href'], + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS + ]); break; } } - return new self($set, $remove); + return new self($sharees); } diff --git a/vendor/sabre/dav/lib/CardDAV/AddressBook.php b/vendor/sabre/dav/lib/CardDAV/AddressBook.php index 70bec8760..6dd098618 100644 --- a/vendor/sabre/dav/lib/CardDAV/AddressBook.php +++ b/vendor/sabre/dav/lib/CardDAV/AddressBook.php @@ -16,6 +16,8 @@ use Sabre\DAVACL; */ class AddressBook extends DAV\Collection implements IAddressBook, DAV\IProperties, DAVACL\IACL, DAV\Sync\ISyncCollection, DAV\IMultiGet { + use DAVACL\ACLTrait; + /** * This is an array with addressbook information * @@ -236,48 +238,6 @@ class AddressBook extends DAV\Collection implements IAddressBook, DAV\IPropertie } - /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** - * Returns a list of ACE's for this node. - * - * Each ACE has the following properties: - * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are - * currently the only supported privileges - * * 'principal', a url to the principal who owns the node - * * 'protected' (optional), indicating that this ACE is not allowed to - * be updated. - * - * @return array - */ - function getACL() { - - return [ - [ - 'privilege' => '{DAV:}read', - 'principal' => $this->getOwner(), - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}write', - 'principal' => $this->getOwner(), - 'protected' => true, - ], - - ]; - - } /** * This method returns the ACL's for card nodes in this address book. @@ -290,12 +250,7 @@ class AddressBook extends DAV\Collection implements IAddressBook, DAV\IPropertie return [ [ - 'privilege' => '{DAV:}read', - 'principal' => $this->getOwner(), - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}write', + 'privilege' => '{DAV:}all', 'principal' => $this->getOwner(), 'protected' => true, ], @@ -303,37 +258,6 @@ class AddressBook extends DAV\Collection implements IAddressBook, DAV\IPropertie } - /** - * Updates the ACL - * - * This method will receive a list of new ACE's. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } /** * This method returns the current sync-token for this collection. diff --git a/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php b/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php index ebc251832..888a44a40 100644 --- a/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php +++ b/vendor/sabre/dav/lib/CardDAV/AddressBookHome.php @@ -18,6 +18,8 @@ use Sabre\Uri; */ class AddressBookHome extends DAV\Collection implements DAV\IExtendedCollection, DAVACL\IACL { + use DAVACL\ACLTrait; + /** * Principal uri * @@ -186,78 +188,4 @@ class AddressBookHome extends DAV\Collection implements DAV\IExtendedCollection, } - /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** - * Returns a list of ACE's for this node. - * - * Each ACE has the following properties: - * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are - * currently the only supported privileges - * * 'principal', a url to the principal who owns the node - * * 'protected' (optional), indicating that this ACE is not allowed to - * be updated. - * - * @return array - */ - function getACL() { - - return [ - [ - 'privilege' => '{DAV:}read', - 'principal' => $this->principalUri, - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}write', - 'principal' => $this->principalUri, - 'protected' => true, - ], - ]; - - } - - /** - * Updates the ACL - * - * This method will receive a list of new ACE's. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php b/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php index b9691b906..54e42b899 100644 --- a/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php +++ b/vendor/sabre/dav/lib/CardDAV/Backend/BackendInterface.php @@ -55,12 +55,15 @@ interface BackendInterface { function updateAddressBook($addressBookId, \Sabre\DAV\PropPatch $propPatch); /** - * Creates a new address book + * Creates a new address book. + * + * This method should return the id of the new address book. The id can be + * in any format, including ints, strings, arrays or objects. * * @param string $principalUri * @param string $url Just the 'basename' of the url. * @param array $properties - * @return void + * @return mixed */ function createAddressBook($principalUri, $url, array $properties); diff --git a/vendor/sabre/dav/lib/CardDAV/Backend/PDO.php b/vendor/sabre/dav/lib/CardDAV/Backend/PDO.php index 5509ddc02..7c3feff93 100644 --- a/vendor/sabre/dav/lib/CardDAV/Backend/PDO.php +++ b/vendor/sabre/dav/lib/CardDAV/Backend/PDO.php @@ -128,7 +128,7 @@ class PDO extends AbstractBackend implements SyncSupport { } else { $query .= ', '; } - $query .= ' `' . $key . '` = :' . $key . ' '; + $query .= ' ' . $key . ' = :' . $key . ' '; } $query .= ' WHERE id = :addressbookid'; @@ -180,7 +180,9 @@ class PDO extends AbstractBackend implements SyncSupport { $query = 'INSERT INTO ' . $this->addressBooksTableName . ' (uri, displayname, description, principaluri, synctoken) VALUES (:uri, :displayname, :description, :principaluri, 1)'; $stmt = $this->pdo->prepare($query); $stmt->execute($values); - return $this->pdo->lastInsertId(); + return $this->pdo->lastInsertId( + $this->addressBooksTableName . '_id_seq' + ); } @@ -230,6 +232,7 @@ class PDO extends AbstractBackend implements SyncSupport { $result = []; while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { $row['etag'] = '"' . $row['etag'] . '"'; + $row['lastmodified'] = (int)$row['lastmodified']; $result[] = $row; } return $result; @@ -258,6 +261,7 @@ class PDO extends AbstractBackend implements SyncSupport { if (!$result) return false; $result['etag'] = '"' . $result['etag'] . '"'; + $result['lastmodified'] = (int)$result['lastmodified']; return $result; } @@ -286,6 +290,7 @@ class PDO extends AbstractBackend implements SyncSupport { $result = []; while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { $row['etag'] = '"' . $row['etag'] . '"'; + $row['lastmodified'] = (int)$row['lastmodified']; $result[] = $row; } return $result; diff --git a/vendor/sabre/dav/lib/CardDAV/Card.php b/vendor/sabre/dav/lib/CardDAV/Card.php index 8da672502..0a040be6b 100644 --- a/vendor/sabre/dav/lib/CardDAV/Card.php +++ b/vendor/sabre/dav/lib/CardDAV/Card.php @@ -14,6 +14,8 @@ use Sabre\DAV; */ class Card extends DAV\File implements ICard, DAVACL\IACL { + use DAVACL\ACLTrait; + /** * CardDAV backend * @@ -181,18 +183,6 @@ class Card extends DAV\File implements ICard, DAVACL\IACL { } - /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } /** * Returns a list of ACE's for this node. @@ -215,12 +205,7 @@ class Card extends DAV\File implements ICard, DAVACL\IACL { return [ [ - 'privilege' => '{DAV:}read', - 'principal' => $this->addressBookInfo['principaluri'], - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}write', + 'privilege' => '{DAV:}all', 'principal' => $this->addressBookInfo['principaluri'], 'protected' => true, ], @@ -228,36 +213,4 @@ class Card extends DAV\File implements ICard, DAVACL\IACL { } - /** - * Updates the ACL - * - * This method will receive a list of new ACE's. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/CardDAV/Plugin.php b/vendor/sabre/dav/lib/CardDAV/Plugin.php index b8bded098..0507df100 100644 --- a/vendor/sabre/dav/lib/CardDAV/Plugin.php +++ b/vendor/sabre/dav/lib/CardDAV/Plugin.php @@ -4,7 +4,7 @@ namespace Sabre\CardDAV; use Sabre\DAV; use Sabre\DAV\Exception\ReportNotSupported; -use Sabre\DAV\Xml\Property\Href; +use Sabre\DAV\Xml\Property\LocalHref; use Sabre\DAVACL; use Sabre\HTTP; use Sabre\HTTP\RequestInterface; @@ -156,11 +156,11 @@ class Plugin extends DAV\ServerPlugin { $path = $propFind->getPath(); $propFind->handle('{' . self::NS_CARDDAV . '}addressbook-home-set', function() use ($path) { - return new Href($this->getAddressBookHomeForPrincipal($path) . '/'); + return new LocalHref($this->getAddressBookHomeForPrincipal($path) . '/'); }); if ($this->directories) $propFind->handle('{' . self::NS_CARDDAV . '}directory-gateway', function() { - return new Href($this->directories); + return new LocalHref($this->directories); }); } @@ -334,12 +334,7 @@ class Plugin extends DAV\ServerPlugin { $data = stream_get_contents($data); } - $before = md5($data); - - // Converting the data to unicode, if needed. - $data = DAV\StringUtil::ensureUTF8($data); - - if (md5($data) !== $before) $modified = true; + $before = $data; try { @@ -366,11 +361,56 @@ class Plugin extends DAV\ServerPlugin { throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.'); } - if (!isset($vobj->UID)) { - // No UID in vcards is invalid, but we'll just add it in anyway. - $vobj->add('UID', DAV\UUIDUtil::getUUID()); + $options = VObject\Node::PROFILE_CARDDAV; + $prefer = $this->server->getHTTPPrefer(); + + if ($prefer['handling'] !== 'strict') { + $options |= VObject\Node::REPAIR; + } + + $messages = $vobj->validate($options); + + $highestLevel = 0; + $warningMessage = null; + + // $messages contains a list of problems with the vcard, along with + // their severity. + foreach ($messages as $message) { + + if ($message['level'] > $highestLevel) { + // Recording the highest reported error level. + $highestLevel = $message['level']; + $warningMessage = $message['message']; + } + + switch ($message['level']) { + + case 1 : + // Level 1 means that there was a problem, but it was repaired. + $modified = true; + break; + case 2 : + // Level 2 means a warning, but not critical + break; + case 3 : + // Level 3 means a critical error + throw new DAV\Exception\UnsupportedMediaType('Validation error in vCard: ' . $message['message']); + + } + + } + if ($warningMessage) { + $this->server->httpResponse->setHeader( + 'X-Sabre-Ew-Gross', + 'vCard validation warning: ' . $warningMessage + ); + + // Re-serializing object. $data = $vobj->serialize(); - $modified = true; + if (!$modified && strcmp($data, $before) !== 0) { + // This ensures that the system does not send an ETag back. + $modified = true; + } } // Destroy circular references to PHP will GC the object. @@ -803,33 +843,49 @@ class Plugin extends DAV\ServerPlugin { /** * Converts a vcard blob to a different version, or jcard. * - * @param string $data + * @param string|resource $data * @param string $target * @return string */ protected function convertVCard($data, $target) { - $data = VObject\Reader::read($data); - switch ($target) { - default : - case 'vcard3' : - $data = $data->convert(VObject\Document::VCARD30); - $newResult = $data->serialize(); - break; - case 'vcard4' : - $data = $data->convert(VObject\Document::VCARD40); - $newResult = $data->serialize(); - break; - case 'jcard' : - $data = $data->convert(VObject\Document::VCARD40); - $newResult = json_encode($data->jsonSerialize()); - break; - + if (is_resource($data)) { + $data = stream_get_contents($data); } - // Destroy circular references to PHP will GC the object. - $data->destroy(); + $input = VObject\Reader::read($data); + $output = null; + try { - return $newResult; + switch ($target) { + default : + case 'vcard3' : + if ($input->getDocumentType() === VObject\Document::VCARD30) { + // Do nothing + return $data; + } + $output = $input->convert(VObject\Document::VCARD30); + return $output->serialize(); + case 'vcard4' : + if ($input->getDocumentType() === VObject\Document::VCARD40) { + // Do nothing + return $data; + } + $output = $input->convert(VObject\Document::VCARD40); + return $output->serialize(); + case 'jcard' : + $output = $input->convert(VObject\Document::VCARD40); + return json_encode($output); + + } + + } finally { + + // Destroy circular references to PHP will GC the object. + $input->destroy(); + if (!is_null($output)) { + $output->destroy(); + } + } } diff --git a/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php b/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php index de8b3bb84..d015589ad 100644 --- a/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php +++ b/vendor/sabre/dav/lib/CardDAV/VCFExportPlugin.php @@ -70,14 +70,34 @@ class VCFExportPlugin extends DAV\ServerPlugin { $aclPlugin->checkPrivileges($path, '{DAV:}read'); } - $response->setHeader('Content-Type', 'text/directory'); - $response->setStatus(200); - $nodes = $this->server->getPropertiesForPath($path, [ '{' . Plugin::NS_CARDDAV . '}address-data', ], 1); - $response->setBody($this->generateVCF($nodes)); + $format = 'text/directory'; + + $output = null; + $filenameExtension = null; + + switch ($format) { + case 'text/directory': + $output = $this->generateVCF($nodes); + $filenameExtension = '.vcf'; + break; + } + + $filename = preg_replace( + '/[^a-zA-Z0-9-_ ]/um', + '', + $node->getName() + ); + $filename .= '-' . date('Y-m-d') . $filenameExtension; + + $response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"'); + $response->setHeader('Content-Type', $format); + + $response->setStatus(200); + $response->setBody($output); // Returning false to break the event chain return false; diff --git a/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php b/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php index 0251decc1..85c5f30d5 100644 --- a/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php +++ b/vendor/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php @@ -155,8 +155,14 @@ abstract class AbstractDigest implements BackendInterface { $response ); $auth->init(); + + $oldStatus = $response->getStatus() ?: 200; $auth->requireLogin(); + // Preventing the digest utility from modifying the http status code, + // this should be handled by the main plugin. + $response->setStatus($oldStatus); + } } diff --git a/vendor/sabre/dav/lib/DAV/Auth/Plugin.php b/vendor/sabre/dav/lib/DAV/Auth/Plugin.php index 818d8a4ad..4b5f35ac3 100644 --- a/vendor/sabre/dav/lib/DAV/Auth/Plugin.php +++ b/vendor/sabre/dav/lib/DAV/Auth/Plugin.php @@ -4,7 +4,6 @@ namespace Sabre\DAV\Auth; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; -use Sabre\HTTP\URLUtil; use Sabre\DAV\Exception\NotAuthenticated; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -26,6 +25,20 @@ use Sabre\DAV\ServerPlugin; class Plugin extends ServerPlugin { /** + * By default this plugin will require that the user is authenticated, + * and refuse any access if the user is not authenticated. + * + * If this setting is set to false, we let the user through, whether they + * are authenticated or not. + * + * This is useful if you want to allow both authenticated and + * unauthenticated access to your server. + * + * @param bool + */ + public $autoRequireLogin = true; + + /** * authentication backends */ protected $backends; @@ -108,27 +121,6 @@ class Plugin extends ServerPlugin { } /** - * Returns the current username. - * - * This method is deprecated and is only kept for backwards compatibility - * purposes. Please switch to getCurrentPrincipal(). - * - * @deprecated Will be removed in a future version! - * @return string|null - */ - function getCurrentUser() { - - // We just do a 'basename' on the principal to give back a sane value - // here. - list(, $userName) = URLUtil::splitPath( - $this->getCurrentPrincipal() - ); - - return $userName; - - } - - /** * This method is called before any HTTP method and forces users to be authenticated * * @param RequestInterface $request @@ -154,6 +146,50 @@ class Plugin extends ServerPlugin { return; } + + $authResult = $this->check($request, $response); + + if ($authResult[0]) { + // Auth was successful + $this->currentPrincipal = $authResult[1]; + $this->loginFailedReasons = null; + return; + } + + + + // If we got here, it means that no authentication backend was + // successful in authenticating the user. + $this->currentPrincipal = null; + $this->loginFailedReasons = $authResult[1]; + + if ($this->autoRequireLogin) { + $this->challenge($request, $response); + throw new NotAuthenticated(implode(', ', $authResult[1])); + } + + } + + /** + * Checks authentication credentials, and logs the user in if possible. + * + * This method returns an array. The first item in the array is a boolean + * indicating if login was successful. + * + * If login was successful, the second item in the array will contain the + * current principal url/path of the logged in user. + * + * If login was not successful, the second item in the array will contain a + * an array with strings. The strings are a list of reasons why login was + * unsuccesful. For every auth backend there will be one reason, so usually + * there's just one. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + */ + function check(RequestInterface $request, ResponseInterface $response) { + if (!$this->backends) { throw new \Sabre\DAV\Exception('No authentication backends were configured on this server.'); } @@ -172,20 +208,56 @@ class Plugin extends ServerPlugin { if ($result[0]) { $this->currentPrincipal = $result[1]; // Exit early - return; + return [true, $result[1]]; } $reasons[] = $result[1]; } - // If we got here, it means that no authentication backend was - // successful in authenticating the user. - $this->currentPrincipal = null; + return [false, $reasons]; + + } + + /** + * This method sends authentication challenges to the user. + * + * This method will for example cause a HTTP Basic backend to set a + * WWW-Authorization header, indicating to the client that it should + * authenticate. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + */ + function challenge(RequestInterface $request, ResponseInterface $response) { foreach ($this->backends as $backend) { $backend->challenge($request, $response); } - throw new NotAuthenticated(implode(', ', $reasons)); + + } + + /** + * List of reasons why login failed for the last login operation. + * + * @var string[]|null + */ + protected $loginFailedReasons; + + /** + * Returns a list of reasons why login was unsuccessful. + * + * This method will return the login failed reasons for the last login + * operation. One for each auth backend. + * + * This method returns null if the last authentication attempt was + * successful, or if there was no authentication attempt yet. + * + * @return string[]|null + */ + function getLoginFailedReasons() { + + return $this->loginFailedReasons; } diff --git a/vendor/sabre/dav/lib/DAV/Browser/Plugin.php b/vendor/sabre/dav/lib/DAV/Browser/Plugin.php index 07ca6c3e5..49359a045 100644 --- a/vendor/sabre/dav/lib/DAV/Browser/Plugin.php +++ b/vendor/sabre/dav/lib/DAV/Browser/Plugin.php @@ -48,7 +48,7 @@ class Plugin extends DAV\ServerPlugin { public $uninterestingProperties = [ '{DAV:}supportedlock', '{DAV:}acl-restrictions', - '{DAV:}supported-privilege-set', +// '{DAV:}supported-privilege-set', '{DAV:}supported-method-set', ]; @@ -112,7 +112,7 @@ class Plugin extends DAV\ServerPlugin { $getVars = $request->getQueryParameters(); // CSP headers - $this->server->httpResponse->setHeader('Content-Security-Policy', "img-src 'self'; style-src 'self';"); + $this->server->httpResponse->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"); $sabreAction = isset($getVars['sabreAction']) ? $getVars['sabreAction'] : null; @@ -354,7 +354,7 @@ class Plugin extends DAV\ServerPlugin { $output = ''; if ($this->enablePost) { - $this->server->emit('onHTMLActionsPanel', [$node, &$output]); + $this->server->emit('onHTMLActionsPanel', [$node, &$output, $path]); } if ($output) { @@ -368,7 +368,7 @@ class Plugin extends DAV\ServerPlugin { $html .= $this->generateFooter(); - $this->server->httpResponse->setHeader('Content-Security-Policy', "img-src 'self'; style-src 'self';"); + $this->server->httpResponse->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"); return $html; @@ -477,7 +477,7 @@ HTML; $version = DAV\Version::VERSION; return <<<HTML -<footer>Generated by SabreDAV $version (c)2007-2015 <a href="http://sabre.io/">http://sabre.io/</a></footer> +<footer>Generated by SabreDAV $version (c)2007-2016 <a href="http://sabre.io/">http://sabre.io/</a></footer> </body> </html> HTML; @@ -493,9 +493,10 @@ HTML; * * @param DAV\INode $node * @param mixed $output + * @param string $path * @return void */ - function htmlActionsPanel(DAV\INode $node, &$output) { + function htmlActionsPanel(DAV\INode $node, &$output, $path) { if (!$node instanceof DAV\ICollection) return; diff --git a/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css b/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css index c9ab2c74f..8869597f0 100644 --- a/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css +++ b/vendor/sabre/dav/lib/DAV/Browser/assets/sabredav.css @@ -96,12 +96,12 @@ header a { vertical-align: middle; border: 0; } -input, button { +input, button, select { font: inherit; color: inherit; } -input[type=text] { +input[type=text], select { border: 1px solid #bbbbbb; line-height: 22px; padding: 5px 10px; @@ -200,7 +200,7 @@ section table { line-height: 40px; } -.actions input[type=text] { +.actions input[type=text], select { width: 450px; } diff --git a/vendor/sabre/dav/lib/DAV/Client.php b/vendor/sabre/dav/lib/DAV/Client.php index d46b397b6..08d5d4702 100644 --- a/vendor/sabre/dav/lib/DAV/Client.php +++ b/vendor/sabre/dav/lib/DAV/Client.php @@ -3,6 +3,7 @@ namespace Sabre\DAV; use Sabre\HTTP; +use Sabre\Uri; /** * SabreDAV DAV client @@ -387,20 +388,10 @@ class Client extends HTTP\Client { */ function getAbsoluteUrl($url) { - // If the url starts with http:// or https://, the url is already absolute. - if (preg_match('/^http(s?):\/\//', $url)) { - return $url; - } - - // If the url starts with a slash, we must calculate the url based off - // the root of the base url. - if (strpos($url, '/') === 0) { - $parts = parse_url($this->baseUri); - return $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port']) ? ':' . $parts['port'] : '') . $url; - } - - // Otherwise... - return $this->baseUri . $url; + return Uri\resolve( + $this->baseUri, + $url + ); } diff --git a/vendor/sabre/dav/lib/DAV/CorePlugin.php b/vendor/sabre/dav/lib/DAV/CorePlugin.php index 3a70b2a7e..a1b052915 100644 --- a/vendor/sabre/dav/lib/DAV/CorePlugin.php +++ b/vendor/sabre/dav/lib/DAV/CorePlugin.php @@ -50,6 +50,8 @@ class CorePlugin extends ServerPlugin { $server->on('propFind', [$this, 'propFindNode'], 120); $server->on('propFind', [$this, 'propFindLate'], 200); + $server->on('exception', [$this, 'exception']); + } /** @@ -844,10 +846,8 @@ class CorePlugin extends ServerPlugin { if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) { $nodeProperties = $node->getProperties($propertyNames); - foreach ($propertyNames as $propertyName) { - if (array_key_exists($propertyName, $nodeProperties)) { - $propFind->set($propertyName, $nodeProperties[$propertyName], 200); - } + foreach ($nodeProperties as $propertyName => $propertyValue) { + $propFind->set($propertyName, $propertyValue, 200); } } @@ -905,6 +905,38 @@ class CorePlugin extends ServerPlugin { } /** + * Listens for exception events, and automatically logs them. + * + * @param Exception $e + */ + function exception($e) { + + $logLevel = \Psr\Log\LogLevel::CRITICAL; + if ($e instanceof \Sabre\DAV\Exception) { + // If it's a standard sabre/dav exception, it means we have a http + // status code available. + $code = $e->getHTTPCode(); + + if ($code >= 400 && $code < 500) { + // user error + $logLevel = \Psr\Log\LogLevel::INFO; + } else { + // Server-side error. We mark it's as an error, but it's not + // critical. + $logLevel = \Psr\Log\LogLevel::ERROR; + } + } + + $this->server->getLogger()->log( + $logLevel, + 'Uncaught exception', + [ + 'exception' => $e, + ] + ); + } + + /** * Returns a bunch of meta-data about the plugin. * * Providing this information is optional, and is mainly displayed by the diff --git a/vendor/sabre/dav/lib/DAV/FS/Directory.php b/vendor/sabre/dav/lib/DAV/FS/Directory.php index 963e5554c..362f7a411 100644 --- a/vendor/sabre/dav/lib/DAV/FS/Directory.php +++ b/vendor/sabre/dav/lib/DAV/FS/Directory.php @@ -140,10 +140,10 @@ class Directory extends Node implements DAV\ICollection, DAV\IQuota { * @return array */ function getQuotaInfo() { - + $absolute = realpath($this->path); return [ - disk_total_space($this->path) - disk_free_space($this->path), - disk_free_space($this->path) + disk_total_space($absolute) - disk_free_space($absolute), + disk_free_space($absolute) ]; } diff --git a/vendor/sabre/dav/lib/DAV/File.php b/vendor/sabre/dav/lib/DAV/File.php index e0a0391db..675956b22 100644 --- a/vendor/sabre/dav/lib/DAV/File.php +++ b/vendor/sabre/dav/lib/DAV/File.php @@ -15,12 +15,24 @@ namespace Sabre\DAV; abstract class File extends Node implements IFile { /** - * Updates the data + * Replaces the contents of the file. * - * data is a readable stream resource. + * The data argument is a readable stream resource. * - * @param resource $data - * @return void + * After a succesful put operation, you may choose to return an ETag. The + * etag must always be surrounded by double-quotes. These quotes must + * appear in the actual string you're returning. + * + * Clients may use the ETag from a PUT request to later on make sure that + * when they update the file, the contents haven't changed in the mean + * time. + * + * If you don't plan to store the file byte-by-byte, and you return a + * different object on a subsequent GET you are strongly recommended to not + * return an ETag, and just return null. + * + * @param string|resource $data + * @return string|null */ function put($data) { diff --git a/vendor/sabre/dav/lib/DAV/ICollection.php b/vendor/sabre/dav/lib/DAV/ICollection.php index 390d9b741..7793070d3 100644 --- a/vendor/sabre/dav/lib/DAV/ICollection.php +++ b/vendor/sabre/dav/lib/DAV/ICollection.php @@ -54,14 +54,14 @@ interface ICollection extends INode { * exist. * * @param string $name - * @return DAV\INode + * @return INode */ function getChild($name); /** * Returns an array with all the child nodes * - * @return DAV\INode[] + * @return INode[] */ function getChildren(); diff --git a/vendor/sabre/dav/lib/DAV/IFile.php b/vendor/sabre/dav/lib/DAV/IFile.php index e16a3a58a..37e7cd33c 100644 --- a/vendor/sabre/dav/lib/DAV/IFile.php +++ b/vendor/sabre/dav/lib/DAV/IFile.php @@ -32,7 +32,7 @@ interface IFile extends INode { * different object on a subsequent GET you are strongly recommended to not * return an ETag, and just return null. * - * @param resource $data + * @param resource|data $data * @return string|null */ function put($data); diff --git a/vendor/sabre/dav/lib/DAV/INode.php b/vendor/sabre/dav/lib/DAV/INode.php index b5e6cb9ef..bb884934d 100644 --- a/vendor/sabre/dav/lib/DAV/INode.php +++ b/vendor/sabre/dav/lib/DAV/INode.php @@ -36,9 +36,10 @@ interface INode { function setName($name); /** - * Returns the last modification time, as a unix timestamp + * Returns the last modification time, as a unix timestamp. Return null + * if the information is not available. * - * @return int + * @return int|null */ function getLastModified(); diff --git a/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php b/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php index 910e4979d..2fe843884 100644 --- a/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php +++ b/vendor/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php @@ -88,6 +88,9 @@ class PDO implements BackendInterface { $stmt->execute([$path]); while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if (gettype($row['value']) === 'resource') { + $row['value'] = stream_get_contents($row['value']); + } switch ($row['valuetype']) { case null : case self::VT_STRING : @@ -121,7 +124,26 @@ class PDO implements BackendInterface { $propPatch->handleRemaining(function($properties) use ($path) { - $updateStmt = $this->pdo->prepare("REPLACE INTO " . $this->tableName . " (path, name, valuetype, value) VALUES (?, ?, ?, ?)"); + + if ($this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME) === 'pgsql') { + + $updateSql = <<<SQL +INSERT INTO {$this->tableName} (path, name, valuetype, value) +VALUES (:path, :name, :valuetype, :value) +ON CONFLICT (path, name) +DO UPDATE SET valuetype = :valuetype, value = :value +SQL; + + + } else { + $updateSql = <<<SQL +REPLACE INTO {$this->tableName} (path, name, valuetype, value) +VALUES (:path, :name, :valuetype, :value) +SQL; + + } + + $updateStmt = $this->pdo->prepare($updateSql); $deleteStmt = $this->pdo->prepare("DELETE FROM " . $this->tableName . " WHERE path = ? AND name = ?"); foreach ($properties as $name => $value) { @@ -136,7 +158,14 @@ class PDO implements BackendInterface { $valueType = self::VT_OBJECT; $value = serialize($value); } - $updateStmt->execute([$path, $name, $valueType, $value]); + + $updateStmt->bindParam('path', $path, \PDO::PARAM_STR); + $updateStmt->bindParam('name', $name, \PDO::PARAM_STR); + $updateStmt->bindParam('valuetype', $valueType, \PDO::PARAM_INT); + $updateStmt->bindParam('value', $value, \PDO::PARAM_LOB); + + $updateStmt->execute(); + } else { $deleteStmt->execute([$path, $name]); } diff --git a/vendor/sabre/dav/lib/DAV/Server.php b/vendor/sabre/dav/lib/DAV/Server.php index b37652812..024b7a557 100644 --- a/vendor/sabre/dav/lib/DAV/Server.php +++ b/vendor/sabre/dav/lib/DAV/Server.php @@ -8,6 +8,10 @@ use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; use Sabre\HTTP\URLUtil; use Sabre\Uri; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; /** * Main DAV server class @@ -16,7 +20,9 @@ use Sabre\Uri; * @author Evert Pot (http://evertpot.com/) * @license http://sabre.io/license/ Modified BSD License */ -class Server extends EventEmitter { +class Server extends EventEmitter implements LoggerAwareInterface { + + use LoggerAwareTrait; /** * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree @@ -431,6 +437,20 @@ class Server extends EventEmitter { } /** + * Returns the PSR-3 logger objcet. + * + * @return LoggerInterface + */ + function getLogger() { + + if (!$this->logger) { + $this->logger = new NullLogger(); + } + return $this->logger; + + } + + /** * Handles a http request, and execute a method based on its name * * @param RequestInterface $request @@ -1177,9 +1197,20 @@ class Server extends EventEmitter { if (!$success) { $result = $mkCol->getResult(); - // generateMkCol needs the href key to exist. - $result['href'] = $uri; - return $result; + + $formattedResult = [ + 'href' => $uri, + ]; + + foreach ($result as $propertyName => $status) { + + if (!isset($formattedResult[$status])) { + $formattedResult[$status] = []; + } + $formattedResult[$status][$propertyName] = null; + + } + return $formattedResult; } $this->tree->markDirty($parentUri); diff --git a/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php b/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php new file mode 100644 index 000000000..034aefbdc --- /dev/null +++ b/vendor/sabre/dav/lib/DAV/Sharing/ISharedNode.php @@ -0,0 +1,69 @@ +<?php + +namespace Sabre\DAV\Sharing; + +use Sabre\DAV\INode; + +/** + * This interface represents a resource that has sharing capabilities, either + * because it's possible for an owner to share the resource, or because this is + * an instance of a shared resource. + * + * @copyright Copyright (C) fruux GmbH. (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +interface ISharedNode extends INode { + + /** + * Returns the 'access level' for the instance of this shared resource. + * + * The value should be one of the Sabre\DAV\Sharing\Plugin::ACCESS_ + * constants. + * + * @return int + */ + function getShareAccess(); + + /** + * This function must return a URI that uniquely identifies the shared + * resource. This URI should be identical across instances, and is + * also used in several other XML bodies to connect invites to + * resources. + * + * This may simply be a relative reference to the original shared instance, + * but it could also be a urn. As long as it's a valid URI and unique. + * + * @return string + */ + function getShareResourceUri(); + + /** + * Updates the list of sharees. + * + * Every item must be a Sharee object. + * + * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees + * @return void + */ + function updateInvites(array $sharees); + + /** + * Returns the list of people whom this resource is shared with. + * + * Every item in the returned array must be a Sharee object with + * at least the following properties set: + * + * * $href + * * $shareAccess + * * $inviteStatus + * + * and optionally: + * + * * $properties + * + * @return \Sabre\DAV\Xml\Element\Sharee[] + */ + function getInvites(); + +} diff --git a/vendor/sabre/dav/lib/DAV/Sharing/Plugin.php b/vendor/sabre/dav/lib/DAV/Sharing/Plugin.php new file mode 100644 index 000000000..354d06a56 --- /dev/null +++ b/vendor/sabre/dav/lib/DAV/Sharing/Plugin.php @@ -0,0 +1,342 @@ +<?php + +namespace Sabre\DAV\Sharing; + +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Xml\Property; +use Sabre\DAV\Xml\Element\Sharee; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +/** + * This plugin implements HTTP requests and properties related to: + * + * draft-pot-webdav-resource-sharing + * + * This specification allows people to share webdav resources with others. + * + * @copyright Copyright (C) 2007-2015 fruux GmbH. (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends ServerPlugin { + + const ACCESS_NOTSHARED = 0; + const ACCESS_SHAREDOWNER = 1; + const ACCESS_READ = 2; + const ACCESS_READWRITE = 3; + const ACCESS_NOACCESS = 4; + + const INVITE_NORESPONSE = 1; + const INVITE_ACCEPTED = 2; + const INVITE_DECLINED = 3; + const INVITE_INVALID = 4; + + /** + * Reference to SabreDAV server object. + * + * @var Sabre\DAV\Server + */ + protected $server; + + /** + * This method should return a list of server-features. + * + * This is for example 'versioning' and is added to the DAV: header + * in an OPTIONS response. + * + * @return array + */ + function getFeatures() { + + return ['resource-sharing']; + + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'sharing'; + + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param Server $server + * @return void + */ + function initialize(Server $server) { + + $this->server = $server; + + $server->xml->elementMap['{DAV:}share-resource'] = 'Sabre\\DAV\\Xml\\Request\\ShareResource'; + + array_push( + $server->protectedProperties, + '{DAV:}share-mode' + ); + + $server->on('method:POST', [$this, 'httpPost']); + $server->on('propFind', [$this, 'propFind']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('onBrowserPostAction', [$this, 'browserPostAction']); + + } + + /** + * Updates the list of sharees on a shared resource. + * + * The sharees array is a list of people that are to be added modified + * or removed in the list of shares. + * + * @param string $path + * @param Sharee[] $sharees + * @return void + */ + function shareResource($path, array $sharees) { + + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof ISharedNode) { + + throw new Forbidden('Sharing is not allowed on this node'); + + } + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}share'); + } + + foreach ($sharees as $sharee) { + // We're going to attempt to get a local principal uri for a share + // href by emitting the getPrincipalByUri event. + $principal = null; + $this->server->emit('getPrincipalByUri', [$sharee->href, &$principal]); + $sharee->principal = $principal; + } + $node->updateInvites($sharees); + + } + + /** + * This event is triggered when properties are requested for nodes. + * + * This allows us to inject any sharings-specific properties. + * + * @param PropFind $propFind + * @param INode $node + * @return void + */ + function propFind(PropFind $propFind, INode $node) { + + if ($node instanceof ISharedNode) { + + $propFind->handle('{DAV:}share-access', function() use ($node) { + + return new Property\ShareAccess($node->getShareAccess()); + + }); + $propFind->handle('{DAV:}invite', function() use ($node) { + + return new Property\Invite($node->getInvites()); + + }); + $propFind->handle('{DAV:}share-resource-uri', function() use ($node) { + + return new Property\Href($node->getShareResourceUri()); + + }); + + } + + } + + /** + * We intercept this to handle POST requests on shared resources + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return null|bool + */ + function httpPost(RequestInterface $request, ResponseInterface $response) { + + $path = $request->getPath(); + $contentType = $request->getHeader('Content-Type'); + + // We're only interested in the davsharing content type. + if (strpos($contentType, 'application/davsharing+xml') === false) { + return; + } + + $message = $this->server->xml->parse( + $request->getBody(), + $request->getUrl(), + $documentType + ); + + switch ($documentType) { + + case '{DAV:}share-resource': + + $this->shareResource($path, $message->sharees); + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + default : + throw new BadRequest('Unexpected document type: ' . $documentType . ' for this Content-Type'); + + } + + } + + /** + * This method is triggered whenever a subsystem reqeuests the privileges + * hat are supported on a particular node. + * + * We need to add a number of privileges for scheduling purposes. + * + * @param INode $node + * @param array $supportedPrivilegeSet + */ + function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) { + + if ($node instanceof ISharedNode) { + $supportedPrivilegeSet['{DAV:}share'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin implements WebDAV resource sharing', + 'link' => 'https://github.com/evert/webdav-sharing' + ]; + + } + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. + * + * @param INode $node + * @param string $output + * @param string $path + * @return bool|null + */ + function htmlActionsPanel(INode $node, &$output, $path) { + + if (!$node instanceof ISharedNode) { + return; + } + + $aclPlugin = $this->server->getPlugin('acl'); + if ($aclPlugin) { + if (!$aclPlugin->checkPrivileges($path, '{DAV:}share', \Sabre\DAVACL\Plugin::R_PARENT, false)) { + // Sharing is not permitted, we will not draw this interface. + return; + } + } + + $output .= '<tr><td colspan="2"><form method="post" action=""> + <h3>Share this resource</h3> + <input type="hidden" name="sabreAction" value="share" /> + <label>Share with (uri):</label> <input type="text" name="href" placeholder="mailto:user@example.org"/><br /> + <label>Access</label> + <select name="access"> + <option value="readwrite">Read-write</option> + <option value="read">Read-only</option> + <option value="no-access">Revoke access</option> + </select><br /> + <input type="submit" value="share" /> + </form> + </td></tr>'; + + } + + /** + * This method is triggered for POST actions generated by the browser + * plugin. + * + * @param string $path + * @param string $action + * @param array $postVars + */ + function browserPostAction($path, $action, $postVars) { + + if ($action !== 'share') { + return; + } + + if (empty($postVars['href'])) { + throw new BadRequest('The "href" POST parameter is required'); + } + if (empty($postVars['access'])) { + throw new BadRequest('The "access" POST parameter is required'); + } + + $accessMap = [ + 'readwrite' => self::ACCESS_READWRITE, + 'read' => self::ACCESS_READ, + 'no-access' => self::ACCESS_NOACCESS, + ]; + + if (!isset($accessMap[$postVars['access']])) { + throw new BadRequest('The "access" POST must be readwrite, read or no-access'); + } + $sharee = new Sharee([ + 'href' => $postVars['href'], + 'access' => $accessMap[$postVars['access']], + ]); + + $this->shareResource( + $path, + [$sharee] + ); + return false; + + } + +} diff --git a/vendor/sabre/dav/lib/DAV/Tree.php b/vendor/sabre/dav/lib/DAV/Tree.php index 4563f7c72..5d2792503 100644 --- a/vendor/sabre/dav/lib/DAV/Tree.php +++ b/vendor/sabre/dav/lib/DAV/Tree.php @@ -229,7 +229,7 @@ class Tree { // flushing the entire cache $path = trim($path, '/'); foreach ($this->cache as $nodePath => $node) { - if ($nodePath == $path || strpos($nodePath, $path . '/') === 0) + if ($path === '' || $nodePath == $path || strpos($nodePath, $path . '/') === 0) unset($this->cache[$nodePath]); } diff --git a/vendor/sabre/dav/lib/DAV/Version.php b/vendor/sabre/dav/lib/DAV/Version.php index f9331943a..5430b967c 100644 --- a/vendor/sabre/dav/lib/DAV/Version.php +++ b/vendor/sabre/dav/lib/DAV/Version.php @@ -14,6 +14,6 @@ class Version { /** * Full version number */ - const VERSION = '3.1.3'; + const VERSION = '3.2.0-beta1'; } diff --git a/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php b/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php new file mode 100644 index 000000000..dcfd7bd2e --- /dev/null +++ b/vendor/sabre/dav/lib/DAV/Xml/Element/Sharee.php @@ -0,0 +1,199 @@ +<?php + +namespace Sabre\DAV\Xml\Element; + +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Sharing\Plugin; +use Sabre\DAV\Xml\Property\Href; +use Sabre\DAV\Xml\Property\ShareAccess; +use Sabre\Xml\Deserializer; +use Sabre\Xml\Element; +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; + +/** + * This class represents the {DAV:}sharee element. + * + * @copyright Copyright (C) fruux GmbH. (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Sharee implements Element { + + /** + * A URL. Usually a mailto: address, could also be a principal url. + * This uniquely identifies the sharee. + * + * @var string + */ + public $href; + + /** + * A local principal path. The server will do its best to locate the + * principal uri based on the given uri. If we could find a local matching + * principal uri, this property will contain the value. + * + * @var string|null + */ + public $principal; + + /** + * A list of WebDAV properties that describe the sharee. This might for + * example contain a {DAV:}displayname with the real name of the user. + * + * @var array + */ + public $properties = []; + + /** + * Share access level. One of the Sabre\DAV\Sharing\Plugin::ACCESS + * constants. + * + * Can be one of: + * + * ACCESS_READ + * ACCESS_READWRITE + * ACCESS_SHAREDOWNER + * ACCESS_NOACCESS + * + * depending on context. + * + * @var int + */ + public $access; + + /** + * When a sharee is originally invited to a share, the sharer may add + * a comment. This will be placed in this property. + * + * @var string + */ + public $comment; + + /** + * The status of the invite, should be one of the + * Sabre\DAV\Sharing\Plugin::INVITE constants. + * + * @var int + */ + public $inviteStatus; + + /** + * Creates the object + * + * $properties will be used to populate all internal properties. + * + * @param array $properties + */ + function __construct(array $properties = []) { + + foreach ($properties as $k => $v) { + + if (property_exists($this, $k)) { + $this->$k = $v; + } else { + throw new \InvalidArgumentException('Unknown property: ' . $k); + } + + } + + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + + $writer->write([ + new Href($this->href), + '{DAV:}prop' => $this->properties, + '{DAV:}share-access' => new ShareAccess($this->access), + ]); + switch ($this->inviteStatus) { + case Plugin::INVITE_NORESPONSE : + $writer->writeElement('{DAV:}invite-noresponse'); + break; + case Plugin::INVITE_ACCEPTED : + $writer->writeElement('{DAV:}invite-accepted'); + break; + case Plugin::INVITE_DECLINED : + $writer->writeElement('{DAV:}invite-declined'); + break; + case Plugin::INVITE_INVALID : + $writer->writeElement('{DAV:}invite-invalid'); + break; + } + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + // Temporarily override configuration + $reader->pushContext(); + $reader->elementMap['{DAV:}share-access'] = 'Sabre\DAV\Xml\Property\ShareAccess'; + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\keyValue'; + + $elems = Deserializer\keyValue($reader, 'DAV:'); + + // Restore previous configuration + $reader->popContext(); + + $sharee = new self(); + if (!isset($elems['href'])) { + throw new BadRequest('Every {DAV:}sharee must have a {DAV:}href child-element'); + } + $sharee->href = $elems['href']; + + if (isset($elems['prop'])) { + $sharee->properties = $elems['prop']; + } + if (isset($elems['comment'])) { + $sharee->comment = $elems['comment']; + } + if (!isset($elems['share-access'])) { + throw new BadRequest('Every {DAV:}sharee must have a {DAV:}share-access child element'); + } + $sharee->access = $elems['share-access']->getValue(); + return $sharee; + + } + +} diff --git a/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php b/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php index 538e98d0f..0027f72e1 100644 --- a/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php +++ b/vendor/sabre/dav/lib/DAV/Xml/Property/Href.php @@ -7,6 +7,7 @@ use Sabre\DAV\Browser\HtmlOutputHelper; use Sabre\Xml\Element; use Sabre\Xml\Reader; use Sabre\Xml\Writer; +use Sabre\Uri; /** * Href property @@ -32,13 +33,6 @@ class Href implements Element, HtmlOutput { protected $hrefs; /** - * Automatically prefix the url with the server base directory - * - * @var bool - */ - protected $autoPrefix = true; - - /** * Constructor * * You must either pass a string for a single href, or an array of hrefs. @@ -47,16 +41,13 @@ class Href implements Element, HtmlOutput { * and not relative to the servers base uri. * * @param string|string[] $href - * @param bool $autoPrefix */ - function __construct($hrefs, $autoPrefix = true) { + function __construct($hrefs) { if (is_string($hrefs)) { $hrefs = [$hrefs]; } $this->hrefs = $hrefs; - $this->autoPrefix = $autoPrefix; - } @@ -104,9 +95,7 @@ class Href implements Element, HtmlOutput { function xmlSerialize(Writer $writer) { foreach ($this->getHrefs() as $href) { - if ($this->autoPrefix) { - $href = $writer->contextUri . \Sabre\HTTP\encodePath($href); - } + $href = Uri\resolve($writer->contextUri, $href); $writer->writeElement('{DAV:}href', $href); } diff --git a/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php b/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php new file mode 100644 index 000000000..0616ff113 --- /dev/null +++ b/vendor/sabre/dav/lib/DAV/Xml/Property/Invite.php @@ -0,0 +1,70 @@ +<?php + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Sharing\Sharee; +use Sabre\Xml\XmlSerializable; +use Sabre\Xml\Writer; + +/** + * This class represents the {DAV:}invite property. + * + * This property is defined here: + * https://tools.ietf.org/html/draft-pot-webdav-resource-sharing-03#section-4.4.2 + * + * This property is used by clients to determine who currently has access to + * a shared resource, what their access level is and what their invite status + * is. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Invite implements XmlSerializable { + + /** + * A list of sharees + * + * @var Sharee[] + */ + public $sharees = []; + + /** + * Creates the property. + * + * @param Sharee[] $sharees + */ + function __construct(array $sharees) { + + $this->sharees = $sharees; + + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + foreach ($this->sharees as $sharee) { + $writer->writeElement('{DAV:}sharee', $sharee); + } + + } + +} diff --git a/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php b/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php new file mode 100644 index 000000000..76a27b95d --- /dev/null +++ b/vendor/sabre/dav/lib/DAV/Xml/Property/LocalHref.php @@ -0,0 +1,48 @@ +<?php + +namespace Sabre\DAV\Xml\Property; + +use Sabre\HTTP; + +/** + * LocalHref property + * + * Like the Href property, this element represents {DAV:}href. The difference + * is that this is used stricly for paths on the server. The LocalHref property + * will prepare the path so it's a valid URI. + * + * These two objects behave identically: + * new LocalHref($path) + * new Href(\Sabre\HTTP\encodePath($path)) + * + * LocalPath basically ensures that your spaces are %20, and everything that + * needs to be is uri encoded. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class LocalHref extends Href { + + /** + * Constructor + * + * You must either pass a string for a single href, or an array of hrefs. + * + * If auto-prefix is set to false, the hrefs will be treated as absolute + * and not relative to the servers base uri. + * + * @param string|string[] $href + */ + function __construct($hrefs) { + + parent::__construct(array_map( + function($href) { + return \Sabre\HTTP\encodePath($href); + }, + (array)$hrefs + )); + + } + +} diff --git a/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php b/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php new file mode 100644 index 000000000..f27af5415 --- /dev/null +++ b/vendor/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php @@ -0,0 +1,143 @@ +<?php + +namespace Sabre\DAV\Xml\Property; + +use Sabre\DAV\Sharing\Plugin as SharingPlugin; +use Sabre\DAV\Exception\BadRequest; +use Sabre\Xml\Element; +use Sabre\Xml\Reader; +use Sabre\Xml\Writer; + +/** + * This class represents the {DAV:}share-access property. + * + * This property is defined here: + * https://tools.ietf.org/html/draft-pot-webdav-resource-sharing-03#section-4.4.1 + * + * This property is used to indicate if a resource is a shared resource, and + * whether the instance of the shared resource is the original instance, or + * an instance belonging to a sharee. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ShareAccess implements Element { + + /** + * Either SHARED or SHAREDOWNER + * + * @var int + */ + protected $value; + + /** + * Creates the property. + * + * The constructor value must be one of the + * \Sabre\DAV\Sharing\Plugin::ACCESS_ constants. + * + * @param int $shareAccess + */ + function __construct($shareAccess) { + + $this->value = $shareAccess; + + } + + /** + * Returns the current value. + * + * @return int + */ + function getValue() { + + return $this->value; + + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializble should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + * + * @param Writer $writer + * @return void + */ + function xmlSerialize(Writer $writer) { + + switch ($this->value) { + + case SharingPlugin::ACCESS_NOTSHARED : + $writer->writeElement('{DAV:}not-shared'); + break; + case SharingPlugin::ACCESS_SHAREDOWNER : + $writer->writeElement('{DAV:}shared-owner'); + break; + case SharingPlugin::ACCESS_READ : + $writer->writeElement('{DAV:}read'); + break; + case SharingPlugin::ACCESS_READWRITE : + $writer->writeElement('{DAV:}read-write'); + break; + case SharingPlugin::ACCESS_NOACCESS : + $writer->writeElement('{DAV:}no-access'); + break; + + } + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $elems = $reader->parseInnerTree(); + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}not-shared' : + return new self(SharingPlugin::ACCESS_NOTSHARED); + case '{DAV:}sharedowner' : + return new self(SharingPlugin::ACCESS_SHAREDOWNER); + case '{DAV:}read' : + return new self(SharingPlugin::ACCESS_READ); + case '{DAV:}read-write' : + return new self(SharingPlugin::ACCESS_READWRITE); + case '{DAV:}no-access' : + return new self(SharingPlugin::ACCESS_NOACCESS); + } + } + throw new BadRequest('Invalid value for {DAV:}share-access element'); + + } +} diff --git a/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php b/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php new file mode 100644 index 000000000..965e5857c --- /dev/null +++ b/vendor/sabre/dav/lib/DAV/Xml/Request/ShareResource.php @@ -0,0 +1,81 @@ +<?php + +namespace Sabre\DAV\Xml\Request; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use Sabre\DAV\Xml\Element\Sharee; + +/** + * ShareResource request parser. + * + * This class parses the {DAV:}share-resource POST request as defined in: + * + * https://tools.ietf.org/html/draft-pot-webdav-resource-sharing-01#section-5.3.2.1 + * + * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://www.rooftopsolutions.nl/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ShareResource implements XmlDeserializable { + + /** + * The list of new people added or updated or removed from the share. + * + * @var Sharee[] + */ + public $sharees = []; + + /** + * Constructor + * + * @param Sharee[] $sharees + */ + function __construct(array $sharees) { + + $this->sharees = $sharees; + + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $elems = $reader->parseInnerTree([ + '{DAV:}sharee' => 'Sabre\DAV\Xml\Element\Sharee', + '{DAV:}share-access' => 'Sabre\DAV\Xml\Property\ShareAccess', + '{DAV:}prop' => 'Sabre\Xml\Deserializer\keyValue', + ]); + + $sharees = []; + + foreach ($elems as $elem) { + if ($elem['name'] !== '{DAV:}sharee') continue; + $sharees[] = $elem['value']; + + } + + return new self($sharees); + + } + +} diff --git a/vendor/sabre/dav/lib/DAVACL/ACLTrait.php b/vendor/sabre/dav/lib/DAVACL/ACLTrait.php new file mode 100644 index 000000000..602654a2e --- /dev/null +++ b/vendor/sabre/dav/lib/DAVACL/ACLTrait.php @@ -0,0 +1,100 @@ +<?php + +namespace Sabre\DAVACL; + +/** + * This trait is a default implementation of the IACL interface. + * + * In many cases you only want to implement 1 or to of the IACL functions, + * this trait allows you to be a bit lazier. + * + * By default this trait grants all privileges to the owner of the resource. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (https://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +trait ACLTrait { + + /** + * Returns the owner principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + function getOwner() { + + return null; + + } + + /** + * Returns a group principal + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + function getGroup() { + + return null; + + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + function getACL() { + + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ] + ]; + + } + + /** + * Updates the ACL + * + * This method will receive a list of new ACE's as an array argument. + * + * @param array $acl + * @return void + */ + function setACL(array $acl) { + + throw new \Sabre\DAV\Exception\Forbidden('Setting ACL is not supported on this node'); + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + function getSupportedPrivilegeSet() { + + return null; + + } + +} diff --git a/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php b/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php index 460f78981..9d2026380 100644 --- a/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php +++ b/vendor/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php @@ -110,7 +110,7 @@ abstract class AbstractPrincipalCollection extends DAV\Collection implements IPr * * @param string $name * @throws DAV\Exception\NotFound - * @return IPrincipal + * @return DAV\INode */ function getChild($name) { diff --git a/vendor/sabre/dav/lib/DAVACL/FS/Collection.php b/vendor/sabre/dav/lib/DAVACL/FS/Collection.php index 5fab4768c..1c08b43b1 100644 --- a/vendor/sabre/dav/lib/DAVACL/FS/Collection.php +++ b/vendor/sabre/dav/lib/DAVACL/FS/Collection.php @@ -3,6 +3,7 @@ namespace Sabre\DAVACL\FS; use Sabre\DAV\FSExt\Directory as BaseCollection; +use Sabre\DAVACL\ACLTrait; use Sabre\DAVACL\IACL; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\NotFound; @@ -16,6 +17,8 @@ use Sabre\DAV\Exception\NotFound; */ class Collection extends BaseCollection implements IACL { + use ACLTrait; + /** * A list of ACL rules. * @@ -52,8 +55,8 @@ class Collection extends BaseCollection implements IACL { * exist. * * @param string $name - * @throws DAV\Exception\NotFound - * @return DAV\INode + * @throws NotFound + * @return \Sabre\DAV\INode */ function getChild($name) { @@ -88,19 +91,6 @@ class Collection extends BaseCollection implements IACL { } /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** * Returns a list of ACE's for this node. * * Each ACE has the following properties: @@ -118,36 +108,4 @@ class Collection extends BaseCollection implements IACL { } - /** - * Updates the ACL - * - * This method will receive a list of new ACE's as an array argument. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new Forbidden('Setting ACL is not allowed here'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/DAVACL/FS/File.php b/vendor/sabre/dav/lib/DAVACL/FS/File.php index 0d549528b..387597bf7 100644 --- a/vendor/sabre/dav/lib/DAVACL/FS/File.php +++ b/vendor/sabre/dav/lib/DAVACL/FS/File.php @@ -4,7 +4,7 @@ namespace Sabre\DAVACL\FS; use Sabre\DAV\FSExt\File as BaseFile; use Sabre\DAVACL\IACL; -use Sabre\DAV\Exception\Forbidden; +use Sabre\DAVACL\ACLTrait; /** * This is an ACL-enabled file node. @@ -15,6 +15,8 @@ use Sabre\DAV\Exception\Forbidden; */ class File extends BaseFile implements IACL { + use ACLTrait; + /** * A list of ACL rules. * @@ -58,19 +60,6 @@ class File extends BaseFile implements IACL { } /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** * Returns a list of ACE's for this node. * * Each ACE has the following properties: @@ -88,36 +77,4 @@ class File extends BaseFile implements IACL { } - /** - * Updates the ACL - * - * This method will receive a list of new ACE's as an array argument. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new Forbidden('Setting ACL is not allowed here'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php b/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php index c27616770..9e21353ea 100644 --- a/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php +++ b/vendor/sabre/dav/lib/DAVACL/FS/HomeCollection.php @@ -2,8 +2,8 @@ namespace Sabre\DAVACL\FS; -use Sabre\DAV\Exception\Forbidden; use Sabre\DAVACL\AbstractPrincipalCollection; +use Sabre\DAVACL\ACLTrait; use Sabre\DAVACL\IACL; use Sabre\DAVACL\PrincipalBackend\BackendInterface; use Sabre\Uri; @@ -21,6 +21,8 @@ use Sabre\Uri; */ class HomeCollection extends AbstractPrincipalCollection implements IACL { + use ACLTrait; + /** * Name of this collection. * @@ -70,20 +72,15 @@ class HomeCollection extends AbstractPrincipalCollection implements IACL { * supplied by the authentication backend. * * @param array $principalInfo - * @return void + * @return \Sabre\DAVACL\INode */ function getChildForPrincipal(array $principalInfo) { $owner = $principalInfo['uri']; $acl = [ [ - 'privilege' => '{DAV:}read', - 'principal' => $owner, - 'protected' => true, - ], - [ - 'privilege' => '{DAV:}write', - 'principal' => $owner, + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', 'protected' => true, ], ]; @@ -103,31 +100,6 @@ class HomeCollection extends AbstractPrincipalCollection implements IACL { } - /** - * Returns the owner principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getOwner() { - - return null; - - } - - /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } /** * Returns a list of ACE's for this node. @@ -153,36 +125,4 @@ class HomeCollection extends AbstractPrincipalCollection implements IACL { } - /** - * Updates the ACL - * - * This method will receive a list of new ACE's as an array argument. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new Forbidden('Setting ACL is not allowed here'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/DAVACL/IACL.php b/vendor/sabre/dav/lib/DAVACL/IACL.php index 81908d08f..f7a138665 100644 --- a/vendor/sabre/dav/lib/DAVACL/IACL.php +++ b/vendor/sabre/dav/lib/DAVACL/IACL.php @@ -71,5 +71,4 @@ interface IACL extends DAV\INode { */ function getSupportedPrivilegeSet(); - } diff --git a/vendor/sabre/dav/lib/DAVACL/Plugin.php b/vendor/sabre/dav/lib/DAVACL/Plugin.php index 601dffecc..59a7b0922 100644 --- a/vendor/sabre/dav/lib/DAVACL/Plugin.php +++ b/vendor/sabre/dav/lib/DAVACL/Plugin.php @@ -4,7 +4,11 @@ namespace Sabre\DAVACL; use Sabre\DAV; use Sabre\DAV\INode; +use Sabre\DAV\Xml\Property\Href; use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\Exception\NotAuthenticated; +use Sabre\DAVACL\Exception\NeedPrivileges; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; use Sabre\Uri; @@ -64,18 +68,6 @@ class Plugin extends DAV\ServerPlugin { ]; /** - * By default ACL is only enforced for nodes that have ACL support (the - * ones that implement IACL). For any other node, access is - * always granted. - * - * To override this behaviour you can turn this setting off. This is useful - * if you plan to fully support ACL in the entire tree. - * - * @var bool - */ - public $allowAccessToNodesWithoutACL = true; - - /** * By default nodes that are inaccessible by the user, can still be seen * in directory listings (PROPFIND on parent with Depth: 1) * @@ -109,6 +101,18 @@ class Plugin extends DAV\ServerPlugin { public $adminPrincipals = []; /** + * The ACL plugin allows privileges to be assigned to users that are not + * logged in. To facilitate that, it modifies the auth plugin's behavior + * to only require login when a privileged operation was denied. + * + * Unauthenticated access can be considered a security concern, so it's + * possible to turn this feature off to harden the server's security. + * + * @var bool + */ + public $allowUnauthenticatedAccess = true; + + /** * Returns a list of features added by this plugin. * * This list is used in the response of a HTTP OPTIONS request. @@ -161,6 +165,7 @@ class Plugin extends DAV\ServerPlugin { return [ '{DAV:}expand-property', + '{DAV:}principal-match', '{DAV:}principal-property-search', '{DAV:}principal-search-property-set', ]; @@ -179,7 +184,8 @@ class Plugin extends DAV\ServerPlugin { * @param array|string $privileges * @param int $recursion * @param bool $throwExceptions if set to false, this method won't throw exceptions. - * @throws Sabre\DAVACL\Exception\NeedPrivileges + * @throws NeedPrivileges + * @throws NotAuthenticated * @return bool */ function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) { @@ -188,18 +194,6 @@ class Plugin extends DAV\ServerPlugin { $acl = $this->getCurrentUserPrivilegeSet($uri); - if (is_null($acl)) { - if ($this->allowAccessToNodesWithoutACL) { - return true; - } else { - if ($throwExceptions) - throw new Exception\NeedPrivileges($uri, $privileges); - else - return false; - - } - } - $failed = []; foreach ($privileges as $priv) { @@ -210,10 +204,22 @@ class Plugin extends DAV\ServerPlugin { } if ($failed) { - if ($throwExceptions) - throw new Exception\NeedPrivileges($uri, $failed); - else + if ($this->allowUnauthenticatedAccess && is_null($this->getCurrentUserPrincipal())) { + // We are not authenticated. Kicking in the Auth plugin. + $authPlugin = $this->server->getPlugin('auth'); + $reasons = $authPlugin->getLoginFailedReasons(); + $authPlugin->challenge( + $this->server->httpRequest, + $this->server->httpResponse + ); + throw new notAuthenticated(implode(', ', $reasons) . '. Login was needed for privilege: ' . implode(', ', $failed) . ' on ' . $uri); + } + if ($throwExceptions) { + + throw new NeedPrivileges($uri, $failed); + } else { return false; + } } return true; @@ -229,10 +235,11 @@ class Plugin extends DAV\ServerPlugin { */ function getCurrentUserPrincipal() { - $authPlugin = $this->server->getPlugin('auth'); - if (is_null($authPlugin)) return null; /** @var $authPlugin Sabre\DAV\Auth\Plugin */ - + $authPlugin = $this->server->getPlugin('auth'); + if (!$authPlugin) { + return null; + } return $authPlugin->getCurrentPrincipal(); } @@ -258,6 +265,56 @@ class Plugin extends DAV\ServerPlugin { } /** + * Sets the default ACL rules. + * + * These rules are used for all nodes that don't implement the IACL interface. + * + * @param array $acl + * @return void + */ + function setDefaultAcl(array $acl) { + + $this->defaultAcl = $acl; + + } + + /** + * Returns the default ACL rules. + * + * These rules are used for all nodes that don't implement the IACL interface. + * + * @param array $acl + * @return void + */ + function getDefaultAcl() { + + return $this->defaultAcl; + + } + + /** + * The default ACL rules. + * + * These rules are used for nodes that don't implement IACL. These default + * set of rules allow anyone to do anything, as long as they are + * authenticated. + * + * var array + */ + protected $defaultAcl = [ + [ + 'principal' => '{DAV:}authenticated', + 'protected' => true, + 'privilege' => '{DAV:}all', + ], + [ + 'principal' => '{DAV:}unauthenticated', + 'protected' => true, + 'privilege' => '{DAV:}read', + ], + ]; + + /** * This array holds a cache for all the principals that are associated with * a single principal. * @@ -311,13 +368,77 @@ class Plugin extends DAV\ServerPlugin { } /** - * Returns the supported privilege structure for this ACL plugin. + * Find out of a principal equals another principal. + * + * This is a quick way to find out wether a principal URI is part of a + * group, or any subgroups. * - * See RFC3744 for more details. Currently we default on a simple, - * standard structure. + * The first argument is the principal URI you want to check against. For + * example the principal group, and the second argument is the principal of + * which you want to find out of it is the same as the first principal, or + * in a member of the first principal's group or subgroups. * - * You can either get the list of privileges by a uri (path) or by - * specifying a Node. + * So the arguments are not interchangable. If principal A is in group B, + * passing 'B', 'A' will yield true, but 'A', 'B' is false. + * + * If the sceond argument is not passed, we will use the current user + * principal. + * + * @param string $checkPrincipal + * @param string $currentPrincipal + * @return bool + */ + function principalMatchesPrincipal($checkPrincipal, $currentPrincipal = null) { + + if (is_null($currentPrincipal)) { + $currentPrincipal = $this->getCurrentUserPrincipal(); + } + if ($currentPrincipal === $checkPrincipal) { + return true; + } + return in_array( + $checkPrincipal, + $this->getPrincipalMembership($currentPrincipal) + ); + + } + + + /** + * Returns a tree of supported privileges for a resource. + * + * The returned array structure should be in this form: + * + * [ + * [ + * 'privilege' => '{DAV:}read', + * 'abstract' => false, + * 'aggregates' => [] + * ] + * ] + * + * Privileges can be nested using "aggregrates". Doing so means that + * if you assign someone the aggregrating privilege, all the + * sub-privileges will automatically be granted. + * + * Marking a privilege as abstract means that the privilege cannot be + * directly assigned, but must be assigned via the parent privilege. + * + * So a more complex version might look like this: + * + * [ + * [ + * 'privilege' => '{DAV:}read', + * 'abstract' => false, + * 'aggregates' => [ + * [ + * 'privilege' => '{DAV:}read-acl', + * 'abstract' => false, + * 'aggregates' => [], + * ] + * ] + * ] + * ] * * @param string|INode $node * @return array @@ -328,73 +449,71 @@ class Plugin extends DAV\ServerPlugin { $node = $this->server->tree->getNodeForPath($node); } + $supportedPrivileges = null; if ($node instanceof IACL) { - $result = $node->getSupportedPrivilegeSet(); - - if ($result) - return $result; + $supportedPrivileges = $node->getSupportedPrivilegeSet(); } - return self::getDefaultSupportedPrivilegeSet(); + if (is_null($supportedPrivileges)) { - } - - /** - * Returns a fairly standard set of privileges, which may be useful for - * other systems to use as a basis. - * - * @return array - */ - static function getDefaultSupportedPrivilegeSet() { - - return [ - 'privilege' => '{DAV:}all', - 'abstract' => true, - 'aggregates' => [ - [ - 'privilege' => '{DAV:}read', + // Default + $supportedPrivileges = [ + '{DAV:}read' => [ + 'abstract' => false, 'aggregates' => [ - [ - 'privilege' => '{DAV:}read-acl', - 'abstract' => false, + '{DAV:}read-acl' => [ + 'abstract' => false, + 'aggregates' => [], ], - [ - 'privilege' => '{DAV:}read-current-user-privilege-set', - 'abstract' => false, + '{DAV:}read-current-user-privilege-set' => [ + 'abstract' => false, + 'aggregates' => [], ], ], - ], // {DAV:}read - [ - 'privilege' => '{DAV:}write', + ], + '{DAV:}write' => [ + 'abstract' => false, 'aggregates' => [ - [ - 'privilege' => '{DAV:}write-acl', - 'abstract' => false, - ], - [ - 'privilege' => '{DAV:}write-properties', - 'abstract' => false, - ], - [ - 'privilege' => '{DAV:}write-content', - 'abstract' => false, + '{DAV:}write-properties' => [ + 'abstract' => false, + 'aggregates' => [], ], - [ - 'privilege' => '{DAV:}bind', - 'abstract' => false, + '{DAV:}write-content' => [ + 'abstract' => false, + 'aggregates' => [], ], - [ - 'privilege' => '{DAV:}unbind', - 'abstract' => false, - ], - [ - 'privilege' => '{DAV:}unlock', - 'abstract' => false, + '{DAV:}unlock' => [ + 'abstract' => false, + 'aggregates' => [], ], ], - ], // {DAV:}write - ], - ]; // {DAV:}all + ], + ]; + if ($node instanceof \Sabre\DAV\ICollection) { + $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}bind'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}unbind'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + if ($node instanceof \Sabre\DAVACL\IACL) { + $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}write-acl'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + + } + + $this->server->emit( + 'getSupportedPrivilegeSet', + [$node, &$supportedPrivileges] + ); + + return $supportedPrivileges; } @@ -414,35 +533,38 @@ class Plugin extends DAV\ServerPlugin { */ final function getFlatPrivilegeSet($node) { - $privs = $this->getSupportedPrivilegeSet($node); + $privs = [ + 'abstract' => false, + 'aggregates' => $this->getSupportedPrivilegeSet($node) + ]; $fpsTraverse = null; - $fpsTraverse = function($priv, $concrete, &$flat) use (&$fpsTraverse) { + $fpsTraverse = function($privName, $privInfo, $concrete, &$flat) use (&$fpsTraverse) { $myPriv = [ - 'privilege' => $priv['privilege'], - 'abstract' => isset($priv['abstract']) && $priv['abstract'], + 'privilege' => $privName, + 'abstract' => isset($privInfo['abstract']) && $privInfo['abstract'], 'aggregates' => [], - 'concrete' => isset($priv['abstract']) && $priv['abstract'] ? $concrete : $priv['privilege'], + 'concrete' => isset($privInfo['abstract']) && $privInfo['abstract'] ? $concrete : $privName, ]; - if (isset($priv['aggregates'])) { + if (isset($privInfo['aggregates'])) { - foreach ($priv['aggregates'] as $subPriv) { + foreach ($privInfo['aggregates'] as $subPrivName => $subPrivInfo) { - $myPriv['aggregates'][] = $subPriv['privilege']; + $myPriv['aggregates'][] = $subPrivName; } } - $flat[$priv['privilege']] = $myPriv; + $flat[$privName] = $myPriv; - if (isset($priv['aggregates'])) { + if (isset($privInfo['aggregates'])) { - foreach ($priv['aggregates'] as $subPriv) { + foreach ($privInfo['aggregates'] as $subPrivName => $subPrivInfo) { - $fpsTraverse($subPriv, $myPriv['concrete'], $flat); + $fpsTraverse($subPrivName, $subPrivInfo, $myPriv['concrete'], $flat); } @@ -451,7 +573,7 @@ class Plugin extends DAV\ServerPlugin { }; $flat = []; - $fpsTraverse($privs, null, $flat); + $fpsTraverse('{DAV:}all', $privs, null, $flat); return $flat; @@ -467,13 +589,13 @@ class Plugin extends DAV\ServerPlugin { * @param string|DAV\INode $node * @return array */ - function getACL($node) { + function getAcl($node) { if (is_string($node)) { $node = $this->server->tree->getNodeForPath($node); } if (!$node instanceof IACL) { - return null; + return $this->getDefaultAcl(); } $acl = $node->getACL(); foreach ($this->adminPrincipals as $adminPrincipal) { @@ -506,12 +628,10 @@ class Plugin extends DAV\ServerPlugin { $acl = $this->getACL($node); - if (is_null($acl)) return null; - - $principals = $this->getCurrentUserPrincipals(); - $collected = []; + $isAuthenticated = $this->getCurrentUserPrincipal() !== null; + foreach ($acl as $ace) { $principal = $ace['principal']; @@ -520,7 +640,7 @@ class Plugin extends DAV\ServerPlugin { case '{DAV:}owner' : $owner = $node->getOwner(); - if ($owner && in_array($owner, $principals)) { + if ($owner && $this->principalMatchesPrincipal($owner)) { $collected[] = $ace; } break; @@ -528,21 +648,25 @@ class Plugin extends DAV\ServerPlugin { // 'all' matches for every user case '{DAV:}all' : + $collected[] = $ace; + break; - // 'authenticated' matched for every user that's logged in. - // Since it's not possible to use ACL while not being logged - // in, this is also always true. case '{DAV:}authenticated' : - $collected[] = $ace; + // Authenticated users only + if ($isAuthenticated) { + $collected[] = $ace; + } break; - // 'unauthenticated' can never occur either, so we simply - // ignore these. case '{DAV:}unauthenticated' : + // Unauthenticated users only + if (!$isAuthenticated) { + $collected[] = $ace; + } break; default : - if (in_array($ace['principal'], $principals)) { + if ($this->principalMatchesPrincipal($ace['principal'])) { $collected[] = $ace; } break; @@ -561,6 +685,11 @@ class Plugin extends DAV\ServerPlugin { $current = array_pop($collected); $collected2[] = $current['privilege']; + if (!isset($flat[$current['privilege']])) { + // Ignoring privileges that are not in the supported-privileges list. + $this->server->getLogger()->debug('A node has the "' . $current['privilege'] . '" in its ACL list, but this privilege was not reported in the supportedPrivilegeSet list. This will be ignored.'); + continue; + } foreach ($flat[$current['privilege']]['aggregates'] as $subPriv) { $collected2[] = $subPriv; $collected[] = $flat[$subPriv]; @@ -587,7 +716,13 @@ class Plugin extends DAV\ServerPlugin { $collections = $this->principalCollectionSet; foreach ($collections as $collection) { - $principalCollection = $this->server->tree->getNodeForPath($collection); + try { + $principalCollection = $this->server->tree->getNodeForPath($collection); + } catch (NotFound $e) { + // Ignore and move on + continue; + } + if (!$principalCollection instanceof IPrincipalCollection) { // Not a principal collection, we're simply going to ignore // this. @@ -673,6 +808,14 @@ class Plugin extends DAV\ServerPlugin { */ function initialize(DAV\Server $server) { + if ($this->allowUnauthenticatedAccess) { + $authPlugin = $server->getPlugin('auth'); + if (!$authPlugin) { + throw new \Exception('The Auth plugin must be loaded before the ACL plugin if you want to allow unauthenticated access.'); + } + $authPlugin->autoRequireLogin = false; + } + $this->server = $server; $server->on('propFind', [$this, 'propFind'], 20); $server->on('beforeMethod', [$this, 'beforeMethod'], 20); @@ -683,6 +826,14 @@ class Plugin extends DAV\ServerPlugin { $server->on('report', [$this, 'report']); $server->on('method:ACL', [$this, 'httpAcl']); $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('getPrincipalByUri', function($principal, &$uri) { + + $uri = $this->getPrincipalByUri($principal); + + // Break event chain + if ($uri) return false; + + }); array_push($server->protectedProperties, '{DAV:}alternate-URI-set', @@ -707,9 +858,11 @@ class Plugin extends DAV\ServerPlugin { // class. $server->xml->elementMap['{DAV:}group-member-set'] = 'Sabre\\DAV\\Xml\\Property\\Href'; $server->xml->elementMap['{DAV:}acl'] = 'Sabre\\DAVACL\\Xml\\Property\\Acl'; + $server->xml->elementMap['{DAV:}acl-principal-prop-set'] = 'Sabre\\DAVACL\\Xml\\Request\\AclPrincipalPropSetReport'; $server->xml->elementMap['{DAV:}expand-property'] = 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport'; $server->xml->elementMap['{DAV:}principal-property-search'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport'; $server->xml->elementMap['{DAV:}principal-search-property-set'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport'; + $server->xml->elementMap['{DAV:}principal-match'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalMatchReport'; } @@ -743,7 +896,6 @@ class Plugin extends DAV\ServerPlugin { case 'PUT' : case 'LOCK' : - case 'UNLOCK' : // This method requires the write-content priv if the node // already exists, and bind on the parent if the node is being // created. @@ -751,6 +903,9 @@ class Plugin extends DAV\ServerPlugin { $this->checkPrivileges($path, '{DAV:}write-content'); break; + case 'UNLOCK' : + // Unlock is always allowed at the moment. + break; case 'PROPPATCH' : $this->checkPrivileges($path, '{DAV:}write-properties'); @@ -774,7 +929,6 @@ class Plugin extends DAV\ServerPlugin { // If MOVE is used beforeUnbind will also be used to check if // the sourcenode can be deleted. $this->checkPrivileges($path, '{DAV:}read', self::R_RECURSIVE); - break; } @@ -864,24 +1018,24 @@ class Plugin extends DAV\ServerPlugin { if ($node instanceof IPrincipal) { $propFind->handle('{DAV:}alternate-URI-set', function() use ($node) { - return new DAV\Xml\Property\Href($node->getAlternateUriSet()); + return new Href($node->getAlternateUriSet()); }); $propFind->handle('{DAV:}principal-URL', function() use ($node) { - return new DAV\Xml\Property\Href($node->getPrincipalUrl() . '/'); + return new Href($node->getPrincipalUrl() . '/'); }); $propFind->handle('{DAV:}group-member-set', function() use ($node) { $members = $node->getGroupMemberSet(); foreach ($members as $k => $member) { $members[$k] = rtrim($member, '/') . '/'; } - return new DAV\Xml\Property\Href($members); + return new Href($members); }); $propFind->handle('{DAV:}group-membership', function() use ($node) { $members = $node->getGroupMembership(); foreach ($members as $k => $member) { $members[$k] = rtrim($member, '/') . '/'; } - return new DAV\Xml\Property\Href($members); + return new Href($members); }); $propFind->handle('{DAV:}displayname', [$node, 'getDisplayName']); @@ -892,7 +1046,7 @@ class Plugin extends DAV\ServerPlugin { $val = $this->principalCollectionSet; // Ensuring all collections end with a slash foreach ($val as $k => $v) $val[$k] = $v . '/'; - return new DAV\Xml\Property\Href($val); + return new Href($val); }); $propFind->handle('{DAV:}current-user-principal', function() { @@ -910,9 +1064,7 @@ class Plugin extends DAV\ServerPlugin { $propFind->set('{DAV:}current-user-privilege-set', null, 403); } else { $val = $this->getCurrentUserPrivilegeSet($node); - if (!is_null($val)) { - return new Xml\Property\CurrentUserPrivilegeSet($val); - } + return new Xml\Property\CurrentUserPrivilegeSet($val); } }); $propFind->handle('{DAV:}acl', function() use ($node, $propFind, $path) { @@ -921,9 +1073,7 @@ class Plugin extends DAV\ServerPlugin { $propFind->set('{DAV:}acl', null, 403); } else { $acl = $this->getACL($node); - if (!is_null($acl)) { - return new Xml\Property\Acl($this->getACL($node)); - } + return new Xml\Property\Acl($this->getACL($node)); } }); $propFind->handle('{DAV:}acl-restrictions', function() { @@ -933,7 +1083,7 @@ class Plugin extends DAV\ServerPlugin { /* Adding ACL properties */ if ($node instanceof IACL) { $propFind->handle('{DAV:}owner', function() use ($node) { - return new DAV\Xml\Property\Href($node->getOwner() . '/'); + return new Href($node->getOwner() . '/'); }); } @@ -952,7 +1102,7 @@ class Plugin extends DAV\ServerPlugin { $propPatch->handle('{DAV:}group-member-set', function($value) use ($path) { if (is_null($value)) { $memberSet = []; - } elseif ($value instanceof DAV\Xml\Property\Href) { + } elseif ($value instanceof Href) { $memberSet = array_map( [$this->server, 'calculateUri'], $value->getHrefs() @@ -990,15 +1140,23 @@ class Plugin extends DAV\ServerPlugin { case '{DAV:}principal-property-search' : $this->server->transactionType = 'report-principal-property-search'; - $this->principalPropertySearchReport($report); + $this->principalPropertySearchReport($path, $report); return false; case '{DAV:}principal-search-property-set' : $this->server->transactionType = 'report-principal-search-property-set'; - $this->principalSearchPropertySetReport($report); + $this->principalSearchPropertySetReport($path, $report); return false; case '{DAV:}expand-property' : $this->server->transactionType = 'report-expand-property'; - $this->expandPropertyReport($report); + $this->expandPropertyReport($path, $report); + return false; + case '{DAV:}principal-match' : + $this->server->transactionType = 'report-principal-match'; + $this->principalMatchReport($path, $report); + return false; + case '{DAV:}acl-principal-prop-set' : + $this->server->transactionType = 'acl-principal-prop-set'; + $this->aclPrincipalPropSetReport($path, $report); return false; } @@ -1073,7 +1231,7 @@ class Plugin extends DAV\ServerPlugin { // Looking up the principal try { $principal = $this->server->tree->getNodeForPath($newAce['principal']); - } catch (DAV\Exception\NotFound $e) { + } catch (NotFound $e) { throw new Exception\NotRecognizedPrincipal('The specified principal (' . $newAce['principal'] . ') does not exist'); } if (!($principal instanceof IPrincipal)) { @@ -1095,7 +1253,110 @@ class Plugin extends DAV\ServerPlugin { /* Reports {{{ */ /** - * The expand-property report is defined in RFC3253 section 3-8. + * The principal-match report is defined in RFC3744, section 9.3. + * + * This report allows a client to figure out based on the current user, + * or a principal URL, the principal URL and principal URLs of groups that + * principal belongs to. + * + * @param string $path + * @param Xml\Request\PrincipalMatchReport $report + * @return void + */ + protected function principalMatchReport($path, Xml\Request\PrincipalMatchReport $report) { + + $depth = $this->server->getHTTPDepth(0); + if ($depth !== 0) { + throw new BadRequest('The principal-match report is only defined on Depth: 0'); + } + + $currentPrincipals = $this->getCurrentUserPrincipals(); + + $result = []; + + if ($report->type === Xml\Request\PrincipalMatchReport::SELF) { + + // Finding all principals under the request uri that match the + // current principal. + foreach ($currentPrincipals as $currentPrincipal) { + + if ($currentPrincipal === $path || strpos($currentPrincipal, $path . '/') === 0) { + $result[] = $currentPrincipal; + } + + } + + } else { + + // We need to find all resources that have a property that matches + // one of the current principals. + $candidates = $this->server->getPropertiesForPath( + $path, + [$report->principalProperty], + 1 + ); + + foreach ($candidates as $candidate) { + + if (!isset($candidate[200][$report->principalProperty])) { + continue; + } + + $hrefs = $candidate[200][$report->principalProperty]; + + if (!$hrefs instanceof Href) { + continue; + } + + foreach ($hrefs->getHrefs() as $href) { + if (in_array(trim($href, '/'), $currentPrincipals)) { + $result[] = $candidate['href']; + continue 2; + } + } + } + + } + + $responses = []; + + foreach ($result as $item) { + + $properties = []; + + if ($report->properties) { + + $foo = $this->server->getPropertiesForPath($item, $report->properties); + $foo = $foo[0]; + $item = $foo['href']; + unset($foo['href']); + $properties = $foo; + + } + + $responses[] = new DAV\Xml\Element\Response( + $item, + $properties, + '200' + ); + + } + + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setBody( + $this->server->xml->write( + '{DAV:}multistatus', + $responses, + $this->server->getBaseUri() + ) + ); + + + } + + /** + * The expand-property report is defined in RFC3253 section 3.8. * * This report is very similar to a standard PROPFIND. The difference is * that it has the additional ability to look at properties containing a @@ -1105,15 +1366,15 @@ class Plugin extends DAV\ServerPlugin { * Other rfc's, such as ACL rely on this report, so it made sense to put * it in this plugin. * + * @param string $path * @param Xml\Request\ExpandPropertyReport $report * @return void */ - protected function expandPropertyReport($report) { + protected function expandPropertyReport($path, $report) { $depth = $this->server->getHTTPDepth(0); - $requestUri = $this->server->getRequestUri(); - $result = $this->expandProperties($requestUri, $report->properties, $depth); + $result = $this->expandProperties($path, $report->properties, $depth); $xml = $this->server->xml->write( '{DAV:}multistatus', @@ -1187,10 +1448,11 @@ class Plugin extends DAV\ServerPlugin { * of properties the client may search on, using the * {DAV:}principal-property-search report. * + * @param string $path * @param Xml\Request\PrincipalSearchPropertySetReport $report * @return void */ - protected function principalSearchPropertySetReport($report) { + protected function principalSearchPropertySetReport($path, $report) { $httpDepth = $this->server->getHTTPDepth(0); if ($httpDepth !== 0) { @@ -1241,14 +1503,14 @@ class Plugin extends DAV\ServerPlugin { * clients to search for groups of principals, based on the value of one * or more properties. * + * @param string $path * @param Xml\Request\PrincipalPropertySearchReport $report * @return void */ - protected function principalPropertySearchReport($report) { + protected function principalPropertySearchReport($path, Xml\Request\PrincipalPropertySearchReport $report) { - $uri = null; - if (!$report->applyToPrincipalCollectionSet) { - $uri = $this->server->httpRequest->getPath(); + if ($report->applyToPrincipalCollectionSet) { + $path = null; } if ($this->server->getHttpDepth('0') !== 0) { throw new BadRequest('Depth must be 0'); @@ -1256,7 +1518,7 @@ class Plugin extends DAV\ServerPlugin { $result = $this->principalSearch( $report->searchProperties, $report->properties, - $uri, + $path, $report->test ); @@ -1269,6 +1531,64 @@ class Plugin extends DAV\ServerPlugin { } + /** + * aclPrincipalPropSet REPORT + * + * This method is responsible for handling the {DAV:}acl-principal-prop-set + * REPORT, as defined in: + * + * https://tools.ietf.org/html/rfc3744#section-9.2 + * + * This REPORT allows a user to quickly fetch information about all + * principals specified in the access control list. Most commonly this + * is used to for example generate a UI with ACL rules, allowing you + * to show names for principals for every entry. + * + * @param string $path + * @param Xml\Request\AclPrincipalPropSetReport $report + * @return void + */ + protected function aclPrincipalPropSetReport($path, Xml\Request\AclPrincipalPropSetReport $report) { + + if ($this->server->getHTTPDepth(0) !== 0) { + throw new BadRequest('The {DAV:}acl-principal-prop-set REPORT only supports Depth 0'); + } + + // Fetching ACL rules for the given path. We're using the property + // API and not the local getACL, because it will ensure that all + // business rules and restrictions are applied. + $acl = $this->server->getProperties($path, '{DAV:}acl'); + + if (!$acl || !isset($acl['{DAV:}acl'])) { + throw new Forbidden('Could not fetch ACL rules for this path'); + } + + $principals = []; + foreach ($acl['{DAV:}acl']->getPrivileges() as $ace) { + + if ($ace['principal'][0] === '{') { + // It's not a principal, it's one of the special rules such as {DAV:}authenticated + continue; + } + + $principals[] = $ace['principal']; + + } + + $properties = $this->server->getPropertiesForMultiplePaths( + $principals, + $report->properties + ); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setBody( + $this->server->generateMultiStatus($properties) + ); + + } + + /* }}} */ /** diff --git a/vendor/sabre/dav/lib/DAVACL/Principal.php b/vendor/sabre/dav/lib/DAVACL/Principal.php index 16375d3fc..6ebb30907 100644 --- a/vendor/sabre/dav/lib/DAVACL/Principal.php +++ b/vendor/sabre/dav/lib/DAVACL/Principal.php @@ -22,6 +22,8 @@ use Sabre\HTTP\URLUtil; */ class Principal extends DAV\Node implements IPrincipal, DAV\IProperties, IACL { + use ACLTrait; + /** * Struct with principal information. * @@ -216,73 +218,4 @@ class Principal extends DAV\Node implements IPrincipal, DAV\IProperties, IACL { } - /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - - return null; - - } - - /** - * Returns a list of ACE's for this node. - * - * Each ACE has the following properties: - * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are - * currently the only supported privileges - * * 'principal', a url to the principal who owns the node - * * 'protected' (optional), indicating that this ACE is not allowed to - * be updated. - * - * @return array - */ - function getACL() { - - return [ - [ - 'privilege' => '{DAV:}read', - 'principal' => '{DAV:}authenticated', - 'protected' => true, - ], - ]; - - } - - /** - * Updates the ACL - * - * This method will receive a list of new ACE's. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new DAV\Exception\MethodNotAllowed('Updating ACLs is not allowed here'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php b/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php index 01b7a175c..a491dc88f 100644 --- a/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php +++ b/vendor/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php @@ -300,7 +300,7 @@ class PDO extends AbstractBackend implements CreatePrincipalSupport { $value = null; $scheme = null; list($scheme, $value) = explode(":", $uri, 2); - if ($value == null) return null; + if (empty($value)) return null; $uri = null; switch ($scheme){ diff --git a/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php b/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php index 54911e7b5..d8a90153a 100644 --- a/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php +++ b/vendor/sabre/dav/lib/DAVACL/PrincipalCollection.php @@ -3,7 +3,6 @@ namespace Sabre\DAVACL; use Sabre\DAV\Exception\InvalidResourceType; -use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\IExtendedCollection; use Sabre\DAV\MkCol; @@ -19,6 +18,8 @@ use Sabre\DAV\MkCol; */ class PrincipalCollection extends AbstractPrincipalCollection implements IExtendedCollection, IACL { + use ACLTrait; + /** * This method returns a node for a principal. * @@ -73,28 +74,6 @@ class PrincipalCollection extends AbstractPrincipalCollection implements IExtend } /** - * Returns the owner principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getOwner() { - return null; - } - - /** - * Returns a group principal - * - * This must be a url to a principal, or null if there's no owner - * - * @return string|null - */ - function getGroup() { - return null; - } - - /** * Returns a list of ACE's for this node. * * Each ACE has the following properties: @@ -116,36 +95,4 @@ class PrincipalCollection extends AbstractPrincipalCollection implements IExtend ]; } - /** - * Updates the ACL - * - * This method will receive a list of new ACE's as an array argument. - * - * @param array $acl - * @return void - */ - function setACL(array $acl) { - - throw new Forbidden('Updating ACLs is not allowed on this node'); - - } - - /** - * Returns the list of supported privileges for this node. - * - * The returned data structure is a list of nested privileges. - * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple - * standard structure. - * - * If null is returned from this method, the default privilege set is used, - * which is fine for most common usecases. - * - * @return array|null - */ - function getSupportedPrivilegeSet() { - - return null; - - } - } diff --git a/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php b/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php index 572bed4dd..55e7783ae 100644 --- a/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php +++ b/vendor/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php @@ -73,7 +73,7 @@ class SupportedPrivilegeSet implements XmlSerializable, HtmlOutput { */ function xmlSerialize(Writer $writer) { - $this->serializePriv($writer, $this->privileges); + $this->serializePriv($writer, '{DAV:}all', [ 'aggregates' => $this->privileges]); } @@ -93,9 +93,9 @@ class SupportedPrivilegeSet implements XmlSerializable, HtmlOutput { */ function toHtml(HtmlOutputHelper $html) { - $traverse = function($priv) use (&$traverse, $html) { + $traverse = function($privName, $priv) use (&$traverse, $html) { echo "<li>"; - echo $html->xmlName($priv['privilege']); + echo $html->xmlName($privName); if (isset($priv['abstract']) && $priv['abstract']) { echo " <i>(abstract)</i>"; } @@ -104,8 +104,8 @@ class SupportedPrivilegeSet implements XmlSerializable, HtmlOutput { } if (isset($priv['aggregates'])) { echo "\n<ul>\n"; - foreach ($priv['aggregates'] as $subPriv) { - $traverse($subPriv); + foreach ($priv['aggregates'] as $subPrivName => $subPriv) { + $traverse($subPrivName, $subPriv); } echo "</ul>"; } @@ -114,7 +114,7 @@ class SupportedPrivilegeSet implements XmlSerializable, HtmlOutput { ob_start(); echo "<ul class=\"tree\">"; - $traverse($this->getValue()); + $traverse('{DAV:}all', [ 'aggregates' => $this->getValue() ]); echo "</ul>\n"; return ob_get_clean(); @@ -132,12 +132,12 @@ class SupportedPrivilegeSet implements XmlSerializable, HtmlOutput { * @param array $privilege * @return void */ - private function serializePriv(Writer $writer, $privilege) { + private function serializePriv(Writer $writer, $privName, $privilege) { $writer->startElement('{DAV:}supported-privilege'); $writer->startElement('{DAV:}privilege'); - $writer->writeElement($privilege['privilege']); + $writer->writeElement($privName); $writer->endElement(); // privilege if (!empty($privilege['abstract'])) { @@ -147,8 +147,8 @@ class SupportedPrivilegeSet implements XmlSerializable, HtmlOutput { $writer->writeElement('{DAV:}description', $privilege['description']); } if (isset($privilege['aggregates'])) { - foreach ($privilege['aggregates'] as $subPrivilege) { - $this->serializePriv($writer, $subPrivilege); + foreach ($privilege['aggregates'] as $subPrivName => $subPrivilege) { + $this->serializePriv($writer, $subPrivName, $subPrivilege); } } diff --git a/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php b/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php new file mode 100644 index 000000000..f01c1e6ab --- /dev/null +++ b/vendor/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php @@ -0,0 +1,67 @@ +<?php + +namespace Sabre\DAVACL\Xml\Request; + +use Sabre\Xml\XmlDeserializable; +use Sabre\Xml\Reader; +use Sabre\Xml\Deserializer; + +/** + * AclPrincipalPropSet request parser. + * + * This class parses the {DAV:}acl-principal-prop-set REPORT, as defined in: + * + * https://tools.ietf.org/html/rfc3744#section-9.2 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (https://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class AclPrincipalPropSetReport implements XmlDeserializable { + + public $properties = []; + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $reader->pushContext(); + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\enum'; + + $elems = Deserializer\keyValue( + $reader, + 'DAV:' + ); + + $reader->popContext(); + + $report = new self(); + + if (!empty($elems['prop'])) { + $report->properties = $elems['prop']; + } + + return $report; + + } + +} diff --git a/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php b/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php new file mode 100644 index 000000000..5c4e88189 --- /dev/null +++ b/vendor/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php @@ -0,0 +1,107 @@ +<?php + +namespace Sabre\DAVACL\Xml\Request; + +use Sabre\Xml\XmlDeserializable; +use Sabre\Xml\Reader; +use Sabre\Xml\Deserializer; + +/** + * PrincipalMatchReport request parser. + * + * This class parses the {DAV:}principal-match REPORT, as defined + * in: + * + * https://tools.ietf.org/html/rfc3744#section-9.3 + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class PrincipalMatchReport implements XmlDeserializable { + + /** + * Report on a list of principals that match the current principal. + */ + const SELF = 1; + + /** + * Report on a property on resources, such as {DAV:}owner, that match the current principal. + */ + const PRINCIPAL_PROPERTY = 2; + + /** + * Must be SELF or PRINCIPAL_PROPERTY + * + * @var int + */ + public $type; + + /** + * List of properties that are being requested for matching resources. + * + * @var string[] + */ + public $properties = []; + + /** + * If $type = PRINCIPAL_PROPERTY, which WebDAV property we should compare + * to the current principal. + * + * @var string + */ + public $principalProperty; + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statictly, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param Reader $reader + * @return mixed + */ + static function xmlDeserialize(Reader $reader) { + + $reader->pushContext(); + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\enum'; + + $elems = Deserializer\keyValue( + $reader, + 'DAV:' + ); + + $reader->popContext(); + + $principalMatch = new self(); + + if (array_key_exists('self', $elems)) { + $principalMatch->type = self::SELF; + } + + if (array_key_exists('principal-property', $elems)) { + $principalMatch->type = self::PRINCIPAL_PROPERTY; + $principalMatch->principalProperty = $elems['principal-property'][0]['name']; + } + + if (!empty($elems['prop'])) { + $principalMatch->properties = $elems['prop']; + } + + return $principalMatch; + + } + +} |