Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
47352bd
feat(desiderata): books the library is looking for, and donations fro…
fabiodalez-dev Sep 16, 2026
91ce48a
fix(desiderata): hide requests everywhere the public can reach, and m…
fabiodalez-dev Sep 16, 2026
d50fd97
fix(desiderata): stop the wanted-list leaking through the seams aroun…
fabiodalez-dev Sep 17, 2026
6e0cbe6
feat(desiderata): make the wanted list a part of the library, not a p…
fabiodalez-dev Sep 17, 2026
529cc5f
ci: keep the workflow linters honest after the requires_app guard mov…
fabiodalez-dev Sep 17, 2026
9d6d1aa
feat(desiderata): tell the operators when a book is offered and when …
fabiodalez-dev Sep 17, 2026
ca43dc4
fix: close the review findings around the wanted list
fabiodalez-dev Sep 17, 2026
4d6ef42
feat(desiderata): let a reader reach the book, page through the list,…
fabiodalez-dev Sep 17, 2026
272005d
test(desiderata): 154 checks over the behaviour this branch added
fabiodalez-dev Sep 17, 2026
0525bc1
test(desiderata): the nine browser tests, and the defect the first of…
fabiodalez-dev Sep 17, 2026
3f5e1a1
fix(i18n): the Danish receipt notification said inventory, not catalogue
fabiodalez-dev Sep 17, 2026
bc48fb8
fix: the locale gate misread JS comments, and a cleanup could swallow…
fabiodalez-dev Sep 17, 2026
9923296
test: a cleanup that dies part-way must not erase the result it was r…
fabiodalez-dev Sep 17, 2026
9281dbd
test: a check that described the developer's machine, and one that wo…
fabiodalez-dev Sep 17, 2026
e9730ea
fix(desiderata): keep the request until the copies exist, and anchor …
fabiodalez-dev Sep 17, 2026
47d098a
chore(release): cut 0.7.86, register desiderata at install, ship book…
fabiodalez-dev Sep 17, 2026
85d7073
test: the ConfigStore precondition described a database, not a behaviour
fabiodalez-dev Sep 17, 2026
7bc704a
fix(desiderata): stop the wanted flag from breaking things outside it…
fabiodalez-dev Sep 17, 2026
1e67740
test(desiderata): stop the dashboard check from asserting a property …
fabiodalez-dev Sep 17, 2026
92e1a1e
docs(visibility): record why the desiderata column probe may be memoised
fabiodalez-dev Sep 17, 2026
fa2fdcf
fix(desiderata): apply the four findings promoted in the walkthrough
fabiodalez-dev Sep 17, 2026
3e28b33
fix(desiderata): stop the thank-you banner depending on a session the…
fabiodalez-dev Sep 17, 2026
8f3153e
test(desiderata): follow the thank-you marker into the two assertions…
fabiodalez-dev Sep 17, 2026
c52e039
fix(forms): show a locked field as locked, and stop three browser che…
fabiodalez-dev Sep 17, 2026
504b93c
test(book-club): stop R16 passing when the declined loan is deleted i…
fabiodalez-dev Sep 17, 2026
305ef6e
fix(desiderata): stop the homepage donation form minting its session …
fabiodalez-dev Sep 18, 2026
5e4ede3
feat(desiderata): let a donor send from a browser the site never gave…
fabiodalez-dev Sep 18, 2026
76fb354
fix(desiderata): serialize CSRF initialization and recover interrupte…
fabiodalez-dev Sep 18, 2026
83e2d54
fix(desiderata): stamp catalogued_at on every insert, fold CSV header…
fabiodalez-dev Sep 18, 2026
24c13c0
test(desiderata): make sandbox suites order-independent and never lea…
fabiodalez-dev Sep 18, 2026
a890f15
fix(desiderata): close the remaining review findings
fabiodalez-dev Sep 18, 2026
757ea10
fix(auth): re-validate the session role on every protected request
fabiodalez-dev Sep 18, 2026
52fff5e
fix: close the remaining review findings and the bugs found while doc…
fabiodalez-dev Sep 18, 2026
67bb024
docs(hooks): document every hook core invokes and correct stale refer…
fabiodalez-dev Sep 18, 2026
e3d61f1
fix: address the CodeRabbit review of 76fb3547
fabiodalez-dev Sep 19, 2026
6364b18
fix(ui): keep typed search text across activity feed swaps, stop fals…
fabiodalez-dev Sep 19, 2026
464bb40
fix(auth): decide admin access on the re-validated role, so promotion…
fabiodalez-dev Sep 19, 2026
9342882
fix: address the CodeRabbit review of 6364b18d
fabiodalez-dev Sep 19, 2026
14d5b8c
test: keep "0" in the sandbox guard's list of real database names
fabiodalez-dev Sep 19, 2026
24bb009
Merge remote-tracking branch 'origin/main' into fix/desiderata-donazioni
fabiodalez-dev Sep 24, 2026
3870484
fix(desiderata): a failed proposals query is not an empty list
fabiodalez-dev Sep 24, 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
70 changes: 68 additions & 2 deletions .github/workflows/ci-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,17 @@ jobs:
composer install --no-interaction --prefer-dist --quiet
composer audit --no-dev --abandoned=ignore 2>&1 || { echo "⚠ composer audit: vulnerabilities found (see above)"; exit 1; }

- name: npm audit (known CVEs)
- name: npm audit (known CVEs) # zizmor: ignore[adhoc-packages]
run: |
# npm bundled with Node 22 still POSTs the retired quick-audit
# endpoint (400 since 2026-09-04); npm 11+ talks to the bulk one.
# Tool upgrade, not a dependency: nothing from it ships in any asset.
# The zizmor exemption on this step's name is anchored to the step
# itself, not to a line number: a config pin drifts the moment
# anything above it changes, and it did three times in one day.
# It stays this narrow on purpose — nothing installed on an
# ephemeral runner ships in an asset, and asset reproducibility is
# enforced separately by the vendor-assets and double-build checks.
npm install -g npm@11.19.1 --silent
npm ci --silent
bash scripts/ci-npm-audit.sh .
Expand Down Expand Up @@ -237,6 +243,36 @@ jobs:
done
[ "$FAILED" -eq 0 ] && echo "✓ Tutte le migration hanno versione ≤ $TARGET" || exit 1

# ── Plugin requires_app guard ─────────────────────────────────────────
# Same version_compare ordering hazard as the migration guard, one layer
# up: PluginManager refuses to activate a bundled plugin whose
# requires_app is above version.json, so the plugin ships registered,
# visible and permanently impossible to enable. An empty or absent
# requires_app means "no floor" and is skipped, not failed.
- name: Plugin requires_app guard (bundled plugin.json ≤ version.json)
run: |
TARGET=$(php -r "echo json_decode(file_get_contents('version.json'))->version;")
FAILED=0
for f in storage/plugins/*/plugin.json; do
[ -f "$f" ] || continue
plugin_name="$(basename "$(dirname "$f")")"
# The single quotes below are load-bearing: $m is a PHP variable and
# must reach php unexpanded. The manifest path travels through the
# environment (getenv), so nothing here needs shell interpolation.
# shellcheck disable=SC2016
REQ=$(MANIFEST="$f" php -r '
$m = json_decode((string)file_get_contents(getenv("MANIFEST")), true);
echo is_array($m) && isset($m["requires_app"]) ? (string)$m["requires_app"] : "";
')
[ -n "$REQ" ] || continue
if ! REQUIRES_APP="$REQ" RELEASE_VERSION="$TARGET" php -r \
'exit(version_compare(getenv("REQUIRES_APP"), getenv("RELEASE_VERSION"), "<=") ? 0 : 1);'; then
echo " ✗ ${plugin_name}: requires_app $REQ > $TARGET (il plugin non potrebbe mai essere attivato)"
FAILED=1
fi
done
[ "$FAILED" -eq 0 ] && echo "✓ Tutti i plugin bundled hanno requires_app ≤ $TARGET" || exit 1

# DB-backed unit tests (migration-*, book-field-types, loan-edge-cases,
# session-fixes) read credentials from .env and connect over TCP. Provide
# a CI .env pointing at the MySQL service and load the base schema so the
Expand Down Expand Up @@ -306,6 +342,30 @@ jobs:
php tests/plugin-zip-update.integration.php
php tests/plugin-zip-update-all-bundled.integration.php

# The desiderata feature's advertised coverage: the lifecycle/offer suite
# and the interop-visibility contract (mobile-api, OAI-PMH, SRU, NCIP,
# OpenURL, BIBFRAME). Neither ran in any workflow before, so 400+ lines of
# regression checks executed only if a developer remembered to. Strict
# mode turns any remaining skip into a failure.
- name: PHP desiderata integration suites
env:
CI_STRICT_TESTS: '1'
run: |
php tests/desiderata.integration.php
php tests/desiderata-visibility.integration.php
php tests/desiderata-extended.integration.php
php tests/desiderata-recaptcha.unit.php
php tests/desiderata-core-hooks.unit.php
# desiderata-return-path, desiderata-bookclub-resolvable and
# desiderata-search-facets are NOT listed here: they are *.unit.php
# and the "PHP unit tests" step above already globs them. Naming them
# again would run each one twice.
# Run after plugin schema setup so the cross-plugin cases cannot skip.
REQUIRE_DESIDERATA_TESTS=1 php tests/bookclub-lending.unit.php

- name: Donation form CSRF concurrency regression
run: node --test tests/desiderata-csrf-concurrency.test.cjs

- name: Shell test — bin/setup-permissions.sh
run: bash tests/setup-permissions.test.sh

Expand Down Expand Up @@ -355,9 +415,15 @@ jobs:
cache-dependency-path: |
package-lock.json
frontend/package-lock.json
- name: Install and audit locked dependencies
- name: Install and audit locked dependencies # zizmor: ignore[adhoc-packages]
run: |
# Same retired quick-audit endpoint workaround as the root audit.
# The zizmor exemption on this step's name is anchored to the step
# itself, not to a line number: a config pin drifts the moment
# anything above it changes, and it did three times in one day.
# It stays this narrow on purpose — nothing installed on an
# ephemeral runner ships in an asset, and asset reproducibility is
# enforced separately by the vendor-assets and double-build checks.
npm install -g npm@11.19.1 --silent
npm ci --silent
bash scripts/ci-npm-audit.sh .
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
node-version: '22'
package-manager-cache: false

- name: Install locked build inputs
- name: Install locked build inputs # zizmor: ignore[adhoc-packages]
run: |
sudo apt-get update -q
sudo apt-get install -y jq rsync unzip zip
Expand All @@ -58,6 +58,12 @@ jobs:
# npm bundled with Node 22 POSTs the retired quick-audit endpoint;
# npm 11 uses the bulk one, and the wrapper distinguishes real
# advisories from registry outages (retry, then loud neutral).
# The zizmor exemption on this step's name is anchored to the step
# itself, not to a line number: a config pin drifts the moment
# anything above it changes, and it did three times in one day.
# It stays this narrow on purpose — nothing installed on an
# ephemeral runner ships in an asset, and asset reproducibility is
# enforced separately by the vendor-assets and double-build checks.
npm install -g npm@11.19.1 --silent
npm ci --silent
bash scripts/ci-npm-audit.sh .
Expand Down
17 changes: 0 additions & 17 deletions .github/zizmor.yml

This file was deleted.

8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ storage/calendar/*
!storage/calendar/.htaccess
!storage/plugins/
storage/plugins/*
!storage/plugins/desiderata/
storage/plugins/desiderata/*
!storage/plugins/desiderata/*.php
!storage/plugins/desiderata/*.json
!storage/plugins/desiderata/*.md
!storage/plugins/desiderata/views/
!storage/plugins/desiderata/views/*.php
!storage/plugins/open-library/
storage/plugins/open-library/*
!storage/plugins/open-library/*.php
Expand Down Expand Up @@ -441,6 +448,7 @@ tests/*
!tests/*.unit.php
!tests/*.integration.php
!tests/*.test.sh
!tests/*.test.cjs
!tests/ci-playwright-policy.json
!tests/seeds/
tests/seeds/*
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@

Full version-by-version history for Pinakes. The README shows only the latest release; everything older lives here.

## [0.7.86]

### Added
- Optional Desiderata plugin: library requests without physical copies, a homepage section that is reorderable, hideable and editable per locale from the CMS, public donations searchable by title, author, publisher or ISBN, a wanted badge in catalogue search, dashboard management, direct receipt, receipt history and operator notifications in each recipient's own language. A wanted title is findable by name and reachable by link, and stays out of catalogue browsing, feeds, the sitemap, the mobile API and the interop protocols. With the plugin off, every one of those surfaces behaves as it did before the feature existed.
- Test coverage for the feature: 475 PHP checks across thirteen suites and 13 browser scenarios, including two that deactivate the plugin through the real admin endpoints, three for reCAPTCHA — which had no automated coverage at all — twenty release regression cases for proposal/receipt concurrency and for Book Club member lending of wanted books, and six new suites covering the defects listed under Fixed: the return-address allow-list, a club book surviving the flag, the search facets counting the same books the results show, every spelling of the CSV affirmative, the OAI-PMH tombstone distinction, the upgrade that carries it to installations that already have the plugin, the thank-you banner surviving a submission sent from a page that has no session, and the donor whose browser was never given one.

### Fixed
- **A donation can be sent from a browser the site never gave a session to.** `/` and a book page are served without a session on purpose — they are edge-cacheable, and a per-visitor CSRF token baked into a shared cache would be handed to everyone — so the form there carries an empty token and the browser mints one before posting. Two things followed from that. The token is now fetched when the form is wired rather than inside the submit itself, so a donor's first write no longer depends on a session created a few milliseconds earlier in the same gesture; a page that already has a session makes no extra request at all. And with scripting off, where nothing can mint anything, the send is answered with the donor's own form back — everything they typed still in it, now carrying a real token — and one more confirmation, instead of a bare "Sessione Scaduta" for something they did nothing wrong in. Nothing is accepted on that first send, and a wrong or missing token where a session already exists is refused exactly as before.
- **A disabled or read-only field now looks it.** `.form-input` kept the white background and the hover border of an editable field in both states, so the only sign was that typing did nothing — which reads as a broken page rather than a field the form is deliberately holding. It shows most on the book form: ticking Desiderata locks the initial copies to zero, and the locked field was indistinguishable from the one beside it. The rule is on the component, so every disabled field in the application gains it.
- **The thank-you banner no longer depends on a session the page does not have.** A proposal sent from the homepage confirmed itself through a session flash, but `/` is deliberately served without a session — so the confirmation had to survive a continuity nothing guarantees, and sometimes did not. It now travels in the redirect as well, which is also what makes it work for a visitor whose browser was never given a session at all.
- **OAI-PMH and ResourceSync no longer announce the deletion of records they never published.** A wanted title produced a `status=deleted` header whether it had been *withdrawn* from the catalogue — which its harvesters are genuinely owed — or simply *created* as a request, which no harvester ever saw; the flag records no difference, because the copies that would have told them apart are deleted outright. Deletions are now derived from a write-once `libri.catalogued_at`, so they are sent for records that were once public and for nothing else. Existing wanted titles cannot be judged retroactively and are left alone: they stop producing deletions rather than keep producing wrong ones.
- **`SÌ` in a CSV's desiderata column is recognised.** The importer lower-cased byte by byte, which folds only the unaccented half of the alphabet, so the accented affirmative worked in lower case and silently imported as an owned holding in upper case — with no warning, since an unrecognised value simply means no.
- **A donor whose book stops being wanted mid-form can act on what the message tells them.** The error says the book can still be offered as a separate donation; the title field stayed locked, and the only control that released it was wired in JavaScript, so the offer held for visitors running scripts and was a dead end for everyone else. Everything already typed survives the refusal.
- The plugin's three public paths are defined once instead of being retyped in seven places across five files, where a rename could land half-done and fail only at the moment someone followed the stale link.
- **A reading club no longer loses a book the library flags as wanted.** Filtering wanted titles out of the club's queries was expressed as a condition on the join over `libri`, which cannot mean "hide the title, keep the entry": on a left join it empties the whole book row, and the guard beside it — written to hide a *deleted* book — then read that as a missing record and dropped the entry outright, so the club book stopped resolving and its state changes, reading schedule, polls, meetings, surveys, discussions, buddy readings and quotes all answered 404. On the sibling queries, where the same condition sat on an inner join, the entry simply vanished from the list. The filter is gone rather than relocated: the wish list is published to anonymous visitors at `/desiderata` by design, so a club naming a book its members chose to read discloses nothing the feature does not publish itself.
- **An open redirect in the donation form.** The allow-list for the address a donor returns to rejected a leading `//` or `/\`, but browsers delete tab, line feed and carriage return from a URL wherever they occur before parsing it, so a value carrying a tab was judged in a form the browser would never see and then read as a reference to another host. Any control character now refuses the value outright.
- **Searching for a wanted title no longer disagrees with the counters beside it.** The result grid widens to include wanted titles as soon as something is typed in the search box; the genre, publisher, author, media and year counts did not, so the same page showed the book and denied it existed, and clicking any filter made it disappear.
- **A favourite is no longer destroyed by trying to add it.** A book flagged as wanted disappears from the reader's favourites list, so the heart on its page shows empty and clicking it means *add* — while the click was in fact deleting the hidden entry for good. The removal is now limited to what the reader can actually see, and the favourite comes back on its own when the donation arrives.
- **Recording a direct receipt refuses a book that already has copies**, instead of adding another one, matching the two other receipt paths.
- **The homepage save no longer claims nothing was saved when something was.** A plugin section reporting a problem produced "Nessuna modifica è stata salvata" although every core section had already been written; the two cases now read differently, because they are different.
- **The book form's save confirmation says what saving will do.** Clearing the Desiderata box creates real inventory copies, and the dialog only asked whether to update the book. Plugins that add a field to that form can now add a line to its confirmation, which is how the warning gets there without the core form having to know what a desiderata is.
- Unticking Desiderata on the book form no longer clears the request before the copies exist: the flag now falls only inside the transaction that creates them, so a copy-creation failure leaves the book a request rather than a catalogue record with nothing on the shelf, and the operator is told instead of seeing a success message. That path also refuses to register copies while a donor proposal is still pending or accepted, which until now only the direct-receipt button did.
- Donation proposals and receipts serialize on the same book row, preventing proposals from being attached to a request that closed during submission.
- reCAPTCHA email tests create their own temporary administrator instead of depending on existing installation users.

### Testing
- Five browser checks waited for a SweetAlert popup to be *absent* before reading the database. The handlers close the confirm dialog, await the fetch and only then open the success dialog, so "or no popup" is already true while the request is still in flight — the assertion then read a row the server had not written yet. Two of the five already waited for the HTTP response; the other three now do too. This is what made a rejected reservation intermittently read as still pending in the regression gate.
- The reCAPTCHA browser check flushes the web server's settings cache after writing the key. The fixture cleared its own process's cache, which is not Apache's — with the APCu backend that cache is in the web server's shared memory — so the check passed or failed according to whether anything had read the contacts settings in the previous sixty seconds.
- The plugin-upgrade check picks its MySQL transport the way the rest of the suite does instead of hard-coding a socket path, which is another way of writing down the developer's own machine.
- The desiderata browser fixture writes the cover it needs instead of borrowing whichever image the machine happened to have in `public/uploads/copertine`. It passed on a developer's installation, where demo covers are lying around, and failed on a clean runner — reported as eight "suspicious skips" and a missing-fixture error that said nothing about the code under test.

## [0.7.85]

### Added
Expand Down
Loading
Loading