Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

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

## [0.8.8]

### Fixed
- **The French loan-window sentences include the last day** ([#490](https://github.com/fabiodalez-dev/Pinakes/pull/490)). "retour avant le D" excluded the day the server still accepts as the return date; it now reads "retour au plus tard le D", as the Italian "entro il" does, in both the plural and the singular sentence. The book-club due date says "À rendre au plus tard le D" for the same reason.

### Tests
- The two database-backed loan-window suites pin the default and maximum loan durations for their run and restore the installation's values afterwards, so they no longer depend on what the environment configures; the capped-loan browser spec restores the title's derived columns that an auto-approved request recomputes.

No migration, no plugin change. 0.8.7 is still the release that carries the PHP security notice for CVE-2026-109618 and CVE-2026-109619 ([#491](https://github.com/fabiodalez-dev/Pinakes/issues/491)).

## [0.8.7]

### Security
Expand Down
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,9 @@ 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.7
## What's New in v0.8.8

**A security notice for PHP, loan windows that explain themselves, and Desiderata codes that follow the book.**

- **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)).

**No migration.** Desiderata 1.2.1 and Mobile API 1.8.2. Full details in **[CHANGELOG.md](CHANGELOG.md)**.
**A French sentence that now includes the last day of a loan.** The loan-window explanation added in 0.8.7 said "retour avant le D" in French, which left out the return day the server still accepts; it now says "retour au plus tard le D" ([#490](https://github.com/fabiodalez-dev/Pinakes/pull/490)). The loan-window test suites no longer depend on the loan durations an environment configures. No migration. If you are upgrading from 0.8.6 or earlier, read the 0.8.7 notes below too: they include a notice about the PHP security release of 21 October 2026. Full details in **[CHANGELOG.md](CHANGELOG.md)**.

---

Expand All @@ -61,6 +53,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.7

**A security notice for PHP, loan windows that explain themselves, and Desiderata codes that follow the book.** CVE-2026-109618 and CVE-2026-109619 are in the PHP runtime, not in Pinakes: the dashboard and the updates page tell administrators when their server's PHP is older than 8.5.12, 8.4.27, 8.3.36 or 8.2.35 ([#491](https://github.com/fabiodalez-dev/Pinakes/issues/491)). Loan requests stop before a scheduled loan and say the longest possible loan ([#490](https://github.com/fabiodalez-dev/Pinakes/pull/490)), renewals say until when they are possible ([#493](https://github.com/fabiodalez-dev/Pinakes/pull/493)), and Desiderata copies get the book's inventory prefix ([#494](https://github.com/fabiodalez-dev/Pinakes/pull/494)). No migration; see **[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)**.
Expand Down
6 changes: 3 additions & 3 deletions locale/fr_FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -6540,7 +6540,7 @@
"Copia offerta al club.": "Exemplaire proposé au club.",
"Copia segnata come restituita.": "Exemplaire marqué comme rendu.",
"Copie offerte dai membri": "Exemplaires proposés par les membres",
"Da restituire entro il %s": "À rendre avant le %s",
"Da restituire entro il %s": "À rendre au plus tard le %s",
"Data di riconsegna (facoltativa)": "Date de retour (facultative)",
"Elimina bozza": "Supprimer le brouillon",
"Eliminare questa bozza? Le domande andranno perse.": "Supprimer ce brouillon ? Les questions seront perdues.",
Expand Down Expand Up @@ -8374,11 +8374,11 @@
"Se lo stesso file è allegato anche ad altre schede, vale la visibilità più aperta tra quelle schede.": "Si le même fichier est aussi joint à d’autres notices, la visibilité la plus ouverte parmi ces notices s’applique.",
"Gestione allegati digitali comprende anche l’apertura degli allegati riservati: chi può cambiare la visibilità di un file può comunque aprirlo, quindi i due permessi vengono salvati insieme.": "La gestion des pièces jointes numériques comprend aussi l’ouverture des pièces jointes réservées : qui peut modifier la visibilité d’un fichier peut de toute façon l’ouvrir, les deux autorisations sont donc enregistrées ensemble.",
"Diritti esistenti": "Droits existants",
"Durata massima del prestito: %1$d giorni (restituzione entro il %2$s). Dal %3$s il libro è già prenotato.": "Durée maximale du prêt : %1$d jours (retour avant le %2$s). À partir du %3$s, le livre est déjà réservé.",
"Durata massima del prestito: %1$d giorni (restituzione entro il %2$s). Dal %3$s il libro è già prenotato.": "Durée maximale du prêt : %1$d jours (retour au plus tard le %2$s). À partir du %3$s, le livre est déjà réservé.",
"Giorno non disponibile (%1$s): %2$s": "Jour non disponible (%1$s) : %2$s",
"Il periodo richiesto supera la durata massima consentita.": "La période demandée dépasse la durée maximale de prêt autorisée.",
"Da questa data di inizio non è possibile alcun prestito: dal %s il libro è già prenotato. Scegli un'altra data.": "Aucun prêt n'est possible à partir de cette date : à partir du %s, le livre est déjà réservé. Choisissez une autre date.",
"Durata massima del prestito: 1 giorno (restituzione entro il %1$s). Dal %2$s il libro è già prenotato.": "Durée maximale du prêt : 1 jour (retour avant le %1$s). À partir du %2$s, le livre est déjà réservé.",
"Durata massima del prestito: 1 giorno (restituzione entro il %1$s). Dal %2$s il libro è già prenotato.": "Durée maximale du prêt : 1 jour (retour au plus tard le %1$s). À partir du %2$s, le livre est déjà réservé.",
"Rinnovo non possibile: dal %s il libro è già prenotato.": "Aucune prolongation possible : à partir du %s, le livre est déjà réservé.",
"Rinnovo possibile al massimo di 1 giorno (fino al %1$s): dal %2$s il libro è già prenotato.": "Prolongation possible d'au plus 1 jour (jusqu'au %1$s) : à partir du %2$s, le livre est déjà réservé.",
"Rinnovo possibile al massimo di %1$d giorni (fino al %2$s): dal %3$s il libro è già prenotato.": "Prolongation possible d'au plus %1$d jours (jusqu'au %2$s) : à partir du %3$s, le livre est déjà réservé.",
Expand Down
1 change: 0 additions & 1 deletion tests/archives-marc21-xsd.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
$ref = new ReflectionClass($plugin);
$call = static function (string $method, mixed ...$args) use ($ref, $plugin): mixed {
$m = $ref->getMethod($method);
$m->setAccessible(true);
return $m->invoke($plugin, ...$args);
};
/** Render records inside a MARC Slim <collection>. */
Expand Down
5 changes: 0 additions & 5 deletions tests/archives-plugin.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@
$xw->openMemory();
$xw->setIndent(true);
$method = $reflection->getMethod($methodName);
$method->setAccessible(true);
$method->invokeArgs($instance, array_merge([$xw], $args));
return $xw->outputMemory();
};
Expand All @@ -155,7 +154,6 @@
$check(str_contains($listRecordsSetXml, 'code="noSetHierarchy"'), 'ListRecords rejects set filters with noSetHierarchy');

$buildDc = $reflection->getMethod('buildDublinCoreXml');
$buildDc->setAccessible(true);
$dcXml = (string) $buildDc->invoke($instance, [
'constructed_title' => 'Test collection',
'reference_code' => 'TEST-DC-001',
Expand All @@ -168,7 +166,6 @@

$configReflection = new ReflectionClass(\App\Support\ConfigStore::class);
$runtimeCache = $configReflection->getProperty('runtimeCache');
$runtimeCache->setAccessible(true);
$previousRuntimeCache = $runtimeCache->getValue();
$runtimeCache->setValue(null, ['app' => ['name' => 'Pinakes Test Repository']]);
$dcFallbackXml = (string) $buildDc->invoke($instance, [
Expand All @@ -195,9 +192,7 @@

echo "\nOAI-PMH resumptionToken round-trip:\n";
$encode = $reflection->getMethod('encodeOaiResumptionToken');
$encode->setAccessible(true);
$decode = $reflection->getMethod('decodeOaiResumptionToken');
$decode->setAccessible(true);
$token = (string) $encode->invoke($instance, 100, 'ead3', '2026-05-01T00:00:00Z', '2026-05-02T12:34:56Z', 'series');
$decoded = $decode->invoke($instance, $token);
$check(is_array($decoded), 'encoded token decodes');
Expand Down
6 changes: 0 additions & 6 deletions tests/archives-ric-jsonld.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@
echo "\nURI scheme allow-list:\n";
$ric = new \ReflectionClass(\App\Plugins\Archives\RicJsonLdBuilder::class);
$isValid = $ric->getMethod('isValidLodUri');
$isValid->setAccessible(true);

$uriCases = [
// Positive — every scheme in ALLOWED_URI_SCHEMES must pass.
Expand Down Expand Up @@ -187,7 +186,6 @@

echo "\nxsd:gYear formatting:\n";
$buildDateRange = $ric->getMethod('buildDateRange');
$buildDateRange->setAccessible(true);

// Helper that returns the two formatted gYear strings (begin, end).
$gyear = static function (?int $start, ?int $end) use ($builder, $buildDateRange): array {
Expand Down Expand Up @@ -246,7 +244,6 @@

echo "\nARK identifier sanitiser:\n";
$sanitizeArk = $ric->getMethod('sanitizeArkIdentifier');
$sanitizeArk->setAccessible(true);

$arkCases = [
// Canonical form already prefixed — pass through.
Expand Down Expand Up @@ -756,7 +753,6 @@
// Unknown CURIE prefix returns empty URI so the writer can fall back.
$reflection = new \ReflectionClass($builder);
$method = $reflection->getMethod('expandCurie');
$method->setAccessible(true);
$ret = $method->invoke($builder, 'unknownprefix:Foo', []);
$check($ret[1] === 'Foo' && $ret[0]['uri'] === '',
'unknown prefix → empty namespace URI (falls back to rdf:Description on emit)');
Expand All @@ -766,9 +762,7 @@
// integer / float datatype emission, the typed-reference compact form,
// and the `@id === '0'` survival without depending on a database row.
$writeProp = $reflection->getMethod('writeRdfProperty');
$writeProp->setAccessible(true);
$writeSubject = $reflection->getMethod('writeRdfSubject');
$writeSubject->setAccessible(true);

$renderProp = static function (string $tag, mixed $value) use ($builder, $writeProp): string {
$xw = new \XMLWriter();
Expand Down
1 change: 0 additions & 1 deletion tests/audit-fixes-p2-p4.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,6 @@ static function () use ($mkLoan, $bookP1, $copyP1, $borrower1, $picker1, $d): ar
$notifUser = $mkUser();
[$bookN, [$copyN]] = $mkBook(1);
$sendMethod = new \ReflectionMethod(ReservationManager::class, 'sendReservationNotification');
$sendMethod->setAccessible(true);
$manager = new ReservationManager($db);
$userRow = $db->query("SELECT email, nome, cognome FROM utenti WHERE id = {$notifUser}")->fetch_assoc();
$mkPayload = static fn (int $resId, int $bookId) => [
Expand Down
1 change: 0 additions & 1 deletion tests/backup-generated-columns.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@

$manager = new \App\Support\BackupManager($db, $root);
$method = new \ReflectionMethod(\App\Support\BackupManager::class, 'dumpDatabaseTo');
$method->setAccessible(true);
$method->invoke($manager, $dumpFile);

$dump = (string) file_get_contents($dumpFile);
Expand Down
3 changes: 0 additions & 3 deletions tests/backup-retention.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
*/
$prune = static function (BackupManager $manager, string $justWritten): void {
$ref = new ReflectionMethod(BackupManager::class, 'pruneOldBackups');
$ref->setAccessible(true);
$ref->invoke($manager, $justWritten);
};

Expand Down Expand Up @@ -89,7 +88,6 @@
$setRetention($retention);
$manager = new BackupManager($db, $root);
$prop = new ReflectionProperty(BackupManager::class, 'backupPath');
$prop->setAccessible(true);
$prop->setValue($manager, $dir);
return $manager;
};
Expand Down Expand Up @@ -282,7 +280,6 @@
}

$nameFor = new ReflectionMethod(BackupManager::class, 'backupFileName');
$nameFor->setAccessible(true);

$manual = $tmp . '/' . $nameFor->invoke(null, '2025-01-01_000000', BackupManager::ORIGIN_MANUAL);
$safety = $tmp . '/' . $nameFor->invoke(null, '2025-01-02_000000', BackupManager::ORIGIN_SAFETY);
Expand Down
2 changes: 0 additions & 2 deletions tests/bookclub-uq-bcbooks-and-isbn-checksum.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
echo "A. normalizedIsbnParts validates the checksum\n";
$repo = new Repo($db);
$rp = new ReflectionMethod(Repo::class, 'normalizedIsbnParts');
$rp->setAccessible(true);
$parts = static fn(string $isbn): array => $rp->invoke($repo, $isbn);

// Sanity-check the fixtures against the validator so the assertions below are
Expand All @@ -115,7 +114,6 @@
// ── Part B. uq_bcbooks self-heal + duplicate-safe refusal ────────────────────
echo "B. ensureBookclubBooksExternalSupport self-heals uq_bcbooks\n";
$rm = new ReflectionMethod(BookClubPlugin::class, 'ensureBookclubBooksExternalSupport');
$rm->setAccessible(true);
$runMigration = static function () use ($rm, $plugin): void { $rm->invoke($plugin); };

$dupClubId = null; $dupLibroId = null;
Expand Down
3 changes: 0 additions & 3 deletions tests/catalog-materialization.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,6 @@
$sentinelPathMethod = $projectionClass->getMethod('degradedSentinelPath');
$readDegradedMethod = $projectionClass->getMethod('readDegradedSet');
$clearDegradedMethod = $projectionClass->getMethod('clearDegradedFor');
$sentinelPathMethod->setAccessible(true);
$readDegradedMethod->setAccessible(true);
$clearDegradedMethod->setAccessible(true);
$sentinelPath = (string) $sentinelPathMethod->invoke(null);
$sentinelLock = $sentinelPath . '.lock';
$sentinelExisted = is_file($sentinelPath);
Expand Down
1 change: 0 additions & 1 deletion tests/circulation-sweep-audit.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,6 @@

foreach (['runtimeCache' => null, 'dbSettingsCache' => null, 'sharedConnection' => null, 'connectionFailed' => false] as $cacheProp => $resetVal) {
$rp = new ReflectionProperty(\App\Support\ConfigStore::class, $cacheProp);
$rp->setAccessible(true);
$rp->setValue(null, $resetVal);
}

Expand Down
2 changes: 0 additions & 2 deletions tests/contributor-roles-237.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,6 @@
'autori_ids' => [$authorId, $relatedCoauthorId],
]);
$relatedMethod = new ReflectionMethod(FrontendController::class, 'getRelatedBooks');
$relatedMethod->setAccessible(true);
$relatedRows = $relatedMethod->invoke(
new FrontendController(),
$db,
Expand Down Expand Up @@ -336,7 +335,6 @@
$db->query("INSERT INTO libri_autori (libro_id, autore_id, ruolo) VALUES ({$bookId}, {$apiCuratorId}, 'curatore')");
$db->query("INSERT INTO libri_autori (libro_id, autore_id, ruolo) VALUES ({$bookId}, {$apiIllustratorId}, 'illustratore')");
$findBooks = new ReflectionMethod(PublicApiController::class, 'findBooks');
$findBooks->setAccessible(true);
$apiContributorRows = $findBooks->invoke(new PublicApiController(), $db, null, null, null, $realName);
$apiContributorBook = array_values(array_filter(
is_array($apiContributorRows) ? $apiContributorRows : [],
Expand Down
1 change: 0 additions & 1 deletion tests/csv-scraping-family-gate-380.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@

$controller = (new ReflectionClass(CsvImportController::class))->newInstanceWithoutConstructor();
$enrich = new ReflectionMethod(CsvImportController::class, 'enrichBookWithScrapedData');
$enrich->setAccessible(true);

// CSV row with EMPTY cells for the gated fields (the pre-fix trigger condition).
$csvEmpty = [
Expand Down
1 change: 0 additions & 1 deletion tests/desiderata-catalogued-on-create.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ function __(string $text, mixed ...$args): string
);

$insertBook = new ReflectionMethod(\App\Controllers\CsvImportController::class, 'insertBook');
$insertBook->setAccessible(true);
$importer = (new ReflectionClass(\App\Controllers\CsvImportController::class))->newInstanceWithoutConstructor();

/** @return array{id:int, stamp:?string, wanted:int} */
Expand Down
2 changes: 0 additions & 2 deletions tests/desiderata-csv-affirmative.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ function __(string $text, mixed ...$args): string
};

$parse = new ReflectionMethod(\App\Controllers\CsvImportController::class, 'parseCsvRow');
$parse->setAccessible(true);
$controller = (new ReflectionClass(\App\Controllers\CsvImportController::class))->newInstanceWithoutConstructor();

/** What the importer decides for a given cell value. */
Expand Down Expand Up @@ -98,7 +97,6 @@ function __(string $text, mixed ...$args): string
// with its values, which is indistinguishable from an import that simply had
// no such column. Both halves of the comparison must fold multibyte.
$map = new ReflectionMethod(\App\Controllers\CsvImportController::class, 'mapColumnHeaders');
$map->setAccessible(true);

$headerCases = [
'RECHERCHÉ' => 'is_desiderata',
Expand Down
4 changes: 0 additions & 4 deletions tests/desiderata-extended.integration.php
Original file line number Diff line number Diff line change
Expand Up @@ -639,10 +639,8 @@ public function sendEmail(string $to, string $subject, string $body, string $toN
// The class statics the plugin memoises. Left alone, the installation's
// plugin id and its libri_editori answer would be reused here.
$resolvedId = new ReflectionProperty(DesiderataPlugin::class, 'resolvedPluginId');
$resolvedId->setAccessible(true);
$resolvedId->setValue(null, null);
$junction = new ReflectionProperty(DesiderataPlugin::class, 'publisherJunction');
$junction->setAccessible(true);
$junction->setValue(null, null);

// The sandbox is EMPTIED, not relieved of a named list. It is shared with
Expand Down Expand Up @@ -856,10 +854,8 @@ public function sendEmail(string $to, string $subject, string $body, string $toN
// Restore the memoised statics for whatever runs next in this process.
try {
$resolved = new ReflectionProperty(DesiderataPlugin::class, 'resolvedPluginId');
$resolved->setAccessible(true);
$resolved->setValue(null, null);
$junctionMemo = new ReflectionProperty(DesiderataPlugin::class, 'publisherJunction');
$junctionMemo->setAccessible(true);
$junctionMemo->setValue(null, null);
} catch (\Throwable) {
// Reflection on a renamed property must not mask the real failure.
Expand Down
1 change: 0 additions & 1 deletion tests/desiderata-recaptcha.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,6 @@ public function sendEmail(string $to, string $subject, string $body, string $toN
// cannot be confused with the markup of a view somebody may be restyling.
$useSecret($secret);
$verify = new ReflectionMethod($plugin, 'verifyRecaptcha');
$verify->setAccessible(true);
$expected = __('Verifica reCAPTCHA fallita. Riprova.');
$unusableTokens = [
'missing altogether' => [],
Expand Down
1 change: 0 additions & 1 deletion tests/desiderata-return-path.unit.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ function __(string $text, mixed ...$args): string
};

$method = new ReflectionMethod(DesiderataPlugin::class, 'returnPath');
$method->setAccessible(true);
$returnPath = static fn(mixed $raw): mixed => $method->invoke(null, $raw);

/** Readable rendering of control bytes, so a failure line is diagnosable. */
Expand Down
Loading
Loading