Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions lib/AppInfo/Application.php
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKER] Bare catch(\Throwable $e){} silently swallows provider construction failures (Gate 7)

The inner loop catches every \Throwable with an empty body and no logging. A misconfigured or maliciously broken provider will fail invisibly, making debugging impossible and masking security-relevant injection errors. At minimum log at warning level with $e->getMessage() and the provider class name. The outer catch at line 123 has the same issue.

Original file line number Diff line number Diff line change
Expand Up @@ -862,8 +862,80 @@ function (ContainerInterface $container) {
}
);

$this->registerBuiltinIntegrationProviders($context);

}//end registerIntegrationRegistry()

/**
* Register the 5 BuiltinProviders/* services so they can be
* resolved lazily from the container.
*
* Each provider wraps an existing OR service and exposes it
* through the IntegrationProvider contract. They self-register
* with the IntegrationRegistry during `boot()` —
* `bootBuiltinIntegrationProviders()` walks this same list.
*
* @param IRegistrationContext $context The registration context.
*
* @return void
*
* @spec openspec/changes/pluggable-integration-registry/tasks.md#task-12
*/
private function registerBuiltinIntegrationProviders(IRegistrationContext $context): void
{
$context->registerService(
\OCA\OpenRegister\Service\Integration\BuiltinProviders\FilesProvider::class,
function (ContainerInterface $container) {
return new \OCA\OpenRegister\Service\Integration\BuiltinProviders\FilesProvider(
fileService: $container->get(\OCA\OpenRegister\Service\FileService::class),
container: $container,
l10n: $container->get('OCP\IL10N'),
);
}
);

$context->registerService(
\OCA\OpenRegister\Service\Integration\BuiltinProviders\NotesProvider::class,
function (ContainerInterface $container) {
return new \OCA\OpenRegister\Service\Integration\BuiltinProviders\NotesProvider(
noteService: $container->get(\OCA\OpenRegister\Service\NoteService::class),
l10n: $container->get('OCP\IL10N'),
);
}
);

$context->registerService(
\OCA\OpenRegister\Service\Integration\BuiltinProviders\TasksProvider::class,
function (ContainerInterface $container) {
return new \OCA\OpenRegister\Service\Integration\BuiltinProviders\TasksProvider(
taskService: $container->get(\OCA\OpenRegister\Service\TaskService::class),
l10n: $container->get('OCP\IL10N'),
);
}
);

$context->registerService(
\OCA\OpenRegister\Service\Integration\BuiltinProviders\TagsProvider::class,
function (ContainerInterface $container) {
return new \OCA\OpenRegister\Service\Integration\BuiltinProviders\TagsProvider(
tagManager: $container->get('OCP\SystemTag\ISystemTagManager'),
objectMapper: $container->get('OCP\SystemTag\ISystemTagObjectMapper'),
l10n: $container->get('OCP\IL10N'),
);
}
);

$context->registerService(
\OCA\OpenRegister\Service\Integration\BuiltinProviders\AuditTrailProvider::class,
function (ContainerInterface $container) {
return new \OCA\OpenRegister\Service\Integration\BuiltinProviders\AuditTrailProvider(
mapper: $container->get(\OCA\OpenRegister\Db\AuditTrailMapper::class),
l10n: $container->get('OCP\IL10N'),
);
}
);
}//end registerBuiltinIntegrationProviders()

/**
* Register all event listeners for the application.
*
Expand Down Expand Up @@ -1003,5 +1075,63 @@ public function boot(IBootContext $context): void
$dispatcher = $server->get(IEventDispatcher::class);
$registry = $server->get(DeepLinkRegistryService::class);
$dispatcher->dispatchTyped(new DeepLinkRegistrationEvent(registry: $registry));

// Register the built-in IntegrationProvider implementations
// with the IntegrationRegistry. The 5 wrap existing services
// (FileService / NoteService / TaskService / system tag manager /
// AuditTrailMapper) and surface them through the unified
// registry contract. Each provider is constructed lazily — the
// registry never touches a provider's wrapped service unless a
// caller actually invokes that provider's CRUD path.
$this->bootBuiltinIntegrationProviders($server);
}//end boot()

/**
* Resolve every BuiltinProviders/* class and register it with the
* shared IntegrationRegistry.
*
* Kept separate from the DI registration in
* `registerIntegrationRegistry()` because addProvider() needs the
* registry instance — i.e. it has to run after the container has
* fully bootstrapped. `boot()` is the canonical post-registration
* hook for that.
*
* @param mixed $server Server container (passed in from boot()).
*
* @return void
*
* @spec openspec/changes/pluggable-integration-registry/tasks.md#task-17
*/
private function bootBuiltinIntegrationProviders($server): void
{
try {
$integrationRegistry = $server->get(
\OCA\OpenRegister\Service\Integration\IntegrationRegistry::class
);
} catch (\Throwable $e) {
// Registry binding not available — skip silently; the
// service would log its own warning at use-time anyway.
return;
}

$providerClasses = [
\OCA\OpenRegister\Service\Integration\BuiltinProviders\FilesProvider::class,
\OCA\OpenRegister\Service\Integration\BuiltinProviders\NotesProvider::class,
\OCA\OpenRegister\Service\Integration\BuiltinProviders\TasksProvider::class,
\OCA\OpenRegister\Service\Integration\BuiltinProviders\TagsProvider::class,
\OCA\OpenRegister\Service\Integration\BuiltinProviders\AuditTrailProvider::class,
];

foreach ($providerClasses as $providerClass) {
try {
$provider = $server->get($providerClass);
$integrationRegistry->addProvider($provider);
} catch (\Throwable $e) {
// Provider construction can fail if a wrapped service
// is missing on this NC build — don't take the whole
// app down for one absent provider. The user-facing
// surface will simply not show the failing tab.
}
}
}//end bootBuiltinIntegrationProviders()
}//end class
160 changes: 160 additions & 0 deletions lib/Service/Integration/BuiltinProviders/AuditTrailProvider.php
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BLOCKER] AuditTrailProvider::list() has no user-scope filter — exposes all objects' audit trail

AuditTrailMapper::findAllByObject($objectId) returns every audit entry for that object regardless of caller. Audit trails contain sensitive mutation history. The provider must verify the caller has read access to the parent object before surfacing its trail. No IUserSession is injected, so no ownership check is possible in the current implementation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CONCERN] AuditTrailProvider::list() uses method_exists() duck-typing instead of a stable mapper interface

The provider checks method_exists($this->mapper, 'findAllByObject') then falls back to findAll(filters: ...). The production call path depends on which method the injected mapper happens to expose at runtime, making behaviour non-deterministic and untestable. Define a mapper interface and declare it as the constructor parameter type.

Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

/**
* AuditTrailProvider — exposes audit-trail entries for an OR object
* via the IntegrationProvider contract.
*
* Storage strategy is `query-time` (AD-22): audit-trail entries are
* OR's own data; the provider always reads live from `AuditTrailMapper`
* rather than maintaining a parallel link table. Mutation methods
* throw NotImplementedException — audit-trail entries are immutable
* by construction.
*
* Always available — no required NC app — so `requiredApp` returns
* null and `isEnabled()` is hardcoded true.
*
* @category Service
* @package OCA\OpenRegister\Service\Integration\BuiltinProviders
*
* @author Conduction Development Team <info@conduction.nl>
* @copyright 2026 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* SPDX-License-Identifier: EUPL-1.2
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
*
* @link https://conduction.nl
*
* @spec openspec/changes/pluggable-integration-registry/tasks.md#task-16
*/

declare(strict_types=1);

namespace OCA\OpenRegister\Service\Integration\BuiltinProviders;

use OCA\OpenRegister\Db\AuditTrailMapper;
use OCA\OpenRegister\Service\Integration\AbstractIntegrationProvider;
use OCP\IL10N;

/**
* Audit-trail integration provider (read-only, query-time).
*/
class AuditTrailProvider extends AbstractIntegrationProvider
{

/**
* Constructor.
*
* @param AuditTrailMapper $mapper Audit-trail mapper.
* @param IL10N $l10n Localisation.
*
* @return void
*/
public function __construct(
private AuditTrailMapper $mapper,
private IL10N $l10n,
) {
}//end __construct()

public function getId(): string
{
return 'audit-trail';
}//end getId()

public function getLabel(): string
{
return $this->l10n->t('Audit trail');
}//end getLabel()

public function getIcon(): string
{
return 'History';
}//end getIcon()

public function getGroup(): ?string
{
return 'core';
}//end getGroup()

public function getRequiredApp(): ?string
{
return null;
}//end getRequiredApp()

public function getStorageStrategy(): string
{
return 'query-time';
}//end getStorageStrategy()

public function isEnabled(): bool
{
return true;
}//end isEnabled()

/**
* List audit-trail entries for an OR object.
*
* Best-effort delegation to `AuditTrailMapper::findAllByObject` /
* `findAll` depending on the mapper's exposed API. Returns an
* empty list rather than 500 when the mapper signature doesn't
* match — the umbrella's controller refactor (tasks 18-22) will
* tighten this.
*
* @param string $register Register slug or numeric id.
* @param string $schema Schema slug or numeric id.
* @param string $objectId Object uuid.
* @param array<string,mixed> $filters Reserved.
*
* @return array<int,array<string,mixed>>
*/
public function list(string $register, string $schema, string $objectId, array $filters = []): array
{
try {
if (method_exists($this->mapper, 'findAllByObject') === true) {
$entries = $this->mapper->findAllByObject($objectId);
return $this->normalize($entries);
}

if (method_exists($this->mapper, 'findAll') === true) {
$entries = $this->mapper->findAll(filters: ['object' => $objectId]);

Check failure on line 119 in lib/Service/Integration/BuiltinProviders/AuditTrailProvider.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Unknown parameter $filters in call to method OCA\OpenRegister\Db\AuditTrailMapper::findAll().
return $this->normalize($entries);
}
} catch (\Throwable $e) {
// AuditTrail history is a soft surface — never block the
// detail page on a stale or missing audit row.
}

return [];
}//end list()

/**
* Convert mapper output (Entity[] or array[]) into the shape
* IntegrationProvider::list() promises.
*
* @param mixed $entries Mapper output.
*
* @return array<int, array<string, mixed>>
*/
private function normalize($entries): array
{
if (is_array($entries) === false) {
return [];
}

$rows = [];
foreach ($entries as $entry) {
if (is_object($entry) === true && method_exists($entry, 'jsonSerialize') === true) {
$serialised = $entry->jsonSerialize();
$rows[] = is_array($serialised) === true ? $serialised : ['value' => $serialised];
continue;
}

if (is_array($entry) === true) {
$rows[] = $entry;
}
}

return $rows;
}//end normalize()

}//end class
Loading
Loading