feat(i18n): Wrap bare strings and convert Dutch to English keys#1273
Conversation
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 147/147 | |||
| npm | ✅ | ✅ 599/599 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-16 17:30 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 147/147 | |||
| npm | ✅ | ✅ 599/599 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-16 18:12 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 147/147 | |||
| npm | ✅ | ✅ 599/599 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-16 18:15 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 147/147 | |||
| npm | ✅ | ✅ 599/599 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-16 22:09 UTC
Download the full PDF report from the workflow artifacts.
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Strict-mode review — 9 blockers, 9 concerns, 4 minors.
Strict mode requires REQUEST_CHANGES on any 🔴 or 🟡. Please address blockers and concerns before merge.
🔴 Blocker — PR targets
|
|
[composer.lock] ### 🔴 Blocker — CI: The Suggested fix: Run |
🔴 Blocker — CI:
|
🔴 Blocker — CI:
|
🔴 Blocker — CI:
|
|
[.phpunit.cache] 🟡 Concern — PHPUnit cache file committed to the repository
Suggested fix: Add |
Review fixes — MWest2020 comments addressed (commits
|
CI fixes — commit
|
Review — feat(i18n): Wrap bare strings and convert Dutch to English keysVerdict: 🔴 REQUEST_CHANGES — twee runtime blockers (PostgreSQL SQL syntax + misleidende security-claim) en CI rood op PHPUnit. Het i18n-werk zelf is groot, schoon, en consistent — de issues zitten in de meegelifte PHP-changes. ✅ Wat goed is
Findings1. 🔴 Blocker — PHP comments staan binnen SQL string literal → PostgreSQL syntax error → silent empty results📍 if ($isPostgres === true) {
// PostgreSQL: use JSONB containment operator.
$sql = "SELECT * FROM {$fullTableName}
WHERE (_deleted IS NULL OR _deleted::text = 'null')
AND {$columnName} IS NOT NULL
// Note: ::jsonb cast is intentional for polymorphic column support, but defeats any GIN
// index on $columnName if it is already jsonb. Acceptable for now; if a GIN index is added
// in a future migration for this column, branch the query to omit the cast in the jsonb case.
AND {$columnName}::jsonb @> to_jsonb(?::text)
LIMIT 100";
}De drie De omhullende Impact:
Suggested fix: verplaats de comment buiten de string, vóór de if ($isPostgres === true) {
// PostgreSQL: use JSONB containment operator.
// Note: ::jsonb cast is intentional for polymorphic column support, but defeats any GIN
// index on $columnName if it is already jsonb. Acceptable for now; if a GIN index is added
// in a future migration for this column, branch the query to omit the cast in the jsonb case.
$sql = "SELECT * FROM {$fullTableName}
WHERE (_deleted IS NULL OR _deleted::text = 'null')
AND {$columnName} IS NOT NULL
AND {$columnName}::jsonb @> to_jsonb(?::text)
LIMIT 100";
}Idealiter ook een integration-test toevoegen die 2. 🔴 Blocker — Comment claimt downstream RBAC die niet bestaat in de code; reverse-lookup lekt cross-tenant📍 De nieuwe comment in
Maar in de code zelf, direct daaronder: foreach ($objects as $object) {
$results[] = [
'entityType' => 'object',
'uuid' => $object->getUuid(),
'name' => $object->getName(),
'schema' => $schema->getTitle(),
'schemaId' => $schema->getId(),
'register' => $object->getRegister(),
];
}Er is geen access-check tussen Impact: elke ingelogde user ( Daarbovenop is het claim-vs-code mismatch een second-order risk: een toekomstige security-reviewer of -auditor leest de comment, gelooft dat downstream-validatie bestaat, en wordt mislead. Een comment die zegt "X is enforced" zonder dat X bestaat is erger dan helemaal geen comment — het verstomt latere checks. Twee fix-pads: (a) Voeg de claim ook daadwerkelijk toe — filter per-row tegen de calling user vóór de foreach ($objects as $object) {
if (!$this->accessControl->canRead($object, $this->userSession->getUser())) {
continue;
}
$results[] = [ ... ];
}(b) Of erken de cross-tenant exposure expliciet en pas comment + scope aan. Voorbeeld: // WARNING: this endpoint exposes object UUIDs, names, schema titles, register and organisation
// names cross-tenant for any authenticated user. Acceptable today because (motivation), but if
// tenant isolation tightens, this needs a per-row access check (TODO #N).Optie (a) is de juiste lange-termijn fix. Optie (b) is acceptabel als interim, mits gekoppeld aan een tracking-issue. Wat NIET acceptabel is, is de huidige stand: een comment die downstream-checks belooft die nergens staan. Per ADR-020 (gate scope = PR diff): de comment-edit is binnen scope. Of de onderliggende missende RBAC-check al pre-existing was doet er minder toe — deze PR introduceert actief de misleidende belofte dat de check bestaat. 3. 🟡 Concern — 2× PHPUnit RED via development-merge inherited, niet door deze PR geïntroduceerd maar wel deze PR's responsibility om te clearen📍 CI:
Geen van deze files staat in de PR-diff. Ze komen mee via de Suggested fix: óf landen er eerst test-fixes op 4. 🟡 Concern —
|
MWest2020
left a comment
There was a problem hiding this comment.
🔴 REQUEST_CHANGES — 2 runtime blockers: (1) PHP comments inside SQL string literal → PostgreSQL syntax error in MagicMapper.php, (2) misleading downstream-RBAC claim in LinkedEntityService.php. Full review: #1273 (comment)
MWest2020
left a comment
There was a problem hiding this comment.
🔴 REQUEST_CHANGES — 2 runtime blockers. Volledige review: #1273 (comment)
|
Sorry voor de dubbele REQUEST_CHANGES — eerste submit retourneerde silent, ik dacht dat hij niet was binnengekomen en retried. Beide verwijzen naar dezelfde review; negeer de tweede gerust. |
- MagicMapper.php: move PHP // comments outside SQL string literal; they were embedded in the double-quoted query string and sent to PostgreSQL verbatim, causing a syntax error on every findByLinkedEntity call (PostgreSQL only supports -- and /* */ comment markers). - LinkedEntityService.php: rewrite the scanMagicTables() comment to accurately document the cross-tenant exposure instead of falsely claiming that per-row RBAC enforcement happens downstream. The previous comment could mislead future reviewers into believing a security control existed that was never implemented. - AuditTrailControllerTest.php: configure userSession/groupManager mocks to return an admin user in setUp(), so that export() and clearAll() — both gated behind requireAdmin() — no longer short- circuit with 401 in the test suite. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brings notify_push REST transport for realtime-updates. Conflicts resolved: - lib/Service/Object/PermissionHandler.php: kept HEAD's more precise docblock wording for $schema cache-key parameter (matches surrounding conditional-rules context). - l10n/en.json + l10n/nl.json: union-merged via JSON dict-merge. HEAD had 1882 translation keys + plurals + (nl) pluralForm; theirs had 1356. Result: 2114 unique keys, plurals+pluralForm preserved from HEAD. HEAD's i18n cleanup work (#1273) wins on conflicts. Line-based merge would have silently dup-keyed 4 entries (Anonymized, Entities detected, Risk level, Unknown error) per feedback_json-merge-revalidate.md — JSON-aware union avoids that.
Brings #1466 (chat-companion orchestrator, /api/chat/health route), the consolidated #1496 deps work, journeydoc tutorials, and the LLPhant Ollama think:false+keep_alive patch into the rollup so CnAiCompanion FAB renders when this branch is deployed. Conflicts resolved: - tests/Unit/Controller/AuditTrailControllerTest.php: took dev's version (more precise mock with ->with('admin') + useful comment). - tests/Unit/Service/ObjectServiceTest.php, ObjectServiceDeepTest.php, Object/PermissionHandlerCacheTest.php: took dev's versions wholesale via 'git checkout --theirs'. Rationale: dev has the most recent test fixes (commits 5504377, 7987a97, 8d17464) from the consolidation merge that recently passed PHPUnit on dev. HEAD's PHPCS-reformatted versions from #1273's quality-fix commit diverged structurally enough that the line-based merge produced duplicated class bodies. - package-lock.json: took dev's; will npm install separately if needed once we touch JS deps.
MWest2020
left a comment
There was a problem hiding this comment.
Code Review
Branch: feature/i18n-complete-translations → development | Head: 1d867eb | 63 files, +2540 / -2386
1. Overview
The PR has two intertwined goals:
- Primary (~95 % of LOC): rewrite
l10n/en.jsonandl10n/nl.jsonso English is the canonical key set per ADR-007, wrap remaining bare strings int()/l10n->t(), and complete the catalog (incl. plural forms). - Secondary (8 PHP files + 1 new Vue component): functional additions bundled in —
TasksController::allUserTasksquota clamp + docblock, route hardening forlinked_entity#reverseLookup, comment-only changes inMagicMapper, cross-tenant scoping notes inLinkedEntityService, extraction of an inlineNcModaltoSwitchOrganisationModal.vue, and constructor-signature updates inAuditTrailControllerTest.
The PR has gone through three review rounds with prior blockers (truncated keys at apostrophes, empty plurals block, identity-mapped nl.json losing 602 Dutch strings, IDOR claim on allUserTasks, inline-modal/ADR-004 violation) already resolved. Current state is CHANGES_REQUESTED with two PHPUnit jobs (PHP 8.3 / 8.4 on NC stable32) failing on the latest commit.
2. Blockers (must address before merge)
-
🔴 PHPUnit still red on HEAD —
quality / PHPUnit (PHP 8.3|8.4, NC stable32)are failing on1d867eb.AuditTrailControllerTest::setUpwas updated to wire two new mocks (IUserSession,IGroupManager) into a 7-arg constructor, but no other test was touched in this PR. If the underlying controller's constructor changed ondevelopmentand these test edits are stale-merge fallout, that needs to be confirmed by running the suite locally before merge — green CI is a precondition. -
🔴 Cross-tenant exposure shipped intentionally —
lib/Service/LinkedEntityService.php:281adds a WARNING comment acknowledging that schema metadata and matched object UUIDs/names are returned cross-tenant fromGET /api/linked/{type}/{entityId}because_rbac: false, _multitenancy: falseis passed toschemaMapper->findAll(). The comment is honest, but landing a known cross-tenant data leak in a release branch needs (a) a feature flag scoping it to the mail-sidebar caller, (b) a tracked follow-up issue (the "TODO #1273" reference points back to this PR which will be closed), and (c) at minimum a@psalm-suppressor admin-acknowledgement on the call site. Not a code-review fix — needs a product/security decision in the PR before merge.
3. Concerns
-
🟡
TasksController::allUserTasks—offsetis unbounded.lib/Controller/TasksController.php:106now clamps$limittomin(..., 200)(good), but$offsetis read as a raw(int)with no upper bound. A caller can pass_offset=10000000and forceTaskServiceto walk N calendars / skip N tasks before slicing — same DoS surface the limit clamp was added to close. Clamp$offsetsymmetrically (e.g.min($offset, 10_000)) or document why the service-layer pagination makes it safe. -
🟡 Scope creep / single-purpose PR principle violated. The PR title is
feat(i18n), but the changeset includesallUserTasksquota changes, route-pattern hardening, cross-tenant scoping notes, and an extracted modal component. Reviewer @MWest2020 flagged the same. For audit / rollback purposes, splitting the 8 non-i18n PHP files and the modal extraction into their own PR — even a same-day chain — keeps the i18n migration revertable without losing the routing fix. Worth confirming with the team before merge whether this PR shape is acceptable. -
🟡 Dynamic translation keys without catalog guarantees.
ApprovalStepList.vuecallst('openregister', step.status)andt('openregister', step.role);PermissionMatrix.vuecallst('openregister', action). Values come from the API — if the catalog is missingpending,read,write, etc., users see the raw token (Nextcloud'st()falls back to identity). Either pre-populate the catalog with the known enum values, or add a comment listing the contract values so future tooling can audit coverage. -
🟡 Numeric
t()wrappers remain at call sites. The"1000","3","30","5"keys are out of the catalog (good), but the call sites still read:placeholder="t('openregister', '1000')"(src/modals/agent/EditAgent.vue:84). The identity fallback hides this, but it's dead translation overhead and re-pollutes the catalog if anyone runs the extractor again. Replace with literal:placeholder="'1000'"in this PR rather than deferring. -
🟡 Backwards-compat route duplication unmanaged.
appinfo/routes.php:265keeps bothlinked_entity#reverseLookup(new) and a legacy underscore-prefixed variant for "older mail-sidebar clients." That's fine, but the inline comment is the only place the duplication is recorded — it'll rot. File a tracked deprecation issue with a target Nextcloud-app version, and add the path to the API-versioning doc if one exists. -
🟡
@NoAdminRequired @NoCSRFRequiredonallUserTasks. The docblock now justifies why the IDOR claim was not exploitable (session-binding inside the service). That reasoning relies onTaskService::getAllUserTasks()always anchoring toIUserSession::getUser()->getUID()— fine today, but the contract is one refactor away from a regression. Add a service-layer unit test that fails ifgetAllUserTasks()ever consults a caller-supplied user identifier, so the docblock claim is enforced.
4. Minor / nits
l10n/en.jsoncasing drift. Mixed Title Case and sentence case persists ("Active Organisation:"vs"Active filters:"); reviewer deferred this to a tooling pass — fine, but link the follow-up issue fromdocs/ADR-007so the convention is single-sourced.SwitchOrganisationModal.vueis clean. One small thing::nameon<NcModal>is used as the modal title — confirm the@nextcloud/vueversion in the project treats it as accessible-name (some versions silently drop unknown props). Worth a one-line@accessibilitycomment.MagicMapper.php:5862comment is useful, but "if a GIN index is added in a future migration" is the kind of note that vanishes — drop a follow-up issue reference inline (See #NNNN) so it surfaces ingrep.- PR contains a
chore(deps): bump webonyx/graphql-php v15.31.5 → v15.32.3commit. The bump resolves two GHSAs per the review thread — good — but it does not belong in a feature-i18n PR. For audit clarity, security bumps should land in their own PR.
5. Strengths
- Reviewer feedback loop is unusually thorough; the multi-round resolution log (28 review threads, all linked to fix commits) is a model for traceability.
- The catalog refresh fixed real bugs: 8 keys truncated at apostrophes, an empty
pluralsblock, and a regression that would have wiped 602 Dutch translations. None of these are user-visible yet because the PR caught them in review — but the cleanup is genuine. - The IDOR-class authorization analysis on
allUserTasks(docblock + reviewer audit atTaskService:104) is exactly the right artifact to produce for a security-sensitive endpoint, and it stays in the codebase. - The inline-modal extraction (ADR-004 compliance) was done correctly — scoped CSS, props/emits contract, no leftover state.
6. Recommendation
Request changes until:
- PHPUnit is green on HEAD (root-cause the failure — likely test-only, but unverified).
offsetclamp added inTasksController::allUserTasks.- Cross-tenant exposure in
LinkedEntityService::scanMagicTableseither gated behind a feature flag / mail-sidebar caller check, or explicitly signed off by a security owner with a tracked follow-up issue (not "#1273").
After those, the catalog work itself is solid. The deferred items (casing, sentence-fragment translations, numeric wrappers) are real but reasonable to handle in a follow-up tooling pass — provided that pass is actually scheduled.
|
@MWest2020 — merge conflict resolved and all 13 of your inline review threads addressed. Posted resolution replies on each thread; summary below. Merge conflict
Review concerns (all resolved unless noted)
AskIf you concur with the resolutions, would you mind closing the corresponding conversations so we can see what (if anything) still needs another pass? The two judgement-calls that aren't strict fixes are:
PR is |
- TasksController::allUserTasks: clamp $offset symmetrically with $limit (max 10_000) to close the DoS surface the limit clamp was added for. - LinkedEntityService::scanMagicTables: gate the RBAC + multitenancy bypass behind an app-config flag (openregister.linkedEntity.crossTenantScan, default '1'). Operators in strict-isolation deployments can set the flag to '0' to fall back to standard tenant-scoped findAll(). Follow-up tightening (per-row access check + caller scoping + audit log) tracked in issue #1534. - appinfo/routes.php: replace ad-hoc "deduplicate in a future cleanup" comment with reference to deprecation issue #1535. - MagicMapper.php: extend the GIN-index comment with issue #1534 reference so the trade-off note doesn't rot. - ApprovalStepList.vue / PermissionMatrix.vue: add dynamic-translation contract comments listing the enum values (status, role, action) so future tooling can audit catalog coverage. - package-lock.json: resync with package.json (CI npm ci was failing on stale entries left by the development merge). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@MWest2020 — addressed your 2026-05-18 re-review at commit Resolved
Not resolved — needs your call
Deferred (acknowledged, fold into follow-up)
PR is at |
Review at HEAD
|
- TasksController::allUserTasks: clamp $offset symmetrically with $limit (max 10_000) to close the DoS surface the limit clamp was added for. - LinkedEntityService::scanMagicTables: gate the RBAC + multitenancy bypass behind an app-config flag (openregister.linkedEntity.crossTenantScan, default '1'). Operators in strict-isolation deployments can set the flag to '0' to fall back to standard tenant-scoped findAll(). Follow-up tightening (per-row access check + caller scoping + audit log) tracked in issue #1534. - appinfo/routes.php: replace ad-hoc "deduplicate in a future cleanup" comment with reference to deprecation issue #1535. - MagicMapper.php: extend the GIN-index comment with issue #1534 reference so the trade-off note doesn't rot. - ApprovalStepList.vue / PermissionMatrix.vue: add dynamic-translation contract comments listing the enum values (status, role, action) so future tooling can audit catalog coverage. - package-lock.json: resync with package.json (CI npm ci was failing on stale entries left by the development merge). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0d9233e to
0579b89
Compare
|
Rebased on top of latest
Resolved 1 conflict:
Branch history collapsed from 3 to 1 commit: the two prior merge commits (which previously absorbed dev) are no longer needed after the rebase, leaving only the substantive |
- TasksController::allUserTasks: clamp $offset symmetrically with $limit (max 10_000) to close the DoS surface the limit clamp was added for. - LinkedEntityService::scanMagicTables: gate the RBAC + multitenancy bypass behind an app-config flag (openregister.linkedEntity.crossTenantScan, default '1'). Operators in strict-isolation deployments can set the flag to '0' to fall back to standard tenant-scoped findAll(). Follow-up tightening (per-row access check + caller scoping + audit log) tracked in issue #1534. - appinfo/routes.php: replace ad-hoc "deduplicate in a future cleanup" comment with reference to deprecation issue #1535. - MagicMapper.php: extend the GIN-index comment with issue #1534 reference so the trade-off note doesn't rot. - ApprovalStepList.vue / PermissionMatrix.vue: add dynamic-translation contract comments listing the enum values (status, role, action) so future tooling can audit catalog coverage. - package-lock.json: resync with package.json (CI npm ci was failing on stale entries left by the development merge). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0579b89 to
1e1ee49
Compare
- TasksController::allUserTasks: clamp $offset symmetrically with $limit (max 10_000) to close the DoS surface the limit clamp was added for. - LinkedEntityService::scanMagicTables: gate the RBAC + multitenancy bypass behind an app-config flag (openregister.linkedEntity.crossTenantScan, default '1'). Operators in strict-isolation deployments can set the flag to '0' to fall back to standard tenant-scoped findAll(). Follow-up tightening (per-row access check + caller scoping + audit log) tracked in issue #1534. - appinfo/routes.php: replace ad-hoc "deduplicate in a future cleanup" comment with reference to deprecation issue #1535. - MagicMapper.php: extend the GIN-index comment with issue #1534 reference so the trade-off note doesn't rot. - ApprovalStepList.vue / PermissionMatrix.vue: add dynamic-translation contract comments listing the enum values (status, role, action) so future tooling can audit catalog coverage. - package-lock.json: resync with package.json (CI npm ci was failing on stale entries left by the development merge). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4ad78b6 to
f74f452
Compare
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Re-review at HEAD f74f4520 — APPROVE
Volg-commits 9d070996 (review fix) + f74f4520 (phpcs auto-fix) lossen alle openstaande punten netjes op. De residuele diff (6 files / 53+/14−) na de development-rebase is alleen nog de hardening-laag — i18n payload is via development geland.
Verified
TasksController::allUserTasks—$offsetsymmetrisch geclampt metmin(max(.., 0), 10000), plusmax(.., 1)lower bound op$limit. Inline comment legt de DoS-surface uit. ✅LinkedEntityService::scanMagicTables— RBAC + multitenancy bypass nu achter app-config flagopenregister.linkedEntity.crossTenantScan(default'1'). Fallback gebruiktfindAll()met defaults. IAppConfig DI correct; follow-up tightening (per-row + audit log) tracked in #1534. ✅appinfo/routes.php/MagicMapper.php— comments verwijzen nu naar de tracking issues (#1535 / #1534) i.p.v. ad-hoc TODO's. ✅ApprovalStepList.vue/PermissionMatrix.vue— dynamic-translation contract comments documenteren de enum-waarden (status / role / action) zodat catalog-coverage te auditen blijft. ✅
Minor (niet-blocking, informational)
- 🟢 Default
crossTenantScan='1'houdt de cross-tenant schema-metadata exposure aan. De flag-vorm is goed; overweeg de default te flippen zodra #1534 (per-row check) is geland zodat strict-isolation de veiligere baseline wordt. - 🟢 De
(string)cast rondgetValueString(...)is redundant — die geeft al een string terug. Cosmetisch.
CI volledig groen; alle eerdere thread-vindings adressed. Goed werk op de follow-up.
Summary
t()/$this->l10n->t()translation callsl10n/en.json(identity-mapped) andl10n/nl.json(Dutch translations)<script setup>components missing directtimport from@nextcloud/l10nTest plan
🤖 Generated with Claude Code