Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8a2742e
Search: quoted terms match the exact spelling (#501)
fabiodalez-dev Oct 26, 2026
c83eabd
Fix exact searches across catalogue and admin surfaces
fabiodalez-dev Oct 26, 2026
89dfe6b
Close the CodeRabbit findings on quoted search
fabiodalez-dev Oct 26, 2026
2ac5775
Cap the words a mixed filter box turns into conditions
fabiodalez-dev Oct 26, 2026
0f92b17
Archives unit test: check the unified search invariant, not a type st…
fabiodalez-dev Oct 27, 2026
976872e
A name typed in quotes is a separate author or publisher
fabiodalez-dev Oct 27, 2026
12f0575
Archives relation typeahead: quoted terms match the exact spelling
fabiodalez-dev Oct 27, 2026
1ded997
Show the quote hint under the admin book, author and publisher filters
fabiodalez-dev Oct 27, 2026
c13ac84
Tell archive searchers that quotes mean the exact spelling
fabiodalez-dev Oct 27, 2026
5ce2900
E2E: quote hint is visible and quoted search works on every main box
fabiodalez-dev Oct 27, 2026
7a189c8
CI: pin runners to ubuntu-24.04 and lift tinymce past GHSA-mf2p-h6hf-…
fabiodalez-dev Oct 27, 2026
d32338a
Mixed quoted filters keep each word's rule everywhere
fabiodalez-dev Oct 28, 2026
96699fa
Serve TinyMCE 8.10.0 from the committed copy
fabiodalez-dev Oct 28, 2026
7612f49
Series picker and pre-migration search follow the per-word quote rule
fabiodalez-dev Oct 28, 2026
2ba1f62
Keep the quotes around a normalised author name, not inside it
fabiodalez-dev Oct 28, 2026
070058f
Series suggestions: every word must be at least two characters
fabiodalez-dev Oct 28, 2026
f8d069b
Allowlist the 28 historic gitleaks findings that are examples
fabiodalez-dev Oct 28, 2026
db2a406
Series minimum only per word with a quoted word; quoted author exact …
fabiodalez-dev Oct 28, 2026
689e363
Quotes: inch marks stay literal, renamed and contributor names drop w…
fabiodalez-dev Oct 29, 2026
5b311c2
Keep the upstream TinyMCE changelog out of the copy and the release ZIP
fabiodalez-dev Oct 29, 2026
9476127
Contributor cache columns drop whole-name quotes on every write path
fabiodalez-dev Oct 30, 2026
60214c8
Stored contributor lists split only on real separators
fabiodalez-dev Oct 30, 2026
bc58703
Author edits keep one name; a single cached name is decoded like a li…
fabiodalez-dev Oct 30, 2026
0af84c4
Quoted fallback: an underscore does not start a word
fabiodalez-dev Oct 31, 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
2 changes: 1 addition & 1 deletion .github/workflows/ci-browser-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ concurrency:
jobs:
browser-security:
name: Packaged app · 3 browsers · axe-core · OWASP ZAP
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 75
permissions:
contents: read # Build and scan the checked-out application.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-database-compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ concurrency:
jobs:
database:
name: ${{ matrix.database.name }} schema and behavior
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 35
permissions:
contents: read # Read application, schema and test sources.
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci-deep-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ concurrency:
jobs:
regression:
name: Browser regression shard ${{ matrix.shard }}/4
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 120
strategy:
fail-fast: false
Expand Down Expand Up @@ -250,7 +250,7 @@ jobs:

installer-locales:
name: Fresh installer (${{ matrix.locale }})
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
fail-fast: false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ concurrency:
jobs:
e2e:
name: Full E2E suite (Playwright · Apache · MySQL)
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 60

services:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ concurrency:
jobs:
quality:
name: Static quality checks
runs-on: ubuntu-latest
runs-on: ubuntu-24.04

# MySQL for the DB-backed unit tests (migration-*, book-field-types,
# loan-edge-cases, session-fixes). Without it those tests can't read a DB
Expand Down Expand Up @@ -376,7 +376,7 @@ jobs:

php-compatibility:
name: PHP ${{ matrix.php }} compatibility
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
Expand All @@ -403,7 +403,7 @@ jobs:

frontend:
name: Frontend audit, lint and reproducible build
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-real-upgrade.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ concurrency:
jobs:
emeroteca-412:
name: Emeroteca 412 — previous release upgraded through the admin UI
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 45

services:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci-security-supply-chain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ concurrency:
jobs:
workflow-security:
name: Workflow, YAML and shell security
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read # Read workflow and script sources.
Expand Down Expand Up @@ -58,7 +58,7 @@ jobs:
dependency-review:
name: Dependency diff policy
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read # Compare dependency manifests in the pull request.
Expand All @@ -75,7 +75,7 @@ jobs:

repository-security:
name: Secrets, vulnerabilities and misconfigurations
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 25
permissions:
contents: read # Scan the repository and complete Git history.
Expand Down Expand Up @@ -108,7 +108,7 @@ jobs:

release-artifact:
name: Reproducible release, SBOM and archive audit
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 35
permissions:
contents: read # Build the release from checked-out sources.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-upgrade-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ concurrency:
jobs:
upgrade-smoke:
name: Upgrade from latest release → verify schema
runs-on: ubuntu-latest
runs-on: ubuntu-24.04

services:
mysql:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ concurrency:
jobs:
analyze:
name: Analyze (javascript-typescript)
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
security-events: write # Upload CodeQL SARIF results.
actions: read # Read workflow metadata for private-repository analysis.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pullfrog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ permissions:
jobs:
pullfrog:
name: Pullfrog agent
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 45 # Two bounded attempts plus checkout and cleanup.
env:
# Whether a second subscription token is configured. Only the presence is
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ concurrency:
jobs:
build:
name: Verify and build release (read-only)
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
contents: read # Checkout and inspect the tagged source; never publish from this job.
Expand Down Expand Up @@ -152,7 +152,7 @@ jobs:
publish:
name: Attest and publish verified release
needs: build
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write # Create the GitHub Release and upload its assets.
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test-migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
# version is silently ignored by the updater (version_compare guard).
verify-versions:
name: Verify migration versions ≤ release
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
# assertion is the real pass/fail gate.
test-full-chain:
name: Full migration chain (schema → all migrations)
runs-on: ubuntu-latest
runs-on: ubuntu-24.04

services:
mysql:
Expand Down Expand Up @@ -196,7 +196,7 @@ jobs:
# ─── Job 3: existing single-migration test (upgrade 0.3.x → 0.4.0) ─────────
test-migrations:
name: Legacy migration regression (0.3.x to 0.4.0)
runs-on: ubuntu-latest
runs-on: ubuntu-24.04

services:
mysql:
Expand Down
39 changes: 39 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Gitleaks fingerprints (commit:path:rule:line) for findings in the full-history
# scan that are documentation examples or published test vectors, not secrets.
# The pull_request and push runs only scan the changed range; the scheduled and
# manual runs scan every commit and were failing on these 28 entries.
#
# api-book-scraper docs and views: placeholder keys in curl examples
b868a6b5d2180172b101171ac9eb98ae3e65ce5f:storage/plugins/api-book-scraper/README.md:stripe-access-token:43
b868a6b5d2180172b101171ac9eb98ae3e65ce5f:storage/plugins/api-book-scraper/SERVER_IMPLEMENTATION_GUIDE.md:stripe-access-token:89
b868a6b5d2180172b101171ac9eb98ae3e65ce5f:storage/plugins/api-book-scraper/SERVER_IMPLEMENTATION_GUIDE.md:stripe-access-token:113
b868a6b5d2180172b101171ac9eb98ae3e65ce5f:storage/plugins/api-book-scraper/SERVER_IMPLEMENTATION_GUIDE.md:curl-auth-header:1034
b868a6b5d2180172b101171ac9eb98ae3e65ce5f:storage/plugins/api-book-scraper/SERVER_IMPLEMENTATION_GUIDE.md:curl-auth-header:1039
b868a6b5d2180172b101171ac9eb98ae3e65ce5f:storage/plugins/api-book-scraper/SERVER_IMPLEMENTATION_GUIDE.md:curl-auth-header:1043
b868a6b5d2180172b101171ac9eb98ae3e65ce5f:app/Views/admin/plugins.php:stripe-access-token:426
84f14515c448bbadf6ee55cec463ba858a32faa4:app/Views/admin/plugins.php:stripe-access-token:474
c9453c46fe2225531790a2edc90c25b1cb667f05:app/Views/admin/plugins.php:stripe-access-token:474
# Settings page and admin docs: "your-api-key-here" style curl examples
16ba645f1f990600f69d3842ce3bcd319fbe15f0:app/Views/settings/advanced-tab.php:curl-auth-header:607
b868a6b5d2180172b101171ac9eb98ae3e65ce5f:app/Views/settings/advanced-tab.php:curl-auth-header:684
e851035fbe920538a6bf8fc5980312891e4eb878:admin/configurazione.md:curl-auth-header:546
# z39-server plugin: array literals matched by the generic rule
412253a18455260a5a793b04df36acc23712e7d2:storage/plugins/z39-server/Z39ServerPlugin.php:generic-api-key:122
b868a6b5d2180172b101171ac9eb98ae3e65ce5f:storage/plugins/z39-server/Z39ServerPlugin.php:generic-api-key:126
b868a6b5d2180172b101171ac9eb98ae3e65ce5f:storage/plugins/z39-server/Z39ServerPlugin.php:generic-api-key:169
# French translation strings matched by the generic rule
ad01c97eb68aefad0c1067adfd8377d8f882fa89:locale/fr_FR.json:generic-api-key:2861
3511b34a1e45f1f59a02cd2ebedf36f7dfe32f3e:locale/fr_FR.json:generic-api-key:2867
8cfb76efc76183f5fdffb702d682238b2b5dcaac:locale/fr_FR.json:generic-api-key:2871
320d3a3df0475fbe34d765e50b243499bd5b1414:locale/fr_FR.json:generic-api-key:2874
9d70038db598cccc7ab2f2f5941e1ff3a66a75cb:locale/fr_FR.json:generic-api-key:4439
# Update scripts: option names matched by the generic rule
03a8f3e1f75140af13b065d9b52b1d41ebf63e96:scripts/manual-update.php:generic-api-key:443
5e4ea3f42c694595559f9122932a0eb9fec023e7:scripts/manual-upgrade.php:generic-api-key:569
f0b1f8b5d473119235892e5dcec8860937eb8f67:test-updater/manual-update.php:generic-api-key:443
# Tests: RFC 8291 Appendix A web-push vectors and fixture strings
abbe641a9221c29f1ed009bde2873835b53c20f7:tests/mobile-webpush-encryption.unit.php:private-key:25
abbe641a9221c29f1ed009bde2873835b53c20f7:tests/mobile-webpush-encryption.unit.php:generic-api-key:34
ac4fcabc00953f3fd5f5c9d59fe5fb5da7f744cb:tests/schema-integrity.spec.js:generic-api-key:91
5eb52517f88d1ea3fadfffc1f92af21dbd9a1da2:tests/schema-integrity.spec.js:generic-api-key:117
8cfea8b8d47a8df9efef4d203ca96d8697203908:tests/security-hardening.spec.js:generic-api-key:37
2 changes: 2 additions & 0 deletions .rsync-filter
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@
- /.github/
- /docs/
- /*.md
# Upstream TinyMCE changelog: gitignored, never part of the release.
- /public/assets/tinymce/CHANGELOG.md
- .travis.yml
- .circleci/
- azure-pipelines.yml
Expand Down
45 changes: 18 additions & 27 deletions app/Controllers/AutoriApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,33 +55,24 @@ public function list(Request $request, Response $response, mysqli $db): Response
$params_for_where = [];
$param_types_for_where = '';

if ($search_text !== '') {
$where_prepared .= " AND (a.nome LIKE ? OR a.pseudonimo LIKE ? OR a.biografia LIKE ?) ";
$escaped = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $search_text) . '%';
$params_for_where[] = $escaped;
$params_for_where[] = $escaped;
$params_for_where[] = $escaped;
$param_types_for_where .= 'sss';
}
if ($search_nome !== '') {
$where_prepared .= " AND a.nome LIKE ? ";
$params_for_where[] = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $search_nome) . '%';
$param_types_for_where .= 's';
}
if ($search_pseudonimo !== '') {
$where_prepared .= " AND a.pseudonimo LIKE ? ";
$params_for_where[] = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $search_pseudonimo) . '%';
$param_types_for_where .= 's';
}
if ($search_sito !== '') {
$where_prepared .= " AND a.sito_web LIKE ? ";
$params_for_where[] = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $search_sito) . '%';
$param_types_for_where .= 's';
}
if ($search_naz !== '' && $colNaz !== null) {
$where_prepared .= " AND a.`$colNaz` LIKE ? ";
$params_for_where[] = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $search_naz) . '%';
$param_types_for_where .= 's';
// A value wrapped in quotes matches the exact spelling, accents
// included (#501): "Haffner" no longer returns Häffner.
$textFilters = [
[['a.nome', 'a.pseudonimo', 'a.biografia'], $search_text],
[['a.nome'], $search_nome],
[['a.pseudonimo'], $search_pseudonimo],
[['a.sito_web'], $search_sito],
];
if ($colNaz !== null) {
$textFilters[] = [["a.`$colNaz`"], $search_naz];
}
foreach ($textFilters as [$exprs, $value]) {
$cond = $value !== '' ? \App\Support\ExactSearch::filterCondition($exprs, $value) : null;
if ($cond !== null) {
$where_prepared .= ' AND ' . $cond['sql'] . ' ';
array_push($params_for_where, ...$cond['params']);
$param_types_for_where .= $cond['types'];
}
}
if ($nascita_from !== '') {
$where_prepared .= " AND a.data_nascita >= ? ";
Expand Down
35 changes: 27 additions & 8 deletions app/Controllers/CollaneController.php
Original file line number Diff line number Diff line change
Expand Up @@ -483,9 +483,17 @@ public function searchApi(Request $request, Response $response, mysqli $db): Res
// SEC2-1 + SEC1-5 (review): require min 2 chars and escape LIKE
// wildcards so callers can't broaden the match with `%`/`_` or
// probe single-char prefixes for enumeration.
if (mb_strlen($q) >= 2) {
$escaped = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $q);
$search = '%' . $escaped . '%';
// Each word keeps its own rule (`Mont "Häffner"`): with a quoted word
// filterCondition() builds one condition per word, so the minimum
// applies to every word (`"a" b` must not probe single characters).
// Unquoted input is still one phrase pattern (`%I Meridiani%`), so it
// is measured as a whole and a one-letter article does not block it.
$tokens = \App\Support\ExactSearch::tokenize($q);
$words = in_array(true, array_column($tokens, 'exact'), true)
? array_column($tokens, 'term')
: [\App\Support\ExactSearch::parseFilter($q)['term']];
$longEnough = $words !== [] && min(array_map('mb_strlen', $words)) >= 2;
Comment thread
fabiodalez-dev marked this conversation as resolved.
Resolved
if ($longEnough) {

$hasCollane = $this->hasCollaneTable($db);
$sqlParts = [];
Expand All @@ -497,9 +505,13 @@ public function searchApi(Request $request, Response $response, mysqli $db): Res
continue; // no collane table — skip its sources
}
$col = $src['c'];
$where = "`{$col}` LIKE ? AND `{$col}` IS NOT NULL AND `{$col}` != ''";
$types .= 's';
$binds[] = $search;
$condition = \App\Support\ExactSearch::filterCondition(["`{$col}`"], $q);
if ($condition === null) {
continue;
}
$where = $condition['sql'] . " AND `{$col}` IS NOT NULL AND `{$col}` != ''";
$types .= $condition['types'];
array_push($binds, ...$condition['params']);
if ($src['t'] === 'libri') {
$where .= ' AND deleted_at IS NULL';
}
Expand All @@ -515,8 +527,15 @@ public function searchApi(Request $request, Response $response, mysqli $db): Res
}

if (!empty($sqlParts)) {
$sql = 'SELECT DISTINCT nome FROM (' . implode(' UNION ', $sqlParts)
. ') AS combined ORDER BY nome LIMIT 10';
// One suggestion per spelling: accents keep "Brücke" and
// "Brucke" apart (#501), case does not ("Serie" = "serie", as
// UNION used to merge them). MIN(nome) keeps the column's
// tolerant collation, so the list sorts like the rest of the UI;
// that collation ties "Haffner" with "Häffner", so the binary
// spelling breaks the tie and the order is the same on every
// server.
$sql = 'SELECT MIN(nome) AS nome FROM (' . implode(' UNION ALL ', $sqlParts)
. ') AS combined GROUP BY LOWER(nome) COLLATE utf8mb4_bin ORDER BY nome, MIN(nome) COLLATE utf8mb4_bin LIMIT 10';
$stmt = $db->prepare($sql);
if ($stmt) {
$stmt->bind_param($types, ...$binds);
Expand Down
Loading