diff --git a/composer.lock b/composer.lock index 69aa85138e..172ee3934c 100644 --- a/composer.lock +++ b/composer.lock @@ -1756,12 +1756,12 @@ "source": { "type": "git", "url": "https://github.com/nextcloud-deps/ocp.git", - "reference": "6a4c892db2749bf19aa3b83a4c2673a2302fa46e" + "reference": "bdcadd5d25136be604f48c963109a56e2b478b27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/6a4c892db2749bf19aa3b83a4c2673a2302fa46e", - "reference": "6a4c892db2749bf19aa3b83a4c2673a2302fa46e", + "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/bdcadd5d25136be604f48c963109a56e2b478b27", + "reference": "bdcadd5d25136be604f48c963109a56e2b478b27", "shasum": "" }, "require": { @@ -1793,7 +1793,7 @@ "issues": "https://github.com/nextcloud-deps/ocp/issues", "source": "https://github.com/nextcloud-deps/ocp/tree/master" }, - "time": "2024-02-01T00:33:54+00:00" + "time": "2024-02-02T00:32:33+00:00" }, { "name": "psr/clock", @@ -1949,12 +1949,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "e3b3cce1f2454ee4575500e084211b11efdaf64b" + "reference": "2f3b470e6ca356a27bf10b2b439c3683d20bebc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/e3b3cce1f2454ee4575500e084211b11efdaf64b", - "reference": "e3b3cce1f2454ee4575500e084211b11efdaf64b", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/2f3b470e6ca356a27bf10b2b439c3683d20bebc1", + "reference": "2f3b470e6ca356a27bf10b2b439c3683d20bebc1", "shasum": "" }, "conflict": { @@ -2008,6 +2008,7 @@ "bolt/bolt": "<3.7.2", "bolt/core": "<=4.2", "bottelet/flarepoint": "<2.2.1", + "bref/bref": "<2.1.13", "brightlocal/phpwhois": "<=4.2.5", "brotkrueml/codehighlight": "<2.7", "brotkrueml/schema": "<1.13.1|>=2,<2.5.1", @@ -2238,7 +2239,7 @@ "liftkit/database": "<2.13.2", "limesurvey/limesurvey": "<3.27.19", "livehelperchat/livehelperchat": "<=3.91", - "livewire/livewire": ">2.2.4,<2.2.6", + "livewire/livewire": ">2.2.4,<2.2.6|>=3,<3.0.4", "lms/routes": "<2.1.1", "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2", "luyadev/yii-helpers": "<1.2.1", @@ -2254,7 +2255,7 @@ "marcwillmann/turn": "<0.3.3", "matyhtf/framework": "<3.0.6", "mautic/core": "<4.3", - "mediawiki/core": ">=1.27,<1.27.6|>=1.29,<1.29.3|>=1.30,<1.30.2|>=1.31,<1.31.9|>=1.32,<1.32.6|>=1.32.99,<1.33.3|>=1.33.99,<1.34.3|>=1.34.99,<1.35", + "mediawiki/core": "<1.36.2", "mediawiki/matomo": "<2.4.3", "mediawiki/semantic-media-wiki": "<4.0.2", "melisplatform/melis-asset-manager": "<5.0.1", @@ -2457,7 +2458,7 @@ "spoonity/tcpdf": "<6.2.22", "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", "ssddanbrown/bookstack": "<22.02.3", - "statamic/cms": "<4.36", + "statamic/cms": "<4.46", "stormpath/sdk": "<9.9.99", "studio-42/elfinder": "<2.1.62", "subhh/libconnect": "<7.0.8|>=8,<8.1", @@ -2673,7 +2674,7 @@ "type": "tidelift" } ], - "time": "2024-01-31T19:04:19+00:00" + "time": "2024-02-02T13:04:17+00:00" } ], "aliases": [], diff --git a/lib/Controller/AccountController.php b/lib/Controller/AccountController.php index bed434dfe7..2831e63430 100644 --- a/lib/Controller/AccountController.php +++ b/lib/Controller/AccountController.php @@ -37,6 +37,7 @@ use OCA\Libresign\Service\AccountFileService; use OCA\Libresign\Service\AccountService; use OCA\Libresign\Service\SessionService; +use OCA\Libresign\Service\SignerElementsService; use OCA\Libresign\Service\SignFileService; use OCP\Accounts\IAccountManager; use OCP\AppFramework\ApiController; @@ -68,6 +69,7 @@ public function __construct( private AccountFileService $accountFileService, private AccountFileMapper $accountFileMapper, protected SignFileService $signFileService, + private SignerElementsService $signerElementsService, private Pkcs12Handler $pkcs12Handler, private Chain $loginChain, private IURLGenerator $urlGenerator, @@ -280,8 +282,8 @@ public function createSignatureElement(array $elements, string $uuid): JSONRespo 'elements' => ( $this->userSession->getUser() instanceof IUser - ? $this->accountService->getUserElements($this->userSession->getUser()->getUID()) - : $this->accountService->getElementsFromSession($this->sessionService->getSessionId()) + ? $this->signerElementsService->getUserElements($this->userSession->getUser()->getUID()) + : $this->signerElementsService->getElementsFromSessionAsArray() ), ], Http::STATUS_OK @@ -298,8 +300,8 @@ public function getSignatureElements(): JSONResponse { 'elements' => ( $userId - ? $this->accountService->getUserElements($userId) - : $this->accountService->getElementsFromSession($this->sessionService->getSessionId()) + ? $this->signerElementsService->getUserElements($userId) + : $this->signerElementsService->getElementsFromSessionAsArray() ) ], Http::STATUS_OK @@ -328,8 +330,8 @@ public function getSignatureElementPreview(int $fileId) { } $preview = $this->preview->getPreview( file: $node, - width: AccountService::ELEMENT_SIGN_WIDTH, - height: AccountService::ELEMENT_SIGN_HEIGHT, + width: SignerElementsService::ELEMENT_SIGN_WIDTH, + height: SignerElementsService::ELEMENT_SIGN_HEIGHT, ); $response = new FileDisplayResponse($preview, Http::STATUS_OK, [ 'Content-Type' => $preview->getMimeType(), @@ -343,7 +345,7 @@ public function getSignatureElement(int $elementId): JSONResponse { $userId = $this->userSession->getUser()->getUID(); try { return new JSONResponse( - $this->accountService->getUserElementByElementId($userId, $elementId), + $this->signerElementsService->getUserElementByElementId($userId, $elementId), Http::STATUS_OK ); } catch (\Throwable $th) { diff --git a/lib/Controller/FileElementController.php b/lib/Controller/FileElementController.php index 75f7e7a814..16b2d44668 100644 --- a/lib/Controller/FileElementController.php +++ b/lib/Controller/FileElementController.php @@ -94,7 +94,7 @@ public function delete(string $uuid, int $elementId): JSONResponse { 'uuid' => $uuid, 'userManager' => $this->userSession->getUser() ]); - $this->validateHelper->validateUserIsOwnerOfPdfVisibleElement($elementId, $this->userSession->getUser()->getUID()); + $this->validateHelper->validateAuthenticatedUserIsOwnerOfPdfVisibleElement($elementId, $this->userSession->getUser()->getUID()); $this->fileElementService->deleteVisibleElement($elementId); $return = []; $statusCode = Http::STATUS_OK; diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index f718e9c0d4..e369343ac8 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -33,10 +33,11 @@ use OCA\Libresign\Middleware\Attribute\RequireSignRequestUuid; use OCA\Libresign\Service\AccountService; use OCA\Libresign\Service\FileService; +use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\TokenService; use OCA\Libresign\Service\IdentifyMethodService; use OCA\Libresign\Service\RequestSignatureService; use OCA\Libresign\Service\SessionService; -use OCA\Libresign\Service\SignatureMethodService; +use OCA\Libresign\Service\SignerElementsService; use OCA\Libresign\Service\SignFileService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; @@ -56,7 +57,6 @@ use OCP\IURLGenerator; use OCP\IUserSession; use OCP\Util; -use Wobeto\EmailBlur\Blur; class PageController extends AEnvironmentPageAwareController { public function __construct( @@ -67,9 +67,9 @@ public function __construct( private AccountService $accountService, protected SignFileService $signFileService, protected RequestSignatureService $requestSignatureService, + private SignerElementsService $signerElementsService, protected IL10N $l10n, private IdentifyMethodService $identifyMethodService, - private SignatureMethodService $signatureMethodService, private IAppConfig $appConfig, private FileService $fileService, private ValidateHelper $validateHelper, @@ -148,9 +148,6 @@ public function sign($uuid): TemplateResponse { $this->getSignRequestEntity(), ) ); - $this->initialState->provideInitialState('identifyMethods', - $this->signFileService->getAvailableIdentifyMethodsFromSignRequest($this->getSignRequestEntity()) - ); $this->initialState->provideInitialState('filename', $this->getFileEntity()->getName()); $file = $this->fileService ->setFile($this->getFileEntity()) @@ -165,10 +162,9 @@ public function sign($uuid): TemplateResponse { $this->initialState->provideInitialState('visibleElements', $file['visibleElements']); $this->initialState->provideInitialState('signers', $file['signers']); $this->provideSignerSignatues(); - $signatureMethods = $this->signatureMethodService->getMethods(); - $this->provideBlurredEmail($signatureMethods, $this->userSession->getUser()?->getEMailAddress()); + $signatureMethods = $this->identifyMethodService->getSignMethodsOfIdentifiedFactors($this->getSignRequestEntity()->getId()); $this->initialState->provideInitialState('signature_methods', $signatureMethods); - $this->initialState->provideInitialState('token_length', SignatureMethodService::TOKEN_LENGTH); + $this->initialState->provideInitialState('token_length', TokenService::TOKEN_LENGTH); $this->initialState->provideInitialState('description', $this->getSignRequestEntity()->getDescription() ?? ''); $this->initialState->provideInitialState('pdf', $this->signFileService->getFileUrl('url', $this->getFileEntity(), $this->getNextcloudFile(), $uuid) @@ -187,32 +183,13 @@ public function sign($uuid): TemplateResponse { private function provideSignerSignatues(): void { $signatures = []; if ($this->userSession->getUser()) { - $signatures = $this->accountService->getUserElements($this->userSession->getUser()->getUID()); + $signatures = $this->signerElementsService->getUserElements($this->userSession->getUser()->getUID()); } else { - $signatures = $this->accountService->getElementsFromSession($this->sessionService->getSessionId()); + $signatures = $this->signerElementsService->getElementsFromSessionAsArray(); } $this->initialState->provideInitialState('user_signatures', $signatures); } - private function provideBlurredEmail(array $signatureMethods, ?string $email): void { - if (empty($email)) { - foreach ($signatureMethods as $id => $method) { - if ($id === IdentifyMethodService::IDENTIFY_EMAIL) { - $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($this->getSignRequestEntity()->getId()); - if (isset($identifyMethods[IdentifyMethodService::IDENTIFY_EMAIL])) { - $method = current($identifyMethods[IdentifyMethodService::IDENTIFY_EMAIL]); - $email = $method->getEntity()->getIdentifierValue(); - break; - } - } - } - } - if (!empty($email)) { - $blur = new Blur($email); - $this->initialState->provideInitialState('blurred_email', $blur->make()); - } - } - /** * Show signature page */ @@ -255,10 +232,9 @@ public function signAccountFile($uuid): TemplateResponse { $this->initialState->provideInitialState('visibleElements', []); $this->initialState->provideInitialState('signers', []); $this->provideSignerSignatues(); - $signatureMethods = $this->signatureMethodService->getMethods(); - $this->provideBlurredEmail($signatureMethods, $this->userSession->getUser()?->getEMailAddress()); + $signatureMethods = $this->identifyMethodService->getSignMethodsOfIdentifiedFactors($this->getSignRequestEntity()->getId()); $this->initialState->provideInitialState('signature_methods', $signatureMethods); - $this->initialState->provideInitialState('token_length', SignatureMethodService::TOKEN_LENGTH); + $this->initialState->provideInitialState('token_length', TokenService::TOKEN_LENGTH); $this->initialState->provideInitialState('description', ''); $nextcloudFile = $this->signFileService->getNextcloudFile($fileEntity->getNodeId()); $this->initialState->provideInitialState('pdf', diff --git a/lib/Controller/SignFileController.php b/lib/Controller/SignFileController.php index 134fdd915d..33c89add74 100644 --- a/lib/Controller/SignFileController.php +++ b/lib/Controller/SignFileController.php @@ -34,7 +34,6 @@ use OCA\Libresign\Middleware\Attribute\RequireManager; use OCA\Libresign\Middleware\Attribute\RequireSigner; use OCA\Libresign\Service\FileService; -use OCA\Libresign\Service\SignatureMethodService; use OCA\Libresign\Service\SignFileService; use OCA\TwoFactorGateway\Exception\SmsTransmissionException; use OCP\AppFramework\Http; @@ -57,7 +56,6 @@ public function __construct( protected IUserSession $userSession, private ValidateHelper $validateHelper, protected SignFileService $signFileService, - protected SignatureMethodService $signatureMethodService, private FileService $fileService, protected LoggerInterface $logger ) { @@ -67,6 +65,7 @@ public function __construct( #[NoAdminRequired] #[NoCSRFRequired] #[RequireManager] + #[PublicPage] public function signUsingFileId(int $fileId, string $method, array $elements = [], string $identifyValue = '', string $token = ''): JSONResponse { return $this->sign($fileId, null, $method, $elements, $identifyValue, $token); } @@ -74,6 +73,7 @@ public function signUsingFileId(int $fileId, string $method, array $elements = [ #[NoAdminRequired] #[NoCSRFRequired] #[RequireSigner] + #[PublicPage] public function signUsingUuid(string $uuid, string $method, array $elements = [], string $identifyValue = '', string $token = ''): JSONResponse { return $this->sign(null, $uuid, $method, $elements, $identifyValue, $token); } @@ -83,16 +83,16 @@ public function sign(int $fileId = null, string $signRequestUuid = null, string $user = $this->userSession->getUser(); $this->validateHelper->canSignWithIdentificationDocumentStatus( $user, - $this->fileService->getIdentificationDocumentsStatus($user->getUID()) + $this->fileService->getIdentificationDocumentsStatus($user?->getUID()) ); $libreSignFile = $this->signFileService->getLibresignFile($fileId, $signRequestUuid); - $signRequest = $this->signFileService->getSignRequestToSign($libreSignFile, $user); + $signRequest = $this->signFileService->getSignRequestToSign($libreSignFile, $signRequestUuid, $user); $this->validateHelper->validateVisibleElementsRelation($elements, $signRequest, $user); $this->validateHelper->validateCredentials($signRequest, $user, $method, $identifyValue, $token); if ($method === 'password') { $this->signFileService->setPassword($identifyValue); } else { - $this->signFileService->setSignWithoutPassword(false); + $this->signFileService->setSignWithoutPassword(true); } $this->signFileService ->setLibreSignFile($libreSignFile) @@ -170,6 +170,7 @@ public function signRenew(string $method): JSONResponse { #[NoAdminRequired] #[NoCSRFRequired] #[RequireSigner] + #[PublicPage] public function getCodeUsingUuid(string $uuid): JSONResponse { return $this->getCode($uuid); } @@ -177,10 +178,14 @@ public function getCodeUsingUuid(string $uuid): JSONResponse { #[NoAdminRequired] #[NoCSRFRequired] #[RequireSigner] + #[PublicPage] public function getCodeUsingFileId(int $fileId): JSONResponse { return $this->getCode(null, $fileId); } + /** + * @todo validate if can request code + */ private function getCode(string $uuid = null, int $fileId = null): JSONResponse { try { try { @@ -192,12 +197,12 @@ private function getCode(string $uuid = null, int $fileId = null): JSONResponse } catch (\Throwable $th) { throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1); } - $this->validateHelper->canRequestCode(); $libreSignFile = $this->fileMapper->getById($signRequest->getFileId()); $this->validateHelper->fileCanBeSigned($libreSignFile); $this->signFileService->requestCode( signRequest: $signRequest, - method: $this->request->getParam('method', ''), + identifyMethodName: $this->request->getParam('identifyMethod', ''), + signMethodName: $this->request->getParam('signMethod', ''), identify: $this->request->getParam('identify', ''), ); $message = $this->l10n->t('The code to sign file was successfully requested.'); diff --git a/lib/DataObjects/VisibleElementAssoc.php b/lib/DataObjects/VisibleElementAssoc.php index 2386856acb..9b42a481a3 100644 --- a/lib/DataObjects/VisibleElementAssoc.php +++ b/lib/DataObjects/VisibleElementAssoc.php @@ -25,30 +25,18 @@ namespace OCA\Libresign\DataObjects; use OCA\Libresign\Db\FileElement; -use OCA\Libresign\Db\UserElement; class VisibleElementAssoc { - /** @var FileElement */ - private $fileElement; - /** @var UserElement */ - private $userElement; - /** @var string */ - private $tempFile; - - public function __construct(FileElement $fileElement, UserElement $userElement, string $tempFile) { - $this->fileElement = $fileElement; - $this->userElement = $userElement; - $this->tempFile = $tempFile; + public function __construct( + private FileElement $fileElement, + private string $tempFile, + ) { } public function getFileElement(): FileElement { return $this->fileElement; } - public function getUserElement(): UserElement { - return $this->userElement; - } - public function getTempFile(): string { return $this->tempFile; } diff --git a/lib/Helper/ValidateHelper.php b/lib/Helper/ValidateHelper.php index 5f6ee2e363..b003859810 100644 --- a/lib/Helper/ValidateHelper.php +++ b/lib/Helper/ValidateHelper.php @@ -40,7 +40,6 @@ use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Service\FileService; use OCA\Libresign\Service\IdentifyMethodService; -use OCA\Libresign\Service\SignatureMethodService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Services\IAppConfig; use OCP\Files\Config\IUserMountCache; @@ -54,7 +53,7 @@ use OCP\Security\IHasher; class ValidateHelper { - /** @var \OCP\Files\File[] */ + /** @var \OCP\Files\Node[] */ private $file = []; public const TYPE_TO_SIGN = 1; @@ -78,7 +77,6 @@ public function __construct( private UserElementMapper $userElementMapper, private IdentifyMethodMapper $identifyMethodMapper, private IdentifyMethodService $identifyMethodService, - private SignatureMethodService $signatureMethodService, private IMimeTypeDetector $mimeTypeDetector, private IHasher $hasher, private IAppConfig $appConfig, @@ -292,25 +290,29 @@ public function validateElementType(array $element): void { } } - public function validateVisibleElementsRelation(array $list, SignRequest $signRequest, IUser $user): void { + public function validateVisibleElementsRelation(array $list, SignRequest $signRequest, ?IUser $user): void { foreach ($list as $elements) { if (!array_key_exists('documentElementId', $elements)) { throw new LibresignException($this->l10n->t('Field %s not found', ['documentElementId'])); } - if (!array_key_exists('profileElementId', $elements)) { - throw new LibresignException($this->l10n->t('Field %s not found', ['profileElementId'])); + if (!array_key_exists('profileElementId', $elements) + && !array_key_exists('profileFileId', $elements) + ) { + throw new LibresignException($this->l10n->t('Field %s not found', ['profileElementId, profileFileId'])); } - $this->validateUserIsOwnerOfPdfVisibleElement($elements['documentElementId'], $user->getUID()); - try { - $this->userElementMapper->findOne(['id' => $elements['profileElementId'], 'user_id' => $user->getUID()]); - } catch (\Throwable $th) { - throw new LibresignException($this->l10n->t('Field %s does not belong to user', $elements['profileElementId'])); + $this->validateSignerIsOwnerOfPdfVisibleElement($elements['documentElementId'], $signRequest); + if ($user instanceof IUser) { + try { + $this->userElementMapper->findOne(['id' => $elements['profileElementId'], 'user_id' => $user->getUID()]); + } catch (\Throwable $th) { + throw new LibresignException($this->l10n->t('Field %s does not belong to user', $elements['profileElementId'])); + } } } $this->validateUserHasNecessaryElements($signRequest, $user, $list); } - private function validateUserHasNecessaryElements(SignRequest $signRequest, IUser $user, array $list = []): void { + private function validateUserHasNecessaryElements(SignRequest $signRequest, ?IUser $user, array $list = []): void { $fileElements = $this->fileElementMapper->getByFileIdAndSignRequestId($signRequest->getFileId(), $signRequest->getId()); $total = array_filter($fileElements, function (FileElement $fileElement) use ($list, $user, $signRequest): bool { $found = array_filter($list, function ($item) use ($fileElement): bool { @@ -318,6 +320,9 @@ private function validateUserHasNecessaryElements(SignRequest $signRequest, IUse }); if (!$found) { try { + if (!$user instanceof $user) { + throw new \Exception(); + } $this->userElementMapper->findMany([ 'user_id' => $user->getUID(), 'type' => $fileElement->getType(), @@ -334,7 +339,14 @@ private function validateUserHasNecessaryElements(SignRequest $signRequest, IUse } } - public function validateUserIsOwnerOfPdfVisibleElement(int $documentElementId, string $uid): void { + private function validateSignerIsOwnerOfPdfVisibleElement(int $documentElementId, SignRequest $signRequest): void { + $documentElement = $this->fileElementMapper->getById($documentElementId); + if ($documentElement->getSignRequestId() !== $signRequest->getId()) { + throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1); + } + } + + public function validateAuthenticatedUserIsOwnerOfPdfVisibleElement(int $documentElementId, string $uid): void { try { $documentElement = $this->fileElementMapper->getById($documentElementId); $signRequest = $this->signRequestMapper->getById($documentElement->getSignRequestId()); @@ -429,11 +441,12 @@ private function getLibreSignFileByNodeId(int $nodeId) { $libresignFile = $this->fileMapper->getByFileId($nodeId); $userFolder = $this->root->getUserFolder($libresignFile->getUserId()); - $this->file[$nodeId] = $userFolder->getById($nodeId); - if (!empty($this->file[$nodeId])) { - $this->file[$nodeId] = $this->file[$nodeId][0]; + $files = $userFolder->getById($nodeId); + if (!empty($files)) { + $this->file[$nodeId] = $files[0]; + return $this->file[$nodeId]; } - return $this->file[$nodeId]; + return []; } public function canRequestSign(IUser $user): void { @@ -644,7 +657,7 @@ private function validateIdentifyMethod(string $uuid, ?IUser $user = null): void foreach ($identifyMethods as $methods) { foreach ($methods as $identifyMethod) { $identifyMethod->setUser($user); - $identifyMethod->validateToSign(); + $identifyMethod->validateToIdentify(); } } } @@ -692,15 +705,7 @@ public function validateUserHasNoFileWithThisType(string $uid, string $type): vo } } - public function canRequestCode(): void { - // @todo make the sign method to say if he can request code - $signatureMethods = $this->signatureMethodService->getMethods(); - if (!array_key_exists('email', $signatureMethods)) { - throw new LibresignException($this->l10n->t('You do not have permission for this action.')); - } - } - - public function canSignWithIdentificationDocumentStatus(IUser $user, int $status): void { + public function canSignWithIdentificationDocumentStatus(?IUser $user, int $status): void { // User that can approve validation documents don't need to have a valid // document attached to their profile. If this were required, nobody // would be able to sign any document @@ -716,7 +721,7 @@ public function canSignWithIdentificationDocumentStatus(IUser $user, int $status } } - public function validateCredentials(SignRequest $signRequest, IUser $user, string $identifyMethodName, string $identifyValue, string $token): void { + public function validateCredentials(SignRequest $signRequest, ?IUser $user, string $identifyMethodName, string $identifyValue, string $token): void { $this->validateIfIdentifyMethodExists($identifyMethodName); if ($signRequest->getId()) { $multidimensionalList = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId()); @@ -732,12 +737,12 @@ public function validateCredentials(SignRequest $signRequest, IUser $user, strin } else { $identifyMethod = $this->identifyMethodService->getInstanceOfIdentifyMethod($identifyMethodName, $identifyValue); } - if ($identifyMethod->getEntity()->getIdentifiedAtDate()) { + if ($signRequest->getSigned()) { throw new LibresignException($this->l10n->t('File already signed.')); } $identifyMethod->setUser($user); $identifyMethod->setCodeSentByUser($token); - $identifyMethod->validateToSign(); + $identifyMethod->validateToIdentify(); } public function validateIfIdentifyMethodExists($identifyMethod): void { diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php index 400e296aa4..b9d39f14bf 100644 --- a/lib/Service/AccountService.php +++ b/lib/Service/AccountService.php @@ -47,10 +47,8 @@ use OCP\Files\Config\IMountProviderCollection; use OCP\Files\Config\IUserMountCache; use OCP\Files\File; -use OCP\Files\Folder; use OCP\Files\IMimeTypeDetector; use OCP\Files\IRootFolder; -use OCP\Files\NotFoundException; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IGroupManager; @@ -62,15 +60,9 @@ use Throwable; class AccountService { - /** @var SignRequest */ - private $signRequest; - /** @var \OCA\Libresign\Db\File */ - private $fileData; - /** @var \OCA\Files\Node\File */ - private $fileToSign; - - public const ELEMENT_SIGN_WIDTH = 350; - public const ELEMENT_SIGN_HEIGHT = 100; + private ?SignRequest $signRequest = null; + private ?\OCA\Libresign\Db\File $fileData = null; + private \OCP\Files\File $fileToSign; public function __construct( private IL10N $l10n, @@ -135,7 +127,7 @@ public function validateCreateToSign(array $data): void { public function getFileByUuid(string $uuid): array { $signRequest = $this->getSignRequestByUuid($uuid); - if (!$this->fileData) { + if (!$this->fileData instanceof \OCA\Libresign\Db\File) { $this->fileData = $this->fileMapper->getById($signRequest->getFileId()); $nodeId = $this->fileData->getNodeId(); @@ -200,7 +192,7 @@ private function validateAccountFile(int $fileIndex, array $file, IUser $user): * Get signRequest by Uuid */ public function getSignRequestByUuid($uuid): SignRequest { - if (!$this->signRequest) { + if (!$this->signRequest instanceof SignRequest) { $this->signRequest = $this->signRequestMapper->getByUuid($uuid); } return $this->signRequest; @@ -468,96 +460,6 @@ private function getFileRaw(array $data) { return $content; } - public function getElementsFromSession(string $sessionId): array { - $folder = $this->folderService->getFolder(); - try { - /** @var Folder $signerFolder */ - $signerFolder = $folder->get($sessionId); - } catch (NotFoundException $th) { - return []; - } - $fileList = $signerFolder->getDirectoryListing(); - $return = []; - foreach ($fileList as $fileElement) { - list($type, $timestamp) = explode('_', pathinfo($fileElement->getName(), PATHINFO_FILENAME)); - $return[] = [ - 'type' => $type, - 'file' => [ - 'url' => $this->urlGenerator->linkToRoute('ocs.libresign.account.getSignatureElementPreview', [ - 'apiVersion' => 'v1', - 'fileId' => $fileElement->getId(), - ]), - 'fileId' => $fileElement->getId(), - ], - 'starred' => 0, - 'createdAt' => (new \DateTime())->setTimestamp((int) $timestamp)->format('Y-m-d H:i:s'), - ]; - } - return $return; - } - - /** - * @return ((int|string)[]|\DateTime|int|string)[][] - * - * @psalm-return list - */ - public function getUserElements(string $userId): array { - $elements = $this->userElementMapper->findMany(['user_id' => $userId]); - $return = []; - foreach ($elements as $element) { - $exists = $this->signatureFileExists($element); - if (!$exists) { - continue; - } - $return[] = [ - 'id' => $element->getId(), - 'type' => $element->getType(), - 'file' => [ - 'url' => $this->urlGenerator->linkToRoute('core.Preview.getPreviewByFileId', ['fileId' => $element->getFileId(), 'x' => self::ELEMENT_SIGN_WIDTH, 'y' => self::ELEMENT_SIGN_HEIGHT]), - 'fileId' => $element->getFileId() - ], - 'uid' => $element->getUserId(), - 'starred' => $element->getStarred() ? 1 : 0, - 'createdAt' => $element->getCreatedAt() - ]; - } - return $return; - } - - private function signatureFileExists(UserElement $userElement): bool { - try { - $this->folderService->getFolder($userElement->getFileId()); - } catch (\Exception $e) { - $this->userElementMapper->delete($userElement); - return false; - } - return true; - } - - /** - * @return ((int|string)[]|\DateTime|int|string)[] - * - * @psalm-return array{id?: int, type?: string, file?: array{url: string, fileId: int}, uid?: string, starred?: 0|1, createdAt?: \DateTime} - */ - public function getUserElementByElementId(string $userId, $elementId): array { - $element = $this->userElementMapper->findOne(['id' => $elementId, 'user_id' => $userId]); - $exists = $this->signatureFileExists($element); - if (!$exists) { - return []; - } - return [ - 'id' => $element->getId(), - 'type' => $element->getType(), - 'file' => [ - 'url' => $this->urlGenerator->linkToRoute('core.Preview.getPreviewByFileId', ['fileId' => $element->getFileId(), 'x' => self::ELEMENT_SIGN_WIDTH, 'y' => self::ELEMENT_SIGN_HEIGHT]), - 'fileId' => $element->getFileId() - ], - 'uid' => $element->getUserId(), - 'starred' => $element->getStarred() ? 1 : 0, - 'createdAt' => $element->getCreatedAt() - ]; - } - public function deleteSignatureElement(string $userId, int $elementId): void { $element = $this->userElementMapper->findOne(['id' => $elementId, 'user_id' => $userId]); $this->userElementMapper->delete($element); diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index fce5eb1753..1f43203195 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -327,13 +327,15 @@ private function getSettings(): array { return $this->settings; } - public function getIdentificationDocumentsStatus(string $userId): int { + public function getIdentificationDocumentsStatus(?string $userId): int { if (!$this->appConfig->getAppValue('identification_documents', '')) { return self::IDENTIFICATION_DOCUMENTS_DISABLED; } - $files = $this->fileMapper->getFilesOfAccount($userId); - if (!count($files)) { + if (!empty($userId)) { + $files = $this->fileMapper->getFilesOfAccount($userId); + } + if (empty($files) || !count($files)) { return self::IDENTIFICATION_DOCUMENTS_NEED_SEND; } $deleted = array_filter($files, function (File $file) { diff --git a/lib/Service/IdentifyMethod/AbstractIdentifyMethod.php b/lib/Service/IdentifyMethod/AbstractIdentifyMethod.php index 99cccff902..930746a849 100644 --- a/lib/Service/IdentifyMethod/AbstractIdentifyMethod.php +++ b/lib/Service/IdentifyMethod/AbstractIdentifyMethod.php @@ -26,61 +26,45 @@ use InvalidArgumentException; use OCA\Libresign\Db\File as FileEntity; -use OCA\Libresign\Db\FileMapper; use OCA\Libresign\Db\IdentifyMethod; -use OCA\Libresign\Db\IdentifyMethodMapper; -use OCA\Libresign\Db\SignRequestMapper; use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Helper\JSActions; +use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\AbstractSignatureMethod; use OCA\Libresign\Service\SessionService; -use OCP\AppFramework\Services\IAppConfig; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\Files\Config\IUserMountCache; -use OCP\Files\IRootFolder; -use OCP\IL10N; -use OCP\IURLGenerator; use OCP\IUser; -use OCP\IUserManager; -use OCP\Security\IHasher; -use Psr\Log\LoggerInterface; use Wobeto\EmailBlur\Blur; abstract class AbstractIdentifyMethod implements IIdentifyMethod { - protected bool $canCreateAccount = true; protected IdentifyMethod $entity; protected string $name; - public string $friendlyName; + protected string $friendlyName; protected ?IUser $user = null; protected string $codeSentByUser = ''; protected array $settings = []; protected bool $willNotify = true; + /** + * @var AbstractSignatureMethod[] + */ + protected array $signatureMethods = []; public function __construct( - private IAppConfig $appConfig, - private IL10N $l10n, - private IdentifyMethodMapper $identifyMethodMapper, - private SignRequestMapper $signRequestMapper, - private FileMapper $fileMapper, - private IRootFolder $root, - private IHasher $hasher, - private IUserManager $userManager, - private IURLGenerator $urlGenerator, - private IUserMountCache $userMountCache, - private ITimeFactory $timeFactory, - private LoggerInterface $logger, - private SessionService $sessionService, + protected IdentifyMethodService $identifyMethodService, ) { $className = (new \ReflectionClass($this))->getShortName(); $this->name = lcfirst($className); $this->cleanEntity(); } + public static function getId(): string { + $id = lcfirst(substr(strrchr(get_called_class(), '\\'), 1)); + return $id; + } + public function getName(): string { return $this->name; } - public function isEnabledAsSignatueMethod(): bool { - $settings = $this->getSettings(); - return $settings['enabled_as_signature_method']; + public function getFriendlyName(): string { + return $this->friendlyName; } public function setCodeSentByUser(string $code): void { @@ -104,6 +88,19 @@ public function getEntity(): IdentifyMethod { return $this->entity; } + public function signatureMethodsToArray(): array { + return array_map(function (AbstractSignatureMethod $method) { + return [ + 'label' => $method->friendlyName, + 'enabled' => $method->isEnabled(), + ]; + }, $this->signatureMethods); + } + + public function getSignatureMethods(): array { + return $this->signatureMethods; + } + public function getSettings(): array { $this->getSettingsFromDatabase(); return $this->settings; @@ -122,39 +119,39 @@ public function validateToRequest(): void { public function validateToCreateAccount(string $value): void { } - public function validateToSign(): void { + public function validateToIdentify(): void { } protected function throwIfFileNotFound(): void { - $signRequest = $this->signRequestMapper->getById($this->getEntity()->getSignRequestId()); - $fileEntity = $this->fileMapper->getById($signRequest->getFileId()); + $signRequest = $this->identifyMethodService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId()); + $fileEntity = $this->identifyMethodService->getFileMapper()->getById($signRequest->getFileId()); $nodeId = $fileEntity->getNodeId(); - $mountsContainingFile = $this->userMountCache->getMountsForFileId($nodeId); + $mountsContainingFile = $this->identifyMethodService->getUserMountCache()->getMountsForFileId($nodeId); foreach ($mountsContainingFile as $fileInfo) { - $this->root->getByIdInPath($nodeId, $fileInfo->getMountPoint()); + $this->identifyMethodService->getRootFolder()->getByIdInPath($nodeId, $fileInfo->getMountPoint()); } - $fileToSign = $this->root->getById($nodeId); + $fileToSign = $this->identifyMethodService->getRootFolder()->getById($nodeId); if (count($fileToSign) < 1) { throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_DO_NOTHING, - 'errors' => [$this->l10n->t('File not found')], + 'errors' => [$this->identifyMethodService->getL10n()->t('File not found')], ])); } } protected function throwIfMaximumValidityExpired(): void { - $maximumValidity = (int) $this->appConfig->getAppValue('maximum_validity', (string) SessionService::NO_MAXIMUM_VALIDITY); + $maximumValidity = (int) $this->identifyMethodService->getAppConfig()->getAppValue('maximum_validity', (string) SessionService::NO_MAXIMUM_VALIDITY); if ($maximumValidity <= 0) { return; } - $signRequest = $this->signRequestMapper->getById($this->getEntity()->getSignRequestId()); - $now = $this->timeFactory->getTime(); + $signRequest = $this->identifyMethodService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId()); + $now = $this->identifyMethodService->getTimeFactory()->getTime(); if ($signRequest->getCreatedAt() + $maximumValidity < $now) { throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_DO_NOTHING, - 'errors' => [$this->l10n->t('Link expired.')], + 'errors' => [$this->identifyMethodService->getL10n()->t('Link expired.')], ])); } } @@ -163,36 +160,37 @@ protected function throwIfInvalidToken(): void { if (empty($this->codeSentByUser)) { return; } - if (!$this->hasher->verify($this->codeSentByUser, $this->getEntity()->getCode())) { - throw new LibresignException($this->l10n->t('Invalid code.')); + if (!$this->identifyMethodService->getHasher()->verify($this->codeSentByUser, $this->getEntity()->getCode())) { + throw new LibresignException($this->identifyMethodService->getL10n()->t('Invalid code.')); } } protected function renewSession(): void { - $this->sessionService->setIdentifyMethodId($this->getEntity()->getId()); - $renewalInterval = (int) $this->appConfig->getAppValue('renewal_interval', (string) SessionService::NO_RENEWAL_INTERVAL); + $this->identifyMethodService->getSessionService()->setIdentifyMethodId($this->getEntity()->getId()); + $renewalInterval = (int) $this->identifyMethodService->getAppConfig()->getAppValue('renewal_interval', (string) SessionService::NO_RENEWAL_INTERVAL); if ($renewalInterval <= 0) { return; } - $this->sessionService->resetDurationOfSignPage(); + $this->identifyMethodService->getSessionService()->resetDurationOfSignPage(); } protected function updateIdentifiedAt(): void { if ($this->getEntity()->getCode() && !$this->getEntity()->getIdentifiedAtDate()) { return; } - $this->getEntity()->setIdentifiedAtDate($this->timeFactory->getDateTime()); + $this->getEntity()->setIdentifiedAtDate($this->identifyMethodService->getTimeFactory()->getDateTime()); $this->willNotify = false; - $this->save(); + $isNew = $this->identifyMethodService->save($this->getEntity()); + $this->notify($isNew); } protected function throwIfRenewalIntervalExpired(): void { - $renewalInterval = (int) $this->appConfig->getAppValue('renewal_interval', (string) SessionService::NO_RENEWAL_INTERVAL); + $renewalInterval = (int) $this->identifyMethodService->getAppConfig()->getAppValue('renewal_interval', (string) SessionService::NO_RENEWAL_INTERVAL); if ($renewalInterval <= 0) { return; } - $signRequest = $this->signRequestMapper->getById($this->getEntity()->getSignRequestId()); - $startTime = $this->sessionService->getSignStartTime(); + $signRequest = $this->identifyMethodService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId()); + $startTime = $this->identifyMethodService->getSessionService()->getSignStartTime(); $createdAt = $signRequest->getCreatedAt(); $lastAttempt = $this->getEntity()->getLastAttemptDate()?->format('U'); $lastActionDate = max( @@ -200,8 +198,8 @@ protected function throwIfRenewalIntervalExpired(): void { $createdAt, $lastAttempt, ); - $now = $this->timeFactory->getTime(); - $this->logger->debug('AbstractIdentifyMethod::throwIfRenewalIntervalExpired Times', [ + $now = $this->identifyMethodService->getTimeFactory()->getTime(); + $this->identifyMethodService->getLogger()->debug('AbstractIdentifyMethod::throwIfRenewalIntervalExpired Times', [ 'renewalInterval' => $renewalInterval, 'startTime' => $startTime, 'createdAt' => $createdAt, @@ -210,13 +208,13 @@ protected function throwIfRenewalIntervalExpired(): void { 'now' => $now, ]); if ($lastActionDate + $renewalInterval < $now) { - $this->logger->debug('AbstractIdentifyMethod::throwIfRenewalIntervalExpired Exception'); + $this->identifyMethodService->getLogger()->debug('AbstractIdentifyMethod::throwIfRenewalIntervalExpired Exception'); $blur = new Blur($this->getEntity()->getIdentifierValue()); throw new LibresignException(json_encode([ 'action' => $this->getRenewAction(), // TRANSLATORS title that is displayed at screen to notify the signer that the link to sign the document expired - 'title' => $this->l10n->t('Link expired'), - 'body' => $this->l10n->t(<<<'BODY' + 'title' => $this->identifyMethodService->getL10n()->t('Link expired'), + 'body' => $this->identifyMethodService->getL10n()->t(<<<'BODY' The link to sign the document has expired. We will send a new link to the email %1$s. Click below to receive the new link and be able to sign the document. @@ -225,26 +223,11 @@ protected function throwIfRenewalIntervalExpired(): void { ), 'uuid' => $signRequest->getUuid(), // TRANSLATORS Button to renew the link to sign the document. Renew is the action to generate a new sign link when the link expired. - 'renewButton' => $this->l10n->t('Renew'), + 'renewButton' => $this->identifyMethodService->getL10n()->t('Renew'), ])); } } - protected function throwIfNeedToCreateAccount() { - if (!$this->canCreateAccount) { - return; - } - if ($this->sessionService->getSignStartTime()) { - return; - } - $email = $this->getEntity()->getIdentifierValue(); - throw new LibresignException(json_encode([ - 'action' => JSActions::ACTION_CREATE_USER, - 'settings' => ['accountHash' => md5($email)], - 'message' => $this->l10n->t('You need to create an account to sign this file.'), - ])); - } - private function getRenewAction(): int { switch ($this->name) { case 'email': @@ -254,14 +237,14 @@ private function getRenewAction(): int { } protected function throwIfAlreadySigned(): void { - $signRequest = $this->signRequestMapper->getById($this->getEntity()->getSignRequestId()); - $fileEntity = $this->fileMapper->getById($signRequest->getFileId()); + $signRequest = $this->identifyMethodService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId()); + $fileEntity = $this->identifyMethodService->getFileMapper()->getById($signRequest->getFileId()); if ($fileEntity->getStatus() === FileEntity::STATUS_SIGNED || (!is_null($signRequest) && $signRequest->getSigned()) ) { throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_SHOW_ERROR, - 'errors' => [$this->l10n->t('File already signed.')], + 'errors' => [$this->identifyMethodService->getL10n()->t('File already signed.')], ])); } } @@ -270,124 +253,78 @@ protected function getSettingsFromDatabase(array $default = [], array $immutable if ($this->settings) { return $this->settings; } + $this->loadSavedSettings(); $default = array_merge( [ 'name' => $this->name, 'friendly_name' => $this->friendlyName, 'enabled' => true, - 'enabled_as_signature_method' => false, 'mandatory' => true, + 'signatureMethods' => $this->signatureMethodsToArray(), ], $default ); - $customConfig = $this->getSavedSettings(); - $customConfig = $this->removeKeysThatDontExists($customConfig, $default); - $customConfig = $this->overrideImmutable($customConfig, $immutable); - $customConfig = $this->getDefaultValues($customConfig, $default); - $this->settings = $customConfig; + $this->removeKeysThatDontExists($default); + $this->overrideImmutable($immutable); + $this->settings = $this->applyDefault($this->settings, $default); return $this->settings; } - private function overrideImmutable(array $customConfig, array $immutable) { - return array_merge($customConfig, $immutable); - } - - private function getSavedSettings(): array { - return array_merge( - $this->getSavedIdentifyMethodsSettings(), - $this->getSavedSignatureMethodsSettings(), - ); + private function overrideImmutable(array $immutable): void { + $this->settings = array_merge($this->settings, $immutable); } - private function getSavedIdentifyMethodsSettings(): array { - $config = $this->appConfig->getAppValue('identify_methods', '[]'); - $config = json_decode($config, true); - if (json_last_error() !== JSON_ERROR_NONE || !is_array($config)) { - return []; - } - $current = array_reduce($config, function ($carry, $config) { + private function loadSavedSettings(): void { + $config = $this->identifyMethodService->getSavedSettings(); + $this->settings = array_reduce($config, function ($carry, $config) { if ($config['name'] === $this->name) { return $config; } return $carry; }, []); - return $current; - } - - private function getSavedSignatureMethodsSettings(): array { - $config = $this->appConfig->getAppValue('signature_methods', '[]'); - $config = json_decode($config, true); - if (json_last_error() !== JSON_ERROR_NONE || !is_array($config)) { - return []; + if (!isset($this->settings['signatureMethods']) || !is_array($this->settings['signatureMethods'])) { + return; } - foreach ($config as $id => $method) { - if ($id !== $this->name) { - continue; + foreach ($this->settings['signatureMethods'] as $method => $settings) { + $this->signatureMethods[$method]->setEntity($this->getEntity()); + if (is_object($this->signatureMethods[$method]) && isset($settings['enabled']) && $settings['enabled']) { + $this->signatureMethods[$method]->enable(); } - return [ - 'enabled_as_signature_method' => array_key_exists('enabled', $method) && $method['enabled'], - ]; } - return []; } - private function getDefaultValues(array $customConfig, array $default): array { + private function applyDefault(array $customConfig, array $default): array { foreach ($default as $key => $value) { - if (!isset($customConfig[$key]) || gettype($value) !== gettype($customConfig[$key])) { + if (!isset($customConfig[$key])) { + $customConfig[$key] = $value; + } elseif (gettype($value) !== gettype($customConfig[$key])) { $customConfig[$key] = $value; + } elseif (gettype($value) === 'array') { + $customConfig[$key] = $this->applyDefault($customConfig[$key], $value); } } return $customConfig; } - private function removeKeysThatDontExists(array $customConfig, array $default): array { - $diff = array_diff_key($customConfig, $default); - foreach (array_keys($diff) as $invalidKey) { - unset($customConfig[$invalidKey]); - } - return $customConfig; - } - - public function validateToRenew(?IUser $user = null): void { - $this->throwIfMaximumValidityExpired(); - $this->throwIfAlreadySigned(); - $this->throwIfFileNotFound(); - } - public function save(): void { - $this->refreshIdFromDatabaseIfNecessary(); - if ($this->getEntity()->getId()) { - $this->identifyMethodMapper->update($this->getEntity()); - $this->notify(false); - } else { - $this->identifyMethodMapper->insertOrUpdate($this->getEntity()); - $this->notify(true); - } + $isNew = $this->identifyMethodService->save($this->getEntity()); + $this->notify($isNew); } public function delete(): void { - if ($this->getEntity()->getId()) { - $this->identifyMethodMapper->delete($this->getEntity()); - } + $this->identifyMethodService->delete($this->getEntity()); } - private function refreshIdFromDatabaseIfNecessary(): void { - $entity = $this->getEntity(); - if ($entity->getId()) { - return; - } - if (!$entity->getSignRequestId() || !$entity->getIdentifierKey()) { - return; + private function removeKeysThatDontExists(array $default): void { + $diff = array_diff_key($this->settings, $default); + foreach (array_keys($diff) as $invalidKey) { + unset($this->settings[$invalidKey]); } + } - $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($entity->getSignRequestId()); - $exists = array_filter($identifyMethods, function (IdentifyMethod $current) use ($entity): bool { - return $current->getIdentifierKey() === $entity->getIdentifierKey(); - }); - if (!$exists) { - return; - } - $exists = current($exists); - $entity->setId($exists->getId()); + public function validateToRenew(?IUser $user = null): void { + $this->throwIfMaximumValidityExpired(); + $this->throwIfAlreadySigned(); + $this->throwIfFileNotFound(); } } diff --git a/lib/Service/IdentifyMethod/Account.php b/lib/Service/IdentifyMethod/Account.php index 488cba5f98..7e3f15b929 100644 --- a/lib/Service/IdentifyMethod/Account.php +++ b/lib/Service/IdentifyMethod/Account.php @@ -24,20 +24,18 @@ namespace OCA\Libresign\Service\IdentifyMethod; -use OCA\Libresign\Db\FileMapper; use OCA\Libresign\Db\IdentifyMethodMapper; -use OCA\Libresign\Db\SignRequestMapper; use OCA\Libresign\Events\SendSignNotificationEvent; use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Helper\JSActions; +use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\ClickToSign; +use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\EmailToken; +use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\Password; use OCA\Libresign\Service\MailService; use OCA\Libresign\Service\SessionService; -use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\Config\IUserMountCache; use OCP\Files\IRootFolder; -use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; @@ -46,50 +44,40 @@ use Psr\Log\LoggerInterface; class Account extends AbstractIdentifyMethod { - public const ID = 'account'; public function __construct( - private IAppConfig $appConfig, - private IL10N $l10n, + protected IdentifyMethodService $identifyMethodService, private IUserManager $userManager, - private SignRequestMapper $signRequestMapper, private IEventDispatcher $eventDispatcher, private IdentifyMethodMapper $identifyMethodMapper, - private FileMapper $fileMapper, private IUserSession $userSession, private IURLGenerator $urlGenerator, private IRootFolder $root, private IHasher $hasher, - private IUserMountCache $userMountCache, private ITimeFactory $timeFactory, private LoggerInterface $logger, private SessionService $sessionService, - private MailService $mail + private MailService $mail, + private Password $password, + private ClickToSign $clickToSign, + private EmailToken $emailToken, ) { // TRANSLATORS Name of possible authenticator method. This signalize that the signer could be identified by Nextcloud acccount - $this->friendlyName = $this->l10n->t('Account'); + $this->friendlyName = $this->identifyMethodService->getL10n()->t('Account'); + $this->signatureMethods = [ + $this->password->getName() => $this->password, + $this->clickToSign->getName() => $this->clickToSign, + $this->emailToken->getName() => $this->emailToken, + ]; parent::__construct( - $appConfig, - $l10n, - $identifyMethodMapper, - $signRequestMapper, - $fileMapper, - $root, - $hasher, - $userManager, - $urlGenerator, - $userMountCache, - $timeFactory, - $logger, - $sessionService, + $identifyMethodService, ); - $this->getSettings(); } public function notify(bool $isNew): void { if (!$this->willNotify) { return; } - $signRequest = $this->signRequestMapper->getById($this->getEntity()->getSignRequestId()); + $signRequest = $this->identifyMethodService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId()); $this->eventDispatcher->dispatchTyped(new SendSignNotificationEvent( $signRequest, $this, @@ -100,11 +88,11 @@ public function notify(bool $isNew): void { public function validateToRequest(): void { $signer = $this->userManager->get($this->entity->getIdentifierValue()); if (!$signer) { - throw new LibresignException($this->l10n->t('User not found.')); + throw new LibresignException($this->identifyMethodService->getL10n()->t('User not found.')); } } - public function validateToSign(): void { + public function validateToIdentify(): void { $signer = $this->getSigner(); $this->throwIfNotAuthenticated($this->user); $this->authenticatedUserIsTheSigner($this->user, $signer); @@ -124,7 +112,7 @@ private function getSigner(): IUser { if (empty($signer) || count($signer) > 1) { throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_DO_NOTHING, - 'errors' => [$this->l10n->t('Invalid user')], + 'errors' => [$this->identifyMethodService->getL10n()->t('Invalid user')], ])); } $signer = current($signer); @@ -136,17 +124,17 @@ private function authenticatedUserIsTheSigner(IUser $user, IUser $signer): void if ($user !== $signer) { throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_DO_NOTHING, - 'errors' => [$this->l10n->t('Invalid user')], + 'errors' => [$this->identifyMethodService->getL10n()->t('Invalid user')], ])); } } private function throwIfNotAuthenticated(?IUser $user = null): void { if (!$user instanceof IUser) { - $signRequest = $this->signRequestMapper->getById($this->getEntity()->getSignRequestId()); + $signRequest = $this->identifyMethodService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId()); throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_REDIRECT, - 'errors' => [$this->l10n->t('You are not logged in. Please log in.')], + 'errors' => [$this->identifyMethodService->getL10n()->t('You are not logged in. Please log in.')], 'redirect' => $this->urlGenerator->linkToRoute('core.login.showLoginForm', [ 'redirect_url' => $this->urlGenerator->linkToRoute( 'libresign.page.sign', @@ -170,7 +158,7 @@ public function getSettings(): array { } private function isEnabledByDefault(): bool { - $config = $this->appConfig->getAppValue('identify_methods', '[]'); + $config = $this->identifyMethodService->getAppConfig()->getAppValue('identify_methods', '[]'); $config = json_decode($config, true); if (json_last_error() !== JSON_ERROR_NONE || !is_array($config)) { return true; diff --git a/lib/Service/IdentifyMethod/ClickToSign.php b/lib/Service/IdentifyMethod/ClickToSign.php deleted file mode 100644 index c7a326d753..0000000000 --- a/lib/Service/IdentifyMethod/ClickToSign.php +++ /dev/null @@ -1,79 +0,0 @@ - - * - * @author Vitor Mattos - * - * @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\Libresign\Service\IdentifyMethod; - -use OCA\Libresign\Db\FileMapper; -use OCA\Libresign\Db\IdentifyMethodMapper; -use OCA\Libresign\Db\SignRequestMapper; -use OCA\Libresign\Handler\Pkcs12Handler; -use OCA\Libresign\Service\MailService; -use OCA\Libresign\Service\SessionService; -use OCP\AppFramework\Services\IAppConfig; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\Files\Config\IUserMountCache; -use OCP\Files\IRootFolder; -use OCP\IL10N; -use OCP\IURLGenerator; -use OCP\IUserManager; -use OCP\Security\IHasher; -use Psr\Log\LoggerInterface; - -class ClickToSign extends AbstractIdentifyMethod { - public function __construct( - private IAppConfig $appConfig, - private IL10N $l10n, - private MailService $mail, - private SignRequestMapper $signRequestMapper, - private IdentifyMethodMapper $identifyMethodMapper, - private FileMapper $fileMapper, - private IUserManager $userManager, - private IURLGenerator $urlGenerator, - private IRootFolder $root, - private IHasher $hasher, - private IUserMountCache $userMountCache, - private ITimeFactory $timeFactory, - private LoggerInterface $logger, - private SessionService $sessionService, - private Pkcs12Handler $pkcs12Handler, - ) { - // TRANSLATORS Name of possible authenticator method. This signalize that the signer only need to click to sign after was identified - $this->friendlyName = $this->l10n->t('Click to sign'); - parent::__construct( - $appConfig, - $l10n, - $identifyMethodMapper, - $signRequestMapper, - $fileMapper, - $root, - $hasher, - $userManager, - $urlGenerator, - $userMountCache, - $timeFactory, - $logger, - $sessionService, - ); - } -} diff --git a/lib/Service/IdentifyMethod/Email.php b/lib/Service/IdentifyMethod/Email.php index 118f876455..410b0e3312 100644 --- a/lib/Service/IdentifyMethod/Email.php +++ b/lib/Service/IdentifyMethod/Email.php @@ -25,68 +25,45 @@ namespace OCA\Libresign\Service\IdentifyMethod; use OCA\Libresign\Db\FileElementMapper; -use OCA\Libresign\Db\FileMapper; use OCA\Libresign\Db\IdentifyMethodMapper; -use OCA\Libresign\Db\SignRequestMapper; use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Helper\JSActions; +use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\ClickToSign; +use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\EmailToken; use OCA\Libresign\Service\MailService; use OCA\Libresign\Service\SessionService; -use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; -use OCP\Files\Config\IUserMountCache; use OCP\Files\IRootFolder; -use OCP\IL10N; -use OCP\IURLGenerator; use OCP\IUser; -use OCP\IUserManager; -use OCP\Security\IHasher; -use Psr\Log\LoggerInterface; class Email extends AbstractIdentifyMethod { - public const ID = 'email'; public function __construct( - private IAppConfig $appConfig, - private IL10N $l10n, + protected IdentifyMethodService $identifyMethodService, private MailService $mail, - private SignRequestMapper $signRequestMapper, private IdentifyMethodMapper $identifyMethodMapper, - private FileMapper $fileMapper, - private IUserManager $userManager, - private IURLGenerator $urlGenerator, private IRootFolder $root, - private IHasher $hasher, - private IUserMountCache $userMountCache, private ITimeFactory $timeFactory, - private LoggerInterface $logger, private SessionService $sessionService, private FileElementMapper $fileElementMapper, + private ClickToSign $clickToSign, + private EmailToken $emailToken, ) { // TRANSLATORS Name of possible authenticator method. This signalize that the signer could be identified by email - $this->friendlyName = $this->l10n->t('Email token'); + $this->friendlyName = $this->identifyMethodService->getL10n()->t('Email'); + $this->signatureMethods = [ + $this->clickToSign->getName() => $this->clickToSign, + $this->emailToken->getName() => $this->emailToken, + ]; parent::__construct( - $appConfig, - $l10n, - $identifyMethodMapper, - $signRequestMapper, - $fileMapper, - $root, - $hasher, - $userManager, - $urlGenerator, - $userMountCache, - $timeFactory, - $logger, - $sessionService, + $identifyMethodService, ); - $this->getSettings(); } public function notify(bool $isNew): void { if (!$this->willNotify) { return; } - $signRequest = $this->signRequestMapper->getById($this->getEntity()->getSignRequestId()); + $signRequest = $this->identifyMethodService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId()); if ($isNew) { $this->mail->notifyUnsignedUser($signRequest, $this->getEntity()->getIdentifierValue()); return; @@ -98,7 +75,7 @@ public function validateToRequest(): void { $this->throwIfInvalidEmail(); } - public function validateToSign(): void { + public function validateToIdentify(): void { $this->throwIfAccountAlreadyExists($this->user); $this->throwIfIsAuthenticatedWithDifferentAccount($this->user); $this->throwIfInvalidToken(); @@ -111,6 +88,22 @@ public function validateToSign(): void { $this->updateIdentifiedAt(); } + protected function throwIfNeedToCreateAccount() { + $settings = $this->getSettings(); + if (!$settings['can_create_account']) { + return; + } + if ($this->identifyMethodService->getSessionService()->getSignStartTime()) { + return; + } + $email = $this->getEntity()->getIdentifierValue(); + throw new LibresignException(json_encode([ + 'action' => JSActions::ACTION_CREATE_USER, + 'settings' => ['accountHash' => md5($email)], + 'message' => $this->identifyMethodService->getL10n()->t('You need to create an account to sign this file.'), + ])); + } + private function throwIfIsAuthenticatedWithDifferentAccount(?IUser $user): void { if (!$user instanceof IUser) { return; @@ -122,7 +115,7 @@ private function throwIfIsAuthenticatedWithDifferentAccount(?IUser $user): void } throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_DO_NOTHING, - 'errors' => [$this->l10n->t('Invalid user')], + 'errors' => [$this->identifyMethodService->getL10n()->t('Invalid user')], ])); } } @@ -132,7 +125,7 @@ private function throwIfAccountAlreadyExists(?IUser $user): void { return; } $email = $this->entity->getIdentifierValue(); - $signer = $this->userManager->getByEmail($email); + $signer = $this->identifyMethodService->getUserManager()->getByEmail($email); if (!$signer) { return; } @@ -141,12 +134,12 @@ private function throwIfAccountAlreadyExists(?IUser $user): void { return; } } - $signRequest = $this->signRequestMapper->getById($this->getEntity()->getSignRequestId()); + $signRequest = $this->identifyMethodService->getSignRequestMapper()->getById($this->getEntity()->getSignRequestId()); throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_REDIRECT, - 'errors' => [$this->l10n->t('User already exists. Please login.')], - 'redirect' => $this->urlGenerator->linkToRoute('core.login.showLoginForm', [ - 'redirect_url' => $this->urlGenerator->linkToRoute( + 'errors' => [$this->identifyMethodService->getL10n()->t('User already exists. Please login.')], + 'redirect' => $this->identifyMethodService->getUrlGenerator()->linkToRoute('core.login.showLoginForm', [ + 'redirect_url' => $this->identifyMethodService->getUrlGenerator()->linkToRoute( 'libresign.page.sign', ['uuid' => $signRequest->getUuid()] ), @@ -157,26 +150,27 @@ private function throwIfAccountAlreadyExists(?IUser $user): void { public function validateToCreateAccount(string $value): void { $this->throwIfInvalidEmail(); $this->throwIfNotAllowedToCreateAccount(); - if ($this->userManager->userExists($value)) { - throw new LibresignException($this->l10n->t('User already exists')); + if ($this->identifyMethodService->getUserManager()->userExists($value)) { + throw new LibresignException($this->identifyMethodService->getL10n()->t('User already exists')); } if ($this->getEntity()->getIdentifierValue() !== $value) { - throw new LibresignException($this->l10n->t('This is not your file')); + throw new LibresignException($this->identifyMethodService->getL10n()->t('This is not your file')); } } private function throwIfNotAllowedToCreateAccount(): void { - if (!$this->canCreateAccount) { + $settings = $this->getSettings(); + if (!$settings['can_create_account']) { throw new LibresignException(json_encode([ 'action' => JSActions::ACTION_SHOW_ERROR, - 'errors' => [$this->l10n->t('It is not possible to create new accounts.')], + 'errors' => [$this->identifyMethodService->getL10n()->t('It is not possible to create new accounts.')], ])); } } private function throwIfInvalidEmail(): void { if (!filter_var($this->entity->getIdentifierValue(), FILTER_VALIDATE_EMAIL)) { - throw new LibresignException($this->l10n->t('Invalid email')); + throw new LibresignException($this->identifyMethodService->getL10n()->t('Invalid email')); } } @@ -187,13 +181,12 @@ public function getSettings(): array { $this->settings = parent::getSettingsFromDatabase( default: [ 'enabled' => false, - 'can_create_account' => $this->canCreateAccount, + 'can_create_account' => true, ], immutable: [ - 'test_url' => $this->urlGenerator->linkToRoute('settings.MailSettings.sendTestMail'), + 'test_url' => $this->identifyMethodService->getUrlGenerator()->linkToRoute('settings.MailSettings.sendTestMail'), ] ); - $this->canCreateAccount = $this->settings['can_create_account']; return $this->settings; } } diff --git a/lib/Service/IdentifyMethod/IIdentifyMethod.php b/lib/Service/IdentifyMethod/IIdentifyMethod.php index 5bd52aa851..52a15b0493 100644 --- a/lib/Service/IdentifyMethod/IIdentifyMethod.php +++ b/lib/Service/IdentifyMethod/IIdentifyMethod.php @@ -25,22 +25,29 @@ namespace OCA\Libresign\Service\IdentifyMethod; use OCA\Libresign\Db\IdentifyMethod; +use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\AbstractSignatureMethod; use OCP\IUser; interface IIdentifyMethod { + public static function getId(): string; public function getName(): string; - public function isEnabledAsSignatueMethod(): bool; + public function getFriendlyName(): string; public function setCodeSentByUser(string $code): void; public function setUser(?IUser $user): void; public function cleanEntity(): void; public function setEntity(IdentifyMethod $entity): void; public function getEntity(): IdentifyMethod; + /** + * @return AbstractSignatureMethod[] + */ + public function getSignatureMethods(): array; + public function signatureMethodsToArray(): array; public function getSettings(): array; public function willNotifyUser(bool $willNotify): void; public function notify(bool $isNew): void; public function validateToRequest(): void; public function validateToCreateAccount(string $value): void; - public function validateToSign(): void; + public function validateToIdentify(): void; public function validateToRenew(?IUser $user = null): void; public function save(): void; public function delete(): void; diff --git a/lib/Service/IdentifyMethod/IdentifyMethodService.php b/lib/Service/IdentifyMethod/IdentifyMethodService.php new file mode 100644 index 0000000000..56dfe95096 --- /dev/null +++ b/lib/Service/IdentifyMethod/IdentifyMethodService.php @@ -0,0 +1,158 @@ + + * + * @author Vitor Mattos + * + * @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\Libresign\Service\IdentifyMethod; + +use OCA\Libresign\Db\FileMapper; +use OCA\Libresign\Db\IdentifyMethod; +use OCA\Libresign\Db\IdentifyMethodMapper; +use OCA\Libresign\Db\SignRequestMapper; +use OCA\Libresign\Service\SessionService; +use OCP\AppFramework\Services\IAppConfig; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\Config\IUserMountCache; +use OCP\Files\IRootFolder; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Security\IHasher; +use Psr\Log\LoggerInterface; + +class IdentifyMethodService { + private array $savedSettings = []; + public function __construct( + private IdentifyMethodMapper $identifyMethodMapper, + private SessionService $sessionService, + private ITimeFactory $timeFactory, + private IRootFolder $root, + private IAppConfig $appConfig, + private SignRequestMapper $signRequestMapper, + private IUserMountCache $userMountCache, + private IL10N $l10n, + private FileMapper $fileMapper, + private IHasher $hasher, + private IUserManager $userManager, + private IURLGenerator $urlGenerator, + private LoggerInterface $logger, + ) { + } + + /** + * @return boolean is new instance + */ + public function save(IdentifyMethod $identifyMethod): bool { + $this->refreshIdFromDatabaseIfNecessary($identifyMethod); + if ($identifyMethod->getId()) { + $this->identifyMethodMapper->update($identifyMethod); + return false; + } + $this->identifyMethodMapper->insertOrUpdate($identifyMethod); + return true; + } + + public function delete(IdentifyMethod $identifyMethod): void { + if ($identifyMethod->getId()) { + $this->identifyMethodMapper->delete($identifyMethod); + } + } + + private function refreshIdFromDatabaseIfNecessary(IdentifyMethod $identifyMethod): void { + if ($identifyMethod->getId()) { + return; + } + if (!$identifyMethod->getSignRequestId() || !$identifyMethod->getIdentifierKey()) { + return; + } + + $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($identifyMethod->getSignRequestId()); + $exists = array_filter($identifyMethods, function (IdentifyMethod $current) use ($identifyMethod): bool { + return $current->getIdentifierKey() === $identifyMethod->getIdentifierKey(); + }); + if (!$exists) { + return; + } + $exists = current($exists); + $identifyMethod->setId($exists->getId()); + } + + public function getSavedSettings(): array { + if (!empty($this->savedSettings)) { + return $this->savedSettings; + } + $config = $this->getAppConfig()->getAppValue('identify_methods', '[]'); + $config = json_decode($config, true); + if (is_array($config)) { + $this->savedSettings = $config; + } + return $this->savedSettings; + } + + public function getSessionService(): SessionService { + return $this->sessionService; + } + + public function getTimeFactory(): ITimeFactory { + return $this->timeFactory; + } + + public function getRootFolder(): IRootFolder { + return $this->root; + } + + public function getAppConfig(): IAppConfig { + return $this->appConfig; + } + + public function getSignRequestMapper(): SignRequestMapper { + return $this->signRequestMapper; + } + + public function getUserMountCache(): IUserMountCache { + return $this->userMountCache; + } + + public function getL10n(): IL10N { + return $this->l10n; + } + + public function getFileMapper(): FileMapper { + return $this->fileMapper; + } + + public function getHasher(): IHasher { + return $this->hasher; + } + + public function getUserManager(): IUserManager { + return $this->userManager; + } + + public function getUrlGenerator(): IURLGenerator { + return $this->urlGenerator; + } + + public function getLogger(): LoggerInterface { + return $this->logger; + } +} diff --git a/lib/Service/IdentifyMethod/Password.php b/lib/Service/IdentifyMethod/Password.php deleted file mode 100644 index 23a49a6a91..0000000000 --- a/lib/Service/IdentifyMethod/Password.php +++ /dev/null @@ -1,119 +0,0 @@ - - * - * @author Vitor Mattos - * - * @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\Libresign\Service\IdentifyMethod; - -use OCA\Libresign\Db\FileMapper; -use OCA\Libresign\Db\IdentifyMethodMapper; -use OCA\Libresign\Db\SignRequestMapper; -use OCA\Libresign\Exception\LibresignException; -use OCA\Libresign\Handler\Pkcs12Handler; -use OCA\Libresign\Service\MailService; -use OCA\Libresign\Service\SessionService; -use OCP\AppFramework\Services\IAppConfig; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\Files\Config\IUserMountCache; -use OCP\Files\IRootFolder; -use OCP\IL10N; -use OCP\IURLGenerator; -use OCP\IUserManager; -use OCP\Security\IHasher; -use Psr\Log\LoggerInterface; - -class Password extends AbstractIdentifyMethod { - public const ID = 'password'; - public function __construct( - private IAppConfig $appConfig, - private IL10N $l10n, - private MailService $mail, - private SignRequestMapper $signRequestMapper, - private IdentifyMethodMapper $identifyMethodMapper, - private FileMapper $fileMapper, - private IUserManager $userManager, - private IURLGenerator $urlGenerator, - private IRootFolder $root, - private IHasher $hasher, - private IUserMountCache $userMountCache, - private ITimeFactory $timeFactory, - private LoggerInterface $logger, - private SessionService $sessionService, - private Pkcs12Handler $pkcs12Handler, - ) { - // TRANSLATORS Name of possible authenticator method. This signalize that the signer could be identified by certificate password - $this->friendlyName = $this->l10n->t('Certificate with password'); - parent::__construct( - $appConfig, - $l10n, - $identifyMethodMapper, - $signRequestMapper, - $fileMapper, - $root, - $hasher, - $userManager, - $urlGenerator, - $userMountCache, - $timeFactory, - $logger, - $sessionService, - ); - } - - public function validateToSign(): void { - $pfx = $this->pkcs12Handler->getPfx($this->user->getUID()); - openssl_pkcs12_read($pfx, $cert_info, $this->getEntity()->getIdentifierValue()); - if (empty($cert_info)) { - throw new LibresignException($this->l10n->t('Invalid password')); - } - } - - public function getSettings(): array { - if (!empty($this->settings)) { - return $this->settings; - } - - if (!$this->sessionService->isAuthenticated()) { - $isEnabledAsSignatueMethod = false; - } else { - $config = $this->appConfig->getAppValue('signature_methods', '[]'); - $config = json_decode($config, true); - if (json_last_error() !== JSON_ERROR_NONE || !is_array($config)) { - $isEnabledAsSignatueMethod = true; - } else { - $isEnabledAsSignatueMethod = array_reduce($config, function (bool $carry, $method) { - if (!is_array($method)) { - $carry = false; - } elseif (array_key_exists('enabled', $method)) { - $carry = ((bool) $method['enabled']) || !$carry; - } - return $carry; - }, true); - } - } - - $this->settings = $this->getSettingsFromDatabase(); - $this->settings['enabled_as_signature_method'] = $isEnabledAsSignatueMethod; - - return $this->settings; - } -} diff --git a/lib/Service/IdentifyMethod/SignatureMethod/AbstractSignatureMethod.php b/lib/Service/IdentifyMethod/SignatureMethod/AbstractSignatureMethod.php new file mode 100644 index 0000000000..36cfbc598f --- /dev/null +++ b/lib/Service/IdentifyMethod/SignatureMethod/AbstractSignatureMethod.php @@ -0,0 +1,45 @@ + + * + * @author Vitor Mattos + * + * @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\Libresign\Service\IdentifyMethod\SignatureMethod; + +use OCA\Libresign\Service\IdentifyMethod\AbstractIdentifyMethod; + +abstract class AbstractSignatureMethod extends AbstractIdentifyMethod implements ISignatureMethod { + private bool $enabled = false; + + public function enable(): void { + $this->enabled = true; + } + + public function isEnabled(): bool { + return $this->enabled; + } + + public function toArray(): array { + return [ + 'label' => $this->getFriendlyName(), + ]; + } +} diff --git a/lib/Service/IdentifyMethod/SignatureMethod/ClickToSign.php b/lib/Service/IdentifyMethod/SignatureMethod/ClickToSign.php new file mode 100644 index 0000000000..65fec18e2f --- /dev/null +++ b/lib/Service/IdentifyMethod/SignatureMethod/ClickToSign.php @@ -0,0 +1,39 @@ + + * + * @author Vitor Mattos + * + * @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\Libresign\Service\IdentifyMethod\SignatureMethod; + +use OCA\Libresign\Service\IdentifyMethod\IdentifyMethodService; + +class ClickToSign extends AbstractSignatureMethod { + public function __construct( + protected IdentifyMethodService $identifyMethodService, + ) { + // TRANSLATORS Name of possible authenticator method. This signalize that the signer only need to click to sign after was identified + $this->friendlyName = $this->identifyMethodService->getL10n()->t('Click to sign'); + parent::__construct( + $identifyMethodService, + ); + } +} diff --git a/lib/Service/IdentifyMethod/SignatureMethod/EmailToken.php b/lib/Service/IdentifyMethod/SignatureMethod/EmailToken.php new file mode 100644 index 0000000000..7b7b465d61 --- /dev/null +++ b/lib/Service/IdentifyMethod/SignatureMethod/EmailToken.php @@ -0,0 +1,71 @@ + + * + * @author Vitor Mattos + * + * @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\Libresign\Service\IdentifyMethod\SignatureMethod; + +use OCA\Libresign\Service\IdentifyMethod\IdentifyMethodService; +use Wobeto\EmailBlur\Blur; + +class EmailToken extends AbstractSignatureMethod implements IToken { + public function __construct( + protected IdentifyMethodService $identifyMethodService, + protected TokenService $tokenService, + ) { + // TRANSLATORS Name of possible authenticator method. This signalize that the signer could be identified by email + $this->friendlyName = $this->identifyMethodService->getL10n()->t('Email token'); + parent::__construct( + $identifyMethodService, + ); + } + + public function toArray(): array { + $return = parent::toArray(); + $entity = $this->getEntity(); + $return['needCode'] = empty($entity->getCode()) + || empty($entity->getIdentifiedAtDate()) + || empty($this->codeSentByUser); + $return['hasConfirmCode'] = !empty($entity->getCode()); + $return['blurredEmail'] = $this->getBlurredEmail(); + $return['hashOfEmail'] = md5($this->getEntity()->getIdentifierValue()); + return $return; + } + + private function getBlurredEmail(): string { + $email = $this->getEntity()->getIdentifierValue(); + $blur = new Blur($email); + return $blur->make(); + } + + public function requestCode(string $identify): void { + $signRequestMapper = $this->identifyMethodService->getSignRequestMapper(); + $signRequest = $signRequestMapper->getById($this->getEntity()->getSignRequestId()); + $displayName = $signRequest->getDisplayName(); + if ($identify === $displayName) { + $displayName = ''; + } + $code = $this->tokenService->sendCodeByEmail($identify, $displayName); + $this->getEntity()->setCode($code); + $this->identifyMethodService->save($this->getEntity()); + } +} diff --git a/lib/Service/IdentifyMethod/SignatureMethod/ISignatureMethod.php b/lib/Service/IdentifyMethod/SignatureMethod/ISignatureMethod.php new file mode 100644 index 0000000000..45dc328664 --- /dev/null +++ b/lib/Service/IdentifyMethod/SignatureMethod/ISignatureMethod.php @@ -0,0 +1,31 @@ + + * + * @author Vitor Mattos + * + * @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\Libresign\Service\IdentifyMethod\SignatureMethod; + +interface ISignatureMethod { + public function enable(): void; + public function isEnabled(): bool; + public function toArray(): array; +} diff --git a/lib/Service/IdentifyMethod/SignatureMethod/IToken.php b/lib/Service/IdentifyMethod/SignatureMethod/IToken.php new file mode 100644 index 0000000000..1526542f8d --- /dev/null +++ b/lib/Service/IdentifyMethod/SignatureMethod/IToken.php @@ -0,0 +1,29 @@ + + * + * @author Vitor Mattos + * + * @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\Libresign\Service\IdentifyMethod\SignatureMethod; + +interface IToken { + public function requestCode(string $identify): void; +} diff --git a/lib/Service/IdentifyMethod/SignatureMethod/Password.php b/lib/Service/IdentifyMethod/SignatureMethod/Password.php new file mode 100644 index 0000000000..4d7ea6f836 --- /dev/null +++ b/lib/Service/IdentifyMethod/SignatureMethod/Password.php @@ -0,0 +1,65 @@ + + * + * @author Vitor Mattos + * + * @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\Libresign\Service\IdentifyMethod\SignatureMethod; + +use OCA\Libresign\Exception\LibresignException; +use OCA\Libresign\Handler\Pkcs12Handler; +use OCA\Libresign\Service\IdentifyMethod\IdentifyMethodService; + +class Password extends AbstractSignatureMethod { + public function __construct( + protected IdentifyMethodService $identifyMethodService, + protected Pkcs12Handler $pkcs12Handler, + ) { + // TRANSLATORS Name of possible authenticator method. This signalize that the signer could be identified by certificate password + $this->friendlyName = $this->identifyMethodService->getL10n()->t('Certificate with password'); + parent::__construct( + $identifyMethodService, + ); + } + + public function validateToIdentify(): void { + $pfx = $this->pkcs12Handler->getPfx($this->user->getUID()); + openssl_pkcs12_read($pfx, $cert_info, $this->getEntity()->getIdentifierValue()); + if (empty($cert_info)) { + throw new LibresignException($this->identifyMethodService->getL10n()->t('Invalid password')); + } + } + + public function toArray(): array { + $return = parent::toArray(); + $return['hasSignatureFile'] = $this->hasSignatureFile(); + return $return; + } + + private function hasSignatureFile(): bool { + try { + $this->pkcs12Handler->getPfx($this->user->getUID()); + return true; + } catch (\Throwable $th) { + } + return false; + } +} diff --git a/lib/Service/IdentifyMethod/SignatureMethod/TokenService.php b/lib/Service/IdentifyMethod/SignatureMethod/TokenService.php new file mode 100644 index 0000000000..3e06ab020b --- /dev/null +++ b/lib/Service/IdentifyMethod/SignatureMethod/TokenService.php @@ -0,0 +1,82 @@ + + * + * @author Vitor Mattos + * + * @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\Libresign\Service\IdentifyMethod\SignatureMethod; + +use OCA\Libresign\Service\MailService; +use OCP\Security\IHasher; +use OCP\Security\ISecureRandom; + +class TokenService { + public const TOKEN_LENGTH = 6; + public const SIGN_PASSWORD = 'password'; + public const SIGN_SIGNAL = 'signal'; + public const SIGN_TELEGRAM = 'telegram'; + public const SIGN_SMS = 'sms'; + public const SIGN_EMAIL = 'email'; + + public function __construct( + private ISecureRandom $secureRandom, + private IHasher $hasher, + private MailService $mail, + ) { + } + + /** + * @todo check this code and put to work + */ + public function sendCodeByGateway(string $code, string $gatewayName): void { + // $user = \OC::$server->get(IUserSession::class)->getUser(); + // $gateway = $this->getGateway($user, $gatewayName); + + // $userAccount = $this->accountManager->getAccount($user); + // $identifier = $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue(); + // $gateway->send($user, $identifier, $this->l10n->t('%s is your LibreSign verification code.', $code)); + // } + + // /** + // * @throws OCSForbiddenException + // */ + // private function getGateway(IUser $user, string $gatewayName): \OCA\TwoFactorGateway\Service\Gateway\IGateway { + // if (!$this->appManager->isEnabledForUser('twofactor_gateway', $user)) { + // throw new OCSForbiddenException($this->l10n->t('Authorize signing using %s token is disabled because Nextcloud Two-Factor Gateway is not enabled.', $gatewayName)); + // } + // $factory = $this->serverContainer->get('\OCA\TwoFactorGateway\Service\Gateway\Factory'); + // $gateway = $factory->getGateway($gatewayName); + // if (!$gateway->getConfig()->isComplete()) { + // throw new OCSForbiddenException($this->l10n->t('Gateway %s not configured on Two-Factor Gateway.', $gatewayName)); + // } + // return $gateway; + } + + public function sendCodeByEmail(string $email, string $displayName): string { + $code = $this->secureRandom->generate(self::TOKEN_LENGTH, ISecureRandom::CHAR_DIGITS); + $this->mail->sendCodeToSign( + email: $email, + name: $displayName, + code: $code + ); + return $this->hasher->hash($code); + } +} diff --git a/lib/Service/IdentifyMethodService.php b/lib/Service/IdentifyMethodService.php index 5353db5a48..126e47de56 100644 --- a/lib/Service/IdentifyMethodService.php +++ b/lib/Service/IdentifyMethodService.php @@ -24,6 +24,7 @@ namespace OCA\Libresign\Service; +use OCA\Libresign\Db\IdentifyMethod; use OCA\Libresign\Db\IdentifyMethodMapper; use OCA\Libresign\Db\SignRequest; use OCA\Libresign\Exception\LibresignException; @@ -50,6 +51,7 @@ class IdentifyMethodService { self::IDENTIFY_PASSWORD, self::IDENTIFY_CLICK_TO_SIGN, ]; + private ?IdentifyMethod $currentIdentifyMethod = null; private array $identifyMethodsSettings = []; /** * @var array> @@ -76,9 +78,11 @@ public function getInstanceOfIdentifyMethod(string $name, ?string $identifyValue $identifyMethod = $this->getNewInstanceOfMethod($name); $entity = $identifyMethod->getEntity(); - $entity->setIdentifierKey($name); - $entity->setIdentifierValue($identifyValue); - $entity->setMandatory($this->isMandatoryMethod($name) ? 1 : 0); + if (!$entity->getId()) { + $entity->setIdentifierKey($name); + $entity->setIdentifierValue($identifyValue); + $entity->setMandatory($this->isMandatoryMethod($name) ? 1 : 0); + } if ($identifyValue) { $identifyMethod->validateToRequest(); } @@ -90,7 +94,11 @@ public function getInstanceOfIdentifyMethod(string $name, ?string $identifyValue private function getNewInstanceOfMethod(string $name): IIdentifyMethod { $className = 'OCA\Libresign\Service\IdentifyMethod\\' . ucfirst($name); $identifyMethod = clone \OC::$server->get($className); - $identifyMethod->cleanEntity(); + if (empty($this->currentIdentifyMethod)) { + $identifyMethod->cleanEntity(); + } else { + $identifyMethod->setEntity($this->currentIdentifyMethod); + } return $identifyMethod; } @@ -141,11 +149,11 @@ public function getByUserData(array $data) { public function getIdentifyMethodsFromSignRequestId(int $signRequestId): array { $entities = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequestId); foreach ($entities as $entity) { - $identifyMethod = $this->getInstanceOfIdentifyMethod( + $this->currentIdentifyMethod = $entity; + $this->getInstanceOfIdentifyMethod( $entity->getIdentifierKey(), $entity->getIdentifierValue(), ); - $identifyMethod->setEntity($entity); } $return = []; foreach ($this->identifyMethods as $methodName => $list) { @@ -158,6 +166,25 @@ public function getIdentifyMethodsFromSignRequestId(int $signRequestId): array { return $return; } + public function getSignMethodsOfIdentifiedFactors(int $signRequestId): array { + $matrix = $this->getIdentifyMethodsFromSignRequestId($signRequestId); + $return = []; + foreach ($matrix as $identifyMethods) { + foreach ($identifyMethods as $identifyMethod) { + if (empty($identifyMethod->getEntity()->getIdentifiedAtDate())) { + continue; + } + foreach ($identifyMethod->getSignatureMethods() as $signatureMethod) { + if (!$signatureMethod->isEnabled()) { + continue; + } + $return[$signatureMethod->getName()] = $signatureMethod->toArray(); + } + } + } + return $return; + } + public function save(SignRequest $signRequest, bool $notify = true): void { foreach ($this->identifyMethods as $methods) { foreach ($methods as $identifyMethod) { diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index a6a45d07b4..a37b453650 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -47,6 +47,7 @@ use OCA\Libresign\Helper\JSActions; use OCA\Libresign\Helper\ValidateHelper; use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod; +use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\EmailToken; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; @@ -81,7 +82,7 @@ class SignFileService { private ?Node $fileToSign = null; private string $userUniqueIdentifier = ''; private string $friendlyName = ''; - private IUser $user; + private ?IUser $user; public function __construct( protected IL10N $l10n, @@ -96,6 +97,7 @@ public function __construct( protected LoggerInterface $logger, private IAppConfig $appConfig, protected ValidateHelper $validateHelper, + private SignerElementsService $signerElementsService, private IRootFolder $root, private IUserSession $userSession, private IUserMountCache $userMountCache, @@ -103,7 +105,6 @@ public function __construct( private UserElementMapper $userElementMapper, private IEventDispatcher $eventDispatcher, private IURLGenerator $urlGenerator, - private SignatureMethodService $signMethod, private IdentifyMethodMapper $identifyMethodMapper, private ITempManager $tempManager, private IdentifyMethodService $identifyMethodService, @@ -215,7 +216,7 @@ public function setPassword(?string $password = null): self { return $this; } - public function setCurrentUser(IUser $user): self { + public function setCurrentUser(?IUser $user): self { $this->user = $user; return $this; } @@ -231,22 +232,33 @@ public function setVisibleElements(array $list): self { }); if ($element) { $c = current($element); - $userElement = $this->userElementMapper->findOne(['id' => $c['profileElementId']]); + if (!empty($c['profileElementId'])) { + $userElement = $this->userElementMapper->findOne(['id' => $c['profileElementId']]); + $nodeId = $userElement->getFileId(); + } elseif (!empty($c['profileFileId'])) { + $nodeId = $c['profileFileId']; + } else { + throw new LibresignException($this->l10n->t('Invalid data to sign file'), 1); + } } else { $userElement = $this->userElementMapper->findOne([ 'user_id' => $this->user->getUID(), 'type' => $fileElement->getType(), ]); + $nodeId = $userElement->getFileId(); } try { - $nodeId = $userElement->getFileId(); - - $mountsContainingFile = $this->userMountCache->getMountsForFileId($nodeId); - foreach ($mountsContainingFile as $fileInfo) { - $this->root->getByIdInPath($nodeId, $fileInfo->getMountPoint()); + if ($this->user instanceof IUser) { + $mountsContainingFile = $this->userMountCache->getMountsForFileId($nodeId); + foreach ($mountsContainingFile as $fileInfo) { + $this->root->getByIdInPath($nodeId, $fileInfo->getMountPoint()); + } + /** @var \OCP\Files\File[] */ + $node = $this->root->getById($nodeId); + } else { + $filesOfElementes = $this->signerElementsService->getElementsFromSession(); + $node = array_filter($filesOfElementes, fn ($file) => $file->getId() === $nodeId); } - /** @var \OCP\Files\File[] */ - $node = $this->root->getById($nodeId); if (!$node) { throw new \Exception('empty'); } @@ -258,7 +270,6 @@ public function setVisibleElements(array $list): self { file_put_contents($tempFile, $node->getContent()); $visibleElements = new VisibleElementAssoc( $fileElement, - $userElement, $tempFile ); $this->elements[] = $visibleElements; @@ -413,38 +424,61 @@ public function renew(SignRequestEntity $signRequest, string $method): void { }, $identifyMethods[$method]); } - public function requestCode(SignRequestEntity $signRequest, string $method, string $identify = ''): string { - return $this->signMethod->requestCode($signRequest, $method, $identify); + public function requestCode( + SignRequestEntity $signRequest, + string $identifyMethodName, + string $signMethodName, + string $identify = '' + ): void { + $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId()); + if (empty($identifyMethods[$identifyMethodName])) { + throw new LibresignException($this->l10n->t('Invalid identification method')); + } + foreach ($identifyMethods[$identifyMethodName] as $identifyMethod) { + $signatureMethods = $identifyMethod->getSignatureMethods(); + if (empty($signatureMethods[$signMethodName])) { + throw new LibresignException($this->l10n->t('Invalid identification method')); + } + /** @var EmailToken $signatureMethod */ + $signatureMethod = $signatureMethods[$signMethodName]; + $signatureMethod->requestCode($identify); + return; + } + throw new LibresignException($this->l10n->t('Sending authorization code not enabled.')); } - public function getSignRequestToSign(FileEntity $libresignFile, IUser $user): SignRequestEntity { + public function getSignRequestToSign(FileEntity $libresignFile, ?string $signRequestUuid, ?IUser $user): SignRequestEntity { $this->validateHelper->fileCanBeSigned($libresignFile); try { $signRequests = $this->signRequestMapper->getByFileId($libresignFile->getId()); - $signRequest = array_reduce($signRequests, function (?SignRequestEntity $carry, SignRequestEntity $signRequest) use ($user): ?SignRequestEntity { - $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId()); - $found = array_filter($identifyMethods, function (IdentifyMethod $identifyMethod) use ($user) { - if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL - && ( - $identifyMethod->getIdentifierValue() === $user->getUID() - || $identifyMethod->getIdentifierValue() === $user->getEMailAddress() - ) - ) { - return true; - } - if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_ACCOUNT - && $identifyMethod->getIdentifierValue() === $user->getUID() - ) { - return true; + if (!empty($signRequestUuid)) { + $signRequest = $this->getSignRequest($signRequestUuid); + } else { + $signRequest = array_reduce($signRequests, function (?SignRequestEntity $carry, SignRequestEntity $signRequest) use ($user): ?SignRequestEntity { + $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId()); + $found = array_filter($identifyMethods, function (IdentifyMethod $identifyMethod) use ($user) { + if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_EMAIL + && ( + $identifyMethod->getIdentifierValue() === $user->getUID() + || $identifyMethod->getIdentifierValue() === $user->getEMailAddress() + ) + ) { + return true; + } + if ($identifyMethod->getIdentifierKey() === IdentifyMethodService::IDENTIFY_ACCOUNT + && $identifyMethod->getIdentifierValue() === $user->getUID() + ) { + return true; + } + return false; + }); + if (count($found) > 0) { + return $signRequest; } - return false; + return $carry; }); - if (count($found) > 0) { - return $signRequest; - } - return $carry; - }); + } if (!$signRequest) { throw new DoesNotExistException('Sign request not found'); @@ -611,19 +645,6 @@ public function getSignerData(?IUser $user, ?SignRequestEntity $signRequest = nu return $return; } - public function getAvailableIdentifyMethodsFromSignRequest(SignRequestEntity $signRequest): array { - $identifyMethods = $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId()); - $return = array_map(function (IdentifyMethod $identifyMethod): array { - return [ - 'mandatory' => $identifyMethod->getMandatory(), - 'identifiedAtDate' => $identifyMethod->getIdentifiedAtDate(), - 'validateCode' => $identifyMethod->getCode() && empty($identifyMethod->getIdentifiedAtDate()) ? true : false, - 'method' => $identifyMethod->getIdentifierKey(), - ]; - }, $identifyMethods); - return $return; - } - public function getAvailableIdentifyMethodsFromSettings(): array { $identifyMethods = $this->identifyMethodService->getIdentifyMethodsSettings(); $return = array_map(function (array $identifyMethod): array { diff --git a/lib/Service/SignatureMethodService.php b/lib/Service/SignatureMethodService.php deleted file mode 100644 index c27c6b448a..0000000000 --- a/lib/Service/SignatureMethodService.php +++ /dev/null @@ -1,163 +0,0 @@ - - * - * @author Vitor Mattos - * - * @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\Libresign\Service; - -use OCA\Libresign\Db\SignRequest; -use OCA\Libresign\Exception\LibresignException; -use OCA\Libresign\Service\IdentifyMethod\AbstractIdentifyMethod; -use OCA\Libresign\Service\IdentifyMethod\ClickToSign; -use OCA\Libresign\Service\IdentifyMethod\Email; -use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod; -use OCA\Libresign\Service\IdentifyMethod\Password; -use OCP\Accounts\IAccountManager; -use OCP\App\IAppManager; -use OCP\AppFramework\OCS\OCSForbiddenException; -use OCP\IL10N; -use OCP\IUser; -use OCP\IUserSession; -use OCP\Security\IHasher; -use OCP\Security\ISecureRandom; -use Psr\Container\ContainerInterface; - -class SignatureMethodService { - public const TOKEN_LENGTH = 6; - private const SIGN_PASSWORD = 'password'; - private const SIGN_SIGNAL = 'signal'; - private const SIGN_TELEGRAM = 'telegram'; - private const SIGN_SMS = 'sms'; - private const SIGN_EMAIL = 'email'; - /** - * @var AbstractIdentifyMethod[] - */ - private array $methods; - - public function __construct( - private IdentifyMethodService $identifyMethodService, - private IAccountManager $accountManager, - private IAppManager $appManager, - private IL10N $l10n, - private ISecureRandom $secureRandom, - private IHasher $hasher, - private ContainerInterface $serverContainer, - private MailService $mail, - private Password $password, - private ClickToSign $clickToSign, - private Email $email, - ) { - $this->methods = [ - $this->password->getName() => $this->password, - $this->clickToSign->getName() => $this->clickToSign, - $this->email->getName() => $this->email, - ]; - } - - public function getMethods(): array { - return array_map(function (AbstractIdentifyMethod $method) { - return [ - 'label' => $method->friendlyName, - 'enabled' => $method->isEnabledAsSignatueMethod(), - ]; - }, $this->methods); - } - - public function requestCode(SignRequest $signRequest, string $methodId, string $identify = ''): string { - if (!array_key_exists($methodId, $this->methods)) { - throw new LibresignException($this->l10n->t('Invalid Sign engine.'), 400); - } - - $identifyMethods = $this->identifyMethodService->getIdentifyMethodsFromSignRequestId($signRequest->getId()); - if (!empty($identifyMethods[$methodId])) { - $method = array_filter($identifyMethods[$methodId], function (IIdentifyMethod $identifyMethod) use ($methodId) { - return $identifyMethod->getName() === $methodId; - }); - $method = current($method); - } - if (empty($method)) { - $method = $this->identifyMethodService->getInstanceOfIdentifyMethod($methodId, $identify); - } else { - if (!empty($identify) && $identify !== $method->getEntity()->getIdentifierKey()) { - $method->getEntity()->setIdentifierValue($identify); - } - $identify = $method->getEntity()->getIdentifierValue(); - } - - $token = $this->secureRandom->generate(self::TOKEN_LENGTH, ISecureRandom::CHAR_DIGITS); - $this->sendCode($signRequest, $methodId, $token, $identify); - - $entity = $method->getEntity(); - $entity->setCode($this->hasher->hash($token)); - $entity->setMandatory(0); - $this->identifyMethodService->save($signRequest, false); - - return $token; - } - - private function sendCode(SignRequest $signRequest, string $methodId, string $code, string $identify = ''): void { - switch ($methodId) { - case SignatureMethodService::SIGN_SMS: - case SignatureMethodService::SIGN_TELEGRAM: - case SignatureMethodService::SIGN_SIGNAL: - $this->sendCodeByGateway($code, gatewayName: $methodId); - break; - case SignatureMethodService::SIGN_EMAIL: - $this->sendCodeByEmail($code, $identify, $signRequest->getDisplayName()); - break; - case SignatureMethodService::SIGN_PASSWORD: - throw new LibresignException($this->l10n->t('Sending authorization code not enabled.')); - } - } - - private function sendCodeByGateway(string $code, string $gatewayName): void { - $user = \OC::$server->get(IUserSession::class)->getUser(); - $gateway = $this->getGateway($user, $gatewayName); - - $userAccount = $this->accountManager->getAccount($user); - $identifier = $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue(); - $gateway->send($user, $identifier, $this->l10n->t('%s is your LibreSign verification code.', $code)); - } - - /** - * @throws OCSForbiddenException - */ - private function getGateway(IUser $user, string $gatewayName): \OCA\TwoFactorGateway\Service\Gateway\IGateway { - if (!$this->appManager->isEnabledForUser('twofactor_gateway', $user)) { - throw new OCSForbiddenException($this->l10n->t('Authorize signing using %s token is disabled because Nextcloud Two-Factor Gateway is not enabled.', $gatewayName)); - } - $factory = $this->serverContainer->get('\OCA\TwoFactorGateway\Service\Gateway\Factory'); - $gateway = $factory->getGateway($gatewayName); - if (!$gateway->getConfig()->isComplete()) { - throw new OCSForbiddenException($this->l10n->t('Gateway %s not configured on Two-Factor Gateway.', $gatewayName)); - } - return $gateway; - } - - private function sendCodeByEmail(string $code, string $email, string $displayName): void { - $this->mail->sendCodeToSign( - email: $email, - name: $displayName, - code: $code - ); - } -} diff --git a/lib/Service/SignerElementsService.php b/lib/Service/SignerElementsService.php new file mode 100644 index 0000000000..d9c9f4f788 --- /dev/null +++ b/lib/Service/SignerElementsService.php @@ -0,0 +1,139 @@ + + * + * @author Vitor Mattos + * + * @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\Libresign\Service; + +use OCA\Libresign\Db\UserElement; +use OCA\Libresign\Db\UserElementMapper; +use OCP\Files\Folder; +use OCP\Files\NotFoundException; +use OCP\IURLGenerator; + +class SignerElementsService { + public const ELEMENT_SIGN_WIDTH = 350; + public const ELEMENT_SIGN_HEIGHT = 100; + + public function __construct( + private FolderService $folderService, + private SessionService $sessionService, + private IURLGenerator $urlGenerator, + private UserElementMapper $userElementMapper, + ) { + } + + /** + * @return ((int|string)[]|\DateTime|int|string)[] + * + * @psalm-return array{id?: int, type?: string, file?: array{url: string, fileId: int}, uid?: string, starred?: 0|1, createdAt?: \DateTime} + */ + public function getUserElementByElementId(string $userId, $elementId): array { + $element = $this->userElementMapper->findOne(['id' => $elementId, 'user_id' => $userId]); + $exists = $this->signatureFileExists($element); + if (!$exists) { + return []; + } + return [ + 'id' => $element->getId(), + 'type' => $element->getType(), + 'file' => [ + 'url' => $this->urlGenerator->linkToRoute('core.Preview.getPreviewByFileId', ['fileId' => $element->getFileId(), 'x' => self::ELEMENT_SIGN_WIDTH, 'y' => self::ELEMENT_SIGN_HEIGHT]), + 'fileId' => $element->getFileId() + ], + 'uid' => $element->getUserId(), + 'starred' => $element->getStarred() ? 1 : 0, + 'createdAt' => $element->getCreatedAt() + ]; + } + + /** + * @return ((int|string)[]|\DateTime|int|string)[][] + * + * @psalm-return list + */ + public function getUserElements(string $userId): array { + $elements = $this->userElementMapper->findMany(['user_id' => $userId]); + $return = []; + foreach ($elements as $element) { + $exists = $this->signatureFileExists($element); + if (!$exists) { + continue; + } + $return[] = [ + 'id' => $element->getId(), + 'type' => $element->getType(), + 'file' => [ + 'url' => $this->urlGenerator->linkToRoute('core.Preview.getPreviewByFileId', ['fileId' => $element->getFileId(), 'x' => self::ELEMENT_SIGN_WIDTH, 'y' => self::ELEMENT_SIGN_HEIGHT]), + 'fileId' => $element->getFileId() + ], + 'uid' => $element->getUserId(), + 'starred' => $element->getStarred() ? 1 : 0, + 'createdAt' => $element->getCreatedAt() + ]; + } + return $return; + } + + private function signatureFileExists(UserElement $userElement): bool { + try { + $this->folderService->getFolder($userElement->getFileId()); + } catch (\Exception $e) { + $this->userElementMapper->delete($userElement); + return false; + } + return true; + } + + public function getElementsFromSession(): array { + $folder = $this->folderService->getFolder(); + try { + /** @var Folder $signerFolder */ + $signerFolder = $folder->get($this->sessionService->getSessionId()); + } catch (NotFoundException $th) { + return []; + } + $fileList = $signerFolder->getDirectoryListing(); + return $fileList; + } + + public function getElementsFromSessionAsArray(): array { + $return = []; + $fileList = $this->getElementsFromSession(); + foreach ($fileList as $fileElement) { + list($type, $timestamp) = explode('_', pathinfo($fileElement->getName(), PATHINFO_FILENAME)); + $return[] = [ + 'type' => $type, + 'file' => [ + 'url' => $this->urlGenerator->linkToRoute('ocs.libresign.account.getSignatureElementPreview', [ + 'apiVersion' => 'v1', + 'fileId' => $fileElement->getId(), + ]), + 'fileId' => $fileElement->getId(), + ], + 'starred' => 0, + 'createdAt' => (new \DateTime())->setTimestamp((int) $timestamp)->format('Y-m-d H:i:s'), + ]; + } + return $return; + } +} diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index f4d7086bf3..4206fbfc92 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -27,7 +27,6 @@ use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Handler\CertificateEngine\Handler as CertificateEngineHandler; use OCA\Libresign\Service\IdentifyMethodService; -use OCA\Libresign\Service\SignatureMethodService; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Services\IInitialState; @@ -40,7 +39,6 @@ public function __construct( private IdentifyMethodService $identifyMethodService, private CertificateEngineHandler $certificateEngineHandler, private IAppConfig $appConfig, - private SignatureMethodService $SignatureMethodService, ) { } public function getForm(): TemplateResponse { @@ -49,10 +47,6 @@ public function getForm(): TemplateResponse { 'identify_methods', $this->identifyMethodService->getIdentifyMethodsSettings() ); - $this->initialState->provideInitialState( - 'signature_methods', - $this->SignatureMethodService->getMethods() - ); $this->initialState->provideInitialState( 'certificate_engine', $this->certificateEngineHandler->getEngine()->getName() diff --git a/package-lock.json b/package-lock.json index 8df9a5a3be..589cdf7726 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@nextcloud/paths": "^2.1.0", "@nextcloud/router": "^3.0.0", "@nextcloud/vue": "^8.6.1", + "blueimp-md5": "^2.19.0", "crypto-js": "^4.2.0", "dompurify": "^3.0.8", "linkify-string": "^4.1.3", @@ -5870,6 +5871,11 @@ "dev": true, "peer": true }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" + }, "node_modules/bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", diff --git a/package.json b/package.json index 79db3f682f..6cf1f99d68 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@nextcloud/paths": "^2.1.0", "@nextcloud/router": "^3.0.0", "@nextcloud/vue": "^8.6.1", + "blueimp-md5": "^2.19.0", "crypto-js": "^4.2.0", "dompurify": "^3.0.8", "linkify-string": "^4.1.3", diff --git a/psalm.xml b/psalm.xml index c7e6af85c3..6e8a7d648b 100644 --- a/psalm.xml +++ b/psalm.xml @@ -32,4 +32,7 @@ + + + diff --git a/src/Components/File/AppFilesTab.vue b/src/Components/File/AppFilesTab.vue index 44d8fb2ecb..60dcd7fce8 100644 --- a/src/Components/File/AppFilesTab.vue +++ b/src/Components/File/AppFilesTab.vue @@ -48,7 +48,7 @@ export default { this.signers = [] this.file = { nodeId: fileInfo.id, - name: fileInfo.name + name: fileInfo.name, } this.requestedBy = {} this.requestDate = '' diff --git a/src/external.js b/src/external.js index 6813e5b77a..b933126796 100644 --- a/src/external.js +++ b/src/external.js @@ -24,6 +24,7 @@ import { generateFilePath } from '@nextcloud/router' import { getRequestToken } from '@nextcloud/auth' import Vue from 'vue' +import { createPinia, PiniaVuePlugin } from 'pinia' import External from './External.vue' import router from './router/router.js' @@ -51,9 +52,14 @@ Vue.prototype.n = n Vue.prototype.OC = OC Vue.prototype.OCA = OCA +Vue.use(PiniaVuePlugin) + +const pinia = createPinia() + export default new Vue({ el: '#content', router, store, + pinia, render: h => h(External), }) diff --git a/src/store/signMethods.js b/src/store/signMethods.js new file mode 100644 index 0000000000..28c9a7c5d6 --- /dev/null +++ b/src/store/signMethods.js @@ -0,0 +1,73 @@ +/* + * @copyright Copyright (c) 2024 Vitor Mattos + * + * @author Vitor Mattos + * + * @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 . + */ + +import { defineStore } from 'pinia' +import { set } from 'vue' +import { loadState } from '@nextcloud/initial-state' + +export const useSignMethodsStore = defineStore('signMethods', { + state: () => ({ + modal: { + emailToken: false, + clickToSign: false, + createPassword: false, + signPassword: false, + createSignature: false, + sms: false, + }, + settings: loadState('libresign', 'signature_methods', []), + }), + actions: { + closeModal(modalCode) { + set(this.modal, modalCode, false) + }, + showModal(modalCode) { + set(this.modal, modalCode, true) + }, + blurredEmail() { + return this.settings.emailToken.blurredEmail + }, + hasEmailConfirmCode(hasConfirmCode) { + set(this.settings.emailToken, 'hasConfirmCode', hasConfirmCode) + }, + setEmailToken(token) { + set(this.settings.emailToken, 'token', token) + }, + hasSignatureFile(hasSignatureFile) { + set(this.signMethodsStore.settings.password, 'hasSignatureFile', hasSignatureFile) + }, + needCreatePassword() { + return Object.hasOwn(this.settings, 'password') + && !this.settings.password.hasSignatureFile + }, + needEmailCode() { + return Object.hasOwn(this.settings, 'emailToken') + && this.settings.emailToken.needCode + }, + needClickToSign() { + return Object.hasOwn(this.settings, 'clickToSign') + }, + needSmsCode() { + return Object.hasOwn(this.settings, 'sms') + && this.settings.sms.needCode + }, + }, +}) diff --git a/src/views/Settings/CertificateCustonOptions.vue b/src/views/Settings/CertificateCustonOptions.vue index 0a54404dbf..8c9589589b 100644 --- a/src/views/Settings/CertificateCustonOptions.vue +++ b/src/views/Settings/CertificateCustonOptions.vue @@ -87,8 +87,7 @@ export default { return item.value.length >= item.min }, validateMax(item) { - // eslint-disable-next-line no-prototype-builtins - if (item.hasOwnProperty('max')) { + if (Object.hasOwn(item, 'max')) { return item.value.length <= item.max } return true diff --git a/src/views/Settings/IdentifierFactor.vue b/src/views/Settings/IdentifierFactor.vue index fad943d3ac..aae9659784 100644 --- a/src/views/Settings/IdentifierFactor.vue +++ b/src/views/Settings/IdentifierFactor.vue @@ -3,46 +3,34 @@

-
- - {{ option.friendly_name }} - -
- - {{ t('libresign', 'Make this method required') }} - -
-
-
- - {{ option.friendly_name }} - -
- + + {{ option.friendly_name }} + +
+
+ {{ t('libresign', 'Request to create account when the user does not have an account') }} - -
-
-
- - {{ option.friendly_name }} - -
-
- - {{ t('libresign', 'Make this method required') }} - -
-
+ + +
+ + {{ t('libresign', 'Make this method required') }} + +
+
+ {{ t('libresign', 'Signature methods') }} + + {{ method.label }} + +
@@ -51,7 +39,6 @@ import { translate as t } from '@nextcloud/l10n' import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js' import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' -import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox.js' import { loadState } from '@nextcloud/initial-state' export default { @@ -59,7 +46,6 @@ export default { components: { NcSettingsSection, NcCheckboxRadioSwitch, - NcActionCheckbox, }, data() { const identifyMethod = loadState('libresign', 'identify_methods') @@ -73,6 +59,7 @@ export default { }, mounted() { this.flagAccountIfAllDisabled() + this.flagFirstSignatureMethodIfAllDisabled() }, methods: { flagAccountIfAllDisabled() { @@ -86,33 +73,61 @@ export default { .enabled = true } }, + flagFirstSignatureMethodIfAllDisabled() { + this.options.forEach(item => { + const allDisabled = Object.values(item.signatureMethods) + .filter(item => item.enabled) + .length === 0 + if (allDisabled) { + // Enable the first signature method + Object.keys(item.signatureMethods).every(methodId => { + item.signatureMethods[methodId].enabled = true + return false + }) + } + }) + }, save() { this.flagAccountIfAllDisabled() + this.flagFirstSignatureMethodIfAllDisabled() + // Get only enabled + let props = this.options.filter(item => item.enabled) + // Remove label from signature method, we don't need to save this + props = JSON.parse(JSON.stringify(props)) + .map(item => { + Object.keys(item.signatureMethods).forEach(id => { + Object.keys(item.signatureMethods[id]).forEach(signatureMethdoPropName => { + if (signatureMethdoPropName === 'label') { + delete item.signatureMethods[id][signatureMethdoPropName] + } + }) + }) + return item + }) OCP.AppConfig.setValue('libresign', 'identify_methods', - JSON.stringify( - this.options.filter(item => item.enabled), - ), + JSON.stringify(props), ) }, }, } diff --git a/src/views/Settings/Settings.vue b/src/views/Settings/Settings.vue index 4910c37d5b..ab7616b964 100644 --- a/src/views/Settings/Settings.vue +++ b/src/views/Settings/Settings.vue @@ -29,7 +29,6 @@ - @@ -55,7 +54,6 @@ import IdentificationDocuments from './IdentificationDocuments.vue' import CollectMetadata from './CollectMetadata.vue' import DefaultUserFolder from './DefaultUserFolder.vue' import IdentifierFactor from './IdentifierFactor.vue' -import SignatureMethods from './SignatureMethods.vue' export default { name: 'Settings', @@ -67,7 +65,6 @@ export default { RootCertificateCfssl, RootCertificateOpenSsl, IdentifierFactor, - SignatureMethods, ExpirationRules, Validation, AllowedGroups, diff --git a/src/views/Settings/SignatureMethods.vue b/src/views/Settings/SignatureMethods.vue deleted file mode 100644 index d24836b2e0..0000000000 --- a/src/views/Settings/SignatureMethods.vue +++ /dev/null @@ -1,53 +0,0 @@ - - diff --git a/src/views/SignPDF/_partials/ModalEmailManager.vue b/src/views/SignPDF/_partials/ModalEmailManager.vue index 0e4e72c841..9db28949e6 100644 --- a/src/views/SignPDF/_partials/ModalEmailManager.vue +++ b/src/views/SignPDF/_partials/ModalEmailManager.vue @@ -8,10 +8,10 @@
-