Skip to content

Commit 713d3d4

Browse files
authored
Merge pull request nextcloud#19196 from nextcloud/feature/17126/allow_apps_to_register_their_own_calendars
Allow apps to provide Calendars in user's calendarHome
2 parents f885cef + b46e5cb commit 713d3d4

File tree

9 files changed

+590
-3
lines changed

9 files changed

+590
-3
lines changed

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
'OCA\\DAV\\CalDAV\\CalendarObject' => $baseDir . '/../lib/CalDAV/CalendarObject.php',
4343
'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php',
4444
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
45+
'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => $baseDir . '/../lib/CalDAV/Integration/ExternalCalendar.php',
46+
'OCA\\DAV\\CalDAV\\Integration\\ICalendarProvider' => $baseDir . '/../lib/CalDAV/Integration/ICalendarProvider.php',
4547
'OCA\\DAV\\CalDAV\\InvitationResponse\\InvitationResponseServer' => $baseDir . '/../lib/CalDAV/InvitationResponse/InvitationResponseServer.php',
4648
'OCA\\DAV\\CalDAV\\Outbox' => $baseDir . '/../lib/CalDAV/Outbox.php',
4749
'OCA\\DAV\\CalDAV\\Plugin' => $baseDir . '/../lib/CalDAV/Plugin.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ class ComposerStaticInitDAV
5757
'OCA\\DAV\\CalDAV\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarObject.php',
5858
'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php',
5959
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
60+
'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ExternalCalendar.php',
61+
'OCA\\DAV\\CalDAV\\Integration\\ICalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ICalendarProvider.php',
6062
'OCA\\DAV\\CalDAV\\InvitationResponse\\InvitationResponseServer' => __DIR__ . '/..' . '/../lib/CalDAV/InvitationResponse/InvitationResponseServer.php',
6163
'OCA\\DAV\\CalDAV\\Outbox' => __DIR__ . '/..' . '/../lib/CalDAV/Outbox.php',
6264
'OCA\\DAV\\CalDAV\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Plugin.php',

apps/dav/lib/AppInfo/PluginManager.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
namespace OCA\DAV\AppInfo;
2626

2727
use OC\ServerContainer;
28+
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
2829
use OCP\App\IAppManager;
2930
use OCP\AppFramework\QueryException;
3031

@@ -58,6 +59,13 @@ class PluginManager {
5859
*/
5960
private $collections = null;
6061

62+
/**
63+
* Calendar plugins
64+
*
65+
* @var array
66+
*/
67+
private $calendarPlugins = null;
68+
6169
/**
6270
* Contstruct a PluginManager
6371
*
@@ -93,11 +101,24 @@ public function getAppCollections() {
93101
return $this->collections;
94102
}
95103

104+
/**
105+
* Returns an array of app-registered calendar plugins
106+
*
107+
* @return array
108+
*/
109+
public function getCalendarPlugins():array {
110+
if (null === $this->calendarPlugins) {
111+
$this->populate();
112+
}
113+
return $this->calendarPlugins;
114+
}
115+
96116
/**
97117
* Retrieve plugin and collection list and populate attributes
98118
*/
99119
private function populate() {
100120
$this->plugins = [];
121+
$this->calendarPlugins = [];
101122
$this->collections = [];
102123
foreach ($this->appManager->getInstalledApps() as $app) {
103124
// load plugins and collections from info.xml
@@ -107,6 +128,7 @@ private function populate() {
107128
}
108129
$this->loadSabrePluginsFromInfoXml($this->extractPluginList($info));
109130
$this->loadSabreCollectionsFromInfoXml($this->extractCollectionList($info));
131+
$this->loadSabreCalendarPluginsFromInfoXml($this->extractCalendarPluginList($info));
110132
}
111133
}
112134

@@ -140,6 +162,21 @@ private function extractCollectionList(array $array) {
140162
return [];
141163
}
142164

165+
private function extractCalendarPluginList(array $array):array {
166+
if (isset($array['sabre']) && is_array($array['sabre'])) {
167+
if (isset($array['sabre']['calendar-plugins']) && is_array($array['sabre']['calendar-plugins'])) {
168+
if (isset($array['sabre']['calendar-plugins']['plugin'])) {
169+
$items = $array['sabre']['calendar-plugins']['plugin'];
170+
if (!is_array($items)) {
171+
$items = [$items];
172+
}
173+
return $items;
174+
}
175+
}
176+
}
177+
return [];
178+
}
179+
143180
private function loadSabrePluginsFromInfoXml(array $plugins) {
144181
foreach ($plugins as $plugin) {
145182
try {
@@ -168,4 +205,24 @@ private function loadSabreCollectionsFromInfoXml(array $collections) {
168205
}
169206
}
170207

208+
private function loadSabreCalendarPluginsFromInfoXml(array $calendarPlugins):void {
209+
foreach ($calendarPlugins as $calendarPlugin) {
210+
try {
211+
$instantiatedCalendarPlugin = $this->container->query($calendarPlugin);
212+
} catch (QueryException $e) {
213+
if (class_exists($calendarPlugin)) {
214+
$instantiatedCalendarPlugin = new $calendarPlugin();
215+
} else {
216+
throw new \Exception("Sabre calendar-plugin class '$calendarPlugin' is unknown and could not be loaded");
217+
}
218+
}
219+
220+
if (!($instantiatedCalendarPlugin instanceof ICalendarProvider)) {
221+
throw new \Exception("Sabre calendar-plugin class '$calendarPlugin' does not implement ICalendarProvider interface");
222+
}
223+
224+
$this->calendarPlugins[] = $instantiatedCalendarPlugin;
225+
}
226+
}
227+
171228
}

apps/dav/lib/CalDAV/CalendarHome.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626

2727
namespace OCA\DAV\CalDAV;
2828

29+
use OCA\DAV\AppInfo\PluginManager;
30+
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
31+
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
2932
use Sabre\CalDAV\Backend\BackendInterface;
3033
use Sabre\CalDAV\Backend\NotificationSupport;
3134
use Sabre\CalDAV\Backend\SchedulingSupport;
@@ -44,13 +47,20 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
4447
/** @var \OCP\IConfig */
4548
private $config;
4649

50+
/** @var PluginManager */
51+
private $pluginManager;
52+
4753
/** @var bool */
4854
private $returnCachedSubscriptions=false;
4955

5056
public function __construct(BackendInterface $caldavBackend, $principalInfo) {
5157
parent::__construct($caldavBackend, $principalInfo);
5258
$this->l10n = \OC::$server->getL10N('dav');
5359
$this->config = \OC::$server->getConfig();
60+
$this->pluginManager = new PluginManager(
61+
\OC::$server,
62+
\OC::$server->getAppManager()
63+
);
5464
}
5565

5666
/**
@@ -66,7 +76,7 @@ public function getCalDAVBackend() {
6676
function createExtendedCollection($name, MkCol $mkCol) {
6777
$reservedNames = [BirthdayService::BIRTHDAY_CALENDAR_URI];
6878

69-
if (in_array($name, $reservedNames)) {
79+
if (\in_array($name, $reservedNames, true) || ExternalCalendar::doesViolateReservedName($name)) {
7080
throw new MethodNotAllowed('The resource you tried to create has a reserved name');
7181
}
7282

@@ -104,6 +114,14 @@ function getChildren() {
104114
}
105115
}
106116

117+
foreach ($this->pluginManager->getCalendarPlugins() as $calendarPlugin) {
118+
/** @var ICalendarProvider $calendarPlugin */
119+
$calendars = $calendarPlugin->fetchAllForCalendarHome($this->principalInfo['uri']);
120+
foreach ($calendars as $calendar) {
121+
$objects[] = $calendar;
122+
}
123+
}
124+
107125
return $objects;
108126
}
109127

@@ -139,7 +157,21 @@ function getChild($name) {
139157
return new Subscription($this->caldavBackend, $subscription);
140158
}
141159
}
160+
}
161+
162+
if (ExternalCalendar::isAppGeneratedCalendar($name)) {
163+
[$appId, $calendarUri] = ExternalCalendar::splitAppGeneratedCalendarUri($name);
142164

165+
foreach ($this->pluginManager->getCalendarPlugins() as $calendarPlugin) {
166+
/** @var ICalendarProvider $calendarPlugin */
167+
if ($calendarPlugin->getAppId() !== $appId) {
168+
continue;
169+
}
170+
171+
if ($calendarPlugin->hasCalendarInCalendarHome($this->principalInfo['uri'], $calendarUri)) {
172+
return $calendarPlugin->getCalendarInCalendarHome($this->principalInfo['uri'], $calendarUri);
173+
}
174+
}
143175
}
144176

145177
throw new NotFound('Node with name \'' . $name . '\' could not be found');
@@ -155,7 +187,7 @@ function calendarSearch(array $filters, $limit=null, $offset=null) {
155187
return $this->caldavBackend->calendarSearch($principalUri, $filters, $limit, $offset);
156188
}
157189

158-
190+
159191
public function enableCachedSubscriptionsForThisRequest() {
160192
$this->returnCachedSubscriptions = true;
161193
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
/**
3+
* @copyright 2020, Georg Ehrke <[email protected]>
4+
*
5+
* @author Georg Ehrke <[email protected]>
6+
*
7+
* @license GNU AGPL version 3 or any later version
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU Affero General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Affero General Public License
20+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
*
22+
*/
23+
namespace OCA\DAV\CalDAV\Integration;
24+
25+
use Sabre\CalDAV;
26+
use Sabre\DAV;
27+
28+
/**
29+
* Class ExternalCalendar
30+
*
31+
* @package OCA\DAV\CalDAV\Integration
32+
* @since 19.0.0
33+
*/
34+
abstract class ExternalCalendar implements CalDAV\ICalendar, DAV\IProperties {
35+
36+
/** @var string */
37+
private const PREFIX = 'app-generated';
38+
39+
/**
40+
* @var string
41+
*
42+
* Double dash is a valid delimiter,
43+
* because it will always split the calendarURIs correctly:
44+
* - our prefix contains only one dash and won't be split
45+
* - appIds are not allowed to contain dashes as per spec:
46+
* > must contain only lowercase ASCII characters and underscore
47+
* - explode has a limit of three, so even if the app-generated
48+
* calendar uri has double dashes, it won't be split
49+
*/
50+
private const DELIMITER = '--';
51+
52+
/** @var string */
53+
private $appId;
54+
55+
/** @var string */
56+
private $calendarUri;
57+
58+
/**
59+
* ExternalCalendar constructor.
60+
*
61+
* @param string $appId
62+
* @param string $calendarUri
63+
*/
64+
public function __construct(string $appId, string $calendarUri) {
65+
$this->appId = $appId;
66+
$this->calendarUri = $calendarUri;
67+
}
68+
69+
/**
70+
* @inheritDoc
71+
*/
72+
final public function getName() {
73+
return implode(self::DELIMITER, [
74+
self::PREFIX,
75+
$this->appId,
76+
$this->calendarUri,
77+
]);
78+
}
79+
80+
/**
81+
* @inheritDoc
82+
*/
83+
final public function setName($name) {
84+
throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported');
85+
}
86+
87+
/**
88+
* @inheritDoc
89+
*/
90+
final public function createDirectory($name) {
91+
throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed');
92+
93+
}
94+
95+
/**
96+
* Checks whether the calendar uri is app-generated
97+
*
98+
* @param string $calendarUri
99+
* @return bool
100+
*/
101+
public static function isAppGeneratedCalendar(string $calendarUri):bool {
102+
return strpos($calendarUri, self::PREFIX) === 0 && substr_count($calendarUri, self::DELIMITER) >= 2;
103+
}
104+
105+
/**
106+
* Splits an app-generated calendar-uri into appId and calendarUri
107+
*
108+
* @param string $calendarUri
109+
* @return array
110+
*/
111+
public static function splitAppGeneratedCalendarUri(string $calendarUri):array {
112+
$array = array_slice(explode(self::DELIMITER, $calendarUri, 3), 1);
113+
// Check the array has expected amount of elements
114+
// and none of them is an empty string
115+
if (\count($array) !== 2 || \in_array('', $array, true)) {
116+
throw new \InvalidArgumentException('Provided calendar uri was not app-generated');
117+
}
118+
119+
return $array;
120+
}
121+
122+
/**
123+
* Checks whether a calendar-name, the user wants to create, violates
124+
* the reserved name for calendar uris
125+
*
126+
* @param string $calendarUri
127+
* @return bool
128+
*/
129+
public static function doesViolateReservedName(string $calendarUri):bool {
130+
return strpos($calendarUri, self::PREFIX) === 0;
131+
}
132+
}

0 commit comments

Comments
 (0)