Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
71.74% |
99 / 138 |
|
66.67% |
8 / 12 |
CRAP | |
0.00% |
0 / 1 |
| |
71.74% |
99 / 138 |
|
66.67% |
8 / 12 |
57.58 | |
0.00% |
0 / 1 |
|
| __construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| validateToRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| validateToIdentify | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| validateToSign | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| throwIfNeedToCreateAccount | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
7 | |||
| throwIfIsAuthenticatedWithDifferentAccount | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
6 | |||
| throwIfAccountAlreadyExists | |
32.00% |
8 / 25 |
|
0.00% |
0 / 1 |
17.32 | |||
| validateToCreateAccount | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| throwIfNotAllowedToCreateAccount | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
| throwIfInvalidEmail | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| getSettings | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
| getDefaultSettings | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | /** |
| 5 | * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors |
| 6 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 7 | */ |
| 8 | |
| 9 | namespace OCA\Libresign\Service\IdentifyMethod; |
| 10 | |
| 11 | use OCA\Libresign\Db\FileElementMapper; |
| 12 | use OCA\Libresign\Db\IdentifyMethodMapper; |
| 13 | use OCA\Libresign\Exception\LibresignException; |
| 14 | use OCA\Libresign\Helper\JSActions; |
| 15 | use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\ISignatureMethod; |
| 16 | use OCA\Libresign\Service\SessionService; |
| 17 | use OCP\AppFramework\Utility\ITimeFactory; |
| 18 | use OCP\Files\IRootFolder; |
| 19 | use OCP\IUser; |
| 20 | use OCP\IUserSession; |
| 21 | use Psr\Log\LoggerInterface; |
| 22 | |
| 23 | class Email extends AbstractIdentifyMethod { |
| 24 | public array $availableSignatureMethods = [ |
| 25 | ISignatureMethod::SIGNATURE_METHOD_CLICK_TO_SIGN, |
| 26 | ISignatureMethod::SIGNATURE_METHOD_EMAIL_TOKEN, |
| 27 | ]; |
| 28 | public string $defaultSignatureMethod = ISignatureMethod::SIGNATURE_METHOD_EMAIL_TOKEN; |
| 29 | public function __construct( |
| 30 | protected IdentifyService $identifyService, |
| 31 | private IdentifyMethodMapper $identifyMethodMapper, |
| 32 | private IRootFolder $root, |
| 33 | private ITimeFactory $timeFactory, |
| 34 | private SessionService $sessionService, |
| 35 | private FileElementMapper $fileElementMapper, |
| 36 | private IUserSession $userSession, |
| 37 | private LoggerInterface $logger, |
| 38 | ) { |
| 39 | // TRANSLATORS Name of possible authenticator method. This signalize that the signer could be identified by email |
| 40 | $this->setFriendlyName($this->identifyService->getL10n()->t('Email')); |
| 41 | parent::__construct( |
| 42 | $identifyService, |
| 43 | ); |
| 44 | } |
| 45 | |
| 46 | #[\Override] |
| 47 | public function validateToRequest(): void { |
| 48 | $this->throwIfInvalidEmail(); |
| 49 | } |
| 50 | |
| 51 | #[\Override] |
| 52 | public function validateToIdentify(): void { |
| 53 | $this->throwIfAccountAlreadyExists(); |
| 54 | $this->throwIfIsAuthenticatedWithDifferentAccount(); |
| 55 | $this->throwIfMaximumValidityExpired(); |
| 56 | $this->throwIfRenewalIntervalExpired(); |
| 57 | $this->throwIfNeedToCreateAccount(); |
| 58 | $this->throwIfFileNotFound(); |
| 59 | $this->throwIfAlreadySigned(); |
| 60 | $this->renewSession(); |
| 61 | $this->updateIdentifiedAt(); |
| 62 | } |
| 63 | |
| 64 | #[\Override] |
| 65 | public function validateToSign(): void { |
| 66 | $this->throwIfAccountAlreadyExists(); |
| 67 | $this->throwIfIsAuthenticatedWithDifferentAccount(); |
| 68 | $this->throwIfInvalidToken(); |
| 69 | $this->throwIfMaximumValidityExpired(); |
| 70 | $this->throwIfRenewalIntervalExpired(); |
| 71 | $this->throwIfNeedToCreateAccount(); |
| 72 | $this->throwIfFileNotFound(); |
| 73 | $this->throwIfAlreadySigned(); |
| 74 | $this->renewSession(); |
| 75 | $this->updateIdentifiedAt(); |
| 76 | } |
| 77 | |
| 78 | protected function throwIfNeedToCreateAccount(): void { |
| 79 | $user = $this->userSession->getUser(); |
| 80 | if ($user instanceof IUser) { |
| 81 | return; |
| 82 | } |
| 83 | $settings = $this->getSettings(); |
| 84 | if (!$settings['enabled']) { |
| 85 | $this->logger->debug('Email identification method is disabled'); |
| 86 | throw new LibresignException(json_encode([ |
| 87 | 'action' => JSActions::ACTION_SHOW_ERROR, |
| 88 | // TRANSLATORS This signalize that the identification method by email is disabled and the signer can't be identified |
| 89 | 'errors' => [['message' => $this->identifyService->getL10n()->t('Invalid identification method')]], |
| 90 | ])); |
| 91 | return; |
| 92 | } |
| 93 | if (!$settings['can_create_account']) { |
| 94 | return; |
| 95 | } |
| 96 | if ($this->identifyService->getSessionService()->getSignStartTime()) { |
| 97 | return; |
| 98 | } |
| 99 | $email = $this->getEntity()->getIdentifierValue(); |
| 100 | |
| 101 | $signer = $this->identifyService->getUserManager()->getByEmail($email); |
| 102 | if (!$signer) { |
| 103 | throw new LibresignException(json_encode([ |
| 104 | 'action' => JSActions::ACTION_CREATE_ACCOUNT, |
| 105 | 'settings' => ['accountHash' => md5($email)], |
| 106 | // TRANSLATORS Message inviting a guest signer identified by email to create a Nextcloud account before signing. |
| 107 | 'message' => $this->identifyService->getL10n()->t('You need to create an account to sign this file.'), |
| 108 | ])); |
| 109 | } |
| 110 | $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId()); |
| 111 | // TRANSLATORS Error shown when the email already belongs to an existing account and the guest must log in to sign. |
| 112 | $errors = [$this->identifyService->getL10n()->t('User already exists. Please login.')]; |
| 113 | if ($this->userSession->isLoggedIn()) { |
| 114 | // TRANSLATORS Error shown when the logged-in user tries to sign a document that was not requested for them. |
| 115 | $errors[] = $this->identifyService->getL10n()->t('This is not your file'); |
| 116 | $this->userSession->logout(); |
| 117 | } |
| 118 | throw new LibresignException(json_encode([ |
| 119 | 'action' => JSActions::ACTION_REDIRECT, |
| 120 | 'errors' => $errors, |
| 121 | 'redirect' => $this->identifyService->getUrlGenerator()->linkToRoute('core.login.showLoginForm', [ |
| 122 | 'redirect_url' => $this->identifyService->getUrlGenerator()->linkToRoute( |
| 123 | 'libresign.page.sign', |
| 124 | ['uuid' => $signRequest->getUuid()] |
| 125 | ), |
| 126 | ]), |
| 127 | ])); |
| 128 | } |
| 129 | |
| 130 | private function throwIfIsAuthenticatedWithDifferentAccount(): void { |
| 131 | $user = $this->userSession->getUser(); |
| 132 | if (!$user instanceof IUser) { |
| 133 | return; |
| 134 | } |
| 135 | $email = $this->entity->getIdentifierValue(); |
| 136 | if (!empty($user->getEMailAddress()) && $user->getEMailAddress() === $email) { |
| 137 | return; |
| 138 | } |
| 139 | if ($this->getEntity()->getCode() && !$this->getEntity()->getIdentifiedAtDate()) { |
| 140 | return; |
| 141 | } |
| 142 | throw new LibresignException(json_encode([ |
| 143 | 'action' => JSActions::ACTION_DO_NOTHING, |
| 144 | 'page_state' => 'authentication_required', |
| 145 | 'page_state_data' => [ |
| 146 | // TRANSLATORS Title shown when a signing link is opened while a different authenticated Nextcloud session is active. This is an expected security check, not a system failure. |
| 147 | 'title' => $this->identifyService->getL10n()->t('Authentication required'), |
| 148 | // TRANSLATORS Description shown when the current authenticated Nextcloud session cannot be used to sign a document because it does not match the authorized signer. |
| 149 | 'description' => $this->identifyService->getL10n()->t('The current authenticated session cannot be used to sign this document.'), |
| 150 | 'noteType' => 'info', |
| 151 | 'icon' => 'info', |
| 152 | ], |
| 153 | 'errors' => [[ |
| 154 | // TRANSLATORS Guidance shown when the current authenticated Nextcloud session cannot be used to sign a document because it does not match the authorized signer. Keep it generic and do not mention emails or delivery channels. |
| 155 | 'message' => $this->identifyService->getL10n()->t('To continue, sign out from the current account and open the signing link again, or open the signing link in a browser session where this account is not active.'), |
| 156 | ]], |
| 157 | ])); |
| 158 | } |
| 159 | |
| 160 | private function throwIfAccountAlreadyExists(): void { |
| 161 | $user = $this->userSession->getUser(); |
| 162 | if (!$user instanceof IUser) { |
| 163 | return; |
| 164 | } |
| 165 | $email = $this->entity->getIdentifierValue(); |
| 166 | $signer = $this->identifyService->getUserManager()->getByEmail($email); |
| 167 | if (!$signer) { |
| 168 | return; |
| 169 | } |
| 170 | foreach ($signer as $s) { |
| 171 | if ($s->getUID() === $user->getUID()) { |
| 172 | return; |
| 173 | } |
| 174 | } |
| 175 | $signRequest = $this->identifyService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId()); |
| 176 | // TRANSLATORS Error shown when the email already belongs to an existing account and the guest must log in to sign. |
| 177 | $errors = [$this->identifyService->getL10n()->t('User already exists. Please login.')]; |
| 178 | if ($this->userSession->isLoggedIn()) { |
| 179 | // TRANSLATORS Error shown when the logged-in user tries to sign a document that was not requested for them. |
| 180 | $errors[] = $this->identifyService->getL10n()->t('This is not your file'); |
| 181 | $this->userSession->logout(); |
| 182 | } |
| 183 | throw new LibresignException(json_encode([ |
| 184 | 'action' => JSActions::ACTION_REDIRECT, |
| 185 | 'errors' => $errors, |
| 186 | 'redirect' => $this->identifyService->getUrlGenerator()->linkToRoute('core.login.showLoginForm', [ |
| 187 | 'redirect_url' => $this->identifyService->getUrlGenerator()->linkToRoute( |
| 188 | 'libresign.page.sign', |
| 189 | ['uuid' => $signRequest->getUuid()] |
| 190 | ), |
| 191 | ]), |
| 192 | ])); |
| 193 | } |
| 194 | |
| 195 | #[\Override] |
| 196 | public function validateToCreateAccount(string $value): void { |
| 197 | $this->throwIfInvalidEmail(); |
| 198 | $this->throwIfNotAllowedToCreateAccount(); |
| 199 | if ($this->identifyService->getUserManager()->userExists($value)) { |
| 200 | // TRANSLATORS Error shown when creating a guest account for signing and that email is already registered. |
| 201 | throw new LibresignException($this->identifyService->getL10n()->t('User already exists')); |
| 202 | } |
| 203 | if ($this->getEntity()->getIdentifierValue() !== $value) { |
| 204 | // TRANSLATORS Error shown when the logged-in user tries to sign a document that was not requested for them. |
| 205 | throw new LibresignException($this->identifyService->getL10n()->t('This is not your file')); |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | private function throwIfNotAllowedToCreateAccount(): void { |
| 210 | $settings = $this->getSettings(); |
| 211 | if (!$settings['can_create_account']) { |
| 212 | throw new LibresignException(json_encode([ |
| 213 | 'action' => JSActions::ACTION_SHOW_ERROR, |
| 214 | // TRANSLATORS Error shown when guest account creation is disabled and the signer cannot register to sign. |
| 215 | 'errors' => [['message' => $this->identifyService->getL10n()->t('It is not possible to create new accounts.')]], |
| 216 | ])); |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | private function throwIfInvalidEmail(): void { |
| 221 | if (!filter_var($this->entity->getIdentifierValue(), FILTER_VALIDATE_EMAIL)) { |
| 222 | // TRANSLATORS Error shown when the email address used to identify the signer is invalid. |
| 223 | throw new LibresignException($this->identifyService->getL10n()->t('Invalid email')); |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | #[\Override] |
| 228 | public function getSettings(): array { |
| 229 | if (!empty($this->settings)) { |
| 230 | return $this->settings; |
| 231 | } |
| 232 | $this->settings = parent::getSettingsFromDatabase( |
| 233 | default: [ |
| 234 | 'enabled' => false, |
| 235 | 'can_create_account' => false, |
| 236 | ], |
| 237 | immutable: [ |
| 238 | 'test_url' => $this->identifyService->getUrlGenerator()->linkToRoute('settings.MailSettings.sendTestMail'), |
| 239 | ] |
| 240 | ); |
| 241 | return $this->settings; |
| 242 | } |
| 243 | |
| 244 | #[\Override] |
| 245 | public function getDefaultSettings(): array { |
| 246 | $settings = parent::getDefaultSettings(); |
| 247 | $settings['enabled'] = false; |
| 248 | $settings['can_create_account'] = false; |
| 249 | $settings['test_url'] = $this->identifyService->getUrlGenerator()->linkToRoute('settings.MailSettings.sendTestMail'); |
| 250 | return $settings; |
| 251 | } |
| 252 | } |