Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ebc2a24
feat(rbac): backfill publication+document read-rule two-rule shape (W…
WilcoLouwerse Aug 25, 2026
02d2f0d
feat(search): refactor assemblePublicSearchResults per WOO-536 (Fase 5)
WilcoLouwerse Aug 27, 2026
bb21d16
fix(search): address review #5041094649 findings (WOO-536)
WilcoLouwerse Aug 27, 2026
9d4449c
chore(openspec): sync + archive fix-fts-catalog-model-alignment (WOO-…
WilcoLouwerse Aug 28, 2026
9b41d66
Merge branch 'main' into hotfix/woo-536-fts-catalog-model
WilcoLouwerse Aug 31, 2026
3d3aa37
fix(search): resolve document→publication via document's own _relatio…
WilcoLouwerse Aug 31, 2026
b6dc3f8
test(search): rewrite PublicationQueryServiceTest for Fase-5 (WOO-536)
WilcoLouwerse Aug 31, 2026
c471e48
test(smoke): add smoke-fts-woo536.sh acceptance-criteria script
WilcoLouwerse Aug 31, 2026
7ed145e
chore(gates): satisfy hydra gate-16 + gate-66 (WOO-536)
WilcoLouwerse Aug 31, 2026
f476bbd
fix(search): rebuild `total` from emitted rows (WOO-536)
WilcoLouwerse Aug 31, 2026
c0595ba
fix(search): address self-review D1-D6 findings (WOO-536)
WilcoLouwerse Aug 31, 2026
b4939ff
fix(search): preserve pagination signal in envelope total (WOO-536)
WilcoLouwerse Sep 1, 2026
45a4c53
fix(quality): resolve PR-introduced PHP quality failures (WOO-536)
WilcoLouwerse Sep 1, 2026
638f8c8
fix(routes): return 404 (not 500) for mixed-case catalog slugs (WOO-536)
WilcoLouwerse Sep 1, 2026
7be7ccb
fix(search): isObjectPublic reads English field names (WOO-536)
WilcoLouwerse Sep 1, 2026
d44ec07
fix(quality): phpcs errors in is_string normaliser (WOO-536)
WilcoLouwerse Sep 1, 2026
5c42d19
Merge pull request #1154 from ConductionNL/hotfix/woo-536-fts-catalog…
WilcoLouwerse Sep 1, 2026
b47ebf4
chore(release): sync main back into development
github-actions[bot] Sep 1, 2026
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
8 changes: 8 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ Doe een [featureverzoek](https://github.com/OpenCatalogi/.github/issues/new/choo
non-destructive; safe to re-run.
-->
<step>OCA\OpenCatalogi\Repair\RenameDutchPublicationColumns</step>
<!--
WOO-536 Fase 4: backfill the two-rule read-block on publication +
document schemas (adds the `depublicationDate` clause required by
Robert's DoD). Runs AFTER RenameDutchPublicationColumns so the guard
sees English field names. Idempotent: skips schemas that already
carry a two-rule shape or admin-customised rules.
-->
<step>OCA\OpenCatalogi\Repair\WOO536RepairReadRules</step>
<!--
The background jobs moved from OCA\OpenCatalogi\Cron to
OCA\OpenCatalogi\BackgroundJob in #1119. The <job> entries above
Expand Down
80 changes: 40 additions & 40 deletions appinfo/routes.php

Large diffs are not rendered by default.

309 changes: 309 additions & 0 deletions lib/Repair/WOO536RepairReadRules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
<?php
/**
* WOO-536 repair step for publication + document read-rule shape.
*
* @category Repair
* @package OCA\OpenCatalogi\Repair
*
* @author Conduction Development Team <info@conduction.nl>
* @copyright 2024 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: 2024 Conduction B.V. <info@conduction.nl>
*
* @version GIT: <git_id>
*
* @link https://www.OpenCatalogi.nl
*
* @spec openspec/changes/fix-fts-catalog-model-alignment/tasks.md
*/

declare(strict_types=1);

namespace OCA\OpenCatalogi\Repair;

use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;
use OCP\App\IAppManager;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;

/**
* Repair step that backfills the two-rule `depublicationDate` read-shape
* on the `publication` and `document` schemas for existing installations.
*
* WOO-536 (Robert Zondervan, 2026-08-12) requires the public search endpoint
* to filter depublished objects. The seed JSON in
* `lib/Settings/publication_register.json` was updated to the two-rule shape:
*
* read: [
* { group: public, match: { publicationDate $lte $now, depublicationDate $gte $now } },
* { group: public, match: { publicationDate $lte $now, depublicationDate $exists false } },
* "authenticated"
* ]
*
* Fresh installs pick this up via the standard seed-import path
* (InitializeSettings::run → SettingsService::loadSettings). Existing
* installations may still carry the older single-rule shape from before
* this change — this repair step upgrades them.
*
* Guards (in order of check):
* 1. OR must be installed (skip otherwise).
* 2. The schema's `authorization.read` must exist on disk.
* 3. The read block must be on the single-rule shape (missing
* depublicationDate). Admin-customised shapes are left alone.
* 4. Update is idempotent — re-running has no effect once schema is on
* the two-rule shape.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity) Guard branches are the point of the class.
*/
class WOO536RepairReadRules implements IRepairStep
{
/**
* Constructor.
*
* @param IAppManager $appManager The app manager.
* @param ContainerInterface $container The container.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
private readonly IAppManager $appManager,
private readonly ContainerInterface $container,
private readonly LoggerInterface $logger
) {

}//end __construct()

/**
* Get the name of this repair step.
*
* @return string
*/
public function getName(): string
{
return "Backfill publication + document read-rule two-rule shape (WOO-536)";

}//end getName()

/**
* Run the repair step.
*
* @param IOutput $output The output interface.
*
* @return void
*
* @spec openspec/specs/search/spec.md
*/
public function run(IOutput $output): void
{
// Guard 1: OR must be enabled (schema authorization lives in OR).
// `isEnabledForAnyone()` replaces the deprecated `getInstalledApps()`
// membership check in NC 30+.
if ($this->appManager->isEnabledForAnyone('openregister') === false) {
$output->warning('OpenRegister app is not enabled - skipping WOO-536 read-rule backfill');
return;
}

try {
$schemaMapper = $this->container->get('OCA\\OpenRegister\\Db\\SchemaMapper');
} catch (\Throwable $e) {
$output->warning('OpenRegister SchemaMapper unavailable - skipping WOO-536 read-rule backfill');
return;
}

$updated = 0;
$skipped = 0;
foreach (['publication', 'document'] as $slug) {
$result = $this->maybeUpgradeSchema(schemaMapper: $schemaMapper, slug: $slug, output: $output);
if ($result === 'updated') {
$updated++;
continue;
}
$skipped++;
}

$output->info(
sprintf('WOO-536 read-rule backfill: %d schema(s) upgraded, %d skipped (already correct or customised)', $updated, $skipped)
);

}//end run()

/**
* Locate a schema by slug and upgrade its read-block if it carries the
* old single-rule shape.
*
* @param object $schemaMapper The OpenRegister SchemaMapper (typed as object
* so this repair step can compile without an OR
* dependency at analysis-time).
* @param string $slug Schema slug ('publication' or 'document').
* @param IOutput $output The output interface for progress reporting.
*
* @return string 'updated' | 'skipped'
*/
private function maybeUpgradeSchema(object $schemaMapper, string $slug, IOutput $output): string
{
try {
$schemas = $schemaMapper->findAll(filters: ['slug' => $slug]);
} catch (\Throwable $e) {
$output->warning("WOO-536: cannot list schemas by slug '{$slug}' — {$e->getMessage()}");
return 'skipped';
}

if (empty($schemas) === true) {
$output->info("WOO-536: schema '{$slug}' not present in this installation — nothing to upgrade");
return 'skipped';
}

$updated = false;
foreach ($schemas as $schema) {
$authorization = $schema->getAuthorization();
if (is_array($authorization) === false || isset($authorization['read']) === false) {
$output->info("WOO-536: schema '{$slug}' (id ".$schema->getId().") has no authorization.read block — skipping");
continue;
}

$read = $authorization['read'];
if ($this->isSingleRuleShape(read: $read) === false) {
$output->info(
"WOO-536: schema '{$slug}' (id ".$schema->getId().") already on two-rule shape or admin-customised — skipping"
);
continue;
}

// Upgrade: replace the single conditional public rule with the two-rule
// depublicationDate-aware shape. Preserve any non-public elements
// (like 'authenticated') by keeping them after the two new rules.
$authorization['read'] = $this->buildTwoRuleRead(existing: $read);
$schema->setAuthorization($authorization);

try {
$schemaMapper->update($schema);
$output->info(
"WOO-536: schema '{$slug}' (id ".$schema->getId().") upgraded to two-rule shape"
);
$updated = true;
$this->logger->info(
'WOO-536 repair: schema authorization.read upgraded to two-rule shape',
['schema' => $slug, 'schemaId' => $schema->getId()]
);
} catch (\Throwable $e) {
$output->warning(
"WOO-536: failed to update schema '{$slug}' (id ".$schema->getId()."): {$e->getMessage()}"
);
}
}//end foreach

if ($updated === true) {
return 'updated';
}
return 'skipped';

}//end maybeUpgradeSchema()

/**
* Detect the old single-rule shape.
*
* Old shape (before this repair):
*
* read: [
* { group: public, match: { publicationDate: { $lte: $now } } },
* "authenticated" (optional third element)
* ]
*
* Any deviation from this shape (extra rules, different match keys,
* additional operators on publicationDate) is considered admin-customised
* and left alone.
*
* @param array $read The current read-block array.
*
* @return bool True when the block matches the pre-fix single-rule shape.
*/
private function isSingleRuleShape(array $read): bool
{
// Expected shape: exactly one conditional public rule + optional simple
// string elements (e.g., "authenticated"). More than one conditional
// rule means admin has customised, and any two-rule shape (post-fix)
// will have exactly two conditional public rules.
$conditionalCount = 0;
$matchedShape = false;
foreach ($read as $element) {
if (is_string($element) === true) {
// Simple element like "authenticated" or "public" — allowed.
continue;
}

if (is_array($element) === false) {
return false;
}

$conditionalCount++;
if (($element['group'] ?? null) !== 'public') {
return false;
}

$match = ($element['match'] ?? []);
if (is_array($match) === false) {
return false;
}

// Old shape: exactly one match key, `publicationDate: { $lte: $now }`.
if (count($match) !== 1 || isset($match['publicationDate']) === false) {
return false;
}

$publicationDate = $match['publicationDate'];
if (is_array($publicationDate) === false
|| count($publicationDate) !== 1
|| ($publicationDate['$lte'] ?? null) !== '$now'
) {
return false;
}

$matchedShape = true;
}//end foreach

// Exactly one conditional public rule with the old-shape match => needs upgrade.
return $matchedShape === true && $conditionalCount === 1;

}//end isSingleRuleShape()

/**
* Build the two-rule shape while preserving any non-conditional
* elements (e.g., "authenticated") from the existing read block.
*
* @param array $existing The existing read-block array (must match single-rule shape).
*
* @return array The two-rule read block.
*/
private function buildTwoRuleRead(array $existing): array
{
$twoRule = [
[
'group' => 'public',
'match' => [
'publicationDate' => ['$lte' => '$now'],
'depublicationDate' => ['$gte' => '$now'],
],
],
[
'group' => 'public',
'match' => [
'publicationDate' => ['$lte' => '$now'],
'depublicationDate' => ['$exists' => false],
],
],
];

// Preserve simple-string elements (like "authenticated") from the
// existing read block, in their original order after the two new rules.
foreach ($existing as $element) {
if (is_string($element) === true) {
$twoRule[] = $element;
}
}

return $twoRule;

}//end buildTwoRuleRead()
}//end class
Loading