Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c89decf
Give copies received through Desiderata the book's own inventory prefix
fabiodalez-dev Oct 21, 2026
c90951d
Check the Desiderata inventory prefix through the real admin screens
fabiodalez-dev Oct 21, 2026
3625297
Cap the public loan request at the day before a scheduled loan and sa…
fabiodalez-dev Oct 21, 2026
bd0980e
Explain the capped loan window on every request surface, with tests
fabiodalez-dev Oct 21, 2026
a0be2d1
Re-pin the coherence audit to the calendars' loanWindowFrom() construct
fabiodalez-dev Oct 21, 2026
37d8a4e
Explain the capped window on the remaining surfaces; accept a return …
fabiodalez-dev Oct 21, 2026
6644fad
Cover every refusal surface and the edge cases; give all calendars th…
fabiodalez-dev Oct 21, 2026
bdd67ef
Fill the calendar explanations on the book page; count from the loan'…
fabiodalez-dev Oct 21, 2026
c1de9d4
Pass both durations through the availability route the calendars fetch
fabiodalez-dev Oct 21, 2026
b517d4a
Capture the run tag in the sandbox user closures of the two loan-wind…
fabiodalez-dev Oct 21, 2026
4c29ad4
Say that authorized users also open a loan-only Digital Library file
fabiodalez-dev Oct 21, 2026
e661f84
Renewals: say until when a renewal is still possible, and renew until…
fabiodalez-dev Oct 21, 2026
bc55e3e
Keep the #336 claimed-window literals, derive the extension flag in t…
fabiodalez-dev Oct 21, 2026
745f539
Keep the #336 claimed-window literals, derive the extension flag in t…
fabiodalez-dev Oct 21, 2026
eaf64ad
Merge ccr-22df4641-br2yzj
fabiodalez-dev Oct 21, 2026
f157cca
Honor the request duration maximum across web and Mobile API
fabiodalez-dev Oct 21, 2026
1ecdd33
Merge branch 'fix/loan-window-review' into fix/renewal-window-review
fabiodalez-dev Oct 21, 2026
3ec1582
Propose renewal dates that satisfy both capacity and copy commitments
fabiodalez-dev Oct 21, 2026
cd976b3
Address renewal review findings for dates and overdue extensions
fabiodalez-dev Oct 21, 2026
7c43528
Preserve staff defaults and bound capacity refusal work
fabiodalez-dev Oct 21, 2026
8058d54
Merge branch 'fix/loan-window-review' into fix/renewal-window-review
fabiodalez-dev Oct 21, 2026
c8bb135
Seed the scheduled loan's borrower in the capped-loan browser spec
fabiodalez-dev Oct 21, 2026
690c249
Merge ccr-22df4641-br2yzj
fabiodalez-dev Oct 21, 2026
1b2c156
Tell administrators when the server's PHP lacks the CVE-2026-109618/1…
fabiodalez-dev Oct 21, 2026
b96c35d
Merge ccr-22df4641-br2yzj-renewals into release/0.8.7
fabiodalez-dev Oct 21, 2026
9308798
Merge fix/digital-loan-visibility-description into release/0.8.7
fabiodalez-dev Oct 21, 2026
b0d6184
Merge fix/desiderata-inventory-prefix-489 into release/0.8.7
fabiodalez-dev Oct 21, 2026
01dafe0
Merge fix/php-security-advisory into release/0.8.7
fabiodalez-dev Oct 21, 2026
4a1156b
Release 0.8.7: version, changelog and README highlights
fabiodalez-dev Oct 21, 2026
8b8d6c3
test: cover the 0.8.7 changes without a database
fabiodalez-dev Oct 21, 2026
642e84b
test: find the uploads of an old release where that release writes them
fabiodalez-dev Oct 21, 2026
c05cda3
Give storage/digital back its group write bits after an upgrade
fabiodalez-dev Oct 21, 2026
fe16d0a
Link the PHP notice to the changelog entry of the release to install
fabiodalez-dev Oct 21, 2026
3c6dc4e
Close the CodeRabbit review of 0.8.7
fabiodalez-dev Oct 21, 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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

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

## [0.8.7]

### Security
- **Administrators are told when the server's PHP lacks the fixes for CVE-2026-109618 and CVE-2026-109619** ([#491](https://github.com/fabiodalez-dev/Pinakes/issues/491)). PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, released on 2026-10-21, fix two unauthenticated remote code execution flaws in the PHP runtime. They do not depend on Pinakes and no Pinakes update can fix them: PHP has to be updated on the server. The dashboard and the updates page now show administrators a notice with the running version, the release to move to, the interim `session.upload_progress.enabled = 0`, and that some distributions backport the fixes without changing the version. `php.ini.recommended` turns session upload progress off; Pinakes does not use it.

### Fixed
- **A loan request stops before a scheduled loan and says how long it can be** ([#490](https://github.com/fabiodalez-dev/Pinakes/pull/490)). When the only copy has a scheduled loan or a reservation inside the default loan window, the book page proposed a return date that fell on a blocked day and the request was refused with a bare "no copies available". Every surface that creates or edits a loan window (book page, user area, Mobile API, admin create, approval, date edit, admin reservations) now caps the window at the day before the conflict and says, in the reader's language, the longest possible loan and the day the book is booked from.
- **A renewal says until when it is still possible, and can renew until that day** ([#493](https://github.com/fabiodalez-dev/Pinakes/pull/493)). A renewal that crossed a scheduled loan on every copy was refused outright on the admin page, the bulk extension, the Mobile API and NCIP RenewItem. It now names the furthest possible due date and how many days that is, the loans list and the book page offer "Rinnova fino al D", and `POST /staff/loans/{id}/renew` accepts a `new_due_date`.
- **Copies received through Desiderata get the book's own inventory prefix** ([#489](https://github.com/fabiodalez-dev/Pinakes/issues/489), [#494](https://github.com/fabiodalez-dev/Pinakes/pull/494)). The plugin built them on `LIB-<id>` and ignored "Prefisso inventario copie", and an inventory code cannot be changed afterwards. One helper, `CopyRepository::inventoryBase()`, now gives the rule to Desiderata, book creation, the copies form, the increase-copies endpoint and the Mobile API. Existing codes are not renamed.
- **`storage/digital` keeps its group write bits after an upgrade.** At the end of every upgrade the updater reset every folder under `storage/` to 0755, so an installation already on 0.8.6 lost the group write bits of `storage/digital` that the maintenance cron needs when it runs as another user than the web server. The updater now gives them back, and the first request after upgrading to 0.8.7 repairs a folder the previous updater already reset.
- **The "Whoever has the book on loan" visibility says that authorized users open the file too** ([#492](https://github.com/fabiodalez-dev/Pinakes/pull/492)). The access rule was already so; only the description in the Digital Library settings left them out.

Plugin versions: Desiderata 1.2.1 and Mobile API 1.8.2, both requiring 0.8.7 (they call `CopyRepository::inventoryBase()`). No migration.

## [0.8.6]

### Added
Expand Down
25 changes: 12 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,17 @@ Pinakes is a self-hosted, full-featured ILS for schools, municipalities, and pri
[![Documentation](https://img.shields.io/badge/Documentazione-Docsify-4285f4?style=for-the-badge&logo=readthedocs&logoColor=white)](https://fabiodalez-dev.github.io/Pinakes/)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/fabiodalez)

## What's New in v0.8.6
## What's New in v0.8.7

**Who may open each digital file, files out of the web root, and custom roles.**
**A security notice for PHP, loan windows that explain themselves, and Desiderata codes that follow the book.**

- **Visibility per Digital Library file** ([#480](https://github.com/fabiodalez-dev/Pinakes/issues/480)): public, signed-in readers, authorised staff, or only the reader who has the book on loan. Every download passes through Pinakes, also from the Android app with signed links (Mobile API 1.8.x).
- **Files out of the web root**: new uploads live in `storage/digital/`; files of older versions move there on their own, at the same addresses.
- **Custom roles and a permission matrix** (Administration → Roles and permissions), with three editable presets: Catalogatore, Addetto prestiti, Archivista.
- **Upgrade heals every older `public/.htaccess`** ([#483](https://github.com/fabiodalez-dev/Pinakes/pull/483)): private uploads are checked on installations first set up before 0.7.71 too; edited rules are left alone and a backup is kept.
- **Older Digital Library links download again** ([#484](https://github.com/fabiodalez-dev/Pinakes/pull/484)), and cataloguing roles can use the FRBR lookups of the book editor.
- **A loan request defaults to the configured loan duration** ([#485](https://github.com/fabiodalez-dev/Pinakes/pull/485)) instead of one day; the Mobile API calendar reports `loan_duration_days`.
- **Lighter public pages** ([#486](https://github.com/fabiodalez-dev/Pinakes/pull/486)): the translation table is a cached script instead of being inlined in every page; sitemap and meta tags cleaned up.
- **The mail template editor says which mail an approved loan sends** ([#487](https://github.com/fabiodalez-dev/Pinakes/pull/487)).
- **PHP security notice** ([#491](https://github.com/fabiodalez-dev/Pinakes/issues/491)): CVE-2026-109618 and CVE-2026-109619 are in the PHP runtime, not in Pinakes: they are fixed upstream in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, and some distributions backport the fixes to older version numbers, so check your provider's packages too. The dashboard and the updates page tell administrators when their server's PHP is older, with the interim `session.upload_progress.enabled = 0` (in `php.ini` or the PHP-FPM pool, not `.user.ini`).
- **Loan requests stop before a scheduled loan** ([#490](https://github.com/fabiodalez-dev/Pinakes/pull/490)) and say the longest possible loan, on the book page, the user area, the Mobile API and every admin form.
- **Renewals say until when they are possible** ([#493](https://github.com/fabiodalez-dev/Pinakes/pull/493)) and can renew until that day, also from the Mobile API and NCIP.
- **Desiderata copies get the book's inventory prefix** ([#489](https://github.com/fabiodalez-dev/Pinakes/issues/489), [#494](https://github.com/fabiodalez-dev/Pinakes/pull/494)) instead of `LIB-<id>`.
- **Digital Library**: the "Whoever has the book on loan" visibility now says that authorized users open the file too ([#492](https://github.com/fabiodalez-dev/Pinakes/pull/492)).

**Migration:** `migrate_0.8.6-rc.1.sql`, additive: creates `ruoli`, adds `utenti.ruolo_id` and `utenti.accesso_digitale`, seeds the three presets and leaves every existing account as it was. It runs when upgrading from 0.8.5 or earlier; an installation on 0.8.6-rc.1 already has it.

**Plugins:** no new bundled plugin; Digital Library 1.5.0, Mobile API 1.8.1, OAI-PMH Server 1.1.2. **No manual steps:** the `.htaccess` heal and the move of digital files run on their own after the upgrade. Full details in **[CHANGELOG.md](CHANGELOG.md)**.
**No migration.** Desiderata 1.2.1 and Mobile API 1.8.2. Full details in **[CHANGELOG.md](CHANGELOG.md)**.

---

Expand All @@ -66,6 +61,10 @@ The showcase contains 50 catalogue books, 8 desiderata, 3 periodicals with 10 is

Highlights of the 0.8 releases are below. The full version-by-version history lives in **[CHANGELOG.md](CHANGELOG.md)**.

### v0.8.6

**Who may open each digital file, files out of the web root, and custom roles.** Every Digital Library attachment can be public, for signed-in readers, for authorised staff, or for the current borrower (authorised users open every file too), and every download passes through Pinakes, also from the app with signed links; administrators can create roles with exactly the permissions they need ([#480](https://github.com/fabiodalez-dev/Pinakes/issues/480)). The upgrade heals every older `public/.htaccess`, keeps files uploaded on older releases working, and public pages are much lighter. One additive migration (`migrate_0.8.6-rc.1.sql`); see **[CHANGELOG.md](CHANGELOG.md)**.

### v0.8.5

**An admin area in the Android app, an offline catalogue and a private library calendar.** Admin and staff accounts can catalogue, lend, return and manage plugin content from the app through the same pipeline as the web forms (Mobile API 1.7.0); the app keeps an offline copy of the catalogue in sync and readers can subscribe to a calendar of their own loans. The anonymous library calendar no longer lists borrowers' names and emails, renaming a series by changing only its case no longer deletes it, and an installable update reads in green again on the updates page. No core migration; see **[CHANGELOG.md](CHANGELOG.md)**.
Expand Down
2 changes: 1 addition & 1 deletion app/Controllers/CopyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ public function createCopy(Request $request, Response $response, mysqli $db, int
// Re-evaluate after sanitisation: an explicit value made only of
// control characters must fall back to automatic allocation.
if ($numero === '') {
$base = !empty($book['numero_inventario']) ? (string) $book['numero_inventario'] : "LIB-{$bookId}";
$base = CopyRepository::inventoryBase($bookId, $book['numero_inventario'] ?? null);
$newCopyIds = $repo->createManyForBookWithIdsAndNote(
$bookId,
$base,
Expand Down
4 changes: 2 additions & 2 deletions app/Controllers/FrontendController.php
Original file line number Diff line number Diff line change
Expand Up @@ -911,8 +911,8 @@ static function () use ($db, $book_id): array {
// default to max_loan_duration_days, so expose the same effective value
// to the view instead of hardcoding one calendar month in JavaScript.
$loanSettings = new \App\Models\SettingsRepository($db);
$maxRequestDays = max(1, (int) ($loanSettings->get('loans', 'max_loan_duration_days', '90') ?? 90));
$defaultRequestLoanDays = min($loanSettings->loanDurationDays(), $maxRequestDays);
$maxRequestDays = $loanSettings->maxLoanDurationDays();
$defaultRequestLoanDays = $loanSettings->requestLoanDurationDays();

// Check whether the BIBFRAME Linked Data plugin is active.
// Done before template include so the view can use $bibframePluginActive.
Expand Down
4 changes: 1 addition & 3 deletions app/Controllers/LibriController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1335,9 +1335,7 @@ public function store(Request $request, Response $response, mysqli $db): Respons
// Genera copie fisiche del libro
$copyRepo = new \App\Models\CopyRepository($db);
$copieTotali = (int) $fields['copie_totali'];
$baseInventario = !empty($fields['numero_inventario'])
? $fields['numero_inventario']
: "LIB-{$id}";
$baseInventario = \App\Models\CopyRepository::inventoryBase($id, $fields['numero_inventario'] ?? null);

// Create the requested holding set with one atomic multi-row INSERT:
// a request for three copies must never leave a partial 1/3 or 2/3
Expand Down
18 changes: 16 additions & 2 deletions app/Controllers/LoanApprovalController.php
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,24 @@ public function approveLoan(Request $request, Response $response, mysqli $db): R
$capacity = new \App\Services\CapacityService($db);
if (!$capacity->hasFreeCapacity($libroId, $today, $dataScadenza, excludePrestitoId: $loanId)) {
$db->rollback();
// Name the longest loan still possible from today when a
// later commitment is what blocks the approval, so staff
// can shorten the request instead of waiting for a return.
// The capacity check starts at today (a fully occupied
// earlier day means the loan would depend on a preceding
// return), but the loan itself starts on data_prestito:
// the figures count from there, and a conflict before it
// cannot be fixed by shortening, so no figures then.
$blocked = array_filter([$capacity->firstUnavailableDateInRange($libroId, $today, $dataScadenza, null, $loanId)]);
$cap = ($blocked !== [] && min($blocked) > $dataPrestito)
? \App\Support\LoanWindow::cap($dataPrestito, $blocked)
: null;
$response->getBody()->write(json_encode([
'success' => false,
'message' => __('Nessuna copia disponibile per il periodo richiesto')
]));
'message' => $cap === null
? __('Nessuna copia disponibile per il periodo richiesto')
: \App\Support\LoanWindow::explain($cap['max_duration_days'], $cap['max_end_date'], $cap['first_conflict_date']),
] + ($cap ?? [])));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}

Expand Down
Loading
Loading