diff options
author | Lukas Reschke <lukas@statuscode.ch> | 2016-09-27 18:51:40 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-09-27 18:51:40 +0200 |
commit | 06e969cb74aacc3d6490d3207a625cc712941968 (patch) | |
tree | 5b5a47b35ae419bdacf37768da99fa37c1ab9f87 /apps/dav/lib/CalDAV | |
parent | 6ca8ce62288aa5b181f97c45ad62c35976dbb65f (diff) | |
parent | dcc23114e9328d822539782644bedcf5cba7e72d (diff) | |
download | nextcloud-server-06e969cb74aacc3d6490d3207a625cc712941968.tar.gz nextcloud-server-06e969cb74aacc3d6490d3207a625cc712941968.zip |
Merge pull request #1197 from nextcloud/oc-public-sharing
CalDAV calendar public sharing
Diffstat (limited to 'apps/dav/lib/CalDAV')
-rw-r--r-- | apps/dav/lib/CalDAV/CalDavBackend.php | 178 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Calendar.php | 39 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/PublicCalendarRoot.php | 67 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Publishing/PublishPlugin.php | 227 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php | 83 |
5 files changed, 593 insertions, 1 deletions
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 17479c490a9..440188c13ca 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -29,9 +29,11 @@ use OCA\DAV\DAV\Sharing\IShareable; use OCP\DB\QueryBuilder\IQueryBuilder; use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\DAV\Sharing\Backend; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IUser; use OCP\IUserManager; +use OCP\Security\ISecureRandom; use Sabre\CalDAV\Backend\AbstractBackend; use Sabre\CalDAV\Backend\SchedulingSupport; use Sabre\CalDAV\Backend\SubscriptionSupport; @@ -41,6 +43,7 @@ use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; use Sabre\DAV; use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; use Sabre\DAV\PropPatch; use Sabre\HTTP\URLUtil; use Sabre\VObject\DateTimeParser; @@ -66,6 +69,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ const MAX_DATE = '2038-01-01'; + const ACCESS_PUBLIC = 4; const CLASSIFICATION_PUBLIC = 0; const CLASSIFICATION_PRIVATE = 1; const CLASSIFICATION_CONFIDENTIAL = 2; @@ -117,6 +121,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** @var IUserManager */ private $userManager; + + /** @var IConfig */ + private $config; + + /** @var ISecureRandom */ + private $random; /** * CalDavBackend constructor. @@ -124,12 +134,20 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param IDBConnection $db * @param Principal $principalBackend * @param IUserManager $userManager + * @param IConfig $config + * @param ISecureRandom $random */ - public function __construct(IDBConnection $db, Principal $principalBackend, IUserManager $userManager) { + public function __construct(IDBConnection $db, + Principal $principalBackend, + IUserManager $userManager, + IConfig $config, + ISecureRandom $random) { $this->db = $db; $this->principalBackend = $principalBackend; $this->userManager = $userManager; $this->sharingBackend = new Backend($this->db, $principalBackend, 'calendar'); + $this->config = $config; + $this->random = $random; } /** @@ -295,6 +313,120 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription return $this->userDisplayNames[$uid]; } + + /** + * @return array + */ + public function getPublicCalendars() { + $fields = array_values($this->propertyMap); + $fields[] = 'a.id'; + $fields[] = 'a.uri'; + $fields[] = 'a.synctoken'; + $fields[] = 'a.components'; + $fields[] = 'a.principaluri'; + $fields[] = 'a.transparent'; + $fields[] = 's.access'; + $fields[] = 's.publicuri'; + $calendars = []; + $query = $this->db->getQueryBuilder(); + $result = $query->select($fields) + ->from('dav_shares', 's') + ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) + ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC))) + ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar'))) + ->execute(); + + while($row = $result->fetch()) { + list(, $name) = URLUtil::splitPath($row['principaluri']); + $row['displayname'] = $row['displayname'] . "($name)"; + $components = []; + if ($row['components']) { + $components = explode(',',$row['components']); + } + $calendar = [ + 'id' => $row['id'], + 'uri' => $row['publicuri'], + 'principaluri' => $row['principaluri'], + '{' . 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', + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ, + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC, + ]; + + foreach($this->propertyMap as $xmlName=>$dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + if (!isset($calendars[$calendar['id']])) { + $calendars[$calendar['id']] = $calendar; + } + } + $result->closeCursor(); + + return array_values($calendars); + } + + /** + * @param string $uri + * @return array + * @throws NotFound + */ + public function getPublicCalendar($uri) { + $fields = array_values($this->propertyMap); + $fields[] = 'a.id'; + $fields[] = 'a.uri'; + $fields[] = 'a.synctoken'; + $fields[] = 'a.components'; + $fields[] = 'a.principaluri'; + $fields[] = 'a.transparent'; + $fields[] = 's.access'; + $fields[] = 's.publicuri'; + $query = $this->db->getQueryBuilder(); + $result = $query->select($fields) + ->from('dav_shares', 's') + ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) + ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC))) + ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar'))) + ->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri))) + ->execute(); + + $row = $result->fetch(\PDO::FETCH_ASSOC); + + $result->closeCursor(); + + if ($row === false) { + throw new NotFound('Node with name \'' . $uri . '\' could not be found'); + } + + list(, $name) = URLUtil::splitPath($row['principaluri']); + $row['displayname'] = $row['displayname'] . ' ' . "($name)"; + $components = []; + if ($row['components']) { + $components = explode(',',$row['components']); + } + $calendar = [ + 'id' => $row['id'], + 'uri' => $row['publicuri'], + 'principaluri' => $row['principaluri'], + '{' . 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', + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ, + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC, + ]; + + foreach($this->propertyMap as $xmlName=>$dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + return $calendar; + + } /** * @param string $principal @@ -1473,6 +1605,50 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** + * @param boolean $value + * @param \OCA\DAV\CalDAV\Calendar $calendar + * @return string|null + */ + public function setPublishStatus($value, $calendar) { + $query = $this->db->getQueryBuilder(); + if ($value) { + $publicUri = $this->random->generate(16, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS); + $query->insert('dav_shares') + ->values([ + 'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()), + 'type' => $query->createNamedParameter('calendar'), + 'access' => $query->createNamedParameter(self::ACCESS_PUBLIC), + 'resourceid' => $query->createNamedParameter($calendar->getResourceId()), + 'publicuri' => $query->createNamedParameter($publicUri) + ]); + $query->execute(); + return $publicUri; + } + $query->delete('dav_shares') + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId()))) + ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC))); + $query->execute(); + return null; + } + + /** + * @param \OCA\DAV\CalDAV\Calendar $calendar + * @return mixed + */ + public function getPublishStatus($calendar) { + $query = $this->db->getQueryBuilder(); + $result = $query->select('publicuri') + ->from('dav_shares') + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId()))) + ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC))) + ->execute(); + + $row = $result->fetch(); + $result->closeCursor(); + return $row ? reset($row) : false; + } + + /** * @param int $resourceId * @param array $acl * @return array diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php index 821a71babb1..3fbcd87acc0 100644 --- a/apps/dav/lib/CalDAV/Calendar.php +++ b/apps/dav/lib/CalDAV/Calendar.php @@ -89,6 +89,13 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { return $this->calendarInfo['id']; } + /** + * @return string + */ + public function getPrincipalURI() { + return $this->calendarInfo['principaluri']; + } + function getACL() { $acl = [ [ @@ -117,6 +124,13 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { ]; } } + if ($this->isPublic()) { + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => 'principals/system/public', + 'protected' => true, + ]; + } /** @var CalDavBackend $calDavBackend */ $calDavBackend = $this->caldavBackend; @@ -236,6 +250,23 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { return $uris; } + /** + * @param boolean $value + * @return string|null + */ + function setPublishStatus($value) { + $publicUri = $this->caldavBackend->setPublishStatus($value, $this); + $this->calendarInfo['publicuri'] = $publicUri; + return $publicUri; + } + + /** + * @return mixed $value + */ + function getPublishStatus() { + return $this->caldavBackend->getPublishStatus($this); + } + private function canWrite() { if (isset($this->calendarInfo['{http://owncloud.org/ns}read-only'])) { return !$this->calendarInfo['{http://owncloud.org/ns}read-only']; @@ -243,8 +274,16 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IShareable { return true; } + private function isPublic() { + return isset($this->calendarInfo['{http://owncloud.org/ns}public']); + } + private function isShared() { return isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']); } + public function isSubscription() { + return isset($this->calendarInfo['{http://calendarserver.org/ns/}source']); + } + } diff --git a/apps/dav/lib/CalDAV/PublicCalendarRoot.php b/apps/dav/lib/CalDAV/PublicCalendarRoot.php new file mode 100644 index 00000000000..6d74b97f96e --- /dev/null +++ b/apps/dav/lib/CalDAV/PublicCalendarRoot.php @@ -0,0 +1,67 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\CalDAV; + +use Sabre\DAV\Collection; +use Sabre\DAV\Exception\NotFound; + +class PublicCalendarRoot extends Collection { + + /** @var CalDavBackend */ + protected $caldavBackend; + + /** @var \OCP\IL10N */ + protected $l10n; + + function __construct(CalDavBackend $caldavBackend) { + $this->caldavBackend = $caldavBackend; + $this->l10n = \OC::$server->getL10N('dav'); + } + + /** + * @inheritdoc + */ + function getName() { + return 'public-calendars'; + } + + /** + * @inheritdoc + */ + function getChild($name) { + $calendar = $this->caldavBackend->getPublicCalendar($name); + return new Calendar($this->caldavBackend, $calendar, $this->l10n); + } + + /** + * @inheritdoc + */ + function getChildren() { + $calendars = $this->caldavBackend->getPublicCalendars(); + $children = []; + foreach ($calendars as $calendar) { + // TODO: maybe implement a new class PublicCalendar ??? + $children[] = new Calendar($this->caldavBackend, $calendar, $this->l10n); + } + + return $children; + } +} diff --git a/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php new file mode 100644 index 00000000000..199bc67e615 --- /dev/null +++ b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php @@ -0,0 +1,227 @@ +<?php +/** + * @author Thomas Citharel <tcit@tcit.fr> + * + * @copyright Copyright (c) 2016 Thomas Citharel <tcit@tcit.fr> + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\CalDAV\Publishing; + +use Sabre\DAV\PropFind; +use Sabre\DAV\INode; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\DAV\Exception\NotFound; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Sabre\CalDAV\Xml\Property\AllowedSharingModes; +use OCA\DAV\CalDAV\Publishing\Xml\Publisher; +use OCA\DAV\CalDAV\Calendar; +use OCP\IURLGenerator; +use OCP\IConfig; + +class PublishPlugin extends ServerPlugin { + const NS_CALENDARSERVER = 'http://calendarserver.org/ns/'; + + /** + * Reference to SabreDAV server object. + * + * @var \Sabre\DAV\Server + */ + protected $server; + + /** + * Config instance to get instance secret. + * + * @var IConfig + */ + protected $config; + + /** + * URL Generator for absolute URLs. + * + * @var IURLGenerator + */ + protected $urlGenerator; + + /** + * PublishPlugin constructor. + * + * @param IConfig $config + * @param IURLGenerator $urlGenerator + */ + public function __construct(IConfig $config, IURLGenerator $urlGenerator) { + $this->config = $config; + $this->urlGenerator = $urlGenerator; + } + + /** + * 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 string[] + */ + public function getFeatures() { + // May have to be changed to be detected + return ['oc-calendar-publishing', 'calendarserver-sharing']; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() { + return 'oc-calendar-publishing'; + } + + /** + * 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 + */ + public function initialize(Server $server) { + $this->server = $server; + + $this->server->on('method:POST', [$this, 'httpPost']); + $this->server->on('propFind', [$this, 'propFind']); + } + + public function propFind(PropFind $propFind, INode $node) { + if ($node instanceof Calendar) { + $propFind->handle('{'.self::NS_CALENDARSERVER.'}publish-url', function () use ($node) { + if ($node->getPublishStatus()) { + // We return the publish-url only if the calendar is published. + $token = $node->getPublishStatus(); + $publishUrl = $this->urlGenerator->getAbsoluteURL($this->server->getBaseUri().'public-calendars/').$token; + + return new Publisher($publishUrl, true); + } + }); + + $propFind->handle('{'.self::NS_CALENDARSERVER.'}allowed-sharing-modes', function() use ($node) { + return new AllowedSharingModes(!$node->isSubscription(), !$node->isSubscription()); + }); + } + } + + /** + * We intercept this to handle POST requests on calendars. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * + * @return void|bool + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) { + $path = $request->getPath(); + + // Only handling xml + $contentType = $request->getHeader('Content-Type'); + if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) { + return; + } + + // Making sure the node exists + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (NotFound $e) { + return; + } + + $requestBody = $request->getBodyAsString(); + + // If this request handler could not deal with this POST request, it + // will return 'null' and other plugins get a chance to handle the + // request. + // + // However, we already requested the full body. This is a problem, + // because a body can only be read once. This is why we preemptively + // re-populated the request body with the existing data. + $request->setBody($requestBody); + + $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); + + switch ($documentType) { + + case '{'.self::NS_CALENDARSERVER.'}publish-calendar' : + + // We can only deal with IShareableCalendar objects + if (!$node instanceof Calendar) { + return; + } + $this->server->transactionType = 'post-publish-calendar'; + + // 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->setPublishStatus(true); + + // iCloud sends back the 202, so we will too. + $response->setStatus(202); + + // 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; + + case '{'.self::NS_CALENDARSERVER.'}unpublish-calendar' : + + // We can only deal with IShareableCalendar objects + if (!$node instanceof Calendar) { + return; + } + $this->server->transactionType = 'post-unpublish-calendar'; + + // 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->setPublishStatus(false); + + $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; + + } + } +} diff --git a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php new file mode 100644 index 00000000000..1fdba22e9e0 --- /dev/null +++ b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php @@ -0,0 +1,83 @@ +<?php +/** + * @author Thomas Citharel <tcit@tcit.fr> + * + * @copyright Copyright (c) 2016 Thomas Citharel <tcit@tcit.fr> + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\CalDAV\Publishing\Xml; + +use OCA\DAV\CalDAV\Publishing\PublishPlugin as Plugin; +use Sabre\Xml\Writer; +use Sabre\Xml\XmlSerializable; + +class Publisher implements XmlSerializable { + + /** + * @var string $publishUrl + */ + protected $publishUrl; + + /** + * @var boolean $isPublished + */ + protected $isPublished; + + /** + * @param string $publishUrl + * @param boolean $isPublished + */ + function __construct($publishUrl, $isPublished) { + $this->publishUrl = $publishUrl; + $this->isPublished = $isPublished; + } + + /** + * @return string + */ + function getValue() { + return $this->publishUrl; + } + + /** + * The xmlSerialize metod 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) { + if (!$this->isPublished) { + // for pre-publish-url + $writer->write($this->publishUrl); + } else { + // for publish-url + $writer->writeElement('{DAV:}href', $this->publishUrl); + } + } +} |