mirror of
https://github.com/LibreSign/libresign.git
synced 2025-12-18 05:20:45 +01:00
Remove internal validation methods from RequestSignatureService and delegate to SequentialSigningService for better separation of concerns. Changes: - Remove hasPendingLowerOrderSigners() private method - Remove isStatusUpgrade() private method - Replace inline ordering validation with call to validateStatusByOrder() - Simplify determineInitialStatus() by delegating validation logic This reduces complexity in RequestSignatureService and makes the code more maintainable by following single responsibility principle. All sequential signing logic is now centralized in the specialized service. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
491 lines
16 KiB
PHP
491 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
/**
|
|
* SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\Libresign\Service;
|
|
|
|
use OCA\Libresign\Db\File as FileEntity;
|
|
use OCA\Libresign\Db\FileElementMapper;
|
|
use OCA\Libresign\Db\FileMapper;
|
|
use OCA\Libresign\Db\IdentifyMethodMapper;
|
|
use OCA\Libresign\Db\SignRequest as SignRequestEntity;
|
|
use OCA\Libresign\Db\SignRequestMapper;
|
|
use OCA\Libresign\Handler\DocMdpHandler;
|
|
use OCA\Libresign\Helper\ValidateHelper;
|
|
use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
use OCP\Files\IMimeTypeDetector;
|
|
use OCP\Files\Node;
|
|
use OCP\Http\Client\IClientService;
|
|
use OCP\IL10N;
|
|
use OCP\IUser;
|
|
use OCP\IUserManager;
|
|
use Psr\Log\LoggerInterface;
|
|
use Sabre\DAV\UUIDUtil;
|
|
|
|
class RequestSignatureService {
|
|
use TFile;
|
|
|
|
public function __construct(
|
|
protected IL10N $l10n,
|
|
protected IdentifyMethodService $identifyMethod,
|
|
protected SignRequestMapper $signRequestMapper,
|
|
protected IUserManager $userManager,
|
|
protected FileMapper $fileMapper,
|
|
protected IdentifyMethodMapper $identifyMethodMapper,
|
|
protected PdfParserService $pdfParserService,
|
|
protected FileElementService $fileElementService,
|
|
protected FileElementMapper $fileElementMapper,
|
|
protected FolderService $folderService,
|
|
protected IMimeTypeDetector $mimeTypeDetector,
|
|
protected ValidateHelper $validateHelper,
|
|
protected IClientService $client,
|
|
protected DocMdpHandler $docMdpHandler,
|
|
protected LoggerInterface $logger,
|
|
protected SequentialSigningService $sequentialSigningService,
|
|
) {
|
|
}
|
|
|
|
public function save(array $data): FileEntity {
|
|
$file = $this->saveFile($data);
|
|
$this->saveVisibleElements($data, $file);
|
|
if (!isset($data['status'])) {
|
|
$data['status'] = $file->getStatus();
|
|
}
|
|
$this->associateToSigners($data, $file->getId());
|
|
return $file;
|
|
}
|
|
|
|
/**
|
|
* Save file data
|
|
*
|
|
* @param array{?userManager: IUser, ?signRequest: SignRequest, name: string, callback: string, uuid?: ?string, status: int, file?: array{fileId?: int, fileNode?: Node}} $data
|
|
*/
|
|
public function saveFile(array $data): FileEntity {
|
|
if (!empty($data['uuid'])) {
|
|
$file = $this->fileMapper->getByUuid($data['uuid']);
|
|
return $this->updateStatus($file, $data['status'] ?? 0);
|
|
}
|
|
$fileId = null;
|
|
if (isset($data['file']['fileNode']) && $data['file']['fileNode'] instanceof Node) {
|
|
$fileId = $data['file']['fileNode']->getId();
|
|
} elseif (!empty($data['file']['fileId'])) {
|
|
$fileId = $data['file']['fileId'];
|
|
}
|
|
if (!is_null($fileId)) {
|
|
try {
|
|
$file = $this->fileMapper->getByFileId($fileId);
|
|
return $this->updateStatus($file, $data['status'] ?? 0);
|
|
} catch (\Throwable) {
|
|
}
|
|
}
|
|
|
|
$node = $this->getNodeFromData($data);
|
|
|
|
$file = new FileEntity();
|
|
$file->setNodeId($node->getId());
|
|
if ($data['userManager'] instanceof IUser) {
|
|
$file->setUserId($data['userManager']->getUID());
|
|
} elseif ($data['signRequest'] instanceof SignRequestEntity) {
|
|
$file->setSignRequestId($data['signRequest']->getId());
|
|
}
|
|
$file->setUuid(UUIDUtil::getUUID());
|
|
$file->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
|
|
$metadata = $this->getFileMetadata($node);
|
|
$file->setName($this->removeExtensionFromName($data['name'], $metadata));
|
|
$file->setMetadata($metadata);
|
|
if (!empty($data['callback'])) {
|
|
$file->setCallback($data['callback']);
|
|
}
|
|
if (isset($data['status'])) {
|
|
$file->setStatus($data['status']);
|
|
} else {
|
|
$file->setStatus(FileEntity::STATUS_ABLE_TO_SIGN);
|
|
}
|
|
$this->fileMapper->insert($file);
|
|
return $file;
|
|
}
|
|
|
|
private function updateStatus(FileEntity $file, int $status): FileEntity {
|
|
if ($status > $file->getStatus()) {
|
|
$file->setStatus($status);
|
|
/** @var FileEntity */
|
|
return $this->fileMapper->update($file);
|
|
}
|
|
return $file;
|
|
}
|
|
|
|
private function getFileMetadata(\OCP\Files\Node $node): array {
|
|
$metadata = [];
|
|
if ($extension = strtolower($node->getExtension())) {
|
|
$metadata = [
|
|
'extension' => $extension,
|
|
];
|
|
if ($metadata['extension'] === 'pdf') {
|
|
$metadata = array_merge(
|
|
$metadata,
|
|
$this->pdfParserService
|
|
->setFile($node)
|
|
->getPageDimensions()
|
|
);
|
|
}
|
|
}
|
|
return $metadata;
|
|
}
|
|
|
|
private function removeExtensionFromName(string $name, array $metadata): string {
|
|
if (!isset($metadata['extension'])) {
|
|
return $name;
|
|
}
|
|
$extensionPattern = '/\.' . preg_quote($metadata['extension'], '/') . '$/i';
|
|
$result = preg_replace($extensionPattern, '', $name);
|
|
return $result ?? $name;
|
|
}
|
|
|
|
private function deleteIdentifyMethodIfNotExits(array $users, int $fileId): void {
|
|
$file = $this->fileMapper->getById($fileId);
|
|
$signRequests = $this->signRequestMapper->getByFileId($fileId);
|
|
foreach ($signRequests as $key => $signRequest) {
|
|
$identifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequest->getId());
|
|
if (empty($identifyMethods)) {
|
|
$this->unassociateToUser($file->getNodeId(), $signRequest->getId());
|
|
continue;
|
|
}
|
|
foreach ($identifyMethods as $methodName => $list) {
|
|
foreach ($list as $method) {
|
|
$exists[$key]['identify'][$methodName] = $method->getEntity()->getIdentifierValue();
|
|
if (!$this->identifyMethodExists($users, $method)) {
|
|
$this->unassociateToUser($file->getNodeId(), $signRequest->getId());
|
|
continue 3;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private function identifyMethodExists(array $users, IIdentifyMethod $identifyMethod): bool {
|
|
foreach ($users as $user) {
|
|
if (!empty($user['identifyMethods'])) {
|
|
foreach ($user['identifyMethods'] as $data) {
|
|
if ($identifyMethod->getEntity()->getIdentifierKey() !== $data['method']) {
|
|
continue;
|
|
}
|
|
if ($identifyMethod->getEntity()->getIdentifierValue() === $data['value']) {
|
|
return true;
|
|
}
|
|
}
|
|
} else {
|
|
foreach ($user['identify'] as $method => $value) {
|
|
if ($identifyMethod->getEntity()->getIdentifierKey() !== $method) {
|
|
continue;
|
|
}
|
|
if ($identifyMethod->getEntity()->getIdentifierValue() === $value) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return SignRequestEntity[]
|
|
*
|
|
* @psalm-return list<SignRequestEntity>
|
|
*/
|
|
private function associateToSigners(array $data, int $fileId): array {
|
|
$return = [];
|
|
if (!empty($data['users'])) {
|
|
$this->deleteIdentifyMethodIfNotExits($data['users'], $fileId);
|
|
|
|
$this->sequentialSigningService->resetOrderCounter();
|
|
$fileStatus = $data['status'] ?? null;
|
|
|
|
foreach ($data['users'] as $user) {
|
|
$userProvidedOrder = isset($user['signingOrder']) ? (int)$user['signingOrder'] : null;
|
|
$signingOrder = $this->sequentialSigningService->determineSigningOrder($userProvidedOrder);
|
|
$signerStatus = $user['status'] ?? null;
|
|
|
|
if (isset($user['identifyMethods'])) {
|
|
foreach ($user['identifyMethods'] as $identifyMethod) {
|
|
$return[] = $this->associateToSigner(
|
|
identifyMethods: [
|
|
$identifyMethod['method'] => $identifyMethod['value'],
|
|
],
|
|
displayName: $user['displayName'] ?? '',
|
|
description: $user['description'] ?? '',
|
|
notify: empty($user['notify']) && $this->isStatusAbleToNotify($fileStatus),
|
|
fileId: $fileId,
|
|
signingOrder: $signingOrder,
|
|
fileStatus: $fileStatus,
|
|
signerStatus: $signerStatus,
|
|
);
|
|
}
|
|
} else {
|
|
$return[] = $this->associateToSigner(
|
|
identifyMethods: $user['identify'],
|
|
displayName: $user['displayName'] ?? '',
|
|
description: $user['description'] ?? '',
|
|
notify: empty($user['notify']) && $this->isStatusAbleToNotify($fileStatus),
|
|
fileId: $fileId,
|
|
signingOrder: $signingOrder,
|
|
fileStatus: $fileStatus,
|
|
signerStatus: $signerStatus,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return $return;
|
|
}
|
|
|
|
private function isStatusAbleToNotify(?int $status): bool {
|
|
return in_array($status, [
|
|
FileEntity::STATUS_ABLE_TO_SIGN,
|
|
FileEntity::STATUS_PARTIAL_SIGNED,
|
|
]);
|
|
}
|
|
|
|
private function associateToSigner(
|
|
array $identifyMethods,
|
|
string $displayName,
|
|
string $description,
|
|
bool $notify,
|
|
int $fileId,
|
|
int $signingOrder = 0,
|
|
?int $fileStatus = null,
|
|
?int $signerStatus = null,
|
|
): SignRequestEntity {
|
|
$identifyMethodsIncances = $this->identifyMethod->getByUserData($identifyMethods);
|
|
if (empty($identifyMethodsIncances)) {
|
|
throw new \Exception($this->l10n->t('Invalid identification method'));
|
|
}
|
|
$signRequest = $this->getSignRequestByIdentifyMethod(
|
|
current($identifyMethodsIncances),
|
|
$fileId
|
|
);
|
|
$displayName = $this->getDisplayNameFromIdentifyMethodIfEmpty($identifyMethodsIncances, $displayName);
|
|
$this->setDataToUser($signRequest, $displayName, $description, $fileId);
|
|
|
|
$signRequest->setSigningOrder($signingOrder);
|
|
|
|
$isNewSignRequest = !$signRequest->getId();
|
|
$currentStatus = $signRequest->getStatusEnum();
|
|
|
|
if ($isNewSignRequest || $currentStatus === \OCA\Libresign\Enum\SignRequestStatus::DRAFT) {
|
|
$desiredStatus = $this->determineInitialStatus($signingOrder, $fileStatus, $signerStatus, $currentStatus, $fileId);
|
|
$this->updateStatusIfAllowed($signRequest, $currentStatus, $desiredStatus, $isNewSignRequest);
|
|
}
|
|
|
|
$this->saveSignRequest($signRequest);
|
|
|
|
$shouldNotify = $notify && $signRequest->getStatusEnum() === \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN;
|
|
|
|
foreach ($identifyMethodsIncances as $identifyMethod) {
|
|
$identifyMethod->getEntity()->setSignRequestId($signRequest->getId());
|
|
$identifyMethod->willNotifyUser($shouldNotify);
|
|
$identifyMethod->save();
|
|
}
|
|
return $signRequest;
|
|
}
|
|
|
|
private function updateStatusIfAllowed(
|
|
SignRequestEntity $signRequest,
|
|
\OCA\Libresign\Enum\SignRequestStatus $currentStatus,
|
|
\OCA\Libresign\Enum\SignRequestStatus $desiredStatus,
|
|
bool $isNewSignRequest
|
|
): void {
|
|
if ($isNewSignRequest || $this->sequentialSigningService->isStatusUpgrade($currentStatus, $desiredStatus)) {
|
|
$signRequest->setStatusEnum($desiredStatus);
|
|
}
|
|
}
|
|
|
|
private function determineInitialStatus(
|
|
int $signingOrder,
|
|
?int $fileStatus = null,
|
|
?int $signerStatus = null,
|
|
?\OCA\Libresign\Enum\SignRequestStatus $currentStatus = null,
|
|
?int $fileId = null
|
|
): \OCA\Libresign\Enum\SignRequestStatus {
|
|
if ($signerStatus !== null) {
|
|
$desiredStatus = \OCA\Libresign\Enum\SignRequestStatus::from($signerStatus);
|
|
if ($currentStatus !== null && !$this->sequentialSigningService->isStatusUpgrade($currentStatus, $desiredStatus)) {
|
|
return $currentStatus;
|
|
}
|
|
|
|
// Validate status transition based on signing order
|
|
if ($fileId !== null) {
|
|
return $this->sequentialSigningService->validateStatusByOrder($desiredStatus, $signingOrder, $fileId);
|
|
}
|
|
|
|
return $desiredStatus;
|
|
}
|
|
|
|
// If fileStatus is explicitly DRAFT (0), keep signer as DRAFT
|
|
// This allows adding new signers in DRAFT mode even when file is not in DRAFT status
|
|
if ($fileStatus === FileEntity::STATUS_DRAFT) {
|
|
return \OCA\Libresign\Enum\SignRequestStatus::DRAFT;
|
|
}
|
|
|
|
if ($fileStatus === FileEntity::STATUS_ABLE_TO_SIGN) {
|
|
if ($this->sequentialSigningService->isOrderedNumericFlow()) {
|
|
// In ordered flow, only first signer (order 1) should be ABLE_TO_SIGN
|
|
// Others remain DRAFT until their turn
|
|
return $signingOrder === 1
|
|
? \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN
|
|
: \OCA\Libresign\Enum\SignRequestStatus::DRAFT;
|
|
}
|
|
// In parallel flow, all can sign
|
|
return \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN;
|
|
}
|
|
|
|
if (!$this->sequentialSigningService->isOrderedNumericFlow()) {
|
|
return \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN;
|
|
}
|
|
|
|
return $signingOrder === 1
|
|
? \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN
|
|
: \OCA\Libresign\Enum\SignRequestStatus::DRAFT;
|
|
}
|
|
|
|
/**
|
|
* @param IIdentifyMethod[] $identifyMethodsIncances
|
|
* @param string $displayName
|
|
* @return string
|
|
*/
|
|
private function getDisplayNameFromIdentifyMethodIfEmpty(array $identifyMethodsIncances, string $displayName): string {
|
|
if (!empty($displayName)) {
|
|
return $displayName;
|
|
}
|
|
foreach ($identifyMethodsIncances as $identifyMethod) {
|
|
if ($identifyMethod->getName() === 'account') {
|
|
return $this->userManager->get($identifyMethod->getEntity()->getIdentifierValue())->getDisplayName();
|
|
}
|
|
}
|
|
foreach ($identifyMethodsIncances as $identifyMethod) {
|
|
if ($identifyMethod->getName() !== 'account') {
|
|
return $identifyMethod->getEntity()->getIdentifierValue();
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
private function saveVisibleElements(array $data, FileEntity $file): array {
|
|
if (empty($data['visibleElements'])) {
|
|
return [];
|
|
}
|
|
$elements = $data['visibleElements'];
|
|
foreach ($elements as $key => $element) {
|
|
$element['fileId'] = $file->getId();
|
|
$elements[$key] = $this->fileElementService->saveVisibleElement($element);
|
|
}
|
|
return $elements;
|
|
}
|
|
|
|
public function validateNewRequestToFile(array $data): void {
|
|
$this->validateNewFile($data);
|
|
$this->validateUsers($data);
|
|
$this->validateHelper->validateFileStatus($data);
|
|
}
|
|
|
|
public function validateNewFile(array $data): void {
|
|
if (empty($data['name'])) {
|
|
throw new \Exception($this->l10n->t('Name is mandatory'));
|
|
}
|
|
$this->validateHelper->validateNewFile($data);
|
|
}
|
|
|
|
public function validateUsers(array $data): void {
|
|
if (empty($data['users'])) {
|
|
throw new \Exception($this->l10n->t('Empty users list'));
|
|
}
|
|
if (!is_array($data['users'])) {
|
|
// TRANSLATION This message will be displayed when the request to API with the key users has a value that is not an array
|
|
throw new \Exception($this->l10n->t('User list needs to be an array'));
|
|
}
|
|
foreach ($data['users'] as $user) {
|
|
if (!array_key_exists('identify', $user)) {
|
|
throw new \Exception('Identify key not found');
|
|
}
|
|
$this->identifyMethod->setAllEntityData($user);
|
|
}
|
|
}
|
|
|
|
public function saveSignRequest(SignRequestEntity $signRequest): void {
|
|
if ($signRequest->getId()) {
|
|
$this->signRequestMapper->update($signRequest);
|
|
} else {
|
|
$this->signRequestMapper->insert($signRequest);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @psalm-suppress MixedMethodCall
|
|
*/
|
|
private function setDataToUser(SignRequestEntity $signRequest, string $displayName, string $description, int $fileId): void {
|
|
$signRequest->setFileId($fileId);
|
|
if (!$signRequest->getUuid()) {
|
|
$signRequest->setUuid(UUIDUtil::getUUID());
|
|
}
|
|
if (!empty($displayName)) {
|
|
$signRequest->setDisplayName($displayName);
|
|
}
|
|
if (!empty($description)) {
|
|
$signRequest->setDescription($description);
|
|
}
|
|
if (!$signRequest->getId()) {
|
|
$signRequest->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC')));
|
|
}
|
|
}
|
|
|
|
private function getSignRequestByIdentifyMethod(IIdentifyMethod $identifyMethod, int $fileId): SignRequestEntity {
|
|
try {
|
|
$signRequest = $this->signRequestMapper->getByIdentifyMethodAndFileId($identifyMethod, $fileId);
|
|
} catch (DoesNotExistException) {
|
|
$signRequest = new SignRequestEntity();
|
|
}
|
|
return $signRequest;
|
|
}
|
|
|
|
public function unassociateToUser(int $fileId, int $signRequestId): void {
|
|
$signRequest = $this->signRequestMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
|
|
$deletedOrder = $signRequest->getSigningOrder();
|
|
|
|
try {
|
|
$this->signRequestMapper->delete($signRequest);
|
|
$groupedIdentifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequestId);
|
|
foreach ($groupedIdentifyMethods as $identifyMethods) {
|
|
foreach ($identifyMethods as $identifyMethod) {
|
|
$identifyMethod->delete();
|
|
}
|
|
}
|
|
$visibleElements = $this->fileElementMapper->getByFileIdAndSignRequestId($fileId, $signRequestId);
|
|
foreach ($visibleElements as $visibleElement) {
|
|
$this->fileElementMapper->delete($visibleElement);
|
|
}
|
|
|
|
$this->sequentialSigningService->reorderAfterDeletion($fileId, $deletedOrder);
|
|
} catch (\Throwable) {
|
|
}
|
|
}
|
|
|
|
public function deleteRequestSignature(array $data): void {
|
|
if (!empty($data['uuid'])) {
|
|
$signatures = $this->signRequestMapper->getByFileUuid($data['uuid']);
|
|
$fileData = $this->fileMapper->getByUuid($data['uuid']);
|
|
} elseif (!empty($data['file']['fileId'])) {
|
|
$signatures = $this->signRequestMapper->getByNodeId($data['file']['fileId']);
|
|
$fileData = $this->fileMapper->getByFileId($data['file']['fileId']);
|
|
} else {
|
|
throw new \Exception($this->l10n->t('Please provide either UUID or File object'));
|
|
}
|
|
foreach ($signatures as $signRequest) {
|
|
$this->signRequestMapper->delete($signRequest);
|
|
}
|
|
$this->fileMapper->delete($fileData);
|
|
$this->fileElementService->deleteVisibleElements($fileData->getId());
|
|
}
|
|
}
|