Skip to content

Commit b092b3d

Browse files
committed
feat(federation): Add accept/decline mechanism for federated calendars
Signed-off-by: David Dreschner <david.dreschner@nextcloud.com>
1 parent b1b4b06 commit b092b3d

18 files changed

Lines changed: 774 additions & 19 deletions

apps/dav/appinfo/routes.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
],
1616
'ocs' => [
1717
['name' => 'direct#getUrl', 'url' => '/api/v1/direct', 'verb' => 'POST'],
18+
['name' => 'federated_calendar#getPending', 'url' => '/api/v1/federated_calendars/pending', 'verb' => 'GET'],
19+
['name' => 'federated_calendar#accept', 'url' => '/api/v1/federated_calendars/pending/{id}', 'verb' => 'POST'],
20+
['name' => 'federated_calendar#decline', 'url' => '/api/v1/federated_calendars/pending/{id}', 'verb' => 'DELETE'],
1821
['name' => 'upcoming_events#getEvents', 'url' => '/api/v1/events/upcoming', 'verb' => 'GET'],
1922
['name' => 'out_of_office#getCurrentOutOfOfficeData', 'url' => '/api/v1/outOfOffice/{userId}/now', 'verb' => 'GET'],
2023
['name' => 'out_of_office#getOutOfOffice', 'url' => '/api/v1/outOfOffice/{userId}', 'verb' => 'GET'],

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@
7676
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarEntity' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarEntity.php',
7777
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarFactory' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarFactory.php',
7878
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarImpl' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarImpl.php',
79+
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarInvitationService' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarInvitationService.php',
80+
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarInviteNotifier' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarInviteNotifier.php',
7981
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarMapper' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarMapper.php',
8082
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarObject' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarObject.php',
8183
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarSyncService' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php',
@@ -262,6 +264,7 @@
262264
'OCA\\DAV\\Controller\\BirthdayCalendarController' => $baseDir . '/../lib/Controller/BirthdayCalendarController.php',
263265
'OCA\\DAV\\Controller\\DirectController' => $baseDir . '/../lib/Controller/DirectController.php',
264266
'OCA\\DAV\\Controller\\ExampleContentController' => $baseDir . '/../lib/Controller/ExampleContentController.php',
267+
'OCA\\DAV\\Controller\\FederatedCalendarController' => $baseDir . '/../lib/Controller/FederatedCalendarController.php',
265268
'OCA\\DAV\\Controller\\InvitationResponseController' => $baseDir . '/../lib/Controller/InvitationResponseController.php',
266269
'OCA\\DAV\\Controller\\OutOfOfficeController' => $baseDir . '/../lib/Controller/OutOfOfficeController.php',
267270
'OCA\\DAV\\Controller\\UpcomingEventsController' => $baseDir . '/../lib/Controller/UpcomingEventsController.php',
@@ -393,6 +396,7 @@
393396
'OCA\\DAV\\Migration\\Version1034Date20250605132605' => $baseDir . '/../lib/Migration/Version1034Date20250605132605.php',
394397
'OCA\\DAV\\Migration\\Version1034Date20250813093701' => $baseDir . '/../lib/Migration/Version1034Date20250813093701.php',
395398
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => $baseDir . '/../lib/Migration/Version1036Date20251202000000.php',
399+
'OCA\\DAV\\Migration\\Version1040Date20260805000000' => $baseDir . '/../lib/Migration/Version1040Date20260805000000.php',
396400
'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',
397401
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php',
398402
'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ class ComposerStaticInitDAV
9191
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarEntity' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarEntity.php',
9292
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarFactory' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarFactory.php',
9393
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarImpl' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarImpl.php',
94+
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarInvitationService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarInvitationService.php',
95+
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarInviteNotifier' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarInviteNotifier.php',
9496
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarMapper' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarMapper.php',
9597
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarObject.php',
9698
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarSyncService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php',
@@ -277,6 +279,7 @@ class ComposerStaticInitDAV
277279
'OCA\\DAV\\Controller\\BirthdayCalendarController' => __DIR__ . '/..' . '/../lib/Controller/BirthdayCalendarController.php',
278280
'OCA\\DAV\\Controller\\DirectController' => __DIR__ . '/..' . '/../lib/Controller/DirectController.php',
279281
'OCA\\DAV\\Controller\\ExampleContentController' => __DIR__ . '/..' . '/../lib/Controller/ExampleContentController.php',
282+
'OCA\\DAV\\Controller\\FederatedCalendarController' => __DIR__ . '/..' . '/../lib/Controller/FederatedCalendarController.php',
280283
'OCA\\DAV\\Controller\\InvitationResponseController' => __DIR__ . '/..' . '/../lib/Controller/InvitationResponseController.php',
281284
'OCA\\DAV\\Controller\\OutOfOfficeController' => __DIR__ . '/..' . '/../lib/Controller/OutOfOfficeController.php',
282285
'OCA\\DAV\\Controller\\UpcomingEventsController' => __DIR__ . '/..' . '/../lib/Controller/UpcomingEventsController.php',
@@ -408,6 +411,7 @@ class ComposerStaticInitDAV
408411
'OCA\\DAV\\Migration\\Version1034Date20250605132605' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250605132605.php',
409412
'OCA\\DAV\\Migration\\Version1034Date20250813093701' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250813093701.php',
410413
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => __DIR__ . '/..' . '/../lib/Migration/Version1036Date20251202000000.php',
414+
'OCA\\DAV\\Migration\\Version1040Date20260805000000' => __DIR__ . '/..' . '/../lib/Migration/Version1040Date20260805000000.php',
411415
'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',
412416
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php',
413417
'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php',

apps/dav/lib/AppInfo/Application.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use OCA\DAV\CalDAV\CalendarManager;
1515
use OCA\DAV\CalDAV\CalendarProvider;
1616
use OCA\DAV\CalDAV\Federation\CalendarFederationProvider;
17+
use OCA\DAV\CalDAV\Federation\FederatedCalendarInviteNotifier;
1718
use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider;
1819
use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider;
1920
use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider;
@@ -216,6 +217,7 @@ public function register(IRegistrationContext $context): void {
216217

217218
$context->registerNotifierService(NotifierCalDAV::class);
218219
$context->registerNotifierService(NotifierCardDAV::class);
220+
$context->registerNotifierService(FederatedCalendarInviteNotifier::class);
219221

220222
$context->registerCalendarProvider(CalendarProvider::class);
221223
$context->registerCalendarProvider(CachedSubscriptionProvider::class);

apps/dav/lib/BackgroundJob/FederatedCalendarSyncJob.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace OCA\DAV\BackgroundJob;
1111

1212
use OCA\DAV\CalDAV\Federation\CalendarFederationConfig;
13+
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity;
1314
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
1415
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
1516
use OCP\AppFramework\Db\DoesNotExistException;
@@ -50,6 +51,11 @@ protected function run($argument): void {
5051
return;
5152
}
5253

54+
// Never fetch data for invitations the sharee did not accept (yet)
55+
if ($calendar->getState() !== FederatedCalendarEntity::STATE_ACCEPTED) {
56+
return;
57+
}
58+
5359
try {
5460
$this->syncService->syncOne($calendar);
5561
} catch (ClientExceptionInterface $e) {

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4053,7 +4053,10 @@ public function unshare(IShareable $shareable, string $principal): void {
40534053
* @return array<string, mixed>[]
40544054
*/
40554055
public function getFederatedCalendarsForUser(string $principalUri): array {
4056-
$federatedCalendars = $this->federatedCalendarMapper->findByPrincipalUri($principalUri);
4056+
$federatedCalendars = $this->federatedCalendarMapper->findByPrincipalUri(
4057+
$principalUri,
4058+
FederatedCalendarEntity::STATE_ACCEPTED,
4059+
);
40574060
return array_map(
40584061
static fn (FederatedCalendarEntity $entity) => $entity->toCalendarInfo(),
40594062
$federatedCalendars,
@@ -4062,6 +4065,10 @@ public function getFederatedCalendarsForUser(string $principalUri): array {
40624065

40634066
public function getFederatedCalendarByUri(string $principalUri, string $uri): ?array {
40644067
$federatedCalendar = $this->federatedCalendarMapper->findByUri($principalUri, $uri);
4065-
return $federatedCalendar?->toCalendarInfo();
4068+
if ($federatedCalendar === null
4069+
|| $federatedCalendar->getState() !== FederatedCalendarEntity::STATE_ACCEPTED) {
4070+
return null;
4071+
}
4072+
return $federatedCalendar->toCalendarInfo();
40664073
}
40674074
}

apps/dav/lib/CalDAV/Federation/CalendarFederationConfig.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,11 @@ public function isIncomingServer2serverShareEnabled(): bool {
4141
}
4242
return $this->appConfig->getValueBool('files_sharing', 'incoming_server2server_share_enabled', true);
4343
}
44+
45+
/**
46+
* Check if incoming shares from trusted servers are accepted automatically
47+
*/
48+
public function isTrustedShareAutoAcceptEnabled(): bool {
49+
return $this->appConfig->getValueBool('files_sharing', 'federatedTrustedShareAutoAccept', true);
50+
}
4451
}

apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function __construct(
3636
private readonly CalendarFederationConfig $calendarFederationConfig,
3737
private readonly IJobList $jobList,
3838
private readonly ICloudIdManager $cloudIdManager,
39+
private readonly FederatedCalendarInvitationService $invitationService,
3940
) {
4041
}
4142

@@ -131,8 +132,8 @@ public function shareReceived(ICloudFederationShare $share): string {
131132

132133
$sharedWithPrincipal = 'principals/users/' . $share->getShareWith();
133134

134-
// Delete existing incoming federated share first
135135
$calendar = $this->federatedCalendarMapper->findByUri($sharedWithPrincipal, $calendarUri);
136+
$isNew = $calendar === null;
136137

137138
if ($calendar === null) {
138139
$calendar = new FederatedCalendarEntity();
@@ -146,17 +147,34 @@ public function shareReceived(ICloudFederationShare $share): string {
146147
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
147148
$calendar->setPermissions($permissions);
148149
$calendar->setComponents($components);
150+
$calendar->setState(FederatedCalendarEntity::STATE_PENDING);
149151
$calendar = $this->federatedCalendarMapper->insert($calendar);
150152
} else {
151153
$calendar->setToken($share->getShareSecret());
152154
$calendar->setPermissions($permissions);
153155
$calendar->setComponents($components);
156+
if ($calendar->getState() === FederatedCalendarEntity::STATE_PENDING) {
157+
// The open invitation shows the sharer's metadata, keep it
158+
// fresh. Accepted calendars are not touched as the sharee owns
159+
// the display name and color from that point on.
160+
$calendar->setDisplayName($displayName);
161+
$calendar->setColor($color);
162+
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
163+
}
154164
$this->federatedCalendarMapper->update($calendar);
155165
}
156166

157-
$this->jobList->add(FederatedCalendarSyncJob::class, [
158-
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
159-
]);
167+
if ($calendar->getState() === FederatedCalendarEntity::STATE_ACCEPTED) {
168+
// Re-share of an already accepted calendar: just refresh the data
169+
$this->jobList->add(FederatedCalendarSyncJob::class, [
170+
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
171+
]);
172+
} elseif ($this->invitationService->shouldAutoAccept($share->getOwner(), $calendar->getRemoteUrl())) {
173+
$this->invitationService->accept($calendar);
174+
} elseif ($isNew) {
175+
// A re-shared pending calendar keeps its original invitation
176+
$this->invitationService->notifyAboutNewShare($calendar);
177+
}
160178

161179
return (string)$calendar->getId();
162180
}
@@ -212,6 +230,7 @@ private function handleSyncCalendarNotification(array $notification): array {
212230
$calendarUrl,
213231
'principals/users/' . $shareWith->getUser(),
214232
$sharedSecret,
233+
FederatedCalendarEntity::STATE_ACCEPTED,
215234
);
216235
if (empty($calendars)) {
217236
throw new ShareNotFound('Calendar is not shared with the sharee');

apps/dav/lib/CalDAV/Federation/FederatedCalendarEntity.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@
3939
* @method void setSharedByDisplayName(string $sharedByDisplayName)
4040
* @method string getComponents()
4141
* @method void setComponents(string $components)
42+
* @method int getState()
43+
* @method void setState(int $state)
4244
*/
4345
class FederatedCalendarEntity extends Entity {
46+
public const STATE_PENDING = 0;
47+
public const STATE_ACCEPTED = 1;
48+
4449
protected string $principaluri = '';
4550
protected string $uri = '';
4651
protected string $displayName = '';
@@ -53,6 +58,7 @@ class FederatedCalendarEntity extends Entity {
5358
protected string $sharedBy = '';
5459
protected string $sharedByDisplayName = '';
5560
protected string $components = '';
61+
protected int $state = self::STATE_PENDING;
5662

5763
public function __construct() {
5864
$this->addType('principaluri', Types::STRING);
@@ -67,6 +73,7 @@ public function __construct() {
6773
$this->addType('sharedBy', Types::STRING);
6874
$this->addType('sharedByDisplayName', Types::STRING);
6975
$this->addType('components', Types::STRING);
76+
$this->addType('state', Types::INTEGER);
7077
}
7178

7279
public function getSyncTokenForSabre(): string {
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\DAV\CalDAV\Federation;
11+
12+
use OCA\DAV\AppInfo\Application;
13+
use OCA\DAV\BackgroundJob\FederatedCalendarSyncJob;
14+
use OCA\Federation\TrustedServers;
15+
use OCP\App\IAppManager;
16+
use OCP\BackgroundJob\IJobList;
17+
use OCP\Federation\ICloudIdManager;
18+
use OCP\IURLGenerator;
19+
use OCP\Notification\IManager as INotificationManager;
20+
use OCP\Server;
21+
use Psr\Log\LoggerInterface;
22+
23+
class FederatedCalendarInvitationService {
24+
public const NOTIFICATION_OBJECT_TYPE = 'federated_calendar_share';
25+
public const NOTIFICATION_SUBJECT_NEW_SHARE = 'federated_calendar_share';
26+
27+
private const PRINCIPAL_PREFIX = 'principals/users/';
28+
29+
public function __construct(
30+
private readonly FederatedCalendarMapper $mapper,
31+
private readonly IJobList $jobList,
32+
private readonly INotificationManager $notificationManager,
33+
private readonly IURLGenerator $url,
34+
private readonly ICloudIdManager $cloudIdManager,
35+
private readonly CalendarFederationConfig $config,
36+
private readonly IAppManager $appManager,
37+
private readonly LoggerInterface $logger,
38+
) {
39+
}
40+
41+
public function accept(FederatedCalendarEntity $calendar): void {
42+
$calendar->setState(FederatedCalendarEntity::STATE_ACCEPTED);
43+
$this->mapper->update($calendar);
44+
45+
$this->jobList->add(FederatedCalendarSyncJob::class, [
46+
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
47+
]);
48+
49+
$this->dismissNotification($calendar);
50+
}
51+
52+
public function decline(FederatedCalendarEntity $calendar): void {
53+
// Dismiss first: a leftover bell notification would offer actions for
54+
// a share that no longer exists
55+
$this->dismissNotification($calendar);
56+
$this->mapper->deleteById($calendar->getId());
57+
}
58+
59+
public function notifyAboutNewShare(FederatedCalendarEntity $calendar): void {
60+
$notification = $this->notificationManager->createNotification();
61+
$notification->setApp(Application::APP_ID)
62+
->setUser($this->getUserId($calendar))
63+
->setDateTime(new \DateTime())
64+
->setObject(self::NOTIFICATION_OBJECT_TYPE, (string)$calendar->getId())
65+
->setSubject(self::NOTIFICATION_SUBJECT_NEW_SHARE, [
66+
'sharedBy' => $calendar->getSharedBy(),
67+
'sharedByDisplayName' => $calendar->getSharedByDisplayName(),
68+
'calendarName' => $calendar->getDisplayName(),
69+
]);
70+
71+
$endpointUrl = $this->url->linkToOCSRouteAbsolute('dav.federated_calendar.accept', [
72+
'id' => $calendar->getId(),
73+
]);
74+
75+
$declineAction = $notification->createAction();
76+
$declineAction->setLabel('decline')
77+
->setLink($endpointUrl, 'DELETE');
78+
$notification->addAction($declineAction);
79+
80+
$acceptAction = $notification->createAction();
81+
$acceptAction->setLabel('accept')
82+
->setLink($endpointUrl, 'POST');
83+
$notification->addAction($acceptAction);
84+
85+
$this->notificationManager->notify($notification);
86+
}
87+
88+
public function dismissNotification(FederatedCalendarEntity $calendar): void {
89+
$notification = $this->notificationManager->createNotification();
90+
$notification->setApp(Application::APP_ID)
91+
->setUser($this->getUserId($calendar))
92+
->setObject(self::NOTIFICATION_OBJECT_TYPE, (string)$calendar->getId());
93+
$this->notificationManager->markProcessed($notification);
94+
}
95+
96+
public function shouldAutoAccept(?string $ownerCloudId, string $remoteUrl): bool {
97+
if ($ownerCloudId === null || $ownerCloudId === '') {
98+
return false;
99+
}
100+
101+
if (!$this->config->isTrustedShareAutoAcceptEnabled()) {
102+
return false;
103+
}
104+
105+
$trustedServers = $this->getTrustedServers();
106+
if ($trustedServers === null) {
107+
return false;
108+
}
109+
110+
try {
111+
$remote = $this->cloudIdManager->resolveCloudId($ownerCloudId)->getRemote();
112+
} catch (\InvalidArgumentException) {
113+
return false;
114+
}
115+
116+
if (!$trustedServers->isTrustedServer($remote)) {
117+
return false;
118+
}
119+
120+
return $this->isSameHost($remote, $remoteUrl);
121+
}
122+
123+
private function isSameHost(string $remote, string $remoteUrl): bool {
124+
$remoteParts = parse_url(str_contains($remote, '://') ? $remote : '//' . $remote);
125+
$urlParts = parse_url($remoteUrl);
126+
if (!is_array($remoteParts) || !is_array($urlParts)
127+
|| !isset($remoteParts['host'], $urlParts['host'])) {
128+
return false;
129+
}
130+
131+
return strcasecmp($remoteParts['host'], $urlParts['host']) === 0
132+
&& ($remoteParts['port'] ?? null) === ($urlParts['port'] ?? null);
133+
}
134+
135+
protected function getTrustedServers(): ?TrustedServers {
136+
if (!$this->appManager->isEnabledForAnyone('federation')
137+
|| !class_exists(TrustedServers::class)) {
138+
return null;
139+
}
140+
141+
try {
142+
return Server::get(TrustedServers::class);
143+
} catch (\Throwable $e) {
144+
$this->logger->debug('Failed to create TrustedServers', ['exception' => $e]);
145+
return null;
146+
}
147+
}
148+
149+
private function getUserId(FederatedCalendarEntity $calendar): string {
150+
return substr($calendar->getPrincipaluri(), strlen(self::PRINCIPAL_PREFIX));
151+
}
152+
}

0 commit comments

Comments
 (0)