From 88d96bbc30c62719524a5c4e711e27d5411f57dc Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:26:35 -0300 Subject: [PATCH 01/15] refactor: extract active signings service Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Controller/AdminController.php | 21 +- lib/Service/ActiveSigningsService.php | 135 ++++++++++ .../Unit/Controller/AdminControllerTest.php | 111 ++++++++ .../Service/ActiveSigningsServiceTest.php | 254 ++++++++++++++++++ 4 files changed, 503 insertions(+), 18 deletions(-) create mode 100644 lib/Service/ActiveSigningsService.php create mode 100644 tests/php/Unit/Controller/AdminControllerTest.php create mode 100644 tests/php/Unit/Service/ActiveSigningsServiceTest.php diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index d419de581f..cb26ffac3b 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -10,10 +10,9 @@ use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Controller\Traits\UploadValidator; -use OCA\Libresign\Db\FileMapper; -use OCA\Libresign\Enum\FileStatus; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Handler\CertificateEngine\IEngineHandler; +use OCA\Libresign\Service\ActiveSigningsService; use OCA\Libresign\Service\Certificate\ValidateService; use OCA\Libresign\Service\CertificatePolicyService; use OCA\Libresign\Service\IdentifyMethodService; @@ -67,7 +66,7 @@ public function __construct( private CertificatePolicyService $certificatePolicyService, private ValidateService $validateService, private IdentifyMethodService $identifyMethodService, - private FileMapper $fileMapper, + private ActiveSigningsService $activeSigningsService, private SetupCheckResultService $setupCheckResultService, ) { parent::__construct(Application::APP_ID, $request); @@ -461,22 +460,8 @@ public function updateOid(string $oid): DataResponse { #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/admin/active-signings', requirements: ['apiVersion' => '(v1)'])] public function getActiveSignings(): DataResponse { try { - $activeSignings = $this->fileMapper->findByStatus(FileStatus::SIGNING_IN_PROGRESS->value); - - $result = []; - foreach ($activeSignings as $file) { - $result[] = [ - 'id' => $file->getId(), - 'uuid' => $file->getUuid(), - 'name' => $file->getName(), - 'signerEmail' => $file->getSignerEmail() ?? '', - 'signerDisplayName' => $file->getSignerName() ?? '', - 'updatedAt' => $file->getUpdatedAt(), - ]; - } - return new DataResponse([ - 'data' => $result, + 'data' => $this->activeSigningsService->getActiveSignings(), ]); } catch (\Exception $e) { return new DataResponse([ diff --git a/lib/Service/ActiveSigningsService.php b/lib/Service/ActiveSigningsService.php new file mode 100644 index 0000000000..6aa4f50400 --- /dev/null +++ b/lib/Service/ActiveSigningsService.php @@ -0,0 +1,135 @@ + + */ + public function getActiveSignings(): array { + $activeSignings = $this->fileMapper->findByStatus(FileStatus::SIGNING_IN_PROGRESS->value); + + $result = []; + foreach ($activeSignings as $file) { + $activeSigner = $this->resolveActiveSigner($file); + $result[] = [ + 'id' => $file->getId(), + 'uuid' => $file->getUuid(), + 'name' => $file->getName(), + 'signerEmail' => $activeSigner['email'], + 'signerDisplayName' => $activeSigner['displayName'], + 'updatedAt' => $this->resolveActiveSigningUpdatedAt($file), + ]; + } + + return $result; + } + + /** + * @return array{email: string, displayName: string} + */ + private function resolveActiveSigner(FileEntity $file): array { + $signRequest = $this->findActiveSignRequest($this->signRequestMapper->getByFileId($file->getId())); + if (!$signRequest instanceof SignRequestEntity) { + return [ + 'email' => '', + 'displayName' => '', + ]; + } + + $identifiedMethod = $this->findPreferredIdentifyMethod( + $this->identifyMethodMapper->getIdentifyMethodsFromSignRequestId($signRequest->getId()) + ); + + $signerEmail = ''; + if ($identifiedMethod instanceof IdentifyMethodEntity + && $identifiedMethod->getIdentifierKey() === self::EMAIL_IDENTIFY_METHOD + ) { + $signerEmail = $identifiedMethod->getIdentifierValue(); + } + + return [ + 'email' => $signerEmail, + 'displayName' => $signRequest->getDisplayName(), + ]; + } + + /** + * @param list $signRequests + */ + private function findActiveSignRequest(array $signRequests): ?SignRequestEntity { + foreach ($signRequests as $signRequest) { + if ($signRequest->getStatusEnum() === SignRequestStatus::ABLE_TO_SIGN) { + return $signRequest; + } + } + + foreach ($signRequests as $signRequest) { + if ($signRequest->getSigned() === null) { + return $signRequest; + } + } + + return $signRequests[0] ?? null; + } + + /** + * @param list $identifyMethods + */ + private function findPreferredIdentifyMethod(array $identifyMethods): ?IdentifyMethodEntity { + $firstMethod = null; + + foreach ($identifyMethods as $identifyMethod) { + $firstMethod ??= $identifyMethod; + + if ($identifyMethod->getIdentifiedAtDate() !== null) { + return $identifyMethod; + } + } + + return $firstMethod; + } + + private function resolveActiveSigningUpdatedAt(FileEntity $file): int { + $metadata = $file->getMetadata(); + if (is_array($metadata) && isset($metadata['status_changed_at']) && is_string($metadata['status_changed_at'])) { + $statusChangedAt = date_create_immutable($metadata['status_changed_at']); + if ($statusChangedAt instanceof \DateTimeImmutable) { + return $statusChangedAt->getTimestamp(); + } + } + + return $file->getCreatedAt()?->getTimestamp() ?? 0; + } +} diff --git a/tests/php/Unit/Controller/AdminControllerTest.php b/tests/php/Unit/Controller/AdminControllerTest.php new file mode 100644 index 0000000000..8658253b16 --- /dev/null +++ b/tests/php/Unit/Controller/AdminControllerTest.php @@ -0,0 +1,111 @@ +request = $this->createMock(IRequest::class); + $appConfig = $this->createMock(IAppConfig::class); + $installService = $this->createMock(InstallService::class); + $certificateEngineFactory = $this->createMock(CertificateEngineFactory::class); + $eventSourceFactory = $this->createMock(IEventSourceFactory::class); + $l10n = $this->createMock(IL10N::class); + $session = $this->createMock(ISession::class); + $signatureBackgroundService = $this->createMock(SignatureBackgroundService::class); + $certificatePolicyService = $this->createMock(CertificatePolicyService::class); + $validateService = $this->createMock(ValidateService::class); + $identifyMethodService = $this->createMock(IdentifyMethodService::class); + $this->activeSigningsService = $this->createMock(ActiveSigningsService::class); + $setupCheckResultService = $this->createMock(SetupCheckResultService::class); + $eventSource = $this->createMock(IEventSource::class); + + $eventSourceFactory + ->method('create') + ->willReturn($eventSource); + + $this->controller = new AdminController( + $this->request, + $appConfig, + $installService, + $certificateEngineFactory, + $eventSourceFactory, + $l10n, + $session, + $signatureBackgroundService, + $certificatePolicyService, + $validateService, + $identifyMethodService, + $this->activeSigningsService, + $setupCheckResultService, + ); + } + + public function testGetActiveSigningsReturnsServicePayload(): void { + $this->activeSigningsService + ->expects($this->once()) + ->method('getActiveSignings') + ->willReturn([[ + 'id' => 7, + 'uuid' => 'uuid-7', + 'name' => 'Contract.pdf', + 'signerEmail' => 'signer@example.com', + 'signerDisplayName' => 'Active signer', + 'updatedAt' => 1751891696, + ]]); + + $response = $this->controller->getActiveSignings(); + + $this->assertSame(Http::STATUS_OK, $response->getStatus()); + $this->assertSame([ + 'data' => [[ + 'id' => 7, + 'uuid' => 'uuid-7', + 'name' => 'Contract.pdf', + 'signerEmail' => 'signer@example.com', + 'signerDisplayName' => 'Active signer', + 'updatedAt' => 1751891696, + ]], + ], $response->getData()); + } + + public function testGetActiveSigningsReturnsErrorResponseWhenServiceFails(): void { + $this->activeSigningsService + ->expects($this->once()) + ->method('getActiveSignings') + ->willThrowException(new \RuntimeException('boom')); + + $response = $this->controller->getActiveSignings(); + + $this->assertSame(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus()); + $this->assertSame(['error' => 'boom'], $response->getData()); + } +} diff --git a/tests/php/Unit/Service/ActiveSigningsServiceTest.php b/tests/php/Unit/Service/ActiveSigningsServiceTest.php new file mode 100644 index 0000000000..c070642fbb --- /dev/null +++ b/tests/php/Unit/Service/ActiveSigningsServiceTest.php @@ -0,0 +1,254 @@ +> */ + private array $filesByStatus = []; + /** @var array> */ + private array $signRequestsByFileId = []; + /** @var array> */ + private array $identifyMethodsBySignRequestId = []; + + protected function setUp(): void { + $this->fileMapper = $this->createStub(FileMapper::class); + $this->fileMapper + ->method('findByStatus') + ->willReturnCallback(fn (int $status): array => $this->filesByStatus[$status] ?? []); + + $this->signRequestMapper = $this->createStub(SignRequestMapper::class); + $this->signRequestMapper + ->method('getByFileId') + ->willReturnCallback(fn (int $fileId): array => $this->signRequestsByFileId[$fileId] ?? []); + + $this->identifyMethodMapper = $this->createStub(IdentifyMethodMapper::class); + $this->identifyMethodMapper + ->method('getIdentifyMethodsFromSignRequestId') + ->willReturnCallback(fn (int $signRequestId): array => $this->identifyMethodsBySignRequestId[$signRequestId] ?? []); + + $this->service = new ActiveSigningsService( + $this->fileMapper, + $this->signRequestMapper, + $this->identifyMethodMapper, + ); + } + + #[DataProvider('provideActiveSigningScenarios')] + public function testGetActiveSignings( + array $files, + array $signRequestsByFileId, + array $identifyMethodsBySignRequestId, + array $expected, + ): void { + $this->filesByStatus[FileStatus::SIGNING_IN_PROGRESS->value] = array_map( + fn (array $definition): FileEntity => $this->buildFile($definition), + $files, + ); + $this->signRequestsByFileId = $this->buildSignRequestsByFileId($signRequestsByFileId); + $this->identifyMethodsBySignRequestId = $this->buildIdentifyMethodsBySignRequestId($identifyMethodsBySignRequestId); + + $this->assertSame($expected, $this->service->getActiveSignings()); + } + + public static function provideActiveSigningScenarios(): array { + return [ + 'resolves active signer and status-changed timestamp' => [ + 'files' => [[ + 'id' => 7, + 'uuid' => 'uuid-7', + 'name' => 'Contract.pdf', + 'status' => FileStatus::SIGNING_IN_PROGRESS->value, + 'createdAt' => '2026-07-07T10:00:00+00:00', + 'metadata' => [ + 'status_changed_at' => '2025-07-07T12:34:56+00:00', + ], + ]], + 'signRequestsByFileId' => [ + 7 => [ + [ + 'id' => 10, + 'fileId' => 7, + 'displayName' => 'Completed signer', + 'status' => SignRequestStatus::SIGNED, + 'signed' => '2026-07-07 10:10:00', + ], + [ + 'id' => 11, + 'fileId' => 7, + 'displayName' => 'Active signer', + 'status' => SignRequestStatus::ABLE_TO_SIGN, + ], + ], + ], + 'identifyMethodsBySignRequestId' => [ + 11 => [[ + 'identifierKey' => IdentifyMethodService::IDENTIFY_EMAIL, + 'identifierValue' => 'signer@example.com', + 'identifiedAtDate' => '2025-07-07T12:00:00+00:00', + ]], + ], + 'expected' => [[ + 'id' => 7, + 'uuid' => 'uuid-7', + 'name' => 'Contract.pdf', + 'signerEmail' => 'signer@example.com', + 'signerDisplayName' => 'Active signer', + 'updatedAt' => 1751891696, + ]], + ], + 'falls back to created-at and unsigned signer' => [ + 'files' => [[ + 'id' => 9, + 'uuid' => 'uuid-9', + 'name' => 'Fallback.pdf', + 'status' => FileStatus::SIGNING_IN_PROGRESS->value, + 'createdAt' => '2026-07-07T09:30:00+00:00', + 'metadata' => [ + 'status_changed_at' => 'not-a-date', + ], + ]], + 'signRequestsByFileId' => [ + 9 => [[ + 'id' => 19, + 'fileId' => 9, + 'displayName' => 'Pending signer', + 'status' => SignRequestStatus::DRAFT, + ]], + ], + 'identifyMethodsBySignRequestId' => [ + 19 => [], + ], + 'expected' => [[ + 'id' => 9, + 'uuid' => 'uuid-9', + 'name' => 'Fallback.pdf', + 'signerEmail' => '', + 'signerDisplayName' => 'Pending signer', + 'updatedAt' => 1783416600, + ]], + ], + 'returns empty signer data when no sign request exists' => [ + 'files' => [[ + 'id' => 22, + 'uuid' => 'uuid-22', + 'name' => 'NoSigner.pdf', + 'status' => FileStatus::SIGNING_IN_PROGRESS->value, + 'createdAt' => '2026-07-07T08:00:00+00:00', + 'metadata' => [], + ]], + 'signRequestsByFileId' => [ + 22 => [], + ], + 'identifyMethodsBySignRequestId' => [], + 'expected' => [[ + 'id' => 22, + 'uuid' => 'uuid-22', + 'name' => 'NoSigner.pdf', + 'signerEmail' => '', + 'signerDisplayName' => '', + 'updatedAt' => 1783411200, + ]], + ], + ]; + } + + /** + * @param array{ + * id: int, + * uuid: string, + * name: string, + * status: int, + * createdAt: string, + * metadata: array + * } $definition + */ + private function buildFile(array $definition): FileEntity { + $file = new FileEntity(); + $file->setId($definition['id']); + $file->setUuid($definition['uuid']); + $file->setName($definition['name']); + $file->setStatus($definition['status']); + $file->setCreatedAt(new \DateTime($definition['createdAt'])); + $file->setMetadata($definition['metadata']); + return $file; + } + + /** + * @param array> $definitionsByFileId + * @return array> + */ + private function buildSignRequestsByFileId(array $definitionsByFileId): array { + $signRequestsByFileId = []; + foreach ($definitionsByFileId as $fileId => $definitions) { + foreach ($definitions as $definition) { + $signRequestsByFileId[$fileId][] = $this->buildSignRequest($definition); + } + } + return $signRequestsByFileId; + } + + /** + * @param array{id: int, fileId: int, displayName: string, status: SignRequestStatus, signed?: string} $definition + */ + private function buildSignRequest(array $definition): SignRequestEntity { + $signRequest = new SignRequestEntity(); + $signRequest->setId($definition['id']); + $signRequest->setFileId($definition['fileId']); + $signRequest->setDisplayName($definition['displayName']); + $signRequest->setStatusEnum($definition['status']); + if (isset($definition['signed'])) { + $signRequest->setSigned($definition['signed']); + } + return $signRequest; + } + + /** + * @param array> $definitionsBySignRequestId + * @return array> + */ + private function buildIdentifyMethodsBySignRequestId(array $definitionsBySignRequestId): array { + $identifyMethodsBySignRequestId = []; + foreach ($definitionsBySignRequestId as $signRequestId => $definitions) { + foreach ($definitions as $definition) { + $identifyMethodsBySignRequestId[$signRequestId][] = $this->buildIdentifyMethod($signRequestId, $definition); + } + } + return $identifyMethodsBySignRequestId; + } + + /** + * @param array{identifierKey: string, identifierValue: string, identifiedAtDate?: ?string} $definition + */ + private function buildIdentifyMethod(int $signRequestId, array $definition): IdentifyMethodEntity { + $identifyMethod = new IdentifyMethodEntity(); + $identifyMethod->setSignRequestId($signRequestId); + $identifyMethod->setIdentifierKey($definition['identifierKey']); + $identifyMethod->setIdentifierValue($definition['identifierValue']); + $identifyMethod->setIdentifiedAtDate($definition['identifiedAtDate'] ?? null); + return $identifyMethod; + } +} From 79baaf4e66d10e48cbfe03e90d43da30a8ba0d99 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:26:40 -0300 Subject: [PATCH 02/15] fix: restore files integration events Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/AppInfo/Application.php | 3 +-- lib/Listener/LoadAdditionalListener.php | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 7aa3640063..9658c04361 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -8,7 +8,6 @@ namespace OCA\Libresign\AppInfo; -use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Files\Event\LoadSidebar; use OCA\Libresign\Activity\Listener as ActivityListener; use OCA\Libresign\Capabilities; @@ -66,7 +65,7 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(SignedEvent::class, SignedCallbackListener::class); // Files newFile listener - $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); + $context->registerEventListener('OCA\\Files\\Event\\LoadAdditionalScriptsEvent', LoadAdditionalListener::class); // Activity listeners $context->registerEventListener(SendSignNotificationEvent::class, ActivityListener::class); diff --git a/lib/Listener/LoadAdditionalListener.php b/lib/Listener/LoadAdditionalListener.php index ba2e87dfcc..53def2647b 100644 --- a/lib/Listener/LoadAdditionalListener.php +++ b/lib/Listener/LoadAdditionalListener.php @@ -8,7 +8,6 @@ namespace OCA\Libresign\Listener; -use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCP\App\IAppManager; @@ -17,9 +16,11 @@ use OCP\Util; /** - * @template-implements IEventListener + * @template-implements IEventListener */ class LoadAdditionalListener implements IEventListener { + private const FILES_LOAD_ADDITIONAL_SCRIPTS_EVENT = 'OCA\\Files\\Event\\LoadAdditionalScriptsEvent'; + public function __construct( private IAppManager $appManager, private CertificateEngineFactory $certificateEngineFactory, @@ -27,7 +28,7 @@ public function __construct( } #[\Override] public function handle(Event $event): void { - if (!($event instanceof LoadAdditionalScriptsEvent)) { + if (!is_a($event, self::FILES_LOAD_ADDITIONAL_SCRIPTS_EVENT)) { return; } From e52b49ee6c15014d047dddfb853fb85057727321 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:26:50 -0300 Subject: [PATCH 03/15] fix: normalize CRL serial handling Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/CertificateEngine/AEngineHandler.php | 13 +++++++++++-- .../CertificateEngine/OpenSslHandlerTest.php | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/Handler/CertificateEngine/AEngineHandler.php b/lib/Handler/CertificateEngine/AEngineHandler.php index 98e9d37379..827cb2fb46 100644 --- a/lib/Handler/CertificateEngine/AEngineHandler.php +++ b/lib/Handler/CertificateEngine/AEngineHandler.php @@ -896,9 +896,8 @@ private function createAndSignCrl(\OCA\Libresign\Vendor\phpseclib3\File\X509 $is $dateFormat = 'D, d M Y H:i:s O'; foreach ($revokedCertificates as $cert) { $serialNumber = $cert->getSerialNumber(); - $normalizedSerial = ltrim((string)$serialNumber, '0') ?: '0'; $crlToSign->revoke( - new \OCA\Libresign\Vendor\phpseclib3\Math\BigInteger($normalizedSerial, 16), + $this->normalizeRevokedCertificateSerialForCrl((string)$serialNumber), $cert->getRevokedAt()->format($dateFormat) ); } @@ -920,6 +919,16 @@ private function createAndSignCrl(\OCA\Libresign\Vendor\phpseclib3\File\X509 $is return $signedCrl; } + private function normalizeRevokedCertificateSerialForCrl(string $serialNumber): string { + $normalizedSerial = ltrim($serialNumber, '0') ?: '0'; + + if (ctype_digit($normalizedSerial)) { + return $normalizedSerial; + } + + return (new \OCA\Libresign\Vendor\phpseclib3\Math\BigInteger($normalizedSerial, 16))->toString(); + } + private function saveCrlToDer(array $signedCrl, string $configPath): string { $crlDerPath = $configPath . DIRECTORY_SEPARATOR . 'crl.der'; $crlToSign = new \OCA\Libresign\Vendor\phpseclib3\File\X509(); diff --git a/tests/php/Unit/Handler/CertificateEngine/OpenSslHandlerTest.php b/tests/php/Unit/Handler/CertificateEngine/OpenSslHandlerTest.php index 189e74dd5e..486bd79eeb 100644 --- a/tests/php/Unit/Handler/CertificateEngine/OpenSslHandlerTest.php +++ b/tests/php/Unit/Handler/CertificateEngine/OpenSslHandlerTest.php @@ -647,6 +647,10 @@ public function testGenerateCrlDerWithRevokedCertificates(array $certificates): public static function dataCrlSerialNumberNormalization(): array { return [ + 'Decimal serial stays decimal semantically and is rendered as hex in CRL' => [ + 'serialNumber' => '1234', + 'expectedInCrl' => '04D2' + ], 'Serial with leading zeros (20 chars)' => [ 'serialNumber' => '00e7a0b277a1008f5fe3', 'expectedInCrl' => 'E7A0B277A1008F5FE3' From acba9f9a239ed8bd6f92b11349348d13f45553f2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:26:59 -0300 Subject: [PATCH 04/15] chore: tighten psalm analysis Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Command/Configure/Cfssl.php | 2 + lib/Command/Configure/Check.php | 2 + lib/Command/Configure/OpenSsl.php | 2 + lib/Command/Developer/Reset.php | 3 + lib/Command/Developer/SignSetup.php | 3 + lib/Command/Install.php | 2 + lib/Command/Uninstall.php | 2 + lib/Dav/SignatureStatusPlugin.php | 1 + lib/Db/File.php | 2 +- lib/Db/IdDocsMapper.php | 3 +- lib/Db/IdentifyMethodMapper.php | 2 +- lib/Db/PagerFantaQueryAdapter.php | 4 +- lib/Db/PermissionSet.php | 2 +- .../CertificateEngine/IEngineHandler.php | 7 +- lib/Helper/Pagination.php | 1 + .../Version16001Date20251227000000.php | 3 +- .../Version8000Date20240405142042.php | 2 + .../File/ValidationMetadataNormalizer.php | 5 +- lib/Service/Install/InstallService.php | 4 +- lib/Service/Install/SignSetupService.php | 18 ++- .../IdentifyMethodsPolicyValue.php | 2 +- lib/Service/RequestSignatureService.php | 15 +- lib/Service/SignFileService.php | 2 +- psalm.xml | 46 +++++- tests/psalm-baseline.xml | 135 +----------------- tests/stubs/OC/Core/Command/Base.php | 16 +++ .../Hooks/Emitter.php} | 4 +- .../Provider/Gateway/Factory.php | 16 +++ .../Provider/Gateway/IGateway.php | 18 +++ .../Http/Client/ClientInterface.php} | 0 30 files changed, 161 insertions(+), 163 deletions(-) create mode 100644 tests/stubs/OC/Core/Command/Base.php rename tests/stubs/{oc_hooks_emitter.php => OC/Hooks/Emitter.php} (94%) create mode 100644 tests/stubs/OCA/TwoFactorGateway/Provider/Gateway/Factory.php create mode 100644 tests/stubs/OCA/TwoFactorGateway/Provider/Gateway/IGateway.php rename tests/stubs/{psr_http_client.php => Psr/Http/Client/ClientInterface.php} (100%) diff --git a/lib/Command/Configure/Cfssl.php b/lib/Command/Configure/Cfssl.php index d4154d85e8..928b20f00d 100644 --- a/lib/Command/Configure/Cfssl.php +++ b/lib/Command/Configure/Cfssl.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Output\OutputInterface; class Cfssl extends Base { + #[\Override] protected function configure(): void { $this ->setName('libresign:configure:cfssl') @@ -69,6 +70,7 @@ protected function configure(): void { ); } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { if (!$this->installService->isCfsslBinInstalled()) { throw new InvalidArgumentException('CFSSL binary not found! run libresign:istall --cfssl first.'); diff --git a/lib/Command/Configure/Check.php b/lib/Command/Configure/Check.php index 9847f21bd8..e9752ef36c 100644 --- a/lib/Command/Configure/Check.php +++ b/lib/Command/Configure/Check.php @@ -26,6 +26,7 @@ public function __construct( parent::__construct(); } + #[\Override] protected function configure(): void { $this ->setName('libresign:configure:check') @@ -44,6 +45,7 @@ protected function configure(): void { ); } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { $sign = $input->getOption('sign'); $certificate = $input->getOption('certificate'); diff --git a/lib/Command/Configure/OpenSsl.php b/lib/Command/Configure/OpenSsl.php index 09ba70cee3..ec9ce2588d 100644 --- a/lib/Command/Configure/OpenSsl.php +++ b/lib/Command/Configure/OpenSsl.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Output\OutputInterface; class OpenSsl extends Base { + #[\Override] protected function configure(): void { $this ->setName('libresign:configure:openssl') @@ -63,6 +64,7 @@ protected function configure(): void { ); } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { $names = []; if (!$commonName = $input->getOption('cn')) { diff --git a/lib/Command/Developer/Reset.php b/lib/Command/Developer/Reset.php index a17a563b77..3432b46ab7 100644 --- a/lib/Command/Developer/Reset.php +++ b/lib/Command/Developer/Reset.php @@ -28,10 +28,12 @@ public function __construct( parent::__construct(); } + #[\Override] public function isEnabled(): bool { return $this->config->getSystemValue('debug', false) === true; } + #[\Override] protected function configure(): void { $this ->setName('libresign:developer:reset') @@ -105,6 +107,7 @@ protected function configure(): void { ; } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { $ok = false; diff --git a/lib/Command/Developer/SignSetup.php b/lib/Command/Developer/SignSetup.php index 090a169502..488a756152 100644 --- a/lib/Command/Developer/SignSetup.php +++ b/lib/Command/Developer/SignSetup.php @@ -29,10 +29,12 @@ public function __construct( parent::__construct(); } + #[\Override] public function isEnabled(): bool { return $this->config->getSystemValue('debug', false) === true; } + #[\Override] protected function configure(): void { $this ->setName('libresign:developer:sign-setup') @@ -42,6 +44,7 @@ protected function configure(): void { ; } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { $privateKeyPath = $input->getOption('privateKey'); $keyBundlePath = $input->getOption('certificate'); diff --git a/lib/Command/Install.php b/lib/Command/Install.php index c2777a21ee..42e25820f4 100644 --- a/lib/Command/Install.php +++ b/lib/Command/Install.php @@ -26,6 +26,7 @@ public function __construct( parent::__construct($installService, $logger); } + #[\Override] protected function configure(): void { $this ->setName('libresign:install') @@ -82,6 +83,7 @@ protected function configure(): void { } } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { $ok = false; $this->installService->setOutput($output); diff --git a/lib/Command/Uninstall.php b/lib/Command/Uninstall.php index 767fc3af7d..2dc2ff36a2 100644 --- a/lib/Command/Uninstall.php +++ b/lib/Command/Uninstall.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Output\OutputInterface; class Uninstall extends Base { + #[\Override] protected function configure(): void { $this ->setName('libresign:uninstall') @@ -55,6 +56,7 @@ protected function configure(): void { ); } + #[\Override] protected function execute(InputInterface $input, OutputInterface $output): int { $ok = false; diff --git a/lib/Dav/SignatureStatusPlugin.php b/lib/Dav/SignatureStatusPlugin.php index 7eaa051878..e763275219 100644 --- a/lib/Dav/SignatureStatusPlugin.php +++ b/lib/Dav/SignatureStatusPlugin.php @@ -16,6 +16,7 @@ use Sabre\DAV\ServerPlugin; class SignatureStatusPlugin extends ServerPlugin { + #[\Override] public function initialize(Server $server): void { $server->on('propFind', $this->propFind(...)); } diff --git a/lib/Db/File.php b/lib/Db/File.php index 4dc8722c42..1f56a3a21c 100644 --- a/lib/Db/File.php +++ b/lib/Db/File.php @@ -24,7 +24,7 @@ * @method void setSignedHash(?string $hash) * @method ?string getSignedHash() * @method void setUserId(?string $userId) - * @method ?string getUserId() + * @method string getUserId() * @method void setUuid(string $uuid) * @method string getUuid() * @method void setCreatedAt(\DateTime $createdAt) diff --git a/lib/Db/IdDocsMapper.php b/lib/Db/IdDocsMapper.php index 112e74b03f..55f13c36e5 100644 --- a/lib/Db/IdDocsMapper.php +++ b/lib/Db/IdDocsMapper.php @@ -12,7 +12,6 @@ use OCA\Libresign\Helper\Pagination; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\DB\Types; use OCP\IDBConnection; use OCP\IL10N; use OCP\IURLGenerator; @@ -210,7 +209,7 @@ private function getQueryBuilder(array $filter = [], bool $count = false): IQuer if (!empty($filter['approved']) && $filter['approved'] === 'yes') { $qb->andWhere( - $qb->expr()->eq('f.status', $qb->createNamedParameter(FileStatus::SIGNED->value, Types::INTEGER)) + $qb->expr()->eq('f.status', $qb->createNamedParameter(FileStatus::SIGNED->value, IQueryBuilder::PARAM_INT)) ); } diff --git a/lib/Db/IdentifyMethodMapper.php b/lib/Db/IdentifyMethodMapper.php index 43e0ad1767..5ac7933391 100644 --- a/lib/Db/IdentifyMethodMapper.php +++ b/lib/Db/IdentifyMethodMapper.php @@ -120,7 +120,7 @@ public function searchByIdentifierValue(string $search, string $userId, string $ $latestQb = $this->db->getQueryBuilder(); $latestQb->select('im2.identifier_key') ->addSelect('im2.identifier_value') - ->addSelect($latestQb->func()->max('sr2.created_at', 'created_at')) + ->selectAlias($latestQb->func()->max('sr2.created_at'), 'created_at') ->from('libresign_identify_method', 'im2') ->join('im2', 'libresign_sign_request', 'sr2', $latestQb->expr()->eq('sr2.id', 'im2.sign_request_id') diff --git a/lib/Db/PagerFantaQueryAdapter.php b/lib/Db/PagerFantaQueryAdapter.php index e8dd24d68f..7e6b8ff6a2 100644 --- a/lib/Db/PagerFantaQueryAdapter.php +++ b/lib/Db/PagerFantaQueryAdapter.php @@ -15,6 +15,8 @@ /** * Adapter which calculates pagination from a Doctrine DBAL QueryBuilder. + * + * @implements AdapterInterface> */ class PagerFantaQueryAdapter implements AdapterInterface { /** @@ -41,7 +43,7 @@ public function getNbResults(): int { /** * @psalm-suppress MixedReturnStatement * - * @return array + * @return array> */ #[\Override] public function getSlice(int $offset, int $length): iterable { diff --git a/lib/Db/PermissionSet.php b/lib/Db/PermissionSet.php index 556df9c4a8..e3db91cda5 100644 --- a/lib/Db/PermissionSet.php +++ b/lib/Db/PermissionSet.php @@ -22,7 +22,7 @@ * @method ?string getDescription() * @method void setScopeType(string $scopeType) * @method string getScopeType() - * @method void setEnabled(int $enabled) + * @method void setEnabled(bool $enabled) * @method void setPriority(int $priority) * @method int getPriority() * @method void setCreatedAt(\DateTime|string $createdAt) diff --git a/lib/Handler/CertificateEngine/IEngineHandler.php b/lib/Handler/CertificateEngine/IEngineHandler.php index 4d0c79ce61..464525dd65 100644 --- a/lib/Handler/CertificateEngine/IEngineHandler.php +++ b/lib/Handler/CertificateEngine/IEngineHandler.php @@ -79,9 +79,10 @@ public function toArray(): array; public function generateCrlDer(array $revokedCertificates, string $instanceId, int $generation, int $crlNumber): string; /** - * Parse an X.509 certificate and return its details with CRL validation - * @param string $certificate PEM-encoded certificate - * @return array Parsed certificate data including CRL validation information + * Scope certificate validation policy resolution to a specific user. + * + * @param string|null $userId User identifier used to resolve validation policies + * @return self */ public function setPolicyUserIdForValidation(?string $userId): self; diff --git a/lib/Helper/Pagination.php b/lib/Helper/Pagination.php index 39379c638b..b7b1d50f82 100644 --- a/lib/Helper/Pagination.php +++ b/lib/Helper/Pagination.php @@ -15,6 +15,7 @@ /** * @psalm-import-type LibresignPagination from \OCA\Libresign\ResponseDefinitions + * @extends Pagerfanta> */ class Pagination extends Pagerfanta { private string $routeName; diff --git a/lib/Migration/Version16001Date20251227000000.php b/lib/Migration/Version16001Date20251227000000.php index 2a4ad09f0d..9b848ae2d8 100644 --- a/lib/Migration/Version16001Date20251227000000.php +++ b/lib/Migration/Version16001Date20251227000000.php @@ -10,6 +10,7 @@ use Closure; use OCP\DB\ISchemaWrapper; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; @@ -82,7 +83,7 @@ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $updateQb->update('libresign_file') ->set('name', $updateQb->createNamedParameter($file['newName'])) - ->where($updateQb->expr()->eq('id', $updateQb->createNamedParameter($file['id'], \OCP\DB\Types::INTEGER))) + ->where($updateQb->expr()->eq('id', $updateQb->createNamedParameter($file['id'], IQueryBuilder::PARAM_INT))) ->executeStatement(); } diff --git a/lib/Migration/Version8000Date20240405142042.php b/lib/Migration/Version8000Date20240405142042.php index 04166915c8..52ecae5638 100644 --- a/lib/Migration/Version8000Date20240405142042.php +++ b/lib/Migration/Version8000Date20240405142042.php @@ -17,6 +17,7 @@ use OCP\Migration\SimpleMigrationStep; class PostgreSQLJsonType extends JsonType { + #[\Override] public function getSQLDeclaration(array $column, AbstractPlatform $platform) { $return = parent::getSQLDeclaration($column, $platform); $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10); @@ -43,6 +44,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); $table = $schema->getTable('libresign_file'); + $changed = false; $newOptions = []; if ($schema->getDatabasePlatform() instanceof PostgreSQLPlatform) { diff --git a/lib/Service/File/ValidationMetadataNormalizer.php b/lib/Service/File/ValidationMetadataNormalizer.php index 04731a8413..be95164cad 100644 --- a/lib/Service/File/ValidationMetadataNormalizer.php +++ b/lib/Service/File/ValidationMetadataNormalizer.php @@ -21,7 +21,7 @@ final class ValidationMetadataNormalizer { /** * @param array $metadata - * @psalm-return array&LibresignValidateMetadata + * @psalm-return LibresignValidateMetadata */ public static function normalize(array $metadata, string $fileName, int $totalPages): array { $normalized = $metadata; @@ -31,6 +31,9 @@ public static function normalize(array $metadata, string $fileName, int $totalPa self::normalizeOptionalScalarFields($normalized); self::normalizeDimensionsField($normalized); + /** + * @var LibresignValidateMetadata $normalized + */ return $normalized; } diff --git a/lib/Service/Install/InstallService.php b/lib/Service/Install/InstallService.php index 10ecbaf7e3..f24ed55350 100644 --- a/lib/Service/Install/InstallService.php +++ b/lib/Service/Install/InstallService.php @@ -95,7 +95,7 @@ public function setArchitecture(string $architecture): self { return $this; } - private function getFolder(string $path = '', ?ISimpleFolder $folder = null, $needToBeEmpty = false): ISimpleFolder { + private function getFolder(string $path = '', ?ISimpleFolder $folder = null, bool $needToBeEmpty = false): ISimpleFolder { if (!$folder) { $folder = $this->appData->getFolder('/'); if (!$path) { @@ -112,7 +112,7 @@ private function getFolder(string $path = '', ?ISimpleFolder $folder = null, $ne return $folder; } try { - $folder = $folder->getFolder($path, $folder); + $folder = $folder->getFolder($path); if ($needToBeEmpty && $path !== $this->architecture) { $folder->delete(); $path = ''; diff --git a/lib/Service/Install/SignSetupService.php b/lib/Service/Install/SignSetupService.php index fff0e4cfa3..302a7e1d3c 100644 --- a/lib/Service/Install/SignSetupService.php +++ b/lib/Service/Install/SignSetupService.php @@ -176,7 +176,7 @@ public function getInstallPath(): string { try { $folder = $this->appData->getFolder('/'); $path = $this->architecture . '/' . $this->getLinuxDistributionToDownloadJava() . '/java'; - $folder = $folder->getFolder($path, $folder); + $folder = $folder->getFolder($path); $path = $this->getDataDir() . '/' . $this->getInternalPathOfFolder($folder); if (is_dir($path)) { return $path; @@ -204,7 +204,7 @@ public function getInstallPath(): string { try { $folder = $this->appData->getFolder('/'); $path = $this->architecture . '/jsignpdf'; - $folder = $folder->getFolder($path, $folder); + $folder = $folder->getFolder($path); $path = $this->getDataDir() . '/' . $this->getInternalPathOfFolder($folder); if (is_dir($path)) { return $path; @@ -223,7 +223,7 @@ public function getInstallPath(): string { try { $folder = $this->appData->getFolder('/'); $path = $this->architecture . '/pdftk'; - $folder = $folder->getFolder($path, $folder); + $folder = $folder->getFolder($path); $path = $this->getDataDir() . '/' . $this->getInternalPathOfFolder($folder); if (is_dir($path)) { return $path; @@ -242,7 +242,7 @@ public function getInstallPath(): string { try { $folder = $this->appData->getFolder('/'); $path = $this->architecture . '/cfssl'; - $folder = $folder->getFolder($path, $folder); + $folder = $folder->getFolder($path); $path = $this->getDataDir() . '/' . $this->getInternalPathOfFolder($folder); if (is_dir($path)) { return $path; @@ -559,7 +559,15 @@ private function getRootCertificatePublicKey(): string { return (string)file_get_contents($localCert); } } - return $this->fileAccessHelper->file_get_contents($this->environmentHelper->getServerRoot() . '/resources/codesigning/root.crt'); + + $rootCertificatePath = $this->environmentHelper->getServerRoot() . '/resources/codesigning/root.crt'; + $rootCertificate = $this->fileAccessHelper->file_get_contents($rootCertificatePath); + + if (!is_string($rootCertificate)) { + throw new LibresignException('Root certificate not found at ' . $rootCertificatePath); + } + + return $rootCertificate; } public function getDevelopCert(): array { diff --git a/lib/Service/Policy/Provider/IdentifyMethods/IdentifyMethodsPolicyValue.php b/lib/Service/Policy/Provider/IdentifyMethods/IdentifyMethodsPolicyValue.php index 44528970e3..de5a0a6b32 100644 --- a/lib/Service/Policy/Provider/IdentifyMethods/IdentifyMethodsPolicyValue.php +++ b/lib/Service/Policy/Provider/IdentifyMethods/IdentifyMethodsPolicyValue.php @@ -36,7 +36,7 @@ public static function normalize(mixed $rawValue, ?IdentifyMethodService $identi } } } - $normalization = self::normalizeFactors($defaultFactors, null, null); + $normalization = self::normalizeFactors($defaultFactors, null); $normalized = $normalization['factors']; if ($catalogFactors !== []) { $normalized = self::mergeFactorsWithCatalog($normalized, $catalogFactors); diff --git a/lib/Service/RequestSignatureService.php b/lib/Service/RequestSignatureService.php index fb9b7b3214..c39501eb41 100644 --- a/lib/Service/RequestSignatureService.php +++ b/lib/Service/RequestSignatureService.php @@ -328,7 +328,20 @@ private function createFileForEnvelope( /** * Save file data * - * @param array{?userManager: IUser, ?signRequest: SignRequestEntity, name: string, callback: string, uuid?: ?string, status: int, file?: array{fileId?: int, fileNode?: Node}} $data + * @param array{ + * userManager?: IUser, + * signRequest?: SignRequestEntity, + * name?: string, + * callback?: string, + * uuid?: ?string, + * status?: int, + * parentFileId?: int, + * file?: array{fileId?: int, nodeId?: int, fileNode?: Node}, + * settings?: array, + * policyOverrides?: array, + * policyActiveContext?: ?array, + * uploadedFile?: array + * } $data */ public function saveFile(array $data): FileEntity { if (!empty($data['uuid'])) { diff --git a/lib/Service/SignFileService.php b/lib/Service/SignFileService.php index 96c73f458b..49d236a2b7 100644 --- a/lib/Service/SignFileService.php +++ b/lib/Service/SignFileService.php @@ -1132,7 +1132,7 @@ protected function getFileToSign(): File { $userId = $this->libreSignFile->getUserId() ?? $this->user?->getUID() - ?? ($this->signRequest?->getUserId() ?? null); + ?? null; $nodeId = $this->libreSignFile->getNodeId(); if ($userId === null) { diff --git a/psalm.xml b/psalm.xml index b00cef1986..3947a439c7 100644 --- a/psalm.xml +++ b/psalm.xml @@ -18,20 +18,45 @@ + + + + + + + + + + + + + + + + + + + + + - - - - @@ -43,8 +68,17 @@ + - - + + + + + diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index f65d7edac0..c67b56af2b 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -1,135 +1,2 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - connection->getDatabasePlatform()]]> - - - - - - - - - getDatabasePlatform()]]> - - - - - newUserMail]]> - newUserMail]]> - - - - - - - - - - - - - - - - - - - - - - + diff --git a/tests/stubs/OC/Core/Command/Base.php b/tests/stubs/OC/Core/Command/Base.php new file mode 100644 index 0000000000..166969e1a1 --- /dev/null +++ b/tests/stubs/OC/Core/Command/Base.php @@ -0,0 +1,16 @@ + Date: Sat, 11 Jul 2026 09:38:46 -0300 Subject: [PATCH 05/15] fix: make psalm config CI compatible Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- psalm.xml | 37 ++++++++++--------- .../DBAL/Platforms/AbstractPlatform.php | 12 ++++++ .../DBAL/Platforms/PostgreSQLPlatform.php | 12 ++++++ .../Doctrine/DBAL/Query/QueryBuilder.php | 13 +++++++ tests/stubs/Doctrine/DBAL/Types/JsonType.php | 15 ++++++++ tests/stubs/Doctrine/DBAL/Types/Types.php | 20 ++++++++++ .../OCA/DAV/Connector/Sabre/Directory.php | 15 ++++++++ tests/stubs/OCA/DAV/Connector/Sabre/File.php | 15 ++++++++ tests/stubs/OCA/Files/Command/ScanAppData.php | 12 ++++++ .../OCA/Files/Controller/ViewController.php | 12 ++++++ tests/stubs/OCA/Files/Event/LoadSidebar.php | 12 ++++++ .../stubs/OCA/Files_Sharing/SharedStorage.php | 13 +++++++ .../OCA/Settings/Mailer/NewUserMailHelper.php | 15 ++++++++ tests/stubs/Sabre/DAV/INode.php | 13 +++++++ tests/stubs/Sabre/DAV/PropFind.php | 14 +++++++ tests/stubs/Sabre/DAV/Server.php | 14 +++++++ tests/stubs/Sabre/DAV/ServerPlugin.php | 13 +++++++ tests/stubs/Sabre/DAV/UUIDUtil.php | 19 ++++++++++ 18 files changed, 258 insertions(+), 18 deletions(-) create mode 100644 tests/stubs/Doctrine/DBAL/Platforms/AbstractPlatform.php create mode 100644 tests/stubs/Doctrine/DBAL/Platforms/PostgreSQLPlatform.php create mode 100644 tests/stubs/Doctrine/DBAL/Query/QueryBuilder.php create mode 100644 tests/stubs/Doctrine/DBAL/Types/JsonType.php create mode 100644 tests/stubs/Doctrine/DBAL/Types/Types.php create mode 100644 tests/stubs/OCA/DAV/Connector/Sabre/Directory.php create mode 100644 tests/stubs/OCA/DAV/Connector/Sabre/File.php create mode 100644 tests/stubs/OCA/Files/Command/ScanAppData.php create mode 100644 tests/stubs/OCA/Files/Controller/ViewController.php create mode 100644 tests/stubs/OCA/Files/Event/LoadSidebar.php create mode 100644 tests/stubs/OCA/Files_Sharing/SharedStorage.php create mode 100644 tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php create mode 100644 tests/stubs/Sabre/DAV/INode.php create mode 100644 tests/stubs/Sabre/DAV/PropFind.php create mode 100644 tests/stubs/Sabre/DAV/Server.php create mode 100644 tests/stubs/Sabre/DAV/ServerPlugin.php create mode 100644 tests/stubs/Sabre/DAV/UUIDUtil.php diff --git a/psalm.xml b/psalm.xml index 3947a439c7..42d585b4e6 100644 --- a/psalm.xml +++ b/psalm.xml @@ -18,23 +18,6 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/tests/stubs/Doctrine/DBAL/Platforms/AbstractPlatform.php b/tests/stubs/Doctrine/DBAL/Platforms/AbstractPlatform.php new file mode 100644 index 0000000000..48c4a0655b --- /dev/null +++ b/tests/stubs/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -0,0 +1,12 @@ + Date: Sat, 11 Jul 2026 09:49:42 -0300 Subject: [PATCH 06/15] fix: replace psalm stubs with upstream CI context Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/psalm.yml | 28 ++++ lib/Listener/TwofactorGatewayListener.php | 20 +-- .../SignatureMethod/TokenService.php | 33 ++-- .../IdentifyMethod/TwofactorGateway.php | 30 +--- lib/Service/TwofactorGatewayService.php | 129 ++++++++++++++++ psalm.xml | 50 +++--- .../SignatureMethod/TokenServiceTest.php | 134 ++++++++++++++++ .../Service/TwofactorGatewayServiceTest.php | 144 ++++++++++++++++++ .../DBAL/Platforms/AbstractPlatform.php | 12 -- .../DBAL/Platforms/PostgreSQLPlatform.php | 12 -- .../Doctrine/DBAL/Query/QueryBuilder.php | 13 -- tests/stubs/Doctrine/DBAL/Types/JsonType.php | 15 -- tests/stubs/Doctrine/DBAL/Types/Types.php | 20 --- tests/stubs/OC/Core/Command/Base.php | 16 -- tests/stubs/OC/Hooks/Emitter.php | 17 --- .../OCA/DAV/Connector/Sabre/Directory.php | 15 -- tests/stubs/OCA/DAV/Connector/Sabre/File.php | 15 -- tests/stubs/OCA/Files/Command/ScanAppData.php | 12 -- .../OCA/Files/Controller/ViewController.php | 12 -- tests/stubs/OCA/Files/Event/LoadSidebar.php | 12 -- .../stubs/OCA/Files_Sharing/SharedStorage.php | 13 -- .../OCA/Settings/Mailer/NewUserMailHelper.php | 15 -- .../Provider/Gateway/Factory.php | 16 -- .../Provider/Gateway/IGateway.php | 18 --- .../stubs/Psr/Http/Client/ClientInterface.php | 15 -- tests/stubs/Sabre/DAV/INode.php | 13 -- tests/stubs/Sabre/DAV/PropFind.php | 14 -- tests/stubs/Sabre/DAV/Server.php | 14 -- tests/stubs/Sabre/DAV/ServerPlugin.php | 13 -- tests/stubs/Sabre/DAV/UUIDUtil.php | 19 --- 30 files changed, 474 insertions(+), 415 deletions(-) create mode 100644 lib/Service/TwofactorGatewayService.php create mode 100644 tests/php/Unit/Service/IdentifyMethod/SignatureMethod/TokenServiceTest.php create mode 100644 tests/php/Unit/Service/TwofactorGatewayServiceTest.php delete mode 100644 tests/stubs/Doctrine/DBAL/Platforms/AbstractPlatform.php delete mode 100644 tests/stubs/Doctrine/DBAL/Platforms/PostgreSQLPlatform.php delete mode 100644 tests/stubs/Doctrine/DBAL/Query/QueryBuilder.php delete mode 100644 tests/stubs/Doctrine/DBAL/Types/JsonType.php delete mode 100644 tests/stubs/Doctrine/DBAL/Types/Types.php delete mode 100644 tests/stubs/OC/Core/Command/Base.php delete mode 100644 tests/stubs/OC/Hooks/Emitter.php delete mode 100644 tests/stubs/OCA/DAV/Connector/Sabre/Directory.php delete mode 100644 tests/stubs/OCA/DAV/Connector/Sabre/File.php delete mode 100644 tests/stubs/OCA/Files/Command/ScanAppData.php delete mode 100644 tests/stubs/OCA/Files/Controller/ViewController.php delete mode 100644 tests/stubs/OCA/Files/Event/LoadSidebar.php delete mode 100644 tests/stubs/OCA/Files_Sharing/SharedStorage.php delete mode 100644 tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php delete mode 100644 tests/stubs/OCA/TwoFactorGateway/Provider/Gateway/Factory.php delete mode 100644 tests/stubs/OCA/TwoFactorGateway/Provider/Gateway/IGateway.php delete mode 100644 tests/stubs/Psr/Http/Client/ClientInterface.php delete mode 100644 tests/stubs/Sabre/DAV/INode.php delete mode 100644 tests/stubs/Sabre/DAV/PropFind.php delete mode 100644 tests/stubs/Sabre/DAV/Server.php delete mode 100644 tests/stubs/Sabre/DAV/ServerPlugin.php delete mode 100644 tests/stubs/Sabre/DAV/UUIDUtil.php diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index b2c2c3322d..c8ec6be9b0 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -33,6 +33,34 @@ jobs: id: versions uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2.3.1.3.2 + - name: Checkout Nextcloud server for Psalm context + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: nextcloud/server + ref: ${{ steps.versions.outputs.branches-max }} + path: nextcloud-server + persist-credentials: false + fetch-depth: 1 + sparse-checkout: | + 3rdparty/doctrine/dbal/src + 3rdparty/sabre/dav/lib/DAV + 3rdparty/sabre/event/lib + 3rdparty/stecman/symfony-console-completion/src + apps/dav/lib/Connector/Sabre + apps/files/lib + apps/files_sharing/lib + apps/settings/lib/Mailer + core/Command + lib + + - name: Mirror Nextcloud server tree for Psalm + run: | + shared_root="$(dirname "$(dirname "$GITHUB_WORKSPACE")")" + ln -sfn "$GITHUB_WORKSPACE/nextcloud-server/3rdparty" "$shared_root/3rdparty" + ln -sfn "$GITHUB_WORKSPACE/nextcloud-server/apps" "$shared_root/apps" + ln -sfn "$GITHUB_WORKSPACE/nextcloud-server/core" "$shared_root/core" + ln -sfn "$GITHUB_WORKSPACE/nextcloud-server/lib" "$shared_root/lib" + - name: Check enforcement of minimum PHP version ${{ steps.versions.outputs.php-min }} in psalm.xml run: grep 'phpVersion="${{ steps.versions.outputs.php-min }}' psalm.xml diff --git a/lib/Listener/TwofactorGatewayListener.php b/lib/Listener/TwofactorGatewayListener.php index aec5713cd2..e2a8e80bdb 100644 --- a/lib/Listener/TwofactorGatewayListener.php +++ b/lib/Listener/TwofactorGatewayListener.php @@ -17,15 +17,13 @@ use OCA\Libresign\Service\IdentifyMethod\IdentifyService; use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod; use OCA\Libresign\Service\IdentifyMethodService; -use OCA\TwoFactorGateway\Provider\Gateway\Factory; -use OCP\App\IAppManager; +use OCA\Libresign\Service\TwofactorGatewayService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\IUserSession; -use OCP\Server; use Psr\Log\LoggerInterface; /** @template-implements IEventListener */ @@ -35,8 +33,8 @@ public function __construct( protected IUserManager $userManager, protected IdentifyService $identifyService, private SignRequestMapper $signRequestMapper, + private TwofactorGatewayService $twofactorGatewayService, private LoggerInterface $logger, - protected IAppManager $appManager, protected IL10N $l10n, protected IURLGenerator $urlGenerator, ) { @@ -72,7 +70,7 @@ protected function sendSignNotification( if (!in_array($entity->getIdentifierKey(), IdentifyMethodService::IDENTIFY_TWOFACTOR_GATEWAY_METHODS, true)) { return; } - if (!$this->appManager->isEnabledForAnyone('twofactor_gateway')) { + if (!$this->twofactorGatewayService->isEnabled()) { $this->logger->info('Twofactor Gateway app is not enabled'); return; } @@ -97,12 +95,9 @@ protected function sendSignNotification( $link = $this->urlGenerator->linkToRouteAbsolute('libresign.page.sign', ['uuid' => $signRequest->getUuid()]); $message .= $libreSignFile->getName() . ': ' . $link; - /** @var Factory */ - $gatewayFactory = Server::get(Factory::class); $gatewayName = IdentifyMethodService::resolveTwofactorGatewayName($entity->getIdentifierKey()); - $gateway = $gatewayFactory->get($gatewayName); try { - $gateway->send($identifier, $message); + $this->twofactorGatewayService->send($gatewayName, $identifier, $message); } catch (Exception $e) { $this->logger->error('Could not send 2FA message', [ 'identifier' => $identifier, @@ -129,7 +124,7 @@ protected function sendSignedNotification( if (!in_array($entity->getIdentifierKey(), IdentifyMethodService::IDENTIFY_TWOFACTOR_GATEWAY_METHODS, true)) { return; } - if (!$this->appManager->isEnabledForAnyone('twofactor_gateway')) { + if (!$this->twofactorGatewayService->isEnabled()) { $this->logger->info('Twofactor Gateway app is not enabled'); return; } @@ -148,12 +143,9 @@ protected function sendSignedNotification( $message .= "\n"; $message .= $libreSignFile->getName() . ': ' . $link; - /** @var Factory */ - $gatewayFactory = Server::get(Factory::class); $gatewayName = IdentifyMethodService::resolveTwofactorGatewayName($entity->getIdentifierKey()); - $gateway = $gatewayFactory->get($gatewayName); try { - $gateway->send($identifier, $message); + $this->twofactorGatewayService->send($gatewayName, $identifier, $message); } catch (Exception $e) { $this->logger->error('Could not send 2FA message', [ 'identifier' => $identifier, diff --git a/lib/Service/IdentifyMethod/SignatureMethod/TokenService.php b/lib/Service/IdentifyMethod/SignatureMethod/TokenService.php index 1b55845178..983567f23d 100644 --- a/lib/Service/IdentifyMethod/SignatureMethod/TokenService.php +++ b/lib/Service/IdentifyMethod/SignatureMethod/TokenService.php @@ -8,14 +8,12 @@ namespace OCA\Libresign\Service\IdentifyMethod\SignatureMethod; -use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Service\MailService; +use OCA\Libresign\Service\TwofactorGatewayService; use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\IL10N; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; -use OCP\Server; -use Psr\Container\NotFoundExceptionInterface; class TokenService { public const TOKEN_LENGTH = 6; @@ -25,34 +23,25 @@ public function __construct( private IHasher $hasher, private MailService $mail, private IL10N $l10n, + private TwofactorGatewayService $twofactorGatewayService, ) { } public function sendCodeByGateway(string $identifier, string $gatewayName): string { - $gateway = $this->getGateway($gatewayName); + $this->twofactorGatewayService->ensureAvailable($gatewayName); + if (!$this->twofactorGatewayService->isGatewayComplete($gatewayName)) { + throw new OCSForbiddenException($this->l10n->t('Gateway %s not configured on Two-Factor Gateway.', $gatewayName)); + } $code = $this->secureRandom->generate(self::TOKEN_LENGTH, ISecureRandom::CHAR_DIGITS); - $gateway->send($identifier, $this->l10n->t('%s is your LibreSign verification code.', $code)); + $this->twofactorGatewayService->send( + $gatewayName, + $identifier, + $this->l10n->t('%s is your LibreSign verification code.', $code) + ); return $this->hasher->hash($code); } - /** - * @throws OCSForbiddenException - * @return \OCA\TwoFactorGateway\Provider\Gateway\IGateway - */ - private function getGateway(string $gatewayName) { - try { - $factory = Server::get(\OCA\TwoFactorGateway\Provider\Gateway\Factory::class); - } catch (NotFoundExceptionInterface) { - throw new LibresignException('App Two-Factor Gateway is not installed.'); - } - $gateway = $factory->get($gatewayName); - if (!$gateway->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( diff --git a/lib/Service/IdentifyMethod/TwofactorGateway.php b/lib/Service/IdentifyMethod/TwofactorGateway.php index ca5f6f77e7..d28ec384fc 100644 --- a/lib/Service/IdentifyMethod/TwofactorGateway.php +++ b/lib/Service/IdentifyMethod/TwofactorGateway.php @@ -12,14 +12,10 @@ use OCA\Libresign\Db\IdentifyMethodMapper; use OCA\Libresign\Service\IdentifyMethodService; use OCA\Libresign\Service\SessionService; -use OCA\TwoFactorGateway\Provider\Gateway\Factory; -use OCP\App\IAppManager; +use OCA\Libresign\Service\TwofactorGatewayService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Files\IRootFolder; use OCP\IUserSession; -use OCP\Server; -use Psr\Log\LoggerInterface; -use Throwable; class TwofactorGateway extends AbstractIdentifyMethod { public function __construct( @@ -30,8 +26,7 @@ public function __construct( private SessionService $sessionService, private FileElementMapper $fileElementMapper, private IUserSession $userSession, - private LoggerInterface $logger, - private IAppManager $appManager, + private TwofactorGatewayService $twofactorGatewayService, ) { parent::__construct( $identifyService, @@ -60,26 +55,7 @@ public function validateToSign(): void { } public function isTwofactorGatewayEnabled(): bool { - $isAppEnabled = $this->appManager->isEnabledForAnyone('twofactor_gateway'); - if (!$isAppEnabled) { - return false; - } - /** @var Factory */ - $gatewayFactory = Server::get(Factory::class); - - $gatewayName = $this->getGatewayName(); - - try { - $gateway = $gatewayFactory->get($gatewayName); - return $gateway->isComplete(); - } catch (Throwable $exception) { - $this->logger->warning('Unable to load twofactor gateway provider.', [ - 'gateway' => $gatewayName, - 'identifyMethod' => $this->getId(), - 'exception' => $exception, - ]); - return false; - } + return $this->twofactorGatewayService->isGatewayComplete($this->getGatewayName()); } private function getGatewayName(): string { diff --git a/lib/Service/TwofactorGatewayService.php b/lib/Service/TwofactorGatewayService.php new file mode 100644 index 0000000000..65e318cd54 --- /dev/null +++ b/lib/Service/TwofactorGatewayService.php @@ -0,0 +1,129 @@ +appManager->isEnabledForAnyone(self::APP_ID); + } + + /** + * @throws LibresignException + */ + public function ensureAvailable(string $gatewayName): void { + $this->resolveGateway($gatewayName); + } + + public function isGatewayComplete(string $gatewayName): bool { + if (!$this->isEnabled()) { + return false; + } + + try { + $gateway = $this->resolveGateway($gatewayName); + } catch (\Exception $exception) { + $this->logger->warning('Unable to load twofactor gateway provider.', [ + 'gateway' => $gatewayName, + 'exception' => $exception, + ]); + return false; + } + + if (!is_callable([$gateway, 'isComplete'])) { + $this->logger->warning('Twofactor gateway provider does not expose isComplete().', [ + 'gateway' => $gatewayName, + ]); + return false; + } + + try { + $isComplete = call_user_func([$gateway, 'isComplete']); + } catch (\Exception $exception) { + $this->logger->warning('Twofactor gateway provider failed during completeness check.', [ + 'gateway' => $gatewayName, + 'exception' => $exception, + ]); + return false; + } + + if (!is_bool($isComplete)) { + $this->logger->warning('Twofactor gateway provider returned an invalid completeness flag.', [ + 'gateway' => $gatewayName, + 'returnedType' => get_debug_type($isComplete), + ]); + return false; + } + + return $isComplete; + } + + /** + * @throws LibresignException + */ + public function send(string $gatewayName, string $identifier, string $message): void { + $gateway = $this->resolveGateway($gatewayName); + if (!is_callable([$gateway, 'send'])) { + throw new \UnexpectedValueException(sprintf( + 'Twofactor gateway provider "%s" does not expose send().', + $gatewayName, + )); + } + + call_user_func([$gateway, 'send'], $identifier, $message); + } + + /** + * @throws LibresignException + */ + private function resolveGateway(string $gatewayName): object { + if (!$this->isEnabled()) { + throw new LibresignException('App Two-Factor Gateway is not enabled.'); + } + + try { + $factory = $this->container->get(self::FACTORY_SERVICE_ID); + } catch (NotFoundExceptionInterface $exception) { + throw new LibresignException('App Two-Factor Gateway is not installed.', 0, $exception); + } + + if (!is_object($factory)) { + throw new \UnexpectedValueException('Twofactor gateway factory is not an object.'); + } + + if (!is_callable([$factory, 'get'])) { + throw new \UnexpectedValueException('Twofactor gateway factory does not expose get().'); + } + + $gateway = call_user_func([$factory, 'get'], $gatewayName); + if (!is_object($gateway)) { + throw new \UnexpectedValueException(sprintf( + 'Twofactor gateway "%s" did not resolve to an object.', + $gatewayName, + )); + } + + return $gateway; + } +} \ No newline at end of file diff --git a/psalm.xml b/psalm.xml index 42d585b4e6..94b9b41c8c 100644 --- a/psalm.xml +++ b/psalm.xml @@ -14,10 +14,29 @@ + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/php/Unit/Service/IdentifyMethod/SignatureMethod/TokenServiceTest.php b/tests/php/Unit/Service/IdentifyMethod/SignatureMethod/TokenServiceTest.php new file mode 100644 index 0000000000..bab2532004 --- /dev/null +++ b/tests/php/Unit/Service/IdentifyMethod/SignatureMethod/TokenServiceTest.php @@ -0,0 +1,134 @@ +secureRandom = $this->createMock(ISecureRandom::class); + $this->hasher = $this->createMock(IHasher::class); + $this->mailService = $this->createMock(MailService::class); + $this->l10n = $this->createMock(IL10N::class); + $this->container = $this->createMock(ContainerInterface::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + } + + public function testSendCodeByGatewayThrowsWhenGatewayIsIncomplete(): void { + $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); + $this->container->method('get') + ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') + ->willReturn(new TokenServiceGatewayFactoryStub(new TokenServiceGatewayProviderStub(false))); + $this->secureRandom->expects($this->never()) + ->method('generate'); + $this->l10n->method('t') + ->willReturnCallback(static fn (string $text, mixed $parameters = []): string => is_array($parameters) + ? vsprintf($text, $parameters) + : sprintf($text, $parameters)); + + $this->expectException(OCSForbiddenException::class); + $this->expectExceptionMessage('Gateway sms not configured on Two-Factor Gateway.'); + + $this->createService()->sendCodeByGateway('+5511999999999', 'sms'); + } + + public function testSendCodeByGatewayUsesGatewayServiceAndReturnsHashedCode(): void { + $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); + $provider = new TokenServiceGatewayProviderStub(true); + $this->container->method('get') + ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') + ->willReturn(new TokenServiceGatewayFactoryStub($provider)); + $this->secureRandom->expects($this->once()) + ->method('generate') + ->with(TokenService::TOKEN_LENGTH, ISecureRandom::CHAR_DIGITS) + ->willReturn('123456'); + $this->l10n->expects($this->once()) + ->method('t') + ->with('%s is your LibreSign verification code.', '123456') + ->willReturn('123456 is your LibreSign verification code.'); + $this->hasher->expects($this->once()) + ->method('hash') + ->with('123456') + ->willReturn('hashed-code'); + + self::assertSame('hashed-code', $this->createService()->sendCodeByGateway('+5511999999999', 'sms')); + self::assertSame([ + ['identifier' => '+5511999999999', 'message' => '123456 is your LibreSign verification code.'], + ], $provider->sentMessages); + } + + private function createService(): TokenService { + return new TokenService( + $this->secureRandom, + $this->hasher, + $this->mailService, + $this->l10n, + new TwofactorGatewayService( + $this->container, + $this->appManager, + $this->logger, + ), + ); + } +} + +final class TokenServiceGatewayFactoryStub { + public function __construct( + private object $gateway, + ) { + } + + public function get(string $name): object { + return $this->gateway; + } +} + +final class TokenServiceGatewayProviderStub { + /** @var list */ + public array $sentMessages = []; + + public function __construct( + private bool $complete, + ) { + } + + public function isComplete(): bool { + return $this->complete; + } + + public function send(string $identifier, string $message): void { + $this->sentMessages[] = [ + 'identifier' => $identifier, + 'message' => $message, + ]; + } +} diff --git a/tests/php/Unit/Service/TwofactorGatewayServiceTest.php b/tests/php/Unit/Service/TwofactorGatewayServiceTest.php new file mode 100644 index 0000000000..2902c97be5 --- /dev/null +++ b/tests/php/Unit/Service/TwofactorGatewayServiceTest.php @@ -0,0 +1,144 @@ +container = $this->createMock(ContainerInterface::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + } + + public function testEnsureAvailableThrowsWhenFactoryServiceIsMissing(): void { + $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); + $this->container->method('get') + ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') + ->willThrowException(new class extends \Exception implements NotFoundExceptionInterface { + }); + + $this->expectException(LibresignException::class); + $this->expectExceptionMessage('App Two-Factor Gateway is not installed.'); + + $this->createService()->ensureAvailable('sms'); + } + + public function testIsGatewayCompleteReturnsFalseWhenAppIsDisabled(): void { + $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(false); + $this->container->expects($this->never())->method('get'); + + self::assertFalse($this->createService()->isGatewayComplete('sms')); + } + + #[DataProvider('providerGatewayCompleteness')] + public function testIsGatewayCompleteReturnsGatewayStatus(bool $gatewayComplete): void { + $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); + $this->logger->expects($this->never())->method('warning'); + $this->container->method('get') + ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') + ->willReturn(new TwofactorGatewayFactoryStub(new TwofactorGatewayProviderStub($gatewayComplete))); + + self::assertSame($gatewayComplete, $this->createService()->isGatewayComplete('sms')); + } + + public static function providerGatewayCompleteness(): array { + return [ + 'complete' => [true], + 'incomplete' => [false], + ]; + } + + public function testIsGatewayCompleteReturnsFalseWhenProviderContractChanges(): void { + $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Twofactor gateway provider does not expose isComplete().', + $this->arrayHasKey('gateway') + ); + $this->container->method('get') + ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') + ->willReturn(new TwofactorGatewayFactoryStub(new class { + public function send(string $identifier, string $message): void { + } + })); + + self::assertFalse($this->createService()->isGatewayComplete('sms')); + } + + public function testSendForwardsIdentifierAndMessage(): void { + $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); + $provider = new TwofactorGatewayProviderStub(true); + $this->container->method('get') + ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') + ->willReturn(new TwofactorGatewayFactoryStub($provider)); + + $this->createService()->send('sms', '+5511999999999', 'hello'); + + self::assertSame([ + ['identifier' => '+5511999999999', 'message' => 'hello'], + ], $provider->sentMessages); + } + + private function createService(): TwofactorGatewayService { + return new TwofactorGatewayService( + $this->container, + $this->appManager, + $this->logger, + ); + } +} + +final class TwofactorGatewayFactoryStub { + public function __construct( + private object $gateway, + ) { + } + + public function get(string $name): object { + return $this->gateway; + } +} + +final class TwofactorGatewayProviderStub { + /** @var list */ + public array $sentMessages = []; + + public function __construct( + private bool $complete, + ) { + } + + public function isComplete(): bool { + return $this->complete; + } + + public function send(string $identifier, string $message): void { + $this->sentMessages[] = [ + 'identifier' => $identifier, + 'message' => $message, + ]; + } +} diff --git a/tests/stubs/Doctrine/DBAL/Platforms/AbstractPlatform.php b/tests/stubs/Doctrine/DBAL/Platforms/AbstractPlatform.php deleted file mode 100644 index 48c4a0655b..0000000000 --- a/tests/stubs/Doctrine/DBAL/Platforms/AbstractPlatform.php +++ /dev/null @@ -1,12 +0,0 @@ - Date: Sat, 11 Jul 2026 09:56:01 -0300 Subject: [PATCH 07/15] refactor: keep psalm workflow aligned with template Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/psalm.yml | 28 ---- composer.json | 10 +- psalm.xml | 3 +- tools/prepare-psalm-context.php | 277 ++++++++++++++++++++++++++++++++ 4 files changed, 287 insertions(+), 31 deletions(-) create mode 100644 tools/prepare-psalm-context.php diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index c8ec6be9b0..b2c2c3322d 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -33,34 +33,6 @@ jobs: id: versions uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2.3.1.3.2 - - name: Checkout Nextcloud server for Psalm context - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - repository: nextcloud/server - ref: ${{ steps.versions.outputs.branches-max }} - path: nextcloud-server - persist-credentials: false - fetch-depth: 1 - sparse-checkout: | - 3rdparty/doctrine/dbal/src - 3rdparty/sabre/dav/lib/DAV - 3rdparty/sabre/event/lib - 3rdparty/stecman/symfony-console-completion/src - apps/dav/lib/Connector/Sabre - apps/files/lib - apps/files_sharing/lib - apps/settings/lib/Mailer - core/Command - lib - - - name: Mirror Nextcloud server tree for Psalm - run: | - shared_root="$(dirname "$(dirname "$GITHUB_WORKSPACE")")" - ln -sfn "$GITHUB_WORKSPACE/nextcloud-server/3rdparty" "$shared_root/3rdparty" - ln -sfn "$GITHUB_WORKSPACE/nextcloud-server/apps" "$shared_root/apps" - ln -sfn "$GITHUB_WORKSPACE/nextcloud-server/core" "$shared_root/core" - ln -sfn "$GITHUB_WORKSPACE/nextcloud-server/lib" "$shared_root/lib" - - name: Check enforcement of minimum PHP version ${{ steps.versions.outputs.php-min }} in psalm.xml run: grep 'phpVersion="${{ steps.versions.outputs.php-min }}' psalm.xml diff --git a/composer.json b/composer.json index a4fa3daf5d..7457985b06 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,14 @@ "rector:check": "rector --dry-run", "rector:fix": "rector", "openapi": "generate-spec --verbose && (npm run typescript:generate || echo 'Please manually regenerate the typescript OpenAPI models')", - "psalm": "psalm --no-cache --threads=$(nproc)", - "psalm:update-baseline": "psalm --threads=$(nproc) --update-baseline --set-baseline=tests/psalm-baseline.xml", + "psalm": [ + "@php tools/prepare-psalm-context.php", + "psalm --no-cache --threads=$(nproc)" + ], + "psalm:update-baseline": [ + "@php tools/prepare-psalm-context.php", + "psalm --threads=$(nproc) --update-baseline --set-baseline=tests/psalm-baseline.xml" + ], "post-install-cmd": [ "@composer bin all install --ansi", "composer dump-autoload -o" diff --git a/psalm.xml b/psalm.xml index 94b9b41c8c..f66803917a 100644 --- a/psalm.xml +++ b/psalm.xml @@ -22,7 +22,8 @@ diff --git a/tools/prepare-psalm-context.php b/tools/prepare-psalm-context.php new file mode 100644 index 0000000000..afed384b93 --- /dev/null +++ b/tools/prepare-psalm-context.php @@ -0,0 +1,277 @@ +getMessage() . PHP_EOL); + return 1; + } + } + + private static function getSharedRoot(string $projectRoot): string { + $sharedRoot = getenv('LIBRESIGN_PSALM_SHARED_ROOT'); + if (is_string($sharedRoot) && $sharedRoot !== '') { + return self::normalizePath($sharedRoot); + } + + return dirname(dirname($projectRoot)); + } + + private static function getServerRoot(string $projectRoot, string $managedCheckoutRoot): string { + $serverCheckout = getenv('LIBRESIGN_PSALM_SERVER_CHECKOUT'); + if (is_string($serverCheckout) && $serverCheckout !== '') { + $serverRoot = self::normalizePath($serverCheckout); + self::assertLayoutExists($serverRoot); + return $serverRoot; + } + + $branch = getenv('LIBRESIGN_PSALM_SERVER_BRANCH'); + if (!is_string($branch) || $branch === '') { + $branch = self::detectNextcloudServerBranch($projectRoot . '/composer.lock'); + } + + self::prepareManagedCheckout($managedCheckoutRoot, $branch); + return $managedCheckoutRoot; + } + + private static function detectNextcloudServerBranch(string $composerLockPath): string { + if (!is_file($composerLockPath)) { + throw new RuntimeException('Unable to determine Nextcloud server branch: composer.lock not found.'); + } + + /** @var array{packages?: list, packages-dev?: list} $composerLock */ + $composerLock = json_decode((string)file_get_contents($composerLockPath), true, 512, JSON_THROW_ON_ERROR); + $packages = array_merge($composerLock['packages'] ?? [], $composerLock['packages-dev'] ?? []); + + foreach ($packages as $package) { + if (($package['name'] ?? null) !== 'nextcloud/ocp') { + continue; + } + + foreach (['pretty_version', 'version'] as $field) { + $value = $package[$field] ?? null; + if (!is_string($value)) { + continue; + } + + if (preg_match('/(?:^|\s)dev-([A-Za-z0-9._-]+)(?:$|\s)/', $value, $matches) === 1) { + return $matches[1]; + } + } + } + + throw new RuntimeException('Unable to determine Nextcloud server branch from nextcloud/ocp in composer.lock.'); + } + + private static function prepareManagedCheckout(string $serverRoot, string $branch): void { + $branchFile = $serverRoot . '/.psalm-branch'; + if (self::preparedCheckoutMatches($serverRoot, $branchFile, $branch)) { + return; + } + + self::removeDirectory($serverRoot); + if (!is_dir(dirname($serverRoot)) && !mkdir(dirname($serverRoot), 0777, true) && !is_dir(dirname($serverRoot))) { + throw new RuntimeException('Unable to create Psalm context directory.'); + } + + fwrite(STDERR, sprintf('[psalm-context] cloning nextcloud/server (%s) for Psalm context%s', $branch, PHP_EOL)); + + self::runCommand(sprintf( + 'git clone --depth 1 --filter=blob:none --sparse --branch %s https://github.com/nextcloud/server.git %s', + escapeshellarg($branch), + escapeshellarg($serverRoot), + )); + + self::runCommand(sprintf( + 'git -C %s sparse-checkout set %s', + escapeshellarg($serverRoot), + implode(' ', array_map('escapeshellarg', self::SPARSE_CHECKOUT_PATHS)), + )); + + if (file_put_contents($branchFile, $branch . PHP_EOL) === false) { + throw new RuntimeException('Unable to write Psalm context branch marker.'); + } + } + + private static function preparedCheckoutMatches(string $serverRoot, string $branchFile, string $branch): bool { + if (!is_dir($serverRoot) || !is_file($branchFile)) { + return false; + } + + $preparedBranch = trim((string)file_get_contents($branchFile)); + return $preparedBranch === $branch && self::layoutExists($serverRoot); + } + + private static function isManagedMirror(string $managedCheckoutRoot, string $sharedRoot): bool { + $managedCheckoutRealPath = realpath($managedCheckoutRoot); + if (!is_string($managedCheckoutRealPath) || $managedCheckoutRealPath === '') { + return false; + } + + foreach (self::MIRRORED_TOP_LEVELS as $topLevel) { + $linkPath = $sharedRoot . '/' . $topLevel; + if (!is_link($linkPath)) { + return false; + } + + $linkTarget = realpath($linkPath); + if (!is_string($linkTarget) || !str_starts_with($linkTarget, $managedCheckoutRealPath . '/')) { + return false; + } + } + + return true; + } + + private static function mirrorServerTree(string $serverRoot, string $sharedRoot): void { + if (!is_dir($sharedRoot) && !mkdir($sharedRoot, 0777, true) && !is_dir($sharedRoot)) { + throw new RuntimeException('Unable to create shared Psalm root: ' . $sharedRoot); + } + + foreach (self::MIRRORED_TOP_LEVELS as $topLevel) { + $targetPath = $serverRoot . '/' . $topLevel; + $linkPath = $sharedRoot . '/' . $topLevel; + + if (is_link($linkPath)) { + unlink($linkPath); + } elseif (file_exists($linkPath)) { + continue; + } + + if (!symlink($targetPath, $linkPath)) { + throw new RuntimeException('Unable to mirror Nextcloud path for Psalm: ' . $linkPath); + } + } + } + + private static function layoutExists(string $rootPath): bool { + foreach (self::REQUIRED_SERVER_PATHS as $relativePath) { + if (!file_exists($rootPath . '/' . $relativePath)) { + return false; + } + } + + return true; + } + + private static function assertLayoutExists(string $rootPath): void { + foreach (self::REQUIRED_SERVER_PATHS as $relativePath) { + if (file_exists($rootPath . '/' . $relativePath)) { + continue; + } + + throw new RuntimeException(sprintf( + 'Missing Nextcloud server path required by Psalm: %s', + $rootPath . '/' . $relativePath, + )); + } + } + + private static function normalizePath(string $path): string { + $realPath = realpath($path); + if (is_string($realPath) && $realPath !== '') { + return $realPath; + } + + return rtrim($path, '/'); + } + + private static function removeDirectory(string $path): void { + if (!file_exists($path)) { + return; + } + + if (is_file($path) || is_link($path)) { + unlink($path); + return; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST, + ); + + foreach ($iterator as $fileInfo) { + $filePath = $fileInfo->getPathname(); + if ($fileInfo->isLink() || $fileInfo->isFile()) { + unlink($filePath); + continue; + } + + rmdir($filePath); + } + + rmdir($path); + } + + private static function runCommand(string $command): void { + $output = []; + $returnCode = 0; + exec($command . ' 2>&1', $output, $returnCode); + if ($returnCode === 0) { + return; + } + + throw new RuntimeException(sprintf( + "Command failed (%d): %s\n%s", + $returnCode, + $command, + implode(PHP_EOL, $output), + )); + } +} + +exit(PreparePsalmContext::main()); From 9192c4184838ce3b5343c6bda7321ef45ebc34f8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:08:30 -0300 Subject: [PATCH 08/15] fix: restore focused psalm stubs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- composer.json | 12 +- composer.lock | 749 +++++++++++++++++- psalm.xml | 35 +- tests/stubs/OC/Core/Command/Base.php | 12 + tests/stubs/OC/Hooks/Emitter.php | 20 + .../OCA/DAV/Connector/Sabre/Directory.php | 13 + tests/stubs/OCA/DAV/Connector/Sabre/File.php | 13 + tests/stubs/OCA/Files/Command/ScanAppData.php | 12 + .../OCA/Files/Controller/ViewController.php | 12 + tests/stubs/OCA/Files/Event/LoadSidebar.php | 12 + .../stubs/OCA/Files_Sharing/SharedStorage.php | 13 + .../OCA/Settings/Mailer/NewUserMailHelper.php | 15 + tools/prepare-psalm-context.php | 277 ------- 13 files changed, 890 insertions(+), 305 deletions(-) create mode 100644 tests/stubs/OC/Core/Command/Base.php create mode 100644 tests/stubs/OC/Hooks/Emitter.php create mode 100644 tests/stubs/OCA/DAV/Connector/Sabre/Directory.php create mode 100644 tests/stubs/OCA/DAV/Connector/Sabre/File.php create mode 100644 tests/stubs/OCA/Files/Command/ScanAppData.php create mode 100644 tests/stubs/OCA/Files/Controller/ViewController.php create mode 100644 tests/stubs/OCA/Files/Event/LoadSidebar.php create mode 100644 tests/stubs/OCA/Files_Sharing/SharedStorage.php create mode 100644 tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php delete mode 100644 tools/prepare-psalm-context.php diff --git a/composer.json b/composer.json index 7457985b06..c4e98110e6 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,9 @@ { "require-dev": { "bamarni/composer-bin-plugin": "^1.8", + "doctrine/dbal": "3.10.4", "nextcloud/ocp": "dev-master", + "sabre/dav": "4.7.0", "roave/security-advisories": "dev-latest" }, "config": { @@ -24,14 +26,8 @@ "rector:check": "rector --dry-run", "rector:fix": "rector", "openapi": "generate-spec --verbose && (npm run typescript:generate || echo 'Please manually regenerate the typescript OpenAPI models')", - "psalm": [ - "@php tools/prepare-psalm-context.php", - "psalm --no-cache --threads=$(nproc)" - ], - "psalm:update-baseline": [ - "@php tools/prepare-psalm-context.php", - "psalm --threads=$(nproc) --update-baseline --set-baseline=tests/psalm-baseline.xml" - ], + "psalm": "psalm --no-cache --threads=$(nproc)", + "psalm:update-baseline": "psalm --threads=$(nproc) --update-baseline --set-baseline=tests/psalm-baseline.xml", "post-install-cmd": [ "@composer bin all install --ansi", "composer dump-autoload -o" diff --git a/composer.lock b/composer.lock index d986c1217e..b18fb24fff 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c978760fde108514de13dc57c60ea571", + "content-hash": "fe15c7870c16d5d6c66300cf0d1e8bc3", "packages": [ { "name": "cweagans/composer-configurable-plugin", @@ -258,6 +258,259 @@ }, "time": "2026-02-04T10:18:12+00:00" }, + { + "name": "doctrine/dbal", + "version": "3.10.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/63a46cb5aa6f60991186cc98c1d1b50c09311868", + "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "doctrine/cache": "< 1.11" + }, + "require-dev": { + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "14.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "2.1.30", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.29", + "slevomat/coding-standard": "8.24.0", + "squizlabs/php_codesniffer": "4.0.0", + "symfony/cache": "^5.4|^6.0|^7.0|^8.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.10.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-11-29T10:46:08+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpdocumentor/guides-cli": "^1.4", + "phpstan/phpstan": "^2.1.32", + "phpunit/phpunit": "^10.5.58" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2026-01-29T07:11:08+00:00" + }, { "name": "nextcloud/ocp", "version": "dev-master", @@ -308,6 +561,55 @@ }, "time": "2026-06-19T02:51:56+00:00" }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -1720,6 +2022,451 @@ } ], "time": "2026-06-19T21:28:22+00:00" + }, + { + "name": "sabre/dav", + "version": "4.7.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/dav.git", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": "^7.1.0 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "sabre/event": "^5.0", + "sabre/http": "^5.0.5", + "sabre/uri": "^2.0", + "sabre/vobject": "^4.2.1", + "sabre/xml": "^2.0.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "monolog/monolog": "^1.27 || ^2.0", + "phpstan/phpstan": "^0.12 || ^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": "*", + "ext-imap": "*", + "ext-pdo": "*" + }, + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "WebDAV Framework for PHP", + "homepage": "http://sabre.io/", + "keywords": [ + "CalDAV", + "CardDAV", + "WebDAV", + "framework", + "iCalendar" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/dav/issues", + "source": "https://github.com/fruux/sabre-dav" + }, + "time": "2024-10-29T11:46:02+00:00" + }, + { + "name": "sabre/event", + "version": "5.1.9", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "743f1d04811fd5b89f67878d002f6a273ccb089f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/743f1d04811fd5b89f67878d002f6a273ccb089f", + "reference": "743f1d04811fd5b89f67878d002f6a273ccb089f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||^3.95", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ], + "psr-4": { + "Sabre\\Event\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" + }, + "time": "2026-07-07T09:13:04+00:00" + }, + { + "name": "sabre/http", + "version": "5.1.13", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/7c2a14097d1a0de2347dcbdc91a02f38e338f4db", + "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/event": ">=4.0 <6.0", + "sabre/uri": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\HTTP\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", + "homepage": "https://github.com/fruux/sabre-http", + "keywords": [ + "http" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/http/issues", + "source": "https://github.com/fruux/sabre-http" + }, + "time": "2025-09-09T10:21:47+00:00" + }, + { + "name": "sabre/uri", + "version": "2.3.4", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.63", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, + "time": "2024-08-27T12:18:16+00:00" + }, + { + "name": "sabre/vobject", + "version": "4.6.1", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "63613f6c53a0a2bddfe22caba0d052e6c59f7d0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/63613f6c53a0a2bddfe22caba0d052e6c59f7d0e", + "reference": "63613f6c53a0a2bddfe22caba0d052e6c59f7d0e", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/xml": "^2.1 || ^3.0 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0", + "phpunit/php-invoker": "^2.0 || ^3.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" + } + ], + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "time": "2026-07-07T03:20:17+00:00" + }, + { + "name": "sabre/xml", + "version": "2.2.11", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.1 || ^8.0", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, + "time": "2024-09-06T07:37:46+00:00" } ], "aliases": [], diff --git a/psalm.xml b/psalm.xml index f66803917a..5ac5bb087c 100644 --- a/psalm.xml +++ b/psalm.xml @@ -19,25 +19,6 @@ - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/tests/stubs/OC/Core/Command/Base.php b/tests/stubs/OC/Core/Command/Base.php new file mode 100644 index 0000000000..4888a96c2d --- /dev/null +++ b/tests/stubs/OC/Core/Command/Base.php @@ -0,0 +1,12 @@ + $option + */ + public function emit(string $class, string $value, array $option): void { + } + + public function listen(string $class, string $value, mixed $closure): void { + } + } +} \ No newline at end of file diff --git a/tests/stubs/OCA/DAV/Connector/Sabre/Directory.php b/tests/stubs/OCA/DAV/Connector/Sabre/Directory.php new file mode 100644 index 0000000000..cc34ab428d --- /dev/null +++ b/tests/stubs/OCA/DAV/Connector/Sabre/Directory.php @@ -0,0 +1,13 @@ +getMessage() . PHP_EOL); - return 1; - } - } - - private static function getSharedRoot(string $projectRoot): string { - $sharedRoot = getenv('LIBRESIGN_PSALM_SHARED_ROOT'); - if (is_string($sharedRoot) && $sharedRoot !== '') { - return self::normalizePath($sharedRoot); - } - - return dirname(dirname($projectRoot)); - } - - private static function getServerRoot(string $projectRoot, string $managedCheckoutRoot): string { - $serverCheckout = getenv('LIBRESIGN_PSALM_SERVER_CHECKOUT'); - if (is_string($serverCheckout) && $serverCheckout !== '') { - $serverRoot = self::normalizePath($serverCheckout); - self::assertLayoutExists($serverRoot); - return $serverRoot; - } - - $branch = getenv('LIBRESIGN_PSALM_SERVER_BRANCH'); - if (!is_string($branch) || $branch === '') { - $branch = self::detectNextcloudServerBranch($projectRoot . '/composer.lock'); - } - - self::prepareManagedCheckout($managedCheckoutRoot, $branch); - return $managedCheckoutRoot; - } - - private static function detectNextcloudServerBranch(string $composerLockPath): string { - if (!is_file($composerLockPath)) { - throw new RuntimeException('Unable to determine Nextcloud server branch: composer.lock not found.'); - } - - /** @var array{packages?: list, packages-dev?: list} $composerLock */ - $composerLock = json_decode((string)file_get_contents($composerLockPath), true, 512, JSON_THROW_ON_ERROR); - $packages = array_merge($composerLock['packages'] ?? [], $composerLock['packages-dev'] ?? []); - - foreach ($packages as $package) { - if (($package['name'] ?? null) !== 'nextcloud/ocp') { - continue; - } - - foreach (['pretty_version', 'version'] as $field) { - $value = $package[$field] ?? null; - if (!is_string($value)) { - continue; - } - - if (preg_match('/(?:^|\s)dev-([A-Za-z0-9._-]+)(?:$|\s)/', $value, $matches) === 1) { - return $matches[1]; - } - } - } - - throw new RuntimeException('Unable to determine Nextcloud server branch from nextcloud/ocp in composer.lock.'); - } - - private static function prepareManagedCheckout(string $serverRoot, string $branch): void { - $branchFile = $serverRoot . '/.psalm-branch'; - if (self::preparedCheckoutMatches($serverRoot, $branchFile, $branch)) { - return; - } - - self::removeDirectory($serverRoot); - if (!is_dir(dirname($serverRoot)) && !mkdir(dirname($serverRoot), 0777, true) && !is_dir(dirname($serverRoot))) { - throw new RuntimeException('Unable to create Psalm context directory.'); - } - - fwrite(STDERR, sprintf('[psalm-context] cloning nextcloud/server (%s) for Psalm context%s', $branch, PHP_EOL)); - - self::runCommand(sprintf( - 'git clone --depth 1 --filter=blob:none --sparse --branch %s https://github.com/nextcloud/server.git %s', - escapeshellarg($branch), - escapeshellarg($serverRoot), - )); - - self::runCommand(sprintf( - 'git -C %s sparse-checkout set %s', - escapeshellarg($serverRoot), - implode(' ', array_map('escapeshellarg', self::SPARSE_CHECKOUT_PATHS)), - )); - - if (file_put_contents($branchFile, $branch . PHP_EOL) === false) { - throw new RuntimeException('Unable to write Psalm context branch marker.'); - } - } - - private static function preparedCheckoutMatches(string $serverRoot, string $branchFile, string $branch): bool { - if (!is_dir($serverRoot) || !is_file($branchFile)) { - return false; - } - - $preparedBranch = trim((string)file_get_contents($branchFile)); - return $preparedBranch === $branch && self::layoutExists($serverRoot); - } - - private static function isManagedMirror(string $managedCheckoutRoot, string $sharedRoot): bool { - $managedCheckoutRealPath = realpath($managedCheckoutRoot); - if (!is_string($managedCheckoutRealPath) || $managedCheckoutRealPath === '') { - return false; - } - - foreach (self::MIRRORED_TOP_LEVELS as $topLevel) { - $linkPath = $sharedRoot . '/' . $topLevel; - if (!is_link($linkPath)) { - return false; - } - - $linkTarget = realpath($linkPath); - if (!is_string($linkTarget) || !str_starts_with($linkTarget, $managedCheckoutRealPath . '/')) { - return false; - } - } - - return true; - } - - private static function mirrorServerTree(string $serverRoot, string $sharedRoot): void { - if (!is_dir($sharedRoot) && !mkdir($sharedRoot, 0777, true) && !is_dir($sharedRoot)) { - throw new RuntimeException('Unable to create shared Psalm root: ' . $sharedRoot); - } - - foreach (self::MIRRORED_TOP_LEVELS as $topLevel) { - $targetPath = $serverRoot . '/' . $topLevel; - $linkPath = $sharedRoot . '/' . $topLevel; - - if (is_link($linkPath)) { - unlink($linkPath); - } elseif (file_exists($linkPath)) { - continue; - } - - if (!symlink($targetPath, $linkPath)) { - throw new RuntimeException('Unable to mirror Nextcloud path for Psalm: ' . $linkPath); - } - } - } - - private static function layoutExists(string $rootPath): bool { - foreach (self::REQUIRED_SERVER_PATHS as $relativePath) { - if (!file_exists($rootPath . '/' . $relativePath)) { - return false; - } - } - - return true; - } - - private static function assertLayoutExists(string $rootPath): void { - foreach (self::REQUIRED_SERVER_PATHS as $relativePath) { - if (file_exists($rootPath . '/' . $relativePath)) { - continue; - } - - throw new RuntimeException(sprintf( - 'Missing Nextcloud server path required by Psalm: %s', - $rootPath . '/' . $relativePath, - )); - } - } - - private static function normalizePath(string $path): string { - $realPath = realpath($path); - if (is_string($realPath) && $realPath !== '') { - return $realPath; - } - - return rtrim($path, '/'); - } - - private static function removeDirectory(string $path): void { - if (!file_exists($path)) { - return; - } - - if (is_file($path) || is_link($path)) { - unlink($path); - return; - } - - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST, - ); - - foreach ($iterator as $fileInfo) { - $filePath = $fileInfo->getPathname(); - if ($fileInfo->isLink() || $fileInfo->isFile()) { - unlink($filePath); - continue; - } - - rmdir($filePath); - } - - rmdir($path); - } - - private static function runCommand(string $command): void { - $output = []; - $returnCode = 0; - exec($command . ' 2>&1', $output, $returnCode); - if ($returnCode === 0) { - return; - } - - throw new RuntimeException(sprintf( - "Command failed (%d): %s\n%s", - $returnCode, - $command, - implode(PHP_EOL, $output), - )); - } -} - -exit(PreparePsalmContext::main()); From 3d6543b85f58510c86a014e519cd32df9ccd972a Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:01:54 -0300 Subject: [PATCH 09/15] chore: add twofactor gateway psalm stub Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/stubs/twofactor_gateway.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/stubs/twofactor_gateway.php diff --git a/tests/stubs/twofactor_gateway.php b/tests/stubs/twofactor_gateway.php new file mode 100644 index 0000000000..71c5894a6b --- /dev/null +++ b/tests/stubs/twofactor_gateway.php @@ -0,0 +1,22 @@ + Date: Mon, 13 Jul 2026 16:01:54 -0300 Subject: [PATCH 10/15] chore: hide external psalm errors Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- psalm.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psalm.xml b/psalm.xml index 5ac5bb087c..3ebf061c5c 100644 --- a/psalm.xml +++ b/psalm.xml @@ -4,6 +4,7 @@ errorLevel="5" findUnusedBaselineEntry="true" findUnusedCode="false" + hideExternalErrors="true" resolveFromConfigFile="true" phpVersion="8.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" @@ -67,5 +68,6 @@ + From 70654af5c9b23287d87180df1d94c40a75546392 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:01:54 -0300 Subject: [PATCH 11/15] fix: import query builder type in migration Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Migration/Version16001Date20251227000000.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Migration/Version16001Date20251227000000.php b/lib/Migration/Version16001Date20251227000000.php index 9b848ae2d8..8addf360bc 100644 --- a/lib/Migration/Version16001Date20251227000000.php +++ b/lib/Migration/Version16001Date20251227000000.php @@ -9,8 +9,8 @@ namespace OCA\Libresign\Migration; use Closure; -use OCP\DB\ISchemaWrapper; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\DB\ISchemaWrapper; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; From dfe002d6cf11e7bf32a92134b947fe8c40e57c9d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:15:30 -0300 Subject: [PATCH 12/15] fix: resolve psalm OCA dependencies from nextcloud sources Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- psalm.xml | 25 +++++++++---------- .../OCA/DAV/Connector/Sabre/Directory.php | 13 ---------- tests/stubs/OCA/DAV/Connector/Sabre/File.php | 13 ---------- tests/stubs/OCA/Files/Command/ScanAppData.php | 12 --------- .../OCA/Files/Controller/ViewController.php | 12 --------- tests/stubs/OCA/Files/Event/LoadSidebar.php | 12 --------- .../stubs/OCA/Files_Sharing/SharedStorage.php | 13 ---------- .../OCA/Settings/Mailer/NewUserMailHelper.php | 15 ----------- tests/stubs/twofactor_gateway.php | 22 ---------------- 9 files changed, 12 insertions(+), 125 deletions(-) delete mode 100644 tests/stubs/OCA/DAV/Connector/Sabre/Directory.php delete mode 100644 tests/stubs/OCA/DAV/Connector/Sabre/File.php delete mode 100644 tests/stubs/OCA/Files/Command/ScanAppData.php delete mode 100644 tests/stubs/OCA/Files/Controller/ViewController.php delete mode 100644 tests/stubs/OCA/Files/Event/LoadSidebar.php delete mode 100644 tests/stubs/OCA/Files_Sharing/SharedStorage.php delete mode 100644 tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php delete mode 100644 tests/stubs/twofactor_gateway.php diff --git a/psalm.xml b/psalm.xml index 3ebf061c5c..3355aaa71a 100644 --- a/psalm.xml +++ b/psalm.xml @@ -20,6 +20,18 @@ + + + + + + + + + + + + - - - - - - - - diff --git a/tests/stubs/OCA/DAV/Connector/Sabre/Directory.php b/tests/stubs/OCA/DAV/Connector/Sabre/Directory.php deleted file mode 100644 index cc34ab428d..0000000000 --- a/tests/stubs/OCA/DAV/Connector/Sabre/Directory.php +++ /dev/null @@ -1,13 +0,0 @@ - Date: Mon, 13 Jul 2026 16:31:47 -0300 Subject: [PATCH 13/15] refactor: simplify twofactor gateway integration Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/TwofactorGatewayService.php | 68 +++++++------------ .../SignatureMethod/TokenServiceTest.php | 37 ++++------ .../Service/TwofactorGatewayServiceTest.php | 54 +++++++-------- 3 files changed, 63 insertions(+), 96 deletions(-) diff --git a/lib/Service/TwofactorGatewayService.php b/lib/Service/TwofactorGatewayService.php index 65e318cd54..68d5010786 100644 --- a/lib/Service/TwofactorGatewayService.php +++ b/lib/Service/TwofactorGatewayService.php @@ -16,7 +16,7 @@ final class TwofactorGatewayService { private const APP_ID = 'twofactor_gateway'; - private const FACTORY_SERVICE_ID = 'OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory'; + private const INTEGRATION_SERVICE_ID = 'OCA\\TwoFactorGateway\\Service\\GatewayDirectIntegrationService'; public function __construct( private ContainerInterface $container, @@ -33,7 +33,7 @@ public function isEnabled(): bool { * @throws LibresignException */ public function ensureAvailable(string $gatewayName): void { - $this->resolveGateway($gatewayName); + $this->callIntegrationMethod('ensureAvailable', [$gatewayName]); } public function isGatewayComplete(string $gatewayName): bool { @@ -42,26 +42,9 @@ public function isGatewayComplete(string $gatewayName): bool { } try { - $gateway = $this->resolveGateway($gatewayName); + $isComplete = $this->callIntegrationMethod('isGatewayComplete', [$gatewayName]); } catch (\Exception $exception) { - $this->logger->warning('Unable to load twofactor gateway provider.', [ - 'gateway' => $gatewayName, - 'exception' => $exception, - ]); - return false; - } - - if (!is_callable([$gateway, 'isComplete'])) { - $this->logger->warning('Twofactor gateway provider does not expose isComplete().', [ - 'gateway' => $gatewayName, - ]); - return false; - } - - try { - $isComplete = call_user_func([$gateway, 'isComplete']); - } catch (\Exception $exception) { - $this->logger->warning('Twofactor gateway provider failed during completeness check.', [ + $this->logger->warning('Unable to determine twofactor gateway completeness.', [ 'gateway' => $gatewayName, 'exception' => $exception, ]); @@ -69,7 +52,7 @@ public function isGatewayComplete(string $gatewayName): bool { } if (!is_bool($isComplete)) { - $this->logger->warning('Twofactor gateway provider returned an invalid completeness flag.', [ + $this->logger->warning('Twofactor gateway integration service returned an invalid completeness flag.', [ 'gateway' => $gatewayName, 'returnedType' => get_debug_type($isComplete), ]); @@ -83,47 +66,42 @@ public function isGatewayComplete(string $gatewayName): bool { * @throws LibresignException */ public function send(string $gatewayName, string $identifier, string $message): void { - $gateway = $this->resolveGateway($gatewayName); - if (!is_callable([$gateway, 'send'])) { + $this->callIntegrationMethod('send', [$gatewayName, $identifier, $message]); + } + + /** + * @throws LibresignException + */ + private function callIntegrationMethod(string $method, array $arguments = []): mixed { + $integrationService = $this->resolveIntegrationService(); + if (!is_callable([$integrationService, $method])) { throw new \UnexpectedValueException(sprintf( - 'Twofactor gateway provider "%s" does not expose send().', - $gatewayName, + 'Twofactor gateway integration service does not expose %s().', + $method, )); } - call_user_func([$gateway, 'send'], $identifier, $message); + return call_user_func([$integrationService, $method], ...$arguments); } /** * @throws LibresignException */ - private function resolveGateway(string $gatewayName): object { + private function resolveIntegrationService(): object { if (!$this->isEnabled()) { throw new LibresignException('App Two-Factor Gateway is not enabled.'); } try { - $factory = $this->container->get(self::FACTORY_SERVICE_ID); + $integrationService = $this->container->get(self::INTEGRATION_SERVICE_ID); } catch (NotFoundExceptionInterface $exception) { throw new LibresignException('App Two-Factor Gateway is not installed.', 0, $exception); } - if (!is_object($factory)) { - throw new \UnexpectedValueException('Twofactor gateway factory is not an object.'); - } - - if (!is_callable([$factory, 'get'])) { - throw new \UnexpectedValueException('Twofactor gateway factory does not expose get().'); - } - - $gateway = call_user_func([$factory, 'get'], $gatewayName); - if (!is_object($gateway)) { - throw new \UnexpectedValueException(sprintf( - 'Twofactor gateway "%s" did not resolve to an object.', - $gatewayName, - )); + if (!is_object($integrationService)) { + throw new \UnexpectedValueException('Twofactor gateway integration service is not an object.'); } - return $gateway; + return $integrationService; } -} \ No newline at end of file +} diff --git a/tests/php/Unit/Service/IdentifyMethod/SignatureMethod/TokenServiceTest.php b/tests/php/Unit/Service/IdentifyMethod/SignatureMethod/TokenServiceTest.php index bab2532004..a0acd8d8ee 100644 --- a/tests/php/Unit/Service/IdentifyMethod/SignatureMethod/TokenServiceTest.php +++ b/tests/php/Unit/Service/IdentifyMethod/SignatureMethod/TokenServiceTest.php @@ -46,8 +46,8 @@ public function setUp(): void { public function testSendCodeByGatewayThrowsWhenGatewayIsIncomplete(): void { $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); $this->container->method('get') - ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') - ->willReturn(new TokenServiceGatewayFactoryStub(new TokenServiceGatewayProviderStub(false))); + ->with('OCA\\TwoFactorGateway\\Service\\GatewayDirectIntegrationService') + ->willReturn(new TokenServiceGatewayIntegrationStub(false)); $this->secureRandom->expects($this->never()) ->method('generate'); $this->l10n->method('t') @@ -63,10 +63,10 @@ public function testSendCodeByGatewayThrowsWhenGatewayIsIncomplete(): void { public function testSendCodeByGatewayUsesGatewayServiceAndReturnsHashedCode(): void { $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); - $provider = new TokenServiceGatewayProviderStub(true); + $integrationService = new TokenServiceGatewayIntegrationStub(true); $this->container->method('get') - ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') - ->willReturn(new TokenServiceGatewayFactoryStub($provider)); + ->with('OCA\\TwoFactorGateway\\Service\\GatewayDirectIntegrationService') + ->willReturn($integrationService); $this->secureRandom->expects($this->once()) ->method('generate') ->with(TokenService::TOKEN_LENGTH, ISecureRandom::CHAR_DIGITS) @@ -82,8 +82,8 @@ public function testSendCodeByGatewayUsesGatewayServiceAndReturnsHashedCode(): v self::assertSame('hashed-code', $this->createService()->sendCodeByGateway('+5511999999999', 'sms')); self::assertSame([ - ['identifier' => '+5511999999999', 'message' => '123456 is your LibreSign verification code.'], - ], $provider->sentMessages); + ['gateway' => 'sms', 'identifier' => '+5511999999999', 'message' => '123456 is your LibreSign verification code.'], + ], $integrationService->sentMessages); } private function createService(): TokenService { @@ -101,19 +101,8 @@ private function createService(): TokenService { } } -final class TokenServiceGatewayFactoryStub { - public function __construct( - private object $gateway, - ) { - } - - public function get(string $name): object { - return $this->gateway; - } -} - -final class TokenServiceGatewayProviderStub { - /** @var list */ +final class TokenServiceGatewayIntegrationStub { + /** @var list */ public array $sentMessages = []; public function __construct( @@ -121,12 +110,16 @@ public function __construct( ) { } - public function isComplete(): bool { + public function ensureAvailable(string $gatewayName): void { + } + + public function isGatewayComplete(string $gatewayName): bool { return $this->complete; } - public function send(string $identifier, string $message): void { + public function send(string $gatewayName, string $identifier, string $message): void { $this->sentMessages[] = [ + 'gateway' => $gatewayName, 'identifier' => $identifier, 'message' => $message, ]; diff --git a/tests/php/Unit/Service/TwofactorGatewayServiceTest.php b/tests/php/Unit/Service/TwofactorGatewayServiceTest.php index 2902c97be5..111951eea9 100644 --- a/tests/php/Unit/Service/TwofactorGatewayServiceTest.php +++ b/tests/php/Unit/Service/TwofactorGatewayServiceTest.php @@ -35,7 +35,7 @@ public function setUp(): void { public function testEnsureAvailableThrowsWhenFactoryServiceIsMissing(): void { $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); $this->container->method('get') - ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') + ->with('OCA\\TwoFactorGateway\\Service\\GatewayDirectIntegrationService') ->willThrowException(new class extends \Exception implements NotFoundExceptionInterface { }); @@ -57,8 +57,8 @@ public function testIsGatewayCompleteReturnsGatewayStatus(bool $gatewayComplete) $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); $this->logger->expects($this->never())->method('warning'); $this->container->method('get') - ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') - ->willReturn(new TwofactorGatewayFactoryStub(new TwofactorGatewayProviderStub($gatewayComplete))); + ->with('OCA\\TwoFactorGateway\\Service\\GatewayDirectIntegrationService') + ->willReturn(new TwofactorGatewayIntegrationStub($gatewayComplete)); self::assertSame($gatewayComplete, $this->createService()->isGatewayComplete('sms')); } @@ -70,36 +70,39 @@ public static function providerGatewayCompleteness(): array { ]; } - public function testIsGatewayCompleteReturnsFalseWhenProviderContractChanges(): void { + public function testIsGatewayCompleteReturnsFalseWhenIntegrationContractChanges(): void { $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); $this->logger->expects($this->once()) ->method('warning') ->with( - 'Twofactor gateway provider does not expose isComplete().', + 'Unable to determine twofactor gateway completeness.', $this->arrayHasKey('gateway') ); $this->container->method('get') - ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') - ->willReturn(new TwofactorGatewayFactoryStub(new class { - public function send(string $identifier, string $message): void { + ->with('OCA\\TwoFactorGateway\\Service\\GatewayDirectIntegrationService') + ->willReturn(new class { + public function ensureAvailable(string $gatewayName): void { } - })); + + public function send(string $gatewayName, string $identifier, string $message): void { + } + }); self::assertFalse($this->createService()->isGatewayComplete('sms')); } public function testSendForwardsIdentifierAndMessage(): void { $this->appManager->method('isEnabledForAnyone')->with('twofactor_gateway')->willReturn(true); - $provider = new TwofactorGatewayProviderStub(true); + $integrationService = new TwofactorGatewayIntegrationStub(true); $this->container->method('get') - ->with('OCA\\TwoFactorGateway\\Provider\\Gateway\\Factory') - ->willReturn(new TwofactorGatewayFactoryStub($provider)); + ->with('OCA\\TwoFactorGateway\\Service\\GatewayDirectIntegrationService') + ->willReturn($integrationService); $this->createService()->send('sms', '+5511999999999', 'hello'); self::assertSame([ - ['identifier' => '+5511999999999', 'message' => 'hello'], - ], $provider->sentMessages); + ['gateway' => 'sms', 'identifier' => '+5511999999999', 'message' => 'hello'], + ], $integrationService->sentMessages); } private function createService(): TwofactorGatewayService { @@ -111,19 +114,8 @@ private function createService(): TwofactorGatewayService { } } -final class TwofactorGatewayFactoryStub { - public function __construct( - private object $gateway, - ) { - } - - public function get(string $name): object { - return $this->gateway; - } -} - -final class TwofactorGatewayProviderStub { - /** @var list */ +final class TwofactorGatewayIntegrationStub { + /** @var list */ public array $sentMessages = []; public function __construct( @@ -131,12 +123,16 @@ public function __construct( ) { } - public function isComplete(): bool { + public function ensureAvailable(string $gatewayName): void { + } + + public function isGatewayComplete(string $gatewayName): bool { return $this->complete; } - public function send(string $identifier, string $message): void { + public function send(string $gatewayName, string $identifier, string $message): void { $this->sentMessages[] = [ + 'gateway' => $gatewayName, 'identifier' => $identifier, 'message' => $message, ]; From e1666d6ddf4907d34b53ecbd3f8e59fca03ea3f8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:59:42 -0300 Subject: [PATCH 14/15] fix: cs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Migration/Version16001Date20251227000000.php | 2 +- tests/stubs/OC/Core/Command/Base.php | 2 +- tests/stubs/OC/Hooks/Emitter.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Migration/Version16001Date20251227000000.php b/lib/Migration/Version16001Date20251227000000.php index 8addf360bc..9b848ae2d8 100644 --- a/lib/Migration/Version16001Date20251227000000.php +++ b/lib/Migration/Version16001Date20251227000000.php @@ -9,8 +9,8 @@ namespace OCA\Libresign\Migration; use Closure; -use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\ISchemaWrapper; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; diff --git a/tests/stubs/OC/Core/Command/Base.php b/tests/stubs/OC/Core/Command/Base.php index 4888a96c2d..d7fd3f702e 100644 --- a/tests/stubs/OC/Core/Command/Base.php +++ b/tests/stubs/OC/Core/Command/Base.php @@ -9,4 +9,4 @@ namespace OC\Core\Command { abstract class Base extends \Symfony\Component\Console\Command\Command { } -} \ No newline at end of file +} diff --git a/tests/stubs/OC/Hooks/Emitter.php b/tests/stubs/OC/Hooks/Emitter.php index d7307afb92..4a3e8f8c1c 100644 --- a/tests/stubs/OC/Hooks/Emitter.php +++ b/tests/stubs/OC/Hooks/Emitter.php @@ -17,4 +17,4 @@ public function emit(string $class, string $value, array $option): void { public function listen(string $class, string $value, mixed $closure): void { } } -} \ No newline at end of file +} From 5f1deaa9054bf21892ee791292297c51c75ea37c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:16:01 -0300 Subject: [PATCH 15/15] fix: use server 3rdparty for psalm Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/psalm.yml | 22 + .github/workflows/update-psalm-baseline.yml | 29 +- composer.json | 2 - composer.lock | 749 +------------------- 4 files changed, 51 insertions(+), 751 deletions(-) diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index b2c2c3322d..75f98180a6 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -20,6 +20,8 @@ permissions: jobs: static-analysis: runs-on: ubuntu-latest + env: + APP_NAME: libresign name: static-psalm-analysis steps: @@ -33,7 +35,23 @@ jobs: id: versions uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2.3.1.3.2 + - name: Checkout server + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + ref: ${{ steps.versions.outputs.branches-max }} + + - name: Checkout app + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + submodules: true + path: apps/${{ env.APP_NAME }} + - name: Check enforcement of minimum PHP version ${{ steps.versions.outputs.php-min }} in psalm.xml + working-directory: apps/${{ env.APP_NAME }} run: grep 'phpVersion="${{ steps.versions.outputs.php-min }}' psalm.xml - name: Set up php${{ steps.versions.outputs.php-available }} @@ -49,15 +67,19 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install dependencies + working-directory: apps/${{ env.APP_NAME }} run: | composer remove nextcloud/ocp --dev --no-scripts composer i - name: Check for vulnerable PHP dependencies + working-directory: apps/${{ env.APP_NAME }} run: composer require --dev roave/security-advisories:dev-latest - name: Install nextcloud/ocp + working-directory: apps/${{ env.APP_NAME }} run: composer require --dev nextcloud/ocp:dev-${{ steps.versions.outputs.branches-max }} --ignore-platform-reqs --with-dependencies - name: Run coding standards check + working-directory: apps/${{ env.APP_NAME }} run: composer run psalm -- --threads=1 --monochrome --no-progress --output-format=github diff --git a/.github/workflows/update-psalm-baseline.yml b/.github/workflows/update-psalm-baseline.yml index 42ff7036f5..a6be088633 100644 --- a/.github/workflows/update-psalm-baseline.yml +++ b/.github/workflows/update-psalm-baseline.yml @@ -15,6 +15,8 @@ on: jobs: update-psalm-baseline: runs-on: ubuntu-latest + env: + APP_NAME: libresign if: ${{ github.repository_owner != 'nextcloud-gmbh' }} @@ -37,6 +39,22 @@ jobs: id: versions uses: icewind1991/nextcloud-version-matrix@8a7bac6300b2f0f3100088b297995a229558ddba # v1.3.2.3.1.3.2 + - name: Checkout server + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + ref: ${{ steps.versions.outputs.branches-max }} + + - name: Checkout app + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + ref: ${{ matrix.branches }} + submodules: true + path: apps/${{ env.APP_NAME }} + - name: Set up php${{ steps.versions.outputs.php-available }} uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: @@ -48,15 +66,24 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install dependencies - run: composer i + working-directory: apps/${{ env.APP_NAME }} + run: | + composer remove nextcloud/ocp --dev --no-scripts + composer i + + - name: Install nextcloud/ocp + working-directory: apps/${{ env.APP_NAME }} + run: composer require --dev nextcloud/ocp:dev-${{ steps.versions.outputs.branches-max }} --ignore-platform-reqs --with-dependencies - name: Psalm + working-directory: apps/${{ env.APP_NAME }} run: composer run psalm:update-baseline -- --monochrome --no-progress --output-format=text continue-on-error: true - name: Create Pull Request uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: + path: apps/${{ env.APP_NAME }} token: ${{ secrets.COMMAND_BOT_PAT }} commit-message: Update psalm baseline committer: GitHub diff --git a/composer.json b/composer.json index c4e98110e6..a4fa3daf5d 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,7 @@ { "require-dev": { "bamarni/composer-bin-plugin": "^1.8", - "doctrine/dbal": "3.10.4", "nextcloud/ocp": "dev-master", - "sabre/dav": "4.7.0", "roave/security-advisories": "dev-latest" }, "config": { diff --git a/composer.lock b/composer.lock index b18fb24fff..d986c1217e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fe15c7870c16d5d6c66300cf0d1e8bc3", + "content-hash": "c978760fde108514de13dc57c60ea571", "packages": [ { "name": "cweagans/composer-configurable-plugin", @@ -258,259 +258,6 @@ }, "time": "2026-02-04T10:18:12+00:00" }, - { - "name": "doctrine/dbal", - "version": "3.10.4", - "source": { - "type": "git", - "url": "https://github.com/doctrine/dbal.git", - "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/63a46cb5aa6f60991186cc98c1d1b50c09311868", - "reference": "63a46cb5aa6f60991186cc98c1d1b50c09311868", - "shasum": "" - }, - "require": { - "composer-runtime-api": "^2", - "doctrine/deprecations": "^0.5.3|^1", - "doctrine/event-manager": "^1|^2", - "php": "^7.4 || ^8.0", - "psr/cache": "^1|^2|^3", - "psr/log": "^1|^2|^3" - }, - "conflict": { - "doctrine/cache": "< 1.11" - }, - "require-dev": { - "doctrine/cache": "^1.11|^2.0", - "doctrine/coding-standard": "14.0.0", - "fig/log-test": "^1", - "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "2.1.30", - "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "9.6.29", - "slevomat/coding-standard": "8.24.0", - "squizlabs/php_codesniffer": "4.0.0", - "symfony/cache": "^5.4|^6.0|^7.0|^8.0", - "symfony/console": "^4.4|^5.4|^6.0|^7.0|^8.0" - }, - "suggest": { - "symfony/console": "For helpful console commands such as SQL execution and import of files." - }, - "bin": [ - "bin/doctrine-dbal" - ], - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\DBAL\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - } - ], - "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", - "homepage": "https://www.doctrine-project.org/projects/dbal.html", - "keywords": [ - "abstraction", - "database", - "db2", - "dbal", - "mariadb", - "mssql", - "mysql", - "oci8", - "oracle", - "pdo", - "pgsql", - "postgresql", - "queryobject", - "sasql", - "sql", - "sqlite", - "sqlserver", - "sqlsrv" - ], - "support": { - "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.10.4" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", - "type": "tidelift" - } - ], - "time": "2025-11-29T10:46:08+00:00" - }, - { - "name": "doctrine/deprecations", - "version": "1.1.6", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", - "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "phpunit/phpunit": "<=7.5 || >=14" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^12 || ^14", - "phpstan/phpstan": "1.4.10 || 2.1.30", - "phpstan/phpstan-phpunit": "^1.0 || ^2", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", - "psr/log": "^1 || ^2 || ^3" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", - "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/1.1.6" - }, - "time": "2026-02-07T07:09:04+00:00" - }, - { - "name": "doctrine/event-manager", - "version": "2.1.1", - "source": { - "type": "git", - "url": "https://github.com/doctrine/event-manager.git", - "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/event-manager/zipball/dda33921b198841ca8dbad2eaa5d4d34769d18cf", - "reference": "dda33921b198841ca8dbad2eaa5d4d34769d18cf", - "shasum": "" - }, - "require": { - "php": "^8.1" - }, - "conflict": { - "doctrine/common": "<2.9" - }, - "require-dev": { - "doctrine/coding-standard": "^14", - "phpdocumentor/guides-cli": "^1.4", - "phpstan/phpstan": "^2.1.32", - "phpunit/phpunit": "^10.5.58" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - }, - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - } - ], - "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", - "homepage": "https://www.doctrine-project.org/projects/event-manager.html", - "keywords": [ - "event", - "event dispatcher", - "event manager", - "event system", - "events" - ], - "support": { - "issues": "https://github.com/doctrine/event-manager/issues", - "source": "https://github.com/doctrine/event-manager/tree/2.1.1" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", - "type": "tidelift" - } - ], - "time": "2026-01-29T07:11:08+00:00" - }, { "name": "nextcloud/ocp", "version": "dev-master", @@ -561,55 +308,6 @@ }, "time": "2026-06-19T02:51:56+00:00" }, - { - "name": "psr/cache", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "support": { - "source": "https://github.com/php-fig/cache/tree/3.0.0" - }, - "time": "2021-02-03T23:26:27+00:00" - }, { "name": "psr/clock", "version": "1.0.0", @@ -2022,451 +1720,6 @@ } ], "time": "2026-06-19T21:28:22+00:00" - }, - { - "name": "sabre/dav", - "version": "4.7.0", - "source": { - "type": "git", - "url": "https://github.com/sabre-io/dav.git", - "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a", - "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-date": "*", - "ext-dom": "*", - "ext-iconv": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-pcre": "*", - "ext-simplexml": "*", - "ext-spl": "*", - "lib-libxml": ">=2.7.0", - "php": "^7.1.0 || ^8.0", - "psr/log": "^1.0 || ^2.0 || ^3.0", - "sabre/event": "^5.0", - "sabre/http": "^5.0.5", - "sabre/uri": "^2.0", - "sabre/vobject": "^4.2.1", - "sabre/xml": "^2.0.1" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.19", - "monolog/monolog": "^1.27 || ^2.0", - "phpstan/phpstan": "^0.12 || ^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" - }, - "suggest": { - "ext-curl": "*", - "ext-imap": "*", - "ext-pdo": "*" - }, - "bin": [ - "bin/sabredav", - "bin/naturalselection" - ], - "type": "library", - "autoload": { - "psr-4": { - "Sabre\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" - } - ], - "description": "WebDAV Framework for PHP", - "homepage": "http://sabre.io/", - "keywords": [ - "CalDAV", - "CardDAV", - "WebDAV", - "framework", - "iCalendar" - ], - "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/dav/issues", - "source": "https://github.com/fruux/sabre-dav" - }, - "time": "2024-10-29T11:46:02+00:00" - }, - { - "name": "sabre/event", - "version": "5.1.9", - "source": { - "type": "git", - "url": "https://github.com/sabre-io/event.git", - "reference": "743f1d04811fd5b89f67878d002f6a273ccb089f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sabre-io/event/zipball/743f1d04811fd5b89f67878d002f6a273ccb089f", - "reference": "743f1d04811fd5b89f67878d002f6a273ccb089f", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2.17.1||^3.95", - "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" - }, - "type": "library", - "autoload": { - "files": [ - "lib/coroutine.php", - "lib/Loop/functions.php", - "lib/Promise/functions.php" - ], - "psr-4": { - "Sabre\\Event\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" - } - ], - "description": "sabre/event is a library for lightweight event-based programming", - "homepage": "http://sabre.io/event/", - "keywords": [ - "EventEmitter", - "async", - "coroutine", - "eventloop", - "events", - "hooks", - "plugin", - "promise", - "reactor", - "signal" - ], - "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/event/issues", - "source": "https://github.com/fruux/sabre-event" - }, - "time": "2026-07-07T09:13:04+00:00" - }, - { - "name": "sabre/http", - "version": "5.1.13", - "source": { - "type": "git", - "url": "https://github.com/sabre-io/http.git", - "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sabre-io/http/zipball/7c2a14097d1a0de2347dcbdc91a02f38e338f4db", - "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-curl": "*", - "ext-mbstring": "*", - "php": "^7.1 || ^8.0", - "sabre/event": ">=4.0 <6.0", - "sabre/uri": "^2.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", - "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" - }, - "suggest": { - "ext-curl": " to make http requests with the Client class" - }, - "type": "library", - "autoload": { - "files": [ - "lib/functions.php" - ], - "psr-4": { - "Sabre\\HTTP\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" - } - ], - "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", - "homepage": "https://github.com/fruux/sabre-http", - "keywords": [ - "http" - ], - "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/http/issues", - "source": "https://github.com/fruux/sabre-http" - }, - "time": "2025-09-09T10:21:47+00:00" - }, - { - "name": "sabre/uri", - "version": "2.3.4", - "source": { - "type": "git", - "url": "https://github.com/sabre-io/uri.git", - "reference": "b76524c22de90d80ca73143680a8e77b1266c291" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291", - "reference": "b76524c22de90d80ca73143680a8e77b1266c291", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.63", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^1.12", - "phpstan/phpstan-phpunit": "^1.4", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "^9.6" - }, - "type": "library", - "autoload": { - "files": [ - "lib/functions.php" - ], - "psr-4": { - "Sabre\\Uri\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" - } - ], - "description": "Functions for making sense out of URIs.", - "homepage": "http://sabre.io/uri/", - "keywords": [ - "rfc3986", - "uri", - "url" - ], - "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/uri/issues", - "source": "https://github.com/fruux/sabre-uri" - }, - "time": "2024-08-27T12:18:16+00:00" - }, - { - "name": "sabre/vobject", - "version": "4.6.1", - "source": { - "type": "git", - "url": "https://github.com/sabre-io/vobject.git", - "reference": "63613f6c53a0a2bddfe22caba0d052e6c59f7d0e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sabre-io/vobject/zipball/63613f6c53a0a2bddfe22caba0d052e6c59f7d0e", - "reference": "63613f6c53a0a2bddfe22caba0d052e6c59f7d0e", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": "^7.1 || ^8.0", - "sabre/xml": "^2.1 || ^3.0 || ^4.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2.17.1", - "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0", - "phpunit/php-invoker": "^2.0 || ^3.1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" - }, - "suggest": { - "hoa/bench": "If you would like to run the benchmark scripts" - }, - "bin": [ - "bin/vobject", - "bin/generate_vcards" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Sabre\\VObject\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" - }, - { - "name": "Dominik Tobschall", - "email": "dominik@fruux.com", - "homepage": "http://tobschall.de/", - "role": "Developer" - }, - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net", - "homepage": "http://mnt.io/", - "role": "Developer" - } - ], - "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", - "homepage": "http://sabre.io/vobject/", - "keywords": [ - "availability", - "freebusy", - "iCalendar", - "ical", - "ics", - "jCal", - "jCard", - "recurrence", - "rfc2425", - "rfc2426", - "rfc2739", - "rfc4770", - "rfc5545", - "rfc5546", - "rfc6321", - "rfc6350", - "rfc6351", - "rfc6474", - "rfc6638", - "rfc6715", - "rfc6868", - "vCalendar", - "vCard", - "vcf", - "xCal", - "xCard" - ], - "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/vobject/issues", - "source": "https://github.com/fruux/sabre-vobject" - }, - "time": "2026-07-07T03:20:17+00:00" - }, - { - "name": "sabre/xml", - "version": "2.2.11", - "source": { - "type": "git", - "url": "https://github.com/sabre-io/xml.git", - "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", - "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xmlreader": "*", - "ext-xmlwriter": "*", - "lib-libxml": ">=2.6.20", - "php": "^7.1 || ^8.0", - "sabre/uri": ">=1.0,<3.0.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", - "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" - }, - "type": "library", - "autoload": { - "files": [ - "lib/Deserializer/functions.php", - "lib/Serializer/functions.php" - ], - "psr-4": { - "Sabre\\Xml\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" - }, - { - "name": "Markus Staab", - "email": "markus.staab@redaxo.de", - "role": "Developer" - } - ], - "description": "sabre/xml is an XML library that you may not hate.", - "homepage": "https://sabre.io/xml/", - "keywords": [ - "XMLReader", - "XMLWriter", - "dom", - "xml" - ], - "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/xml/issues", - "source": "https://github.com/fruux/sabre-xml" - }, - "time": "2024-09-06T07:37:46+00:00" } ], "aliases": [],