Skip to content

Commit 424ee48

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

21 files changed

Lines changed: 1641 additions & 19 deletions

apps/dav/appinfo/routes.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
],
1818
'ocs' => [
1919
['name' => 'direct#getUrl', 'url' => '/api/v1/direct', 'verb' => 'POST'],
20+
['name' => 'federated_calendar#getPending', 'url' => '/api/v1/federated_calendars/pending', 'verb' => 'GET'],
21+
['name' => 'federated_calendar#accept', 'url' => '/api/v1/federated_calendars/pending/{id}', 'verb' => 'POST'],
22+
['name' => 'federated_calendar#decline', 'url' => '/api/v1/federated_calendars/pending/{id}', 'verb' => 'DELETE'],
2023
['name' => 'upcoming_events#getEvents', 'url' => '/api/v1/events/upcoming', 'verb' => 'GET'],
2124
['name' => 'out_of_office#getCurrentOutOfOfficeData', 'url' => '/api/v1/outOfOffice/{userId}/now', 'verb' => 'GET'],
2225
['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',
@@ -263,6 +265,7 @@
263265
'OCA\\DAV\\Controller\\BirthdayCalendarController' => $baseDir . '/../lib/Controller/BirthdayCalendarController.php',
264266
'OCA\\DAV\\Controller\\DirectController' => $baseDir . '/../lib/Controller/DirectController.php',
265267
'OCA\\DAV\\Controller\\ExampleContentController' => $baseDir . '/../lib/Controller/ExampleContentController.php',
268+
'OCA\\DAV\\Controller\\FederatedCalendarController' => $baseDir . '/../lib/Controller/FederatedCalendarController.php',
266269
'OCA\\DAV\\Controller\\InvitationResponseController' => $baseDir . '/../lib/Controller/InvitationResponseController.php',
267270
'OCA\\DAV\\Controller\\OutOfOfficeController' => $baseDir . '/../lib/Controller/OutOfOfficeController.php',
268271
'OCA\\DAV\\Controller\\UpcomingEventsController' => $baseDir . '/../lib/Controller/UpcomingEventsController.php',
@@ -397,6 +400,7 @@
397400
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => $baseDir . '/../lib/Migration/Version1036Date20251202000000.php',
398401
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => $baseDir . '/../lib/Migration/Version1038Date20260302000000.php',
399402
'OCA\\DAV\\Migration\\Version1039Date20260408000000' => $baseDir . '/../lib/Migration/Version1039Date20260408000000.php',
403+
'OCA\\DAV\\Migration\\Version1040Date20260805000000' => $baseDir . '/../lib/Migration/Version1040Date20260805000000.php',
400404
'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',
401405
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php',
402406
'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',
@@ -278,6 +280,7 @@ class ComposerStaticInitDAV
278280
'OCA\\DAV\\Controller\\BirthdayCalendarController' => __DIR__ . '/..' . '/../lib/Controller/BirthdayCalendarController.php',
279281
'OCA\\DAV\\Controller\\DirectController' => __DIR__ . '/..' . '/../lib/Controller/DirectController.php',
280282
'OCA\\DAV\\Controller\\ExampleContentController' => __DIR__ . '/..' . '/../lib/Controller/ExampleContentController.php',
283+
'OCA\\DAV\\Controller\\FederatedCalendarController' => __DIR__ . '/..' . '/../lib/Controller/FederatedCalendarController.php',
281284
'OCA\\DAV\\Controller\\InvitationResponseController' => __DIR__ . '/..' . '/../lib/Controller/InvitationResponseController.php',
282285
'OCA\\DAV\\Controller\\OutOfOfficeController' => __DIR__ . '/..' . '/../lib/Controller/OutOfOfficeController.php',
283286
'OCA\\DAV\\Controller\\UpcomingEventsController' => __DIR__ . '/..' . '/../lib/Controller/UpcomingEventsController.php',
@@ -412,6 +415,7 @@ class ComposerStaticInitDAV
412415
'OCA\\DAV\\Migration\\Version1036Date20251202000000' => __DIR__ . '/..' . '/../lib/Migration/Version1036Date20251202000000.php',
413416
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260302000000.php',
414417
'OCA\\DAV\\Migration\\Version1039Date20260408000000' => __DIR__ . '/..' . '/../lib/Migration/Version1039Date20260408000000.php',
418+
'OCA\\DAV\\Migration\\Version1040Date20260805000000' => __DIR__ . '/..' . '/../lib/Migration/Version1040Date20260805000000.php',
415419
'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',
416420
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php',
417421
'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
@@ -15,6 +15,7 @@
1515
use OCA\DAV\CalDAV\CalendarManager;
1616
use OCA\DAV\CalDAV\CalendarProvider;
1717
use OCA\DAV\CalDAV\Federation\CalendarFederationProvider;
18+
use OCA\DAV\CalDAV\Federation\FederatedCalendarInviteNotifier;
1819
use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider;
1920
use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider;
2021
use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider;
@@ -226,6 +227,7 @@ public function register(IRegistrationContext $context): void {
226227

227228
$context->registerNotifierService(NotifierCalDAV::class);
228229
$context->registerNotifierService(NotifierCardDAV::class);
230+
$context->registerNotifierService(FederatedCalendarInviteNotifier::class);
229231

230232
$context->registerCalendarProvider(CalendarProvider::class);
231233
$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;
@@ -51,6 +52,11 @@ protected function run($argument): void {
5152
return;
5253
}
5354

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

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4083,7 +4083,10 @@ public function unshare(IShareable $shareable, string $principal): void {
40834083
* @return array<string, mixed>[]
40844084
*/
40854085
public function getFederatedCalendarsForUser(string $principalUri): array {
4086-
$federatedCalendars = $this->federatedCalendarMapper->findByPrincipalUri($principalUri);
4086+
$federatedCalendars = $this->federatedCalendarMapper->findByPrincipalUri(
4087+
$principalUri,
4088+
FederatedCalendarEntity::STATE_ACCEPTED,
4089+
);
40874090
return array_map(
40884091
static fn (FederatedCalendarEntity $entity) => $entity->toCalendarInfo(),
40894092
$federatedCalendars,
@@ -4092,6 +4095,10 @@ public function getFederatedCalendarsForUser(string $principalUri): array {
40924095

40934096
public function getFederatedCalendarByUri(string $principalUri, string $uri): ?array {
40944097
$federatedCalendar = $this->federatedCalendarMapper->findByUri($principalUri, $uri);
4095-
return $federatedCalendar?->toCalendarInfo();
4098+
if ($federatedCalendar === null
4099+
|| $federatedCalendar->getState() !== FederatedCalendarEntity::STATE_ACCEPTED) {
4100+
return null;
4101+
}
4102+
return $federatedCalendar->toCalendarInfo();
40964103
}
40974104
}

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

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

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

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

@@ -121,8 +122,8 @@ public function shareReceived(ICloudFederationShare $share): string {
121122

122123
$sharedWithPrincipal = 'principals/users/' . $share->getShareWith();
123124

124-
// Delete existing incoming federated share first
125125
$calendar = $this->federatedCalendarMapper->findByUri($sharedWithPrincipal, $calendarUri);
126+
$isNew = $calendar === null;
126127

127128
if ($calendar === null) {
128129
$calendar = new FederatedCalendarEntity();
@@ -136,17 +137,34 @@ public function shareReceived(ICloudFederationShare $share): string {
136137
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
137138
$calendar->setPermissions($permissions);
138139
$calendar->setComponents($components);
140+
$calendar->setState(FederatedCalendarEntity::STATE_PENDING);
139141
$calendar = $this->federatedCalendarMapper->insert($calendar);
140142
} else {
141143
$calendar->setToken($share->getShareSecret());
142144
$calendar->setPermissions($permissions);
143145
$calendar->setComponents($components);
146+
if ($calendar->getState() === FederatedCalendarEntity::STATE_PENDING) {
147+
// The open invitation shows the sharer's metadata, keep it
148+
// fresh. Accepted calendars are not touched as the sharee owns
149+
// the display name and color from that point on.
150+
$calendar->setDisplayName($displayName);
151+
$calendar->setColor($color);
152+
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
153+
}
144154
$this->federatedCalendarMapper->update($calendar);
145155
}
146156

147-
$this->jobList->add(FederatedCalendarSyncJob::class, [
148-
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
149-
]);
157+
if ($calendar->getState() === FederatedCalendarEntity::STATE_ACCEPTED) {
158+
// Re-share of an already accepted calendar: just refresh the data
159+
$this->jobList->add(FederatedCalendarSyncJob::class, [
160+
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
161+
]);
162+
} elseif ($this->invitationService->shouldAutoAccept($share->getOwner(), $calendar->getRemoteUrl())) {
163+
$this->invitationService->accept($calendar);
164+
} elseif ($isNew) {
165+
// A re-shared pending calendar keeps its original invitation
166+
$this->invitationService->notifyAboutNewShare($calendar);
167+
}
150168

151169
return (string)$calendar->getId();
152170
}
@@ -204,6 +222,7 @@ private function handleSyncCalendarNotification(array $notification): array {
204222
$calendarUrl,
205223
'principals/users/' . $shareWith->getUser(),
206224
$sharedSecret,
225+
FederatedCalendarEntity::STATE_ACCEPTED,
207226
);
208227
if (empty($calendars)) {
209228
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
@@ -40,8 +40,13 @@
4040
* @method void setSharedByDisplayName(string $sharedByDisplayName)
4141
* @method string getComponents()
4242
* @method void setComponents(string $components)
43+
* @method int getState()
44+
* @method void setState(int $state)
4345
*/
4446
class FederatedCalendarEntity extends Entity {
47+
public const STATE_PENDING = 0;
48+
public const STATE_ACCEPTED = 1;
49+
4550
protected string $principaluri = '';
4651
protected string $uri = '';
4752
protected string $displayName = '';
@@ -54,6 +59,7 @@ class FederatedCalendarEntity extends Entity {
5459
protected string $sharedBy = '';
5560
protected string $sharedByDisplayName = '';
5661
protected string $components = '';
62+
protected int $state = self::STATE_PENDING;
5763

5864
public function __construct() {
5965
$this->addType('principaluri', Types::STRING);
@@ -68,6 +74,7 @@ public function __construct() {
6874
$this->addType('sharedBy', Types::STRING);
6975
$this->addType('sharedByDisplayName', Types::STRING);
7076
$this->addType('components', Types::STRING);
77+
$this->addType('state', Types::INTEGER);
7178
}
7279

7380
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)