feat: add PdfSignatureDetectionService for detecting existing signatures

This service provides a dedicated, testable way to detect if a PDF
already contains signatures. It encapsulates the signature detection
logic that was previously scattered across multiple handlers.

The service creates a memory resource from PDF content and uses the
SignEngineFactory to check for existing certificate chains. It handles
exceptions gracefully and returns false for any detection failures.

Tests use real PDF fixtures (signed and unsigned) instead of mocks,
providing better confidence in the detection logic.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
This commit is contained in:
Vitor Mattos 2025-12-09 00:43:43 +00:00
parent 252018adb3
commit cd7db4ea37
2 changed files with 101 additions and 0 deletions

View file

@ -0,0 +1,48 @@
<?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\Handler\SignEngine\SignEngineFactory;
use Psr\Log\LoggerInterface;
class PdfSignatureDetectionService {
public function __construct(
private SignEngineFactory $signEngineFactory,
private LoggerInterface $logger,
) {
}
/**
* Check if a PDF has existing signatures
*
* @param string $pdfContent The PDF file content
* @return bool True if the file has signatures, false otherwise
*/
public function hasSignatures(string $pdfContent): bool {
$resource = fopen('php://memory', 'r+');
if ($resource === false) {
$this->logger->warning('Failed to create resource for signature detection');
return false;
}
fwrite($resource, $pdfContent);
rewind($resource);
try {
$engine = $this->signEngineFactory->resolve('pdf');
$certificates = $engine->getCertificateChain($resource);
return !empty($certificates);
} catch (\Throwable $e) {
$this->logger->debug('Failed to detect signatures: ' . $e->getMessage());
return false;
} finally {
fclose($resource);
}
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Libresign\Tests\Unit\Service;
use OCA\Libresign\Handler\SignEngine\SignEngineFactory;
use OCA\Libresign\Service\PdfSignatureDetectionService;
use OCA\Libresign\Tests\Unit\PdfFixtureTrait;
use OCA\Libresign\Tests\Unit\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use Psr\Log\LoggerInterface;
class PdfSignatureDetectionServiceTest extends TestCase {
use PdfFixtureTrait;
private PdfSignatureDetectionService $service;
public function setUp(): void {
parent::setUp();
$signEngineFactory = \OCP\Server::get(SignEngineFactory::class);
$logger = \OCP\Server::get(LoggerInterface::class);
$this->service = new PdfSignatureDetectionService(
$signEngineFactory,
$logger
);
}
public static function pdfContentProvider(): array {
$fixture = new class { use PdfFixtureTrait; };
return [
'signed PDF with DocMDP level 1' => [fn() => $fixture->createPdfWithDocMdp(1), true],
'signed PDF with DocMDP level 2' => [fn() => $fixture->createPdfWithDocMdp(2), true],
'signed PDF with DocMDP level 3' => [fn() => $fixture->createPdfWithDocMdp(3), true],
'unsigned minimal PDF' => [fn() => $fixture->createMinimalPdf(), false],
'empty string' => [fn() => '', false],
'invalid content' => [fn() => 'not a valid pdf content', false],
];
}
#[DataProvider('pdfContentProvider')]
public function testHasSignatures(callable $pdfProvider, bool $expected): void {
$result = $this->service->hasSignatures($pdfProvider());
$this->assertSame($expected, $result);
}
}