saveFile($data); $this->saveVisibleElements($data, $file); if (!isset($data['status'])) { $data['status'] = $file->getStatus(); } $this->sequentialSigningService->setFile($file); $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']); $this->updateSignatureFlowIfAllowed($file, $data); return $this->fileStatusService->updateFileStatusIfUpgrade($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); $this->updateSignatureFlowIfAllowed($file, $data); return $this->fileStatusService->updateFileStatusIfUpgrade($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->setSignatureFlow($file, $data); $this->setDocMdpLevelFromGlobalConfig($file); $this->fileMapper->insert($file); return $file; } private function updateSignatureFlowIfAllowed(FileEntity $file, array $data): void { $adminFlow = $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', SignatureFlow::NONE->value); $adminForcedConfig = $adminFlow !== SignatureFlow::NONE->value; if ($adminForcedConfig) { $adminFlowEnum = SignatureFlow::from($adminFlow); if ($file->getSignatureFlowEnum() !== $adminFlowEnum) { $file->setSignatureFlowEnum($adminFlowEnum); $this->fileMapper->update($file); } return; } if (isset($data['signatureFlow']) && !empty($data['signatureFlow'])) { $newFlow = SignatureFlow::from($data['signatureFlow']); if ($file->getSignatureFlowEnum() !== $newFlow) { $file->setSignatureFlowEnum($newFlow); $this->fileMapper->update($file); } } } private function setSignatureFlow(FileEntity $file, array $data): void { $adminFlow = $this->appConfig->getValueString(Application::APP_ID, 'signature_flow', SignatureFlow::NONE->value); if (isset($data['signatureFlow']) && !empty($data['signatureFlow'])) { $file->setSignatureFlowEnum(SignatureFlow::from($data['signatureFlow'])); } elseif ($adminFlow !== SignatureFlow::NONE->value) { $file->setSignatureFlowEnum(SignatureFlow::from($adminFlow)); } else { $file->setSignatureFlowEnum(SignatureFlow::NONE); } } private function setDocMdpLevelFromGlobalConfig(FileEntity $file): void { if ($this->docMdpConfigService->isEnabled()) { $docmdpLevel = $this->docMdpConfigService->getLevel(); $file->setDocmdpLevelEnum($docmdpLevel); } } 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 */ 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']), 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']), fileId: $fileId, signingOrder: $signingOrder, fileStatus: $fileStatus, signerStatus: $signerStatus, ); } } } return $return; } 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->signRequestStatusService->determineInitialStatus($signingOrder, $fileId, $fileStatus, $signerStatus, $currentStatus); $this->signRequestStatusService->updateStatusIfAllowed($signRequest, $currentStatus, $desiredStatus, $isNewSignRequest); } $this->saveSignRequest($signRequest); $shouldNotify = $notify && $this->signRequestStatusService->shouldNotifySignRequest( $signRequest->getStatusEnum(), $fileStatus ); foreach ($identifyMethodsIncances as $identifyMethod) { $identifyMethod->getEntity()->setSignRequestId($signRequest->getId()); $identifyMethod->willNotifyUser($shouldNotify); $identifyMethod->save(); } return $signRequest; } /** * @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(); $groupedIdentifyMethods = $this->identifyMethod->getIdentifyMethodsFromSignRequestId($signRequestId); $this->dispatchCancellationEventIfNeeded($signRequest, $fileId, $groupedIdentifyMethods); try { $this->signRequestMapper->delete($signRequest); 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) { } } private function dispatchCancellationEventIfNeeded( SignRequestEntity $signRequest, int $fileId, array $groupedIdentifyMethods, ): void { if ($signRequest->getStatus() !== \OCA\Libresign\Enum\SignRequestStatus::ABLE_TO_SIGN->value) { return; } try { $libreSignFile = $this->fileMapper->getByFileId($fileId); foreach ($groupedIdentifyMethods as $identifyMethods) { foreach ($identifyMethods as $identifyMethod) { $event = new SignRequestCanceledEvent( $signRequest, $libreSignFile, $identifyMethod, ); $this->eventDispatcher->dispatchTyped($event); } } } catch (\Throwable $e) { $this->logger->error('Error dispatching SignRequestCanceledEvent: ' . $e->getMessage(), ['exception' => $e]); } } 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()); } }