mirror of
https://github.com/LibreSign/libresign.git
synced 2025-12-18 05:20:45 +01:00
When adding a new signer with status 0, the backend was ignoring it because empty() treated 0 as falsy. Changed to isset() to properly handle status 0. Also updated determineInitialStatus() to allow new signers to be added in DRAFT mode even when the file is not in DRAFT status, allowing gradual signer addition before requesting signatures. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
444 lines
14 KiB
PHP
444 lines
14 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);
|
|
|
|
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,
|
|
);
|
|
}
|
|
} 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,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
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,
|
|
): 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) {
|
|
$initialStatus = $this->determineInitialStatus($signingOrder, $fileStatus);
|
|
$signRequest->setStatusEnum($initialStatus);
|
|
}
|
|
|
|
$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 determineInitialStatus(int $signingOrder, ?int $fileStatus = null): \OCA\Libresign\Enum\SignRequestStatus {
|
|
// 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 (!$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());
|
|
}
|
|
}
|