Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b0280c4
fix(plugins): stop api-book-scraper wiping its API key on blank save;…
fabiodalez-dev Aug 23, 2026
b77883f
fix(plugins): route SRU + book-club AI through the guarded HttpClient…
fabiodalez-dev Aug 23, 2026
6da3fa2
fix(plugins): rate-limit resource-sync basic auth; require admin for …
fabiodalez-dev Aug 23, 2026
37965b4
fix(plugins): schema and correctness across archives, digital-library…
fabiodalez-dev Aug 23, 2026
09eb8a4
fix(plugins): align version metadata and drop stale max_app_version
fabiodalez-dev Aug 23, 2026
6fd3ca5
Wire up api-book-scraper plugin settings page
fabiodalez-dev Aug 23, 2026
814db6d
Wire UNIMARC import to complete the advertised round-trip (z39-server)
fabiodalez-dev Aug 23, 2026
dc8eaa8
Surface VIAF/ISNI authority endpoints in the author edit UI (#32)
fabiodalez-dev Aug 23, 2026
7f1ba1a
Wire FRBR/LRM book-to-Work linking and Expression CRUD into the admin UI
fabiodalez-dev Aug 23, 2026
701d650
Drop the orphaned relator table and de-advertise unbuilt FRBR features
fabiodalez-dev Aug 23, 2026
0ecf743
Make EspressioniRepository::create() null-safe on absent form fields
fabiodalez-dev Aug 23, 2026
74d755a
test(goodlib): make the custom-domains spec self-contained
fabiodalez-dev Aug 23, 2026
9176fa7
Harden plugin schema, hooks, and input guards from PR #376 review
fabiodalez-dev Aug 23, 2026
602ab46
fix(ci): drop redundant null-coalesce and point the discogs hook test…
fabiodalez-dev Aug 23, 2026
5776ccf
Make plugin hook registration and NCIP FK migration atomic and fail-loud
fabiodalez-dev Aug 23, 2026
024ecb4
fix(archives): preserve the reference_code prefix on soft-delete inst…
fabiodalez-dev Aug 23, 2026
3231c72
Harden HttpClient SSRF guard, centralise Dewey data-file resolution, …
fabiodalez-dev Aug 24, 2026
3081db4
chore(release): 0.7.66 — bump 5 plugin versions so ensureSchema/hooks…
fabiodalez-dev Aug 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
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,59 @@

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

## [0.7.66] - 2026-08-24

A full audit of all 20 bundled plugins — security, correctness and schema fixes — plus four previously-orphaned plugin features wired into the UI.

### Security

- **Centralised SSRF guard for caller-configurable endpoints**: `HttpClient` gains
an `ssrf_guard` option that resolves the target host, rejects hosts resolving to
private/reserved address space, pins the connection to the vetted public IP, and
rejects cross-host/port/scheme redirects. Enabled for the book-club AI endpoint
and the z39 SRU client, both of which also moved off raw `curl` onto the guarded
client.
- **book-club AI settings are admin-only**: the global API key and outbound
endpoint were reachable by `staff` (AdminAuthMiddleware admits both roles); an
inline admin re-check now gates both read and save.
- **resource-sync** Basic Auth is rate-limited; **dewey-editor** destructive
endpoints require admin inline; **digital-library** file-preview links get
`rel="noopener"`.

### Features

- **api-book-scraper settings page** is wired (`hasSettingsPage`/
`getSettingsViewPath`) — previously a 404.
- **z39-server UNIMARC import** completes the advertised round-trip: a pasted or
uploaded UNIMARC record pre-fills the book form (like the SBN import), never a
direct insert, so validation and soft-delete rules are preserved.
- **viaf-authority** surfaces a VIAF/ISNI panel on the author edit page via the
existing `author.form.fields` hook.
- **frbr-lrm** wires Book↔Work linking and full Expression CRUD into the admin UI
via `book.form.fields`; the manifest drops the unbuilt dedup/relator claims.

### Fixed

- **api-book-scraper no longer wipes its stored API key** on a settings save that
leaves the key field blank (blank means "keep the existing key").
- **archives soft-delete** frees the UNIQUE `reference_code`/`ark_identifier` for
reuse by appending a per-id token (truncated to fit `VARCHAR(64)`), keeping the
original code as a readable prefix.
- **dewey-editor** keys its data file off the full locale (`en_US` ≠ `en_GB`) with
a legacy fallback; readers and writers share one resolver.
- Plugin hook registration (api-book-scraper) is now transactional and rethrows on
failure; removed dead `activate()` code in discogs/open-library; aligned
plugin.json versions and dropped stale `max_app_version` fields.

### Upgrade

- The five plugins with schema or hook changes (ncip-server, digital-library,
frbr-lrm, viaf-authority, z39-server) get a plugin.json version bump so the
Updater re-runs their `ensureSchema()`/hook registration on existing installs —
the ncip `ncip_transactions` FOREIGN KEYs, digital-library's `file_url`/
`audio_url` columns and the new frbr/viaf hooks are applied on upgrade, not only
on fresh install. No core SQL migration.

## [0.7.65] - 2026-08-22

Add-to-calendar links in loan emails, the #366 legacy ready-pickup backfill, and a public-facing SEO pass.
Expand Down
19 changes: 5 additions & 14 deletions app/Controllers/DeweyApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use App\Support\DeweyDataFiles;
use App\Support\I18n;

class DeweyApiController
Expand All @@ -17,20 +18,10 @@ private function loadDeweyData(): array
$locale = $this->getActiveLocale();

if (!isset(self::$deweyDataCache[$locale])) {
// Prima prova a caricare il nuovo formato completo (con decimali)
$jsonFile = ($locale === 'en_US') ? 'dewey_completo_en.json' : 'dewey_completo_it.json';
$jsonPath = __DIR__ . '/../../data/dewey/' . $jsonFile;

// Fallback al vecchio formato se il completo non esiste
if (!file_exists($jsonPath)) {
$jsonFile = ($locale === 'en_US') ? 'dewey_en.json' : 'dewey.json';
$jsonPath = __DIR__ . '/../../data/dewey/' . $jsonFile;
}

// Fallback finale al file italiano
if (!file_exists($jsonPath)) {
$jsonPath = __DIR__ . '/../../data/dewey/dewey_completo_it.json';
}
// Prefer the full-locale file written by Dewey Editor, retain the
// language-prefix file as an upgrade fallback, then fall back to
// Italian when the selected locale has no Dewey dataset.
$jsonPath = DeweyDataFiles::resolveReadPath($locale, 'it_IT');

if (!file_exists($jsonPath)) {
throw new \Exception('Dewey JSON file not found');
Expand Down
11 changes: 9 additions & 2 deletions app/Controllers/PluginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,14 @@ public function updateSettings(Request $request, Response $response, array $args

// Save all settings
$this->pluginManager->setSetting($pluginId, 'api_endpoint', $apiEndpoint, true);
$this->pluginManager->setSetting($pluginId, 'api_key', $apiKey, true);
// Preserve the stored API key when the field is left blank: the admin
// modal always clears the key input on open and tells the admin that
// blank means "keep the existing key". Only overwrite when a new value
// is actually entered, otherwise a save that just toggles enabled or
// changes the timeout would silently wipe the key and disable scraping.
if ($apiKey !== '') {
$this->pluginManager->setSetting($pluginId, 'api_key', $apiKey, true);
}
$this->pluginManager->setSetting($pluginId, 'timeout', (string) $timeout, true);
$this->pluginManager->setSetting($pluginId, 'enabled', $enabled ? '1' : '0', true);

Expand All @@ -392,7 +399,7 @@ public function updateSettings(Request $request, Response $response, array $args
'message' => __('Impostazioni API Book Scraper salvate correttamente.'),
'data' => [
'api_endpoint' => $apiEndpoint !== '' ? 'saved' : 'empty',
'api_key' => $apiKey !== '' ? 'saved' : 'empty',
'api_key' => $apiKey !== '' ? 'saved' : 'preserved',
'timeout' => $timeout,
'enabled' => $enabled
]
Expand Down
52 changes: 11 additions & 41 deletions app/Support/DeweyAutoPopulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,6 @@ class DeweyAutoPopulator
'k10plus' => 'de', // K10plus is German
];

/**
* Map of locale codes to JSON file names
*/
private const LOCALE_FILES = [
'it' => 'dewey_completo_it.json',
'en' => 'dewey_completo_en.json',
];

private static string $dataDir = '';

/**
* Add or update a Dewey entry if missing
*
Expand All @@ -55,23 +45,24 @@ public static function addIfMissing(string $code, string $name, string $source =
}

// Get current app locale
$appLocale = self::getAppLocaleShort();
$appLocale = self::getAppLocale();
$appLanguage = substr($appLocale, 0, 2);

// Only update if source language matches app locale
if ($sourceLanguage !== $appLocale) {
if ($sourceLanguage !== $appLanguage) {
// Don't update - language mismatch
return false;
}

// Get JSON file for this locale
$jsonFile = self::getJsonFilePath($appLocale);
if (!$jsonFile || !file_exists($jsonFile)) {
$readPath = DeweyDataFiles::resolveReadPath($appLocale);
if (!file_exists($readPath)) {
error_log("[DeweyAutoPopulator] JSON file not found for locale: $appLocale");
return false;
}

// Load existing data
$data = self::loadJson($jsonFile);
$data = self::loadJson($readPath);
if ($data === null) {
return false;
}
Expand All @@ -90,7 +81,7 @@ public static function addIfMissing(string $code, string $name, string $source =
}

// Save back to file
return self::saveJson($jsonFile, $data);
return self::saveJson(DeweyDataFiles::canonicalPath($appLocale), $data);
}

/**
Expand All @@ -113,37 +104,16 @@ public static function processBookData(array $bookData): bool
}

/**
* Get short locale code (it, en, etc.)
* Get normalized full locale code (it_IT, en_US, etc.).
*/
private static function getAppLocaleShort(): string
private static function getAppLocale(): string
{
if (class_exists('\App\Support\I18n')) {
$locale = I18n::getLocale();
return substr($locale, 0, 2);
return I18n::normalizeLocaleCode(I18n::getLocale());
}

// Fallback to ENV
$locale = $_ENV['APP_LOCALE'] ?? 'it_IT';
return substr($locale, 0, 2);
}

/**
* Get JSON file path for a locale
*/
private static function getJsonFilePath(string $locale): ?string
{
$file = self::LOCALE_FILES[$locale] ?? null;
if (!$file) {
return null;
}

if (empty(self::$dataDir)) {
// Determine base path
$basePath = defined('BASE_PATH') ? BASE_PATH : dirname(__DIR__, 2);
self::$dataDir = $basePath . '/data/dewey';
}

return self::$dataDir . '/' . $file;
return I18n::normalizeLocaleCode((string) ($_ENV['APP_LOCALE'] ?? 'it_IT'));
}

/**
Expand Down
72 changes: 72 additions & 0 deletions app/Support/DeweyDataFiles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);

namespace App\Support;

/**
* Single source of truth for locale-aware Dewey JSON paths.
*
* New writes use the full locale (it_IT, en_US, ...). Reads prefer that file
* and fall back to the legacy language-only filename so upgrades remain
* compatible until the first write migrates a locale.
*/
final class DeweyDataFiles
{
public static function canonicalPath(string $locale): string
{
return self::dataDir() . '/dewey_completo_' . self::normalizeLocale($locale) . '.json';
}

public static function resolveReadPath(string $locale, ?string $fallbackLocale = null): string
{
$locale = self::normalizeLocale($locale);
$canonical = self::canonicalPath($locale);
if (is_file($canonical)) {
return $canonical;
}

$legacy = self::dataDir() . '/dewey_completo_' . substr($locale, 0, 2) . '.json';
if (is_file($legacy)) {
return $legacy;
}

// Very old installations used these pre-"completo" filenames. Keep
// the core API's historical last-resort compatibility while all new
// writes continue to migrate into the full-locale canonical file.
$language = substr($locale, 0, 2);
$historicName = match ($language) {
'it' => 'dewey.json',
'en' => 'dewey_en.json',
default => null,
};
if ($historicName !== null) {
$historic = self::dataDir() . '/' . $historicName;
if (is_file($historic)) {
return $historic;
}
}

if ($fallbackLocale !== null) {
return self::resolveReadPath($fallbackLocale);
}

return $canonical;
}

private static function dataDir(): string
{
$basePath = defined('BASE_PATH') ? BASE_PATH : dirname(__DIR__, 2);
return $basePath . '/data/dewey';
}

private static function normalizeLocale(string $locale): string
{
$locale = str_replace('-', '_', trim($locale));
if (preg_match('/^([A-Za-z]{2,3})(?:_([A-Za-z]{2}))?$/', $locale, $matches) !== 1) {
return 'it_IT';
}

$language = strtolower($matches[1]);
return isset($matches[2]) ? $language . '_' . strtoupper($matches[2]) : $language;
}
}
Loading