-
Notifications
You must be signed in to change notification settings - Fork 2
Add LibraryThing plugin support and improve CSV import/export #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
* feat: add pre-update and post-install patch systems to Updater Add remote patch system to fix bugs without manual file updates: - Pre-update patches: fix Updater bugs before update runs - Post-install patches: apply hotfixes, cleanup, SQL after install Both systems use SHA256 checksum verification and graceful 404 fallback. * fix: improve reservations system consistency and data integrity - Align admin reservations with user reservations (date derivation) - Uniform copy state management (da_ritirare flow) - Add transaction safety to avoid nested transactions - Add date range validation (defensive check) - Add HTTP 403 logging and str_replace uniqueness in Updater - Add updater.md to .gitignore (internal documentation) * fix: check UPDATE execute() result in ReservationsAdminController::update() * chore: bump version to 0.4.6 and update plugin compatibility - Updated version.json to 0.4.6 - Updated max_app_version in all tracked plugins
- Remove .gitattributes from .gitignore (needs to be tracked) - Add export-ignore for public/installer/assets symlink - Prevents update failures on older Updater versions
- LoanApprovalController: validate pre-assigned copy before global slot counting to prevent false rejections when slots are at capacity but the loan already has a valid assigned copy - ReservationsAdminController: add date format validation for dataPrenotazione and dataScadenza in store method (matching update method) - Updater: improve path protection to also block deletion of files with protected basename in subdirectories (e.g., subdir/.env)
- MaintenanceService.checkExpiredPickups: don't reset copies in perso/danneggiato/manutenzione states to 'disponibile' (non-restorable) - DataIntegrity: only mark copy as 'prestato' for in_corso/in_ritardo states, not for prenotato with reached date. With da_ritirare flow, the physical copy stays 'disponibile' until pickup is confirmed.
…ter-improvements fix: improve loan approval logic and add security hardening
- Update version.json to 0.4.7 - Update max_app_version in all 6 bundled plugins
…0.4.6/v0.4.7 notes
Adds a new method that correctly handles semicolons inside quoted strings (e.g., CSS inline styles like style="padding: 20px; margin: 10px"). This prevents migration SQL files from breaking when they contain email templates with CSS.
- Remove router.php (PHP dev server file) - Remove update-0.4.4.md (temp documentation) - Add router.php to .gitignore
- Add phpstan.neon configuration for app/, installer/, cron/, public/ - Create phpstan-stubs.php for helper functions (__(), __n(), etc.) - Generate baseline for 282 View template variable errors - Add phpstan-report.json to .gitignore
PHPStan requires explicit \mysqli and \Exception in namespaced code. No runtime behavior change.
Support sprintf-style arguments for consistency with app translation function. Fixes PHPStan arguments.count errors.
- Mailer: use empty string for SMTPSecure when no encryption - RateLimitMiddleware: use Stream constructor instead of non-existent create()
Replace non-existent generate() and generateToken() calls.
Clean up closures that declared use (\$app) but never used the variable.
- ReservationManager: complete array shape for reservation parameter - public/index.php: type hint for ErrorHandler return value
Includes what each fix influences and how to test it.
Correct the list of routes where unused use ($app) was removed. The previous list included routes that actually use $app.
Updated the example code in PHPSTAN_FIXES.md to match the real implementation in installer/index.php, which uses: - static $translations variable for caching - $_SESSION['app_locale'] for locale detection - $message variable (not $translated) - Lazy loading of translations from JSON file
- Upgrade PHPStan analysis level from 3 to 4 - Remove 6 unused methods detected by level 4 analysis: - columnExists() from AutoriApiController - createSlug(), render() from FrontendController - searchUsers() from SearchController - redirect() from UpdateController - findAvailableCopy() from ReservationReassignmentService - Remove 2 unused properties: - $db from NotificationsController - $copyRepo from ReservationReassignmentService - Regenerate baseline (now 341 entries for View/template variables) - Update PHPSTAN_FIXES.md documentation Note: RateLimitMiddleware::$maxAttempts flagged but NOT removed - it's a design bug (accepts config but never uses it), not dead code.
- Fix rate limit response: JSON contains error/message, not retry_after (Retry-After is an HTTP header, not a JSON field) - Fix namespace prefix description: unqualified class names in namespaced code DO cause runtime errors, not "no behavior change"
Staff members were incorrectly shown as "Admin" in the OPAC interface. Now displays role-appropriate label based on user type. Fixes #27
- Add ESLint 9.x with flat config (eslint.config.js) - Configure for browser/jQuery/Webpack environment - Add npm lint script for manual analysis - Fix empty catch block warning in index.js - Add eslint, @eslint/js, globals as dev dependencies
- Install ESLint 9.x with flat config format - Configure globals for browser, jQuery, DataTables, Swal, Chart.js - Add npm lint script for frontend JS files - Fix empty catch block warning in index.js
feat(lint): add ESLint for JavaScript static analysis
- Add ESLint for JavaScript static analysis - Update plugin compatibility to 0.4.7.1 - Regenerate plugin ZIP files
TinyMCE was loading before main.bundle.js, causing initialization to fail with visibility:hidden on textarea. Moving TinyMCE to load AFTER all other scripts (vendor, main, csrf-helper, swal-config) ensures the DOM and other components are fully initialized first. This fixes the issue where editor.initialized = false and container not visible, preventing toolbar from rendering.
TinyMCE with defer cannot auto-detect base_url from script tag, causing initialization to hang after setup() without loading assets. Added explicit configuration: - base_url: '/assets/tinymce' - suffix: '.min' This allows TinyMCE to correctly load themes, skins, and plugins. Also restored original router.php from backup with proper handling of /public/, /uploads/, and /installer/ paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@app/Views/libri/partials/book_form.php`:
- Around line 3916-3996: Rimuovi tutti i console.log/console.error presenti
nell'implementazione di initBookTinyMCE e nelle callback/promise di tinymce.init
(inclusi i log dentro init_instance_callback, setup, then/catch e nel setTimeout
di controllo manuale), oppure sostituiscili con chiamate condizionali che usano
un flag globale controllabile (es. window.DEBUG_MODE) così che i messaggi
vengano stampati solo se il flag è true; assicurati di mantenere eventuali log
critici d'errore visibili solo quando DEBUG_MODE è attivo o di inviarli a un
logger centralizzato, e non modificare la logica di retry che usa
tinyMceInitAttempts e TINYMCE_MAX_RETRIES né il comportamento di tinymce.init.
🧹 Nitpick comments (1)
app/Views/libri/partials/book_form.php (1)
811-814: Escapare$groupNameper consistenza e sicurezza.Anche se
$fieldGroupsè definito staticamente nel codice PHP, è buona pratica escapare sempre l'output per prevenire XSS e mantenere consistenza con il resto del file.♻️ Proposta di fix
<?php foreach ($fieldGroups as $groupName => $fields): ?> <div class="border border-gray-200 rounded-lg p-3 bg-gray-50"> - <h4 class="text-sm font-semibold text-gray-700 mb-2"><?= $groupName ?></h4> + <h4 class="text-sm font-semibold text-gray-700 mb-2"><?= HtmlHelper::e($groupName) ?></h4>
Remove extensive debug code while preserving functional TinyMCE initialization with base_url and suffix configuration. Changes: - Remove debug asset checking function - Remove CSP violation listeners - Remove global TinyMCE event logging - Remove all console.log statements - Remove promise then/catch debug handlers - Remove manual timeout checks at 2s and 5s - Simplify retry logic (keep functionality, remove logging)
Apply HTML escaping to $groupName output for consistency and defense-in-depth security, even though values are statically defined.
Transform LibraryThing section into a collapsible accordion to: - Reduce visual clutter in the book form - Keep extended fields hidden by default - Provide smooth expand/collapse animation - Maintain full accessibility (ARIA attributes) Changes: - Convert card-header to clickable button with chevron icon - Add smooth CSS transitions (300ms) - Implement toggleLibraryThingAccordion() JS function - Add proper ARIA attributes for screen readers - Collapse by default to save space
Add explicit width styling and remove default button styles to ensure the accordion header button occupies the full row width. Changes: - Add border-0 and bg-transparent classes - Add inline style display: flex; width: 100% - Ensures button behaves like full-width block element
Replace select dropdown with star-rating.js library for better UX. Uses the already-installed star-rating.js v4.3.1 package. Changes: - Add data-star-rating attribute to select element - Include star-rating.min.css and star-rating.min.js - Initialize StarRating with clearable option - Update option labels to use "stella/stelle" format - Maintains form compatibility (select still submits value) Benefits: - Visual consistency with frontend - Interactive star selection - Click to rate, click again to clear - Native library usage (no custom code)
Removed administrative/metadata fields from hardcoded private fields list to allow user control via visibility checkboxes. Changes: - Reduced $privateFields array from 10 to 5 fields - Kept only privacy-sensitive fields: private_comment, lending_* - Removed: source, from_where, bcid, oclc, work_id - These metadata fields now respect lt_fields_visibility checkbox settings This allows fields like "fonte venditore" (source) to be displayed in the frontend when the visibility checkbox is enabled in the backend.
|
Created idempotent migration that consolidates all LibraryThing schema requirements: Database Changes: - 25+ LibraryThing fields (review, rating, comment, classifications, dates, etc.) - lt_fields_visibility JSON column for frontend visibility control - 8 performance indexes (rating, date_read, lending_status, identifiers) - Rating constraint (1-5 or NULL, MySQL 8.0.16+) Features: - Fully idempotent using prepared statements with INFORMATION_SCHEMA checks - Can be safely run multiple times without errors - Compatible with MySQL 5.7+ and MariaDB 10.0+ - Comprehensive documentation in migrations/README.md Documentation: - Migration principles and best practices - Idempotency patterns with code examples - Testing and rollback procedures - Complete migration history - Troubleshooting guide This migration ensures all LibraryThing functionality works regardless of whether the plugin was installed programmatically or via migrations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@installer/database/migrations/migrate_0.4.7.sql`:
- Around line 230-234: The migration uses JSON for the
libri.lt_fields_visibility column which breaks on MariaDB 10.0/10.1; update the
migration so it either raises the minimum MariaDB requirement to 10.2.7+ or
implements a version-aware fallback that creates the column as JSON when the
server version >= 10.2.7 and as LONGTEXT (or TEXT) otherwise. Modify the
dynamic-SQL block that builds `@sql` (the PREPARE/EXECUTE sequence around `@sql` and
stmt) to detect the server version (via VERSION() or @@version) and choose the
appropriate column type string for ALTER TABLE `libri` ADD COLUMN
`lt_fields_visibility` ... so the migration succeeds on older MariaDB servers.
Ensure the column comment and NULLability remain the same for both types.
In `@installer/database/migrations/README.md`:
- Around line 18-20: Il blocco di codice fenced che mostra `migrate_X.Y.Z.sql`
non specifica il linguaggio (usa solo ```), causando l'avviso MD040; modifica il
blocco per specificare la lingua (ad es. `text` o `plaintext`) subito dopo i
backtick in modo che la riga contenente `migrate_X.Y.Z.sql` sia racchiusa in
```text (o ```plaintext) ... ``` e risolva il warning di markdownlint.
Security Headers: - Updated CSP in .htaccess and index.php to allow cdnjs.cloudflare.com - Added frame-src, child-src directives for data/blob/about URIs - Required for star-rating.js and other CDN-hosted libraries TinyMCE Updates: - Updated model.min.js to latest version - Added index.js and model.js (unminified sources) - Improves editor stability and features Cleanup: - Removed librarything.tsv test file (sample data no longer needed)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@public/.htaccess`:
- Line 32: The Content-Security-Policy header in the Header always set
Content-Security-Policy line currently includes 'unsafe-eval' in script-src;
remove 'unsafe-eval' from the script-src directive while leaving 'unsafe-inline'
intact (since the codebase relies on inline scripts) by updating that Header
line in public/.htaccess accordingly; keep the existing data:/blob:/about:
allowances in frame-src/child-src as-is and consider a future refactor to use
nonces or hashes for inline scripts to eliminate the need for 'unsafe-inline'.
In `@public/index.php`:
- Around line 346-351: Update the misleading comment and tighten the $csp
string: change the comment above the $csp declaration (currently "Tutti gli
asset (font inclusi) sono self-hosted: nessuna dipendenza esterna") to reflect
that external CDNs are used, and remove the insecure http:// entries from the
CSP (specifically drop http://fonts.googleapis.com and http://fonts.gstatic.com
from the "style-src" and "font-src" directives); leave or consolidate the
allowed HTTPS CDN hosts (e.g. https://cdnjs.cloudflare.com,
https://fonts.googleapis.com, https://fonts.gstatic.com) in the $csp string so
only secure (HTTPS) sources are permitted.
Database Migration (migrate_0.4.7.sql): - Add MariaDB 10.0/10.1 compatibility for lt_fields_visibility column - Implement version-aware fallback: JSON for MariaDB 10.2.7+/MySQL 5.7.8+, LONGTEXT for older versions - Detect server version via @@Version and dynamically choose column type - Maintains same NULL-ability and comment for both types Documentation (README.md): - Fix MD040 markdownlint warning by specifying language for fenced code block - Changed ``` to ```text for migrate file format example Security Headers (.htaccess): - Remove 'unsafe-eval' from script-src directive in CSP - Keep 'unsafe-inline' (required by codebase inline scripts) - Maintain data:/blob:/about: in frame-src/child-src (required for maps) Security Headers (index.php): - Update misleading comment about self-hosted assets to reflect CDN usage - Remove insecure HTTP URLs from CSP (http://fonts.googleapis.com, http://fonts.gstatic.com) - Remove 'unsafe-eval' from script-src (not used in application code) - Keep only HTTPS CDN sources (https://cdnjs.cloudflare.com, https://fonts.googleapis.com, https://fonts.gstatic.com) All changes improve security posture while maintaining backward compatibility.
Improvements: - Add comprehensive PHPDoc block at top of view file - Document all variables passed from controller ($book, $authors, $reviewStats, etc.) - Specify array types and nullable types for proper static analysis Results: - PHPStan errors reduced from 20 to 6 (70% reduction) - Eliminated all "Variable might not be defined" warnings - Remaining 6 warnings are about unnecessary null coalescing operators (pre-existing) - Our LibraryThing visibility modifications remain clean (0 warnings) This improves code maintainability and helps developers understand the view's data contract.
Problem: - Scraping works in book form but not during LibraryThing TSV import - User has checkbox enabled but no covers are downloaded - Need to diagnose if scraping plugins are loaded during import Debug logging added: - Check if 'scrape.fetch.custom' hook is available before scraping - Log warning if hook is missing (plugins not loaded) - Suggest which plugins need to be active This will help identify if the issue is: 1. Plugins not loaded during chunked import 2. Hook registration timing issue 3. Plugin activation state Next steps: Test import and check storage/logs/app.log for hook status.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@app/Controllers/LibraryThingImportController.php`:
- Around line 133-169: The upload currently accepts ['tsv','csv','txt'] but the
parsing always uses a tab delimiter (fgetcsv(..., "\t", ...)), so CSV/TXT files
will fail; either (A) restrict allowed extensions to only 'tsv' by updating the
extension check and error message where $extension is validated, or (B) detect
the delimiter after opening the file (read the first non-BOM line, inspect for
"\t" vs "," vs ";"), store that delimiter in a variable and pass it into the
subsequent fgetcsv calls and any chunk parsing logic (where fopen, $headers =
fgetcsv(..., "\t", ...), and isLibraryThingFormat($headers) are used) so parsing
matches the uploaded file. Ensure the chosen change updates the error response
text and any downstream parsing that relies on a hardcoded "\t".
In `@app/Views/frontend/book-detail.php`:
- Around line 1711-1744: The view currently treats a NULL lt_fields_visibility
as an empty array, hiding all LT fields; fix by falling back to the default
visibility set so fields with data are shown. Either (A) add
lt_fields_visibility JSON to LIBRARYTHING_FIELDS_DEFAULTS (the constant used
when creating new books) so new records default to all-visible, or (B) change
the view logic in book-detail.php where $ltVisibility is computed to detect
NULL/empty and instead populate $ltVisibility from
\App\Support\LibraryThingInstaller::getLibraryThingFields() (or a dedicated
visibility-default map) so the foreach over $ltVisibility will include all
fields by default; keep the existing privateFields checks and existing label
lookup via getLibraryThingFields().
In `@installer/database/migrations/migrate_0.4.7.sql`:
- Around line 234-241: La condizione che imposta `@is_old_mariadb` è troppo grezza
e marca come "vecchio" MySQL 5.x (incluso 5.7.8+) e non esclude MariaDB 10.2.7+;
aggiorna la logica che popolates `@is_old_mariadb` (usata per determinare
`@col_type` e la costruzione di `@sql` per la tabella `libri` / colonna
`lt_fields_visibility`) per considerare "vecchie" solo MySQL < 5.7.8 e MariaDB
10.0/10.1 e 10.2.0–10.2.6: modifica il controllo versione (la regex/condizione
su @@version) per riconoscere MySQL 5.7.0–5.7.7 come legacy e lasciar fuori
5.7.8+; analogamente estendi il pattern MariaDB per escludere 10.2.7+; dopo la
modifica `@col_type` dovrà rimanere 'LONGTEXT' solo per quelle versioni rilevate
come vecchie e 'JSON' altrimenti.
🧹 Nitpick comments (1)
app/Views/frontend/book-detail.php (1)
1753-1823: Riduci la duplicazione della logica di rendering dei campi LT.
Lo stesso blocco di formattazione è duplicato per entrambe le colonne; rischia divergenze future e rende la manutenzione più costosa. Consiglio di estrarre un renderer unico (closure o partial) e riutilizzarlo.♻️ Proposta di refactor
@@ - <div class="details-column"> + <?php + $renderLtValue = function(string $fieldName, array $field): string { + if ($fieldName === 'rating') { + $rating = max(0, min(5, (int)$field['value'])); + $stars = ''; + for ($i = 1; $i <= 5; $i++) { + $stars .= $i <= $rating + ? '<i class="fas fa-star text-warning"></i>' + : '<i class="far fa-star text-muted"></i>'; + } + return $stars; + } + if (in_array($fieldName, ['date_started', 'date_read', 'lending_start', 'lending_end'], true)) { + $timestamp = strtotime($field['value']); + return ($timestamp && $timestamp > 0) + ? htmlspecialchars(date('d/m/Y', $timestamp), ENT_QUOTES, 'UTF-8') + : '-'; + } + if ($fieldName === 'value') { + return '€ ' . number_format((float)$field['value'], 2); + } + if (in_array($fieldName, ['review', 'comment'], true)) { + return \App\Support\HtmlHelper::sanitizeHtml(nl2br($field['value'], false)); + } + return htmlspecialchars((string)$field['value'], ENT_QUOTES, 'UTF-8'); + }; + ?> + <div class="details-column"> <?php foreach ($column1 as $fieldName => $field): ?> <div class="meta-item"> <div class="meta-label"><?= htmlspecialchars($field['label']) ?></div> <div class="meta-value"> - <?php if (in_array($fieldName, ['rating'])): ?> - <?php - $rating = (int)$field['value']; - for ($i = 1; $i <= 5; $i++): - if ($i <= $rating): - echo '<i class="fas fa-star text-warning"></i>'; - else: - echo '<i class="far fa-star text-muted"></i>'; - endif; - endfor; - ?> - <?php elseif (in_array($fieldName, ['date_started', 'date_read', 'lending_start', 'lending_end'])): ?> - <?php - $timestamp = strtotime($field['value']); - echo ($timestamp && $timestamp > 0) - ? htmlspecialchars(date('d/m/Y', $timestamp)) - : '-'; - ?> - <?php elseif (in_array($fieldName, ['value'])): ?> - € <?= number_format((float)$field['value'], 2) ?> - <?php elseif (in_array($fieldName, ['review', 'comment'])): ?> - <div class="prose prose-sm"><?= \App\Support\HtmlHelper::sanitizeHtml(nl2br($field['value'], false)) ?></div> - <?php else: ?> - <?= htmlspecialchars($field['value']) ?> - <?php endif; ?> + <?= $renderLtValue($fieldName, $field) ?> </div> </div> <?php endforeach; ?> </div> @@ <div class="details-column"> <?php foreach ($column2 as $fieldName => $field): ?> <div class="meta-item"> <div class="meta-label"><?= htmlspecialchars($field['label']) ?></div> <div class="meta-value"> - <?php if (in_array($fieldName, ['rating'])): ?> - <?php - $rating = (int)$field['value']; - for ($i = 1; $i <= 5; $i++): - if ($i <= $rating): - echo '<i class="fas fa-star text-warning"></i>'; - else: - echo '<i class="far fa-star text-muted"></i>'; - endif; - endfor; - ?> - <?php elseif (in_array($fieldName, ['date_started', 'date_read', 'lending_start', 'lending_end'])): ?> - <?php - $timestamp = strtotime($field['value']); - echo ($timestamp && $timestamp > 0) - ? htmlspecialchars(date('d/m/Y', $timestamp)) - : '-'; - ?> - <?php elseif (in_array($fieldName, ['value'])): ?> - € <?= number_format((float)$field['value'], 2) ?> - <?php elseif (in_array($fieldName, ['review', 'comment'])): ?> - <div class="prose prose-sm"><?= \App\Support\HtmlHelper::sanitizeHtml(nl2br($field['value'], false)) ?></div> - <?php else: ?> - <?= htmlspecialchars($field['value']) ?> - <?php endif; ?> + <?= $renderLtValue($fieldName, $field) ?> </div> </div> <?php endforeach; ?> </div>
| // Validazione estensione file | ||
| $filename = $uploadedFile->getClientFilename(); | ||
| $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); | ||
| if (!in_array($extension, ['tsv', 'csv', 'txt'], true)) { | ||
| $response->getBody()->write(json_encode([ | ||
| 'success' => false, | ||
| 'error' => __('Il file deve avere estensione .tsv, .csv o .txt') | ||
| ], JSON_THROW_ON_ERROR)); | ||
| return $response->withHeader('Content-Type', 'application/json')->withStatus(400); | ||
| } | ||
|
|
||
| // Save to temporary location | ||
| $tmpDir = sys_get_temp_dir() . '/librarything_imports'; | ||
| if (!is_dir($tmpDir)) { | ||
| mkdir($tmpDir, 0755, true); | ||
| } | ||
|
|
||
| $importId = uniqid('lt_', true); | ||
| $savedPath = $tmpDir . '/' . $importId . '.tsv'; | ||
| $uploadedFile->moveTo($savedPath); | ||
|
|
||
| // Validate format and count rows | ||
| try { | ||
| $file = fopen($savedPath, 'r'); | ||
| if (!$file) { | ||
| throw new \Exception(__('Impossibile aprire il file')); | ||
| } | ||
|
|
||
| // Skip BOM | ||
| $bom = fread($file, 3); | ||
| if ($bom !== "\xEF\xBB\xBF") { | ||
| rewind($file); | ||
| } | ||
|
|
||
| // Read and validate headers | ||
| $headers = fgetcsv($file, 0, "\t", '"', ""); | ||
| if (!$headers || !$this->isLibraryThingFormat($headers)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Estensioni accettate non allineate al parsing TSV.
Accetti .csv e .txt, ma leggi sempre con delimitatore \t, quindi un CSV “vero” fallirà con errore formato. O restringi alle sole TSV, oppure salva un delimitatore rilevato e riusalo nel chunk.
🔧 Fix minimale (restringere a .tsv)
- if (!in_array($extension, ['tsv', 'csv', 'txt'], true)) {
+ if (!in_array($extension, ['tsv'], true)) {
$response->getBody()->write(json_encode([
'success' => false,
- 'error' => __('Il file deve avere estensione .tsv, .csv o .txt')
+ 'error' => __('Il file deve avere estensione .tsv')
], JSON_THROW_ON_ERROR));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}🤖 Prompt for AI Agents
In `@app/Controllers/LibraryThingImportController.php` around lines 133 - 169, The
upload currently accepts ['tsv','csv','txt'] but the parsing always uses a tab
delimiter (fgetcsv(..., "\t", ...)), so CSV/TXT files will fail; either (A)
restrict allowed extensions to only 'tsv' by updating the extension check and
error message where $extension is validated, or (B) detect the delimiter after
opening the file (read the first non-BOM line, inspect for "\t" vs "," vs ";"),
store that delimiter in a variable and pass it into the subsequent fgetcsv calls
and any chunk parsing logic (where fopen, $headers = fgetcsv(..., "\t", ...),
and isLibraryThingFormat($headers) are used) so parsing matches the uploaded
file. Ensure the chosen change updates the error response text and any
downstream parsing that relies on a hardcoded "\t".
| -- Detect if we're on MariaDB 10.0/10.1 or MySQL < 5.7.8 | ||
| SET @is_old_mariadb = (@@version REGEXP '^(MariaDB )?10\\.(0|1)\\.' OR @@version REGEXP '^5\\.'); | ||
| SET @col_type = IF(@is_old_mariadb, 'LONGTEXT', 'JSON'); | ||
|
|
||
| -- Build ALTER statement with version-appropriate column type | ||
| SET @sql = IF(@col_exists = 0, | ||
| CONCAT('ALTER TABLE `libri` ADD COLUMN `lt_fields_visibility` ', @col_type, | ||
| ' NULL COMMENT ''Frontend visibility preferences for LibraryThing fields'' AFTER `condition_lt`'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cd installer/database/migrations && cat -n migrate_0.4.7.sql | sed -n '230,250p'Repository: fabiodalez-dev/Pinakes
Length of output: 1201
🌐 Web query:
MySQL JSON data type support version history
💡 Result:
- Before MySQL 5.7.8: No native
JSONdata type (JSON typically stored asTEXT/VARCHAR). - MySQL 5.7.8 (Aug 2015): Introduced the native
JSONdata type (stored in an internal binary format with automatic validation) and the initial set of server-side JSON functions. [1] - MySQL 5.7.22 (Apr 19, 2018): Added JSON aggregation functions
JSON_ARRAYAGG()andJSON_OBJECTAGG()(and other JSON-related additions such asJSON_PRETTY()mentioned in the same release notes). [2] - MySQL 8.0.4 (Jan 23, 2018): Added
JSON_TABLE()(SQL/JSON table function to map JSON to relational rows/columns). [3] - MySQL 8.0 (general): Expanded JSON capabilities further; for example, MySQL 8.0 supports expressions as default values for
JSONcolumns (which was not allowed previously). [4]
Sources:
[1] MySQL 5.7 Reference Manual – “As of MySQL 5.7.8, MySQL supports a native JSON data type” https://dev.mysql.com/doc/refman/5.7/en/json.html
[2] MySQL 5.7 Release Notes (5.7.22) – JSON_ARRAYAGG/JSON_OBJECTAGG (and other JSON additions) https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-22.html
[3] MySQL 8.0 Release Notes (8.0.4) – Added JSON_TABLE() https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-4.html
[4] MySQL 8.0 Reference Manual (“What Is New in MySQL 8.0”) – JSON_TABLE in 8.0.4; default expressions for JSON supported in 8.0 https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html
🌐 Web query:
MariaDB JSON type support version history
💡 Result:
MariaDB’s “JSON type” support is mostly compatibility syntax (not a separate binary JSON storage type like MySQL). Version history:
- Before 10.2.x: No
JSONcolumn type; JSON typically stored inTEXT/LONGTEXTand handled/validated manually. - MariaDB 10.2.3: JSON functions were added (e.g.,
JSON_VALID(),JSON_EXTRACT(), etc.). [2] - MariaDB 10.2.7: The
JSONdata type name was introduced, but it is an alias forLONGTEXT(withutf8mb4_bincollation), mainly for MySQL compatibility (replication/mysqldump interoperability). [1] [3] - MariaDB 10.4.3: Tables created with the
JSONalias automatically include aCHECK (JSON_VALID(...))constraint to enforce valid JSON on insert/update. [4]
Key point: Even in current MariaDB, JSON is stored as text (LONGTEXT), not as a dedicated binary JSON type. [3]
Sources: MariaDB 10.2.7 release notes and MariaDB documentation pages on JSON / JSON_VALID. [1] [3] [4]
Affina il rilevamento della versione MySQL per evitare il downgrade non necessario a LONGTEXT.
La regex '^5\.' identifica come "vecchio" tutto MySQL 5.x, incluso 5.7.8+ che supporta nativamente JSON. Di conseguenza, versioni come 5.7.8, 5.7.9 e successive vengono forzate a LONGTEXT nonostante il JSON sia disponibile. Se il codice utilizzerà funzioni JSON in futuro, questa restrizione causerà errori. Distingi almeno MySQL 5.7.0–5.7.7 (senza JSON) da 5.7.8+ (con JSON), e aggiorna anche il rilevamento di MariaDB per escludere 10.2.0–10.2.6 dal downgrade.
Modifica proposta
--- a/installer/database/migrations/migrate_0.4.7.sql
+++ b/installer/database/migrations/migrate_0.4.7.sql
@@ -234,7 +234,9 @@
-- Detect if we're on MariaDB 10.0/10.1 or MySQL < 5.7.8
-SET `@is_old_mariadb` = (@@version REGEXP '^(MariaDB )?10\\.(0|1)\\.' OR @@version REGEXP '^5\\.');
-SET `@col_type` = IF(`@is_old_mariadb`, 'LONGTEXT', 'JSON');
+-- Detect if we're on MariaDB < 10.2.7 or MySQL < 5.7.8
+SET `@is_old_mariadb` = (@@version REGEXP '^(MariaDB )?10\\.(0|1)\\.' OR @@version REGEXP '^(MariaDB )?10\\.2\\.[0-6]([^0-9]|$)');
+SET `@is_old_mysql` = (@@version NOT LIKE '%MariaDB%' AND (@@version REGEXP '^5\\.[0-6]\\.' OR @@version REGEXP '^5\\.7\\.[0-7]([^0-9]|$)'));
+SET `@col_type` = IF(`@is_old_mariadb` OR `@is_old_mysql`, 'LONGTEXT', 'JSON');🤖 Prompt for AI Agents
In `@installer/database/migrations/migrate_0.4.7.sql` around lines 234 - 241, La
condizione che imposta `@is_old_mariadb` è troppo grezza e marca come "vecchio"
MySQL 5.x (incluso 5.7.8+) e non esclude MariaDB 10.2.7+; aggiorna la logica che
popolates `@is_old_mariadb` (usata per determinare `@col_type` e la costruzione di
`@sql` per la tabella `libri` / colonna `lt_fields_visibility`) per considerare
"vecchie" solo MySQL < 5.7.8 e MariaDB 10.0/10.1 e 10.2.0–10.2.6: modifica il
controllo versione (la regex/condizione su @@version) per riconoscere MySQL
5.7.0–5.7.7 come legacy e lasciar fuori 5.7.8+; analogamente estendi il pattern
MariaDB per escludere 10.2.7+; dopo la modifica `@col_type` dovrà rimanere
'LONGTEXT' solo per quelle versioni rilevate come vecchie e 'JSON' altrimenti.
- Expand ALLOWED_DOMAINS whitelist to include 24+ domains: - Google Books international domains (it, co.uk) - Additional Amazon CDN domains (m.media-amazon.com, images-amazon.com) - Goodreads CDN (d/i/s.gr-assets.com) - Italian bookstores (IBS, Ubik) - International bookstores (Barnes & Noble, Bookshop.org) - Improve LibraryThing import scraping reliability: - Remove verbose debug logging for production - Simplify error handling with silent failures - Maintain exponential backoff retry logic (1s, 2s, 4s) This resolves cover download failures during LibraryThing TSV imports where scraping succeeded but cover downloads were blocked by domain whitelist restrictions.
**Critical Issues Fixed:** - Fix incorrect include path in LibraryThingImportController (/../Views/ vs /../../Views/) - Align LibraryThing format validation across CsvImportController and LibraryThingImportController - Use consistent columns: ['Book Id', 'Title', 'ISBNs'] (2/3 required) - Add case-insensitive comparison for better compatibility **Major Issues Fixed:** - Populate missing export fields in LibriController - Add dewey_wording and entry_date to LibraryThing TSV export - Prevent data loss during export/re-import cycle - Improve DDL error handling in LibraryThingInstaller - Add explicit error checking for DROP INDEX/CHECK operations - Only ignore "doesn't exist" errors (errno 1091) - Throw exceptions for other DDL failures **Already Correct (verified):** - CSRF protection on /import/librarything/chunk endpoint - executeOrFail() return value handling - ISBN normalization in CSV import - Transaction handling in import process This resolves all Critical and Major issues identified in CodeRabbit review.
**Issues Fixed:** 1. Fix incorrect Sort Character mapping (LibraryThingImportController.php:557) - BEFORE: Mapped 'Sort Character' to 'sottotitolo' (subtitle) - AFTER: Empty (Sort Character is for alphabetical sorting, not subtitle) - Impact: Prevents semantic data corruption during import 2. Add safe default for $libraryThingInstalled (book_form.php:585) - BEFORE: !empty($libraryThingInstalled) - generates PHP notice if undefined - AFTER: !empty($libraryThingInstalled ?? false) - safe fallback - Impact: Eliminates PHP notices, improves robustness **Verified Already Correct (16 issues):** - Whitelist validation for lt_visibility fields - Rating range validation (1-5) - empty() vs null checks for numeric values - AJAX error handling (response.ok checks) - CSRF token generation - ISBN normalization - Transaction handling - And 9 others... All high-priority Minor issues resolved or verified correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@app/Controllers/CsvImportController.php`:
- Around line 352-407: The mapping erroneously includes "sort character" under
the 'sottotitolo' variations in mapColumnHeaders, which causes LibraryThing's
sort-character values to be imported as subtitles; remove "sort character" from
the 'sottotitolo' array in the $columnMapping inside mapColumnHeaders and, if
you need to preserve that field, add a new canonical key like 'sort_character'
(or similar) to $columnMapping with variations ['sort
character','sort_char','sortchar','sort key'] so those inputs map to the correct
canonical field instead of 'sottotitolo'.
In `@app/Support/ScrapingService.php`:
- Around line 22-55: The parameter $context in scrapeBookData is unused—either
remove it or use it for a single log when all attempts fail; update the
scrapeBookData function to log one warning after the retry loop (referencing
$isbn, $maxAttempts and $context) when no result is returned from the
scrape.fetch.custom hook (or remove the $context parameter from the method
signature and all callers if you prefer); ensure the log call is placed once
after the for-loop failure path and uses your app logger (e.g., Log::warning or
\App\Logger) to include context, ISBN and that scrape.fetch.custom returned
empty/failed.
🧹 Nitpick comments (3)
app/Controllers/CoverController.php (1)
158-262: Riduci duplicazione tradownload()edownloadFromUrl().La validazione e la pipeline di salvataggio sono quasi identiche; estrarre un helper privato riduce il rischio di divergenze future.
app/Controllers/LibriController.php (2)
46-77: Evita drift tra defaults LT e definizioni dell’installer.Consiglierei di derivare i defaults da
LibraryThingInstaller::getLibraryThingFields()(es. helper che faarray_fill_keys(...)) così la lista resta sincronizzata quando i campi cambiano.
1893-2003: Valuta il riuso diScrapingServiceinfetchCover()per coerenza.Così riduci l’accoppiamento con
ScrapeControllere il plumbing PSR‑7; se ti serve distinguereinvalid_isbn, puoi mappare i casi sul risultato del servizio.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@app/Controllers/CsvImportController.php`:
- Around line 362-371: La chiave 'publication' presente in $columnMapping come
alias di 'editore' impedisce a parseLibraryThingFormat() di sovrascrivere
correttamente il campo (LibraryThing mette "Città, Editore, Anno" in
Publication); rimuovi 'publication' dall'array associato a 'editore' dentro la
variabile $columnMapping oppure, se preferisci mantenere l'alias, modifica
parseLibraryThingFormat() per forzare l'override di 'editore' quando viene
rilevato il formato LibraryThing (usa il nome della variabile $columnMapping e
la funzione parseLibraryThingFormat() per localizzare il cambiamento).
In `@app/Controllers/LibriController.php`:
- Around line 2841-2846: The export currently uses $libro['ean'] for the
"Barcode" column and drops $libro['barcode']; update the row formatter in
LibriController (the array building that includes $libro['lccn'],
$libro['data_acquisizione'], etc.) to use $libro['barcode'] when present and
fall back to $libro['ean'] (e.g. use the value of $libro['barcode'] ??
$libro['ean'] ?? '') so existing plugin-provided barcode data is preserved.
In `@app/Views/libri/partials/book_form.php`:
- Around line 612-614: The label's for attribute ("rating") doesn't match the
select's id ("book-rating"), breaking click/focus and assistive tech; update
either the label's for to "book-rating" or the select's id to "rating" so they
match (adjust the label element and/or the select element where the <label
for="rating"> and <select id="book-rating" name="rating" data-star-rating> are
defined), ensuring the visible text stays unchanged.
🧹 Nitpick comments (2)
app/Support/ScrapingService.php (1)
22-45: Parametro$contextinutilizzato: usarlo nei log o rimuoverlo.
È passato dai caller ma qui non viene mai usato. Se è voluto per diagnosi, usalo almeno in un log su fallimento; altrimenti rimuovi il parametro per evitare warning/ambiguità.♻️ Opzione: log contestuale solo su errore
} catch (\Throwable $e) { - // Silent failure, will retry + error_log("[Scraping][$context] Tentativo {$attempt} fallito: " . $e->getMessage()); }app/Controllers/CoverController.php (1)
158-262: Duplicazione tradownload()edownloadFromUrl(): valutare helper condiviso.
Le due routine replicano validazione, download e salvataggio. Per evitare divergenze, estrai una funzione privata che ritorna{filename, file_url}e riusa in entrambi i percorsi.
CsvImportController.php: - Remove 'publication' from editore mapping to allow LibraryThing parsing - Remove 'sort character' from sottotitolo mapping (semantic fix) - Add dedicated 'sort_character' canonical field with variations LibriController.php: - Use barcode field with fallback to ean in export (preserve plugin data) - Exclude placeholder.jpg from "already has cover" check in fetchCover() - Include placeholder.jpg in syncCovers() query for batch download ScrapingService.php: - Add logging for scraping failures using $context parameter - Track $lastError throughout retry attempts - Log comprehensive failure info (context, ISBN, attempts, error) book_form.php: - Fix label for attribute to match select id (rating → book-rating) - Improves accessibility for screen readers and click-to-focus .gitignore: - Add *.tsv and *.csv protection with installer/docs exceptions - Prevent data export files from being committed
2859321 to
73d9b3c
Compare
- Check HTTP response status before parsing JSON - Display actual error messages instead of generic 'Connection error' - Handle authentication errors gracefully - Add fallback error messages for better UX The issue was not in the backend merge logic but in the frontend error handling. Now users will see the real error message (e.g., 'Authentication required') instead of 'Connection error'. Fixes #45
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
docs/stampa_etichette.MD (2)
170-170:⚠️ Potential issue | 🟡 MinorRefuso: “bibilioteche” → “biblioteche”.
Piccola correzione ortografica.
301-301:⚠️ Potential issue | 🟡 MinorArticolo errato: “Una etichetta” → “Un’etichetta”.
Migliora la scorrevolezza del testo.docs/SEO_IMPLEMENTATION_TESTING.md (1)
123-128:⚠️ Potential issue | 🟡 MinorRimuovere lo spazio dentro i delimitatori di enfasi.
Le righe con** Pass Criteria:**generano MD037; eliminare lo spazio iniziale in tutte le occorrenze.🛠️ Fix suggerito (applicare a tutte le occorrenze)
-** Pass Criteria:** +**Pass Criteria:**Also applies to: 141-145, 162-167, 176-183, 216-221, 235-240, 254-258, 295-300, 314-319, 329-334, 343-347, 357-361, 373-377, 384-389, 405-410, 427-431, 448-452, 463-467, 481-485, 493-498, 505-509, 526-530, 714-721
app/Controllers/PrestitiController.php (1)
353-410:⚠️ Potential issue | 🟠 MajorVerificare l'esito di execute() prima di procedere con gli aggiornamenti.
Se l'INSERT fallisce,
execute()ritornafalsema il codice prosegue comunque con l'aggiornamento della copia e il commit, lasciando lo stato del database incoerente (UPDATE orfana di INSERT).Fix proposto
- $stmt->execute(); - $newLoanId = (int) $db->insert_id; - $stmt->close(); + if (!$stmt->execute()) { + $err = $stmt->error; + $stmt->close(); + throw new \RuntimeException('Errore inserimento prestito: ' . $err); + } + $newLoanId = (int) $db->insert_id; + $stmt->close();app/Routes/web.php (1)
2468-2591:⚠️ Potential issue | 🟠 MajorAggiungere validazione dello schema e ricerca DNS sui redirect del proxy immagini.
Il codice consente redirect HTTP e non ri-valida la risoluzione DNS per l'host finale. Un server può reindirizzare verso
http://example.com(bypassando il vincolo HTTPS) o verso un hostname che si risolve a indirizzi privati, consentendo SSRF. Occorre convalidare che il schema finale sia HTTPS, ricercare DNS sul final host, e limitare i protocolli CURL.🛡️ Proposta di fix
curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; BibliotecaBot/1.0) Safari/537.36', CURLOPT_HTTPHEADER => [ 'Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8' ], ]); $img = curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $finalUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); curl_close($ch); if ($img === false || $httpCode !== 200) { return $response->withStatus(404); } // Re-validate final URL after redirects to prevent SSRF via redirect if ($finalUrl && $finalUrl !== $url) { $finalParts = parse_url($finalUrl); $finalHost = strtolower($finalParts['host'] ?? ''); + $finalScheme = strtolower($finalParts['scheme'] ?? ''); + + // Ensure final URL is still HTTPS + if ($finalScheme !== 'https') { + return $response->withStatus(403); + } // Check if redirect went to private network if (in_array($finalHost, $blockedHosts, true)) { return $response->withStatus(403); } foreach ($privatePatterns as $pattern) { if (preg_match($pattern, $finalHost)) { return $response->withStatus(403); } } + + // Re-validate DNS for final host (public IPs only) + $finalIps = `@gethostbynamel`($finalHost) ?: []; + $finalAaaa = `@dns_get_record`($finalHost, DNS_AAAA) ?: []; + if (!$finalIps && !$finalAaaa) { + return $response->withStatus(403); + } + foreach ($finalIps as $ip) { + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) { + return $response->withStatus(403); + } + } + foreach ($finalAaaa as $record) { + $ipv6 = $record['ipv6'] ?? null; + if ($ipv6 && filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) { + return $response->withStatus(403); + } + } }
🤖 Fix all issues with AI agents
In `@app/Controllers/LibraryThingImportController.php`:
- Around line 909-977: La proprietà entry_date viene parsata in
parseLibraryThingRow() ma non salvata: nelle query UPDATE/INSERT LT aggiungi la
colonna entry_date nello statement SQL, inserisci il relativo valore in $params
(usando !empty($data['entry_date']) ? $data['entry_date'] : null) e aggiorna la
stringa $types corrispondente per includere il tipo corretto (es. 's' o altro
coerente con gli altri campi); ripeti questa stessa modifica sia nel blocco
UPDATE (dove è definito $stmt e $params) sia nel blocco INSERT corrispondente
per assicurare la persistenza di entry_date.
In `@app/Controllers/LibriApiController.php`:
- Around line 255-265: The code uses $_SERVER['HTTP_HOST'] directly to build
absolute URLs ($cover) which allows Host header poisoning; update the HTTP_HOST
fallback in LibriApiController to validate and sanitize the host before
concatenation: read and trim $_SERVER['HTTP_HOST'], verify it against the same
hostname/hostname:port regex used elsewhere in the codebase (use the existing
regex constant/pattern you already have), ensure it only contains a safe
hostname and optional numeric port, and only then build $cover from $protocol .
'://' . $host . $cover; if validation fails, fall back to leaving $cover
unchanged or use a safe default base URL and consider logging the invalid host.
Ensure you reference and reuse the existing validation routine/pattern rather
than inventing a new one.
In `@app/Middleware/RateLimitMiddleware.php`:
- Around line 34-44: The current RateLimitMiddleware block assumes fopen() and
json_encode() succeed; validate both results before using them: check that $body
= fopen('php://temp', 'r+') !== false and that $payload = json_encode([...]) !==
false (or use json_encode with JSON_THROW_ON_ERROR and catch), and if either
fails, log the error (use the middleware's logger) and build a safe fallback
response body (e.g. a simple hard-coded minimal JSON string or an empty temp
stream) so you never pass false into fwrite() or new \Slim\Psr7\Stream; then
continue to rewind($body) and return the 429 response with headers as before.
Ensure checks reference the variables $body, $payload (or the json result),
fwrite, rewind, and \Slim\Psr7\Stream in RateLimitMiddleware so failures are
handled deterministically.
In `@app/Views/libri/partials/book_form.php`:
- Around line 602-605: The hidden accordion content with id
"librarything-accordion-content" is still focusable via Tab because only CSS
max-height/opacity are changed; update the accordion toggle logic to make the
container non-interactive when closed by setting container.inert = true and
container.style.pointerEvents = 'none' (and keep aria-hidden="true"), and when
opening remove inert, restore pointerEvents, and set aria-hidden="false" and
move focus into a sensible first focusable element; apply the same change to the
other accordion container referenced in the same file (the second accordion
block around lines 931-960) so both toggle handlers add/remove inert +
pointer-events and manage aria-hidden and focus consistently.
In `@cron/automatic-notifications.php`:
- Around line 64-80: Move the offending use statements so they appear before any
executable statements: place "use Dotenv\Dotenv;" and "use
App\Support\NotificationService;" immediately after the file's opening (and
after any declare/namespace if present) and before require_once/other code
(e.g., before autoloader load and Dotenv instantiation) to avoid PHP parse
errors from having use declarations after executable code.
In `@cron/full-maintenance.php`:
- Around line 68-85: Move the two misplaced use statements so they appear
immediately after the file-level declare(strict_types=1); and before any
executable code (e.g. before require_once and the process lock logic);
specifically relocate use Dotenv\Dotenv and use App\Support\MaintenanceService
to that top section so PSR-12 is satisfied and no use occurs after executable
statements like require_once or the lock handling.
In `@data/dewey/dewey_completo_it.json`:
- Around line 7453-7480: Nel ramo con i codici 914.9, 914.96 e 914.960 ci sono
campi "name" vuoti: aggiorna i record con quei codici (914.9, 914.96, 914.960)
inserendo titoli descrittivi appropriati coerenti con 914.9604 ("GEOGRAFIA.
PENISOLA BALCANICA. Viaggi"), oppure, se preferibile, appiattisci la struttura
rimuovendo i nodi intermedi vuoti e collegando direttamente 914.9604 come figlio
di 914.9; assicurati che ogni oggetto abbia un campo "name" non vuoto e che la
gerarchia "children" rimanga consistente dopo la modifica.
In `@docs/frontend/prenotazioni.md`:
- Around line 16-46: The fenced ASCII diagram block lacks a language identifier;
update the opening fence from ``` to ```text to satisfy markdownlint and ensure
the diagram is treated as plain text (locate the ASCII diagram block beginning
with ``` and change it to ```text).
In `@docs/modifica_libro.MD`:
- Line 441: Fix the typo in the section heading "## Modifche Frequenti - Casi
Reali" by changing "Modifche" to "Modifiche" so the heading reads "## Modifiche
Frequenti - Casi Reali"; update the heading text in docs/modifica_libro.MD where
that exact heading string appears.
- Around line 644-646: The footer uses bold markers with extra spaces inside
(e.g., "** Documento aggiornato: 19 Ottobre 2025**") which breaks Markdown
rendering; update each footer line to remove spaces adjacent to asterisks so the
emphasis markers are directly adjacent to text (for example change "** Documento
aggiornato: 19 Ottobre 2025**" to "**Documento aggiornato: 19 Ottobre 2025**",
similarly adjust "** Versione: 3.0.0 - Guida Completa Estesa**" and "** Per
utenti finali della biblioteca**").
In `@docs/PLUGIN_EXAMPLES.md`:
- Line 7: Section headings use two spaces after the numbered period (e.g., "##
1. **") which triggers markdownlint warnings; update each heading to have a
single space after the period (e.g., "## 1. **") by replacing the "## <n>. **"
pattern with "## <n>. **" across all section headers (examples include the
heading text "Advanced Analytics Dashboard" and any other "## <number>. **..."
headings) to remove the extra space and fix the "Spaces inside emphasis markers"
warnings.
In `@docs/scheda_libro.MD`:
- Line 388: Correggi il refuso nella frase "Le nuove etichette userannno il
formato configurato": sostituisci la stringa "userannno" con la corretta
"useranno" in modo che la riga legga "Le nuove etichette useranno il formato
configurato".
- Around line 497-498: The footer contains invalid Markdown emphasis due to
spaces inside the bold markers; update the two footer lines that currently read
"** Documento aggiornato: 19 Ottobre 2025**" and "** Versione: 1.0.0 - Guida
Scheda Libro**" to remove the leading space inside the ** markers (e.g., change
to "**Documento aggiornato: 19 Ottobre 2025**" and "**Versione: 1.0.0 - Guida
Scheda Libro**") so they render correctly.
In `@docs/stampa_etichette.MD`:
- Line 50: La frase che descrive il pulsante "Stampa Etichetta" contiene un
placeholder vuoto "(icona )"; sostituisci il placeholder con il nome o la
descrizione dell'icona effettiva usata nell'interfaccia (es. "(icona stampante)"
o "(icona label)") oppure rimuovi completamente "(icona )" se non c'è icona,
aggiornando la stringa che contiene "Clicca il bottone \"Stampa Etichetta\"" nel
documento.
In `@frontend/eslint.config.js`:
- Around line 51-58: Update the ignores array in the ESLint configuration:
replace the root-only pattern '*.min.js' with a recursive glob '**/*.min.js' so
that the ignores array (the ignores entry) excludes minified JS files in all
subdirectories rather than only the project root.
In `@frontend/package.json`:
- Around line 60-61: Il campo "type": "module" in package.json causa parse error
perché forza tutti i .js come ESM while webpack.config.js, postcss.config.js e
tailwind.config.js use CommonJS; either rimuovi "type": "module" from
package.json or rename those CommonJS config files to .cjs (webpack.config.cjs,
postcss.config.cjs, tailwind.config.cjs) so Node treats them as CommonJS, or
alternatively convert eslint.config.js, webpack.config.js, postcss.config.js and
tailwind.config.js to consistent ESM syntax (import/export) — pick one approach
and apply it consistently across eslint.config.js, webpack.config.js,
postcss.config.js and tailwind.config.js.
🧹 Nitpick comments (19)
app/Controllers/PasswordController.php (1)
62-66: LGTM! Gestione corretta dell'eccezione con suggerimento opzionale.La gestione dell'eccezione è appropriata per la sicurezza. Considera di loggare l'eccezione per facilitare il debugging in produzione:
try { $baseUrl = $this->getValidatedBaseUrl(); } catch (\RuntimeException $e) { + error_log('Password reset failed: ' . $e->getMessage()); return $response->withHeader('Location', RouteTranslator::route('forgot_password') . '?error=config')->withStatus(302); }app/Support/IcsGenerator.php (1)
190-205: Codice morto: il casependentenon verrà mai raggiunto.Il docblock a linea 190 è stato aggiornato rimuovendo
pendentedalla lista degli stati validi, ma il match statement a linea 201 contiene ancora il case'pendente'. PoichégetLoanTitleè un metodo privato chiamato solo dafetchEvents, e la query SQL a linea 109 non include piùpendente, questo case è ora codice morto.♻️ Rimozione del codice morto
private function getLoanTitle(string $status, string $bookTitle): string { $prefix = match($status) { 'in_corso' => '📖 ' . __('Prestito'), 'da_ritirare' => '📦 ' . __('Da Ritirare'), 'prenotato' => '📋 ' . __('Prestito Programmato'), 'in_ritardo' => '⚠️ ' . __('Prestito Scaduto'), - 'pendente' => '⏳ ' . __('Richiesta Pendente'), default => '📖 ' . __('Prestito') }; return $prefix . ': ' . $bookTitle; }app/Views/profile/reservations.php (1)
694-694: Inconsistenza nell'escaping del token CSRF.Questa linea usa
Csrf::ensureToken()senzahtmlspecialchars(), mentre la linea 511 applica l'escaping. Per coerenza stilistica nel file, considera di uniformare l'approccio.♻️ Suggerimento per uniformare l'escaping
- <input type="hidden" name="csrf_token" value="<?php echo Csrf::ensureToken(); ?>"> + <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(Csrf::ensureToken(), ENT_QUOTES, 'UTF-8'); ?>">app/Views/prestiti/crea_prestito.php (2)
329-350: Considerare il ripristino dello stato del checkbox quando si torna alla data odierna.Quando l'utente seleziona una data futura, il checkbox viene deselezionato e nascosto. Se poi torna alla data odierna, il container viene mostrato ma il checkbox rimane deselezionato. Questo potrebbe non essere l'esperienza utente attesa.
♻️ Proposta per ripristinare lo stato checked quando il container diventa visibile
function updateConsegnaImmediataVisibility(dateStr) { if (!consegnaContainer || !consegnaCheckbox) return; const today = formatDate(new Date()); const isImmediate = dateStr <= today; if (isImmediate) { // Show checkbox for immediate loans (today or past - though past shouldn't happen) consegnaContainer.classList.remove('hidden'); + // Restore default checked state when showing + consegnaCheckbox.checked = true; } else { // Hide checkbox for future loans - they always go through da_ritirare flow consegnaContainer.classList.add('hidden'); consegnaCheckbox.checked = false; // Uncheck when hidden } }
349-350: Verifica del timing di inizializzazione.La funzione
updateConsegnaImmediataVisibilityviene chiamata alla riga 350 prima che flatpickr sia inizializzato (riga 386). Questo funziona perché usa direttamentedataPrestitoEl.value, ma se in futuro il valore iniziale dovesse provenire da flatpickr, potrebbe causare problemi.Considerare di spostare l'inizializzazione dopo
fpPrestito = flatpickr(...)per garantire coerenza:fpPrestito = flatpickr(dataPrestitoEl, fpConfig); + + // Initialize visibility on page load (after flatpickr init) + updateConsegnaImmediataVisibility(dataPrestitoEl.value); // Initialize flatpickr for data_scadenza...E rimuovere la chiamata anticipata alla riga 350.
docs/review.MD (3)
7-26: Considera di aggiungere il file corrente alla tabella.La tabella elenca tutti i file della cartella
docs/, ma mancareview.MDstesso. Per completezza, potresti aggiungere una riga:| `review.MD` | Guida alla revisione della documentazione del progetto. |
30-36: Considera di estendere i passaggi per includere la documentazione dei plugin.Dato che la PR introduce il sistema LibraryThing con 25 campi aggiuntivi, nuovi endpoint API e formati di import/export, potrebbe essere utile aggiungere un passaggio specifico per la revisione della documentazione dei plugin. Ad esempio, dopo il punto 4, potresti aggiungere:
5. **Documenta le estensioni dei plugin**: se sono stati aggiunti o modificati plugin, assicurati di documentare i nuovi campi, gli endpoint API esposti, i formati di import/export supportati e le dipendenze.E rinumerare i punti successivi di conseguenza.
40-45: Estendi la checklist con voci specifiche per plugin e import/export.Considerando l'introduzione del plugin LibraryThing e delle funzionalità di import/export CSV estese, la checklist potrebbe beneficiare di voci aggiuntive come:
🔍 Voci suggerite per la checklist
- [ ] Documenta le procedure di installazione/disinstallazione dei nuovi plugin. - [ ] Verifica che i nuovi campi del database siano descritti in `libri.MD`. - [ ] Aggiorna `api.MD` con i nuovi endpoint esposti dai plugin. - [ ] Documenta i formati di import/export supportati (CSV, TSV, LibraryThing). - [ ] Verifica che le mappature multi-lingua dei campi siano documentate.Questo renderebbe la checklist più completa e allineata con le funzionalità introdotte nella PR.
frontend/eslint.config.js (1)
10-27: Globals$ejQueryridondanti.
globals.jquery(linea 12) include già$ejQuery. Le dichiarazioni manuali alle linee 19-20 sono superflue.♻️ Rimozione della ridondanza
...globals.commonjs, // Webpack/Node globals require: 'readonly', module: 'readonly', __dirname: 'readonly', // Project globals - $: 'readonly', - jQuery: 'readonly', DataTable: 'readonly', flatpickr: 'readonly', Swal: 'readonly', Chart: 'readonly', Sortable: 'readonly', bootstrap: 'readonly',app/Views/layout.php (1)
714-714: Considera di aggiungere il parametro di versione per il cache busting.Lo script
tinymce.min.jsnon ha il parametro di versione?v=<?= $appVersion ?>a differenza degli altri asset nella pagina. Questo potrebbe causare problemi di cache dopo gli aggiornamenti.♻️ Suggerimento per il cache busting
- <script src="/assets/tinymce/tinymce.min.js" defer></script> + <script src="/assets/tinymce/tinymce.min.js?v=<?= $appVersion ?>" defer></script>.gitignore (1)
162-169: Ok ignorare export CSV/TSV.
Se in futuro servissero fixture versionate altrove, valutare eccezioni mirate.docs/utenti.md (1)
38-38: Spazio extra dopo il simbolo blockquote.C'è uno spazio extra dopo
>che potrebbe causare problemi di rendering in alcuni parser markdown.📝 Correzione proposta
-> **Importante**: Se non ricevi l'email entro 5 minuti, controlla la cartella **Spam** o Posta Indesiderata. +> **Importante**: Se non ricevi l'email entro 5 minuti, controlla la cartella **Spam** o Posta Indesiderata.Lo stesso problema si presenta alla linea 98.
app/Controllers/CoverController.php (1)
166-262: Ridurre la duplicazione con un helper condiviso.La logica di validazione/resize/salvataggio è praticamente identica a
download(). Per evitare divergenze future, estrarre un metodo privato (es.saveCoverFromImageData) e riusarlo in entrambi i flussi.♻️ Diff proposta (uso helper in downloadFromUrl)
- if (strlen($imageData) > 5 * 1024 * 1024) { - throw new \RuntimeException(__('File troppo grande. Dimensione massima 5MB.')); - } - ... - return [ - 'filename' => $filename, - 'file_url' => $fileUrl - ]; + return $this->saveCoverFromImageData($imageData);Esempio di helper da aggiungere (da richiamare anche in
download()):private function saveCoverFromImageData(string $imageData): array { // contiene tutta la logica comune: size check, getimagesize, // resize, save, chmod, build file_url, return array }app/Controllers/LoanApprovalController.php (1)
171-233: Aggiungere filtro libro per la copia pre‑assegnata.Per difesa da dati inconsistenti, conviene garantire che la copia appartenga allo stesso libro.
♻️ Diff proposta
- $existingCopyStmt = $db->prepare(" - SELECT c.id FROM copie c - WHERE c.id = ? + $existingCopyStmt = $db->prepare(" + SELECT c.id FROM copie c + WHERE c.id = ? AND c.libro_id = ? AND c.stato NOT IN ('perso', 'danneggiato', 'manutenzione') AND NOT EXISTS ( SELECT 1 FROM prestiti p WHERE p.copia_id = c.id AND p.attivo = 1 AND p.id != ? AND p.stato IN ('in_corso', 'prenotato', 'da_ritirare', 'in_ritardo') AND p.data_prestito <= ? AND p.data_scadenza >= ? ) "); - $existingCopyStmt->bind_param('iiss', $existingCopiaId, $loanId, $dataScadenza, $dataPrestito); + $existingCopyStmt->bind_param('iiiss', $existingCopiaId, $libroId, $loanId, $dataScadenza, $dataPrestito);app/Controllers/ReservationsAdminController.php (1)
164-168: Gestire l’esito direcalculateBookAvailability()per evitare fallimenti silenziosi.
Se fallisce, oggi non resta traccia; un log migliorerebbe l’affidabilità operativa.💡 Proposta di aggiornamento
if ($libroId > 0) { $integrity = new \App\Support\DataIntegrity($db); - $integrity->recalculateBookAvailability($libroId); + if (!$integrity->recalculateBookAvailability($libroId)) { + error_log("[ReservationsAdminController] recalculateBookAvailability failed for libro_id={$libroId}"); + } } @@ if ($stmt->execute()) { $stmt->close(); // Recalculate book availability after new reservation $integrity = new \App\Support\DataIntegrity($db); - $integrity->recalculateBookAvailability($libroId); + if (!$integrity->recalculateBookAvailability($libroId)) { + error_log("[ReservationsAdminController] recalculateBookAvailability failed for libro_id={$libroId}"); + }Also applies to: 295-298
docs/installation.MD (1)
270-270: Piccolo refuso: "ad usare" → "a usare".La "d" eufonica si usa preferibilmente quando la vocale successiva è la stessa (es. "ad andare", "ed ecco"). In questo caso "a usare" è la forma corretta.
✏️ Fix proposto
-**Sei pronto!** Inizia subito ad usare Pinakes! +**Sei pronto!** Inizia subito a usare Pinakes!README.md (1)
84-98: Considera l'aggiunta del language identifier ai blocchi di flusso.I blocchi che mostrano il flusso degli stati prestito potrebbero beneficiare di
textoplaintextcome identificatore per evitare warning di linting.✏️ Fix proposto
**Loan State Flow:** -``` +```text Request → [pending] → Approval → [ready_for_pickup] → Confirm Pickup → [active]**For future-dated loans:** -``` +```text [pending] → [reserved] → (date reached) → [ready_for_pickup] → Confirm → [active]app/Views/libri/index.php (1)
403-428: Memory leak: listener duplicati sudocument.La funzione
setupDropdownaggiunge un nuovodocument.addEventListener('click', ...)per ogni dropdown. Con 3 dropdown, vengono registrati 3 listener identici che chiudono lo stesso menu.♻️ Proposta di refactor
- const setupDropdown = (btnClass, menuClass) => { - const btn = document.querySelector(btnClass); - const menu = document.querySelector(menuClass); - if (btn && menu) { - allDropdownMenus.push(menu); - btn.addEventListener('click', (e) => { - e.stopPropagation(); - // Close all other dropdowns - allDropdownMenus.forEach(m => { - if (m !== menu) m.classList.add('hidden'); - }); - menu.classList.toggle('hidden'); - }); - document.addEventListener('click', () => { - menu.classList.add('hidden'); - }); - } - }; - - setupDropdown('.export-btn', '.export-menu'); - setupDropdown('.import-btn', '.import-menu'); - setupDropdown('.export-btn-mobile', '.export-menu-mobile'); + const setupDropdown = (btnClass, menuClass) => { + const btn = document.querySelector(btnClass); + const menu = document.querySelector(menuClass); + if (btn && menu) { + allDropdownMenus.push(menu); + btn.addEventListener('click', (e) => { + e.stopPropagation(); + allDropdownMenus.forEach(m => { + if (m !== menu) m.classList.add('hidden'); + }); + menu.classList.toggle('hidden'); + }); + } + }; + + setupDropdown('.export-btn', '.export-menu'); + setupDropdown('.import-btn', '.import-menu'); + setupDropdown('.export-btn-mobile', '.export-menu-mobile'); + + // Single global listener for closing all dropdowns + document.addEventListener('click', () => { + allDropdownMenus.forEach(m => m.classList.add('hidden')); + });app/Support/Updater.php (1)
1670-1725: Gestire anche escape con backslash nelle stringhe SQL.
Il parser considera solo'', ma in MySQL l’escape\'è comune: con;nel contenuto potrebbe spezzare lo statement.♻️ Suggerimento di robustezza
for ($i = 0; $i < $length; $i++) { $char = $sql[$i]; + // Handle backslash-escaped characters inside strings (MySQL default) + if ($inString && $char === '\\' && $i + 1 < $length) { + $current .= $char . $sql[$i + 1]; + $i++; + continue; + } + // Handle single quotes (SQL string delimiter) if ($char === "'") {
| $stmt = $db->prepare(" | ||
| UPDATE libri SET | ||
| isbn10 = ?, isbn13 = ?, ean = ?, titolo = ?, sottotitolo = ?, | ||
| anno_pubblicazione = ?, lingua = ?, edizione = ?, numero_pagine = ?, | ||
| genere_id = ?, descrizione = ?, formato = ?, prezzo = ?, editore_id = ?, | ||
| collana = ?, numero_serie = ?, traduttore = ?, parole_chiave = ?, | ||
| classificazione_dewey = ?, peso = ?, dimensioni = ?, data_acquisizione = ?, | ||
| review = ?, rating = ?, comment = ?, private_comment = ?, | ||
| physical_description = ?, | ||
| lccn = ?, lc_classification = ?, other_call_number = ?, | ||
| date_started = ?, date_read = ?, | ||
| bcid = ?, oclc = ?, work_id = ?, issn = ?, | ||
| original_languages = ?, source = ?, from_where = ?, | ||
| lending_patron = ?, lending_status = ?, lending_start = ?, lending_end = ?, | ||
| value = ?, condition_lt = ?, | ||
| updated_at = NOW() | ||
| WHERE id = ? | ||
| "); | ||
|
|
||
| $params = [ | ||
| !empty($data['isbn10']) ? $data['isbn10'] : null, | ||
| !empty($data['isbn13']) ? $data['isbn13'] : null, | ||
| !empty($data['ean']) ? $data['ean'] : null, | ||
| $data['titolo'], | ||
| !empty($data['sottotitolo']) ? $data['sottotitolo'] : null, | ||
| !empty($data['anno_pubblicazione']) ? (int) $data['anno_pubblicazione'] : null, | ||
| !empty($data['lingua']) ? $data['lingua'] : 'italiano', | ||
| !empty($data['edizione']) ? $data['edizione'] : null, | ||
| !empty($data['numero_pagine']) ? (int) $data['numero_pagine'] : null, | ||
| $genreId, | ||
| !empty($data['descrizione']) ? $data['descrizione'] : null, | ||
| !empty($data['formato']) ? $data['formato'] : 'cartaceo', | ||
| !empty($data['prezzo']) ? (float) str_replace(',', '.', $data['prezzo']) : null, | ||
| $editorId, | ||
| !empty($data['collana']) ? $data['collana'] : null, | ||
| !empty($data['numero_serie']) ? $data['numero_serie'] : null, | ||
| !empty($data['traduttore']) ? $data['traduttore'] : null, | ||
| !empty($data['parole_chiave']) ? $data['parole_chiave'] : null, | ||
| !empty($data['classificazione_dewey']) ? $data['classificazione_dewey'] : null, | ||
| // Native fields from LibraryThing mapping | ||
| !empty($data['peso']) ? (float) $data['peso'] : null, | ||
| !empty($data['dimensioni']) ? $data['dimensioni'] : null, | ||
| !empty($data['data_acquisizione']) ? $data['data_acquisizione'] : null, | ||
| // LibraryThing-specific fields (25 unique fields) | ||
| !empty($data['review']) ? $data['review'] : null, | ||
| !empty($data['rating']) ? (int) $data['rating'] : null, | ||
| !empty($data['comment']) ? $data['comment'] : null, | ||
| !empty($data['private_comment']) ? $data['private_comment'] : null, | ||
| !empty($data['physical_description']) ? $data['physical_description'] : null, | ||
| !empty($data['lccn']) ? $data['lccn'] : null, | ||
| !empty($data['lc_classification']) ? $data['lc_classification'] : null, | ||
| !empty($data['other_call_number']) ? $data['other_call_number'] : null, | ||
| !empty($data['date_started']) ? $data['date_started'] : null, | ||
| !empty($data['date_read']) ? $data['date_read'] : null, | ||
| !empty($data['bcid']) ? $data['bcid'] : null, | ||
| !empty($data['oclc']) ? $data['oclc'] : null, | ||
| !empty($data['work_id']) ? $data['work_id'] : null, | ||
| !empty($data['issn']) ? $data['issn'] : null, | ||
| !empty($data['original_languages']) ? $data['original_languages'] : null, | ||
| !empty($data['source']) ? $data['source'] : null, | ||
| !empty($data['from_where']) ? $data['from_where'] : null, | ||
| !empty($data['lending_patron']) ? $data['lending_patron'] : null, | ||
| !empty($data['lending_status']) ? $data['lending_status'] : null, | ||
| !empty($data['lending_start']) ? $data['lending_start'] : null, | ||
| !empty($data['lending_end']) ? $data['lending_end'] : null, | ||
| !empty($data['value']) ? (float) str_replace(',', '.', $data['value']) : null, | ||
| !empty($data['condition_lt']) ? $data['condition_lt'] : null, | ||
| $bookId | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
entry_date viene parsato ma non viene mai salvato.
parseLibraryThingRow() popola entry_date e l’export la usa, ma nei blocchi UPDATE/INSERT LT non viene persistita: il dato si perde all’import. Aggiungila a query, params e $types in entrambi i metodi.
🛠️ Aggiunta a UPDATE/INSERT (da completare aggiornando $types)
- peso = ?, dimensioni = ?, data_acquisizione = ?,
+ peso = ?, dimensioni = ?, data_acquisizione = ?, entry_date = ?,
...
- !empty($data['data_acquisizione']) ? $data['data_acquisizione'] : null,
+ !empty($data['data_acquisizione']) ? $data['data_acquisizione'] : null,
+ !empty($data['entry_date']) ? $data['entry_date'] : null,- peso, dimensioni, data_acquisizione,
+ peso, dimensioni, data_acquisizione, entry_date,
...
- !empty($data['data_acquisizione']) ? $data['data_acquisizione'] : null,
+ !empty($data['data_acquisizione']) ? $data['data_acquisizione'] : null,
+ !empty($data['entry_date']) ? $data['entry_date'] : null,Also applies to: 1039-1119
🤖 Prompt for AI Agents
In `@app/Controllers/LibraryThingImportController.php` around lines 909 - 977, La
proprietà entry_date viene parsata in parseLibraryThingRow() ma non salvata:
nelle query UPDATE/INSERT LT aggiungi la colonna entry_date nello statement SQL,
inserisci il relativo valore in $params (usando !empty($data['entry_date']) ?
$data['entry_date'] : null) e aggiorna la stringa $types corrispondente per
includere il tipo corretto (es. 's' o altro coerente con gli altri campi);
ripeti questa stessa modifica sia nel blocco UPDATE (dove è definito $stmt e
$params) sia nel blocco INSERT corrispondente per assicurare la persistenza di
entry_date.
| // Priority 1: Use APP_CANONICAL_URL from .env if configured | ||
| $canonicalUrl = $_ENV['APP_CANONICAL_URL'] ?? getenv('APP_CANONICAL_URL') ?: false; | ||
| if ($canonicalUrl !== false && trim((string)$canonicalUrl) !== '' && filter_var($canonicalUrl, FILTER_VALIDATE_URL)) { | ||
| $baseUrl = rtrim((string)$canonicalUrl, '/'); | ||
| $cover = $baseUrl . $cover; | ||
| } elseif (isset($_SERVER['HTTP_HOST'])) { | ||
| // Priority 2: Fallback to HTTP_HOST | ||
| $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http'; | ||
| $host = $_SERVER['HTTP_HOST']; | ||
| $cover = $protocol . '://' . $host . $cover; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valida HTTP_HOST prima di costruire URL assoluti.
Senza validazione, un Host header manipolato può generare URL avvelenati in risposta. Allineiamo la regex già usata in altri servizi.
🔒 Diff proposta
- } elseif (isset($_SERVER['HTTP_HOST'])) {
- // Priority 2: Fallback to HTTP_HOST
- $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http';
- $host = $_SERVER['HTTP_HOST'];
- $cover = $protocol . '://' . $host . $cover;
- }
+ } elseif (isset($_SERVER['HTTP_HOST'])) {
+ // Priority 2: Fallback to HTTP_HOST (validated)
+ $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http';
+ $host = $_SERVER['HTTP_HOST'];
+ if (preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*(:[0-9]{1,5})?$/', $host)) {
+ $cover = $protocol . '://' . $host . $cover;
+ }
+ }🤖 Prompt for AI Agents
In `@app/Controllers/LibriApiController.php` around lines 255 - 265, The code uses
$_SERVER['HTTP_HOST'] directly to build absolute URLs ($cover) which allows Host
header poisoning; update the HTTP_HOST fallback in LibriApiController to
validate and sanitize the host before concatenation: read and trim
$_SERVER['HTTP_HOST'], verify it against the same hostname/hostname:port regex
used elsewhere in the codebase (use the existing regex constant/pattern you
already have), ensure it only contains a safe hostname and optional numeric
port, and only then build $cover from $protocol . '://' . $host . $cover; if
validation fails, fall back to leaving $cover unchanged or use a safe default
base URL and consider logging the invalid host. Ensure you reference and reuse
the existing validation routine/pattern rather than inventing a new one.
| $body = fopen('php://temp', 'r+'); | ||
| fwrite($body, json_encode([ | ||
| 'error' => __('Too many requests'), | ||
| 'message' => __('Rate limit exceeded. Please try again later.') | ||
| ])); | ||
| rewind($body); | ||
| return $response | ||
| ->withStatus(429) | ||
| ->withHeader('Content-Type', 'application/json') | ||
| ->withHeader('Retry-After', (string)$this->window) | ||
| ->withBody(\Slim\Psr7\Stream::create(json_encode([ | ||
| 'error' => __('Too many requests'), | ||
| 'message' => __('Rate limit exceeded. Please try again later.') | ||
| ]))); | ||
| ->withBody(new \Slim\Psr7\Stream($body)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Manca la gestione degli errori per fopen() e json_encode().
fopen('php://temp', 'r+') può restituire false in caso di fallimento, e json_encode() può restituire false se la codifica fallisce. Passare false a fwrite() o al costruttore di Stream causerebbe warning o eccezioni.
🛡️ Proposta di fix con gestione errori difensiva
- $body = fopen('php://temp', 'r+');
- fwrite($body, json_encode([
+ $body = fopen('php://temp', 'r+');
+ if ($body === false) {
+ throw new \RuntimeException('Failed to create temporary stream');
+ }
+ $json = json_encode([
'error' => __('Too many requests'),
'message' => __('Rate limit exceeded. Please try again later.')
- ]));
+ ]);
+ fwrite($body, $json !== false ? $json : '{"error":"Too many requests"}');
rewind($body);🤖 Prompt for AI Agents
In `@app/Middleware/RateLimitMiddleware.php` around lines 34 - 44, The current
RateLimitMiddleware block assumes fopen() and json_encode() succeed; validate
both results before using them: check that $body = fopen('php://temp', 'r+') !==
false and that $payload = json_encode([...]) !== false (or use json_encode with
JSON_THROW_ON_ERROR and catch), and if either fails, log the error (use the
middleware's logger) and build a safe fallback response body (e.g. a simple
hard-coded minimal JSON string or an empty temp stream) so you never pass false
into fwrite() or new \Slim\Psr7\Stream; then continue to rewind($body) and
return the 429 response with headers as before. Ensure checks reference the
variables $body, $payload (or the json result), fwrite, rewind, and
\Slim\Psr7\Stream in RateLimitMiddleware so failures are handled
deterministically.
| <div id="librarything-accordion-content" | ||
| class="card-body form-section overflow-hidden transition-all duration-300" | ||
| style="max-height: 0; opacity: 0; padding: 0;" | ||
| aria-hidden="true"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accordion: evitare focus su campi nascosti.
Con max-height: 0 gli input restano raggiungibili via Tab. Conviene rendere il contenuto non interattivo quando è chiuso (es. inert + pointer-events: none) e ripristinare in apertura.
🔧 Possibile fix (focus management)
function toggleLibraryThingAccordion() {
const content = document.getElementById('librarything-accordion-content');
const icon = document.getElementById('librarything-accordion-icon');
const button = content.previousElementSibling;
const isExpanded = button.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
+ content.setAttribute('inert', '');
+ content.style.pointerEvents = 'none';
// Collapse
content.style.maxHeight = '0';
content.style.opacity = '0';
content.style.padding = '0';
content.setAttribute('aria-hidden', 'true');
button.setAttribute('aria-expanded', 'false');
icon.style.transform = 'rotate(0deg)';
} else {
+ content.removeAttribute('inert');
+ content.style.pointerEvents = '';
// Expand
content.style.maxHeight = content.scrollHeight + 'px';
content.style.opacity = '1';
content.style.padding = '';
content.setAttribute('aria-hidden', 'false');
button.setAttribute('aria-expanded', 'true');
icon.style.transform = 'rotate(180deg)';Also applies to: 931-960
🤖 Prompt for AI Agents
In `@app/Views/libri/partials/book_form.php` around lines 602 - 605, The hidden
accordion content with id "librarything-accordion-content" is still focusable
via Tab because only CSS max-height/opacity are changed; update the accordion
toggle logic to make the container non-interactive when closed by setting
container.inert = true and container.style.pointerEvents = 'none' (and keep
aria-hidden="true"), and when opening remove inert, restore pointerEvents, and
set aria-hidden="false" and move focus into a sensible first focusable element;
apply the same change to the other accordion container referenced in the same
file (the second accordion block around lines 931-960) so both toggle handlers
add/remove inert + pointer-events and manage aria-hidden and focus consistently.
| // Include autoloader | ||
| require_once __DIR__ . '/../vendor/autoload.php'; | ||
|
|
||
| // Load environment variables from .env file | ||
| use Dotenv\Dotenv; | ||
| $dotenv = Dotenv::createImmutable(__DIR__ . '/../'); | ||
| try { | ||
| $dotenv->load(); | ||
| } catch (Throwable $e) { | ||
| fwrite(STDERR, "ERROR: Failed to load .env file: " . $e->getMessage() . "\n"); | ||
| exit(1); | ||
| } | ||
|
|
||
| // Include settings | ||
| $settings = require __DIR__ . '/../config/settings.php'; | ||
|
|
||
| use App\Support\NotificationService; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Le use dopo codice eseguibile causano errore di parsing.
In PHP i use devono stare subito dopo declare/namespace e prima di qualunque istruzione. Qui sono dopo lock/require, quindi lo script non parte.
🔧 Proposta di fix
declare(strict_types=1);
+use Dotenv\Dotenv;
+use App\Support\NotificationService;
+
// ============================================================
// PROCESS LOCK - Prevent concurrent cron executions
// ============================================================
@@
// Include autoloader
require_once __DIR__ . '/../vendor/autoload.php';
-// Load environment variables from .env file
-use Dotenv\Dotenv;
+// Load environment variables from .env file
$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
@@
-// Include settings
-$settings = require __DIR__ . '/../config/settings.php';
-
-use App\Support\NotificationService;
+// Include settings
+$settings = require __DIR__ . '/../config/settings.php';🤖 Prompt for AI Agents
In `@cron/automatic-notifications.php` around lines 64 - 80, Move the offending
use statements so they appear before any executable statements: place "use
Dotenv\Dotenv;" and "use App\Support\NotificationService;" immediately after the
file's opening (and after any declare/namespace if present) and before
require_once/other code (e.g., before autoloader load and Dotenv instantiation)
to avoid PHP parse errors from having use declarations after executable code.
| ``` | ||
| Vai alla scheda libro → 🏷️ "Stampa Etichetta" | ||
| Vai alla scheda libro → "Stampa Etichetta" | ||
| Le nuove etichette userannno il formato configurato |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Errore di battitura.
"userannno" contiene tre 'n' invece di due.
📝 Correzione proposta
-Le nuove etichette userannno il formato configurato
+Le nuove etichette useranno il formato configurato📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Le nuove etichette userannno il formato configurato | |
| Le nuove etichette useranno il formato configurato |
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)
[warning] 388-388: Spaces inside emphasis markers
(MD037, no-space-in-emphasis)
🤖 Prompt for AI Agents
In `@docs/scheda_libro.MD` at line 388, Correggi il refuso nella frase "Le nuove
etichette userannno il formato configurato": sostituisci la stringa "userannno"
con la corretta "useranno" in modo che la riga legga "Le nuove etichette
useranno il formato configurato".
| ** Documento aggiornato: 19 Ottobre 2025** | ||
| ** Versione: 1.0.0 - Guida Scheda Libro** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formattazione markdown non valida nel footer.
Gli spazi all'interno dei marcatori di enfasi causano problemi di rendering.
📝 Correzione proposta
-** Documento aggiornato: 19 Ottobre 2025**
-** Versione: 1.0.0 - Guida Scheda Libro**
-** Per utenti finali della biblioteca**
+**Documento aggiornato: 19 Ottobre 2025**
+**Versione: 1.0.0 - Guida Scheda Libro**
+**Per utenti finali della biblioteca**🧰 Tools
🪛 LanguageTool
[uncategorized] ~497-~497: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "".
Context: ...Documento aggiornato: 19 Ottobre 2025 ** Versione: 1.0.0 - Guida Scheda Libro* ...
(GR_09_001)
[uncategorized] ~498-~498: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "".
Context: ... Versione: 1.0.0 - Guida Scheda Libro ** Per utenti finali della biblioteca*
(GR_09_001)
🤖 Prompt for AI Agents
In `@docs/scheda_libro.MD` around lines 497 - 498, The footer contains invalid
Markdown emphasis due to spaces inside the bold markers; update the two footer
lines that currently read "** Documento aggiornato: 19 Ottobre 2025**" and "**
Versione: 1.0.0 - Guida Scheda Libro**" to remove the leading space inside the
** markers (e.g., change to "**Documento aggiornato: 19 Ottobre 2025**" and
"**Versione: 1.0.0 - Guida Scheda Libro**") so they render correctly.
|
|
||
| ### Passo 2: Generare l'Etichetta | ||
| 1. **Clicca** il bottone **"Stampa Etichetta"** (icona 🏷️) | ||
| 1. **Clicca** il bottone **"Stampa Etichetta"** (icona ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correggere il riferimento all’icona mancante.
La dicitura “(icona )” risulta vuota e può confondere. Indica l’icona specifica o rimuovi il placeholder.
🧰 Tools
🪛 LanguageTool
[uncategorized] ~50-~50: L’asterisco si usa per un’omissione nel numero di tre consecutivi: "***".
Context: ...re l'Etichetta 1. Clicca il bottone "Stampa Etichetta" (icona ) 2. **Atten...
(GR_09_001)
🤖 Prompt for AI Agents
In `@docs/stampa_etichette.MD` at line 50, La frase che descrive il pulsante
"Stampa Etichetta" contiene un placeholder vuoto "(icona )"; sostituisci il
placeholder con il nome o la descrizione dell'icona effettiva usata
nell'interfaccia (es. "(icona stampante)" o "(icona label)") oppure rimuovi
completamente "(icona )" se non c'è icona, aggiornando la stringa che contiene
"Clicca il bottone \"Stampa Etichetta\"" nel documento.
| { | ||
| ignores: [ | ||
| 'node_modules/**', | ||
| 'dist/**', | ||
| 'build/**', | ||
| '*.min.js', | ||
| ] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pattern ignore incompleto per file minificati.
Il pattern *.min.js corrisponde solo ai file nella directory root. Per ignorare i file .min.js in tutte le sottodirectory, usare **/*.min.js.
🐛 Fix per il pattern glob
{
ignores: [
'node_modules/**',
'dist/**',
'build/**',
- '*.min.js',
+ '**/*.min.js',
]
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| { | |
| ignores: [ | |
| 'node_modules/**', | |
| 'dist/**', | |
| 'build/**', | |
| '*.min.js', | |
| ] | |
| } | |
| { | |
| ignores: [ | |
| 'node_modules/**', | |
| 'dist/**', | |
| 'build/**', | |
| '**/*.min.js', | |
| ] | |
| } |
🤖 Prompt for AI Agents
In `@frontend/eslint.config.js` around lines 51 - 58, Update the ignores array in
the ESLint configuration: replace the root-only pattern '*.min.js' with a
recursive glob '**/*.min.js' so that the ignores array (the ignores entry)
excludes minified JS files in all subdirectories rather than only the project
root.
| }, | ||
| "type": "module" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, explore the frontend directory structure to understand the setup
cd frontend && find . -maxdepth 2 -type f \( -name "*.json" -o -name "*config*" \) | head -20Repository: fabiodalez-dev/Pinakes
Length of output: 180
🏁 Script executed:
# Check the package.json to see the context around line 60-61
cat -n frontend/package.json | sed -n '55,65p'Repository: fabiodalez-dev/Pinakes
Length of output: 289
🏁 Script executed:
# Look for eslint.config.js and check if it uses ES modules
fd -type f -name "eslint.config.js" frontend/ && cat frontend/eslint.config.js | head -30Repository: fabiodalez-dev/Pinakes
Length of output: 236
🏁 Script executed:
# Check for webpack config files
fd -type f -name "*webpack*" frontend/ --exec cat -n {} \; | head -50Repository: fabiodalez-dev/Pinakes
Length of output: 236
🏁 Script executed:
# Check for babel config files
fd -type f \( -name ".babelrc*" -o -name "babel.config.*" \) frontend/ --exec cat {} \;Repository: fabiodalez-dev/Pinakes
Length of output: 236
🏁 Script executed:
# Check eslint.config.js
cat -n frontend/eslint.config.js | head -40Repository: fabiodalez-dev/Pinakes
Length of output: 1605
🏁 Script executed:
# Check webpack.config.js
cat -n frontend/webpack.config.js | head -60Repository: fabiodalez-dev/Pinakes
Length of output: 1652
🏁 Script executed:
# Check if babel config exists
find frontend -maxdepth 1 -name ".babelrc*" -o -name "babel.config.*"Repository: fabiodalez-dev/Pinakes
Length of output: 48
🏁 Script executed:
# Check for any require() calls in webpack.config.js that might be problematic
grep -n "require\|module.exports\|import\|export" frontend/webpack.config.js | head -20Repository: fabiodalez-dev/Pinakes
Length of output: 203
🏁 Script executed:
# Check postcss and tailwind configs
grep -n "require\|module.exports\|import\|export" frontend/postcss.config.js frontend/tailwind.config.jsRepository: fabiodalez-dev/Pinakes
Length of output: 447
Rimuovere "type": "module" da package.json oppure rinominare i file di configurazione a .cjs
Sebbene "type": "module" sia necessario per eslint.config.js, questo setting causa un'incompatibilità critica con gli altri file di configurazione. Quando "type": "module" è impostato, tutti i file .js sono trattati come ES modules. Tuttavia:
webpack.config.jsusa CommonJS (require/module.exports)postcss.config.jsusa CommonJS (module.exports)tailwind.config.jsusa CommonJS (require/module.exports)
Node.js cercherà di parsare questi file come ES modules, causando errori di sintassi. La soluzione è:
- Rinominare questi file a
.cjs(webpack.config.cjs, postcss.config.cjs, tailwind.config.cjs) per preservare il comportamento CommonJS, oppure - Convertire tutti i file di configurazione a sintassi ES modules
🤖 Prompt for AI Agents
In `@frontend/package.json` around lines 60 - 61, Il campo "type": "module" in
package.json causa parse error perché forza tutti i .js come ESM while
webpack.config.js, postcss.config.js e tailwind.config.js use CommonJS; either
rimuovi "type": "module" from package.json or rename those CommonJS config files
to .cjs (webpack.config.cjs, postcss.config.cjs, tailwind.config.cjs) so Node
treats them as CommonJS, or alternatively convert eslint.config.js,
webpack.config.js, postcss.config.js and tailwind.config.js to consistent ESM
syntax (import/export) — pick one approach and apply it consistently across
eslint.config.js, webpack.config.js, postcss.config.js and tailwind.config.js.
Resolved conflict in locale/en_US.json by keeping all LibraryThing import translations
9a002ca to
d6ada7f
Compare




Summary
This PR adds comprehensive LibraryThing plugin support to the application, significantly enhancing CSV import/export capabilities with multi-format and multi-delimiter support, plus 25 new LibraryThing-specific book fields.
Key Changes
LibraryThing Plugin Infrastructure
New Plugin Installer (
LibraryThingInstaller.php): Handles installation/uninstallation of LibraryThing fields with database schema managementNew LibraryThing Controller: Manages plugin-specific functionality and API endpoints
CSV Import Enhancements
Multi-delimiter Support: Now supports semicolon (
;), comma (,), and tab (\t) delimiters with automatic detectionIntelligent Header Mapping: New
mapColumnHeaders()method that:LibraryThing Format Detection: Automatically detects and parses LibraryThing CSV exports
CSV Export Enhancements
Multiple Export Formats:
Flexible Delimiter Options: Users can choose
;,,, or\tas delimiterSmart Field Quoting: Only quotes fields containing the selected delimiter, newlines, or quotes
Book Management Updates
store()andupdate()methods to handle 25 new LibraryThing fieldsImplementation Details
__()translation functionTesting Recommendations
https://claude.ai/code/session_01XdiVE4JpU8DFXr3eeLwFuB
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Chores