Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 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
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
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
12 changes: 8 additions & 4 deletions app/Support/RateLimiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ class RateLimiter
*/
public static function isLimited(string $identifier, int $maxAttempts = self::MAX_ATTEMPTS, int $window = self::WINDOW): bool
{
$currentTime = time();
// Elapsed-based window measurement: microtime(true) keeps the window a
// true float duration instead of quantizing the first-attempt marker to
// whole seconds. Same limits and same file-backed shared state — only
// the window boundary is finer-grained.
$currentTime = microtime(true);
$file = self::getStateFile($identifier);

// Open (or create) state file with exclusive lock for atomic read-modify-write
Expand All @@ -34,20 +38,20 @@ public static function isLimited(string $identifier, int $maxAttempts = self::MA

// Read current state under lock
$raw = stream_get_contents($handle);
$state = ['attempts' => 0, 'first_attempt' => 0];
$state = ['attempts' => 0, 'first_attempt' => 0.0];
if ($raw !== false && $raw !== '') {
$data = json_decode($raw, true);
if (is_array($data)) {
$state = [
'attempts' => (int) ($data['attempts'] ?? 0),
'first_attempt' => (int) ($data['first_attempt'] ?? 0),
'first_attempt' => (float) ($data['first_attempt'] ?? 0),
];
}
}

// Reset if window expired
if ($state['first_attempt'] > 0 && ($currentTime - $state['first_attempt']) > $window) {
$state = ['attempts' => 0, 'first_attempt' => 0];
$state = ['attempts' => 0, 'first_attempt' => 0.0];
}

if ($state['attempts'] === 0) {
Expand Down
72 changes: 71 additions & 1 deletion locale/da_DK.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{
"Importa UNIMARC (MARCXchange)": "Importér UNIMARC (MARCXchange)",
"Incolla un record UNIMARC/MARCXchange, oppure carica un file .xml, per precompilare il form dal record esportato.": "Indsæt en UNIMARC/MARCXchange-post, eller upload en .xml-fil, for at udfylde formularen ud fra den eksporterede post.",
"Incolla o carica un record UNIMARC.": "Indsæt eller upload en UNIMARC-post.",
"Dati importati da UNIMARC.": "Data importeret fra UNIMARC.",
"Record UNIMARC non valido o non riconosciuto.": "Ugyldig eller ukendt UNIMARC-post.",
"Nessun dato UNIMARC valido da importare.": "Ingen gyldige UNIMARC-data at importere.",
"Esiste già un utente con l'email %s. Se stai reinstallando su un database esistente, accedi con le credenziali esistenti oppure usa un'altra email.": "En bruger med e-mailadressen %s findes allerede. Hvis du geninstallerer på en eksisterende database, skal du logge ind med de eksisterende legitimationsoplysninger eller bruge en anden e-mailadresse.",
" libri": " bøger",
"\"%s\" prestato a %s è in ritardo di %d giorni": "\"%s\" udlånt til %s er %d dage forsinket",
Expand Down Expand Up @@ -6901,5 +6907,69 @@
"Tutte le opere di %s presenti in biblioteca: sfoglia i titoli disponibili e richiedili in prestito.": "Alle værker af %s i biblioteket: gennemse de tilgængelige titler og bestil dem til udlån.",
"Tutti i libri pubblicati da %s presenti nella nostra biblioteca, disponibili per il prestito.": "Alle bøger udgivet af %s i vores bibliotek, tilgængelige til udlån.",
"Esplora i libri del genere %s disponibili nella nostra biblioteca per il prestito.": "Udforsk bøgerne i genren %s, som kan lånes på vores bibliotek.",
"Pagina %d": "Side %d"
"Pagina %d": "Side %d",
"API key non valida": "Ugyldig API-nøgle",
"Chiave configurata": "Nøgle konfigureret",
"Chiave configurata — lascia vuoto per mantenere": "Nøgle konfigureret — lad feltet stå tomt for at beholde den",
"Codici Risposta HTTP": "HTTP-svarkoder",
"Configura il client per il servizio web di scraping dati libri tramite API personalizzata.": "Konfigurer klienten til webtjenesten til indsamling af bogdata via et brugerdefineret API.",
"Configurazione API": "API-konfiguration",
"Documentazione Completa:": "Fuld dokumentation:",
"Errore server": "Serverfejl",
"Formato Richiesta": "Anmodningsformat",
"Formato Risposta JSON (Campi Supportati)": "JSON-svarformat (understøttede felter)",
"Guida Rapida": "Hurtig guide",
"Ha priorità 3 (più alta di Open Library che ha priorità 5), quindi verrà interrogato per primo.": "Det har prioritet 3 (højere end Open Library, som har prioritet 5), så det forespørges først.",
"ISBN non trovato nel database": "ISBN blev ikke fundet i databasen",
"Impostazioni salvate correttamente!": "Indstillinger gemt!",
"L'API key viene criptata con AES-256-GCM prima di essere salvata nel database.": "API-nøglen krypteres med AES-256-GCM, før den gemmes i databasen.",
"L'API key viene criptata nel database e trasmessa tramite header HTTP sicuro.": "API-nøglen krypteres i databasen og sendes via en sikker HTTP-header.",
"Libro trovato, dati restituiti": "Bog fundet, data returneret",
"Mostra/nascondi chiave": "Vis/skjul nøgle",
"Per la guida completa su come implementare il server API, consulta il file": "Den komplette vejledning i, hvordan du implementerer API-serveren, finder du i filen",
"Questo plugin si collega a un servizio web esterno per recuperare automaticamente i dati dei libri durante la creazione o modifica.": "Dette plugin opretter forbindelse til en ekstern webtjeneste for automatisk at hente bogdata under oprettelse eller redigering.",
"Sicurezza:": "Sikkerhed:",
"Tempo massimo di attesa per la risposta dell'API. Consigliato: 10 secondi.": "Maksimal ventetid på API-svaret. Anbefalet: 10 sekunder.",
"Test non ancora implementato. Salva le impostazioni e prova a importare un libro.": "Testen er endnu ikke implementeret. Gem indstillingerne, og prøv at importere en bog.",
"Timeout Richiesta (secondi)": "Timeout for anmodning (sekunder)",
"Tutti i campi sono opzionali. Il plugin userà solo i campi presenti nella risposta.": "Alle felter er valgfrie. Pluginnet bruger kun de felter, der findes i svaret.",
"Usa il segnaposto {isbn} nell'URL; in alternativa l'ISBN sarà aggiunto come parametro ?isbn=...": "Brug pladsholderen {isbn} i URL'en; ellers tilføjes ISBN som parameteren ?isbn=...",
"Authority control VIAF / ISNI": "VIAF-/ISNI-autoritetskontrol",
"Collega l'autore al Virtual International Authority File (VIAF) e a ISNI. Cerca per nome per trovare un identificatore, oppure inseriscilo manualmente, e salva.": "Knyt forfatteren til Virtual International Authority File (VIAF) og til ISNI. Søg efter navn for at finde en identifikator, eller indtast den manuelt, og gem.",
"VIAF ID": "VIAF-id",
"es. 102333412": "f.eks. 102333412",
"Apri in VIAF": "Åbn i VIAF",
"ISNI": "ISNI",
"es. 0000 0001 2143 6543": "f.eks. 0000 0001 2143 6543",
"16 cifre (l'ultima può essere una X).": "16 cifre (det sidste kan være et X).",
"Cerca su VIAF": "Søg i VIAF",
"Salva VIAF / ISNI": "Gem VIAF / ISNI",
"Ricerca su VIAF…": "Søger i VIAF…",
"Nessun risultato trovato su VIAF.": "Ingen resultater fundet i VIAF.",
"Inserisci prima il nome dell'autore.": "Indtast forfatterens navn først.",
"Opera FRBR/LRM (Work)": "FRBR/LRM-værk (Work)",
"Collega questo libro a un'Opera per raggruppare tutte le sue edizioni sotto un unico record.": "Knyt denne bog til et værk for at samle alle dens udgaver under én post.",
"Salva prima il libro: potrai collegarlo a un'Opera dalla pagina di modifica.": "Gem bogen først: du kan knytte den til et værk fra redigeringssiden.",
"Collegato a": "Knyttet til",
"Vedi Opera": "Vis værk",
"Scollega": "Fjern tilknytning",
"Cerca un'Opera": "Søg efter et værk",
"Digita un titolo uniforme…": "Skriv en ensartet titel…",
"Crea una nuova Opera": "Opret et nyt værk",
"Ricerca in corso…": "Søger…",
"Nessuna Opera trovata.": "Intet værk fundet.",
"Collegamento aggiornato.": "Tilknytning opdateret.",
"Libro scollegato dall'Opera.": "Bogens tilknytning til værket er fjernet.",
"Testo (originale)": "Tekst (original)",
"Traduzione": "Oversættelse",
"Revisione": "Revision",
"Adattamento": "Bearbejdning",
"Edizione critica": "Kritisk udgave",
"Modifica espressione": "Rediger udtryk",
"Tipo di espressione": "Udtrykstype",
"Titolo dell'espressione": "Udtrykkets titel",
"Revisore": "Revisor",
"Salva espressione": "Gem udtryk",
"Eliminare questa espressione?": "Slet dette udtryk?",
"Aggiungi espressione": "Tilføj udtryk"
}
72 changes: 71 additions & 1 deletion locale/de_DE.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{
"Importa UNIMARC (MARCXchange)": "UNIMARC importieren (MARCXchange)",
"Incolla un record UNIMARC/MARCXchange, oppure carica un file .xml, per precompilare il form dal record esportato.": "Fügen Sie einen UNIMARC/MARCXchange-Datensatz ein oder laden Sie eine .xml-Datei hoch, um das Formular aus dem exportierten Datensatz vorauszufüllen.",
"Incolla o carica un record UNIMARC.": "Fügen Sie einen UNIMARC-Datensatz ein oder laden Sie ihn hoch.",
"Dati importati da UNIMARC.": "Daten aus UNIMARC importiert.",
"Record UNIMARC non valido o non riconosciuto.": "Ungültiger oder nicht erkannter UNIMARC-Datensatz.",
"Nessun dato UNIMARC valido da importare.": "Keine gültigen UNIMARC-Daten zum Importieren.",
"Esiste già un utente con l'email %s. Se stai reinstallando su un database esistente, accedi con le credenziali esistenti oppure usa un'altra email.": "Ein Benutzer mit der E-Mail-Adresse %s existiert bereits. Wenn Sie auf einer bestehenden Datenbank neu installieren, melden Sie sich mit den vorhandenen Zugangsdaten an oder verwenden Sie eine andere E-Mail-Adresse.",
" libri": " Bücher",
"\"%s\" prestato a %s è in ritardo di %d giorni": "\"%s\" ausgeliehen an %s ist %d Tage überfällig",
Expand Down Expand Up @@ -6901,5 +6907,69 @@
"Tutte le opere di %s presenti in biblioteca: sfoglia i titoli disponibili e richiedili in prestito.": "Alle Werke von %s in der Bibliothek: stöbern Sie in den verfügbaren Titeln und leihen Sie sie aus.",
"Tutti i libri pubblicati da %s presenti nella nostra biblioteca, disponibili per il prestito.": "Alle von %s veröffentlichten Bücher in unserer Bibliothek, zum Ausleihen verfügbar.",
"Esplora i libri del genere %s disponibili nella nostra biblioteca per il prestito.": "Entdecken Sie die Bücher des Genres %s, die in unserer Bibliothek zum Ausleihen verfügbar sind.",
"Pagina %d": "Seite %d"
"Pagina %d": "Seite %d",
"API key non valida": "Ungültiger API-Schlüssel",
"Chiave configurata": "Schlüssel konfiguriert",
"Chiave configurata — lascia vuoto per mantenere": "Schlüssel konfiguriert — leer lassen, um ihn beizubehalten",
"Codici Risposta HTTP": "HTTP-Antwortcodes",
"Configura il client per il servizio web di scraping dati libri tramite API personalizzata.": "Konfigurieren Sie den Client für den Webdienst zum Auslesen von Buchdaten über eine benutzerdefinierte API.",
"Configurazione API": "API-Konfiguration",
"Documentazione Completa:": "Vollständige Dokumentation:",
"Errore server": "Serverfehler",
"Formato Richiesta": "Anfrageformat",
"Formato Risposta JSON (Campi Supportati)": "JSON-Antwortformat (unterstützte Felder)",
"Guida Rapida": "Kurzanleitung",
"Ha priorità 3 (più alta di Open Library che ha priorità 5), quindi verrà interrogato per primo.": "Es hat Priorität 3 (höher als Open Library mit Priorität 5) und wird daher zuerst abgefragt.",
"ISBN non trovato nel database": "ISBN nicht in der Datenbank gefunden",
"Impostazioni salvate correttamente!": "Einstellungen erfolgreich gespeichert!",
"L'API key viene criptata con AES-256-GCM prima di essere salvata nel database.": "Der API-Schlüssel wird mit AES-256-GCM verschlüsselt, bevor er in der Datenbank gespeichert wird.",
"L'API key viene criptata nel database e trasmessa tramite header HTTP sicuro.": "Der API-Schlüssel wird in der Datenbank verschlüsselt und über einen sicheren HTTP-Header übertragen.",
"Libro trovato, dati restituiti": "Buch gefunden, Daten zurückgegeben",
"Mostra/nascondi chiave": "Schlüssel anzeigen/ausblenden",
"Per la guida completa su come implementare il server API, consulta il file": "Die vollständige Anleitung zur Implementierung des API-Servers finden Sie in der Datei",
"Questo plugin si collega a un servizio web esterno per recuperare automaticamente i dati dei libri durante la creazione o modifica.": "Dieses Plugin verbindet sich mit einem externen Webdienst, um Buchdaten beim Erstellen oder Bearbeiten automatisch abzurufen.",
"Sicurezza:": "Sicherheit:",
"Tempo massimo di attesa per la risposta dell'API. Consigliato: 10 secondi.": "Maximale Wartezeit auf die API-Antwort. Empfohlen: 10 Sekunden.",
"Test non ancora implementato. Salva le impostazioni e prova a importare un libro.": "Test noch nicht implementiert. Speichern Sie die Einstellungen und versuchen Sie, ein Buch zu importieren.",
"Timeout Richiesta (secondi)": "Anfrage-Timeout (Sekunden)",
"Tutti i campi sono opzionali. Il plugin userà solo i campi presenti nella risposta.": "Alle Felder sind optional. Das Plugin verwendet nur die in der Antwort vorhandenen Felder.",
"Usa il segnaposto {isbn} nell'URL; in alternativa l'ISBN sarà aggiunto come parametro ?isbn=...": "Verwenden Sie den Platzhalter {isbn} in der URL; andernfalls wird die ISBN als Parameter ?isbn=... angehängt.",
"Authority control VIAF / ISNI": "VIAF-/ISNI-Normdaten",
"Collega l'autore al Virtual International Authority File (VIAF) e a ISNI. Cerca per nome per trovare un identificatore, oppure inseriscilo manualmente, e salva.": "Verknüpfe den Autor mit dem Virtual International Authority File (VIAF) und mit ISNI. Suche nach Namen, um einen Bezeichner zu finden, oder gib ihn manuell ein und speichere.",
"VIAF ID": "VIAF-ID",
"es. 102333412": "z. B. 102333412",
"Apri in VIAF": "In VIAF öffnen",
"ISNI": "ISNI",
"es. 0000 0001 2143 6543": "z. B. 0000 0001 2143 6543",
"16 cifre (l'ultima può essere una X).": "16 Ziffern (die letzte kann ein X sein).",
"Cerca su VIAF": "In VIAF suchen",
"Salva VIAF / ISNI": "VIAF / ISNI speichern",
"Ricerca su VIAF…": "Suche in VIAF…",
"Nessun risultato trovato su VIAF.": "Keine Ergebnisse in VIAF gefunden.",
"Inserisci prima il nome dell'autore.": "Gib zuerst den Namen des Autors ein.",
"Opera FRBR/LRM (Work)": "FRBR/LRM-Werk (Work)",
"Collega questo libro a un'Opera per raggruppare tutte le sue edizioni sotto un unico record.": "Verknüpfen Sie dieses Buch mit einem Werk, um alle seine Ausgaben unter einem Datensatz zu gruppieren.",
"Salva prima il libro: potrai collegarlo a un'Opera dalla pagina di modifica.": "Speichern Sie zuerst das Buch: Sie können es dann auf der Bearbeitungsseite mit einem Werk verknüpfen.",
"Collegato a": "Verknüpft mit",
"Vedi Opera": "Werk anzeigen",
"Scollega": "Trennen",
"Cerca un'Opera": "Werk suchen",
"Digita un titolo uniforme…": "Einheitstitel eingeben…",
"Crea una nuova Opera": "Neues Werk erstellen",
"Ricerca in corso…": "Suche läuft…",
"Nessuna Opera trovata.": "Kein Werk gefunden.",
"Collegamento aggiornato.": "Verknüpfung aktualisiert.",
"Libro scollegato dall'Opera.": "Buch vom Werk getrennt.",
"Testo (originale)": "Text (Original)",
"Traduzione": "Übersetzung",
"Revisione": "Überarbeitung",
"Adattamento": "Bearbeitung",
"Edizione critica": "Kritische Ausgabe",
"Modifica espressione": "Expression bearbeiten",
"Tipo di espressione": "Expressionstyp",
"Titolo dell'espressione": "Titel der Expression",
"Revisore": "Bearbeiter",
"Salva espressione": "Expression speichern",
"Eliminare questa espressione?": "Diese Expression löschen?",
"Aggiungi espressione": "Expression hinzufügen"
}
Loading