Skip to content

feat(fr-bnf): French locale (fr_FR) + BNF SRU/UNIMARC integration#131

Closed
fabiodalez-dev wants to merge 31 commits into
feat/all-prs-mergedfrom
feat/fr-bnf-integration
Closed

feat(fr-bnf): French locale (fr_FR) + BNF SRU/UNIMARC integration#131
fabiodalez-dev wants to merge 31 commits into
feat/all-prs-mergedfrom
feat/fr-bnf-integration

Conversation

@fabiodalez-dev
Copy link
Copy Markdown
Owner

Summary

  • Locale fr_FR completa: locale/fr_FR.json (4080 chiavi), locale/routes_fr_FR.json (slug inglesi), installer/database/data_fr_FR.sql (generi, CMS, email in francese), radio tricolore nell'installer step0
  • Backfill in tutte le data_XX.sql e in migrate_0.7.4.sql (INSERT IGNORE) per installazioni esistenti
  • BNF SRU/UNIMARC: SruClient.php — metodo parseMarcxchangeXml() per campi UNIMARC (200 titolo, 214 editore/anno, 700/701/702 autori, 010 ISBN, 073 EAN, 215 pagine, 461 collana, 676 Dewey, 101 lingua, 330 abstract, 600-608 soggetti); namespace MARCXchange info:lc/xmlns/marcxchange-v2; flag quote_search_terms per quoting CQL BNF; fallback local-name() su numberOfRecords
  • Admin UI Z39: preset BNF (unimarcxchange, v1.2, quote_search_terms=true), campo version, checkbox quote, dropdown syntax con opzione unimarcxchange
  • Book form: hidden inputs scraped_numero_serie e scraped_dimensions
  • 71 test E2E riutilizzabili: install-french.spec.js (48 test in 6 fasi) + bnf-sru-features.spec.js (23 test in 4 fasi); PHPStan level 5: OK

Test Plan

  • install-french.spec.js — 44 passati, 4 saltati (installer skip corretto), 0 falliti
  • bnf-sru-features.spec.js — 23/23 passati
  • PHPStan level 5: nessun errore
  • Verifica BNF SRU reale: ISBN 2070360024 (Le Grand Meaulnes) restituisce record UNIMARC
  • Fresh install in francese: step0 mostra radio fr_FR con bandiera tricolore
  • Upgrade da versione precedente: migrate_0.7.4.sql inserisce fr_FR nella tabella languages

- Add fr_FR locale: locale/fr_FR.json (4080 keys), routes_fr_FR.json
- Add installer/database/data_fr_FR.sql with French genres, CMS, emails
- Backfill fr_FR in data_it_IT.sql, data_en_US.sql, data_de_DE.sql
- Backfill fr_FR in migrate_0.7.4.sql (existing installs upgrade path)
- Add French language option in installer step0.php (tricolor SVG)
- Register fr_FR in Installer.php, installer/index.php, config/default_texts.php
- SruClient.php: add parseMarcxchangeXml() for UNIMARC fields (200/214/700/701/702/010/073/215/461/676/101/330/600-608)
- SruClient.php: register mxc namespace (info:lc/xmlns/marcxchange-v2)
- SruClient.php: add quote_search_terms server flag for BNF CQL quoting
- SruClient.php: add local-name() fallback for numberOfRecords XPath
- plugins.php: add BNF preset (unimarcxchange, v1.2, quote_search_terms)
- plugins.php: add version and quote_search_terms fields to Z39 server rows
- book_form.php: add scraped_numero_serie and scraped_dimensions hidden inputs
- 71 E2E tests: install-french.spec.js (48) + bnf-sru-features.spec.js (23)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f56f9bb3-8cab-4114-b2be-b7ce59230293

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fr-bnf-integration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Code review fixes:
- SruClient: catch \Throwable (not \Exception) in retry loop — TypeError from strict_types would bypass retry
- SruClient: restore libxml_use_internal_errors state after XML parse; use \RuntimeException for parse errors
- plugins.php: BNF preset URL changed from http:// to https://
- data_fr_FR.sql: hero button_link changed from /catalogue to /catalog (matches routes_fr_FR.json)
- routes_fr_FR.json: add missing bibframe.book route (parity with it_IT/en_US)

New test file install-compat-standard.spec.js (59 tests, 8 phases):
- Phase 1: all seed files include all 4 supported languages
- Phase 2: fr_FR route keys are compatible with it_IT (count, presence, no duplicates)
- Phase 3: migrate_0.7.4.sql backfills fr_FR with correct column names
- Phase 4: installer code registers fr_FR in all required locations
- Phase 5: DB state after upgrade (fr_FR present, no duplicate defaults)
- Phase 6: BNF preset uses HTTPS
- Phase 7: SruClient \Throwable catch, libxml state, RuntimeException
- Phase 8: data_fr_FR button links match routes_fr_FR.json (no Italian/German slugs)
- Restore `novalidate` on #bookForm (removed in 2c0264e), which caused
  browser HTML5 validation to intercept submit before the SweetAlert2
  handler could fire — breaking book create/edit confirmation dialog
- Fix smoke-install.spec.js search parameter: the /api/libri endpoint
  uses `search_text` not `search[value]` (DataTables format), so the
  wrong parameter silently returned the first 5 alphabetical books
  instead of the searched title, making createdBookId wrong and
  the edit-title verification fail
- fr_FR.json: +65 translated strings for LibraryThing plugin sections
  (Recensione e Valutazione, Visibilità nel Frontend, Condizione Fisica, etc.)
- en_US.json, de_DE.json: add missing "Valore Corrente" translation
- book_form.php: wrap LibraryThing field labels with __() so they
  respect the active locale (previously always rendered in Italian)
- full-test.spec.js: Phase 21 (4 serial tests) verifies fr/en/de/it
  language switching including LibraryThing French strings assertion
- Minor fixes: archives plugin.json trailing comma, CI workflow tweaks,
  phpstan baseline cleanup, test file updates
Upgrades from 0.7.4 → 0.7.5 now execute migrate_0.7.5.sql which
inserts the fr_FR language row (INSERT IGNORE, idempotent).
0.7.4 installs that already have the row will see a no-op.
…activate fr_FR

INSERT IGNORE left pre-existing fr_FR rows with is_active=0 untouched.
ON DUPLICATE KEY UPDATE now forces is_active=1 on upgrade, so the language
bootstrap (step 3: WHERE is_default=1 AND is_active=1) can find the French
locale after the admin sets it as default.
setDefault() now sets is_active=1 alongside is_default=1, preventing the
inconsistent state where a language is marked default but inactive.
I18n::bootstrap() step 3 requires is_active=1, so an inactive default
language was silently ignored and the app stayed in the previous locale.

Also adds missing bibframe.book route key to routes_de_DE.json.
… dev schema detected

Pre-release dev builds used source_code/source_id column names. The current
migration expects source/authority_id. CREATE TABLE IF NOT EXISTS skipped the
DDL on installations that had the dev table, causing ADD KEY idx_authority
(source, authority_id) to fail with 'Key column source does not exist'.

Add a guard that detects the old schema via source_code column presence and
drops the empty dev table before recreating with the correct schema.
…sue #130)

On Windows, RecursiveDirectoryIterator::getPathname() returns backslash paths,
breaking str_replace prefix-stripping and mkdir target construction in
copyDirectoryRecursive() and copyDirectory(). All paths now normalized to
forward slashes at function entry; realpath() returns also normalized.

Also remove unused SEED_FILES variable from install-compat-standard.spec.js
(CodeQL js/unused-local-variable #20) and harden test selectors.
…ition

archives-upload-assets.spec.js ensureUploadFixtures():
- Remove existsSync+writeFileSync TOCTOU pattern (race condition fix)
- Replace hardcoded /tmp/ paths with path.join(os.tmpdir(), ...) for
  cross-platform safety and to satisfy CodeQL js/insecure-tmp rule
__() uses Italian as the native fallback — keys ARE Italian text, so
if a key is missing from it_IT.json the function returns the key itself.
it_IT.json is intentionally sparse (overrides only).

Only en_US ↔ de_DE requires exact key-set parity.
CodeQL js/insecure-temporary-file:
  - Add binary fixtures to tests/fixtures/ (jpg/pdf/mp3, csv/tsv) and
    allow the directory in .gitignore (mirrors tests/seeds/ pattern)
  - archives-upload-assets.spec.js reads from tests/fixtures/ instead of
    writing predictable filenames to os.tmpdir()

OAI-PMH:
  - Catch remaining \Throwable in record serialisation loop to skip
    malformed metadata with a warning log instead of crashing

E2E test stability:
  - bibframe-persistent-uri: check @id contains /id/work/{id}, not suffix /work
  - book-form-comprehensive: simplified Choices.js check (no XPath)
  - bulk-enrich: nullify ISBN before DELETE to avoid UK constraint on re-run
  - multisource-scraping: accept 'Z39.50/SRU' as valid source for Nevermind
  - smoke-install: make author input fill conditional (visible guard)
Fix groups (committed):
- [FG-1] F002, F007 — storage/plugins/archives/ArchivesPlugin.php: verified
- [FG-2] F003 — storage/plugins/bibframe-linked-data/BibframeLinkedDataPlugin.php: verified
- [FG-3] F005 — storage/plugins/oai-pmh-server/OaiPmhServerPlugin.php: verified
- [FG-4] F010 — app/Controllers/FrontendController.php: verified
- [FG-5] F020 — storage/plugins/oai-pmh-server/views/book-digital-assets.php: verified
- [FG-6] F023 — storage/plugins/archives/views/authorities/show.php: verified
- [FG-7] F027 — app/Views/admin/plugins.php: verified
- [FG-8] F028 — storage/plugins/archives/views/show.php: verified

Post-fix review: 8/8 groups verified complete; 0 group(s) partial; 0 group(s) reverted.
Fix groups (committed):
- [FG-1] F003 — storage/plugins/z39-server/classes/SRUServer.php: verified
- [FG-2] F018 — storage/plugins/z39-server/classes/SruClient.php: verified
- [FG-3] F019 — storage/plugins/openurl-resolver/OpenUrlResolverPlugin.php: verified
- [FG-4] F020 — storage/plugins/resource-sync/ResourceSyncPlugin.php: verified
- [FG-5] F023 — app/Controllers/ScrapeController.php: verified
- [FG-6] F024, F025, F045 — storage/plugins/ncip-server/NcipServerPlugin.php: verified
- [FG-7] F027 — storage/plugins/viaf-authority/ViafAuthorityPlugin.php: verified
- [FG-8] F028 — storage/plugins/archives/ArchivesPlugin.php: verified
- [FG-9] F029 — storage/plugins/oai-pmh-server/OaiPmhServerPlugin.php: verified

Post-fix review: 9/9 groups verified complete; 0 group(s) partial; 0 group(s) reverted.
Fix groups (committed):
- [FG-1] F007, F041, F012 — storage/plugins/oai-pmh-server/OaiPmhServerPlugin.php: verified
- [FG-2] F031 — app/Views/frontend/layout.php, locale/en_US.json, locale/de_DE.json, locale/fr_FR.json, locale/it_IT.json: verified
- [FG-3] F034 — app/Views/admin/plugins.php: verified

Post-fix review: 3/3 groups verified complete; 0 group(s) partial; 0 group(s) reverted.
Reconciled fix (one merge pass after Phase 9.pre overlap):
  Findings: F008, F018, F029, F033, F040
  Files:    app/Controllers/FrontendController.php, app/Support/PluginManager.php, app/Support/Updater.php, app/Views/frontend/book-detail.php, app/Views/frontend/layout.php, storage/plugins/oai-pmh-server/OaiPmhServerPlugin.php
  Overlaps: app/Views/frontend/layout.php <- FG-1, FG-3

Findings addressed:
- F008 (PluginManager::isActive cache): replace uncached `SELECT 1 FROM plugins`
  in FrontendController::bookDetail() and frontend/layout.php with per-process
  cached PluginManager::isActive() lookup. Eliminates an extra DB round-trip on
  every catalog page render (including anonymous crawls).
- F018 (CWE-22 prefix-without-separator): tighten path-traversal guard in
  Updater.php — use str_starts_with($parentTarget, $realDest.'/') to prevent
  '/var/www/dest2' from passing when $realDest='/var/www/dest'.
- F029 (WCAG 2.1 SC 1.1.1): add aria-hidden="true" to fa-archive icon in
  search-dropdown builder (frontend/layout.php JS).
- F033 (WCAG 2.1 SC 1.1.1): add aria-hidden="true" to media-type icon in
  book-detail view (label is announced separately).
- F040 (RFC 7235): split requireAdminForDownload() OAI-PMH auth flow —
  401 + WWW-Authenticate when no credentials supplied, 403 when credentials
  present-but-invalid.

Post-fix review: 1/1 group verified complete; 0 partial; 0 reverted.
PR #132 polish bundle:

- book-detail.php: htmlspecialchars() around ucfirst($author['ruolo']) — was the
  only raw role output in the author span (F048).
- tests/pr132-fix-regressions.spec.js: new E2E regression suite covering
  F003 (SRU errorResponse namespace), F018 (UNIMARC 461 $t/$v), F019 (OpenURL
  locale-aware book_url), F020 (ResourceSync 30-day deleted window), F024/F025
  (NCIP UPDATE libri AND deleted_at IS NULL), F027 (ViafAuthority ensureSchema
  return shape), F029 (OAI-PMH ListMetadataFormats includes unimarc), F045
  (NCIP admin endpoints reachable by staff), F048 (book-detail XSS escape).
- tests/discogs-import.spec.js: mock Discogs scrape endpoint for deterministic
  E2E so the suite no longer depends on live Discogs API.
- tests/series-collane-crud.spec.js: mysql invocation now supports TCP host
  alongside socket connection and pipes the migration via stdin.
- tests/bulk-enrich.spec.js: raise per-test timeout to 240s and surface
  Gateway-Timeout retries as explicit errors.
- locale/fr_FR.json: 849 additional Italian→French translations (admin UI,
  archives, NCIP, OAI-PMH, plugin metadata) — closes the FR locale gap left
  by feat/fr-bnf-integration.
- storage/plugins/archives/views/index.php: responsive toolbar (hidden on
  mobile, accessible "more" dropdown), aria-hidden on decorative SVG icons.
15 fix-group Opus agents applied surgical fixes to 20 findings from
adamsreview rev_01KRDSE2PB0P2TGEY78GT38J4F. All edits PHPStan level 5
clean; compat with existing installs preserved per user directive.

Bug fixes:
- F003  ScrapeController::byIsbn(): conditional session_write_close
  via request attribute marker (HTTP entry only; in-process callers
  keep session lock so $_SESSION writes persist).
- F004  SearchController public search: cap core results at 12 before
  Hooks::apply('search.unified.sources') so plugin sources (archives)
  always get headroom in the 15-slot dropdown.
- F010  Updater::copyDirectory() (sibling of F018 hotspot): apply the
  same prefix-collision guard (!== $realDest + str_starts_with
  $realDest . '/').
- F027  ApiBookScraperPlugin merge: restore '|| === null' branch so
  null-valued existing fields are overwritten by API data, matching
  the "fill empty fields" inline comment.
- F067  OaiPmhServerPlugin bind_param: fix type string 'issssiii' ->
  'isssiiii' for digital_assets insert ($bookId,$url,$filetype,$md5,
  $filesize,$width,$height,$ppi).
- F072  OpenLibraryPlugin merge: same '|| === null' restoration as
  F027 (via array_key_exists to keep PHPStan happy).
- F081  ViafAuthorityPlugin setAuthorIsniAction: preserve existing
  VIAF authority assignment when setting/clearing ISNI; only stomp
  authority_source when no prior VIAF exists.
- F083  Z39ServerPlugin fallback merge: same '|| === null' restoration.
- F084  Z39ServerPlugin decryptSetting: match PluginManager order
  ($_ENV first then getenv) so existing encrypted settings remain
  decryptable under phpdotenv createImmutable (which populates $_ENV
  but not getenv).

Archives plugin (5 findings, FG-10):
- F028  migrateImageColumns docblock updated to current behavior.
- F029  add fr_FR to public archive routes loop ([it_IT,en_US,de_DE,
  fr_FR]).
- F031  handleAssetUpload data-loss: defer legacy document_path
  unlink until AFTER the archival_unit_files INSERT succeeds.
- F032  remove htmlspecialchars from 10 Location HTTP headers
  (HTTP, not HTML — entity encoding corrupts URLs with reserved chars).
- F034  archiveSearchPattern: use mb_strlen/mb_substr so UTF-8 input
  (citta, editeur, Ubung) is not truncated mid-sequence.

i18n / install:
- F020  data_it_IT.sql + data_en_US.sql: add ON DUPLICATE KEY UPDATE
  to languages INSERT (mirrors de_DE/fr_FR pattern; idempotent
  re-install over existing DB).
- F023  data_*.sql key counts realigned to actual JSON file contents
  (it_IT=493, en_US=4653, de_DE=4653, fr_FR=4993; replaces stale
  2015/4080/4145 values that disagreed across migrations and JSON).
- F025  routes_fr_FR.json: translate all 33 routes to French
  (/connexion, /catalogue, /livre, /auteur, /editeur, ...) mirroring
  de_DE convention with accent-free URLs for SEO.

Plugin registry repair:
- F021  Create migrate_0.7.6.sql to register oai-pmh-server +
  viaf-authority (omitted from migrate_0.7.4.sql despite being in
  BundledPlugins::LIST). Bumps version.json to 0.7.6 so the migration
  fires. Historic migrate_0.7.4.sql untouched per compat directive.

Linked data:
- F045  BibframeLinkedDataPlugin: previously-dead $viafId now emits
  Work-level owl:sameAs VIAF URI, mirroring the per-author pattern.

Release tooling:
- F026  scripts/create-release.sh: restore executable bit
  (mode 100644 -> 100755) via git update-index --chmod=+x.

Compat-preservation strategy applied per user directive:
- Historic migration files (migrate_0.7.4.sql, migrate_0.7.5.sql)
  NOT edited; new migrate_0.7.6.sql handles missing plugin registry.
- Seeder files updated only for fresh-install correctness; existing
  installs at 0.7.5 unaffected.
- ScrapeController fix uses request-attribute opt-in so HTTP behavior
  is unchanged.

Post-fix review: 20/20 verified by fix-group PHPStan checks; no
regression triggered; 0 reverted.

Review artifact: rev_01KRDSE2PB0P2TGEY78GT38J4F
17 fix-group Opus agents applied surgical fixes to 30 findings from
adamsreview rev_01KRDSQ3MA7PCZDZJHCY9ZB7JF (medium-confidence batch).
All edits PHPStan level 5 clean; backward-compat preserved per user
directive — historic migrations untouched, opt-in toggles, default
behavior preserved on existing installs.

== Security (8 fixes) ==

- F007  HtmlHelper getBaseUrl: optional APP_TRUSTED_HOSTS whitelist
  (env var, comma-separated). When set, validates HTTP_HOST and
  X-Forwarded-Host (proxy-gated). When unset, behavior unchanged.
- F030  Archives: already covered by prior F032 (no-op verified).
- F033  ArchivesPlugin findLeafTitle: AND deleted_at IS NULL guard.
- F048  NcipServerPlugin: MAX_REQUEST_BYTES dropped 1 MiB to 256 KiB
  (unauth /ncip endpoint; real NCIP messages <10 KB); unset($body) +
  unset($xml) to free SimpleXML DOM early.
- F058  OAI-PMH: honest deletedRecord advertising — Identify now
  returns 'no' (instead of false 'persistent') when trigger install
  failed (shared hosting without TRIGGER privilege).
- F059  OAI baseURL: oaiBaseUrl() helper logs one-shot warning when
  APP_CANONICAL_URL unset (OAI ids are Host-header-spoofable).
- F077  ResourceSync: opt-in `require_basic_auth` plugin setting
  (default '0' = spec-compliant public). When '1', endpoints require
  admin/staff Basic Auth (RFC 7235 401+WWW-Authenticate / 403).
- F078  ResourceSync /changelist?from=: tombstones capped at 90-day
  window (was unbounded); live rows now include deleted_at IS NULL.
- F079  ViafAuthority curl: add CURLOPT_REDIR_PROTOCOLS to prevent
  redirect-to-file:// SSRF on older libcurl.
- F082  ViafAuthority validateCsrf: remove Basic Auth bypass (BREAKING
  for API clients — they must now supply csrf_token in body or
  X-CSRF-Token header for write endpoints).

== Correctness (17 fixes) ==

- F006  HtmlHelper isRemoteAddrTrustedProxy: IPv6 CIDR support added
  (was silently skipping IPv6 entries).
- F008  QueryCache stampede: at flock timeout now performs 5 LOCK_NB
  retries before unprotected fallthrough; warns on bypass.
- F012  book-detail.php: skip empty sameAs to avoid noisy
  "sameAs": [] in JSON-LD.
- F015  catalog.php: archive URL regex broadened to allow query
  strings, fragments, percent-encoded chars while still rejecting
  javascript:/data: and CRLF injection.
- F018  bin/pinakes viaf-reconcile-all: keyset-paginated chunks of
  500 rows (was loading entire SELECT into memory; OOM on 100k+
  catalogs).
- F022  Append to migrate_0.7.6.sql: ON DUPLICATE KEY UPDATE forces
  fr_FR is_active=1 for installs that only ran migrate_0.7.4.sql
  (which used INSERT IGNORE). Historic 0.7.4 untouched.
- F042  archives/show.php Schema.org: image and associatedMedia
  .contentUrl now wrap via absoluteUrl() (matches isPartOf.url
  pattern; Schema.org consumers require absolute URIs).
- F044  Bibframe wantsMachineReadable: removed application/json from
  accept matcher (was returning Content-Type application/ld+json for
  Accept: application/json — strict negotiators reject).
- F047  NcipServerPlugin: hardcoded url('/admin/login') replaced with
  url(RouteTranslator::route('login')) so redirect honors locale-
  translated login path (/accedi IT, /login EN, /anmelden DE).
- F052  NcipServerPlugin closeLoan: replaced brittle affected_rows!==1
  rollback with idempotent retry: SELECT current stato when
  affected_rows=0; treat 'restituito' as no-op (NCIP partners can
  safely retry duplicate CheckIn).
- F057  OAI-PMH onUninstall: DROP TRIGGER IF EXISTS for both triggers
  (was empty {}). Tables kept for data preservation.
- F060  OAI-PMH oaiListSets archives: requires BOTH plugin active AND
  table exists (was table-only); set correctly disappears when
  Archives is deactivated.
- F063  OAI-PMH resumption tokens: expires_at via MySQL DATE_ADD(
  NOW(), INTERVAL) instead of PHP date() — fixes TZ mismatch causing
  spurious badResumptionToken (shared hosting Europe/Rome vs UTC).
- F065  OAI-PMH bookToIso2709 leader: reconciled UNIMARC leader
  between MARCXML and binary serializations ('nam a22...' + ' u 4500',
  IFLA 2008 compliant).
- F068  oai book-digital-assets.php: wrap oaiFormatBytes() in
  function_exists() guard (prevents fatal on multi-include via hook).
- F080  ViafAuthority reconcileSearch: switch LIKE ESCAPE clause from
  backslash to '!' — unambiguous across all sql_mode settings (was
  fragile under NO_BACKSLASH_ESCAPES).
- F086  SRUServer: diagnostic <uri>/<details>/<message> now in NS_DIAG
  namespace (info:srw/diagnostic/1/) per SRU spec.
- F088  SruClient rate limiter: 3-tier (APCu preferred, file-lock
  fallback, in-process degraded) — was per-PHP-process only (multi-
  worker box got N× expected rate to BNF/SUDOC).
- F091  SruClient parseMarcxchangeXml: fallback xpath now descends
  into recordData first (was matching outer SRU envelope).
- F092  UNIMARCXMLFormatter: switch to MARCXchange namespace (info:lc
  /xmlns/marcxchange-v2). BREAKING for harvesters dispatching on
  MARC21 namespace — UNIMARC field codes 200/210/700 belong in
  MARCXchange, not MARC21 slim. SruClient already routes UNIMARC
  through parseMarcxchangeXml so client-side was already correct.

Compat-preservation strategy applied per user directive:
- Historic migrations (migrate_0.7.4.sql, migrate_0.7.5.sql) NOT
  edited; F022 extends migrate_0.7.6.sql.
- HtmlHelper Host-trust gated behind APP_TRUSTED_HOSTS env (unset =
  pre-fix behavior preserved).
- ResourceSync Basic Auth opt-in (default off = spec-compliant).
- OAI deletedRecord soft-fallback (advertise 'no' instead of failing
  activation when triggers can't be installed).
- ViafAuthority Basic Auth CSRF removal documented as breaking.
- UNIMARCXMLFormatter namespace change documented as breaking.

Post-fix review: 30/30 verified by fix-group PHPStan checks; 0
regression; 0 reverted.

Review artifact: rev_01KRDSQ3MA7PCZDZJHCY9ZB7JF
…irmation)

20 finding promossi via /adamsreview:walkthrough batch (1 Opus briefer per
tutti, 16 fix-group Opus paralleli). Tutti option A (compat-preserving).

== Architecture (2) ==

- F001  FrontendController catalogAPI(): rimuovi dead $archiveResults lookup
  + Hooks::apply ('frontend.catalog.archive_results') + 'archives' chiave
  dalla JSON response. catalog() (full-page) preserva il pannello archives
  invariato. Nessun JS consumer leggeva response.archives.
- F019  Installer.php installPluginsFromZip(): aggiunto comment block
  documentando perche solo 5 plugin storici sono auto-attivati al fresh
  install; gli altri 11 (BundledPlugins::LIST) sono registrati piu tardi
  da autoRegisterBundledPlugins() e restano deactivated. Allow-list non
  espansa per conservare opt-in policy.

== Policy / Doc drift (2) ==

- F046  NcipServerPlugin docblock: aggiunti RequestItem e CancelRequestItem
  alla lista 'Supported messages' (match() handler gia li gestiva; capability
  XML gia li advertise).
- F076  openurl-resolver/wrapper.php: corretto comment classname.
  PluginManager::getPluginClassName('openurl-resolver') restituisce
  'OpenurlResolverPlugin' (single capital, per explode('-')+ucfirst), non
  'OpenUrlResolverPlugin'. Classe non rinominata (PHP case-insensitive).

== UX accessibility (3) ==

- F017  user_layout.php: aria-hidden=true sull'icona fa-archive nel search
  dropdown (line 1216) + $searchViewAllLabel via json_encode(__('Vedi tutti
  i risultati')) per i18n. Mirror del fix gia applicato a frontend/layout.
- F040  archives/views/public/index.php: clear-filters button con aria-label
  + 44x44 min touch target + fa-times icon visibile.
- F069  oai-pmh-server/views/book-digital-assets.php: aria-label sui delete
  icon button (static + JS-appended).

== UX i18n & plurals (3) ==

- F024  en_US.json + de_DE.json: aggiunte 8 key mancanti (archive form
  section headings + plugin uninstall aria-label).
- F039  archives/views/index.php: __n('%d risultato', '%d risultati', N)
  per pluralizzazione corretta + $levelLabel mapping (fonds/series/file
  /item → Fondo/Serie/Fascicolo/Unita) invece del raw enum.
- F041  archives/views/public/index.php: stessa pluralizzazione + branch
  espliciti 'dal'/'fino al' al posto dell'ellipsis '...' nei date range.

== UX behavior (9) ==

- F005  Language::setDefault(): docblock espanso per documentare la side
  effect (inactive→active). Controller (LanguagesController::setDefault)
  ora rileva is_active=0 prima del setDefault e emette flash_info via
  __('Language %s has been activated automatically'). Aggiunto blocco
  flash_info nel template languages/index.
- F014  catalog.php: archive panel suggestion ('Trovato anche
  nell'archivio') ora rendered solo quando empty($books); prima si
  mostrava sempre con archiveResults non-empty.
- F035  archives/views/authorities: list view salva
  window.location.href su sessionStorage; show.php usa quello come
  fallback intelligente quando document.referrer e empty (apertura
  in nuova tab da search). Pattern try/catch per private-mode/disabled
  storage.
- F037  archives/views/form.php: placeholder version_note semplificato
  (rimosso ternary ridondante) + esempio "inventario 2024"→"inventario
  2026". Tradotto in tutte le 4 locale.
- F038  archives/views/index.php: inline <style> block estratto in
  storage/plugins/archives/assets/css/archives-admin.css (rispetta la
  Slim asset route convention; #arc-actions-details[open] .arc-chevron
  + summary::-webkit-details-marker).
- F043  archives/views/public/show.php: filename fallback sanitizzato
  via preg_replace per evitare path leak; file size renderizzato accanto
  al MIME via inline closure bytesStr (legge $uf['file_size'] o fallback
  a @filesize() guarded).
- F054  ncip-server/views/partners.php: native confirm() sostituito con
  Swal.fire mostrando nome/ISIL/endpoint del partner + warning su
  transazioni orfane. Fallback a window.confirm se Swal non disponibile.
  i18n via PHP-rendered json_encode().
- F055  ncip-server/views/transactions.php: $statusLabel map per
  localizzare le badge ok/error/pending (era raw protocol string in
  tutte le locale).
- F070  oai-pmh-server/views/book-digital-assets.php: client-side
  validation con inline errors + aria-invalid prima del fetch (URL
  via URL(), MD5 regex /^[0-9a-fA-F]{32}$/, filesize/width/height/ppi
  positive int). setFieldError() + clearFieldErrors() helpers.
- F071  oai-pmh-server/views/book-digital-assets.php: native confirm()/
  alert() sostituiti con Swal.fire (toast success + error dialogs);
  appendRow() distingue URL accettato vs respinto (rejected: aria-label
  warning, no target=_blank, classe text-red-500).

Compat-preservation preserved per direttiva utente:
- Nessun edit a migration storiche (gia da prima)
- SQL behavior in Language::setDefault() invariato; docblock + flash
  message sono additive
- Tutti i fix opt-in o additivi (icon labels, plurals, validation,
  alert UX); nessun breaking change

Post-fix review: 20/20 verified via PHPStan/php -l/json_lint dei fix-group.
Total ~1.1M tokens (16 Opus fix-group agents in parallelo, 1 batch briefer).

Review artifact: rev_01KRDSQ3MA7PCZDZJHCY9ZB7JF
F075: localBookUrl() built origin from $request->getUri()->getHost(),
bypassing the APP_TRUSTED_HOSTS guard added to HtmlHelper::getBaseUrl().
A spoofed Host header could produce a 302 redirect to an attacker-
controlled domain. Use absoluteUrl() which routes through the trusted-
host validation.

F089/F090: quote_search_terms wrapped $term in CQL double-quotes without
escaping embedded " or \. A search term containing " would produce
malformed CQL sent to external SRU servers (BNF, SUDOC). Backslash-
escape both chars per the CQL specification.
@fabiodalez-dev
Copy link
Copy Markdown
Owner Author

Duplicato di #132 (PR completa per v0.7.5 → main). Chiuso.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant