Skip to content

[WIP] exchange cloud ID #4417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
# SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
# SPDX-License-Identifier: AGPL-3.0-or-later

app_name=$(notdir $(CURDIR))
project_directory=$(CURDIR)/../$(app_name)
app_name="contacts"
project_folder="nextcloud-contacts"

project_directory=$(CURDIR)/../$(project_folder)
appstore_build_directory=$(CURDIR)/build/artifacts
appstore_package_name=$(appstore_build_directory)/$(app_name)

Expand All @@ -20,3 +22,55 @@ clean-dev:
# Builds the source package for the app store, ignores php and js tests
appstore:
krankerl package

# Builds the source package for the app store, ignores php and js tests
# command: make version={version_number} buildapp
# concatenate cd, ls and tar commands with '&&' otherwise the script context will remain the root instead of build
.PHONY: buildapp
buildapp:
make check-version

version=$(version)

make clean-buildapp

mkdir -p $(appstore_build_directory)
cd build && \
ln -s ../ $(app_name) && \
tar cvzfh $(appstore_build_directory)/$(app_name)_$(version).tar.gz \
--exclude="$(app_name)/build" \
--exclude="$(app_name)/release" \
--exclude="$(app_name)/tests" \
--exclude="$(app_name)/src" \
--exclude="$(app_name)/tests" \
--exclude="$(app_name)/vite.config.js" \
--exclude="$(app_name)/*.log" \
--exclude="$(app_name)/phpunit*xml" \
--exclude="$(app_name)/composer.*" \
--exclude="$(app_name)/node_modules" \
--exclude="$(app_name)/js/node_modules" \
--exclude="$(app_name)/js/tests" \
--exclude="$(app_name)/js/test" \
--exclude="$(app_name)/js/*.log" \
--exclude="$(app_name)/js/package.json" \
--exclude="$(app_name)/js/bower.json" \
--exclude="$(app_name)/js/karma.*" \
--exclude="$(app_name)/js/protractor.*" \
--exclude="$(app_name)/package.json" \
--exclude="$(app_name)/bower.json" \
--exclude="$(app_name)/karma.*" \
--exclude="$(app_name)/protractor\.*" \
--exclude="$(app_name)/.*" \
--exclude="$(app_name)/js/.*" \
--exclude-vcs \
$(app_name) && \
rm $(app_name)

clean-buildapp:
rm -rf ${appstore_build_directory}

check-version:
@if [ "${version}" = "" ]; then\
echo "Error: You must set version, eg. make -e version=v0.0.1 buildapp";\
exit 1;\
fi
10 changes: 10 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

// OCM: the path to the invite accept dialog
// TODO read from config
$inviteAcceptDialogPath = '/ocm/invite-accept-dialog';

return [
'routes' => [
['name' => 'contacts#direct', 'url' => '/direct/contact/{contact}', 'verb' => 'GET'],
['name' => 'contacts#directcircle', 'url' => '/direct/circle/{singleId}', 'verb' => 'GET'],

['name' => 'federated_invites#get_invites', 'url' => '/ocm/invitations', 'verb' => 'GET'],
['name' => 'federated_invites#create_invite', 'url' => '/ocm/invitations', 'verb' => 'POST'],
['name' => 'federated_invites#invite_accepted', 'url' => '/ocm/invitations/{token}/accept', 'verb' => 'PATCH'],
['name' => 'federated_invites#invite_accept_dialog', 'url' => $inviteAcceptDialogPath, 'verb' => 'GET'],

['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'page#index', 'url' => '/{group}', 'verb' => 'GET', 'postfix' => 'group'],
['name' => 'page#index', 'url' => '/{group}/{contact}', 'verb' => 'GET', 'postfix' => 'group.contact'],
Expand Down
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
*/
namespace OCA\Contacts\AppInfo;

use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent;
use OCA\Contacts\Dav\PatchPlugin;
use OCA\Contacts\Event\LoadContactsOcaApiEvent;
use OCA\Contacts\Listener\FederatedInviteAcceptedListener;
use OCA\Contacts\Listener\LoadContactsFilesActions;
use OCA\Contacts\Listener\LoadContactsOcaApi;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
Expand All @@ -32,6 +34,7 @@ public function __construct() {
public function register(IRegistrationContext $context): void {
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadContactsFilesActions::class);
$context->registerEventListener(LoadContactsOcaApiEvent::class, LoadContactsOcaApi::class);
$context->registerEventListener(FederatedInviteAcceptedEvent::class, FederatedInviteAcceptedListener::class);
}

public function boot(IBootContext $context): void {
Expand Down
231 changes: 231 additions & 0 deletions lib/Controller/FederatedInvitesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
<?php

/**
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Contacts\Controller;

use Exception;
use OC\App\CompareVersion;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\Contacts\AppInfo\Application;
use OCA\Contacts\Db\FederatedInvite;
use OCA\Contacts\Db\FederatedInviteMapper;
use OCA\Contacts\Service\FederatedInvitesService;
use OCA\Contacts\Service\GroupSharingService;
use OCA\Contacts\Service\SocialApiService;
use OCA\FederatedFileSharing\AddressHandler;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Contacts\IManager;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IInitialStateService;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use Psr\Log\LoggerInterface;
use Sabre\DAV\UUIDUtil;

/**
* Controller for federated invites related routes.
*
*/

class FederatedInvitesController extends PageController
{

public function __construct(
IRequest $request,
private AddressHandler $addressHandler,
private CardDavBackend $cardDavBackend,
private FederatedInviteMapper $federatedInviteMapper,
private FederatedInvitesService $federatedInvitesService,
private IAppManager $appManager,
private IClientService $httpClient,
private IConfig $config,
private IInitialStateService $initialStateService,
private IFactory $languageFactory,
private IManager $contactsManager,
private IUserSession $userSession,
private SocialApiService $socialApiService,
private ITimeFactory $timeFactory,
private CompareVersion $compareVersion,
private GroupSharingService $groupSharingService,
private IL10N $il10,
private IURLGenerator $urlGenerator,
private IUserManager $userManager,
private LoggerInterface $logger,
) {
parent::__construct(
$request,
$federatedInvitesService,
$config,
$initialStateService,
$languageFactory,
$userSession,
$socialApiService,
$appManager,
$compareVersion,
$groupSharingService,
$logger,
);
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Returns all open (not yet accepted) invites.
*
* @return DataResponse
*/
public function getInvites(): DataResponse {
$_invites = $this->federatedInviteMapper->findOpenInvitesByUiddd($this->userSession->getUser()->getUID());
$invites = [];
foreach ($_invites as $invite) {
if ($invite instanceof FederatedInvite) {
array_push(
$invites,
$invite->jsonSerialize()
);
}
}
return new DataResponse($invites, Http::STATUS_OK);
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Sets the token and provider states which triggers display of the invite accept dialog.
*
* @param string $token
* @param string $provider
*/
public function inviteAcceptDialog(string $token = "", string $provider = ""): TemplateResponse {
$this->initialStateService->provideInitialState(Application::APP_ID, 'inviteToken', $token);
$this->initialStateService->provideInitialState(Application::APP_ID, 'inviteProvider', $provider);
// TODO read from config
$this->initialStateService->provideInitialState(Application::APP_ID, 'acceptInviteDialogUrl', '/ocm/invite-accept-dialog');

return $this->index();
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Creates an invitation to exchange contact info for the user with the specified uid.
*
* @param string $email the recipient email to send the invitation to
* @param string $message the optional message to send with the invitation
* @return DataResponse with data signature ['token' | 'error'] - the token of the invitation or an error message in case of error
*/
public function createInvite(string $email = null, string $message = null): DataResponse {
if(!isset($email)) {
return new DataResponse(['error' => 'Recipient email is required'], Http::STATUS_BAD_REQUEST);
}
$invite = new FederatedInvite();
$invite->setUserId($this->userSession->getUser()->getUID());
$token = UUIDUtil::getUUID();
$invite->setToken($token);
$invite->setCreatedAt($this->timeFactory->getTime());
// TODO get expiration period from config
// take 30 days
$invite->setExpiredAt($invite->getCreatedAt() + 2592000000);
$invite->setRecipientEmail($email);
$invite->setAccepted(false);
$this->federatedInviteMapper->insert($invite);

// TODO send email

return new DataResponse(['token' => $token], Http::STATUS_OK);
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Accepts the invite and creates a new contact for it.
* On success the user will be redirected to the contacts list with the newly created contact in focus.
*
* @param string $token the token of the invite
* @param string $provider the provider of the sender of the invite
* @return DataResponse with data signature ['contact' | 'error'] - the new contact url or an error message in case of error
*/
public function inviteAccepted(string $token = "", string $provider = ""): DataResponse {
if ($token === "" || $provider === "") {
$this->logger->error("Both token and provider must be specified. Received: token=$token, provider=$provider", ['app' => Application::APP_ID]);
return new DataResponse(['error' => 'Both token and provider must be specified.'], Http::STATUS_NOT_FOUND);
}
try {
// delegate further to OCM /invite-accepted
// this returns a response with the following data signature: ['userID', 'email', 'name']
// @link https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post
$localUser = $this->userSession->getUser();
$client = $this->httpClient->newClient();
$responseData = null;
$response = $client->post(
// TODO get the correct url in an appropriate manner
"http://nc-ocm.nl/ocm/invite-accepted",
[
'body' =>
[
'recipientProvider' => $provider,
'token' => $token,
'userId' => $localUser->getUID(),
'email' => $localUser->getEMailAddress(),
'name' => $localUser->getDisplayName(),
],
'connect_timeout' => 10,
]
);
$responseData = $response->getBody();
$data = json_decode($responseData, true);
$newContact = $this->socialApiService->createFederatedContact(
// the ocm address: nextcloud cloud id format
$data['userID'] . "@" . $this->addressHandler->removeProtocolFromUrl($provider),
$data['email'],
$data['name'],
$localUser->getUID(),
);
if (!isset($newContact)) {
return new DataResponse(['error' => 'An unexpected error occurred trying to accept invite: could not create new contact'], Http::STATUS_NOT_FOUND);
}
$this->logger->info("Created new contact with UID: " . $newContact['UID'] . " for user with UID: " . $localUser->getUID(), ['app' => Application::APP_ID]);

$contact = $newContact['UID'] . "~" . CardDavBackend::PERSONAL_ADDRESSBOOK_URI;
$url = $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->linkToRoute('contacts.page.index') . $this->il10->t('All contacts') . '/' . $contact
);
return new DataResponse(['contact' => $url], Http::STATUS_OK);
} catch (\GuzzleHttp\Exception\RequestException $e) {
$this->logger->error("/invite-accepted returned an error: " . print_r($responseData, true), ['app' => Application::APP_ID]);
/**
* 400: Invalid or non existing token
* 409: Invite already accepted
*/
$statusCode = $e->getCode();
switch ($statusCode) {
case Http::STATUS_BAD_REQUEST:
return new DataResponse(['error' => 'Invalid, non existing or expired token'], $e->getCode());
case Http::STATUS_CONFLICT:
return new DataResponse(['error' => 'Invite already accepted'], $e->getCode());
}
$this->logger->error("An unexpected error occurred accepting invite with token=$token and provider=$provider. Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]);
return new DataResponse(['error' => 'An unexpected error occurred trying to accept invite.'], Http::STATUS_NOT_FOUND);
} catch (Exception $e) {
$this->logger->error("An unexpected error occurred accepting invite with token=$token and provider=$provider. Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]);
return new DataResponse(['error' => 'An unexpected error occurred trying to accept invite'], Http::STATUS_NOT_FOUND);
}
}
}
4 changes: 4 additions & 0 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

use OC\App\CompareVersion;
use OCA\Contacts\AppInfo\Application;
use OCA\Contacts\Service\FederatedInvitesService;
use OCA\Contacts\Service\GroupSharingService;
use OCA\Contacts\Service\SocialApiService;
use OCP\App\IAppManager;
Expand All @@ -25,6 +26,7 @@ class PageController extends Controller {

public function __construct(
IRequest $request,
private FederatedInvitesService $federatedInvitesService,
private IConfig $config,
private IInitialStateService $initialStateService,
private IFactory $languageFactory,
Expand Down Expand Up @@ -67,6 +69,7 @@ public function index(): TemplateResponse {
$isTalkEnabled = $this->appManager->isEnabledForUser('spreed') === true;

$isTalkVersionCompatible = $this->compareVersion->isCompatible($talkVersion ? $talkVersion : '0.0.0', 2);
$isOcmInvitesEnabled = $this->federatedInvitesService->isOcmInvitesEnabled();

$this->initialStateService->provideInitialState(Application::APP_ID, 'isGroupSharingEnabled', $isGroupSharingEnabled);
$this->initialStateService->provideInitialState(Application::APP_ID, 'locales', $locales);
Expand All @@ -77,6 +80,7 @@ public function index(): TemplateResponse {
$this->initialStateService->provideInitialState(Application::APP_ID, 'isContactsInteractionEnabled', $isContactsInteractionEnabled);
$this->initialStateService->provideInitialState(Application::APP_ID, 'isCirclesEnabled', $isCirclesEnabled && $isCircleVersionCompatible);
$this->initialStateService->provideInitialState(Application::APP_ID, 'isTalkEnabled', $isTalkEnabled && $isTalkVersionCompatible);
$this->initialStateService->provideInitialState(Application::APP_ID, 'isOcmInvitesEnabled', $isOcmInvitesEnabled);

Util::addStyle(Application::APP_ID, 'contacts-main');
Util::addScript(Application::APP_ID, 'contacts-main');
Expand Down
Loading