Skip to content

Commit 632955b

Browse files
committed
Merge branch 'mickenordin-kano-ocm-wayf' into invite-for-cloudid-exchange
2 parents b673d70 + 3039520 commit 632955b

26 files changed

+555
-258
lines changed

appinfo/routes.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
['name' => 'federated_invites#invite_accepted', 'url' => '/ocm/invitations/{token}/accept', 'verb' => 'PATCH'],
2020
['name' => 'federated_invites#invite_accept_dialog', 'url' => FederatedInvitesService::OCM_INVITE_ACCEPT_DIALOG_ROUTE, 'verb' => 'GET'],
2121
['name' => 'federated_invites#wayf', 'url' => IWayfProvider::WAYF_ROUTE, 'verb' => 'GET'],
22+
['name' => 'federated_invites#discover', 'url' => IWayfProvider::DISCOVERY_ROUTE, 'verb' => 'GET'],
2223

2324
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
2425
['name' => 'page#index', 'url' => '/{group}', 'verb' => 'GET', 'postfix' => 'group'],

composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@
3333
]
3434
},
3535
"require": {
36-
"php": ">=8.1 <=8.4"
36+
"php": ">=8.1 <=8.4",
37+
"bamarni/composer-bin-plugin": "^1.8"
3738
},
3839
"require-dev": {
3940
"christophwurst/nextcloud_testing": "^1.0.1",
4041
"phpunit/phpunit": "^9",
41-
"nextcloud/coding-standard": "^1.4",
42-
"bamarni/composer-bin-plugin": "^1.8"
42+
"nextcloud/coding-standard": "^1.4"
4343
},
4444
"extra": {
4545
"bamarni-bin": {

composer.lock

Lines changed: 111 additions & 66 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/AppInfo/Application.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
*/
77
namespace OCA\Contacts\AppInfo;
88

9-
use OCA\Contacts\Capabilities;
109
use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent;
10+
use OCA\Contacts\Capabilities;
1111
use OCA\Contacts\Dav\PatchPlugin;
1212
use OCA\Contacts\Event\LoadContactsOcaApiEvent;
1313
use OCA\Contacts\IWayfProvider;
@@ -37,8 +37,8 @@ public function __construct() {
3737
#[\Override]
3838
public function register(IRegistrationContext $context): void {
3939
$context->registerCapability(Capabilities::class);
40-
$context->registerServiceAlias(IWayfProvider::class, WayfProvider::class);
41-
40+
$context->registerServiceAlias(IWayfProvider::class, WayfProvider::class);
41+
4242
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadContactsFilesActions::class);
4343
$context->registerEventListener(LoadContactsOcaApiEvent::class, LoadContactsOcaApi::class);
4444
$context->registerEventListener(FederatedInviteAcceptedEvent::class, FederatedInviteAcceptedListener::class);

lib/Controller/FederatedInvitesController.php

Lines changed: 100 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99

1010
use Exception;
1111
use OC\App\CompareVersion;
12-
use OCA\DAV\CardDAV\CardDavBackend;
1312
use OCA\Contacts\AppInfo\Application;
1413
use OCA\Contacts\Db\FederatedInvite;
1514
use OCA\Contacts\Db\FederatedInviteMapper;
1615
use OCA\Contacts\IWayfProvider;
1716
use OCA\Contacts\Service\FederatedInvitesService;
1817
use OCA\Contacts\Service\GroupSharingService;
1918
use OCA\Contacts\Service\SocialApiService;
19+
use OCA\DAV\CardDAV\CardDavBackend;
2020
use OCA\FederatedFileSharing\AddressHandler;
2121
use OCP\App\IAppManager;
2222
use OCP\AppFramework\Db\DoesNotExistException;
@@ -47,11 +47,10 @@
4747

4848
/**
4949
* Controller for federated invites related routes.
50-
*
50+
*
5151
*/
5252

53-
class FederatedInvitesController extends PageController
54-
{
53+
class FederatedInvitesController extends PageController {
5554
public function __construct(
5655
IRequest $request,
5756
private AddressHandler $addressHandler,
@@ -94,7 +93,7 @@ public function __construct(
9493

9594
/**
9695
* Returns all open (not yet accepted) invites.
97-
*
96+
*
9897
* @return JSONResponse
9998
*/
10099
#[NoAdminRequired]
@@ -105,7 +104,7 @@ public function getInvites(): JSONResponse {
105104
foreach ($_invites as $invite) {
106105
if ($invite instanceof FederatedInvite) {
107106
array_push(
108-
$invites,
107+
$invites,
109108
$invite->jsonSerialize()
110109
);
111110
}
@@ -122,15 +121,15 @@ public function getInvites(): JSONResponse {
122121
#[NoAdminRequired]
123122
#[NoCSRFRequired]
124123
public function deleteInvite(string $token): JSONResponse {
125-
if(!isset($token)) {
124+
if (!isset($token)) {
126125
return new JSONResponse(['message' => 'Token is required'], Http::STATUS_BAD_REQUEST);
127126
}
128127
try {
129128
$uid = $this->userSession->getUser()->getUID();
130129
$invite = $this->federatedInviteMapper->findInviteByTokenAndUidd($token, $uid);
131130
$this->federatedInviteMapper->delete($invite);
132131
return new JSONResponse(['token' => $token], Http::STATUS_OK);
133-
} catch(DoesNotExistException $e) {
132+
} catch (DoesNotExistException $e) {
134133
$this->logger->error("Could not find invite with token=$token for user with uid=$uid . Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]);
135134
return new JSONResponse(['message' => 'An unexpected error occurred trying to delete the invite'], Http::STATUS_NOT_FOUND);
136135
} catch (Exception $e) {
@@ -141,32 +140,32 @@ public function deleteInvite(string $token): JSONResponse {
141140

142141
/**
143142
* Sets the token and provider states which triggers display of the invite accept dialog.
144-
*
143+
*
145144
* @param string $token
146-
* @param string $provider
145+
* @param string $providerDomain
147146
* @return TemplateResponse
148147
*/
149148
#[NoAdminRequired]
150149
#[NoCSRFRequired]
151-
public function inviteAcceptDialog(string $token = "", string $provider = ""): TemplateResponse {
150+
public function inviteAcceptDialog(string $token = '', string $providerDomain = ''): TemplateResponse {
152151
$this->initialStateService->provideInitialState(Application::APP_ID, 'inviteToken', $token);
153-
$this->initialStateService->provideInitialState(Application::APP_ID, 'inviteProvider', $provider);
152+
$this->initialStateService->provideInitialState(Application::APP_ID, 'inviteProvider', $providerDomain);
154153
$this->initialStateService->provideInitialState(Application::APP_ID, 'acceptInviteDialogUrl', FederatedInvitesService::OCM_INVITE_ACCEPT_DIALOG_ROUTE);
155154

156155
return $this->index();
157156
}
158157

159158
/**
160159
* Creates an invitation to exchange contact info for the user with the specified uid.
161-
*
160+
*
162161
* @param string $emailAddress the recipient email address to send the invitation to
163-
* @param string $message the optional message to send with the invitation
162+
* @param string $message the optional message to send with the invitation
164163
* @return JSONResponse with data signature ['token' | 'message'] - the token of the invitation or an error message in case of error
165164
*/
166165
#[NoAdminRequired]
167166
#[NoCSRFRequired]
168167
public function createInvite(string $email, string $message): JSONResponse {
169-
if(!isset($email)) {
168+
if (!isset($email)) {
170169
return new JSONResponse(['message' => 'Recipient email is required'], Http::STATUS_BAD_REQUEST);
171170
}
172171

@@ -176,7 +175,7 @@ public function createInvite(string $email, string $message): JSONResponse {
176175
$uid,
177176
$email,
178177
);
179-
if(count($existingInvites) > 0) {
178+
if (count($existingInvites) > 0) {
180179
$this->logger->error("An open invite already exists for user with uid $uid and for recipient email $email", ['app' => Application::APP_ID]);
181180
return new JSONResponse(['message' => $this->il10->t('An open invite already exists.')], Http::STATUS_CONFLICT);
182181
}
@@ -194,18 +193,18 @@ public function createInvite(string $email, string $message): JSONResponse {
194193
$invite->setAccepted(false);
195194
try {
196195
$this->federatedInviteMapper->insert($invite);
197-
} catch(Exception $e) {
198-
$this->logger->error("An unexpected error occurred saving a new invite. Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]);
196+
} catch (Exception $e) {
197+
$this->logger->error('An unexpected error occurred saving a new invite. Stacktrace: ' . $e->getTraceAsString(), ['app' => Application::APP_ID]);
199198
return new JSONResponse(['message' => 'An unexpected error occurred creating the invite.'], Http::STATUS_NOT_FOUND);
200199
}
201200

202201
/** @var DataResponse */
203202
$response = $this->sendEmail($token, $email, $message);
204-
if($response->getStatus() !== Http::STATUS_OK) {
203+
if ($response->getStatus() !== Http::STATUS_OK) {
205204
// delete invite in case sending the email has failed
206205
try {
207206
$this->federatedInviteMapper->delete($invite);
208-
} catch(Exception $e) {
207+
} catch (Exception $e) {
209208
$this->logger->error("An unexpected error occurred deleting invite with token $token. Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]);
210209
return new JSONResponse(['message' => 'An unexpected error occurred creating the invite.'], Http::STATUS_NOT_FOUND);
211210
}
@@ -221,15 +220,15 @@ public function createInvite(string $email, string $message): JSONResponse {
221220
/**
222221
* Accepts the invite and creates a new contact from the inviter.
223222
* On success the user is redirected to the new contact url.
224-
*
223+
*
225224
* @param string $token the token of the invite
226-
* @param string $provider the provider of the sender of the invite
225+
* @param string $provider the provider of the sender of the invite
227226
* @return JSONResponse with data signature ['contact' | 'message'] - the new contact url or an error message in case of error
228227
*/
229228
#[NoAdminRequired]
230229
#[NoCSRFRequired]
231-
public function inviteAccepted(string $token = "", string $provider = ""): JSONResponse {
232-
if ($token === "" || $provider === "") {
230+
public function inviteAccepted(string $token = '', string $provider = ''): JSONResponse {
231+
if ($token === '' || $provider === '') {
233232
$this->logger->error("Both token and provider must be specified. Received: token=$token, provider=$provider", ['app' => Application::APP_ID]);
234233
return new JSONResponse(['message' => 'Both token and provider must be specified.'], Http::STATUS_NOT_FOUND);
235234
}
@@ -245,8 +244,8 @@ public function inviteAccepted(string $token = "", string $provider = ""): JSONR
245244
// TODO take provider as is, or do some verification ??
246245
"https://$provider/ocm/invite-accepted",
247246
[
248-
'body' =>
249-
[
247+
'body'
248+
=> [
250249
'recipientProvider' => $recipientProvider,
251250
'token' => $token,
252251
'userId' => $localUser->getUID(),
@@ -261,10 +260,10 @@ public function inviteAccepted(string $token = "", string $provider = ""): JSONR
261260

262261
// Creating a contact does not return a specific 'contact already exists' error,
263262
// so we must check that explicitly
264-
$cloudId = $data['userID'] . "@" . $this->addressHandler->removeProtocolFromUrl($provider);
263+
$cloudId = $data['userID'] . '@' . $this->addressHandler->removeProtocolFromUrl($provider);
265264
$searchResult = $this->contactsManager->search($cloudId, ['CLOUD']);
266265
if (count($searchResult) > 0) {
267-
$this->logger->info("Contact with cloud id " . $cloudId . " already exists.", ['app' => Application::APP_ID]);
266+
$this->logger->info('Contact with cloud id ' . $cloudId . ' already exists.', ['app' => Application::APP_ID]);
268267
return new JSONResponse(['message' => "Contact with cloudID $cloudId already exists."], Http::STATUS_CONFLICT);
269268
}
270269

@@ -279,15 +278,15 @@ public function inviteAccepted(string $token = "", string $provider = ""): JSONR
279278
$this->logger->error("Error accepting invite (token=$token, provider=$provider): Could not create new contact.", ['app' => Application::APP_ID]);
280279
return new JSONResponse(['message' => 'An unexpected error occurred trying to accept invite: could not create new contact'], Http::STATUS_NOT_FOUND);
281280
}
282-
$this->logger->info("Created new contact with UID: " . $newContact['UID'] . " for user with UID: " . $localUser->getUID(), ['app' => Application::APP_ID]);
281+
$this->logger->info('Created new contact with UID: ' . $newContact['UID'] . ' for user with UID: ' . $localUser->getUID(), ['app' => Application::APP_ID]);
283282

284-
$contact = $newContact['UID'] . "~" . CardDavBackend::PERSONAL_ADDRESSBOOK_URI;
283+
$contact = $newContact['UID'] . '~' . CardDavBackend::PERSONAL_ADDRESSBOOK_URI;
285284
$url = $this->urlGenerator->getAbsoluteURL(
286285
$this->urlGenerator->linkToRoute('contacts.page.index') . $this->il10->t('All contacts') . '/' . $contact
287286
);
288287
return new JSONResponse(['contact' => $url], Http::STATUS_OK);
289288
} catch (\GuzzleHttp\Exception\RequestException $e) {
290-
$this->logger->error("/invite-accepted returned an error: " . print_r($responseData, true), ['app' => Application::APP_ID]);
289+
$this->logger->error('/invite-accepted returned an error: ' . print_r($responseData, true), ['app' => Application::APP_ID]);
291290
/**
292291
* 400: Invalid or non existing token
293292
* 409: Invite already accepted
@@ -306,30 +305,85 @@ public function inviteAccepted(string $token = "", string $provider = ""): JSONR
306305
return new JSONResponse(['message' => 'An unexpected error occurred trying to accept invite'], Http::STATUS_NOT_FOUND);
307306
}
308307
}
308+
/**
309+
* Do OCM discovery on behalf of VUE frontend to avoid CSRF issues
310+
* @param string $base base url to discover
311+
* @return DataResponse
312+
*/
313+
#[PublicPage]
314+
#[NoCSRFRequired]
315+
public function discover(string $base): DataResponse {
316+
$base = trim($base);
317+
if ($base === '') {
318+
return new DataResponse(['error' => 'empty base'], 400);
319+
}
320+
321+
// normalize base
322+
if (!preg_match('#^https?://#i', $base)) {
323+
$base = 'https://' . $base;
324+
}
325+
$base = rtrim($base, '/');
326+
327+
$client = $this->httpClient->newClient([
328+
'timeout' => 5,
329+
'connect_timeout' => 5,
330+
'allow_redirects' => true,
331+
]);
332+
333+
foreach ([$base . '/.well-known/ocm', $base . '/ocm-provider'] as $ep) {
334+
try {
335+
$resp = $client->get($ep, ['headers' => ['Accept' => 'application/json']]);
336+
$code = $resp->getStatusCode();
337+
if ($code >= 200 && $code < 300) {
338+
$data = json_decode($resp->getBody(), true);
339+
if (is_array($data) && !empty($data['inviteAcceptDialog'])) {
340+
$dialog = $data['inviteAcceptDialog'];
341+
$absolute = preg_match('#^https?://#i', $dialog) ? $dialog : $base . $dialog;
342+
return new DataResponse([
343+
'base' => $base,
344+
'inviteAcceptDialog' => $dialog,
345+
'inviteAcceptDialogAbsolute' => $absolute,
346+
'raw' => $data,
347+
]);
348+
}
349+
}
350+
} catch (\Throwable $e) {
351+
// try next endpoint
352+
}
353+
}
354+
return new DataResponse(['error' => 'OCM discovery failed', 'base' => $base], 404);
355+
}
309356

310357
/**
311358
* Accepts the invite and creates a new contact from the inviter.
312359
* On success the user is redirected to the new contact url.
313-
*
360+
*
314361
* @param string $token the token of the invite
315-
* @param string $provider the provider of the sender of the invite
362+
* @param string $provider the provider of the sender of the invite
316363
* @return TemplateResponse the WAYF page
317364
*/
318365
#[PublicPage]
319366
#[NoCSRFRequired]
320-
public function wayf(string $token = "", string $provider = ""): TemplateResponse {
321-
try {
367+
public function wayf(string $token = ''): TemplateResponse {
368+
Util::addScript(Application::APP_ID, 'contacts-wayf');
369+
Util::addStyle(Application::APP_ID, 'contacts-wayf');
370+
try {
322371
$providers = $this->wayfProvider->getMeshProviders();
323-
$params = ['providers' => $providers, 'token' => $token, 'provider' => $provider];
324-
$template = new TemplateResponse('contacts', 'wayf', $params, TemplateResponse::RENDER_AS_BLANK);
325-
return $template;
372+
usort($providers, function ($a, $b) {
373+
return strcmp($a['name'], $b['name']);
374+
});
375+
$providerDomain = parse_url($this->urlGenerator->getBaseUrl(), PHP_URL_HOST);
376+
$this->initialStateService->provideInitialState(Application::APP_ID, 'wayf', [
377+
'providers' => $providers,
378+
'providerDomain' => $providerDomain,
379+
'token' => $token,
380+
]);
326381

327-
} catch (Exception $e) {
328-
$this->logger->error($e->getMessage() . ' Trace: ' . $e->getTraceAsString(), ['app' => Application::APP_ID]);
329-
$params = ['error' => 'An error has occurred'];
330-
$template = new TemplateResponse('contacts', 'wayf', $params, TemplateResponse::RENDER_AS_BLANK);
331-
return $template;
332-
}
382+
} catch (Exception $e) {
383+
$this->logger->error($e->getMessage() . ' Trace: ' . $e->getTraceAsString(), ['app' => Application::APP_ID]);
384+
}
385+
$template = new TemplateResponse('contacts', 'wayf', [], TemplateResponse::RENDER_AS_GUEST);
386+
return $template;
333387
}
334388

335389
/**
@@ -341,7 +395,7 @@ public function wayf(string $token = "", string $provider = ""): TemplateRespons
341395
private function sendEmail(string $token, string $address, string $message): JSONResponse {
342396
/** @var IMessage */
343397
$email = $this->mailer->createMessage();
344-
if(!$this->mailer->validateMailAddress($address)) {
398+
if (!$this->mailer->validateMailAddress($address)) {
345399
$this->logger->error("Could not sent invite, invalid email address '$address'", ['app' => Application::APP_ID]);
346400
return new JSONResponse(['message' => 'Recipient email address is invalid'], Http::STATUS_NOT_FOUND);
347401
}
@@ -359,9 +413,8 @@ private function sendEmail(string $token, string $address, string $message): JSO
359413
);
360414
$email->setFrom([Util::getDefaultEmailAddress($instanceName) => $senderName]);
361415

362-
$fqdn = $this->federatedInvitesService->getProviderFQDN();
363416
$wayfEndpoint = $this->wayfProvider->getWayfEndpoint();
364-
$inviteLink = "$wayfEndpoint?token=$token&provider=$fqdn";
417+
$inviteLink = "$wayfEndpoint?token=$token";
365418

366419
$body = "$message\nThe invite link: $inviteLink";
367420
$email->setPlainBody($body);

lib/Db/FederatedInvite.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ class FederatedInvite extends DbFederatedInvite {
1515

1616
public function __construct() {
1717
}
18-
1918
public function jsonSerialize(): array {
2019
return [
2120
'accepted' => $this->accepted,

0 commit comments

Comments
 (0)