diff --git a/.gitignore b/.gitignore index 5f4186b..48cf206 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /.php_cs.cache +/tests/clover.xml +/tests/coverage-html /tests/.phpunit.result.cache /vendor diff --git a/appinfo/routes.php b/appinfo/routes.php index 7db7778..d83acb7 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -13,14 +13,16 @@ return [ 'routes' => [ ['name' => 'settings#admin', 'url' => '/settings', 'verb' => 'POST'], - ['name' => 'register#askEmail', 'url' => '/', 'verb' => 'GET'], - ['name' => 'register#validateEmail', 'url' => '/', 'verb' => 'POST'], - ['name' => 'register#verifyToken', 'url' => '/verify/{token}', 'verb' => 'GET'], - ['name' => 'register#createAccount', 'url' => '/verify/{token}', 'verb' => 'POST'] + ['name' => 'register#showEmailForm', 'url' => '/', 'verb' => 'GET'], + ['name' => 'register#submitEmailForm', 'url' => '/', 'verb' => 'POST'], + ['name' => 'register#showVerificationForm', 'url' => '/verify/{secret}', 'verb' => 'GET'], + ['name' => 'register#submitVerificationForm', 'url' => '/verify/{secret}', 'verb' => 'POST'], + ['name' => 'register#showUserForm', 'url' => '/register/{secret}/{token}', 'verb' => 'GET'], + ['name' => 'register#submitUserForm', 'url' => '/register/{secret}/{token}', 'verb' => 'POST'], ], 'ocs' => [ ['root' => '/registration', 'name' => 'api#validate', 'url' => '/v1/validate', 'verb' => 'POST'], ['root' => '/registration', 'name' => 'api#status', 'url' => '/v1/status', 'verb' => 'POST'], - ['root' => '/registration', 'name' => 'api#register', 'url' => '/v1/register', 'verb' => 'POST'] + ['root' => '/registration', 'name' => 'api#register', 'url' => '/v1/register', 'verb' => 'POST'], ] ]; diff --git a/css/register-button.css b/css/register-button.css index a732b81..9cf50b4 100644 --- a/css/register-button.css +++ b/css/register-button.css @@ -2,7 +2,11 @@ color: var(--color-primary-element); } -.register-button:only-child { +#alternative-logins .register-button:only-child { width: 220px; margin-left: 70px !important; } + +#alternative-logins .register-button.hidden { + display: none; +} diff --git a/css/style.css b/css/style.css index 3987ce6..7171a26 100644 --- a/css/style.css +++ b/css/style.css @@ -1,8 +1,14 @@ -#body-login #email, #body-login #username, #body-login #password { +#body-login #email, +#body-login #token, +#body-login #username, +#body-login #password { width: calc(100% - 56px); padding-left: 36px; } -#email-icon, #username-icon, #password-icon { +#email-icon, +#token-icon, +#username-icon, +#password-icon { position: absolute; left: 16px; top: 22px; diff --git a/img/verify.svg b/img/verify.svg new file mode 100644 index 0000000..f1b2091 --- /dev/null +++ b/img/verify.svg @@ -0,0 +1 @@ + diff --git a/lib/Controller/RegisterController.php b/lib/Controller/RegisterController.php index deb7e55..cc5ea0e 100644 --- a/lib/Controller/RegisterController.php +++ b/lib/Controller/RegisterController.php @@ -1,4 +1,7 @@ * @author Julius Härtl + * @author 2020 Joas Schilling * @copyright Pellaeon Lin 2014 */ namespace OCA\Registration\Controller; +use Exception; use OCA\Registration\Db\Registration; +use OCA\Registration\Service\LoginFlowService; use OCA\Registration\Service\MailService; use OCA\Registration\Service\RegistrationException; use OCA\Registration\Service\RegistrationService; -use \OCP\IRequest; -use \OCP\AppFramework\Http\TemplateResponse; -use \OCP\AppFramework\Http\RedirectResponse; -use \OCP\AppFramework\Controller; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\RedirectToDefaultAppResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\StandaloneTemplateResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IL10N; +use OCP\IRequest; use OCP\IURLGenerator; -use \OCP\IConfig; -use \OCP\IL10N; +use OCP\IConfig; class RegisterController extends Controller { /** @var IL10N */ private $l10n; /** @var IURLGenerator */ - private $urlgenerator; + private $urlGenerator; /** @var IConfig */ private $config; /** @var RegistrationService */ private $registrationService; /** @var MailService */ private $mailService; - + /** @var LoginFlowService */ + private $loginFlowService; public function __construct( - $appName, + string $appName, IRequest $request, IL10N $l10n, - IURLGenerator $urlgenerator, + IURLGenerator $urlGenerator, IConfig $config, RegistrationService $registrationService, + LoginFlowService $loginFlowService, MailService $mailService ) { parent::__construct($appName, $request); $this->l10n = $l10n; - $this->urlgenerator = $urlgenerator; + $this->urlGenerator = $urlGenerator; $this->config = $config; $this->registrationService = $registrationService; + $this->loginFlowService = $loginFlowService; $this->mailService = $mailService; } @@ -59,136 +73,213 @@ class RegisterController extends Controller { * @NoCSRFRequired * @PublicPage * - * @param $errormsg - * @param $entered + * @param string $email + * @param string $message * @return TemplateResponse */ - public function askEmail($errormsg, $entered) { + public function showEmailForm(string $email = '', string $message = ''): TemplateResponse { $params = [ - 'errormsg' => $errormsg ? $errormsg : $this->request->getParam('errormsg'), - 'entered' => $entered ? $entered : $this->request->getParam('entered') + 'email' => $email, + 'message' => $message, ]; - return new TemplateResponse('registration', 'register', $params, 'guest'); + return new TemplateResponse('registration', 'form/email', $params, 'guest'); } /** - * User POST email, if email is valid and not duplicate, we send token by mail * @PublicPage * @AnonRateThrottle(limit=5, period=1) * * @param string $email * @return TemplateResponse */ - public function validateEmail($email) {//TODO rename to receiveUserEmail - if (!$this->registrationService->checkAllowedDomains($email)) {//TODO Duplicate code with Service - return new TemplateResponse('registration', 'domains', [ - 'domains' => $this->registrationService->getAllowedDomains() - ], 'guest'); - } + public function submitEmailForm(string $email): Response { try { - $reg = $this->registrationService->validateEmail($email); - if ($reg === true) { - $registration = $this->registrationService->createRegistration($email); - $this->mailService->sendTokenByMail($registration); - } else { - $this->registrationService->generateNewToken($reg); - $this->mailService->sendTokenByMail($reg); - return new TemplateResponse('registration', 'message', ['msg' => - $this->l10n->t('There is already a pending registration with this email, a new verification email has been sent to the address.') - ], 'guest'); + // Registration already in progress, update token and continue with verification + $registration = $this->registrationService->getRegistrationForEmail($email); + $this->registrationService->generateNewToken($registration); + } catch (DoesNotExistException $e) { + // No registration in progress + try { + $this->registrationService->validateEmail($email); + } catch (RegistrationException $e) { + return $this->showEmailForm($email, $e->getMessage()); } - } catch (RegistrationException $e) { - return new TemplateResponse('registration', 'message', ['msg' => - $e->getMessage().'
'.$e->getHint() - ], 'guest'); + + $registration = $this->registrationService->createRegistration($email); } + try { + $this->mailService->sendTokenByMail($registration); + } catch (RegistrationException $e) { + return $this->showEmailForm($email, $e->getMessage()); + } - return new TemplateResponse('registration', 'message', ['msg' => - $this->l10n->t('Verification email successfully sent.') - ], 'guest'); + return new RedirectResponse( + $this->urlGenerator->linkToRoute( + 'registration.register.showVerificationForm', + ['secret' => $registration->getClientSecret()] + ) + ); } /** * @NoCSRFRequired * @PublicPage * - * @param $token + * @param string $secret + * @param string $message * @return TemplateResponse */ - public function verifyToken($token) { + public function showVerificationForm(string $secret, string $message = ''): TemplateResponse { try { - /** @var Registration $registration */ - $registration = $this->registrationService->verifyToken($token); - $this->registrationService->confirmEmail($registration); + $this->registrationService->getRegistrationForSecret($secret); + } catch (DoesNotExistException $e) { + return $this->validateSecretAndTokenErrorPage(); + } - // create account without form if username/password are already stored - if ($registration->getUsername() !== "" && $registration->getPassword() !== "") { - $this->registrationService->createAccount($registration); - return new TemplateResponse('registration', 'message', - ['msg' => $this->l10n->t('Your account has been successfully created, you can log in now.', [$this->urlgenerator->getAbsoluteURL('/')])], - 'guest' + return new TemplateResponse('registration', 'form/verification', [ + 'message' => $message, + ], 'guest'); + } + + /** + * @PublicPage + * @AnonRateThrottle(limit=5, period=1) + * + * @param string $secret + * @param string $token + * @return Response + */ + public function submitVerificationForm(string $secret, string $token): Response { + try { + $registration = $this->registrationService->getRegistrationForSecret($secret); + + if ($registration->getToken() !== $token) { + return $this->showVerificationForm( + $secret, + $this->l10n->t('The entered verification code is wrong') ); } - - return new TemplateResponse('registration', 'form', [ - 'email' => $registration->getEmail(), - 'email_is_login' => $this->config->getAppValue('registration', 'email_is_login', '0') === '1', - 'token' => $registration->getToken(), - ], 'guest'); - } catch (RegistrationException $exception) { - return $this->renderError($exception->getMessage(), $exception->getHint()); + } catch (DoesNotExistException $e) { + return $this->validateSecretAndTokenErrorPage(); } + + return new RedirectResponse( + $this->urlGenerator->linkToRoute( + 'registration.register.showUserForm', + [ + 'secret' => $secret, + 'token' => $token, + ] + ) + ); + } + + /** + * @NoCSRFRequired + * @PublicPage + * + * @param string $secret + * @param string $token + * @param string $username + * @param string $message + * @return TemplateResponse + */ + public function showUserForm(string $secret, string $token, string $username = '', string $message = ''): TemplateResponse { + try { + $registration = $this->validateSecretAndToken($secret, $token); + } catch (RegistrationException $e) { + return $this->validateSecretAndTokenErrorPage(); + } + + return new TemplateResponse('registration', 'form/user', [ + 'email' => $registration->getEmail(), + 'email_is_login' => $this->config->getAppValue('registration', 'email_is_login', '0') === '1', + 'username' => $username, + 'message' => $message, + ], 'guest'); } /** * @PublicPage * @UseSession + * @AnonRateThrottle(limit=5, period=1) * - * @param $token + * @param string $secret + * @param string $token + * @param string $username + * @param string $password * @return RedirectResponse|TemplateResponse */ - public function createAccount($token) { - $registration = $this->registrationService->getRegistrationForToken($token); + public function submitUserForm(string $secret, string $token, string $username, string $password): Response { + try { + $registration = $this->validateSecretAndToken($secret, $token); + } catch (RegistrationException $e) { + return $this->validateSecretAndTokenErrorPage(); + } + if ($this->config->getAppValue('registration', 'email_is_login', '0') === '1') { $username = $registration->getEmail(); - } else { - $username = $this->request->getParam('username'); } - $password = $this->request->getParam('password'); try { $user = $this->registrationService->createAccount($registration, $username, $password); - } catch (\Exception $exception) { - // Render form with previously sent values - return new TemplateResponse('registration', 'form', - [ - 'email' => $registration->getEmail(), - 'entered_data' => ['user' => $username], - 'errormsgs' => [$exception->getMessage()], - 'token' => $token - ], 'guest'); + } catch (Exception $exception) { + return $this->showUserForm($secret, $token, $username, $exception->getMessage()); } + // Delete registration + $this->registrationService->deleteRegistration($registration); + if ($user->isEnabled()) { - // log the user - return $this->registrationService->loginUser($user->getUID(), $username, $password, false); - } else { - // warn the user their account needs admin validation - return new TemplateResponse( - 'registration', - 'message', - ['msg' => $this->l10n->t("Your account has been successfully created, but it still needs approval from an administrator.")], - 'guest'); + $this->registrationService->loginUser($user->getUID(), $user->getUID(), $password); + + if ($this->loginFlowService->isUsingLoginFlow(2)) { + $response = $this->loginFlowService->tryLoginFlowV2($user); + if ($response instanceof Response) { + return $response; + } + } + + if ($this->loginFlowService->isUsingLoginFlow(1)) { + $response = $this->loginFlowService->tryLoginFlowV1(); + if ($response instanceof Response && $response->getStatus() === Http::STATUS_SEE_OTHER) { + return $response; + } + } + + return new RedirectToDefaultAppResponse(); } + + // warn the user their account needs admin validation + return new StandaloneTemplateResponse('registration', 'approval-required', [], 'guest'); } - private function renderError($error, $hint="") { - return new TemplateResponse('', 'error', [ - 'errors' => [[ - 'error' => $error, - 'hint' => $hint - ]] + /** + * @param string $secret + * @param string $token + * @return Registration + * @throws RegistrationException + */ + protected function validateSecretAndToken(string $secret, string $token): Registration { + try { + $registration = $this->registrationService->getRegistrationForSecret($secret); + } catch (DoesNotExistException $e) { + throw new RegistrationException('Invalid secret'); + } + + if ($registration->getToken() !== $token) { + throw new RegistrationException('Invalid token'); + } + + return $registration; + } + + protected function validateSecretAndTokenErrorPage(): TemplateResponse { + return new TemplateResponse('core', 'error', [ + 'errors' => [ + $this->l10n->t('The verification failed.'), + ], ], 'error'); } } diff --git a/lib/RegistrationLoginOption.php b/lib/RegistrationLoginOption.php index d436574..ceda53e 100644 --- a/lib/RegistrationLoginOption.php +++ b/lib/RegistrationLoginOption.php @@ -47,7 +47,7 @@ class RegistrationLoginOption implements IAlternativeLogin { } public function getLink(): string { - return $this->url->linkToRoute('registration.register.askEmail'); + return $this->url->linkToRoute('registration.register.showEmailForm'); } public function getClass(): string { diff --git a/lib/Service/LoginFlowService.php b/lib/Service/LoginFlowService.php new file mode 100644 index 0000000..9e5e7e3 --- /dev/null +++ b/lib/Service/LoginFlowService.php @@ -0,0 +1,107 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Registration\Service; + +use OC\Core\Controller\ClientFlowLoginController; +use OC\Core\Controller\ClientFlowLoginV2Controller; +use OC\Core\Service\LoginFlowV2Service; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\StandaloneTemplateResponse; +use OCP\IRequest; +use OCP\ISession; +use OCP\IUser; + +class LoginFlowService { + + /** @var IRequest */ + protected $request; + /** @var ISession */ + protected $session; + /** @var LoginFlowV2Service */ + protected $loginFlowV2Service; + + public function __construct( + IRequest $request, + ISession $session, + LoginFlowV2Service $loginFlowV2Service + ) { + $this->request = $request; + $this->session = $session; + $this->loginFlowV2Service = $loginFlowV2Service; + } + + public function isUsingLoginFlow(?int $version = null): bool { + if (($version === 1 || $version === null) && $this->session->get(ClientFlowLoginController::STATE_NAME) !== null) { + return true; + } + + if (($version === 2 || $version === null) && $this->session->get(ClientFlowLoginV2Controller::TOKEN_NAME) !== null) { + return true; + } + + return false; + } + + public function tryLoginFlowV1(): ?Response { + /** @var ClientFlowLoginController $controller */ + $container = \OC::$server->getRegisteredAppContainer('core'); + $controller = $container->query(ClientFlowLoginController::class); + return $controller->generateAppPassword( + $this->session->get(ClientFlowLoginController::STATE_NAME) + ); + } + + public function tryLoginFlowV2(IUser $user): ?StandaloneTemplateResponse { + $result = $this->loginFlowV2Service->flowDone( + $this->session->get(ClientFlowLoginV2Controller::TOKEN_NAME), + $this->session->getId(), + $this->getServerPath(), + $user->getUID() + ); + + if (!$result) { + return null; + } + + return new StandaloneTemplateResponse( + 'core', + 'loginflowv2/done', + [], + 'guest' + ); + } + + private function getServerPath(): string { + $serverPostfix = ''; + + if (strpos($this->request->getRequestUri(), '/index.php') !== false) { + $serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/index.php')); + } elseif (strpos($this->request->getRequestUri(), '/login/v2') !== false) { + $serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/login/v2')); + } + + $protocol = $this->request->getServerProtocol(); + return $protocol . '://' . $this->request->getServerHost() . $serverPostfix; + } +} diff --git a/lib/Service/MailService.php b/lib/Service/MailService.php index 5d8f581..0a8b688 100644 --- a/lib/Service/MailService.php +++ b/lib/Service/MailService.php @@ -81,7 +81,10 @@ class MailService { * @throws RegistrationException */ public function sendTokenByMail(Registration $registration): void { - $link = $this->urlGenerator->linkToRouteAbsolute('registration.register.verifyToken', ['token' => $registration->getToken()]); + $link = $this->urlGenerator->linkToRouteAbsolute('registration.register.showUserForm', [ + 'secret' => $registration->getClientSecret(), + 'token' => $registration->getToken(), + ]); $subject = $this->l10n->t('Verify your %s registration request', [$this->defaults->getName()]); $template = $this->mailer->createEMailTemplate('registration_verify', [ @@ -100,6 +103,10 @@ class MailService { $body ); + $template->addBodyText( + $this->l10n->t('Verification code: %s', $registration->getToken()) + ); + $template->addBodyButton( $this->l10n->t('Continue registration'), $link diff --git a/lib/Service/RegistrationService.php b/lib/Service/RegistrationService.php index e29c2ee..26bf83b 100644 --- a/lib/Service/RegistrationService.php +++ b/lib/Service/RegistrationService.php @@ -1,4 +1,7 @@ * @copyright Copyright (c) 2017 Pellaeon Lin @@ -34,12 +37,11 @@ use OC\Authentication\Token\IToken; use OCA\Registration\Db\Registration; use OCA\Registration\Db\RegistrationMapper; use OCP\AppFramework\Db\DoesNotExistException; -use \OCP\AppFramework\Http\TemplateResponse; -use \OCP\AppFramework\Http\RedirectResponse; use OCP\ILogger; use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; +use OCP\IUser; use OCP\Security\ICrypto; use OCP\Session\Exceptions\SessionNotAvailableException; use \OCP\IUserManager; @@ -70,7 +72,7 @@ class RegistrationService { /** @var ISecureRandom */ private $random; /** @var IUserSession */ - private $usersession; + private $userSession; /** @var IRequest */ private $request; /** @var ILogger */ @@ -82,9 +84,23 @@ class RegistrationService { /** @var ICrypto */ private $crypto; - public function __construct($appName, MailService $mailService, IL10N $l10n, IURLGenerator $urlGenerator, - RegistrationMapper $registrationMapper, IUserManager $userManager, IConfig $config, IGroupManager $groupManager, - ISecureRandom $random, IUserSession $us, IRequest $request, ILogger $logger, ISession $session, IProvider $tokenProvider, ICrypto $crypto) { + public function __construct( + string $appName, + MailService $mailService, + IL10N $l10n, + IURLGenerator $urlGenerator, + RegistrationMapper $registrationMapper, + IUserManager $userManager, + IConfig $config, + IGroupManager $groupManager, + ISecureRandom $random, + IUserSession $userSession, + IRequest $request, + ILogger $logger, + ISession $session, + IProvider $tokenProvider, + ICrypto $crypto + ) { $this->appName = $appName; $this->mailService = $mailService; $this->l10n = $l10n; @@ -94,7 +110,7 @@ class RegistrationService { $this->config = $config; $this->groupManager = $groupManager; $this->random = $random; - $this->usersession = $us; + $this->userSession = $userSession; $this->request = $request; $this->logger = $logger; $this->session = $session; @@ -102,21 +118,16 @@ class RegistrationService { $this->crypto = $crypto; } - /** - * @param Registration $registration - */ - public function confirmEmail(Registration $registration) { + public function confirmEmail(Registration $registration): void { $registration->setEmailConfirmed(true); $this->registrationMapper->update($registration); } - /** - * @param Registration $registration - */ - public function generateNewToken(Registration $registration) { + public function generateNewToken(Registration $registration): void { $this->registrationMapper->generateNewToken($registration); $this->registrationMapper->update($registration); } + /** * Create registration request, used by both the API and form * @param string $email @@ -125,7 +136,7 @@ class RegistrationService { * @param string $displayname * @return Registration */ - public function createRegistration($email, $username="", $password="", $displayname="") { + public function createRegistration(string $email, string $username = '', string $password = '', string $displayname = ''): Registration { $registration = new Registration(); $registration->setEmail($email); $registration->setUsername($username); @@ -135,24 +146,24 @@ class RegistrationService { $registration->setPassword($password); } $this->registrationMapper->generateNewToken($registration); - if ($password !== '' && $username !== '') { - $this->registrationMapper->generateClientSecret($registration); - } + $this->registrationMapper->generateClientSecret($registration); $this->registrationMapper->insert($registration); return $registration; } /** * @param string $email - * @return Registration|true if there is a pending reg with this email, return the pending reg, if there are no problems with the email, return true. * @throws RegistrationException */ - public function validateEmail($email) { + public function validateEmail(string $email): void { $this->mailService->validateEmail($email); // check for pending registrations try { - return $this->registrationMapper->find($email);//if not found DB will throw a exception + $this->registrationMapper->find($email);//if not found DB will throw a exception + throw new RegistrationException( + $this->l10n->t('A user has already taken this email, maybe you already have an account?') + ); } catch (DoesNotExistException $e) { } @@ -171,15 +182,14 @@ class RegistrationService { ) ); } - return true; } /** * @param string $displayname * @throws RegistrationException */ - public function validateDisplayname($displayname) { - if ($displayname === "") { + public function validateDisplayname(string $displayname): void { + if ($displayname === '') { throw new RegistrationException($this->l10n->t('Please provide a valid display name.')); } } @@ -188,7 +198,7 @@ class RegistrationService { * @param string $username * @throws RegistrationException */ - public function validateUsername($username) { + public function validateUsername(string $username): void { if ($username === "") { throw new RegistrationException($this->l10n->t('Please provide a valid user name.')); } @@ -204,15 +214,15 @@ class RegistrationService { * @param string $email * @return bool */ - public function checkAllowedDomains($email) { - $allowed_domains = $this->config->getAppValue($this->appName, 'allowed_domains', ''); - if ($allowed_domains !== '') { - $allowed_domains = explode(';', $allowed_domains); + public function checkAllowedDomains(string $email): bool { + $allowedDomains = $this->config->getAppValue($this->appName, 'allowed_domains', ''); + if ($allowedDomains !== '') { + $allowedDomains = explode(';', $allowedDomains); $allowed = false; - foreach ($allowed_domains as $domain) { - $maildomain = explode("@", $email)[1]; - // valid domain, everythings fine - if ($maildomain === $domain) { + foreach ($allowedDomains as $domain) { + [,$mailDomain] = explode('@', $email, 2); + // valid domain, everything's fine + if ($mailDomain === $domain) { $allowed = true; break; } @@ -223,22 +233,22 @@ class RegistrationService { } /** - * @return array + * @return string[] */ - public function getAllowedDomains() { - $allowed_domains = $this->config->getAppValue($this->appName, 'allowed_domains', ''); - $allowed_domains = explode(';', $allowed_domains); - return $allowed_domains; + public function getAllowedDomains(): array { + $allowedDomains = $this->config->getAppValue($this->appName, 'allowed_domains', ''); + $allowedDomains = explode(';', $allowedDomains); + return $allowedDomains; } /** * Find registration entity for token * * @param string $token - * @return string + * @return Registration * @throws RegistrationException */ - public function verifyToken($token) { + public function verifyToken(string $token): Registration { try { return $this->registrationMapper->findByToken($token); } catch (DoesNotExistException $exception) { @@ -248,12 +258,12 @@ class RegistrationService { /** * @param $registration - * @param string $username - * @param string $password - * @return \OCP\IUser + * @param string|null $username + * @param string|null $password + * @return IUser * @throws RegistrationException|InvalidTokenException */ - public function createAccount(Registration $registration, $username = null, $password = null) { + public function createAccount(Registration $registration, ?string $username = null, ?string $password = null): IUser { if ($password === null && $registration->getPassword() === null) { $generatedPassword = $this->generateRandomDeviceToken(); $registration->setPassword($this->crypto->encrypt($generatedPassword)); @@ -281,6 +291,7 @@ class RegistrationService { throw new RegistrationException($this->l10n->t('Unable to create user, there are problems with the user backend.')); } $userId = $user->getUID(); + // Set user email try { $user->setEMailAddress($registration->getEmail()); @@ -289,72 +300,64 @@ class RegistrationService { } // Add user to group - $registered_user_group = $this->config->getAppValue($this->appName, 'registered_user_group', 'none'); - if ($registered_user_group !== 'none') { - $group = $this->groupManager->get($registered_user_group); + $registeredUserGroup = $this->config->getAppValue($this->appName, 'registered_user_group', 'none'); + if ($registeredUserGroup !== 'none') { + $group = $this->groupManager->get($registeredUserGroup); if ($group === null) { // This might happen if $registered_user_group is deleted after setting the value // Here I choose to log error instead of stopping the user to register - $this->logger->error("You specified newly registered users be added to '$registered_user_group' group, but it does not exist."); + $this->logger->error("You specified newly registered users be added to '$registeredUserGroup' group, but it does not exist."); $groupId = ''; } else { $group->addUser($user); $groupId = $group->getGID(); } } else { - $groupId = ""; + $groupId = ''; } // disable user if this is requested by config - $admin_approval_required = $this->config->getAppValue($this->appName, 'admin_approval_required', "no"); - if ($admin_approval_required === "yes") { + $adminApprovalRequired = $this->config->getAppValue($this->appName, 'admin_approval_required', 'no'); + if ($adminApprovalRequired === 'yes') { $user->setEnabled(false); } - // Delete pending registration if no client secret is stored - // with client secret implies registered via API - // without client secret implies registered via form - // if registered via API, the registration request will be deleted in apicontroller::status - if ($registration->getClientSecret() === null) { - $res = $this->registrationMapper->delete($registration); - if ($res === false) { - throw new RegistrationException($this->l10n->t('Failed to delete pending registration request')); - } - } - $this->mailService->notifyAdmins($userId, $user->isEnabled(), $groupId); return $user; } /** - * @param $token + * @param string $email * @return Registration + * @throws DoesNotExistException */ - public function getRegistrationForToken($token) { + public function getRegistrationForEmail(string $email): Registration { + return $this->registrationMapper->find($email); + } + + /** + * @param string $token + * @return Registration + * @throws DoesNotExistException + */ + public function getRegistrationForToken(string $token): Registration { return $this->registrationMapper->findByToken($token); } /** - * @param $secret + * @param string $secret * @return Registration + * @throws DoesNotExistException */ - public function getRegistrationForSecret($secret) { + public function getRegistrationForSecret(string $secret): Registration { return $this->registrationMapper->findBySecret($secret); } - /** - * @param Registration $registation - * @return null|\OCP\IUser - */ - public function getUserAccount(Registration $registation) { - $user = $this->userManager->get($registation->getUsername()); - return $user; + public function getUserAccount(Registration $registration): ?IUser { + return $this->userManager->get($registration->getUsername()); } - /** - * @param Registration $registration - */ - public function deleteRegistration(Registration $registration) { + public function deleteRegistration(Registration $registration): void { $this->registrationMapper->delete($registration); } @@ -365,7 +368,7 @@ class RegistrationService { * * @return string */ - private function generateRandomDeviceToken() { + private function generateRandomDeviceToken(): string { $groups = []; for ($i = 0; $i < 5; $i++) { $groups[] = $this->random->generate(5, ISecureRandom::CHAR_HUMAN_READABLE); @@ -378,7 +381,7 @@ class RegistrationService { * @return string * @throws RegistrationException */ - public function generateAppPassword($uid) { + public function generateAppPassword(string $uid): string { $name = $this->l10n->t('Registration app auto setup'); try { $sessionId = $this->session->getId(); @@ -404,40 +407,25 @@ class RegistrationService { } /** - * @param $userId - * @param $username - * @param $password - * @param $decrypt - * @return RedirectResponse|TemplateResponse + * @param string $userId + * @param string $username + * @param string $password + * @param bool $decrypt */ - public function loginUser($userId, $username, $password, $decrypt = false) { + public function loginUser(string $userId, string $username, string $password, bool $decrypt = false): void { if ($decrypt) { $password = $this->crypto->decrypt($password); } - if (method_exists($this->usersession, 'createSessionToken')) { - $this->usersession->login($username, $password); - $this->usersession->createSessionToken($this->request, $userId, $username, $password); - return new RedirectResponse($this->urlGenerator->linkTo('', 'index.php')); - } elseif (\OC_User::login($username, $password)) { - $this->cleanupLoginTokens($userId); - // FIXME unsetMagicInCookie will fail from session already closed, so now we always remember - $logintoken = $this->random->generate(32); - $this->config->setUserValue($userId, 'login_token', $logintoken, time()); - \OC_User::setMagicInCookie($userId, $logintoken); - \OC_Util::redirectToDefaultPage(); - } - // Render message in case redirect failed - return new TemplateResponse('registration', 'message', - ['msg' => $this->l10n->t('Your account has been successfully created, you can log in now.', [$this->urlGenerator->getAbsoluteURL('/')])] - , 'guest' - ); + + $this->userSession->login($username, $password); + $this->userSession->createSessionToken($this->request, $userId, $username, $password); } /** * Replicates OC::cleanupLoginTokens() since it's protected * @param string $userId */ - public function cleanupLoginTokens($userId) { + public function cleanupLoginTokens(string $userId): void { $cutoff = time() - $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); $tokens = $this->config->getUserKeys($userId, 'login_token'); foreach ($tokens as $token) { diff --git a/templates/admin.php b/templates/admin.php index 16ff9e5..e9aeb12 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -26,9 +26,13 @@ foreach ($_['groups'] as $group) {

- + > - + +

+ + t('Enabling "admin approval" will prevent registrations from mobile and desktop clients to complete as the credentials can not be verified by the client until the user was enabled.'));?>
diff --git a/templates/approval-required.php b/templates/approval-required.php new file mode 100644 index 0000000..fd20b3c --- /dev/null +++ b/templates/approval-required.php @@ -0,0 +1,13 @@ + +
+

t('Approval required')) ?>

+
    +
  • +

    t('Your account has been successfully created, but it still needs approval from an administrator.')) ?>

    +
  • +
+
diff --git a/templates/domains.php b/templates/domains.php deleted file mode 100644 index 13ce4ef..0000000 --- a/templates/domains.php +++ /dev/null @@ -1,16 +0,0 @@ - -
    -
  • t('Registration is only allowed for the following domains:')); ?> - "; - p($domain); - echo "

    "; - } - ?> -
  • -
diff --git a/templates/form/email.php b/templates/form/email.php new file mode 100644 index 0000000..e68727e --- /dev/null +++ b/templates/form/email.php @@ -0,0 +1,26 @@ + +
+
+ +
    +
  • +
+ + +

+ + + +

+ + + + + t('Back to login')); ?> + +
+
diff --git a/templates/form.php b/templates/form/user.php similarity index 93% rename from templates/form.php rename to templates/form/user.php index c9e81b0..c3f3dc0 100644 --- a/templates/form.php +++ b/templates/form/user.php @@ -23,7 +23,7 @@ script('registration', 'form');

- +

t('Username')); ?>

+ +

diff --git a/templates/form/verification.php b/templates/form/verification.php new file mode 100644 index 0000000..816f351 --- /dev/null +++ b/templates/form/verification.php @@ -0,0 +1,26 @@ + +

+
+ +
    +
  • +
+ + +

+ + + +

+ + + + + t('Back to login')); ?> + +
+
diff --git a/templates/message.php b/templates/message.php deleted file mode 100644 index c895520..0000000 --- a/templates/message.php +++ /dev/null @@ -1,8 +0,0 @@ - -
    -
  • -
diff --git a/templates/register.php b/templates/register.php deleted file mode 100644 index 6c2d542..0000000 --- a/templates/register.php +++ /dev/null @@ -1,58 +0,0 @@ - - -
    -
  • - t('Thank you for registering, you should receive a verification link in a few minutes.')); ?> -
  • -
- -
-
-
    -
  • -
-

- - - -

- - - - - t('Back to login')); ?> - -
-
- - -
-
- -
    -
  • -
  • t('Please re-enter a valid email address')); ?>
  • -
- -
    -
  • t('You will receive an email with a verification link')); ?>
  • -
- -

- - - -

- - - - - t('Back to login')); ?> - -
-
- diff --git a/tests/Integration/Controller/RegisterControllerTest.php b/tests/Integration/Controller/RegisterControllerTest.php deleted file mode 100644 index b3bb45b..0000000 --- a/tests/Integration/Controller/RegisterControllerTest.php +++ /dev/null @@ -1,138 +0,0 @@ -mailService = $this->createMock(MailService::class); - $this->l10n = $this->createMock(IL10N::class); - $this->urlGenerator = $this->createMock(IURLGenerator::class); - #$this->userManager = $this->createMock(IUserManager::class); - $this->userManager = \OC::$server->getUserManager(); - $this->config = $this->createMock(IConfig::class); - $this->groupManager = \OC::$server->getGroupManager(); - $this->random = \OC::$server->getSecureRandom(); - $this->usersession = $this->createMock(IUserSession::class); - $this->request = $this->createMock(IRequest::class); - $this->logger = $this->createMock(ILogger::class); - $this->session = $this->createMock(ISession::class); - $this->tokenProvider = $this->createMock(IProvider::class); - $this->crypto = $this->createMock(ICrypto::class); - - $this->registrationMapper = new RegistrationMapper( - \OC::$server->getDatabaseConnection(), - $this->random - ); - - $this->registrationService = new RegistrationService( - 'registration', - $this->mailService, - $this->l10n, - $this->urlGenerator, - $this->registrationMapper, - $this->userManager, - $this->config, - $this->groupManager, - $this->random, - $this->usersession, - $this->request, - $this->logger, - $this->session, - $this->tokenProvider, - $this->crypto - ); - - $this->controller = new RegisterController( - 'registration', - $this->request, - $this->l10n, - $this->urlGenerator, - $this->config, - $this->registrationService, - $this->mailService - ); - } - - public function testValidateEmailNormal() { - $email = 'aaaa@example.com'; - - $this->config->expects($this->atLeastOnce()) - ->method('getAppValue') - ->with("registration", 'allowed_domains', '') - ->willReturn(''); - $this->mailService->expects($this->once()) - ->method('sendTokenByMail'); - - $this->assertEquals($this->registrationService->validateEmail($email), true); - - $ret = $this->controller->validateEmail($email); - - $expected = new TemplateResponse('registration', 'message', ['msg' => - $this->l10n->t('Verification email successfully sent.') - ], 'guest'); - - - $this->assertEquals($expected, $ret, print_r($ret, true)); - } -} diff --git a/tests/Integration/Service/RegistrationServiceTest.php b/tests/Integration/Service/RegistrationServiceTest.php index 0ab0aaa..4130ffd 100644 --- a/tests/Integration/Service/RegistrationServiceTest.php +++ b/tests/Integration/Service/RegistrationServiceTest.php @@ -61,6 +61,9 @@ class RegistrationServiceTest extends TestCase { /** @var ICrypto */ private $crypto; + /** @var RegistrationService */ + private $service; + public function setUp(): void { parent::setUp(); $this->mailService = $this->createMock(MailService::class); @@ -107,13 +110,10 @@ class RegistrationServiceTest extends TestCase { $this->config->expects($this->once()) ->method('getAppValue') - ->with("registration", 'allowed_domains', '') + ->with('registration', 'allowed_domains', '') ->willReturn(''); - $ret = $this->service->validateEmail($email); - - //$this->assertInstanceOf(Registration::class, $ret); - $this->assertTrue($ret); + $this->service->validateEmail($email); } public function testValidateNewEmailWithinAllowedDomain() { @@ -121,18 +121,23 @@ class RegistrationServiceTest extends TestCase { $this->config->expects($this->atLeastOnce()) ->method('getAppValue') - ->with("registration", 'allowed_domains', '') + ->with('registration', 'allowed_domains', '') ->willReturn('example.com'); - $ret = $this->service->validateEmail($email); - $this->assertTrue($ret, print_r($ret, true)); + $this->service->validateEmail($email); } + /** * @depends testValidateNewEmailWithinAllowedDomain */ public function testValidateNewEmailNotWithinAllowedDomain() { $email2 = 'bbbb@gmail.com'; + $this->config->expects($this->atLeastOnce()) + ->method('getAppValue') + ->with('registration', 'allowed_domains', '') + ->willReturn('example.com'); + $this->expectException(RegistrationException::class); $this->service->validateEmail($email2); } @@ -143,18 +148,24 @@ class RegistrationServiceTest extends TestCase { $this->config->expects($this->atLeastOnce()) ->method('getAppValue') - ->with("registration", 'allowed_domains', '') + ->with('registration', 'allowed_domains', '') ->willReturn('example.com;gmail.com'); - $this->assertTrue($this->service->validateEmail($email)); - $this->assertTrue($this->service->validateEmail($email2)); + $this->service->validateEmail($email); + $this->service->validateEmail($email2); } + /** * @depends testValidateNewEmailWithinMultipleAllowedDomain */ public function testValidateNewEmailNotWithinMultipleAllowedDomain() { $email2 = 'cccc@yahoo.com'; + $this->config->expects($this->atLeastOnce()) + ->method('getAppValue') + ->with('registration', 'allowed_domains', '') + ->willReturn('example.com;gmail.com'); + $this->expectException(RegistrationException::class); $this->service->validateEmail($email2); } @@ -180,10 +191,8 @@ class RegistrationServiceTest extends TestCase { $email = 'aaaa@example.com'; $this->service->createRegistration($email, 'alice'); - $ret = $this->service->validateEmail($email); - - $this->assertInstanceOf(Registration::class, $ret); - $this->assertEquals($email, $ret->getEmail()); + $this->expectException(RegistrationException::class); + $this->service->validateEmail($email); } public function testCreateAccountWebForm() { diff --git a/tests/Unit/Controller/ApiControllerTest.php b/tests/Unit/Controller/ApiControllerTest.php index 2701f42..e37d46a 100644 --- a/tests/Unit/Controller/ApiControllerTest.php +++ b/tests/Unit/Controller/ApiControllerTest.php @@ -157,7 +157,11 @@ class ApiControllerTest extends TestCase { $registration = new Registration(); $registration->setEmailConfirmed(true); $registration->setClientSecret('mysecret'); + $registration->setUsername('user'); + $registration->setPassword('password'); $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); $this->registrationService ->method('getRegistrationForSecret') ->with('mysecret') diff --git a/tests/Unit/Controller/RegisterControllerTest.php b/tests/Unit/Controller/RegisterControllerTest.php new file mode 100644 index 0000000..896f209 --- /dev/null +++ b/tests/Unit/Controller/RegisterControllerTest.php @@ -0,0 +1,717 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Registration\Tests\Unit\Controller; + +use OCA\Registration\Controller\RegisterController; +use OCA\Registration\Db\Registration; +use OCA\Registration\Service\LoginFlowService; +use OCA\Registration\Service\MailService; +use OCA\Registration\Service\RegistrationException; +use OCA\Registration\Service\RegistrationService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\RedirectToDefaultAppResponse; +use OCP\AppFramework\Http\StandaloneTemplateResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IURLGenerator; +use ChristophWurst\Nextcloud\Testing\TestCase; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; + +class RegisterControllerTest extends TestCase { + + /** @var IRequest */ + private $request; + /** @var IL10N|MockObject */ + private $l10n; + /** @var IURLGenerator|MockObject */ + private $urlGenerator; + /** @var IConfig|MockObject */ + private $config; + /** @var RegistrationService|MockObject */ + private $registrationService; + /** @var LoginFlowService|MockObject */ + private $loginFlowService; + /** @var MailService|MockObject */ + private $mailService; + + public function setUp(): void { + parent::setUp(); + $this->request = $this->createMock(IRequest::class); + $this->l10n = $this->createMock(IL10N::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->config = $this->createMock(IConfig::class); + $this->registrationService = $this->createMock(RegistrationService::class); + $this->loginFlowService = $this->createMock(LoginFlowService::class); + $this->mailService = $this->createMock(MailService::class); + + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($text, $parameters = []) { + return vsprintf($text, $parameters); + }); + } + + /** + * @param string[] $methods + * @return RegisterController|MockObject + */ + protected function getController(array $methods = []) { + if (empty($methods)) { + return new RegisterController( + 'registration', + $this->request, + $this->l10n, + $this->urlGenerator, + $this->config, + $this->registrationService, + $this->loginFlowService, + $this->mailService + ); + } + + return $this->getMockBuilder(RegisterController::class) + ->onlyMethods($methods) + ->setConstructorArgs([ + 'registration', + $this->request, + $this->l10n, + $this->urlGenerator, + $this->config, + $this->registrationService, + $this->loginFlowService, + $this->mailService, + ]) + ->getMock(); + } + + public function dataShowEmailForm(): array { + return [ + ['', ''], + ['test@example.tld', 'Registration is only allowed for the following domains: nextcloud.com'], + ]; + } + + /** + * @dataProvider dataShowEmailForm + * @param string $email + * @param string $message + */ + public function testShowEmailForm(string $email, string $message): void { + $controller = $this->getController(); + $response = $controller->showEmailForm($email, $message); + + self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs()); + self::assertSame('form/email', $response->getTemplateName()); + + self::assertSame([ + 'email' => $email, + 'message' => $message, + ], $response->getParams()); + } + + public function testSubmitEmailForm(): void { + $email = 'nextcloud@example.tld'; + + $this->registrationService + ->method('getRegistrationForEmail') + ->with($email) + ->willThrowException(new DoesNotExistException($email)); + + $registration = Registration::fromParams([ + 'clientSecret' => 'clientSecret', + ]); + + $this->registrationService + ->expects($this->once()) + ->method('validateEmail') + ->with($email); + $this->registrationService + ->expects($this->once()) + ->method('createRegistration') + ->with($email) + ->willReturn($registration); + + $this->mailService + ->expects($this->once()) + ->method('sendTokenByMail') + ->with($registration); + + $this->urlGenerator + ->method('linkToRoute') + ->willReturnCallback(function () { + return json_encode(func_get_args()); + }); + + $controller = $this->getController(); + $response = $controller->submitEmailForm($email); + + self::assertInstanceOf(RedirectResponse::class, $response); + /** @var RedirectResponse $response */ + self::assertSame('["registration.register.showVerificationForm",{"secret":"clientSecret"}]', $response->getRedirectURL()); + } + + public function testSubmitEmailFormInvalidEmail(): void { + $email = 'nextcloud@example.tld'; + + $this->registrationService + ->method('getRegistrationForEmail') + ->with($email) + ->willThrowException(new DoesNotExistException($email)); + + $this->registrationService + ->expects($this->once()) + ->method('validateEmail') + ->with($email) + ->willThrowException(new RegistrationException('Invalid email')); + $this->registrationService + ->expects($this->never()) + ->method('createRegistration'); + + $controller = $this->getController([ + 'showEmailForm', + ]); + + $response = $this->createMock(TemplateResponse::class); + $controller->expects($this->once()) + ->method('showEmailForm') + ->with($email, 'Invalid email') + ->willReturn($response); + + self::assertSame($response, $controller->submitEmailForm($email)); + } + + public function testSubmitEmailFormErrorSendingEmail(): void { + $email = 'nextcloud@example.tld'; + + $this->registrationService + ->method('getRegistrationForEmail') + ->with($email) + ->willThrowException(new DoesNotExistException($email)); + + $registration = Registration::fromParams([ + 'clientSecret' => 'clientSecret', + ]); + + $this->registrationService + ->expects($this->once()) + ->method('validateEmail') + ->with($email); + $this->registrationService + ->expects($this->once()) + ->method('createRegistration') + ->with($email) + ->willReturn($registration); + + $this->mailService + ->expects($this->once()) + ->method('sendTokenByMail') + ->with($registration) + ->willThrowException(new RegistrationException('Error sending email')); + + $controller = $this->getController([ + 'showEmailForm', + ]); + + $response = $this->createMock(TemplateResponse::class); + $controller->expects($this->once()) + ->method('showEmailForm') + ->with($email, 'Error sending email') + ->willReturn($response); + + self::assertSame($response, $controller->submitEmailForm($email)); + } + + public function testSubmitEmailFormResendPendingRequest(): void { + $email = 'nextcloud@example.tld'; + + $registration = Registration::fromParams([ + 'clientSecret' => 'clientSecret', + ]); + + $this->registrationService + ->method('getRegistrationForEmail') + ->with($email) + ->willReturn($registration); + + $this->registrationService + ->expects($this->once()) + ->method('generateNewToken') + ->with($registration); + + $this->mailService + ->expects($this->once()) + ->method('sendTokenByMail') + ->with($registration); + + $this->urlGenerator + ->method('linkToRoute') + ->willReturnCallback(function () { + return json_encode(func_get_args()); + }); + + $controller = $this->getController(); + $response = $controller->submitEmailForm($email); + self::assertInstanceOf(RedirectResponse::class, $response); + /** @var RedirectResponse $response */ + self::assertSame('["registration.register.showVerificationForm",{"secret":"clientSecret"}]', $response->getRedirectURL()); + } + + public function dataShowVerificationForm(): array { + return [ + [''], + ['The entered verification code is wrong'], + ]; + } + + /** + * @dataProvider dataShowVerificationForm + * @param string $message + */ + public function testShowVerificationForm(string $message): void { + $secret = '123456789'; + + $this->registrationService + ->expects($this->once()) + ->method('getRegistrationForSecret') + ->with($secret); + + $controller = $this->getController(); + $response = $controller->showVerificationForm($secret, $message); + + self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs()); + self::assertSame('form/verification', $response->getTemplateName()); + + self::assertSame([ + 'message' => $message, + ], $response->getParams()); + } + + public function testShowVerificationFormInvalidSecret(): void { + $secret = '123456789'; + $message = ''; + + $this->registrationService + ->expects($this->once()) + ->method('getRegistrationForSecret') + ->with($secret) + ->willThrowException(new DoesNotExistException('Not found')); + + $response = $this->createMock(TemplateResponse::class); + $controller = $this->getController([ + 'validateSecretAndTokenErrorPage' + ]); + + $controller->expects($this->once()) + ->method('validateSecretAndTokenErrorPage') + ->willReturn($response); + + self::assertSame($response, $controller->showVerificationForm($secret, $message)); + } + + public function testSubmitVerificationForm(): void { + $secret = '123456789'; + $token = 'abcdefghi'; + + $registration = Registration::fromParams([ + 'clientSecret' => $secret, + 'token' => $token, + ]); + + $this->registrationService + ->expects($this->once()) + ->method('getRegistrationForSecret') + ->with($secret) + ->willReturn($registration); + + $this->urlGenerator + ->method('linkToRoute') + ->willReturnCallback(function () { + return json_encode(func_get_args()); + }); + + $controller = $this->getController(); + $response = $controller->submitVerificationForm($secret, $token); + self::assertInstanceOf(RedirectResponse::class, $response); + /** @var RedirectResponse $response */ + self::assertSame('["registration.register.showUserForm",{"secret":"123456789","token":"abcdefghi"}]', $response->getRedirectURL()); + } + + public function testSubmitVerificationFormInvalidToken(): void { + $secret = '123456789'; + $token = 'abcdefghi'; + + $registration = Registration::fromParams([ + 'clientSecret' => $secret, + 'token' => 'zyxwvu', + ]); + + $this->registrationService + ->expects($this->once()) + ->method('getRegistrationForSecret') + ->with($secret) + ->willReturn($registration); + + $response = $this->createMock(TemplateResponse::class); + $controller = $this->getController([ + 'showVerificationForm', + ]); + $controller->expects($this->once()) + ->method('showVerificationForm') + ->with($secret, 'The entered verification code is wrong') + ->willReturn($response); + + self::assertSame($response, $controller->submitVerificationForm($secret, $token)); + } + + public function testSubmitVerificationFormInvalidSecret(): void { + $secret = '123456789'; + $token = 'abcdefghi'; + + $registration = Registration::fromParams([ + 'clientSecret' => $secret, + 'token' => $token, + ]); + + $this->registrationService + ->expects($this->once()) + ->method('getRegistrationForSecret') + ->with($secret) + ->willThrowException(new DoesNotExistException('Invalid secret')); + + $response = $this->createMock(TemplateResponse::class); + $controller = $this->getController([ + 'validateSecretAndTokenErrorPage', + ]); + $controller->expects($this->once()) + ->method('validateSecretAndTokenErrorPage') + ->willReturn($response); + + self::assertSame($response, $controller->submitVerificationForm($secret, $token)); + } + + public function dataShowUserForm(): array { + return [ + ['', ''], + ['tester', ''], + ['', 'Unable to create user, there are problems with the user backend.'], + ]; + } + + /** + * @dataProvider dataShowUserForm + * @param string $username + * @param string $message + */ + public function testShowUserForm(string $username, string $message): void { + $secret = '123456789'; + $token = 'abcdefghi'; + $email = 'nextcloud@example.tld'; + + $registration = Registration::fromParams([ + 'email' => 'nextcloud@example.tld', + ]); + + $controller = $this->getController([ + 'validateSecretAndToken' + ]); + + $controller->expects($this->once()) + ->method('validateSecretAndToken') + ->willReturn($registration); + + $response = $controller->showUserForm($secret, $token, $username, $message); + + self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs()); + self::assertSame('form/user', $response->getTemplateName()); + + self::assertSame([ + 'email' => $email, + 'email_is_login' => false, + 'username' => $username, + 'message' => $message, + ], $response->getParams()); + } + + public function testShowUserFormInvalidSecretAndToken(): void { + $secret = '123456789'; + $token = 'abcdefghi'; + + $controller = $this->getController([ + 'validateSecretAndToken', + 'validateSecretAndTokenErrorPage', + ]); + + $controller->expects($this->once()) + ->method('validateSecretAndToken') + ->willThrowException(new RegistrationException('Invalid secret or token')); + + $response = $this->createMock(TemplateResponse::class); + $controller->expects($this->once()) + ->method('validateSecretAndTokenErrorPage') + ->willReturn($response); + + self::assertSame($response, $controller->showUserForm($secret, $token)); + } + + public function testSubmitUserFormInvalidSecretAndToken(): void { + $secret = '123456789'; + $token = 'abcdefghi'; + + $controller = $this->getController([ + 'validateSecretAndToken', + 'validateSecretAndTokenErrorPage', + ]); + + $controller->expects($this->once()) + ->method('validateSecretAndToken') + ->willThrowException(new RegistrationException('Invalid secret or token')); + + $response = $this->createMock(TemplateResponse::class); + $controller->expects($this->once()) + ->method('validateSecretAndTokenErrorPage') + ->willReturn($response); + + self::assertSame($response, $controller->submitUserForm($secret, $token, '', '')); + } + + public function testSubmitUserFormCreateAccountException(): void { + $secret = '123456789'; + $token = 'abcdefghi'; + $username = 'user'; + $password = 'password'; + + $registration = Registration::fromParams([ + 'email' => 'nextcloud@example.tld', + ]); + + $controller = $this->getController([ + 'validateSecretAndToken', + 'showUserForm' + ]); + + $controller->expects($this->once()) + ->method('validateSecretAndToken') + ->willReturn($registration); + + $response = $this->createMock(TemplateResponse::class); + $controller->expects($this->once()) + ->method('showUserForm') + ->willReturn($response); + + $this->registrationService->expects($this->once()) + ->method('createAccount') + ->with($registration, $username, $password) + ->willThrowException(new RegistrationException('Invalid account data')); + + self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $password)); + } + + public function testSubmitUserFormRequiresAdminApproval(): void { + $secret = '123456789'; + $token = 'abcdefghi'; + $username = 'user'; + $password = 'password'; + + $registration = Registration::fromParams([ + 'email' => 'nextcloud@example.tld', + ]); + + $controller = $this->getController([ + 'validateSecretAndToken', + 'showUserForm' + ]); + + $controller->expects($this->once()) + ->method('validateSecretAndToken') + ->willReturn($registration); + + $user = $this->createMock(IUser::class); + $user->method('isEnabled') + ->willReturn(false); + + $this->registrationService->expects($this->once()) + ->method('createAccount') + ->with($registration, $username, $password) + ->willReturn($user); + + $this->registrationService->expects($this->once()) + ->method('deleteRegistration') + ->with($registration); + + $response = $controller->submitUserForm($secret, $token, $username, $password); + + self::assertInstanceOf(StandaloneTemplateResponse::class, $response); + self::assertSame(TemplateResponse::RENDER_AS_GUEST, $response->getRenderAs()); + self::assertSame('approval-required', $response->getTemplateName()); + } + + public function testSubmitUserFormSuccessful(): void { + $secret = '123456789'; + $token = 'abcdefghi'; + $username = 'user'; + $password = 'password'; + + $registration = Registration::fromParams([ + 'email' => 'nextcloud@example.tld', + ]); + + $controller = $this->getController([ + 'validateSecretAndToken', + 'showUserForm' + ]); + + $controller->expects($this->once()) + ->method('validateSecretAndToken') + ->willReturn($registration); + + $user = $this->createMock(IUser::class); + $user->method('isEnabled') + ->willReturn(true); + $user->method('getUID') + ->willReturn($username); + + $this->registrationService->expects($this->once()) + ->method('createAccount') + ->with($registration, $username, $password) + ->willReturn($user); + + $this->registrationService->expects($this->once()) + ->method('deleteRegistration') + ->with($registration); + + $this->registrationService->expects($this->once()) + ->method('loginUser') + ->with($username, $username, $password); + + $response = $controller->submitUserForm($secret, $token, $username, $password); + + self::assertInstanceOf(RedirectToDefaultAppResponse::class, $response); + } + + public function testSubmitUserFormSuccessfulLoginFlow2(): void { + $secret = '123456789'; + $token = 'abcdefghi'; + $username = 'user'; + $password = 'password'; + + $registration = Registration::fromParams([ + 'email' => 'nextcloud@example.tld', + ]); + + $controller = $this->getController([ + 'validateSecretAndToken', + 'showUserForm' + ]); + + $controller->expects($this->once()) + ->method('validateSecretAndToken') + ->willReturn($registration); + + $user = $this->createMock(IUser::class); + $user->method('isEnabled') + ->willReturn(true); + $user->method('getUID') + ->willReturn($username); + + $this->registrationService->expects($this->once()) + ->method('createAccount') + ->with($registration, $username, $password) + ->willReturn($user); + + $this->registrationService->expects($this->once()) + ->method('deleteRegistration') + ->with($registration); + + $this->registrationService->expects($this->once()) + ->method('loginUser') + ->with($username, $username, $password); + + $this->loginFlowService->method('isUsingLoginFlow') + ->with(2) + ->willReturn(true); + + $response = $this->createMock(StandaloneTemplateResponse::class); + $this->loginFlowService->method('tryLoginFlowV2') + ->with($user) + ->willReturn($response); + + self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $password)); + } + + public function testSubmitUserFormSuccessfulLoginFlow1(): void { + $secret = '123456789'; + $token = 'abcdefghi'; + $username = 'user'; + $password = 'password'; + + $registration = Registration::fromParams([ + 'email' => 'nextcloud@example.tld', + ]); + + $controller = $this->getController([ + 'validateSecretAndToken', + 'showUserForm' + ]); + + $controller->expects($this->once()) + ->method('validateSecretAndToken') + ->willReturn($registration); + + $user = $this->createMock(IUser::class); + $user->method('isEnabled') + ->willReturn(true); + $user->method('getUID') + ->willReturn($username); + + $this->registrationService->expects($this->once()) + ->method('createAccount') + ->with($registration, $username, $password) + ->willReturn($user); + + $this->registrationService->expects($this->once()) + ->method('deleteRegistration') + ->with($registration); + + $this->registrationService->expects($this->once()) + ->method('loginUser') + ->with($username, $username, $password); + + $this->loginFlowService->method('isUsingLoginFlow') + ->withConsecutive([2], [1]) + ->willReturnOnConsecutiveCalls(false, true); + + $response = $this->createMock(RedirectResponse::class); + $response->method('getStatus') + ->willReturn(Http::STATUS_SEE_OTHER); + $this->loginFlowService->method('tryLoginFlowV1') + ->willReturn($response); + + self::assertSame($response, $controller->submitUserForm($secret, $token, $username, $password)); + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index b833956..b437e68 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -9,7 +9,8 @@ - ../ + ../../registration/appinfo + ../../registration/lib