Skip to content

Commit e05f396

Browse files
redblommickenordinMahdiBaghbani
committed
feat(OCM-Invites): Implement exchanging cloud IDs through OCM invitation workflow
see https://github.com/cs3org/OCM-API/blob/v1.2.1/IETF-RFC.md Features: - Button to invite remote users to exchange cloudIDs. - Button to manually accept invite to exchange cloudIDs. - WAYF page allowing the receiver of the invite to open and accept the invitation. - Listing of open invitations. - Option to resend, revoke open invitations. Signed-off-by: Antoon P. <antoon.prins@surf.nl> Co-authored-by: Micke Nordin <kano@sunet.se> Co-authored-by: Mahdi Baghbani <mahdi-baghbani@azadehafzar.io>
1 parent c6eca19 commit e05f396

49 files changed

Lines changed: 6907 additions & 163 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

appinfo/info.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* 🙈 **We’re not reinventing the wheel!** Based on the great and open SabreDAV library.
1717
</description>
1818

19-
<version>8.6.0-dev.0</version>
19+
<version>8.5.0-dev.0</version>
2020
<licence>agpl</licence>
2121

2222
<author mail="hamza221@users.noreply.github.com">Hamza Mahjoubi</author>
@@ -49,8 +49,16 @@
4949

5050
<background-jobs>
5151
<job>OCA\Contacts\Cron\SocialUpdateRegistration</job>
52+
<job>OCA\Contacts\Cron\UpdateOcmProviders</job>
5253
</background-jobs>
5354

55+
<commands>
56+
<command>OCA\Contacts\Command\EnableOcmInvites</command>
57+
<command>OCA\Contacts\Command\DisableOcmInvites</command>
58+
<command>OCA\Contacts\Command\OcmInvitesConfig</command>
59+
<command>OCA\Contacts\Command\SetMeshProvidersService</command>
60+
</commands>
61+
5462
<settings>
5563
<admin>OCA\Contacts\Settings\AdminSettings</admin>
5664
</settings>
@@ -67,4 +75,5 @@
6775
<contactsmenu>
6876
<provider>OCA\Contacts\ContactsMenu\Providers\DetailsProvider</provider>
6977
</contactsmenu>
78+
7079
</info>

appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
'routes' => [
1010
['name' => 'contacts#direct', 'url' => '/direct/contact/{contact}', 'verb' => 'GET'],
1111
['name' => 'contacts#directcircle', 'url' => '/direct/circle/{singleId}', 'verb' => 'GET'],
12+
1213
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
1314
['name' => 'page#index', 'url' => '/{group}', 'verb' => 'GET', 'postfix' => 'group'],
1415
['name' => 'page#index', 'url' => '/{group}/{contact}', 'verb' => 'GET', 'postfix' => 'group.contact'],

lib/AppInfo/Application.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@
77
namespace OCA\Contacts\AppInfo;
88

99
use OCA\Contacts\Capabilities;
10+
use OCA\Contacts\ConfigLexicon;
1011
use OCA\Contacts\Dav\PatchPlugin;
1112
use OCA\Contacts\Event\LoadContactsOcaApiEvent;
13+
use OCA\Contacts\Listener\FederatedInviteAcceptedListener;
1214
use OCA\Contacts\Listener\LoadContactsFilesActions;
1315
use OCA\Contacts\Listener\LoadContactsOcaApi;
16+
use OCA\Contacts\Listener\OcmDiscoveryListener;
1417
use OCA\DAV\Events\SabrePluginAddEvent;
1518
use OCA\Files\Event\LoadAdditionalScriptsEvent;
1619
use OCP\AppFramework\App;
1720
use OCP\AppFramework\Bootstrap\IBootContext;
1821
use OCP\AppFramework\Bootstrap\IBootstrap;
1922
use OCP\AppFramework\Bootstrap\IRegistrationContext;
2023
use OCP\EventDispatcher\IEventDispatcher;
24+
use OCP\OCM\Events\LocalOCMDiscoveryEvent;
25+
use OCP\OCM\Events\OCMEndpointRequestEvent;
2126

2227
class Application extends App implements IBootstrap {
2328
public const APP_ID = 'contacts';
@@ -33,8 +38,12 @@ public function __construct() {
3338
#[\Override]
3439
public function register(IRegistrationContext $context): void {
3540
$context->registerCapability(Capabilities::class);
41+
$context->registerConfigLexicon(ConfigLexicon::class);
42+
3643
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadContactsFilesActions::class);
3744
$context->registerEventListener(LoadContactsOcaApiEvent::class, LoadContactsOcaApi::class);
45+
$context->registerEventListener(OCMEndpointRequestEvent::class, FederatedInviteAcceptedListener::class);
46+
$context->registerEventListener(LocalOCMDiscoveryEvent::class, OcmDiscoveryListener::class);
3847
}
3948

4049
#[\Override]

lib/Command/OcmInvitesConfig.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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\Contacts\Command;
11+
12+
use OCA\Contacts\AppInfo\Application;
13+
use OCA\Contacts\Service\FederatedInvitesService;
14+
use OCP\IAppConfig;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Helper\Table;
17+
use Symfony\Component\Console\Input\InputArgument;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
21+
/**
22+
* Read or write the per-deployment OCM invite admin toggles.
23+
*
24+
* Usage:
25+
* occ contacts:ocm-invites-config list all toggles
26+
* occ contacts:ocm-invites-config <option> print "1" or "0"
27+
* occ contacts:ocm-invites-config <option> on|off persist a new value
28+
*
29+
* The supported options match FederatedInvitesService::OCM_INVITES_BOOL_KEYS
30+
* so admins can drive the same flags as the settings UI, plus the occ-only
31+
* SSRF guard override, without needing one command per key.
32+
*/
33+
class OcmInvitesConfig extends Command {
34+
public function __construct(
35+
private IAppConfig $appConfig,
36+
) {
37+
parent::__construct();
38+
}
39+
40+
#[\Override]
41+
protected function configure(): void {
42+
$this
43+
->setName('contacts:ocm-invites-config')
44+
->setDescription('Read or write OCM invite admin toggles.')
45+
->addArgument(
46+
'option',
47+
InputArgument::OPTIONAL,
48+
'Toggle key (' . implode(', ', FederatedInvitesService::OCM_INVITES_BOOL_KEYS) . '). Omit to list all toggles.',
49+
)
50+
->addArgument(
51+
'value',
52+
InputArgument::OPTIONAL,
53+
'New value (on/off, true/false, 1/0, yes/no). Omit to read the current value.',
54+
);
55+
}
56+
57+
#[\Override]
58+
protected function execute(InputInterface $input, OutputInterface $output): int {
59+
$option = $input->getArgument('option');
60+
$value = $input->getArgument('value');
61+
62+
if ($option === null) {
63+
return $this->listAll($output);
64+
}
65+
66+
if (!in_array($option, FederatedInvitesService::OCM_INVITES_BOOL_KEYS, true)) {
67+
$output->writeln(sprintf(
68+
'<error>Unknown OCM invite toggle "%s". Allowed: %s.</error>',
69+
$option,
70+
implode(', ', FederatedInvitesService::OCM_INVITES_BOOL_KEYS),
71+
));
72+
return self::FAILURE;
73+
}
74+
75+
if ($value === null) {
76+
$current = $this->appConfig->getValueBool(Application::APP_ID, $option);
77+
$output->writeln($current ? '1' : '0');
78+
return self::SUCCESS;
79+
}
80+
81+
$parsed = $this->parseBool($value);
82+
if ($parsed === null) {
83+
$output->writeln(sprintf(
84+
'<error>Cannot parse "%s" as boolean. Use on/off, true/false, 1/0, or yes/no.</error>',
85+
$value,
86+
));
87+
return self::INVALID;
88+
}
89+
90+
$current = $this->appConfig->getValueBool(Application::APP_ID, $option);
91+
if ($current === $parsed) {
92+
$output->writeln(sprintf('%s already %s.', $option, $parsed ? 'on' : 'off'));
93+
return self::SUCCESS;
94+
}
95+
96+
$this->appConfig->setValueBool(Application::APP_ID, $option, $parsed);
97+
$output->writeln(sprintf('%s set to %s.', $option, $parsed ? 'on' : 'off'));
98+
return self::SUCCESS;
99+
}
100+
101+
private function listAll(OutputInterface $output): int {
102+
$table = new Table($output);
103+
$table->setHeaders(['option', 'value']);
104+
foreach (FederatedInvitesService::OCM_INVITES_BOOL_KEYS as $key) {
105+
$current = $this->appConfig->getValueBool(Application::APP_ID, $key);
106+
$table->addRow([$key, $current ? 'on' : 'off']);
107+
}
108+
$table->render();
109+
return self::SUCCESS;
110+
}
111+
112+
private function parseBool(string $raw): ?bool {
113+
$normalised = strtolower(trim($raw));
114+
if (in_array($normalised, ['true', '1', 'on', 'yes'], true)) {
115+
return true;
116+
}
117+
if (in_array($normalised, ['false', '0', 'off', 'no'], true)) {
118+
return false;
119+
}
120+
return null;
121+
}
122+
}

lib/ConfigLexicon.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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\Contacts;
11+
12+
use OCP\Config\Lexicon\Entry;
13+
use OCP\Config\Lexicon\ILexicon;
14+
use OCP\Config\Lexicon\Strictness;
15+
use OCP\Config\ValueType;
16+
use OCP\IAppConfig;
17+
18+
/**
19+
* Config Lexicon for contacts.
20+
*
21+
* Please add and manage your config keys in this file and keep the
22+
* Lexicon up to date.
23+
*
24+
* {@see ILexicon}
25+
*/
26+
class ConfigLexicon implements ILexicon {
27+
public const OCM_INVITES_ENABLED = 'ocm_invites_enabled';
28+
public const OCM_INVITES_OPTIONAL_MAIL = 'ocm_invites_optional_mail';
29+
public const OCM_INVITES_CC_SENDER = 'ocm_invites_cc_sender';
30+
public const OCM_INVITES_ENCODED_COPY_BUTTON = 'ocm_invites_encoded_copy_button';
31+
public const OCM_INVITES_DISABLE_SSRF_GUARD = 'ocm_invites_disable_ssrf_guard';
32+
public const MESH_PROVIDERS_SERVICE = 'mesh_providers_service';
33+
public const WAYF_ENDPOINT = 'wayf_endpoint';
34+
public const FEDERATIONS_CACHE = 'federations_cache';
35+
36+
public function getStrictness(): Strictness {
37+
return Strictness::NOTICE;
38+
}
39+
40+
public function getAppConfigs(): array {
41+
return [
42+
new Entry(
43+
self::OCM_INVITES_ENABLED,
44+
ValueType::BOOL,
45+
defaultRaw: false,
46+
definition: 'Whether OCM invites for contacts are enabled.',
47+
lazy: true,
48+
),
49+
new Entry(
50+
self::OCM_INVITES_OPTIONAL_MAIL,
51+
ValueType::BOOL,
52+
defaultRaw: false,
53+
definition: 'Whether the recipient email field is optional when creating an OCM invite.',
54+
lazy: true,
55+
),
56+
new Entry(
57+
self::OCM_INVITES_CC_SENDER,
58+
ValueType::BOOL,
59+
defaultRaw: true,
60+
definition: 'Whether the sender is CC-ed on the OCM invite email.',
61+
lazy: true,
62+
),
63+
new Entry(
64+
self::OCM_INVITES_ENCODED_COPY_BUTTON,
65+
ValueType::BOOL,
66+
defaultRaw: false,
67+
definition: 'Whether the invite email "Open invite" button uses the encoded WAYF URL instead of the raw token.',
68+
lazy: true,
69+
),
70+
new Entry(
71+
self::OCM_INVITES_DISABLE_SSRF_GUARD,
72+
ValueType::BOOL,
73+
defaultRaw: false,
74+
definition: 'Unsafe development override that disables private-host and localhost checks for OCM invite discovery.',
75+
lazy: true,
76+
),
77+
new Entry(
78+
self::MESH_PROVIDERS_SERVICE,
79+
ValueType::STRING,
80+
defaultRaw: '',
81+
definition: 'Space-separated list of mesh provider service URLs used for WAYF discovery.',
82+
lazy: true,
83+
),
84+
new Entry(
85+
self::WAYF_ENDPOINT,
86+
ValueType::STRING,
87+
defaultRaw: '',
88+
definition: 'Optional override for the base WAYF endpoint used in invite links.',
89+
note: 'If empty, the app route is used at runtime.',
90+
lazy: true,
91+
),
92+
new Entry(
93+
self::FEDERATIONS_CACHE,
94+
ValueType::ARRAY,
95+
defaultRaw: [],
96+
definition: 'Internal cron-maintained cache for discovered federations and expiry metadata.',
97+
flags: IAppConfig::FLAG_SENSITIVE,
98+
lazy: true,
99+
),
100+
];
101+
}
102+
103+
public function getUserConfigs(): array {
104+
return [];
105+
}
106+
}

0 commit comments

Comments
 (0)