Skip to content

feat: German locale, dynamic route registration, GitHub API token, manual upgrade script#69

Open
fabiodalez-dev wants to merge 39 commits intomainfrom
fix-old-issues
Open

feat: German locale, dynamic route registration, GitHub API token, manual upgrade script#69
fabiodalez-dev wants to merge 39 commits intomainfrom
fix-old-issues

Conversation

@fabiodalez-dev
Copy link
Owner

@fabiodalez-dev fabiodalez-dev commented Feb 25, 2026

Summary

This PR bundles 4 improvements that address long-standing issues and community requests:

1. Native German language support (de_DE) — Closes discussion #65

  • 4,009 translated strings in locale/de_DE.json (formal "Sie" style, library-specific terminology)
  • German URL routes (/katalog, /anmelden, /buch, /autor, /wunschliste, etc.)
  • German installer seed data (data_de_DE.sql): 181 genres, 18 email templates, CMS pages (Über uns, Datenschutzerklärung, Impressum), cookie banner, home content
  • Installer step 0: Deutsch option with SVG flag, full German UI throughout all 8 installation steps
  • Installer __() function made language-agnostic (loads any locale JSON, not just en_US)

2. Dynamic locale registration from DB

  • $supportedLocales in web.php now reads from I18n::getAvailableLocales() instead of hardcoded ['it_IT', 'en_US']
  • New languages added via Admin → Languages automatically get their translated routes registered — no code changes needed

3. GitHub API token support for Updater

  • Optional PAT stored in system_settings (category: updater, key: github_token)
  • Bypasses GitHub's 60 req/hr anonymous rate limit → 5,000 req/hr authenticated
  • All GitHub API calls (check updates, download release, fetch changelog) now use centralized getGitHubHeaders()
  • Admin UI card: save/remove token, masked display, step-by-step setup instructions
  • New route: POST /admin/updates/token with CSRF validation

4. Standalone manual upgrade script

  • scripts/manual-upgrade.php: password-protected, standalone PHP script for users stuck on old versions that can't use the built-in updater
  • Auto-detects root path, mysqldump backup, ZIP upload + extraction
  • Preserves .env, uploads, plugins, covers, configs
  • Runs pending DB migrations (idempotent), clears cache

5. Changelog rewrite

  • README changelog for v0.4.9.2 rewritten with user-facing bug fix descriptions instead of internal implementation details

Changed files

File Change
locale/de_DE.json NEW — 4,009 German translation strings
locale/routes_de_DE.json NEW — German URL route mappings
installer/database/data_de_DE.sql NEW — German seed data (genres, emails, CMS, etc.)
scripts/manual-upgrade.php NEW — Standalone upgrade script
app/Routes/web.php Dynamic locale loading + token route
app/Support/I18n.php Added de_DE to fallback locales
app/Support/Updater.php GitHub token support, centralized headers
app/Controllers/UpdateController.php saveToken() endpoint
app/Views/admin/updates.php GitHub token UI card
installer/index.php Language-agnostic __(), HTML lang mapping
installer/steps/step0.php German option with flag SVG
installer/classes/Installer.php de/de_DE locale mapping
README.md Rewritten v0.4.9.2 changelog

Test plan

  • Full German installation walkthrough via Playwright (8 steps, all in German)
  • German frontend verified: home, catalog (/katalog), login (/anmelden), admin dashboard
  • Admin languages page shows Deutsch 100%, English 98.7%
  • Verify GitHub token save/remove works in admin updates page
  • Verify manual upgrade script works end-to-end
  • Verify dynamic locale registration: adding a new language via admin makes its routes available without restart

@coderabbitai full review

Summary by CodeRabbit

  • New Features

    • German language added across site, routes, installer, and public catalog
    • Admin: GitHub API token UI to raise update rate limits
    • Standalone manual upgrade script; scroll-to-top button; keyboard shortcuts
  • Improvements

    • "View all" links on genre carousels; more home genre books shown
    • Book exports support selected IDs; new book metadata fields (pages, edition, translator, illustrator, language)
    • Locale-aware event date/time and unified DataTables language handling
    • Expanded UI styles and themes
  • Bug Fixes

    • Genre pages include subcategories when listing books
    • Safer handling of external file/audio links; updater/installer robustness fixes

- Updater: add GitHub PAT support to bypass 60 req/hr rate limit (5000 with token)
- Updater: centralize headers via getGitHubHeaders(), store token in system_settings
- UpdateController: add saveToken() endpoint with CSRF validation
- Admin UI: GitHub token card with save/remove, masked display, setup instructions
- Route: POST /admin/updates/token
- scripts/manual-upgrade.php: standalone upgrade for users stuck on old versions
  - Password-protected, auto-detects root path
  - DB backup via mysqldump, ZIP upload + extraction
  - Preserves .env, uploads, plugins, covers, configs
  - Runs pending migrations (idempotent), clears cache
New languages added via Admin → Languages now automatically get
their translated routes registered without code changes.

Closes discussion #65.
- Add locale/de_DE.json with 4,009 translated strings
- Add locale/routes_de_DE.json with German URL routes
- Add installer/database/data_de_DE.sql with German seed data
  (genres, email templates, CMS pages, cookie banner, home content)
- Add Deutsch option to installer language selection (step 0)
- Add de_DE to I18n fallback locales and Installer locale map
- Make installer __() translation function language-agnostic
@fabiodalez-dev
Copy link
Owner Author

@CodeRabbit full review

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds German locale and extensive German seed data; implements encrypted GitHub API token storage with admin UI and API; introduces a standalone manual-upgrade script; enhances genre descendant filtering, book metadata, DataTables i18n centralization, date-format fallbacks, UI shortcuts, many vendor updates, and assorted frontend/backend fixes.

Changes

Cohort / File(s) Summary
Updater & Admin token UI
app/Support/Updater.php, app/Controllers/UpdateController.php, app/Views/admin/updates.php, app/Routes/web.php
Add encrypted GitHub token lifecycle (load, encrypt/decrypt, save, masked display, hasToken), centralized GitHub headers and auth-retry; new POST /admin/updates/token route/controller; admin UI + JS for save/remove token; SecureLogger usage.
Installer & German locale data
installer/index.php, installer/classes/Installer.php, installer/steps/step0.php, installer/database/data_de_DE.sql, app/Support/I18n.php, locale/routes_de_DE.json
Add de_DE support and normalization, installer locale UI, route JSON, and large German seed SQL (genres, system settings, CMS pages, email templates, languages, themes).
Manual upgrade & server config
scripts/manual-upgrade.php, scripts/.user.ini, .htaccess, (removed) scripts/manual-update.php
Add browser-accessible, passworded manual-upgrade script (backup, safe ZIP extraction, migrations, idempotency), PHP upload ini for upgrades, update rewrite exclusions, and remove legacy manual-update script.
Genre filtering & frontend UI
app/Controllers/FrontendController.php, app/Views/frontend/home-sections/genre_carousel.php, app/Views/frontend/home.php
Collect genre descendants (BFS) to build IN(...) filters for genre archives; increase home genre book limit (4→12); add per-genre "View all" links and related CSS.
Book form, controller & repository
app/Views/libri/partials/book_form.php, app/Controllers/LibriController.php, app/Models/BookRepository.php
Genere/sottogenere now sync to hidden ID inputs; adjust author Enter handling; add numero_pagine validation/storage in controller and repository; repository guarded per-column insert/update for extra metadata; CSV export supports selected IDs filter.
Date formatting fallbacks
app/Views/events/index.php, app/Views/frontend/event-detail.php, app/Views/frontend/events.php, app/Views/frontend/home-sections/events.php
Guard IntlDateFormatter with class_exists and provide locale-based fallback formats (de: d.m.Y, it: d/m/Y, default Y-m-d); time falls back to 24h 'H:i'.
DataTable language centralization
frontend/js/vendor.js, app/Views/libri/index.php, app/Views/autori/index.php, app/Views/editori/index.php, app/Views/prestiti/index.php, app/Views/utenti/index.php
Add German DataTables translations, implement window.getDtLanguage() and update views to call it (fallback {}); libri table adds stateLoadCallback to reset paging when URL filters applied.
File/link safety & book view
app/Views/libri/scheda_libro.php
Add URL safety/normalization checks for file/audio links and render styled action buttons; compute genreFilterId from hierarchy and link to admin filter when available; omit unsafe links.
Keyboard shortcuts & helpers
app/Views/layout.php, app/Views/partials/scroll-to-top.php, app/Views/frontend/layout.php
Add keyboard-shortcuts modal and handlers (global search focus, G-prefix navigation, Esc/Modal behavior) and include scroll-to-top partial.
Assets & CSS utilities
public/assets/main.css
Add many utility classes (spacing, colors, gradients, rounded, focus/ring, hover/dark variants) and adjust responsive rules.
Tests & E2E
tests/genre-bugs.spec.js, tests/full-test.spec.js
Switch test cleanup to admin API soft-delete; add large Playwright end-to-end suite covering install→admin→data workflows and cleanup.
Vendor & autoload updates
vendor/composer/*, many vendor/* files (monolog, tcpdf, slim, serializable-closure, phpdotenv, etc.)
Multiple package bumps, large autoload/classmap expansions, vendor library tweaks (Monolog, TCPDF PHP8 guards/SVG improvements, serializable-closure parsing changes), and composer manifest edits.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(0,128,0,0.5)
    participant Browser
    end
    rect rgba(0,0,255,0.5)
    participant Server
    note right of Server: UpdateController::saveToken
    end
    rect rgba(128,0,128,0.5)
    participant Updater
    end
    rect rgba(255,165,0,0.5)
    participant SettingsStore
    end

    Browser->>Server: POST /admin/updates/token (csrf, github_token)
    Server->>Server: validate CSRF & AdminAuth
    Server->>Updater: saveGitHubToken(token)
    Updater->>SettingsStore: encrypt & persist token in system_settings
    SettingsStore-->>Updater: ack
    Updater-->>Server: success / throw on error (logged via SecureLogger)
    Server-->>Browser: JSON {ok:true,msg:"..."} or 500 error
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I hopped through locales, soft and spry,
I hid a token safe where secrets lie,
I chased the genres down each branching lane,
I tuned the tables, exports, and the main,
A rabbit's cheer: "Jetzt auch Deutsch — yippee!" 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: German locale, dynamic route registration, GitHub API token, manual upgrade script' clearly and specifically summarizes the four main features introduced in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-old-issues

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (3)
scripts/manual-upgrade.php (2)

52-81: Helper functions duplicated from app/Support/Updater.php.

formatBytes, deleteDirectory, and the env-loading pattern are near-identical copies of methods in Updater.php (see relevant code snippets). This is expected for a standalone script that can't import app classes, so just noting it for awareness — keep them in sync manually if either side changes.

Also applies to: 83-102, 109-118, 120-141

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 52 - 81, The functions loadEnv,
formatBytes, and deleteDirectory are duplicated from app/Support/Updater.php;
add a short inline comment above each (loadEnv, formatBytes, deleteDirectory)
stating that this duplication is intentional for a standalone script and include
a pointer to the canonical implementation in Updater.php so future maintainers
know to keep them in sync; do not change behavior—just document the duplication
clearly so it’s obvious why these near-identical helpers exist and where to
update them.

269-288: DB password visible in process listing during mysqldump.

The --password= argument is visible to any user who can run ps aux on the server. The standard mitigation is a temporary --defaults-extra-file:

$tmpCnf = tempnam(sys_get_temp_dir(), 'my');
file_put_contents($tmpCnf, "[client]\npassword=" . $env['DB_PASS'] . "\n");
chmod($tmpCnf, 0600);
// use --defaults-extra-file=$tmpCnf instead of --password=
// unlink($tmpCnf) after exec

Given this is a temporary script, the risk is lower, but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 269 - 288, Replace the plain
--password= arg (used when building $args for $mysqldumpBin) with a secure
temporary defaults file: create a temp file (e.g. $tmpCnf), write a [client]
section containing the password from $env['DB_PASS'], chmod it to 0600, add
'--defaults-extra-file=' . $tmpCnf to $args instead of '--password=', run
exec($safeCmd, ... ) as before, then immediately unlink($tmpCnf) after exec to
remove the file; keep references to $mysqldumpBin, $args, $env['DB_PASS'],
$safeCmd, $backupFile and exec() so you replace the insecure password exposure
without changing the overall flow.
app/Controllers/UpdateController.php (1)

34-38: $data variable is assigned but never used (static analysis).

The compact() result is stored in $data but the view file (included via require on line 39) accesses the individual variables ($githubTokenMasked, $hasGithubToken, etc.) directly from the local scope rather than through $data. This is a pre-existing pattern, but the static analysis flagging is valid.

♻️ Option: remove the unused assignment
-        $data = compact('updateInfo', 'requirements', 'history', 'changelog', 'githubTokenMasked', 'hasGithubToken');
+        // Variables are accessed directly by the view via local scope
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/UpdateController.php` around lines 34 - 38, The assignment to
$data via $data = compact('updateInfo', 'requirements', 'history', 'changelog',
'githubTokenMasked', 'hasGithubToken') is unused and triggers static analysis;
remove that $data assignment line (leaving ob_start() and the individual local
variables like $githubTokenMasked, $hasGithubToken, $updateInfo, $requirements,
$history, $changelog intact) so the view included afterward consumes the
existing locals directly, or alternatively replace the require-based view
consumption by using the $data array consistently—pick one approach and remove
the unused compact() call if you keep the current require/local-variable
pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Support/Updater.php`:
- Around line 193-196: The token load/save paths (loadGitHubToken and
saveGitHubToken) call $stmt->execute(), $stmt->get_result() and
$result->fetch_column() without checking for errors; update both functions to
validate $stmt->execute() return (or check $stmt->error), ensure $result !==
false before calling fetch_column(), and verify fetch_column() actually returned
a value; for saveGitHubToken also check execution success and affected_rows to
confirm persistence and return/throw a failure indicator instead of reporting
success when execute() failed; use the existing $stmt variable and these method
names to locate and add proper error handling and early returns.
- Around line 186-200: The GitHub token is being stored and read in plaintext;
add two helpers (encryptUpdaterSecret(string): string and
decryptUpdaterSecret(string): string) on the Updater class and use them when
persisting and loading $this->githubToken: when saving the token (the code
around where $this->githubToken is written to system_settings, referenced in the
later block ~224-241) call encryptUpdaterSecret() and store the cipher text, and
when loading (the code shown here that prepares the SELECT and assigns
$this->githubToken) call decryptUpdaterSecret($value) and only set
$this->githubToken if decryption succeeds and returns a non-empty string; also
handle null/empty DB values and catch/deal with decryption errors so corrupt
ciphertext does not crash the updater.

In `@installer/classes/Installer.php`:
- Around line 263-265: The installer adds German mappings ('de', 'de_de',
'de_DE') in the locale map but then forces non-whitelisted locales to 'it_IT';
update createEnvFile() to include 'de_DE' in its allowed locales list (in the
validation/whitelist that currently contains ['it_IT','en_US']) so APP_LOCALE
can be set to 'de_DE', and update getInstallerLocale() to handle the German case
by adding a branch that returns 'de_DE' for the German mapping keys (so the
installer will load German texts); reference createEnvFile() and
getInstallerLocale() and ensure the whitelist and the new case use the exact
'de_DE' identifier.

In `@installer/database/data_de_DE.sql`:
- Around line 7-340: The seed file uses many plain INSERTs with explicit IDs
(tables: generi, system_settings, cms_pages, email_templates, languages,
home_content, themes) which cause duplicate-key failures on re-run and can leave
FOREIGN_KEY_CHECKS disabled; convert these into idempotent/upsert operations
(e.g. replace plain INSERTs with INSERT ... ON DUPLICATE KEY UPDATE or REPLACE
INTO) for each affected table and consolidate repeated system_settings inserts
into a single guarded block (use VALUES(...) ON DUPLICATE KEY UPDATE) and ensure
the script always re-enables FOREIGN_KEY_CHECKS (wrap inserts in a transaction
or add a finally-style restore) so partial failures won’t leave the session with
foreign key checks off.

In `@README.md`:
- Around line 28-45: Update the v0.4.9.2 release notes in README.md to include
the missing major features: add entries for German locale support, dynamic
locale route registration, GitHub updater token management, and the manual
upgrade script under the "Genre Management, Book Filters & Bug Fixes" section
(or a new "Locale & Upgrade Improvements" subsection) so admins see these
shipped features alongside the existing items; ensure the wording matches the
style of the other bullets and includes concise descriptions for "German locale
support", "dynamic locale route registration", "GitHub updater token
management", and "manual upgrade script".

In `@scripts/manual-upgrade.php`:
- Around line 424-451: The current use of $db->multi_query can lose errors from
earlier statements and cause "Commands out of sync"; replace multi_query usage
in the migration runner with splitting the migration SQL (from the file
processed by the migration loop) into individual statements (e.g., explode on
';' but trim and ignore empty/comment-only parts), then execute each statement
with $db->query() and check $db->errno/$db->error after each execution; if any
statement fails (and errno not in the existing $ignorableErrors list), log the
specific statement error and abort recording the migration (do not run the
INSERT IGNORE via $recStmt or push to $migrationsRun), otherwise continue
through all statements and only then run the existing $recStmt
prepare/execute/close and push $migVersion and OK log; this removes multi_query
and prevents "Commands out of sync" for subsequent prepare()/query() calls.
- Around line 29-30: Change the PHP runtime settings so errors are not rendered
to the browser: replace the ini_set call that sets display_errors to '1' with
display_errors = '0' (i.e., change ini_set('display_errors', '1') to
ini_set('display_errors', '0')), and ensure error logging remains enabled (e.g.,
keep error_reporting(E_ALL) and enable log_errors via ini_set('log_errors','1')
or a configured error_log) so diagnostics go to logs instead of exposing
sensitive data.
- Line 23: The script defines a hardcoded default secret via the
UPGRADE_PASSWORD constant which is a security risk; update the auth flow to
refuse to run when UPGRADE_PASSWORD equals the known default "pinakes2026" by
checking the UPGRADE_PASSWORD constant at startup (or at the auth check) and
aborting with a clear error instructing the operator to set a new password (or
implement a first-use setter) before continuing; reference the UPGRADE_PASSWORD
constant and the existing auth check block to add this validation and exit early
if the sentinel/default value is detected.

---

Nitpick comments:
In `@app/Controllers/UpdateController.php`:
- Around line 34-38: The assignment to $data via $data = compact('updateInfo',
'requirements', 'history', 'changelog', 'githubTokenMasked', 'hasGithubToken')
is unused and triggers static analysis; remove that $data assignment line
(leaving ob_start() and the individual local variables like $githubTokenMasked,
$hasGithubToken, $updateInfo, $requirements, $history, $changelog intact) so the
view included afterward consumes the existing locals directly, or alternatively
replace the require-based view consumption by using the $data array
consistently—pick one approach and remove the unused compact() call if you keep
the current require/local-variable pattern.

In `@scripts/manual-upgrade.php`:
- Around line 52-81: The functions loadEnv, formatBytes, and deleteDirectory are
duplicated from app/Support/Updater.php; add a short inline comment above each
(loadEnv, formatBytes, deleteDirectory) stating that this duplication is
intentional for a standalone script and include a pointer to the canonical
implementation in Updater.php so future maintainers know to keep them in sync;
do not change behavior—just document the duplication clearly so it’s obvious why
these near-identical helpers exist and where to update them.
- Around line 269-288: Replace the plain --password= arg (used when building
$args for $mysqldumpBin) with a secure temporary defaults file: create a temp
file (e.g. $tmpCnf), write a [client] section containing the password from
$env['DB_PASS'], chmod it to 0600, add '--defaults-extra-file=' . $tmpCnf to
$args instead of '--password=', run exec($safeCmd, ... ) as before, then
immediately unlink($tmpCnf) after exec to remove the file; keep references to
$mysqldumpBin, $args, $env['DB_PASS'], $safeCmd, $backupFile and exec() so you
replace the insecure password exposure without changing the overall flow.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and 4902b32.

📒 Files selected for processing (13)
  • README.md
  • app/Controllers/UpdateController.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • scripts/manual-upgrade.php

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
README.md (4)

8-8: ⚠️ Potential issue | 🟡 Minor

"Languages" header is stale — German is no longer listed.

This PR adds a full de_DE locale. Line 8 still reads Languages: Italian, English; it should be updated to include German/Deutsch.

📝 Proposed fix
-> **Open-Source Integrated Library System**
-> License: GPL-3  |  Languages: Italian, English
+> **Open-Source Integrated Library System**
+> License: GPL-3  |  Languages: Italian, English, Deutsch
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 8, Update the stale "Languages" header string that
currently reads "License: GPL-3  |  Languages: Italian, English" to include
German (Deutsch) — e.g., change the header to "License: GPL-3  |  Languages:
Italian, English, German (Deutsch)" or similar; ensure the displayed languages
match the added de_DE locale so the README's languages list and the new locale
are consistent.

219-219: ⚠️ Potential issue | 🟡 Minor

Quick Start step 4 still limits language choice to Italian or English.

With the German locale now available, the installer step should be reflected here too.

📝 Proposed fix
-4. **Select language** (Italian or English).
+4. **Select language** (Italian, English, or Deutsch).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 219, Update the Quick Start step text so the installer
language choice includes German; replace the existing line "Select language
(Italian or English)." in the README's Quick Start section with a version that
lists Italian, English, and German (e.g., "Select language (Italian, English or
German).") so the documentation matches the newly added German locale.

344-344: ⚠️ Potential issue | 🟡 Minor

OPAC description still advertises only two languages.

Line 344 reads Responsive, multilingual frontend (Italian + English) — German is now a supported frontend locale and should be listed here.

📝 Proposed fix
-- **Responsive, multilingual frontend** (Italian + English)
+- **Responsive, multilingual frontend** (Italian, English, Deutsch)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 344, Update the OPAC feature line that currently reads
"Responsive, multilingual frontend (Italian + English)" to include German;
locate the exact string "Responsive, multilingual frontend (Italian + English)"
and change it to something like "Responsive, multilingual frontend (Italian,
English, German)" so the README accurately lists supported frontend locales.

362-370: ⚠️ Potential issue | 🟡 Minor

Auto-Updater section omits the new GitHub API token capability.

The feature list describes the updater but has no mention of the optional personal access token (stored in system_settings, manageable from Admin → Updates) introduced in this PR to raise GitHub API rate limits. Worth adding a bullet so admins who hit rate-limit errors know the option exists.

📝 Suggested addition
 - **Security** — CSRF validation, admin-only access, path traversal protection, Zip Slip prevention
+- **GitHub API token** — Optional personal access token (Admin → Updates) to raise GitHub API rate limits
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 362 - 370, Add a bullet to the Auto-Updater feature
list mentioning the new optional GitHub personal access token support: note that
admins can store a GitHub API token in system_settings (configurable from Admin
→ Updates) to raise GitHub API rate limits and avoid rate-limit errors when
using the built-in update system. Ensure the text matches the style of the other
bullets and briefly states where to configure the token and its purpose.
♻️ Duplicate comments (8)
README.md (1)

28-45: ⚠️ Potential issue | 🟡 Minor

v0.4.9.2 changelog still omits major features shipped in this PR.

The new section covers genre management and bug fixes but does not mention German locale, dynamic locale route registration, GitHub API token for the updater, or the manual upgrade script.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 28 - 45, The v0.4.9.2 changelog entry in README.md is
missing several items; update the release notes under the "🏷️ Genre Management,
Book Filters & Bug Fixes" (or the v0.4.9.2 section) to include entries for the
German locale addition, dynamic locale route registration, the GitHub API token
support for the updater, and the manual upgrade script; add concise bullet
points matching the existing style and wording so the changelog reflects all
features shipped in this PR.
installer/database/data_de_DE.sql (1)

7-342: ⚠️ Potential issue | 🟠 Major

Seed script is not safely rerunnable.

From Line 7 onward, most statements are plain INSERT with explicit IDs/unique keys. On rerun, duplicate-key failures can halt execution before all records are applied, making install retries fragile.

♻️ Suggested direction
-INSERT INTO `system_settings` (`category`, `setting_key`, `setting_value`, `description`) VALUES
-('cms', 'events_page_enabled', '1', '...');
+INSERT INTO `system_settings` (`category`, `setting_key`, `setting_value`, `description`, `updated_at`) VALUES
+('cms', 'events_page_enabled', '1', '...', NOW())
+ON DUPLICATE KEY UPDATE
+  setting_value = VALUES(setting_value),
+  description = VALUES(description),
+  updated_at = NOW();

Apply the same idempotent strategy (ON DUPLICATE KEY UPDATE / INSERT IGNORE where appropriate) to the other seeded tables with explicit IDs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/database/data_de_DE.sql` around lines 7 - 342, The seed file uses
plain INSERTs with explicit IDs (e.g. INSERT INTO `generi` VALUES (...), INSERT
INTO `cms_pages`, INSERT INTO `email_templates`, INSERT INTO `languages`, INSERT
INTO `home_content`, INSERT INTO `themes`) which will fail on reruns due to
duplicate keys; make these statements idempotent by converting them to either
INSERT ... ON DUPLICATE KEY UPDATE (updating relevant columns like
setting_value/description/updated_at or content fields) or INSERT IGNORE where
overwriting isn't desired, and apply the same pattern to the bulk generi inserts
and other explicit-id inserts so the script can be safely re-run without error.
app/Support/Updater.php (2)

186-200: ⚠️ Potential issue | 🟠 Major

GitHub token is stored in plaintext.

Line 199 and Line 240 keep the raw credential in DB/memory persistence flow. This increases blast radius in dumps/backups and violates secret-handling best practices.

🔐 Proposed fix
--- a/app/Support/Updater.php
+++ b/app/Support/Updater.php
@@
-        if (is_string($value) && $value !== '') {
-            $this->githubToken = $value;
+        if (is_string($value) && trim($value) !== '') {
+            $plain = $this->decryptUpdaterSecret($value);
+            if ($plain !== '') {
+                $this->githubToken = $plain;
+            }
         }
@@
-        $stmt->bind_param('sss', $cat, $key, $token);
+        $encrypted = $token === '' ? '' : $this->encryptUpdaterSecret($token);
+        $stmt->bind_param('sss', $cat, $key, $encrypted);
         $stmt->execute();
         $stmt->close();

         $this->githubToken = $token;
private function encryptUpdaterSecret(string $plain): string { /* use app key */ }
private function decryptUpdaterSecret(string $cipher): string { /* use app key */ }

Also applies to: 224-241

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 186 - 200, The code reads and stores
the GitHub token in plaintext; add symmetric encryption/decryption around
persistence: implement private methods encryptUpdaterSecret(string $plain):
string and decryptUpdaterSecret(string $cipher): string that use the application
key (from config/env) and a secure crypto library, then update the Updater DB
write path to call encryptUpdaterSecret before binding/storing the setting and
update the read path (the block using $stmt->bind_param / $stmt->execute /
$result->fetch_column and the later similar block around lines 224-241) to
retrieve the stored value and call decryptUpdaterSecret before assigning
$this->githubToken; ensure existing code handles decryption failures gracefully
and keep DB column as ciphertext.

193-196: ⚠️ Potential issue | 🟠 Major

Handle statement execution failures in token load/save paths.

Line 193 through Line 196 and Line 236 through Line 238 assume execute()/get_result() succeed. Failures can silently desync state (save) or break loading logic.

✅ Proposed fix
@@
-        $stmt->execute();
-        $result = $stmt->get_result();
-        $value = $result->fetch_column();
+        if (!$stmt->execute()) {
+            $stmt->close();
+            return;
+        }
+        $result = $stmt->get_result();
+        if ($result === false) {
+            $stmt->close();
+            return;
+        }
+        $value = $result->fetch_column();
@@
-        $stmt->bind_param('sss', $cat, $key, $token);
-        $stmt->execute();
+        $stmt->bind_param('sss', $cat, $key, $token);
+        if (!$stmt->execute()) {
+            $error = $this->db->error;
+            $stmt->close();
+            throw new Exception(__('Errore nel salvataggio del token') . ': ' . $error);
+        }
         $stmt->close();
#!/bin/bash
set -euo pipefail

echo "== loadGitHubToken/saveGitHubToken blocks =="
sed -n '181,245p' app/Support/Updater.php

echo
echo "== Execute/get_result calls in Updater =="
rg -n -C2 '\-\>execute\(|\-\>get_result\(|fetch_column\(' app/Support/Updater.php

Expected result: confirm whether error paths are checked before consuming get_result() and before reporting save success.

Also applies to: 236-238

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 193 - 196, The
execute()/get_result()/fetch_column() calls in Updater.php (used by
loadGitHubToken and saveGitHubToken) assume success and can silently fail;
modify the code around $stmt->execute(), $stmt->get_result(), and
$result->fetch_column() to check the boolean/return values, log or throw a clear
error when execute() returns false, verify get_result() !== false before using
it, handle a false/null fetch_column() appropriately (treat as missing token or
error), ensure $stmt->close() always runs (use finally-like cleanup), and
propagate failure to the caller so state cannot silently desync.
installer/classes/Installer.php (1)

263-265: ⚠️ Potential issue | 🔴 Critical

German locale mapping is incomplete and still downgrades to Italian.

Line 263 to Line 265 adds de_DE for data import, but Line 85 (createEnvFile whitelist) and Line 1096 onward (getInstallerLocale) still force non-it_IT/en_US to it_IT. Result: a German install can seed German data but persist APP_LOCALE=it_IT.

🔧 Proposed fix
--- a/installer/classes/Installer.php
+++ b/installer/classes/Installer.php
@@
-        if ($normalizedLocale === 'en' || $normalizedLocale === 'en_us') {
+        if ($normalizedLocale === 'en' || $normalizedLocale === 'en_us') {
             $normalizedLocale = 'en_US';
+        } elseif ($normalizedLocale === 'de' || $normalizedLocale === 'de_de') {
+            $normalizedLocale = 'de_DE';
         } elseif ($normalizedLocale === 'it' || $normalizedLocale === 'it_it') {
             $normalizedLocale = 'it_IT';
         }
@@
-        if (!in_array($normalizedLocale, ['it_IT', 'en_US'], true)) {
+        if (!in_array($normalizedLocale, ['it_IT', 'en_US', 'de_DE'], true)) {
             $normalizedLocale = 'it_IT';
         }
@@
     private function getInstallerLocale(): string
     {
         $locale = $this->config['APP_LOCALE'] ?? 'it_IT';
         $locale = str_replace('-', '_', strtolower($locale));
         if ($locale === 'en' || $locale === 'en_us') {
             return 'en_US';
         }
+        if ($locale === 'de' || $locale === 'de_de') {
+            return 'de_DE';
+        }
         if ($locale === 'it' || $locale === 'it_it') {
             return 'it_IT';
         }
         return 'it_IT';
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/classes/Installer.php` around lines 263 - 265, The installer
currently maps German data but then forces APP_LOCALE to it_IT because the
createEnvFile whitelist and getInstallerLocale logic override non-it/en locales;
update the createEnvFile whitelist to allow 'de_DE' (and equivalent keys like
'de'/'de_de') through when persisting APP_LOCALE, and modify getInstallerLocale
to stop defaulting all non-'it_IT'/'en_US' values to 'it_IT'—instead return the
actual mapped locale (e.g., 'de_DE') when the installer mapping contains it;
adjust any locale-normalization code paths to treat 'de', 'de_de', and 'de_DE'
as 'de_DE' so German installs both seed German data and persist
APP_LOCALE=de_DE.
scripts/manual-upgrade.php (3)

23-23: ⚠️ Potential issue | 🟠 Major

Block execution when default password is unchanged.

Line 23 still uses the known default value; this leaves the upgrader practically guessable if uploaded as-is.

🛡️ Proposed fix
 define('UPGRADE_PASSWORD', 'pinakes2026');
+define('UPGRADE_PASSWORD_IS_DEFAULT', UPGRADE_PASSWORD === 'pinakes2026');
 if ($requestMethod === 'POST' && isset($_POST['password'])) {
-    if ($_POST['password'] === UPGRADE_PASSWORD) {
+    if (UPGRADE_PASSWORD_IS_DEFAULT) {
+        $error = 'SICUREZZA: cambia la password nel file prima di procedere.';
+    } elseif ($_POST['password'] === UPGRADE_PASSWORD) {
         $_SESSION['upgrade_auth'] = true;
     } else {
         $error = 'Password errata.';
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` at line 23, The script still defines the known
default upgrade password via the constant UPGRADE_PASSWORD ('pinakes2026'); stop
blind execution by detecting this default at runtime and aborting: replace the
hardcoded use with a required configuration (e.g., read from an environment
variable) and add a guard in the script startup that checks if UPGRADE_PASSWORD
equals 'pinakes2026' (or is empty) and immediately exit with an error message
instructing the operator to set a secure password before proceeding.

423-451: ⚠️ Potential issue | 🟠 Major

multi_query can hide statement-level migration failures.

Lines 424-451 still use multi_query + final errno/error, which can miss mid-batch failures and mark a migration as completed incorrectly.

#!/bin/bash
# Verify migration execution strategy and post-check logic
rg -n "multi_query|store_result|next_result|lastErrno|lastError|INSERT IGNORE INTO migrations" scripts/manual-upgrade.php -C4
🛠️ Proposed direction
-                // Execute multi-statement SQL
-                if ($db->multi_query($sql)) {
-                    do {
-                        $result = $db->store_result();
-                        if ($result instanceof mysqli_result) {
-                            $result->free();
-                        }
-                    } while ($db->next_result());
-                }
-
-                // Check for errors (ignore idempotent ones)
-                $lastError = $db->error;
-                $lastErrno = $db->errno;
+                $statements = preg_split('/;\s*[\r\n]+/', $sql) ?: [];
+                $migrationFailed = false;
+                $lastError = '';
+                $lastErrno = 0;
+
+                foreach ($statements as $stmtSql) {
+                    $stmtSql = trim($stmtSql);
+                    if ($stmtSql === '' || str_starts_with($stmtSql, '--')) {
+                        continue;
+                    }
+                    if (!$db->query($stmtSql)) {
+                        $lastError = $db->error;
+                        $lastErrno = $db->errno;
+                        $migrationFailed = true;
+                        break;
+                    }
+                }
@@
-                if ($lastErrno !== 0 && !in_array($lastErrno, $ignorableErrors, true)) {
+                if ($migrationFailed && !in_array($lastErrno, $ignorableErrors, true)) {
                     $log[] = '[ERROR] Migrazione ' . $migVersion . ' fallita: [' . $lastErrno . '] ' . $lastError;
                 } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 423 - 451, Replace the current
multi-statement execution that uses $db->multi_query, store_result and
next_result with per-statement execution and immediate error checks: split the
$sql batch into individual statements (or iterate through prepared statements),
execute each with $db->query or prepared execution, inspect
$db->errno/$db->error after each statement and log/abort the migration on the
first non-ignorable error (using the same $ignorableErrors list), and only when
all statements succeed insert the migration record via the existing $recStmt
(INSERT IGNORE INTO migrations), push $migVersion onto $migrationsRun and log
success; ensure you still free results where needed and include the failing
statement and its error in the log so mid-batch failures are not hidden by the
final errno check.

29-31: ⚠️ Potential issue | 🟠 Major

Do not render runtime errors to the browser.

Line 30 enables display_errors, which can leak filesystem paths and DB details during failures.

🛡️ Proposed fix
 error_reporting(E_ALL);
-ini_set('display_errors', '1');
+ini_set('display_errors', '0');
+ini_set('log_errors', '1');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 29 - 31, The script currently
enables display of runtime errors via ini_set('display_errors', '1') which can
leak sensitive info; change this to disable browser output by setting
ini_set('display_errors', '0') (or remove the call) and ensure errors are logged
instead (e.g., ini_set('log_errors','1') and a proper error_log target) while
keeping error_reporting(E_ALL) and the max_execution_time setting; update the
ini_set call for 'display_errors' and add/verify logging-related ini_set calls
so runtime errors are not rendered to the browser.
🧹 Nitpick comments (2)
app/Controllers/UpdateController.php (2)

38-38: Remove unused local variable $data.

Line 38 builds $data but never uses it; this is dead local state.

♻️ Proposed fix
-        $data = compact('updateInfo', 'requirements', 'history', 'changelog', 'githubTokenMasked', 'hasGithubToken');
         require __DIR__ . '/../Views/admin/updates.php';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/UpdateController.php` at line 38, Remove the unused local
variable $data created by the compact(...) call in UpdateController (the
assignment "$data = compact('updateInfo', 'requirements', 'history',
'changelog', 'githubTokenMasked', 'hasGithubToken')"); either delete this
assignment entirely or replace usages to consume $updateInfo, $requirements,
$history, $changelog, $githubTokenMasked, and $hasGithubToken directly so there
is no dead local $data left behind.

509-513: Log token save failures for operability.

The catch block returns a generic error but drops the exception context, making incident diagnosis harder.

📝 Proposed fix
         } catch (\Throwable $e) {
+            error_log('[UpdateController] saveToken failed: ' . $e->getMessage());
             return $this->jsonResponse($response, [
                 'success' => false,
                 'error' => __('Errore nel salvataggio del token')
             ], 500);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/UpdateController.php` around lines 509 - 513, In
UpdateController's catch block (the one catching \Throwable $e) replace the
silent swallow with logging of the exception before returning the jsonResponse:
call the application's logger (e.g., $this->logger or app('log')) to record
$e->getMessage() and stack trace (e.g., ->error(...) or logger()->error(...,
['exception' => $e])) and then return the same jsonResponse; ensure you
reference the existing catch block around the token save logic so the log
includes context like "token save failure".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Routes/web.php`:
- Line 39: Guard against an empty locale set by checking the result of
I18n::getAvailableLocales() when building $supportedLocales and providing a
sensible fallback before the route registration loop: after $supportedLocales =
array_keys(I18n::getAvailableLocales()) verify if $supportedLocales is empty and
if so replace it with a default (e.g. [app()->getLocale()] or ['en']) or log an
error; this ensures the subsequent locale-based route registration (the loop
referencing $supportedLocales) still registers routes instead of skipping them
entirely.

In `@app/Views/admin/updates.php`:
- Around line 552-556: The fetch calls that post the GitHub token (the one
constructing body `csrf_token=...&github_token=...` and the similar call at the
other occurrence) must include the Accept and X-Requested-With AJAX headers to
ensure CSRF failures return JSON; update the request headers object used in
those fetch calls to include 'Accept': 'application/json' and
'X-Requested-With': 'XMLHttpRequest' in addition to the existing 'Content-Type':
'application/x-www-form-urlencoded' so response.json() won't throw on HTML error
pages.
- Line 162: The external GitHub link anchor (the <a> with href
"https://github.com/settings/tokens" in the updates view) opens in a new tab
using target="_blank" but lacks rel="noopener noreferrer"; update that anchor in
app/Views/admin/updates.php to include rel="noopener noreferrer" alongside
target="_blank" to prevent window.opener attacks and improve security.
- Around line 153-155: The icon-only remove button lacks an accessible name;
update the button rendered where removeGitHubToken() is invoked so it exposes a
label to assistive tech (e.g., add aria-label="Remove token" or include a
visually hidden text span alongside the <i class="fas fa-trash..."> icon).
Ensure the change preserves existing classes/onclick and keeps visual appearance
(hidden text or aria-only) while providing a clear, descriptive name for screen
readers.

In `@scripts/manual-upgrade.php`:
- Around line 290-298: The DB backup failure branch currently logs a warning and
continues; change it to abort the upgrade when the backup cannot be created by
checking the same conditions around $exitCode and filesize($backupFile) (the
block that builds $log[] and calls `@unlink`($backupFile)), and replace the
"continue without backup" behavior with a fatal stop (e.g., add a
processLogger/error $log[] entry and call exit(1) or throw an exception) so
execution does not proceed past the failed mysqldump.
- Around line 306-312: The extraction currently calls $zip->extractTo($tempDir)
on untrusted $file['tmp_name'], which allows path traversal; instead iterate
entries via ZipArchive::numFiles and ZipArchive::getNameIndex and validate each
name: reject names starting with '/' or drive letters, and reject any that
contain '..' path segments or resolve outside $tempDir when joined; for valid
entries create required directories under $tempDir and extract/write files only
to the sanitized target paths (or skip/throw on invalid entries) rather than
calling extractTo directly; keep using $zip->open and $zip->close but replace
the direct extractTo call with per-entry validation and safe extraction logic
referencing $zip, $tempDir and $file['tmp_name'].

---

Outside diff comments:
In `@README.md`:
- Line 8: Update the stale "Languages" header string that currently reads
"License: GPL-3  |  Languages: Italian, English" to include German (Deutsch) —
e.g., change the header to "License: GPL-3  |  Languages: Italian, English,
German (Deutsch)" or similar; ensure the displayed languages match the added
de_DE locale so the README's languages list and the new locale are consistent.
- Line 219: Update the Quick Start step text so the installer language choice
includes German; replace the existing line "Select language (Italian or
English)." in the README's Quick Start section with a version that lists
Italian, English, and German (e.g., "Select language (Italian, English or
German).") so the documentation matches the newly added German locale.
- Line 344: Update the OPAC feature line that currently reads "Responsive,
multilingual frontend (Italian + English)" to include German; locate the exact
string "Responsive, multilingual frontend (Italian + English)" and change it to
something like "Responsive, multilingual frontend (Italian, English, German)" so
the README accurately lists supported frontend locales.
- Around line 362-370: Add a bullet to the Auto-Updater feature list mentioning
the new optional GitHub personal access token support: note that admins can
store a GitHub API token in system_settings (configurable from Admin → Updates)
to raise GitHub API rate limits and avoid rate-limit errors when using the
built-in update system. Ensure the text matches the style of the other bullets
and briefly states where to configure the token and its purpose.

---

Duplicate comments:
In `@app/Support/Updater.php`:
- Around line 186-200: The code reads and stores the GitHub token in plaintext;
add symmetric encryption/decryption around persistence: implement private
methods encryptUpdaterSecret(string $plain): string and
decryptUpdaterSecret(string $cipher): string that use the application key (from
config/env) and a secure crypto library, then update the Updater DB write path
to call encryptUpdaterSecret before binding/storing the setting and update the
read path (the block using $stmt->bind_param / $stmt->execute /
$result->fetch_column and the later similar block around lines 224-241) to
retrieve the stored value and call decryptUpdaterSecret before assigning
$this->githubToken; ensure existing code handles decryption failures gracefully
and keep DB column as ciphertext.
- Around line 193-196: The execute()/get_result()/fetch_column() calls in
Updater.php (used by loadGitHubToken and saveGitHubToken) assume success and can
silently fail; modify the code around $stmt->execute(), $stmt->get_result(), and
$result->fetch_column() to check the boolean/return values, log or throw a clear
error when execute() returns false, verify get_result() !== false before using
it, handle a false/null fetch_column() appropriately (treat as missing token or
error), ensure $stmt->close() always runs (use finally-like cleanup), and
propagate failure to the caller so state cannot silently desync.

In `@installer/classes/Installer.php`:
- Around line 263-265: The installer currently maps German data but then forces
APP_LOCALE to it_IT because the createEnvFile whitelist and getInstallerLocale
logic override non-it/en locales; update the createEnvFile whitelist to allow
'de_DE' (and equivalent keys like 'de'/'de_de') through when persisting
APP_LOCALE, and modify getInstallerLocale to stop defaulting all
non-'it_IT'/'en_US' values to 'it_IT'—instead return the actual mapped locale
(e.g., 'de_DE') when the installer mapping contains it; adjust any
locale-normalization code paths to treat 'de', 'de_de', and 'de_DE' as 'de_DE'
so German installs both seed German data and persist APP_LOCALE=de_DE.

In `@installer/database/data_de_DE.sql`:
- Around line 7-342: The seed file uses plain INSERTs with explicit IDs (e.g.
INSERT INTO `generi` VALUES (...), INSERT INTO `cms_pages`, INSERT INTO
`email_templates`, INSERT INTO `languages`, INSERT INTO `home_content`, INSERT
INTO `themes`) which will fail on reruns due to duplicate keys; make these
statements idempotent by converting them to either INSERT ... ON DUPLICATE KEY
UPDATE (updating relevant columns like setting_value/description/updated_at or
content fields) or INSERT IGNORE where overwriting isn't desired, and apply the
same pattern to the bulk generi inserts and other explicit-id inserts so the
script can be safely re-run without error.

In `@README.md`:
- Around line 28-45: The v0.4.9.2 changelog entry in README.md is missing
several items; update the release notes under the "🏷️ Genre Management, Book
Filters & Bug Fixes" (or the v0.4.9.2 section) to include entries for the German
locale addition, dynamic locale route registration, the GitHub API token support
for the updater, and the manual upgrade script; add concise bullet points
matching the existing style and wording so the changelog reflects all features
shipped in this PR.

In `@scripts/manual-upgrade.php`:
- Line 23: The script still defines the known default upgrade password via the
constant UPGRADE_PASSWORD ('pinakes2026'); stop blind execution by detecting
this default at runtime and aborting: replace the hardcoded use with a required
configuration (e.g., read from an environment variable) and add a guard in the
script startup that checks if UPGRADE_PASSWORD equals 'pinakes2026' (or is
empty) and immediately exit with an error message instructing the operator to
set a secure password before proceeding.
- Around line 423-451: Replace the current multi-statement execution that uses
$db->multi_query, store_result and next_result with per-statement execution and
immediate error checks: split the $sql batch into individual statements (or
iterate through prepared statements), execute each with $db->query or prepared
execution, inspect $db->errno/$db->error after each statement and log/abort the
migration on the first non-ignorable error (using the same $ignorableErrors
list), and only when all statements succeed insert the migration record via the
existing $recStmt (INSERT IGNORE INTO migrations), push $migVersion onto
$migrationsRun and log success; ensure you still free results where needed and
include the failing statement and its error in the log so mid-batch failures are
not hidden by the final errno check.
- Around line 29-31: The script currently enables display of runtime errors via
ini_set('display_errors', '1') which can leak sensitive info; change this to
disable browser output by setting ini_set('display_errors', '0') (or remove the
call) and ensure errors are logged instead (e.g., ini_set('log_errors','1') and
a proper error_log target) while keeping error_reporting(E_ALL) and the
max_execution_time setting; update the ini_set call for 'display_errors' and
add/verify logging-related ini_set calls so runtime errors are not rendered to
the browser.

---

Nitpick comments:
In `@app/Controllers/UpdateController.php`:
- Line 38: Remove the unused local variable $data created by the compact(...)
call in UpdateController (the assignment "$data = compact('updateInfo',
'requirements', 'history', 'changelog', 'githubTokenMasked',
'hasGithubToken')"); either delete this assignment entirely or replace usages to
consume $updateInfo, $requirements, $history, $changelog, $githubTokenMasked,
and $hasGithubToken directly so there is no dead local $data left behind.
- Around line 509-513: In UpdateController's catch block (the one catching
\Throwable $e) replace the silent swallow with logging of the exception before
returning the jsonResponse: call the application's logger (e.g., $this->logger
or app('log')) to record $e->getMessage() and stack trace (e.g., ->error(...) or
logger()->error(..., ['exception' => $e])) and then return the same
jsonResponse; ensure you reference the existing catch block around the token
save logic so the log includes context like "token save failure".

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and 4902b32.

📒 Files selected for processing (13)
  • README.md
  • app/Controllers/UpdateController.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • scripts/manual-upgrade.php

README.md:
- Add Deutsch to language header, Quick Start, OPAC description
- Add German locale and updater improvements to v0.4.9.2 changelog
- Add GitHub API token bullet to Auto-Updater features

installer/classes/Installer.php:
- Add de_DE to createEnvFile locale whitelist and normalization
- Add de_DE to getInstallerLocale() so German installs persist APP_LOCALE=de_DE

installer/database/data_de_DE.sql:
- Make all INSERT statements idempotent (INSERT IGNORE / ON DUPLICATE KEY UPDATE)

scripts/manual-upgrade.php:
- Disable display_errors, enable log_errors
- Block execution when default password is unchanged
- Abort upgrade on DB backup failure (mandatory backup)
- Validate ZIP entries for path traversal before extraction
- Replace multi_query with per-statement execution for migration error detection

app/Routes/web.php:
- Guard against empty locale set with fallback to installation locale

app/Support/Updater.php:
- Add error handling for execute()/get_result() in token load/save

app/Controllers/UpdateController.php:
- Log token save failures via SecureLogger

app/Views/admin/updates.php:
- Add aria-label/title to icon-only remove button
- Add rel="noopener noreferrer" to external GitHub link
- Add Accept/X-Requested-With headers to token fetch calls
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Views/admin/updates.php (1)

289-289: ⚠️ Potential issue | 🟡 Minor

External link missing rel="noopener noreferrer".

The GitHub releases link on line 289 uses target="_blank" without rel="noopener noreferrer", while the same fix was applied to the tokens link on line 162.

🔐 Proposed fix
-                                <p class="mt-1"><?= __("Vai alla") ?> <a href="https://github.com/fabiodalez-dev/Pinakes/releases" target="_blank" class="text-green-600 hover:text-green-700 underline">pagina releases</a> <?= __("e scarica il file") ?> <code class="bg-gray-100 px-1 rounded text-xs">pinakes-vX.X.X.zip</code></p>
+                                <p class="mt-1"><?= __("Vai alla") ?> <a href="https://github.com/fabiodalez-dev/Pinakes/releases" target="_blank" rel="noopener noreferrer" class="text-green-600 hover:text-green-700 underline">pagina releases</a> <?= __("e scarica il file") ?> <code class="bg-gray-100 px-1 rounded text-xs">pinakes-vX.X.X.zip</code></p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/admin/updates.php` at line 289, The anchor element that renders the
GitHub releases link in the updates view (the <a
href="https://github.com/fabiodalez-dev/Pinakes/releases" target="_blank" ...>
anchor near the "Vai alla" text) opens a new tab without rel="noopener
noreferrer"; update that anchor to include rel="noopener noreferrer" (matching
the tokens link fix already applied elsewhere) to prevent window.opener-based
attacks and ensure safe external linking.
♻️ Duplicate comments (5)
installer/database/data_de_DE.sql (1)

4-347: ⚠️ Potential issue | 🟠 Major

FOREIGN_KEY_CHECKS can remain disabled if the script aborts mid-run.

If execution stops before the final restore, the session keeps FK checks off. This was previously raised and still applies.

Hardening option
-SET FOREIGN_KEY_CHECKS=0;
+-- Prefer wrapping in a procedure with an EXIT HANDLER that always restores:
+-- 1) SET FOREIGN_KEY_CHECKS=0
+-- 2) run seed statements
+-- 3) SET FOREIGN_KEY_CHECKS=1 in both success and SQLEXCEPTION paths
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/database/data_de_DE.sql` around lines 4 - 347, The script leaves
FOREIGN_KEY_CHECKS disabled for the whole run which can persist if execution
aborts; instead ensure FK checks are restored no matter what by (1) removing the
global SET FOREIGN_KEY_CHECKS=0/1 pair at the top/footer and (2) if you must
disable constraints, scope the disable to the minimal block(s) that require it
(surround only those INSERT IGNORE INTO `generi` / other problematic statements
with SET FOREIGN_KEY_CHECKS=0; ... SET FOREIGN_KEY_CHECKS=1;) and (3) add an
explicit safety guard near the top of the script that forces SET
FOREIGN_KEY_CHECKS=1; to ensure checks are enabled if a previous run aborted;
look for the existing FOREIGN_KEY_CHECKS lines to update.
app/Support/Updater.php (1)

205-207: ⚠️ Potential issue | 🟠 Major

Encrypt the GitHub token before storing it.

At Line 205 and Line 243, the token is still read/written as plaintext in system_settings.setting_value. DB dumps/backups would expose a live credential.

🔐 Suggested direction
-        if (is_string($value) && $value !== '') {
-            $this->githubToken = $value;
+        if (is_string($value) && trim($value) !== '') {
+            $decrypted = $this->decryptUpdaterSecret($value);
+            if ($decrypted !== '') {
+                $this->githubToken = $decrypted;
+            }
         }
-        $stmt->bind_param('sss', $cat, $key, $token);
+        $storedToken = $token === '' ? '' : $this->encryptUpdaterSecret($token);
+        $stmt->bind_param('sss', $cat, $key, $storedToken);
private function encryptUpdaterSecret(string $plain): string { /* app-key backed encryption */ }
private function decryptUpdaterSecret(string $cipher): string { /* return '' on invalid/corrupt */ }

Also applies to: 231-252

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 205 - 207, The GitHub token is
currently stored and read in plaintext into $this->githubToken and saved into
system_settings.setting_value; implement app-key backed encryption by adding
helper methods encryptUpdaterSecret(string $plain): string and
decryptUpdaterSecret(string $cipher): string (decrypt should return '' on
invalid/corrupt data) and update the setter/reader logic to call
encryptUpdaterSecret before persisting the token and decryptUpdaterSecret when
loading it into $this->githubToken (ensure all places that write/read the token
in Updater.php use these helpers).
scripts/manual-upgrade.php (1)

320-329: ⚠️ Potential issue | 🟠 Major

ZIP path validation is incomplete for Windows-style absolute paths.

At Line 321-324, traversal checks cover / and ../, but not entries like C:/.... With extractTo() at Line 328, that can still escape the temp target on Windows hosts.

#!/bin/bash
# Verify current ZIP entry guards and check whether drive-letter guard exists.
rg -n "getNameIndex|str_starts_with\\(\\$entry, '/\\)|str_contains\\(\\$entry, '\\.\\./'|extractTo\\(\\$tempDir\\)" scripts/manual-upgrade.php -C3
rg -n "A-Za-z\\]:\\[\\\\/\\]" scripts/manual-upgrade.php
# Expected: first command shows current checks; second should return no matches (missing drive-letter guard).
🛡️ Minimal hardening patch
-            if (
-                str_starts_with($entry, '/')
-                || str_contains($entry, '../')
-                || str_contains($entry, '..\\')
-            ) {
+            if (preg_match('#(^/|^[A-Za-z]:/|(^|/)\.\.(/|$))#', $entry)) {
                 throw new RuntimeException('ZIP non valido: contiene percorsi pericolosi');
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 320 - 329, The ZIP entry validation
misses Windows absolute paths and leading backslashes; update the guard that
checks $entry (used with str_starts_with/str_contains and before
$zip->extractTo($tempDir)) to also reject entries that start with a backslash
and entries matching a Windows drive-letter absolute path (e.g. pattern like
/^[A-Za-z]:[\/\\]/). Throw the same RuntimeException when such entries are
detected (referencing $entry, str_starts_with, str_contains, and extractTo) so
extractTo cannot escape the target on Windows.
app/Routes/web.php (1)

39-42: LGTM — empty-locale fallback properly addresses the earlier concern.

The guard correctly prevents route registration from being skipped when the DB returns no languages, falling back to the installation locale or it_IT.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Routes/web.php` around lines 39 - 42, No change required: the fallback
logic around $supportedLocales correctly uses
array_keys(I18n::getAvailableLocales()) and substitutes [$installationLocale ?:
'it_IT'] when empty; leave the $supportedLocales assignment and guard as-is to
ensure routes register even when the DB returns no locales.
app/Views/admin/updates.php (1)

119-170: Past review findings (accessibility, rel="noopener", AJAX headers) have all been addressed.

  • Line 153: aria-label and title on the remove button ✓
  • Line 162: rel="noopener noreferrer" on the external link ✓
  • Lines 554-557, 596-599: Accept: application/json and X-Requested-With headers ✓
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/admin/updates.php` around lines 119 - 170, The review comment
includes a stray duplicate marker ([duplicate_comment]) after the approval
tokens; remove that duplicate marker so only the intended [approve_code_changes]
remains in the review text, no code edits required—verify the referenced UI
elements (removeGitHubToken() button aria/title, the external link with
rel="noopener noreferrer", and the AJAX headers) remain unchanged.
🧹 Nitpick comments (1)
app/Routes/web.php (1)

601-704: Legacy redirect blocks only handle en_US and it_IT — no coverage for de_DE (or future locales).

With dynamic locale registration, new locales like de_DE are now automatically supported for route generation, but the legacy redirect mechanism here is hard-coded to two locales. A German installation won't get redirects from English or Italian URLs to the canonical German ones.

This isn't a bug today (there are no legacy German URLs in the wild), but as more locales are added the pattern doesn't scale. Consider a data-driven approach or at minimum a comment noting the limitation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Routes/web.php` around lines 601 - 704, The legacy-redirect logic is
hard-coded for only en_US and it_IT; replace it with a data-driven approach by
extracting the legacyRedirects arrays into a mapping keyed by source locale
(e.g., $legacyRedirectMap = ['en_US' => [...], 'it_IT' => [...]]), then iterate
over each $sourceLocale => $legacyRedirects and, for any $sourceLocale !==
$installationLocale, register the redirects using
RouteTranslator::route($routeKey) and $registerRouteIfUnique('GET', $legacyPath,
...) as currently used; alternatively, if you intentionally want to limit
redirects, add a clear comment near $installationLocale and the existing blocks
explaining the limitation. Ensure you reference and update the blocks that
currently use $legacyRedirects, RouteTranslator::route, $installationLocale, and
$registerRouteIfUnique.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Views/admin/updates.php`:
- Around line 145-148: The placeholder for the github-token input is invisible
because it's a password field; change the UI so the masked value is visible:
when $hasGithubToken is true, render the input with type="text" (or render a
separate read-only element) so HtmlHelper::e($githubTokenMasked) is readable—use
the id="github-token" to locate the element and adjust its type or add a sibling
read-only span/input to show $githubTokenMasked while keeping the
editable/secret input behavior (autocomplete="off") for actual token entry.

In `@installer/database/data_de_DE.sql`:
- Around line 313-317: The INSERT ... ON DUPLICATE KEY UPDATE for the languages
table only updates name/is_active/is_default, leaving translation_file and the
completion counters stale; modify the ON DUPLICATE KEY UPDATE clause (for the
INSERT that touches `languages` with columns `translation_file`, `total_keys`,
`translated_keys`, `completion_percentage`) to also assign those columns from
VALUES(...) so that `translation_file`, `total_keys`, `translated_keys`, and
`completion_percentage` are refreshed on upsert.
- Around line 320-333: The INSERT INTO home_content upsert only updates the
content column so rerunning the seed won't refresh translated strings in title,
subtitle, button_text or button_link; modify the ON DUPLICATE KEY UPDATE clause
in this INSERT (the statement inserting rows like 'hero', 'features_title',
'feature_1', etc.) to also update title, subtitle, button_text and button_link =
VALUES(title), VALUES(subtitle), VALUES(button_text), VALUES(button_link)
(keeping content = VALUES(content)) so all translated fields are overwritten on
rerun.

In `@scripts/manual-upgrade.php`:
- Around line 451-474: The loop currently breaks on the first SQL error and
later records the migration as complete even if that first error was ignorable;
change the error handling inside the loop where $db->query($stmtSql) fails so it
checks $lastErrno against the $ignorableErrors list and does NOT break for
ignorable errors (log them and continue), but still sets $migrationFailed = true
and breaks only for non-ignorable errors; then ensure the post-loop logic that
inserts the migration record (using $recStmt and adding to $migrationsRun) only
runs when $migrationFailed is false (i.e., all non-ignorable statements
succeeded), otherwise push an error log and skip recording the migration.

---

Outside diff comments:
In `@app/Views/admin/updates.php`:
- Line 289: The anchor element that renders the GitHub releases link in the
updates view (the <a href="https://github.com/fabiodalez-dev/Pinakes/releases"
target="_blank" ...> anchor near the "Vai alla" text) opens a new tab without
rel="noopener noreferrer"; update that anchor to include rel="noopener
noreferrer" (matching the tokens link fix already applied elsewhere) to prevent
window.opener-based attacks and ensure safe external linking.

---

Duplicate comments:
In `@app/Routes/web.php`:
- Around line 39-42: No change required: the fallback logic around
$supportedLocales correctly uses array_keys(I18n::getAvailableLocales()) and
substitutes [$installationLocale ?: 'it_IT'] when empty; leave the
$supportedLocales assignment and guard as-is to ensure routes register even when
the DB returns no locales.

In `@app/Support/Updater.php`:
- Around line 205-207: The GitHub token is currently stored and read in
plaintext into $this->githubToken and saved into system_settings.setting_value;
implement app-key backed encryption by adding helper methods
encryptUpdaterSecret(string $plain): string and decryptUpdaterSecret(string
$cipher): string (decrypt should return '' on invalid/corrupt data) and update
the setter/reader logic to call encryptUpdaterSecret before persisting the token
and decryptUpdaterSecret when loading it into $this->githubToken (ensure all
places that write/read the token in Updater.php use these helpers).

In `@app/Views/admin/updates.php`:
- Around line 119-170: The review comment includes a stray duplicate marker
([duplicate_comment]) after the approval tokens; remove that duplicate marker so
only the intended [approve_code_changes] remains in the review text, no code
edits required—verify the referenced UI elements (removeGitHubToken() button
aria/title, the external link with rel="noopener noreferrer", and the AJAX
headers) remain unchanged.

In `@installer/database/data_de_DE.sql`:
- Around line 4-347: The script leaves FOREIGN_KEY_CHECKS disabled for the whole
run which can persist if execution aborts; instead ensure FK checks are restored
no matter what by (1) removing the global SET FOREIGN_KEY_CHECKS=0/1 pair at the
top/footer and (2) if you must disable constraints, scope the disable to the
minimal block(s) that require it (surround only those INSERT IGNORE INTO
`generi` / other problematic statements with SET FOREIGN_KEY_CHECKS=0; ... SET
FOREIGN_KEY_CHECKS=1;) and (3) add an explicit safety guard near the top of the
script that forces SET FOREIGN_KEY_CHECKS=1; to ensure checks are enabled if a
previous run aborted; look for the existing FOREIGN_KEY_CHECKS lines to update.

In `@scripts/manual-upgrade.php`:
- Around line 320-329: The ZIP entry validation misses Windows absolute paths
and leading backslashes; update the guard that checks $entry (used with
str_starts_with/str_contains and before $zip->extractTo($tempDir)) to also
reject entries that start with a backslash and entries matching a Windows
drive-letter absolute path (e.g. pattern like /^[A-Za-z]:[\/\\]/). Throw the
same RuntimeException when such entries are detected (referencing $entry,
str_starts_with, str_contains, and extractTo) so extractTo cannot escape the
target on Windows.

---

Nitpick comments:
In `@app/Routes/web.php`:
- Around line 601-704: The legacy-redirect logic is hard-coded for only en_US
and it_IT; replace it with a data-driven approach by extracting the
legacyRedirects arrays into a mapping keyed by source locale (e.g.,
$legacyRedirectMap = ['en_US' => [...], 'it_IT' => [...]]), then iterate over
each $sourceLocale => $legacyRedirects and, for any $sourceLocale !==
$installationLocale, register the redirects using
RouteTranslator::route($routeKey) and $registerRouteIfUnique('GET', $legacyPath,
...) as currently used; alternatively, if you intentionally want to limit
redirects, add a clear comment near $installationLocale and the existing blocks
explaining the limitation. Ensure you reference and update the blocks that
currently use $legacyRedirects, RouteTranslator::route, $installationLocale, and
$registerRouteIfUnique.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4902b32 and f46d244.

📒 Files selected for processing (8)
  • README.md
  • app/Controllers/UpdateController.php
  • app/Routes/web.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • scripts/manual-upgrade.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • installer/classes/Installer.php

updates.php:
- Add rel="noopener noreferrer" to GitHub releases link (line 289)
- Show masked token as readable text, switch to password on focus

data_de_DE.sql:
- Expand languages upsert to refresh all metadata columns
- Expand home_content upsert to refresh title/subtitle/button fields
- Add FK_CHECKS=1 safety guard at script start

manual-upgrade.php:
- Harden ZIP path validation with Windows drive-letter check
- Fix migration: ignorable errors continue execution, only fatal errors break
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a 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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/manual-upgrade.php`:
- Around line 461-472: When a migration fails the code currently only appends to
$log and continues; change this so the upgrade aborts on failure: either
immediately stop execution after logging by calling exit(1) (or throw an
Exception) when $migrationFailed is true, or set a persistent failure flag (e.g.
$upgradeFailed = true) and after the migration loop check that flag before
printing the final success message and exit with non-zero status; reference the
$migrationFailed check, the $log array, $migrationsRun handling and the final
success message to ensure the script never prints success or returns zero when
any migration failed.
- Around line 186-190: The copy() call that writes $srcPath to $dstPath is
unchecked and $count is incremented regardless; change the logic in the block
around copy($srcPath, $dstPath) so you check its boolean return value, only
increment $count when copy(...) returns true, and handle failures (e.g., log an
error/throw an exception or collect a failure list) instead of silently
proceeding; keep the existing is_dir(dirname($dstPath)) / mkdir(...) behavior
but ensure error handling uses the same context variables ($srcPath, $dstPath,
$count) so partial upgrade failures are not masked.
- Line 93: Enabling mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)
causes SQL errors to throw mysqli_sql_exception and bypass the existing
ignorable-error handling around $db->query(); wrap the database query execution
where ignorable errors are checked (the block that inspects $db->query() result
around the existing ignorable-errors logic) in a try-catch that catches
mysqli_sql_exception, examine the exception message/code for the same ignorable
cases (duplicate column/table/index, already exists, etc.), and if it matches
treat it as the original false-return path (log/skip) otherwise rethrow or
abort; ensure this change keeps mysqli_report(...) as-is and only adds the
try-catch around the $db->query() calls that previously relied on false returns.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f46d244 and 4449401.

📒 Files selected for processing (3)
  • app/Views/admin/updates.php
  • installer/database/data_de_DE.sql
  • scripts/manual-upgrade.php

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
scripts/manual-upgrade.php (2)

186-190: ⚠️ Potential issue | 🟠 Major

Do not count files as copied when copy() fails.

At Line 189, copy($srcPath, $dstPath) is unchecked and Line 190 increments $count anyway, which can mask partial upgrade failures.

Proposed fix
-            copy($srcPath, $dstPath);
-            $count++;
+            if (!copy($srcPath, $dstPath)) {
+                throw new RuntimeException('Copia file fallita: ' . $srcPath . ' -> ' . $dstPath);
+            }
+            $count++;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 186 - 190, The code currently calls
copy($srcPath, $dstPath) and increments $count unconditionally; change this so
the return value of copy() is checked and $count is only incremented when copy
returns true. Locate the block around copy($srcPath, $dstPath) that uses
$srcPath, $dstPath and $count, and if copy fails handle the error (e.g.
log/throw/collect failures) instead of incrementing $count to avoid masking
partial upgrade failures.

445-472: ⚠️ Potential issue | 🔴 Critical

Migration error handling is inconsistent and can still report a successful upgrade.

Two coupled problems remain:

  1. With MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT, SQL errors throw instead of returning false, so the ignorable-error branch at Line 450 is bypassed.
  2. If $migrationFailed is true, Line 462 only logs and the script can still end with success at Line 513.

This can leave a partially migrated DB while signaling success.

In PHP MySQLi, when mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT) is enabled, does mysqli::query() return false on SQL error or throw mysqli_sql_exception?
Proposed fix
                 foreach ($statements as $stmtSql) {
                     $stmtSql = trim($stmtSql);
                     if ($stmtSql === '' || str_starts_with($stmtSql, '--')) {
                         continue;
                     }
-                    if (!$db->query($stmtSql)) {
-                        $lastError = $db->error;
-                        $lastErrno = $db->errno;
-                        if (in_array($lastErrno, $ignorableErrors, true)) {
-                            continue;
-                        }
-                        $migrationFailed = true;
-                        break;
-                    }
+                    try {
+                        $db->query($stmtSql);
+                    } catch (mysqli_sql_exception $ex) {
+                        $lastErrno = (int) $ex->getCode();
+                        $lastError = $ex->getMessage();
+                        if (in_array($lastErrno, $ignorableErrors, true)) {
+                            continue;
+                        }
+                        $migrationFailed = true;
+                        break;
+                    }
                 }

                 if ($migrationFailed) {
-                    $log[] = '[ERROR] Migrazione ' . $migVersion . ' fallita: [' . $lastErrno . '] ' . $lastError;
+                    throw new RuntimeException('Migrazione ' . $migVersion . ' fallita: [' . $lastErrno . '] ' . $lastError);
                 } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 445 - 472, The migration loop
currently assumes $db->query returns false on error and sets $migrationFailed,
but with mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT) enabled SQL
errors throw mysqli_sql_exception and bypass the ignorable-errors branch; update
the logic around the execution of statements (the foreach that uses $db->query)
to either temporarily suppress strict reporting or wrap each execution in a
try/catch that catches mysqli_sql_exception, extracts errno/message (use
$e->getCode() and $e->getMessage()) to compare against $ignorableErrors and only
set $migrationFailed when appropriate; ensure that when $migrationFailed is true
you never run the migration recording block (the prepare/execute/close using
$recStmt and pushing $migrationsRun) and that the script exits/fails (non-zero)
or ultimately reports overall failure instead of success so partial migrations
are not signaled as completed.
app/Support/Updater.php (1)

205-207: ⚠️ Potential issue | 🟠 Major

Persisting GitHub PAT in plaintext is still a credential-exposure risk.

github_token is read/written as clear text, so DB dumps or low-privileged DB reads expose a live secret. Please encrypt at rest before writing to system_settings, and decrypt only when constructing auth headers in memory.

Also applies to: 231-252

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 205 - 207, The github token is being
stored as plaintext in the updater (property $this->githubToken) which risks
credential exposure; change the write path so the value is encrypted before
persisting to system_settings and change all read/usage paths so decryption
happens only in-memory when constructing auth headers (e.g., when building
Authorization headers or any method that reads $this->githubToken).
Specifically: encrypt the token before assigning/persisting in the Updater class
(where $this->githubToken is set) and decrypt right before use (e.g., in the
method that builds HTTP auth headers or any function that references
$this->githubToken); apply the same change to the other block referenced (lines
231–252) to ensure stored values are encrypted at rest and only decrypted in
memory.
🧹 Nitpick comments (1)
installer/database/data_de_DE.sql (1)

4-5: Remove redundant FOREIGN_KEY_CHECKS toggle preface.

Line 4 (SET FOREIGN_KEY_CHECKS=1;) is immediately overridden by Line 5, so it adds noise without effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/database/data_de_DE.sql` around lines 4 - 5, Remove the redundant
preface statement "SET FOREIGN_KEY_CHECKS=1;" since it is immediately overridden
by the following "SET FOREIGN_KEY_CHECKS=0;"; delete the line containing SET
FOREIGN_KEY_CHECKS=1; so the script only toggles FOREIGN_KEY_CHECKS to 0 where
intended and avoids confusing/noisy statements.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Controllers/UpdateController.php`:
- Around line 500-503: Updater::saveGitHubToken currently persists the raw PAT
into system_settings.setting_value; change it to store an encrypted value
instead (or move storage to a secure secrets backend) so backups/dumps don't
expose plaintext. Update Updater::saveGitHubToken to encrypt the token with the
app's encryption service/key before writing, update the corresponding reader
(e.g., Updater::getGitHubToken or wherever the value is read) to decrypt on
load, and ensure any DB column usage still references
system_settings.setting_value only for ciphertext (or switch to a dedicated
secrets table/vault integration) and add tests verifying encryption/decryption
round-trip and that plaintext is never written.

In `@installer/classes/Installer.php`:
- Around line 77-88: The locale normalization currently lowercases but doesn't
convert hyphens to underscores so inputs like "de-DE" bypass the mapping and hit
the fallback; update the normalization of $locale in Installer.php by replacing
hyphens with underscores (e.g., $normalizedLocale = str_replace('-', '_',
strtolower((string)$locale));) before the conditional mapping for 'en', 'en_us',
'de', 'de_de', 'it', 'it_it', and ensure the in_array check still tests against
the canonical targets ['it_IT','en_US','de_DE'] so mapped values match the
fallback check.

In `@installer/index.php`:
- Around line 36-41: The locale normalization is inconsistent: $locale is
normalized to $localeCode using a match expression before building
$translationFile but the same short-form mapping isn't applied when setting the
HTML lang attribute later; unify by extracting the normalization into a single
helper (e.g., normalizeLocale) and use it in both places so 'en' and 'de'
produce the same canonical form for $translationFile and the HTML lang attribute
(update uses of $locale, $localeCode and the HTML-lang assignment to call the
helper).

In `@README.md`:
- Line 8: The tagline line "License: GPL-3  |  Languages: Italian, English,
Deutsch" uses mixed language names; update that line in README.md to be
consistent—either change "Deutsch" to "German" to keep all names in English, or
change "Italian, English, Deutsch" to native names like "Italiano, English,
Deutsch" (or better yet "Italiano, English, Deutsch" making all native) and save
the file.

---

Duplicate comments:
In `@app/Support/Updater.php`:
- Around line 205-207: The github token is being stored as plaintext in the
updater (property $this->githubToken) which risks credential exposure; change
the write path so the value is encrypted before persisting to system_settings
and change all read/usage paths so decryption happens only in-memory when
constructing auth headers (e.g., when building Authorization headers or any
method that reads $this->githubToken). Specifically: encrypt the token before
assigning/persisting in the Updater class (where $this->githubToken is set) and
decrypt right before use (e.g., in the method that builds HTTP auth headers or
any function that references $this->githubToken); apply the same change to the
other block referenced (lines 231–252) to ensure stored values are encrypted at
rest and only decrypted in memory.

In `@scripts/manual-upgrade.php`:
- Around line 186-190: The code currently calls copy($srcPath, $dstPath) and
increments $count unconditionally; change this so the return value of copy() is
checked and $count is only incremented when copy returns true. Locate the block
around copy($srcPath, $dstPath) that uses $srcPath, $dstPath and $count, and if
copy fails handle the error (e.g. log/throw/collect failures) instead of
incrementing $count to avoid masking partial upgrade failures.
- Around line 445-472: The migration loop currently assumes $db->query returns
false on error and sets $migrationFailed, but with
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT) enabled SQL errors
throw mysqli_sql_exception and bypass the ignorable-errors branch; update the
logic around the execution of statements (the foreach that uses $db->query) to
either temporarily suppress strict reporting or wrap each execution in a
try/catch that catches mysqli_sql_exception, extracts errno/message (use
$e->getCode() and $e->getMessage()) to compare against $ignorableErrors and only
set $migrationFailed when appropriate; ensure that when $migrationFailed is true
you never run the migration recording block (the prepare/execute/close using
$recStmt and pushing $migrationsRun) and that the script exits/fails (non-zero)
or ultimately reports overall failure instead of success so partial migrations
are not signaled as completed.

---

Nitpick comments:
In `@installer/database/data_de_DE.sql`:
- Around line 4-5: Remove the redundant preface statement "SET
FOREIGN_KEY_CHECKS=1;" since it is immediately overridden by the following "SET
FOREIGN_KEY_CHECKS=0;"; delete the line containing SET FOREIGN_KEY_CHECKS=1; so
the script only toggles FOREIGN_KEY_CHECKS to 0 where intended and avoids
confusing/noisy statements.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and 4449401.

📒 Files selected for processing (13)
  • README.md
  • app/Controllers/UpdateController.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • scripts/manual-upgrade.php

Comment on lines +500 to +503
try {
$updater = new Updater($db);
$updater->saveGitHubToken($token);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Persisting GitHub PAT in plaintext is a security posture gap.

This path stores the raw token via Updater::saveGitHubToken, and that implementation writes directly to system_settings.setting_value. DB backups/dumps would expose the credential.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/UpdateController.php` around lines 500 - 503,
Updater::saveGitHubToken currently persists the raw PAT into
system_settings.setting_value; change it to store an encrypted value instead (or
move storage to a secure secrets backend) so backups/dumps don't expose
plaintext. Update Updater::saveGitHubToken to encrypt the token with the app's
encryption service/key before writing, update the corresponding reader (e.g.,
Updater::getGitHubToken or wherever the value is read) to decrypt on load, and
ensure any DB column usage still references system_settings.setting_value only
for ciphertext (or switch to a dedicated secrets table/vault integration) and
add tests verifying encryption/decryption round-trip and that plaintext is never
written.

manual-upgrade.php:
- Check copy() return value, throw on failure
- Wrap migration queries in try/catch for mysqli_sql_exception
  (MYSQLI_REPORT_STRICT throws instead of returning false)
- Abort upgrade on migration failure (throw RuntimeException)

Installer.php:
- Normalize hyphens to underscores in locale (de-DE -> de_DE)

installer/index.php:
- Handle short-form locale codes in HTML lang mapping

data_de_DE.sql:
- Remove redundant FK_CHECKS=1 preface

README.md:
- Use consistent English names (German, not Deutsch)
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
app/Support/Updater.php (1)

205-207: ⚠️ Potential issue | 🟠 Major

Store the GitHub token encrypted at rest (currently plaintext).

At Line [205] and Line [251], the token is read/written directly in cleartext. That exposes a live credential in DB dumps and backups.

🔐 Suggested fix
@@
-        if (is_string($value) && $value !== '') {
-            $this->githubToken = $value;
+        if (is_string($value) && trim($value) !== '') {
+            $decrypted = $this->decryptUpdaterSecret($value);
+            if ($decrypted !== '') {
+                $this->githubToken = $decrypted;
+            }
         }
@@
-        $stmt->bind_param('sss', $cat, $key, $token);
+        $encryptedToken = $token === '' ? '' : $this->encryptUpdaterSecret($token);
+        $stmt->bind_param('sss', $cat, $key, $encryptedToken);
@@
         $this->githubToken = $token;
     }
+
+    private function encryptUpdaterSecret(string $plain): string
+    {
+        // Use your app key (e.g. PLUGIN_ENCRYPTION_KEY) and authenticated encryption.
+        // Return base64-encoded ciphertext with nonce/tag payload.
+    }
+
+    private function decryptUpdaterSecret(string $cipher): string
+    {
+        // Reverse of encryptUpdaterSecret(); return '' on invalid payload/decrypt failure.
+    }

Also applies to: 231-252

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 205 - 207, The GitHub token is being
stored in plaintext via assignments to $this->githubToken; change the write path
(e.g., setGithubToken or where $this->githubToken is assigned at the shown
snippet) to encrypt the token before persisting (use your app's encryption API,
e.g., Laravel's Crypt::encryptString) and change the read path (where the token
is read back, e.g., getGithubToken or the code at lines ~231-252) to decrypt it
(Crypt::decryptString) before use; ensure the stored property/DB column holds
the encrypted string, handle empty strings/null the same as today, and add
try/catch around decrypt to surface errors rather than returning raw ciphertext.
app/Controllers/UpdateController.php (1)

500-503: ⚠️ Potential issue | 🟠 Major

Persisting PAT in plaintext remains a security gap.

This endpoint still stores the raw token through Updater::saveGitHubToken(...); DB dumps/backups can expose credentials. Please encrypt-at-rest before persisting and decrypt only when constructing outbound GitHub headers.

#!/bin/bash
# Verify how GitHub token is persisted in Updater implementation.
rg -n -C4 "function saveGitHubToken|github_token|setting_value|INSERT INTO system_settings|UPDATE system_settings" app/Support/Updater.php
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/UpdateController.php` around lines 500 - 503, The code stores
the GitHub PAT in plaintext via Updater::saveGitHubToken, so change persistence
to encrypt-at-rest: update Updater::saveGitHubToken (or its caller) to encrypt
the token using the app's centralized encryption service/key before writing to
the DB and mark the setting as encrypted; then only decrypt inside the code path
that constructs outbound GitHub headers (e.g., Updater::getGitHubHeaders /
Updater::buildAuthHeaders) so Authorization headers are built from a decrypted
value in memory, not persisted in plaintext. Ensure you use the existing
application encryption API/config (app key or crypto service) and preserve
backwards compatibility by detecting/handling previously stored plaintext values
where necessary.
🧹 Nitpick comments (1)
app/Views/admin/updates.php (1)

553-563: Add a Content-Type pre-check before response.json() — same pattern used in startUpdate().

startUpdate() (line 758) already guards against non-JSON responses before calling .json(). Both new token functions lack this, so a CSRF failure or server error returning an HTML error page will surface a raw SyntaxError: Unexpected token '<' instead of a meaningful message.

♻️ Proposed fix (identical pattern for both functions)
         const response = await fetch(window.BASE_PATH + '/admin/updates/token', {
             method: 'POST',
             headers: {
                 'Content-Type': 'application/x-www-form-urlencoded',
                 'Accept': 'application/json',
                 'X-Requested-With': 'XMLHttpRequest'
             },
             body: `csrf_token=${encodeURIComponent(csrfToken)}&github_token=${encodeURIComponent(token)}`
         });
+
+        const contentType = response.headers.get('content-type') || '';
+        if (!contentType.includes('application/json')) {
+            const text = await response.text();
+            console.error('Server returned non-JSON response:', text.substring(0, 500));
+            throw new Error(<?= json_encode(__("Il server ha restituito una risposta non valida. Controlla i log per dettagli."), JSON_HEX_TAG) ?>);
+        }
 
         const data = await response.json();

Also applies to: 595-605

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/admin/updates.php` around lines 553 - 563, The token POST handlers
that fetch '/admin/updates/token' (the fetch block constructing body with
csrf_token and github_token) and the similar fetch at lines 595-605 must mirror
startUpdate(): before calling response.json() check
response.headers.get('content-type') includes 'application/json'; if not, await
response.text() and throw or surface a clear error containing that text (or
response.status) instead of calling .json(), so CSRF/server HTML errors don't
trigger a SyntaxError. Update both token-fetching functions to perform this
content-type guard and handle non-JSON responses as startUpdate() does.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 228: Replace inconsistent language name "Deutsch" with the English term
"German" throughout the README to match the document's English style; update the
occurrences in the phrase "Select language (Italian, English, or Deutsch)" and
the other instance noted near line 353 so they read "Select language (Italian,
English, or German)" (and any other occurrences of "Deutsch") to keep
terminology consistent with the "German" entry used earlier.

In `@scripts/manual-upgrade.php`:
- Around line 350-352: The current check in scripts/manual-upgrade.php only logs
a warning when $targetVersion is not newer than $currentVersion; change this to
abort the process: replace the warning-only branch in the version_compare(...)
conditional to log an error (using $log or error_log) and then immediately stop
execution (e.g., throw an exception or call exit with non-zero status) so the
upgrade script cannot continue when $targetVersion <= $currentVersion.
- Around line 447-451: In the foreach over $statements as $stmtSql, don’t skip
the whole chunk just because it begins with a comment line; instead strip
leading SQL comment lines (lines beginning with "--") from $stmtSql, then trim
and run the emptiness check on the stripped version so any executable SQL after
initial comments is still executed. Update the logic around $stmtSql (the
trim/str_starts_with check) to first remove leading comment lines (but preserve
non-comment lines and inline comments) and only continue when the remaining
trimmed string is empty.
- Around line 275-291: The code currently injects the DB password into the
mysqldump argument list via '--password=' in the $args array (around usage of
$mysqldumpBin and $env), which can expose credentials; change this to create a
secure temporary credentials file (e.g., write a file containing [client] and
password=... from $env['DB_PASS']), set strict permissions (600), add the file
path to $args as '--defaults-extra-file=' . $tmpFile instead of
'--password=...', run the command (using escapeshellarg for $tmpFile in the
constructed $safeCmd), and ensure the temp file is securely deleted after
execution and on error before returning; keep the other args ($args, --host,
--user, --port, --socket, --single-transaction, --routines, --triggers,
$env['DB_NAME']) unchanged.

---

Duplicate comments:
In `@app/Controllers/UpdateController.php`:
- Around line 500-503: The code stores the GitHub PAT in plaintext via
Updater::saveGitHubToken, so change persistence to encrypt-at-rest: update
Updater::saveGitHubToken (or its caller) to encrypt the token using the app's
centralized encryption service/key before writing to the DB and mark the setting
as encrypted; then only decrypt inside the code path that constructs outbound
GitHub headers (e.g., Updater::getGitHubHeaders / Updater::buildAuthHeaders) so
Authorization headers are built from a decrypted value in memory, not persisted
in plaintext. Ensure you use the existing application encryption API/config (app
key or crypto service) and preserve backwards compatibility by
detecting/handling previously stored plaintext values where necessary.

In `@app/Support/Updater.php`:
- Around line 205-207: The GitHub token is being stored in plaintext via
assignments to $this->githubToken; change the write path (e.g., setGithubToken
or where $this->githubToken is assigned at the shown snippet) to encrypt the
token before persisting (use your app's encryption API, e.g., Laravel's
Crypt::encryptString) and change the read path (where the token is read back,
e.g., getGithubToken or the code at lines ~231-252) to decrypt it
(Crypt::decryptString) before use; ensure the stored property/DB column holds
the encrypted string, handle empty strings/null the same as today, and add
try/catch around decrypt to surface errors rather than returning raw ciphertext.

---

Nitpick comments:
In `@app/Views/admin/updates.php`:
- Around line 553-563: The token POST handlers that fetch '/admin/updates/token'
(the fetch block constructing body with csrf_token and github_token) and the
similar fetch at lines 595-605 must mirror startUpdate(): before calling
response.json() check response.headers.get('content-type') includes
'application/json'; if not, await response.text() and throw or surface a clear
error containing that text (or response.status) instead of calling .json(), so
CSRF/server HTML errors don't trigger a SyntaxError. Update both token-fetching
functions to perform this content-type guard and handle non-JSON responses as
startUpdate() does.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and a2a0569.

📒 Files selected for processing (13)
  • README.md
  • app/Controllers/UpdateController.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • scripts/manual-upgrade.php

…y, version guard

- Updater: encrypt GitHub PAT at rest (AES-256-GCM, ENC: prefix, backward-compatible with plaintext)
- manual-upgrade: use --defaults-extra-file for mysqldump instead of --password in process args
- manual-upgrade: abort upgrade when target version <= current (was warn-only)
- manual-upgrade: strip SQL comment lines instead of skipping whole statements
- updates.php: add Content-Type pre-check before response.json() on token save/remove
- README: consistent "German" instead of "Deutsch"
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
vendor/composer/installed.php (1)

1-290: Consider excluding vendor/ from version control.

This is a Composer auto-generated file that gets regenerated on every composer install. Tracking the vendor/ directory in Git is generally discouraged because:

  • It bloats repository size
  • Dependencies can be reliably reproduced from composer.lock
  • It creates noise in diffs when unrelated changes bump internal references (like this commit hash update)

If deployment environments lack Composer access, consider using CI/CD to bundle dependencies in release artifacts instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/composer/installed.php` around lines 1 - 290, The repo is tracking the
vendor/ directory (e.g., vendor/composer/installed.php); stop committing
generated Composer dependencies by adding vendor/ to .gitignore, removing the
tracked files with git rm -r --cached vendor (or equivalent), and committing
that removal; ensure composer.json and composer.lock remain committed so
dependencies can be reproduced and CI/CD or release artifacts include the vendor
tree for deployments that need it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Support/Updater.php`:
- Around line 221-238: The current token encryption block (in Updater.php)
silently returns the plaintext on failures (the early return when !$rawKey ||
$plain === '' and the returns inside the openssl failure branch and catch
block), which allows plaintext to be persisted; change this to fail closed: do
not return raw $plain on error—either throw a specific EncryptionException or
return a distinct failure value (e.g., null/false) so callers (e.g., the code
paths that persist tokens) will not store plaintext; update the three locations
that currently "return $plain" to instead propagate an error (throw new
\RuntimeException or return null) and ensure the successful path still returns
'ENC:' . base64_encode($iv . $tag . $ciphertext) only on successful encryption
(keep key derivation via hash(...) and iv/tag handling unchanged).

In `@scripts/manual-upgrade.php`:
- Line 539: The current code appends internal filesystem paths using
$e->getFile() and $e->getLine() to the user-visible $log, which leaks server
internals; update the handling where $log is populated (the exception catch that
uses $log[] and the exception variable $e) to remove file/line details from
output and instead append a generic, non-sensitive message (e.g., an anonymized
trace or "internal error during upgrade") to $log for the browser, and send the
full exception details to server-side logging via error_log or your existing
logger (call error_log with $e->__toString() or $e->getMessage() plus context)
so diagnostics remain available without exposing filesystem paths.
- Around line 212-214: After successful password authentication (the branch
where $_POST['password'] === UPGRADE_PASSWORD), rotate the session ID before
setting authenticated state to prevent session fixation: call
session_regenerate_id(true) and then set $_SESSION['upgrade_auth'] = true (or
perform the regenerate immediately after setting the flag if you prefer),
ensuring you reference the existing authentication check and session variable
names ($_POST['password'], UPGRADE_PASSWORD, $_SESSION['upgrade_auth']) so the
change is applied in the same conditional block.
- Around line 338-339: Check the boolean return of ZipArchive::extractTo when
extracting into $tempDir and abort on failure: after calling
$zip->extractTo($tempDir) (the $zip object in scripts/manual-upgrade.php) verify
the result is true, call $zip->close() and then log/throw an error and stop
further processing if it returned false so partial or failed extractions don't
continue; ensure any cleanup (removing $tempDir if created) runs on failure.

---

Nitpick comments:
In `@vendor/composer/installed.php`:
- Around line 1-290: The repo is tracking the vendor/ directory (e.g.,
vendor/composer/installed.php); stop committing generated Composer dependencies
by adding vendor/ to .gitignore, removing the tracked files with git rm -r
--cached vendor (or equivalent), and committing that removal; ensure
composer.json and composer.lock remain committed so dependencies can be
reproduced and CI/CD or release artifacts include the vendor tree for
deployments that need it.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2a0569 and 73d6731.

📒 Files selected for processing (5)
  • README.md
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • scripts/manual-upgrade.php
  • vendor/composer/installed.php

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
scripts/manual-upgrade.php (3)

338-339: ⚠️ Potential issue | 🟠 Major

Abort when ZIP extraction fails.

Line 338 ignores ZipArchive::extractTo()’s boolean return, so the script can proceed after a failed/partial extraction.

💡 Suggested fix
-        $zip->extractTo($tempDir);
-        $zip->close();
+        if (!$zip->extractTo($tempDir)) {
+            $zip->close();
+            throw new RuntimeException('Estrazione ZIP fallita');
+        }
+        $zip->close();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 338 - 339, The code calls
ZipArchive::extractTo via $zip->extractTo($tempDir) but ignores its boolean
return value, so update the block around $zip->extractTo and $zip->close() to
check the return; if extractTo returns false, close the archive (via
$zip->close()), log or echo a clear error mentioning $tempDir and abort the
script (exit with non-zero status or throw an exception) and perform any
necessary cleanup of partial files; only proceed when extractTo returns true.

212-214: ⚠️ Potential issue | 🟠 Major

Regenerate session ID on successful authentication.

Line 213 sets authenticated session state without rotating the session ID, leaving a session fixation window.

💡 Suggested fix
-    } elseif ($_POST['password'] === UPGRADE_PASSWORD) {
-        $_SESSION['upgrade_auth'] = true;
+    } elseif ($_POST['password'] === UPGRADE_PASSWORD) {
+        session_regenerate_id(true);
+        $_SESSION['upgrade_auth'] = true;
     } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 212 - 214, Regenerate the session ID
when authentication succeeds to prevent session fixation: after verifying the
password (the branch where $_POST['password'] === UPGRADE_PASSWORD) call
session_regenerate_id(true) and then set $_SESSION['upgrade_auth'] = true
(ensure a session is started via session_start() earlier in the script). This
ensures the authenticated state set by $_SESSION['upgrade_auth'] is tied to a
fresh session ID.

539-539: ⚠️ Potential issue | 🟠 Major

Do not expose filesystem internals in user-visible logs.

Line 539 leaks absolute file paths and source lines to the browser output. Keep detailed traces server-side only.

💡 Suggested fix
-        $log[] = '[TRACE] ' . $e->getFile() . ':' . $e->getLine();
+        $log[] = '[TRACE] Errore interno durante l\'upgrade.';
+        error_log('[manual-upgrade] ' . $e->__toString());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` at line 539, Remove exposure of filesystem paths
and line numbers from the user-visible $log array: replace the line that appends
"[TRACE] " . $e->getFile() . ':' . $e->getLine() to $log with a generic trace
message (e.g., "[TRACE] exception in upgrade process") and send the detailed
trace (file, line, stack) to a server-side log using error_log or the existing
logger; locate the code around the $log array and exception variable $e in
manual-upgrade.php and ensure only sanitized messages are added to $log while
full exception details are recorded server-side.
app/Support/Updater.php (1)

300-322: ⚠️ Potential issue | 🟠 Major

Missing verification that encryption succeeded before persisting.

Lines 312-313 call encryptValue() but don't verify the result is actually encrypted. If encryption fails silently (due to missing key or errors), the plaintext token will be persisted to the database.

Add a guard to ensure only encrypted tokens are stored:

🔐 Proposed fix
         $cat = 'updater';
         $key = 'github_token';
         $encrypted = $token !== '' ? $this->encryptValue($token) : '';
+        if ($token !== '' && !str_starts_with($encrypted, 'ENC:')) {
+            throw new Exception(__('Impossibile cifrare il token GitHub: salvataggio annullato'));
+        }
         $stmt->bind_param('sss', $cat, $key, $encrypted);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 300 - 322, In saveGitHubToken, ensure
encryptValue actually succeeded before writing to the DB: call
$this->encryptValue($token), verify the returned $encrypted is non-empty and not
equal to the raw $token (or otherwise indicates success), and if encryption
failed throw an Exception (do not proceed to bind/execute the INSERT); only
bind/execute and set $this->githubToken when encryption succeeded. Refer to
saveGitHubToken and encryptValue for where to add the guard and the exception
handling.
🧹 Nitpick comments (4)
vendor/composer/installed.php (1)

6-6: Remove vendor/ from version control; only composer.lock is needed for reproducible builds.

The vendor/ directory is not properly excluded from .gitignore (only vendor/**/.github/ is excluded), and 2,402 vendor files are currently tracked by git. This bloats the repository and introduces unnecessary merge conflicts.

Standard practice is to commit only composer.lock and ignore vendor/. Run composer install to restore dependencies. If vendor files must be committed for deployment reasons, this is acceptable—but document the rationale.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/composer/installed.php` at line 6, The repo currently tracks vendor/
(e.g., files like vendor/composer/installed.php with 'reference' entries), which
should be removed from version control; update .gitignore to include vendor/
(rather than only vendor/**/.github/), stop tracking existing vendor files (git
rm -r --cached vendor) and commit that removal, ensure composer.lock is
committed so dependencies are reproducible, and document the rationale only if
you intentionally keep vendor/ committed for deployment.
app/Controllers/UpdateController.php (1)

39-40: Remove unused $data assignment in index().

$data is not read before rendering and adds static-analysis noise.

🧹 Proposed cleanup
         ob_start();
-        $data = compact('updateInfo', 'requirements', 'history', 'changelog', 'githubTokenMasked', 'hasGithubToken');
         require __DIR__ . '/../Views/admin/updates.php';
         $content = ob_get_clean();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/UpdateController.php` around lines 39 - 40, In the index()
method remove the unused assignment to $data (the line using
compact('updateInfo', 'requirements', 'history', 'changelog',
'githubTokenMasked', 'hasGithubToken')) since those variables are already
available to the required view and $data is never used; simply delete that
assignment to eliminate static-analysis noise while keeping the subsequent
require __DIR__ . '/../Views/admin/updates.php' call intact.
installer/classes/Installer.php (1)

258-270: Remove unreachable locale-map entries after normalization.

$normalizedLocale is always lowercase with underscores, so canonical keys like it_IT, en_US, and de_DE are never matched. Keeping only normalized keys reduces maintenance drift.

♻️ Proposed cleanup
-        $localeMap = [
-            'it' => 'it_IT',
-            'it_it' => 'it_IT',
-            'it_IT' => 'it_IT',
-            'en' => 'en_US',
-            'en_us' => 'en_US',
-            'en_US' => 'en_US',
-            'de' => 'de_DE',
-            'de_de' => 'de_DE',
-            'de_DE' => 'de_DE',
-        ];
+        $localeMap = [
+            'it' => 'it_IT',
+            'it_it' => 'it_IT',
+            'en' => 'en_US',
+            'en_us' => 'en_US',
+            'de' => 'de_DE',
+            'de_de' => 'de_DE',
+        ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/classes/Installer.php` around lines 258 - 270, The locale map
contains unreachable mixed-case keys because $normalizedLocale is lowercase with
underscores; update the $localeMap used by Installer:: where $normalizedLocale
is computed so it only contains normalized keys (e.g. 'it', 'it_it', 'en',
'en_us', 'de', 'de_de') and remove entries like 'it_IT', 'en_US', 'de_DE'; keep
the fallback assignment to $fullLocale = $localeMap[$normalizedLocale] ??
'it_IT' unchanged so the default remains 'it_IT'.
app/Views/admin/updates.php (1)

552-635: Deduplicate token API request handling.

saveGitHubToken() and removeGitHubToken() repeat the same fetch headers, content-type check, and JSON parsing. A shared helper would reduce drift risk.

♻️ Proposed refactor
+async function postGitHubToken(tokenValue) {
+    const response = await fetch(window.BASE_PATH + '/admin/updates/token', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/x-www-form-urlencoded',
+            'Accept': 'application/json',
+            'X-Requested-With': 'XMLHttpRequest'
+        },
+        body: `csrf_token=${encodeURIComponent(csrfToken)}&github_token=${encodeURIComponent(tokenValue)}`
+    });
+
+    const ct = response.headers.get('content-type') || '';
+    if (!ct.includes('application/json')) {
+        const text = await response.text();
+        console.error('Server returned non-JSON response:', text.substring(0, 500));
+        throw new Error(<?= json_encode(__("Il server ha restituito una risposta non valida. Controlla i log per dettagli."), JSON_HEX_TAG) ?>);
+    }
+
+    return response.json();
+}
@@
-        const response = await fetch(window.BASE_PATH + '/admin/updates/token', {
-            method: 'POST',
-            headers: {
-                'Content-Type': 'application/x-www-form-urlencoded',
-                'Accept': 'application/json',
-                'X-Requested-With': 'XMLHttpRequest'
-            },
-            body: `csrf_token=${encodeURIComponent(csrfToken)}&github_token=${encodeURIComponent(token)}`
-        });
-
-        const ct = response.headers.get('content-type') || '';
-        if (!ct.includes('application/json')) {
-            const text = await response.text();
-            console.error('Server returned non-JSON response:', text.substring(0, 500));
-            throw new Error(<?= json_encode(__("Il server ha restituito una risposta non valida. Controlla i log per dettagli."), JSON_HEX_TAG) ?>);
-        }
-
-        const data = await response.json();
+        const data = await postGitHubToken(token);
@@
-        const response = await fetch(window.BASE_PATH + '/admin/updates/token', {
-            method: 'POST',
-            headers: {
-                'Content-Type': 'application/x-www-form-urlencoded',
-                'Accept': 'application/json',
-                'X-Requested-With': 'XMLHttpRequest'
-            },
-            body: `csrf_token=${encodeURIComponent(csrfToken)}&github_token=`
-        });
-
-        const ct = response.headers.get('content-type') || '';
-        if (!ct.includes('application/json')) {
-            const text = await response.text();
-            console.error('Server returned non-JSON response:', text.substring(0, 500));
-            throw new Error(<?= json_encode(__("Il server ha restituito una risposta non valida. Controlla i log per dettagli."), JSON_HEX_TAG) ?>);
-        }
-
-        const data = await response.json();
+        const data = await postGitHubToken('');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/admin/updates.php` around lines 552 - 635, Both saveGitHubToken()
and removeGitHubToken() duplicate the same fetch setup, content-type checking
and JSON parsing; extract that common behavior into a helper (e.g.,
postTokenRequest or postJsonToTokenEndpoint) that accepts the github_token value
(and csrfToken) and returns the parsed JSON or throws on non-JSON response;
update saveGitHubToken() and removeGitHubToken() to call this helper and keep
only the UI/Swal handling; ensure the helper replicates the existing headers
('Content-Type','Accept','X-Requested-With'), content-type validation using
response.headers.get('content-type'), console.error of non-JSON body, and throws
the same localized Error message so behavior remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/Support/Updater.php`:
- Around line 300-322: In saveGitHubToken, ensure encryptValue actually
succeeded before writing to the DB: call $this->encryptValue($token), verify the
returned $encrypted is non-empty and not equal to the raw $token (or otherwise
indicates success), and if encryption failed throw an Exception (do not proceed
to bind/execute the INSERT); only bind/execute and set $this->githubToken when
encryption succeeded. Refer to saveGitHubToken and encryptValue for where to add
the guard and the exception handling.

In `@scripts/manual-upgrade.php`:
- Around line 338-339: The code calls ZipArchive::extractTo via
$zip->extractTo($tempDir) but ignores its boolean return value, so update the
block around $zip->extractTo and $zip->close() to check the return; if extractTo
returns false, close the archive (via $zip->close()), log or echo a clear error
mentioning $tempDir and abort the script (exit with non-zero status or throw an
exception) and perform any necessary cleanup of partial files; only proceed when
extractTo returns true.
- Around line 212-214: Regenerate the session ID when authentication succeeds to
prevent session fixation: after verifying the password (the branch where
$_POST['password'] === UPGRADE_PASSWORD) call session_regenerate_id(true) and
then set $_SESSION['upgrade_auth'] = true (ensure a session is started via
session_start() earlier in the script). This ensures the authenticated state set
by $_SESSION['upgrade_auth'] is tied to a fresh session ID.
- Line 539: Remove exposure of filesystem paths and line numbers from the
user-visible $log array: replace the line that appends "[TRACE] " .
$e->getFile() . ':' . $e->getLine() to $log with a generic trace message (e.g.,
"[TRACE] exception in upgrade process") and send the detailed trace (file, line,
stack) to a server-side log using error_log or the existing logger; locate the
code around the $log array and exception variable $e in manual-upgrade.php and
ensure only sanitized messages are added to $log while full exception details
are recorded server-side.

---

Nitpick comments:
In `@app/Controllers/UpdateController.php`:
- Around line 39-40: In the index() method remove the unused assignment to $data
(the line using compact('updateInfo', 'requirements', 'history', 'changelog',
'githubTokenMasked', 'hasGithubToken')) since those variables are already
available to the required view and $data is never used; simply delete that
assignment to eliminate static-analysis noise while keeping the subsequent
require __DIR__ . '/../Views/admin/updates.php' call intact.

In `@app/Views/admin/updates.php`:
- Around line 552-635: Both saveGitHubToken() and removeGitHubToken() duplicate
the same fetch setup, content-type checking and JSON parsing; extract that
common behavior into a helper (e.g., postTokenRequest or
postJsonToTokenEndpoint) that accepts the github_token value (and csrfToken) and
returns the parsed JSON or throws on non-JSON response; update saveGitHubToken()
and removeGitHubToken() to call this helper and keep only the UI/Swal handling;
ensure the helper replicates the existing headers
('Content-Type','Accept','X-Requested-With'), content-type validation using
response.headers.get('content-type'), console.error of non-JSON body, and throws
the same localized Error message so behavior remains identical.

In `@installer/classes/Installer.php`:
- Around line 258-270: The locale map contains unreachable mixed-case keys
because $normalizedLocale is lowercase with underscores; update the $localeMap
used by Installer:: where $normalizedLocale is computed so it only contains
normalized keys (e.g. 'it', 'it_it', 'en', 'en_us', 'de', 'de_de') and remove
entries like 'it_IT', 'en_US', 'de_DE'; keep the fallback assignment to
$fullLocale = $localeMap[$normalizedLocale] ?? 'it_IT' unchanged so the default
remains 'it_IT'.

In `@vendor/composer/installed.php`:
- Line 6: The repo currently tracks vendor/ (e.g., files like
vendor/composer/installed.php with 'reference' entries), which should be removed
from version control; update .gitignore to include vendor/ (rather than only
vendor/**/.github/), stop tracking existing vendor files (git rm -r --cached
vendor) and commit that removal, ensure composer.lock is committed so
dependencies are reproducible, and document the rationale only if you
intentionally keep vendor/ committed for deployment.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and 73d6731.

📒 Files selected for processing (14)
  • README.md
  • app/Controllers/UpdateController.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • scripts/manual-upgrade.php
  • vendor/composer/installed.php

…ation, cleanup

- Updater: encryptValue throws on failure instead of returning plaintext (fail-closed)
- Updater: saveGitHubToken guards that ENC: prefix is present before persisting
- manual-upgrade: session_regenerate_id(true) after successful authentication
- manual-upgrade: check ZipArchive::extractTo() return value, abort on failure
- manual-upgrade: move filesystem trace to error_log, keep user log generic
- updates.php: extract postTokenRequest() helper to deduplicate save/remove fetch logic
- UpdateController: remove unused $data compact() assignment
- Installer: remove unreachable mixed-case locale map entries
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a 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

♻️ Duplicate comments (3)
installer/database/data_de_DE.sql (1)

4-4: ⚠️ Potential issue | 🟠 Major

FOREIGN_KEY_CHECKS can remain disabled if execution aborts early.

Line 4 disables FK checks and they are only restored at Line 347. Any SQL error before the end can leave the installer DB session in an unsafe state for subsequent operations.

🛠️ Safer baseline restore in-script
-SET FOREIGN_KEY_CHECKS=0;
+SET `@OLD_FOREIGN_KEY_CHECKS` := @@FOREIGN_KEY_CHECKS;
+SET FOREIGN_KEY_CHECKS=0;
...
-SET FOREIGN_KEY_CHECKS=1;
+SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;

Please also ensure the installer SQL executor restores FK checks in a finally-style path on exceptions/interrupted execution.

Also applies to: 347-347

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/database/data_de_DE.sql` at line 4, The script currently disables
FOREIGN_KEY_CHECKS and only restores it much later; update the SQL and the
installer SQL executor so FOREIGN_KEY_CHECKS is always re-enabled on
error/interrupt: in the SQL bundle ensure you include a matching SET
FOREIGN_KEY_CHECKS=1 close to any transactional/critical section boundaries and
in the installer SQL execution code wrap execution in a try/finally (or
equivalent) that always runs SET FOREIGN_KEY_CHECKS=1 (or issues the server
command to restore FK checks) in the finally block; reference the
FOREIGN_KEY_CHECKS toggle and the installer SQL executor function to implement
the unconditional restore path.
installer/index.php (1)

36-40: ⚠️ Potential issue | 🟡 Minor

Normalize locale before both translation-file and <html lang> mapping.

Line [36] and Line [347] still match raw locale values; variants like de-DE / en-us can miss expected mappings and fall back incorrectly. Use a shared normalizer before both decisions.

♻️ Proposed refactor
+function normalizeInstallerLocale(string $locale): string {
+    $locale = str_replace('-', '_', strtolower($locale));
+    return match($locale) {
+        'en', 'en_us' => 'en_US',
+        'de', 'de_de' => 'de_DE',
+        default => 'it_IT',
+    };
+}
+
 // Simple translation function for installer
 function __(string $key, mixed ...$args): string {
@@
-            $localeCode = match($locale) {
-                'en', 'en_US' => 'en_US',
-                'de', 'de_DE' => 'de_DE',
-                default => $locale,
-            };
+            $localeCode = normalizeInstallerLocale((string)$locale);
             $translationFile = dirname(__DIR__) . '/locale/' . $localeCode . '.json';
@@
-    $lang = $_SESSION['app_locale'] ?? 'it';
-    $htmlLang = match($lang) {
-        'en', 'en_US' => 'en',
-        'de', 'de_DE' => 'de',
-        default => 'it',
-    };
+    $localeCode = normalizeInstallerLocale((string)($_SESSION['app_locale'] ?? 'it'));
+    $htmlLang = match($localeCode) {
+        'en_US' => 'en',
+        'de_DE' => 'de',
+        default => 'it',
+    };

Also applies to: 347-351

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/index.php` around lines 36 - 40, Create and call a single locale
normalizer (e.g., normalizeLocale) that canonicalizes input $locale to a
consistent format (lowercase, convert '-' to '_', map common variants like
'en_us'/'en-us' to 'en_US' and 'de_de'/'de-de' to 'de_DE') and use its output
everywhere you currently use $locale or $localeCode; replace the inline match on
$locale in the $localeCode assignment and the separate mapping used for the HTML
lang (the second match block around lines 347-351) to operate on
normalizeLocale($locale) so both translation-file selection and the <html lang>
mapping use the same normalized value.
app/Support/Updater.php (1)

306-323: ⚠️ Potential issue | 🟠 Major

Reject control characters in token input before persistence.

Line [308] only trims boundaries. A token containing control characters (especially CR/LF) can corrupt the Authorization header assembled at Line [297]. Validate and reject control chars before encryption/save.

🔧 Proposed fix
 public function saveGitHubToken(string $token): void
 {
     $token = trim($token);
+    if ($token !== '' && preg_match('/[[:cntrl:]]/u', $token)) {
+        throw new Exception(__('Token GitHub non valido'));
+    }

     $stmt = $this->db->prepare(
         'INSERT INTO system_settings (category, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)'
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 306 - 323, In saveGitHubToken, reject
any token containing control characters (CR, LF, or other ASCII control bytes)
before calling encryptValue and before binding/persisting; after trim(),
validate the token (e.g., detect \r or \n or general control chars) and throw an
Exception with a clear message if found, so the Authorization header assembler
that consumes this token never receives embedded control characters; perform
this check on the trimmed value and only proceed to encryptValue,
prepare/bind_param and execute when the token passes validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@locale/routes_de_DE.json`:
- Line 15: The "events" key in locale/routes_de_DE.json currently maps to
"/veranstaltungen", which doesn't match the registered endpoints /events and
/events/{slug} and will 404; fix by either updating the "events" translation
value to "/events" (so the "events" key resolves to existing routes) or
alternatively add localized routes for "/veranstaltungen" and
"/veranstaltungen/{slug}" in your routing (app/Routes/web.php) so the "events"
key can remain "/veranstaltungen".

---

Duplicate comments:
In `@app/Support/Updater.php`:
- Around line 306-323: In saveGitHubToken, reject any token containing control
characters (CR, LF, or other ASCII control bytes) before calling encryptValue
and before binding/persisting; after trim(), validate the token (e.g., detect \r
or \n or general control chars) and throw an Exception with a clear message if
found, so the Authorization header assembler that consumes this token never
receives embedded control characters; perform this check on the trimmed value
and only proceed to encryptValue, prepare/bind_param and execute when the token
passes validation.

In `@installer/database/data_de_DE.sql`:
- Line 4: The script currently disables FOREIGN_KEY_CHECKS and only restores it
much later; update the SQL and the installer SQL executor so FOREIGN_KEY_CHECKS
is always re-enabled on error/interrupt: in the SQL bundle ensure you include a
matching SET FOREIGN_KEY_CHECKS=1 close to any transactional/critical section
boundaries and in the installer SQL execution code wrap execution in a
try/finally (or equivalent) that always runs SET FOREIGN_KEY_CHECKS=1 (or issues
the server command to restore FK checks) in the finally block; reference the
FOREIGN_KEY_CHECKS toggle and the installer SQL executor function to implement
the unconditional restore path.

In `@installer/index.php`:
- Around line 36-40: Create and call a single locale normalizer (e.g.,
normalizeLocale) that canonicalizes input $locale to a consistent format
(lowercase, convert '-' to '_', map common variants like 'en_us'/'en-us' to
'en_US' and 'de_de'/'de-de' to 'de_DE') and use its output everywhere you
currently use $locale or $localeCode; replace the inline match on $locale in the
$localeCode assignment and the separate mapping used for the HTML lang (the
second match block around lines 347-351) to operate on normalizeLocale($locale)
so both translation-file selection and the <html lang> mapping use the same
normalized value.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and cf6e76d.

📒 Files selected for processing (14)
  • README.md
  • app/Controllers/UpdateController.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • scripts/manual-upgrade.php
  • vendor/composer/installed.php

…rmalizer

- web.php: register events routes via RouteTranslator loop (fixes /veranstaltungen 404)
- Updater: reject control characters in GitHub token before persist
- Installer: try/finally to always restore FOREIGN_KEY_CHECKS on data import
- installer/index.php: shared normalizeInstallerLocale() for translation + HTML lang
Copy link

@coderabbitai coderabbitai bot left a 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

♻️ Duplicate comments (5)
scripts/manual-upgrade.php (1)

351-358: ⚠️ Potential issue | 🟠 Major

Fail fast if mysqldump credentials file cannot be written.

Line 351 currently ignores file_put_contents() failure; backup can proceed with a bad/empty defaults file and weaken recovery guarantees.

🧯 Suggested patch
-            file_put_contents($defaultsFile,
+            $written = file_put_contents($defaultsFile,
                 "[client]\n"
                 . "host=" . ($env['DB_HOST'] ?? 'localhost') . "\n"
                 . "user=" . ($env['DB_USER'] ?? '') . "\n"
                 . "password=" . ($env['DB_PASS'] ?? '') . "\n"
                 . "port=" . (int) ($env['DB_PORT'] ?? 3306) . "\n"
             );
+            if ($written === false || $written === 0) {
+                `@unlink`($defaultsFile);
+                throw new RuntimeException('Impossibile scrivere il file temporaneo credenziali mysqldump');
+            }
             `@chmod`($defaultsFile, 0600);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 351 - 358, The code currently calls
file_put_contents($defaultsFile, ...) and then `@chmod`($defaultsFile, 0600)
without checking for failure; update the logic in the manual-upgrade script to
check the return value of file_put_contents for false and handle failure by
logging an error (including $defaultsFile and the PHP error message), preventing
further backup/upgrade steps (exit or throw), and only call chmod when
file_put_contents succeeded; reference the $defaultsFile variable, the
file_put_contents() call and the `@chmod`() call when making this change.
installer/database/data_de_DE.sql (1)

290-291: ⚠️ Potential issue | 🟡 Minor

Avoid root-absolute links in seeded content.

Line 290, Line 291, Line 322, and Line 332 use /... paths. In subfolder installs these can bypass the app base path and break navigation.

🔧 Suggested patch
-... href=\"/admin/cms/about-us\" ...
+... href=\"admin/cms/about-us\" ...

-... href=\"/admin/cms/privacy-policy\" ...
+... href=\"admin/cms/privacy-policy\" ...

-('hero', ..., 'Katalog entdecken', '/katalog', ...)
+('hero', ..., 'Katalog entdecken', 'katalog', ...)

-('cta', ..., 'Jetzt registrieren', '/registrieren', ...)
+('cta', ..., 'Jetzt registrieren', 'registrieren', ...)

Also applies to: 322-323, 332-332

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/database/data_de_DE.sql` around lines 290 - 291, The seeded CMS
page records for slugs 'about-us' and 'privacy-policy' include root-absolute
links (e.g. '/admin/cms/about-us' and '/admin/cms/privacy-policy') which can
break installs in a subfolder; update those link strings in the INSERTs to use
either a relative path ('admin/cms/about-us', etc.) or a configurable base-path
placeholder (e.g. '{{APP_BASE_PATH}}/admin/cms/about-us') and apply the same
change to every occurrence in the 'about-us' and 'privacy-policy' content
entries (and the other duplicated occurrences called out) so seeded content
respects the app base path.
app/Support/Updater.php (2)

739-780: ⚠️ Potential issue | 🟠 Major

Handle auth failures in getAllReleases() before treating payload as release list.

With an invalid/revoked token at Line 739 or Line 757, GitHub can return an error JSON object instead of a release array. This method currently returns that object, and downstream getChangelog() expects list items with tag_name, which can break at runtime.

🔧 Proposed fix
-            if ($curlResult !== false && $httpCode >= 200 && $httpCode < 400) {
+            if ($curlResult !== false && $httpCode >= 200 && $httpCode < 400) {
                 $response = $curlResult;
+            } elseif ($httpCode === 401 || $httpCode === 403) {
+                // retry once without token
+                $headers = [
+                    'User-Agent: Pinakes-Updater/1.0',
+                    'Accept: application/vnd.github.v3+json',
+                ];
+                curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+                $retryResult = curl_exec($ch);
+                $retryCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+                if ($retryResult !== false && $retryCode >= 200 && $retryCode < 400) {
+                    $response = $retryResult;
+                }
             }
@@
-        if (!is_array($releases)) {
+        if (!is_array($releases) || !array_is_list($releases)) {
             $this->debugLog('ERROR', 'Risposta releases non valida', [
                 'json_error' => json_last_error_msg()
             ]);
             return [];
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 739 - 780, In getAllReleases(), don't
treat any JSON payload as a releases list—check for auth/errors before
returning: if an HTTP status (from $httpCode when curl used) is >=400
(especially 401/403) log and return []; after json_decode($response, true)
detect an error object (assoc array with keys like "message", "error" or
"documentation_url") or non-indexed array and in that case debugLog the GitHub
error details (include json_last_error_msg() and the decoded message) and return
[]; ensure callers like getChangelog() only receive a numeric-indexed array of
releases by validating $releases is an indexed array before using tag_name.

877-903: ⚠️ Potential issue | 🟠 Major

Avoid blocking update ZIP download when PAT is invalid.

Line 877 and Line 913 always attach Authorization. If the saved token is revoked/invalid, both download paths can fail with 401 and the update aborts, even when anonymous download would work.

🔧 Proposed fix
-                if ($curlErrno !== 0 || $httpCode >= 400) {
+                if ($curlErrno !== 0 || $httpCode >= 400) {
+                    if (in_array($httpCode, [401, 403], true) && $this->githubToken !== '') {
+                        $this->debugLog('WARNING', 'Retry download senza token', ['http_code' => $httpCode]);
+                        $headers = [
+                            'User-Agent: Pinakes-Updater/1.0',
+                            'Accept: application/octet-stream',
+                        ];
+                        $ch = curl_init($downloadUrl);
+                        curl_setopt_array($ch, [
+                            CURLOPT_RETURNTRANSFER => true,
+                            CURLOPT_FOLLOWLOCATION => true,
+                            CURLOPT_MAXREDIRS => 10,
+                            CURLOPT_TIMEOUT => 300,
+                            CURLOPT_CONNECTTIMEOUT => 30,
+                            CURLOPT_USERAGENT => 'Pinakes-Updater/1.0',
+                            CURLOPT_HTTPHEADER => $headers,
+                            CURLOPT_SSL_VERIFYPEER => true,
+                        ]);
+                        $retryContent = curl_exec($ch);
+                        $retryInfo = curl_getinfo($ch);
+                        curl_close($ch);
+                        if ($retryContent !== false && (($retryInfo['http_code'] ?? 0) < 400)) {
+                            $fileContent = $retryContent;
+                        }
+                    }
                     $this->debugLog('WARNING', 'cURL fallito, tentativo con file_get_contents', [
                         'error' => $curlError,
                         'http_code' => $httpCode
                     ]);
-                    $fileContent = false;
+                    if ($fileContent === false) {
+                        $fileContent = false;
+                    }
                 }

Also applies to: 913-942

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 877 - 903, The download always attaches
Authorization via getGitHubHeaders('application/octet-stream') which causes both
the cURL and file_get_contents paths to fail when the saved PAT is revoked;
change the download flow so that if the request returns HTTP 401/403 (or curl
errno indicating auth failure) you immediately retry the download without the
Authorization header (i.e. call a header helper that omits the token or pass a
flag to getGitHubHeaders to build anonymous headers) for both the cURL branch
around the curl_exec/curl_getinfo logic and the fallback file_get_contents path,
and only abort if the anonymous retry also fails.
public/assets/main.css (1)

4662-4665: ⚠️ Potential issue | 🟡 Minor

Fix unresolved Stylelint declaration spacing in generated utility blocks.

Line 4664 still violates declaration-empty-line-before, and the same pattern repeats across the newly added utility blocks (for example Line 4688, Line 4855, Line 5490, Line 7050). This remains CI-blocking unless regenerated/auto-fixed.

🧩 Example fix pattern
 .border-amber-300 {
   --tw-border-opacity: 1;
+
   border-color: rgb(252 211 77 / var(--tw-border-opacity, 1));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public/assets/main.css` around lines 4662 - 4665, The generated utility
blocks (e.g., the .border-amber-300 block) violate the Stylelint rule
declaration-empty-line-before because there is no empty line before the first
declaration; update each generated utility class (for example .border-amber-300
and the other similar blocks around the noted lines) to insert a single blank
line before the first declaration (so the line before "--tw-border-opacity: 1;"
is empty) and apply the same spacing fix across all newly added utility blocks
(or regenerate/auto-fix the CSS) so the declaration-empty-line-before rule is
satisfied consistently.
🧹 Nitpick comments (5)
app/Views/libri/partials/book_form.php (1)

2245-2248: Minor redundancy in sync event listeners (acceptable).

The syncHidden calls are already present within the change handlers above (lines 2194, 2202, 2229). These additional addEventListener calls provide a safety net for any future code paths that might modify select values without going through the main handlers. This defensive approach is fine, though it does mean syncHidden() may execute twice per change event.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/libri/partials/book_form.php` around lines 2245 - 2248, Duplicate
change listeners on genereSelect and sottogenereSelect cause syncHidden to run
twice; remove the redundant addEventListener calls (the lines attaching 'change'
to genereSelect and sottogenereSelect that call syncHidden) so only the existing
handlers (which already call syncHidden in their change callbacks) remain, or
alternatively consolidate by replacing the duplicates with a single shared
handler registration to avoid double execution; locate references to
genereSelect, sottogenereSelect, and syncHidden to make the change.
locale/routes_de_DE.json (1)

1-32: Add an automated route-key parity check across locale route JSON files.

This file looks good, but a CI check comparing key sets (e.g., against the base locale) would prevent future route-map drift and silent 404 regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@locale/routes_de_DE.json` around lines 1 - 32, Add a CI parity check that
ensures the route key set in locale/routes_de_DE.json (e.g., keys like "login",
"logout", "register", "verify_email", "api_catalog", "language_switch") exactly
matches the base locale route file; implement a small script (e.g.,
routes_parity_check) that loads each locale JSON, computes set differences
against the base locale (missing and extra keys), prints human-readable diffs
and exits non-zero on mismatch, and wire that script into the CI pipeline to run
on PRs so any future drift in route keys fails the build.
app/Views/frontend/events.php (1)

32-38: Extract shared localized date/time formatting into a helper.

This logic is now duplicated across multiple views (app/Views/frontend/event-detail.php, app/Views/events/index.php, and this file). Centralizing it will reduce drift and keep locale fallback behavior consistent.

Also applies to: 59-63, 65-78, 80-93

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/frontend/events.php` around lines 32 - 38, Extract the duplicated
IntlDateFormatter setup into a single helper function (e.g.,
getLocalizedFormatters($locale)) that returns an array or object with
dateFormatter and timeFormatter (falling back to null when
class_exists('IntlDateFormatter') is false); replace the repeated blocks in the
views (where $dateFormatter, $timeFormatter and $locale are used) with a call to
getLocalizedFormatters($locale) and use the returned dateFormatter/timeFormatter
values, ensuring the helper captures the LONG/SHORT and NONE constants exactly
as in the original code so behavior and locale fallback remain unchanged.
scripts/manual-upgrade.php (1)

260-269: Add basic authentication throttling to the upgrade login.

This endpoint guards a high-impact operation; add attempt limits/cooldown (session + IP based) to reduce brute-force exposure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 260 - 269, The POST password check
needs rate-limiting: implement per-session and per-IP attempt tracking and
cooldown around the existing check that uses UPGRADE_PASSWORD and sets
$_SESSION['upgrade_auth']; on each failed attempt increment counters (e.g.
$_SESSION['upgrade_attempts'] and $_SESSION['upgrade_last_attempt']) and persist
per-IP attempts (e.g. simple flatfile or APCu keyed by $_SERVER['REMOTE_ADDR'])
and if limits are exceeded return an $error with remaining cooldown; on
successful hash_equals reset the session counters and regenerate id as you
already do; ensure timestamps are stored to enforce a cooldown window and that
counters decay after the window to avoid permanent lockout.
app/Routes/web.php (1)

257-282: Consider centralizing hybrid profile-update aliases in a map.

The locale if/elseif branch for hybrid aliases will keep growing as more locales are added. A map + shared handler is easier to maintain and less error-prone.

♻️ Suggested refactor
-        $profileUpdateRoute = RouteTranslator::getRouteForLocale('profile_update', $locale);
-        $registerRouteIfUnique('POST', $profileUpdateRoute, function ($request, $response) use ($app) {
+        $profileUpdateRoute = RouteTranslator::getRouteForLocale('profile_update', $locale);
+        $profileUpdateHandler = function ($request, $response) use ($app) {
             $db = $app->getContainer()->get('db');
             $controller = new ProfileController();
             return $controller->update($request, $response, $db);
-        }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
+        };
+        $registerRouteIfUnique('POST', $profileUpdateRoute, $profileUpdateHandler, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
@@
-        if ($locale === 'it_IT') {
-            // Italian base + English action
-            $registerRouteIfUnique('POST', '/profilo/update', function ($request, $response) use ($app) {
-                $db = $app->getContainer()->get('db');
-                $controller = new ProfileController();
-                return $controller->update($request, $response, $db);
-            }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
-        } elseif ($locale === 'en_US') {
-            // English base + Italian action
-            $registerRouteIfUnique('POST', '/profile/aggiorna', function ($request, $response) use ($app) {
-                $db = $app->getContainer()->get('db');
-                $controller = new ProfileController();
-                return $controller->update($request, $response, $db);
-            }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
-        } elseif ($locale === 'de_DE') {
-            // German base + English/Italian action
-            $registerRouteIfUnique('POST', '/profil/update', function ($request, $response) use ($app) {
-                $db = $app->getContainer()->get('db');
-                $controller = new ProfileController();
-                return $controller->update($request, $response, $db);
-            }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
-            $registerRouteIfUnique('POST', '/profil/aggiorna', function ($request, $response) use ($app) {
-                $db = $app->getContainer()->get('db');
-                $controller = new ProfileController();
-                return $controller->update($request, $response, $db);
-            }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
-        }
+        $profileUpdateAliases = [
+            'it_IT' => ['/profilo/update'],
+            'en_US' => ['/profile/aggiorna'],
+            'de_DE' => ['/profil/update', '/profil/aggiorna'],
+        ];
+        foreach ($profileUpdateAliases[$locale] ?? [] as $aliasRoute) {
+            $registerRouteIfUnique('POST', $aliasRoute, $profileUpdateHandler, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Routes/web.php` around lines 257 - 282, Replace the repeated
locale-specific route blocks with a centralized map of hybrid aliases and a
single shared handler: build an associative array keyed by $locale (or a global
list) mapping to arrays of route paths (e.g.
'/profilo/update','/profile/aggiorna','/profil/update','/profil/aggiorna'), then
iterate over that map and call $registerRouteIfUnique for each path using one
closure that retrieves $db from $app and invokes (new
ProfileController())->update($request,$response,$db) with the same middleware
stack (CsrfMiddleware and
AuthMiddleware(['admin','staff','standard','premium'])); keep references to the
existing symbols $registerRouteIfUnique, ProfileController::update,
CsrfMiddleware, AuthMiddleware and $locale so the behavior and authorization
remain identical while removing duplicated blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.htaccess:
- Around line 18-20: The manual-upgrade.php script currently relies on a
hardcoded default password 'pinakes2026' (commented) which must be changed
manually; update scripts/manual-upgrade.php to prevent accidental production use
by (a) loading the upgrade password from an environment variable (e.g.,
UPGRADE_PASSWORD) instead of a hardcoded string and (b) adding an explicit
runtime check in the authentication logic that aborts/returns an error if the
configured password equals the known default 'pinakes2026' or is empty—locate
the password variable/constant and the authentication block in
manual-upgrade.php and implement the env lookup plus the guard that fails fast
when the default is detected.

In `@app/Models/BookRepository.php`:
- Around line 348-351: The validation for anno_pubblicazione currently uses
filter_var with ['options' => ['min_range' => 1]] which rejects negative (BCE)
years; update the validation where anno_pubblicazione is parsed (the block using
hasColumn('anno_pubblicazione'), the filter_var call, and the subsequent
addField('anno_pubblicazione', 'i', ...)) to accept signed integers — either
remove the min_range option or set options to the DB integer bounds (e.g.
min_range => -2147483648, max_range => 2147483647) so negative years are
allowed, and preserve the existing behavior of passing null to addField when
validation fails.

In `@installer/classes/Installer.php`:
- Around line 481-483: Move the calls to $pdo->beginTransaction() and
$pdo->exec('SET FOREIGN_KEY_CHECKS=0') inside the guarded try block so failures
during transaction start or FK disable are covered by the existing
catch/rollback/finally logic; locate the block that currently calls
$pdo->beginTransaction() and $pdo->exec('SET FOREIGN_KEY_CHECKS=0') immediately
before the try, remove them from there, and place them as the first statements
inside the try (keeping the existing catch/rollback/finally that references
rollback()/commit()/exec('SET FOREIGN_KEY_CHECKS=1')).

---

Duplicate comments:
In `@app/Support/Updater.php`:
- Around line 739-780: In getAllReleases(), don't treat any JSON payload as a
releases list—check for auth/errors before returning: if an HTTP status (from
$httpCode when curl used) is >=400 (especially 401/403) log and return []; after
json_decode($response, true) detect an error object (assoc array with keys like
"message", "error" or "documentation_url") or non-indexed array and in that case
debugLog the GitHub error details (include json_last_error_msg() and the decoded
message) and return []; ensure callers like getChangelog() only receive a
numeric-indexed array of releases by validating $releases is an indexed array
before using tag_name.
- Around line 877-903: The download always attaches Authorization via
getGitHubHeaders('application/octet-stream') which causes both the cURL and
file_get_contents paths to fail when the saved PAT is revoked; change the
download flow so that if the request returns HTTP 401/403 (or curl errno
indicating auth failure) you immediately retry the download without the
Authorization header (i.e. call a header helper that omits the token or pass a
flag to getGitHubHeaders to build anonymous headers) for both the cURL branch
around the curl_exec/curl_getinfo logic and the fallback file_get_contents path,
and only abort if the anonymous retry also fails.

In `@installer/database/data_de_DE.sql`:
- Around line 290-291: The seeded CMS page records for slugs 'about-us' and
'privacy-policy' include root-absolute links (e.g. '/admin/cms/about-us' and
'/admin/cms/privacy-policy') which can break installs in a subfolder; update
those link strings in the INSERTs to use either a relative path
('admin/cms/about-us', etc.) or a configurable base-path placeholder (e.g.
'{{APP_BASE_PATH}}/admin/cms/about-us') and apply the same change to every
occurrence in the 'about-us' and 'privacy-policy' content entries (and the other
duplicated occurrences called out) so seeded content respects the app base path.

In `@public/assets/main.css`:
- Around line 4662-4665: The generated utility blocks (e.g., the
.border-amber-300 block) violate the Stylelint rule
declaration-empty-line-before because there is no empty line before the first
declaration; update each generated utility class (for example .border-amber-300
and the other similar blocks around the noted lines) to insert a single blank
line before the first declaration (so the line before "--tw-border-opacity: 1;"
is empty) and apply the same spacing fix across all newly added utility blocks
(or regenerate/auto-fix the CSS) so the declaration-empty-line-before rule is
satisfied consistently.

In `@scripts/manual-upgrade.php`:
- Around line 351-358: The code currently calls file_put_contents($defaultsFile,
...) and then `@chmod`($defaultsFile, 0600) without checking for failure; update
the logic in the manual-upgrade script to check the return value of
file_put_contents for false and handle failure by logging an error (including
$defaultsFile and the PHP error message), preventing further backup/upgrade
steps (exit or throw), and only call chmod when file_put_contents succeeded;
reference the $defaultsFile variable, the file_put_contents() call and the
`@chmod`() call when making this change.

---

Nitpick comments:
In `@app/Routes/web.php`:
- Around line 257-282: Replace the repeated locale-specific route blocks with a
centralized map of hybrid aliases and a single shared handler: build an
associative array keyed by $locale (or a global list) mapping to arrays of route
paths (e.g.
'/profilo/update','/profile/aggiorna','/profil/update','/profil/aggiorna'), then
iterate over that map and call $registerRouteIfUnique for each path using one
closure that retrieves $db from $app and invokes (new
ProfileController())->update($request,$response,$db) with the same middleware
stack (CsrfMiddleware and
AuthMiddleware(['admin','staff','standard','premium'])); keep references to the
existing symbols $registerRouteIfUnique, ProfileController::update,
CsrfMiddleware, AuthMiddleware and $locale so the behavior and authorization
remain identical while removing duplicated blocks.

In `@app/Views/frontend/events.php`:
- Around line 32-38: Extract the duplicated IntlDateFormatter setup into a
single helper function (e.g., getLocalizedFormatters($locale)) that returns an
array or object with dateFormatter and timeFormatter (falling back to null when
class_exists('IntlDateFormatter') is false); replace the repeated blocks in the
views (where $dateFormatter, $timeFormatter and $locale are used) with a call to
getLocalizedFormatters($locale) and use the returned dateFormatter/timeFormatter
values, ensuring the helper captures the LONG/SHORT and NONE constants exactly
as in the original code so behavior and locale fallback remain unchanged.

In `@app/Views/libri/partials/book_form.php`:
- Around line 2245-2248: Duplicate change listeners on genereSelect and
sottogenereSelect cause syncHidden to run twice; remove the redundant
addEventListener calls (the lines attaching 'change' to genereSelect and
sottogenereSelect that call syncHidden) so only the existing handlers (which
already call syncHidden in their change callbacks) remain, or alternatively
consolidate by replacing the duplicates with a single shared handler
registration to avoid double execution; locate references to genereSelect,
sottogenereSelect, and syncHidden to make the change.

In `@locale/routes_de_DE.json`:
- Around line 1-32: Add a CI parity check that ensures the route key set in
locale/routes_de_DE.json (e.g., keys like "login", "logout", "register",
"verify_email", "api_catalog", "language_switch") exactly matches the base
locale route file; implement a small script (e.g., routes_parity_check) that
loads each locale JSON, computes set differences against the base locale
(missing and extra keys), prints human-readable diffs and exits non-zero on
mismatch, and wire that script into the CI pipeline to run on PRs so any future
drift in route keys fails the build.

In `@scripts/manual-upgrade.php`:
- Around line 260-269: The POST password check needs rate-limiting: implement
per-session and per-IP attempt tracking and cooldown around the existing check
that uses UPGRADE_PASSWORD and sets $_SESSION['upgrade_auth']; on each failed
attempt increment counters (e.g. $_SESSION['upgrade_attempts'] and
$_SESSION['upgrade_last_attempt']) and persist per-IP attempts (e.g. simple
flatfile or APCu keyed by $_SERVER['REMOTE_ADDR']) and if limits are exceeded
return an $error with remaining cooldown; on successful hash_equals reset the
session counters and regenerate id as you already do; ensure timestamps are
stored to enforce a cooldown window and that counters decay after the window to
avoid permanent lockout.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and 4c4ade7.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (41)
  • .htaccess
  • README.md
  • app/Controllers/FrontendController.php
  • app/Controllers/LibriController.php
  • app/Controllers/UpdateController.php
  • app/Models/BookRepository.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • app/Views/autori/index.php
  • app/Views/editori/index.php
  • app/Views/events/index.php
  • app/Views/frontend/event-detail.php
  • app/Views/frontend/events.php
  • app/Views/frontend/home-sections/events.php
  • app/Views/frontend/home-sections/genre_carousel.php
  • app/Views/frontend/home.php
  • app/Views/libri/index.php
  • app/Views/libri/partials/book_form.php
  • app/Views/libri/scheda_libro.php
  • app/Views/prestiti/index.php
  • app/Views/utenti/index.php
  • composer.json
  • frontend/js/vendor.js
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • public/assets/main.css
  • public/assets/vendor.bundle.js
  • scripts/.user.ini
  • scripts/create-release.sh
  • scripts/manual-update.php
  • scripts/manual-upgrade.php
  • vendor/composer/autoload_files.php
  • vendor/composer/autoload_static.php
  • vendor/composer/installed.json
  • vendor/composer/installed.php
💤 Files with no reviewable changes (1)
  • scripts/manual-update.php

Comment on lines +18 to 20
RewriteCond %{REQUEST_URI} (^|/)installer(/|$) [NC,OR]
RewriteCond %{REQUEST_URI} (^|/)scripts/manual-upgrade\.php$ [NC]
RewriteRule ^ - [L]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Locate upgrade/installer entrypoints ==="
fd manual-upgrade.php --type f
fd -g '*.php' installer --type f | head -20

echo
echo "=== Check manual-upgrade authentication and request guards ==="
rg -n -C2 "password|hash_equals|password_verify|UPGRADE|REQUEST_METHOD|csrf|token|die\(|exit\(" scripts/manual-upgrade.php 2>/dev/null || echo "File not found or no matches"

echo
echo "=== Check installer lockout after installation ==="
rg -n -C2 "\.installed|already installed|isInstalled|redirect|forbidden|deny|exit\(" installer 2>/dev/null || echo "No matches found in installer directory"

echo
echo "=== List all files in installer directory (if exists) ==="
find . -type d -name "installer" -o -name "scripts" | head -10

Repository: fabiodalez-dev/Pinakes

Length of output: 12733


🏁 Script executed:

#!/bin/bash

echo "=== Check force parameter authentication in installer/index.php ==="
sed -n '103,220p' installer/index.php

echo
echo "=== Check if there's any admin authentication check ==="
rg -n "admin|authenticate|authorized|session|check.*auth" installer/index.php | head -20

Repository: fabiodalez-dev/Pinakes

Length of output: 7097


Verify that the default password in scripts/manual-upgrade.php is explicitly changed before production deployment.

The .installed lock file properly protects the installer, and the ?force=1 parameter requires admin authentication—both are sound guards. However, manual-upgrade.php relies entirely on developer action: the default password 'pinakes2026' must be manually changed before uploading. This requirement is documented only in comments (lines 21–23), not enforced by code. Add an explicit check that fails the script if the password remains at its default value, or require the password to be provided via .env rather than hardcoded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.htaccess around lines 18 - 20, The manual-upgrade.php script currently
relies on a hardcoded default password 'pinakes2026' (commented) which must be
changed manually; update scripts/manual-upgrade.php to prevent accidental
production use by (a) loading the upgrade password from an environment variable
(e.g., UPGRADE_PASSWORD) instead of a hardcoded string and (b) adding an
explicit runtime check in the authentication logic that aborts/returns an error
if the configured password equals the known default 'pinakes2026' or is
empty—locate the password variable/constant and the authentication block in
manual-upgrade.php and implement the env lookup plus the guard that fails fast
when the default is detected.

- BookRepository: remove min_range=>1 from year validation to allow BCE years
- Updater: add auth retry (401/403) without token for releases and download
- Updater: validate releases response is indexed array via array_is_list()
- Installer: move beginTransaction() inside try block for proper error handling
- manual-upgrade.php: fail fast if credentials file write fails
- genre-bugs test: replace undefined dbQuery() with admin API delete call
phpstan as a require-dev dependency caused the autoloader to reference
vendor/phpstan/bootstrap.php, which broke every production ZIP that
didn't run composer install --no-dev first. Since phpstan is installed
system-wide (~/.composer/vendor/bin/phpstan), remove it from the
project's composer.json entirely. The pre-commit hook and the
code editor hook already fall back to the global binary.

Also updates vendor dependencies to latest compatible versions.
@fabiodalez-dev
Copy link
Owner Author

@CodeRabbit full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
vendor/slim/psr7/src/Cookies.php (1)

1-176: ⚠️ Potential issue | 🔴 Critical

Do not commit vendor files to git or modify vendor code directly.

The entire vendor/ directory (2400+ files) is currently tracked in git without a blanket .gitignore entry. This creates multiple critical issues:

  1. Modified vendor files will be lost or cause conflicts on composer install or composer update
  2. Repository bloat: Vendor code should never be version-controlled; only composer.lock is needed for reproducibility
  3. Broken dependency management: Composer expects to control the vendor directory; tracking it defeats this purpose

Required actions:

  1. Add vendor/ to .gitignore immediately (replace selective vendor ignores)
  2. Revert all modifications to vendor files, including the changes to vendor/slim/psr7/src/Cookies.php
  3. Remove the vendor directory from git history using git rm -r --cached vendor/ and commit
  4. If the upstream library has a bug, submit a PR to slimphp/Slim-Psr7, fork the package, or use middleware/wrapper classes
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/slim/psr7/src/Cookies.php` around lines 1 - 176, The PR modified
tracked vendor files (example: class Slim\Psr7\Cookies, method toHeader in
vendor/slim/psr7/src/Cookies.php); do not commit vendor changes — add "vendor/"
to .gitignore, revert all edits to the vendor directory (restore from composer
or HEAD), run git rm -r --cached vendor/ and commit that removal, then run
composer install to repopulate vendor; if you need behavior changes in
Cookies::toHeader or other vendor APIs, implement them in your own
wrapper/middleware or open a PR against slimphp/Slim-Psr7 instead of editing
vendor code.
vendor/slim/csrf/composer.json (1)

1-53: ⚠️ Potential issue | 🔴 Critical

Remove vendor directory from version control and add comprehensive .gitignore rule.

Vendor files are tracked in git, which violates Composer best practices. The current .gitignore contains only selective patterns (specific subdirectories and CHANGELOG files) rather than excluding the entire vendor/ directory. This causes:

  • Inflated repository size
  • Merge conflicts during dependency updates
  • Difficulty maintaining the codebase

Add /vendor/ to .gitignore to exclude the entire vendor directory, then remove tracked vendor files from git using:

git rm -r --cached vendor/
git commit -m "Remove vendor directory from version control"

Developers will regenerate the vendor directory locally via composer install.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/slim/csrf/composer.json` around lines 1 - 53, The repo is tracking the
vendor/ directory; update the project .gitignore to include a top-level entry
"/vendor/" to stop committing Composer-installed dependencies, then remove the
currently tracked vendor files from git cache (e.g., run git rm -r --cached
vendor/ and commit the removal) so future installs are generated via composer
install; reference the vendor/ path and ensure composer.json remains unchanged.
♻️ Duplicate comments (3)
app/Controllers/FrontendController.php (1)

1340-1341: ⚠️ Potential issue | 🟡 Minor

Remove unused $totalPages in genreArchive.

Line 1341 computes $totalPages, but this variable is never used in the method.

✂️ Minimal cleanup
         $row = $stmt->get_result()->fetch_assoc();
         $totalBooks = $row['total'] ?? 0;
-        $totalPages = ceil($totalBooks / $limit);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/FrontendController.php` around lines 1340 - 1341, In the
genreArchive method of FrontendController, remove the unused computation of
$totalPages (the line assigning $totalPages = ceil($totalBooks / $limit)); keep
the $totalBooks calculation if it’s used elsewhere but delete the $totalPages
variable and its assignment to eliminate the unused variable warning and dead
code.
installer/database/data_de_DE.sql (1)

290-291: ⚠️ Potential issue | 🟡 Minor

Avoid absolute /admin/... links in seeded CMS HTML.

Lines 290-291 still embed absolute admin paths (/admin/...), which can break subfolder installs. Use base-path-aware placeholders or relative links.

🔧 Minimal string-level fix
- href=\"/admin/cms/about-us\"
+ href=\"{{BASE_PATH}}/admin/cms/about-us\"

- href=\"/admin/cms/privacy-policy\"
+ href=\"{{BASE_PATH}}/admin/cms/privacy-policy\"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/database/data_de_DE.sql` around lines 290 - 291, Update the seeded
HTML rows in the cms_pages INSERTs for slugs 'about-us' and 'privacy-policy' to
remove hardcoded absolute admin paths (/admin/cms/about-us and
/admin/cms/privacy-policy) and use the app's base-path-aware placeholder or a
relative admin path instead (so subfolder installs work); modify the VALUES for
the two INSERT IGNORE INTO `cms_pages` entries to replace those two links with
the project's agreed placeholder (e.g., a template variable or APP_BASE_PATH +
relative path) so the runtime will resolve the correct base path.
app/Support/Updater.php (1)

780-781: ⚠️ Potential issue | 🟠 Major

Add unauthenticated retry to the file_get_contents fallback paths.

Line 780 and Line 964 always send the Authorization header. If cURL is unavailable and the stored token is invalid/revoked, the updater can fail even though tokenless requests would work. This makes an optional token effectively mandatory on those paths.

🔧 Proposed fix
-            $context = stream_context_create([
+            $context = stream_context_create([
                 'http' => [
                     'method' => 'GET',
                     'header' => $this->getGitHubHeaders(),
                     'timeout' => 30,
                     'ignore_errors' => true
                 ]
             ]);
-
-            $response = `@file_get_contents`($url, false, $context);
+            $response = `@file_get_contents`($url, false, $context);
+            $status = 0;
+            /** `@var` array<int, string> $http_response_header */
+            if (!empty($http_response_header[0]) && preg_match('/HTTP\/\d\.\d\s+(\d+)/', $http_response_header[0], $m)) {
+                $status = (int) $m[1];
+            }
+            if (in_array($status, [401, 403], true) && $this->githubToken !== '') {
+                $savedToken = $this->githubToken;
+                $this->githubToken = '';
+                try {
+                    $context = stream_context_create([
+                        'http' => [
+                            'method' => 'GET',
+                            'header' => $this->getGitHubHeaders(),
+                            'timeout' => 30,
+                            'ignore_errors' => true
+                        ]
+                    ]);
+                    $response = `@file_get_contents`($url, false, $context);
+                } finally {
+                    $this->githubToken = $savedToken;
+                }
+            }

Also applies to: 964-965

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 780 - 781, The fallback
file_get_contents calls currently always include the Authorization header from
getGitHubHeaders(), causing failures when the stored token is invalid; update
the fallback logic in the Updater methods that use file_get_contents (the calls
around where 'header' => $this->getGitHubHeaders(), 'timeout' => 30 are passed)
to do an unauthenticated retry: first attempt file_get_contents with the current
headers, and if that returns false or produces an HTTP auth error (401/403) then
call file_get_contents again with headers that omit the Authorization value
(build headers without the token or call a variant of getGitHubHeaders that
excludes auth) and the same timeout so public tokenless requests succeed. Ensure
this change is applied to both the occurrence around getGitHubHeaders() at the
shown block and the similar block at lines ~964-965.
🧹 Nitpick comments (4)
vendor/tecnickcom/tcpdf/config/tcpdf_config.php (1)

11-11: Malformed copyright year range.

Same pattern as other TCPDF files: 2004-2014 2026 should be 2004-2026. This is an upstream issue in TCPDF 6.11.0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/config/tcpdf_config.php` at line 11, The copyright
header in tcpdf_config.php contains a malformed year range string "2004-2014
2026"; update that header to the correct range "2004-2026" so it matches other
TCPDF files (i.e., replace the "2004-2014 2026" token in the file with
"2004-2026").
vendor/tecnickcom/tcpdf/include/tcpdf_colors.php (1)

10-10: Malformed copyright year range in vendor file.

The copyright line shows 2002-2013 2026 which appears to be a merge artifact or upstream bug. The expected format would be 2002-2026 (consistent with other TCPDF headers like the README line 9).

This is part of the TCPDF 6.11.0 vendor update. Since this is a third-party dependency, no action is required unless you want to report this upstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_colors.php` at line 10, Update the
malformed copyright header in the TCPDF vendor file by replacing the incorrect
year sequence "2002-2013 2026" with the correct hyphenated range "2002-2026" in
the top-of-file copyright comment (the header comment string in
tcpdf_colors.php) so the license header matches other TCPDF files.
vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php (1)

100-107: Reset inode/retry state in close() to avoid stale lifecycle state.

$inodeUrl and $retrying can remain stale across explicit close/reset cycles.

♻️ Suggested patch
 public function close(): void
 {
     if (null !== $this->url && \is_resource($this->stream)) {
         fclose($this->stream);
     }
     $this->stream = null;
     $this->dirCreated = null;
+    $this->inodeUrl = null;
+    $this->retrying = false;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php` around lines
100 - 107, The close() method currently nulls $this->stream and
$this->dirCreated but leaves $this->inodeUrl and $this->retrying stale; update
StreamHandler::close() to also reset $this->inodeUrl (set to null) and
$this->retrying (set to false) so any subsequent open/reopen uses a fresh
lifecycle state; locate the close() method and add clearing of $this->inodeUrl
and $this->retrying alongside the existing $this->stream and $this->dirCreated
resets.
app/Controllers/LibriController.php (1)

541-542: Normalize numero_pagine consistently with the other numeric fields.

Good addition in both create/update defaults. Consider explicitly casting/clamping numero_pagine near peso/prezzo conversion so invalid input handling is consistent in both flows.

♻️ Suggested patch
@@
-        $fields['peso'] = $fields['peso'] !== null && $fields['peso'] !== '' ? (float) $fields['peso'] : null;
-        $fields['prezzo'] = $fields['prezzo'] !== null && $fields['prezzo'] !== '' ? (float) $fields['prezzo'] : null;
+        $fields['peso'] = $fields['peso'] !== null && $fields['peso'] !== '' ? (float) $fields['peso'] : null;
+        $fields['prezzo'] = $fields['prezzo'] !== null && $fields['prezzo'] !== '' ? (float) $fields['prezzo'] : null;
+        $fields['numero_pagine'] = $fields['numero_pagine'] !== null && $fields['numero_pagine'] !== ''
+            ? max(1, (int) $fields['numero_pagine'])
+            : null;
@@
-        $fields['peso'] = $fields['peso'] !== null && $fields['peso'] !== '' ? (float) $fields['peso'] : null;
-        $fields['prezzo'] = $fields['prezzo'] !== null && $fields['prezzo'] !== '' ? (float) $fields['prezzo'] : null;
+        $fields['peso'] = $fields['peso'] !== null && $fields['peso'] !== '' ? (float) $fields['peso'] : null;
+        $fields['prezzo'] = $fields['prezzo'] !== null && $fields['prezzo'] !== '' ? (float) $fields['prezzo'] : null;
+        $fields['numero_pagine'] = $fields['numero_pagine'] !== null && $fields['numero_pagine'] !== ''
+            ? max(1, (int) $fields['numero_pagine'])
+            : null;

Also applies to: 1047-1048

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/LibriController.php` around lines 541 - 542, Normalize
numero_pagine the same way as peso/prezzo by explicitly casting and clamping it
when you build defaults and during create/update flows: locate where
'numero_pagine' is set to null in the defaults and the conversion logic around
the peso/prezzo handling (the methods that perform peso/prezzo conversion), and
apply the same validation pattern (cast to int, enforce min 0 and optional max,
and fallback to null or 0 as your other numeric fields do) so input is handled
consistently in both create and update paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Support/Updater.php`:
- Around line 921-954: The code accepts cURL response bodies even when HTTP
status >=400, so ensure responses with $curlErrno !== 0 or $httpCode >= 400 are
treated as failures: when the initial request yields $curlErrno !== 0 or
$httpCode >= 400 set $fileContent = false (instead of leaving any non-false
body), then attempt the retry without token and only assign $fileContent =
$retryContent if $retryContent !== false AND $retryCode >= 200 AND $retryCode <
400; otherwise keep $fileContent as false and log the error (use the existing
variables $curlErrno, $curlError, $httpCode, $retryContent, $retryCode and keep
using debugLog for warnings).

In `@app/Views/libri/index.php`:
- Line 839: The DataTable init uses window.getDtLanguage() directly which can
throw if the global helper is missing; change the code around the DataTable
options (the line containing language: window.getDtLanguage()) to call a safe
accessor like (typeof window.getDtLanguage === 'function' ?
window.getDtLanguage() : {}) or provide a sensible default (e.g. language: {} or
language: { url: '...' }) so table initialization won't crash on stale assets;
update the DataTable initialization block that references window.getDtLanguage
to use this guarded fallback.

In `@installer/steps/step0.php`:
- Around line 15-29: The locale fallback and allowed list use inconsistent short
code 'it'—update $allowedLanguages to use 'it_IT' instead of 'it', change the
fallback assignment for $selectedLanguage to 'it_IT', and update the match
expression that sets $_SESSION['installer_language'] to map 'it_IT' to 'it'
(keeping 'en_US' => 'en' and 'de_DE' => 'de'); ensure $_SESSION['app_locale']
continues to be set to $selectedLanguage and that this aligns with the project's
normalizeInstallerLocale() and canonical I18n locale codes.

In `@scripts/manual-upgrade.php`:
- Around line 575-578: The current preg_replace on $stmtSql removes every line
starting with -- inside the statement body; change it to remove only a leading
block of SQL comment lines so inline/embedded -- comments remain in multi-line
SQL. Locate the foreach over $statements and the $stmtSql manipulation (the
preg_replace and subsequent trim), and replace the pattern/logic so it strips
only an initial contiguous sequence of comment lines (a leading comment block) —
e.g. match from the start of the string for repeated lines that are purely
comments/whitespace and remove that chunk — leaving any -- lines that occur
after the first executable SQL untouched.

In `@tests/genre-bugs.spec.js`:
- Around line 305-306: The cleanup POST to
`${BASE}/admin/libri/elimina/${bookId}` currently isn't checked; update the test
to assert the request succeeded by capturing the response from page.request.post
and asserting its status is in the 2xx range (or the expected status code) and
optionally validating response body for success; reference the existing call to
page.request.post(`${BASE}/admin/libri/elimina/${bookId}`) and the variables
BASE and bookId so you add the assertions immediately after that call.

In `@vendor/laravel/serializable-closure/composer.json`:
- Around line 24-28: Add a new GitHub Actions workflow (e.g.,
test-new-majors.yml) that runs CI against the expanded dev-version combinations
introduced in composer.json: test with illuminate/support ^13.0, pestphp/pest
^4.0, and symfony/var-dumper ^8.0.0 by adding a matrix that varies PHP versions
(at least 8.1 and 8.2 to match existing release/test workflows) and separate
matrix entries for the new major versions of "illuminate/support",
"pestphp/pest", and "symfony/var-dumper"; ensure the job runs the project’s
existing composer install/test steps so these combinations are exercised in CI.

In `@vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php`:
- Around line 1363-1433: The parser in verifyCandidateSignature incorrectly
flips from 'params' to 'body' on any ')' token, breaking param parsing for
defaults with parentheses; modify the token loop to track parenthesis nesting
(e.g., introduce $depth = 0), set $state = 'params' and $depth = 1 when the
first '(' is seen, increment $depth for additional '(' and decrement on ')', and
only switch $state to 'body' when in 'params' and $depth returns to 0; ensure
$params still collects T_VARIABLE when $state === 'params' and $vars collects
when $state === 'body' (skipping $this) so parameter counting and use/static
variable checks remain correct.

In `@vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php`:
- Around line 48-54: The constructor's timezone is assigned after computing
$this->nextRotation which causes rotation to use server default timezone; move
the assignment of $this->timezone before computing $this->nextRotation, and
change getNextRotation() to accept a DateTimeZone (or null) parameter and use
that timezone when building the DateTimeImmutable for the next rotation; update
the constructor call to pass $this->timezone into
getNextRotation($this->timezone) and adjust any other internal calls to
getNextRotation to supply the configured timezone so filename generation and
rotation boundaries use the same timezone.

In `@vendor/tecnickcom/tcpdf/config/tcpdf_config.php`:
- Line 117: The PDF_HEADER_STRING constant in tcpdf_config.php is corrupted
("by2026 Nicola Asuni..."); update the constant PDF_HEADER_STRING to the correct
header text (e.g., "by Nicola Asuni - Tecnick.com\nwww.tcpdf.org") to restore
the intended PDF header, or alternatively override this constant in your
application configuration if you must avoid editing vendor files; after fixing,
run a PDF generation test to confirm the header displays correctly and consider
reporting the typo upstream to the TCPDF maintainers.

In `@vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php`:
- Line 10: Update the malformed copyright year range in the file's header
comment: replace the string "2010-2014 2026" with the corrected range
"2010-2026" in the datamatrix.php header comment so the copyright line reads a
single valid range; locate the header comment at the top of
vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php and edit that copyright
line accordingly.

In `@vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php`:
- Line 10: Replace the malformed copyright header string "Copyright (C)
2010-2013 2026 Nicola Asuni - Tecnick.com LTD" in pdf417.php with the correct
upstream text (e.g., match the official TCPDF header such as "Copyright (C)
2010–2010 Nicola Asuni - Tecnick.com LTD") or, if this year-range change was
intentional, add a short inline comment explaining the local modification and
its rationale; update only the header line containing that exact string so the
vendor file matches upstream or documents the deviation.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php`:
- Line 1387: The checksum adjustment for subsetted TTF is being written to the
wrong bytes: locate the write that composes $font using $table['head']['offset']
and $offset (the line that builds $font with pack('N', $checkSumAdjustment)) and
change the slice boundaries from +4..+8 to +8..+12 so pack('N',
$checkSumAdjustment) is inserted at bytes 0x08–0x0B of the head table (i.e.
replace the substr offsets +4 and +8 with +8 and +12 respectively when
assembling $font).

In `@vendor/tecnickcom/tcpdf/include/tcpdf_images.php`:
- Line 277: Replace the misspelled diagnostic comment "Unknownn" with "Unknown"
in the PNG parser comments: search for the literal "Unknownn" in
vendor/tecnickcom/tcpdf/include/tcpdf_images.php (occurrences referenced near
the PNG parsing logic/comments around the PNG handling functions) and update
each comment (e.g., the lines currently reading "// Unknownn compression method"
and "// Unknownn filter method") to read "// Unknown ... method" — i.e., change
"Unknownn" to "Unknown" to fix the typo.

In `@vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php`:
- Line 157: The PDF header default constant PDF_HEADER_STRING contains a typo
"by2026 Nicola Asuni - Tecnick.com\nwww.tcpdf.org"; update the definition of
PDF_HEADER_STRING in tcpdf_autoconfig.php to remove the accidental "2026" so it
reads "by Nicola Asuni - Tecnick.com\nwww.tcpdf.org" (update the string assigned
to the PDF_HEADER_STRING constant).

In `@vendor/tecnickcom/tcpdf/tcpdf.php`:
- Around line 6925-6927: In fitBlock(), the clamp that checks if ($y + $h) >
$this->PageBreakTrigger + $this->bMargin allows content into the footer margin
when fitonpage is used; change the clamp to compare against
$this->PageBreakTrigger (remove the added + $this->bMargin) so the computed $h
and derived $w ($h * $ratio_wh) are constrained to the PageBreakTrigger
boundary; update any related calculations in fitBlock() that adjust $h/$w when
the clamp fires to use $this->PageBreakTrigger and ensure fitonpage honors the
configured bottom margin.
- Around line 23733-23737: The preg_split used to parse SVG path parameters is
using a capturing group which keeps delimiters (commas) in $rawparams and causes
misalignment when later passed to getHTMLUnitToUnits(); fix by changing the
split to not capture delimiters (e.g., remove the parentheses in the regex so it
splits on /[,\s]+/si) or use a non-capturing pattern and/or PREG_SPLIT_NO_EMPTY,
and ensure the $rawparams filter removes empty and delimiter tokens so only
numeric parameter strings reach getHTMLUnitToUnits().
- Around line 2909-2914: The setCompression method currently sets
$this->compress = false and immediately returns, making the gzcompress check and
the $compress parameter ineffective; remove the early "return;" so the method
can check function_exists('gzcompress') and set $this->compress = true when
$compress is true and gzcompress is available (otherwise leave it false),
ensuring setCompression($compress=true) behaves as intended in function
setCompression and uses the $compress argument and PHP's gzcompress availability
to toggle the $this->compress property.

---

Outside diff comments:
In `@vendor/slim/csrf/composer.json`:
- Around line 1-53: The repo is tracking the vendor/ directory; update the
project .gitignore to include a top-level entry "/vendor/" to stop committing
Composer-installed dependencies, then remove the currently tracked vendor files
from git cache (e.g., run git rm -r --cached vendor/ and commit the removal) so
future installs are generated via composer install; reference the vendor/ path
and ensure composer.json remains unchanged.

In `@vendor/slim/psr7/src/Cookies.php`:
- Around line 1-176: The PR modified tracked vendor files (example: class
Slim\Psr7\Cookies, method toHeader in vendor/slim/psr7/src/Cookies.php); do not
commit vendor changes — add "vendor/" to .gitignore, revert all edits to the
vendor directory (restore from composer or HEAD), run git rm -r --cached vendor/
and commit that removal, then run composer install to repopulate vendor; if you
need behavior changes in Cookies::toHeader or other vendor APIs, implement them
in your own wrapper/middleware or open a PR against slimphp/Slim-Psr7 instead of
editing vendor code.

---

Duplicate comments:
In `@app/Controllers/FrontendController.php`:
- Around line 1340-1341: In the genreArchive method of FrontendController,
remove the unused computation of $totalPages (the line assigning $totalPages =
ceil($totalBooks / $limit)); keep the $totalBooks calculation if it’s used
elsewhere but delete the $totalPages variable and its assignment to eliminate
the unused variable warning and dead code.

In `@app/Support/Updater.php`:
- Around line 780-781: The fallback file_get_contents calls currently always
include the Authorization header from getGitHubHeaders(), causing failures when
the stored token is invalid; update the fallback logic in the Updater methods
that use file_get_contents (the calls around where 'header' =>
$this->getGitHubHeaders(), 'timeout' => 30 are passed) to do an unauthenticated
retry: first attempt file_get_contents with the current headers, and if that
returns false or produces an HTTP auth error (401/403) then call
file_get_contents again with headers that omit the Authorization value (build
headers without the token or call a variant of getGitHubHeaders that excludes
auth) and the same timeout so public tokenless requests succeed. Ensure this
change is applied to both the occurrence around getGitHubHeaders() at the shown
block and the similar block at lines ~964-965.

In `@installer/database/data_de_DE.sql`:
- Around line 290-291: Update the seeded HTML rows in the cms_pages INSERTs for
slugs 'about-us' and 'privacy-policy' to remove hardcoded absolute admin paths
(/admin/cms/about-us and /admin/cms/privacy-policy) and use the app's
base-path-aware placeholder or a relative admin path instead (so subfolder
installs work); modify the VALUES for the two INSERT IGNORE INTO `cms_pages`
entries to replace those two links with the project's agreed placeholder (e.g.,
a template variable or APP_BASE_PATH + relative path) so the runtime will
resolve the correct base path.

---

Nitpick comments:
In `@app/Controllers/LibriController.php`:
- Around line 541-542: Normalize numero_pagine the same way as peso/prezzo by
explicitly casting and clamping it when you build defaults and during
create/update flows: locate where 'numero_pagine' is set to null in the defaults
and the conversion logic around the peso/prezzo handling (the methods that
perform peso/prezzo conversion), and apply the same validation pattern (cast to
int, enforce min 0 and optional max, and fallback to null or 0 as your other
numeric fields do) so input is handled consistently in both create and update
paths.

In `@vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php`:
- Around line 100-107: The close() method currently nulls $this->stream and
$this->dirCreated but leaves $this->inodeUrl and $this->retrying stale; update
StreamHandler::close() to also reset $this->inodeUrl (set to null) and
$this->retrying (set to false) so any subsequent open/reopen uses a fresh
lifecycle state; locate the close() method and add clearing of $this->inodeUrl
and $this->retrying alongside the existing $this->stream and $this->dirCreated
resets.

In `@vendor/tecnickcom/tcpdf/config/tcpdf_config.php`:
- Line 11: The copyright header in tcpdf_config.php contains a malformed year
range string "2004-2014 2026"; update that header to the correct range
"2004-2026" so it matches other TCPDF files (i.e., replace the "2004-2014 2026"
token in the file with "2004-2026").

In `@vendor/tecnickcom/tcpdf/include/tcpdf_colors.php`:
- Line 10: Update the malformed copyright header in the TCPDF vendor file by
replacing the incorrect year sequence "2002-2013 2026" with the correct
hyphenated range "2002-2026" in the top-of-file copyright comment (the header
comment string in tcpdf_colors.php) so the license header matches other TCPDF
files.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and 922c99d.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (104)
  • .htaccess
  • README.md
  • app/Controllers/FrontendController.php
  • app/Controllers/LibriController.php
  • app/Controllers/UpdateController.php
  • app/Models/BookRepository.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • app/Views/autori/index.php
  • app/Views/editori/index.php
  • app/Views/events/index.php
  • app/Views/frontend/event-detail.php
  • app/Views/frontend/events.php
  • app/Views/frontend/home-sections/events.php
  • app/Views/frontend/home-sections/genre_carousel.php
  • app/Views/frontend/home.php
  • app/Views/libri/index.php
  • app/Views/libri/partials/book_form.php
  • app/Views/libri/scheda_libro.php
  • app/Views/prestiti/index.php
  • app/Views/utenti/index.php
  • frontend/js/vendor.js
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • public/assets/main.css
  • public/assets/vendor.bundle.js
  • scripts/.user.ini
  • scripts/create-release.sh
  • scripts/manual-update.php
  • scripts/manual-upgrade.php
  • tests/genre-bugs.spec.js
  • vendor/composer/autoload_classmap.php
  • vendor/composer/autoload_files.php
  • vendor/composer/autoload_static.php
  • vendor/composer/installed.json
  • vendor/composer/installed.php
  • vendor/graham-campbell/result-type/composer.json
  • vendor/laravel/serializable-closure/composer.json
  • vendor/laravel/serializable-closure/src/Serializers/Native.php
  • vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php
  • vendor/monolog/monolog/README.md
  • vendor/monolog/monolog/composer.json
  • vendor/monolog/monolog/src/Monolog/Formatter/GelfMessageFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php
  • vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php
  • vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/Handler.php
  • vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php
  • vendor/slim/csrf/composer.json
  • vendor/slim/psr7/composer.json
  • vendor/slim/psr7/src/Cookies.php
  • vendor/slim/slim/MAINTAINERS.md
  • vendor/slim/slim/Slim/App.php
  • vendor/slim/slim/composer.json
  • vendor/slim/slim/psalm.xml
  • vendor/tecnickcom/tcpdf/CHANGELOG.TXT
  • vendor/tecnickcom/tcpdf/LICENSE.TXT
  • vendor/tecnickcom/tcpdf/README.md
  • vendor/tecnickcom/tcpdf/VERSION
  • vendor/tecnickcom/tcpdf/composer.json
  • vendor/tecnickcom/tcpdf/config/tcpdf_config.php
  • vendor/tecnickcom/tcpdf/fonts/ae_fonts_2.0/README
  • vendor/tecnickcom/tcpdf/fonts/freefont-20100919/COPYING
  • vendor/tecnickcom/tcpdf/fonts/freefont-20100919/CREDITS
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/COPYING
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/CREDITS
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/README
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/TROUBLESHOOTING
  • vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php
  • vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php
  • vendor/tecnickcom/tcpdf/include/barcodes/qrcode.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_colors.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_filters.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_font_data.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_images.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_static.php
  • vendor/tecnickcom/tcpdf/tcpdf.php
  • vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php
  • vendor/tecnickcom/tcpdf/tcpdf_barcodes_1d.php
  • vendor/tecnickcom/tcpdf/tcpdf_barcodes_2d.php
  • vendor/tecnickcom/tcpdf/tools/tcpdf_addfont.php
  • vendor/vlucas/phpdotenv/composer.json
  • vendor/vlucas/phpdotenv/src/Parser/EntryParser.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php
💤 Files with no reviewable changes (7)
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php
  • vendor/slim/slim/MAINTAINERS.md
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php
  • vendor/slim/slim/psalm.xml
  • vendor/vlucas/phpdotenv/src/Parser/EntryParser.php
  • vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php
  • scripts/manual-update.php

}
if (ord(fread($f, 1)) != 0) {
// Unknown compression method
// Unknownn compression method
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "Unknownn compression method|Unknownn filter method" vendor/tecnickcom/tcpdf/include/tcpdf_images.php

Repository: fabiodalez-dev/Pinakes

Length of output: 171


🏁 Script executed:

# Also check the specific lines mentioned
sed -n '277p;282p;334p' vendor/tecnickcom/tcpdf/include/tcpdf_images.php

Repository: fabiodalez-dev/Pinakes

Length of output: 159


🏁 Script executed:

# Check broader context around these lines
sed -n '275,280p;280,285p;332,336p' vendor/tecnickcom/tcpdf/include/tcpdf_images.php

Repository: fabiodalez-dev/Pinakes

Length of output: 403


Fix typo regression in PNG parser diagnostics.

Lines 277, 282, and 334 contain "Unknownn" (double n) in diagnostic comments. Correct to "Unknown" for consistency.

Affected locations
  • Line 277: // Unknownn compression method
  • Line 282: // Unknownn filter method
  • Line 334: // Unknownn filter method
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_images.php` at line 277, Replace the
misspelled diagnostic comment "Unknownn" with "Unknown" in the PNG parser
comments: search for the literal "Unknownn" in
vendor/tecnickcom/tcpdf/include/tcpdf_images.php (occurrences referenced near
the PNG parsing logic/comments around the PNG handling functions) and update
each comment (e.g., the lines currently reading "// Unknownn compression method"
and "// Unknownn filter method") to read "// Unknown ... method" — i.e., change
"Unknownn" to "Unknown" to fix the typo.


if (!defined('PDF_HEADER_STRING')) {
define ('PDF_HEADER_STRING', "by Nicola Asuni - Tecnick.com\nwww.tcpdf.org");
define ('PDF_HEADER_STRING', "by2026 Nicola Asuni - Tecnick.com\nwww.tcpdf.org");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "PDF_HEADER_STRING|by2026" vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php

Repository: fabiodalez-dev/Pinakes

Length of output: 193


Fix default PDF header text typo.

Line 157 uses by2026, which appears to be an accidental concatenation and will leak into generated document headers. Should be by.

Proposed fix
-define ('PDF_HEADER_STRING', "by2026 Nicola Asuni - Tecnick.com\nwww.tcpdf.org");
+define ('PDF_HEADER_STRING', "by Nicola Asuni - Tecnick.com\nwww.tcpdf.org");
📝 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.

Suggested change
define ('PDF_HEADER_STRING', "by2026 Nicola Asuni - Tecnick.com\nwww.tcpdf.org");
define ('PDF_HEADER_STRING', "by Nicola Asuni - Tecnick.com\nwww.tcpdf.org");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php` at line 157, The PDF header
default constant PDF_HEADER_STRING contains a typo "by2026 Nicola Asuni -
Tecnick.com\nwww.tcpdf.org"; update the definition of PDF_HEADER_STRING in
tcpdf_autoconfig.php to remove the accidental "2026" so it reads "by Nicola
Asuni - Tecnick.com\nwww.tcpdf.org" (update the string assigned to the
PDF_HEADER_STRING constant).

Comment on lines 2909 to 2914
public function setCompression($compress=true) {
$this->compress = false;
$this->compress = false; return;
if (function_exists('gzcompress')) {
if ($compress) {
if ( !$this->pdfa_mode) {
$this->compress = true;
}
$this->compress = true;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="vendor/tecnickcom/tcpdf/tcpdf.php"

echo "[local]"
rg -n -C2 'public function setCompression|\$this->compress = false; return;|\$this->compress = true;|pdfa_mode' "$FILE"

echo ""
echo "[upstream 6.10.0]"
curl -fsSL https://raw.githubusercontent.com/tecnickcom/TCPDF/6.10.0/tcpdf.php \
  | rg -n -C2 'public function setCompression|\$this->compress = false;|\$this->compress = true;|pdfa_mode'

echo ""
echo "[upstream 6.11.0]"
curl -fsSL https://raw.githubusercontent.com/tecnickcom/TCPDF/6.11.0/tcpdf.php \
  | rg -n -C2 'public function setCompression|\$this->compress = false; return;|\$this->compress = true;|pdfa_mode'

Repository: fabiodalez-dev/Pinakes

Length of output: 25553


setCompression() is disabled by an early return on line 2910.

The early return; statement prevents the function from ever enabling compression. This is a regression from upstream version 6.10.0, which had the correct implementation. The rest of the method (lines 2911–2915) is unreachable dead code.

Proposed fix
 public function setCompression($compress=true) {
-		$this->compress = false; return;
-		if (function_exists('gzcompress')) {
-			if ($compress) {
-                $this->compress = true;
-			}
-		}
+		$this->compress = false;
+		if (function_exists('gzcompress') && $compress) {
+			if (!$this->pdfa_mode) {
+				$this->compress = true;
+			}
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php` around lines 2909 - 2914, The
setCompression method currently sets $this->compress = false and immediately
returns, making the gzcompress check and the $compress parameter ineffective;
remove the early "return;" so the method can check function_exists('gzcompress')
and set $this->compress = true when $compress is true and gzcompress is
available (otherwise leave it false), ensuring setCompression($compress=true)
behaves as intended in function setCompression and uses the $compress argument
and PHP's gzcompress availability to toggle the $this->compress property.

Comment on lines +6925 to 6927
if (($y + $h) > $this->PageBreakTrigger + $this->bMargin) {
$h = $this->PageBreakTrigger + $this->bMargin - $y;
$w = ($h * $ratio_wh);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

fitBlock() clamp appears to allow content into footer margin.

Line 6925-Line 6926 uses PageBreakTrigger + bMargin (effectively page bottom), which can violate the configured bottom margin when fitonpage is true.

📏 Proposed fix
-			if (($y + $h) > $this->PageBreakTrigger + $this->bMargin) {
-				$h = $this->PageBreakTrigger + $this->bMargin - $y;
+			if (($y + $h) > $this->PageBreakTrigger) {
+				$h = $this->PageBreakTrigger - $y;
 				$w = ($h * $ratio_wh);
 			}

Use this script to compare current behavior with upstream versions:

#!/bin/bash
set -euo pipefail

FILE="vendor/tecnickcom/tcpdf/tcpdf.php"

echo "[local fitBlock clamp]"
rg -n -C3 'protected function fitBlock|PageBreakTrigger \+ \$this->bMargin|PageBreakTrigger - \$y' "$FILE"

echo "[upstream 6.10.0 fitBlock clamp]"
curl -fsSL https://raw.githubusercontent.com/tecnickcom/TCPDF/6.10.0/tcpdf.php \
  | rg -n -C3 'protected function fitBlock|\(\$y \+ \$h\) > \$this->PageBreakTrigger'

echo "[upstream 6.11.0 fitBlock clamp]"
curl -fsSL https://raw.githubusercontent.com/tecnickcom/TCPDF/6.11.0/tcpdf.php \
  | rg -n -C3 'protected function fitBlock|\(\$y \+ \$h\) > \$this->PageBreakTrigger(\s*\+\s*\$this->bMargin)?'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php` around lines 6925 - 6927, In fitBlock(),
the clamp that checks if ($y + $h) > $this->PageBreakTrigger + $this->bMargin
allows content into the footer margin when fitonpage is used; change the clamp
to compare against $this->PageBreakTrigger (remove the added + $this->bMargin)
so the computed $h and derived $w ($h * $ratio_wh) are constrained to the
PageBreakTrigger boundary; update any related calculations in fitBlock() that
adjust $h/$w when the clamp fires to use $this->PageBreakTrigger and ensure
fitonpage honors the configured bottom margin.

Lightweight fixed button in bottom-right corner that appears after
scrolling 400px. Uses pure JS DOM creation appended to body to avoid
Tailwind specificity conflicts and CSS containment issues with
position:fixed. Rose accent on hover matches app theme.

Closes #72
- Updater: set $fileContent = false on HTTP >= 400 before retry logic
- Updater: add unauthenticated retry to file_get_contents fallback paths
- DataTables: guard getDtLanguage() with typeof check in all 5 views
- Installer step0: use canonical it_IT locale code instead of short 'it'
- manual-upgrade: strip only leading SQL comment block, not inline comments
- genre-bugs test: assert cleanup response status
- LibriController: normalize numero_pagine consistently with peso/prezzo
- CMS seed data: remove hardcoded /admin/cms/ links from all 3 locales
- FrontendController $totalPages: NOT removed (used by archive.php view)
Move keyboard shortcuts from books-only page to global layout with:
- Keyboard icon button in admin header toolbar (hidden on mobile)
- Two-section modal: Navigation (global) + Books Management (contextual)
- GitHub-style G-prefix navigation (G→D dashboard, G→B books, etc.)
- ? key opens shortcuts help from any admin page
- Mac-aware modifier key labels (⌘ vs Ctrl)
- Fix E2E test to use correct #shortcuts-modal selector
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

♻️ Duplicate comments (6)
vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php (1)

48-54: ⚠️ Potential issue | 🟠 Major

Rotation cutoff still ignores configured timezone.

Line 53 computes nextRotation before Line 54 assigns $this->timezone, and getNextRotation() (Lines 226-232) still builds dates in server-default timezone. This makes rotation happen at a different boundary than filename dating (Line 183).

Suggested fix
-        $this->nextRotation = $this->getNextRotation();
         $this->timezone = $timezone;
+        $this->nextRotation = $this->getNextRotation();
-    protected function getNextRotation(): \DateTimeImmutable
+    protected function getNextRotation(): \DateTimeImmutable
     {
+        $now = new \DateTimeImmutable('now', $this->timezone);
+
         return match (str_replace(['/','_','.'], '-', $this->dateFormat)) {
-            self::FILE_PER_MONTH => (new \DateTimeImmutable('first day of next month'))->setTime(0, 0, 0),
-            self::FILE_PER_YEAR => (new \DateTimeImmutable('first day of January next year'))->setTime(0, 0, 0),
-            default => (new \DateTimeImmutable('tomorrow'))->setTime(0, 0, 0),
+            self::FILE_PER_MONTH => $now->modify('first day of next month')->setTime(0, 0, 0),
+            self::FILE_PER_YEAR => $now->modify('first day of January next year')->setTime(0, 0, 0),
+            default => $now->modify('tomorrow')->setTime(0, 0, 0),
         };
     }
#!/bin/bash
# Read-only verification of timezone flow in constructor + rotation boundary
nl -ba vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php | sed -n '48,58p;123,130p;178,186p;226,233p'

Also applies to: 127-127, 226-232

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php` around
lines 48 - 54, The constructor computes $this->nextRotation by calling
getNextRotation() before assigning $this->timezone, causing rotation cutoff to
ignore the configured timezone; fix it by assigning $this->timezone (the
DateTimeZone|null parameter) before calling $this->getNextRotation() in
__construct() (or alternatively change getNextRotation() to accept a timezone
argument), ensuring getNextRotation() uses the configured timezone when
calculating rotation boundaries and keeping filename dating in
setFilenameFormat()/getDatedFilename() consistent.
vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php (1)

157-157: ⚠️ Potential issue | 🟡 Minor

Fix visible typo in default PDF header string.

Line 157 contains by2026, which will leak into generated PDF headers. It should be by.

🩹 Proposed fix
-	define ('PDF_HEADER_STRING', "by2026 Nicola Asuni - Tecnick.com\nwww.tcpdf.org");
+	define ('PDF_HEADER_STRING', "by Nicola Asuni - Tecnick.com\nwww.tcpdf.org");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php` at line 157, The PDF header
constant PDF_HEADER_STRING contains a typo "by2026" that will appear in
generated PDFs; update the constant definition for PDF_HEADER_STRING (the define
call) to use "by" instead of "by2026" so the string reads "by Nicola Asuni -
Tecnick.com\nwww.tcpdf.org". Ensure you update the define('PDF_HEADER_STRING',
...) entry where it's declared.
app/Support/Updater.php (1)

587-601: ⚠️ Potential issue | 🟠 Major

Auth retry in makeGitHubRequest() is too restrictive compared to other updater paths.

At Line 590, retry is allowed only for specific message text. This can still block update checks when auth fails with a different 401/403 payload, while getAllReleases() and downloadUpdate() already retry by status alone.

💡 Suggested fix
-            if ($allowAuthRetry && $this->githubToken !== '' && in_array($statusCode, [401, 403], true)) {
-                $errorData = json_decode($response, true);
-                $message = strtolower((string) ($errorData['message'] ?? ''));
-                if (str_contains($message, 'bad credentials') || str_contains($message, 'requires authentication')) {
-                    $this->debugLog('WARNING', 'Token GitHub non valido, retry senza token', [
-                        'status_code' => $statusCode,
-                    ]);
-                    $savedToken = $this->githubToken;
-                    $this->githubToken = '';
-                    try {
-                        return $this->makeGitHubRequest($url, false);
-                    } finally {
-                        $this->githubToken = $savedToken;
-                    }
-                }
-            }
+            if ($allowAuthRetry && $this->githubToken !== '' && in_array($statusCode, [401, 403], true)) {
+                $this->debugLog('WARNING', 'Auth GitHub fallita, retry senza token', [
+                    'status_code' => $statusCode,
+                ]);
+                $savedToken = $this->githubToken;
+                $this->githubToken = '';
+                try {
+                    return $this->makeGitHubRequest($url, false);
+                } finally {
+                    $this->githubToken = $savedToken;
+                }
+            }
#!/bin/bash
set -euo pipefail

echo "=== makeGitHubRequest retry gate ==="
rg -n -C3 "in_array\\(\\$statusCode, \\[401, 403\\], true\\)|bad credentials|requires authentication" app/Support/Updater.php

echo
echo "=== Other updater retry branches (status-only) ==="
rg -n -C3 "Releases auth fallito, retry senza token|Download auth fallito, retry senza token|in_array\\(\\$httpCode, \\[401, 403\\], true\\)" app/Support/Updater.php
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 587 - 601, The auth-retry in
makeGitHubRequest is too restrictive because it only retries when the response
message contains specific text; change it to mirror
getAllReleases()/downloadUpdate by retrying on status codes alone: when
$allowAuthRetry is true, $this->githubToken is non-empty and $statusCode is in
[401, 403], skip the json message checks and perform the same saved-token swap
and recursive call to $this->makeGitHubRequest($url, false) (preserving the
finally block that restores $this->githubToken) so any 401/403 triggers a retry
without token.
public/assets/main.css (1)

4662-4689: ⚠️ Potential issue | 🟡 Minor

Stylelint violations still present in generated utility blocks.

declaration-empty-line-before errors are still triggered across these newly added utility declarations, so this CSS needs to be regenerated or auto-fixed before merge.

Also applies to: 4853-4973, 5460-5626, 6398-6605, 6798-7071

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public/assets/main.css` around lines 4662 - 4689, The generated CSS contains
Stylelint violations for declaration-empty-line-before across utility rules
(e.g., .border-amber-300, .border-blue-100, .border-blue-200, .border-blue-300,
.border-blue-600, .border-blue-700, .border-cyan-200); regenerate or re-run your
build step that produces public/assets/main.css (or run stylelint --fix) to
insert the required empty lines before declarations and auto-fix similar blocks
(also address the same violations in the other ranges noted: 4853-4973,
5460-5626, 6398-6605, 6798-7071) so the produced file passes
declaration-empty-line-before rules before merging.
vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php (1)

1387-1387: ⚠️ Potential issue | 🔴 Critical

checkSumAdjustment is written to the wrong head offset.

At Line 1387, the write uses +4..+8 instead of +8..+12, which overwrites fontRevision instead of checkSumAdjustment.

🐛 Proposed fix
-$font = substr($font, 0, $table['head']['offset'] + $offset + 4).pack('N', $checkSumAdjustment).substr($font, $table['head']['offset'] + $offset + 8);
+$font = substr($font, 0, $table['head']['offset'] + $offset + 8).pack('N', $checkSumAdjustment).substr($font, $table['head']['offset'] + $offset + 12);
#!/bin/bash
# Verify current write offsets used for checkSumAdjustment in subsetted TTF output.
sed -n '1384,1388p' vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php
rg -n "checkSumAdjustment|\\$table\\['head'\\]\\['offset'\\] \\+ \\$offset \\+ (4|8|12)" vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php` at line 1387, The current
write to the TTF head table uses the wrong byte window and overwrites
fontRevision; locate the assignment that builds $font (the line using
$table['head']['offset'], $offset and $checkSumAdjustment) and change the substr
boundaries so checkSumAdjustment is written at head offset + $offset + 8 .. +12
(i.e. slice up to offset+8, insert pack('N',$checkSumAdjustment), then append
from offset+12) instead of the current +4..+8 to ensure checkSumAdjustment is
written to the correct head field.
vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php (1)

1381-1387: ⚠️ Potential issue | 🟠 Major

Parameter parser can misclassify valid closures with default-call expressions.

At Line 1385, any ) in parameter parsing flips state to body. Defaults like fn($a = foo(), $b) can be parsed incorrectly, leading to false signature mismatch at Line 1404.

#!/bin/bash
set -euo pipefail

FILE="vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php"

echo "=== verifyCandidateSignature excerpt ==="
sed -n '1363,1433p' "$FILE"

echo
echo "=== Check for depth tracking inside verifyCandidateSignature ==="
# Expectation: no depth variable currently present in method body.
awk 'NR>=1363 && NR<=1433 {print}' "$FILE" | rg -n '\$depth|token === '\''\)'\'' && \$state === '\''params'\'''

echo
echo "If ')' transition exists without depth accounting, nested defaults can be misparsed."
♻️ Proposed fix
 protected function verifyCandidateSignature($candidate)
 {
     $code = $candidate['code'];
     $use = $candidate['use'];
     $isShortClosure = $candidate['isShortClosure'];

@@
     $tokens = token_get_all('<?php '.$code);
     $params = [];
     $vars = [];
     $state = 'start';
+    $depth = 0;

     foreach ($tokens as $token) {
         if (! is_array($token)) {
-            if ($token === '(' && $state === 'start') {
-                $state = 'params';
-            } elseif ($token === ')' && $state === 'params') {
-                $state = 'body';
+            if ($token === '(') {
+                if ($state === 'start') {
+                    $state = 'params';
+                    $depth = 1;
+                } elseif ($state === 'params') {
+                    $depth++;
+                }
+            } elseif ($token === ')' && $state === 'params') {
+                $depth--;
+                if ($depth === 0) {
+                    $state = 'body';
+                }
             }

             continue;
         }

Also applies to: 1404-1406

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php` around
lines 1381 - 1387, The parameter parser in verifyCandidateSignature
misclassifies closures when default parameter expressions contain parentheses
because the tokens loop flips $state from 'params' to 'body' on any ')'
regardless of nesting; update the tokens iteration in verifyCandidateSignature
to track parenthesis nesting (e.g., introduce a $depth counter incrementing on
'(' and decrementing on ')') and only transition from 'params' to 'body' when
encountering a ')' while $depth is zero; adjust the checks that set $state ('('
-> 'params' and ')' -> 'body') to use the $depth guard so default-call
expressions like fn($a = foo(), $b) are parsed correctly.
🧹 Nitpick comments (9)
vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php (1)

1-130: Reconsider committing vendor files to version control.

This is a third-party dependency file from the monolog/monolog package. Standard PHP practice excludes the vendor/ directory from version control (via .gitignore), managing dependencies solely through composer.json and composer.lock. Committing vendor files leads to repository bloat, large diffs, and potential merge conflicts.

If your project intentionally commits vendor files (some legacy or deployment-specific setups do), then these changes appear to be from a legitimate Monolog package update and are fine—they simply improve type documentation with non-empty-string annotations without altering behavior.

</review_comment_end>

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php` around
lines 1 - 130, The PR accidentally includes a third-party vendor file (the
Monolog class SlackWebhookHandler / Monolog\Handler\SlackWebhookHandler) which
should not be committed; remove this file from version control (or revert the
change) and ensure vendor/ is ignored by adding it to .gitignore, then restore
dependencies via Composer (composer install) so the project uses the package
from composer.lock; if your repo intentionally vendors dependencies, instead
keep only the upstream package bump in composer.json/composer.lock and avoid raw
edits to vendor files.
vendor/tecnickcom/tcpdf/fonts/freefont-20120503/README (1)

33-33: Vendor file modified directly.

These HTTPS URL updates are appropriate improvements, but modifying vendor files directly is discouraged. Changes will be overwritten on the next composer update. Consider contributing these fixes upstream to the TCPDF repository instead.

Also applies to: 111-111

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/fonts/freefont-20120503/README` at line 33, Revert
direct edits to the vendor file README in freefont-20120503 (the modified
vendor/tecnickcom/tcpdf/fonts/freefont-20120503/README) and instead either (a)
submit the HTTPS URL correction as a PR to the upstream TCPDF repository, or (b)
if you need the change applied in this project immediately, create a persistent
patch via composer-patch or maintain a small fork of tecnickcom/tcpdf and update
composer.json to require that fork; ensure the vendor README remains untouched
in the committed tree and document the upstream PR or patch entry in the project
changelog.
vendor/tecnickcom/tcpdf/include/tcpdf_filters.php (1)

8-10: Vendor files should be managed via Composer, not committed directly.

These TCPDF header updates (HTTPS license URL, copyright year 2026) appear to be from upstream version 6.11.0. Committing vendor files directly into the repository can lead to merge conflicts, makes updates error-prone, and bloats the repository.

Consider:

  • Adding vendor/ to .gitignore
  • Using composer install / composer update to manage dependencies
  • Running composer update tecnickcom/tcpdf to get upstream updates cleanly
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_filters.php` around lines 8 - 10, This
change modifies upstream vendor files (tecnickcom/tcpdf) directly; instead
revert these committed vendor edits, remove the vendor snapshot from the repo
(undo or delete the tracked vendor/ files), add vendor/ to .gitignore, and
manage the dependency via Composer by ensuring tecnickcom/tcpdf is declared in
composer.json and running composer install/update (e.g., composer update
tecnickcom/tcpdf or composer require tecnickcom/tcpdf) so the correct upstream
6.11.0 files are pulled and the repo no longer contains committed vendor code.
vendor/tecnickcom/tcpdf/include/tcpdf_static.php (1)

495-495: Fix author docblock formatting typo.

Line 495 reads Klemen Vodopivec,2026 Nicola Asuni, which looks accidental and hurts readability.

✏️ Proposed docblock fix
-	 * `@author` Klemen Vodopivec,2026 Nicola Asuni
+	 * `@author` Klemen Vodopivec, Nicola Asuni (2026)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_static.php` at line 495, The author
docblock on the tcpdf_static.php header contains a typo "Klemen Vodopivec,2026
Nicola Asuni"; update the `@author` line in the file's docblock to correctly
separate authors and the year (e.g., "Klemen Vodopivec, 2004; Nicola Asuni,
2026" or "Klemen Vodopivec and Nicola Asuni, 2026") so the authors and year are
properly formatted and readable; locate the `@author` tag in tcpdf_static.php and
replace the malformed string accordingly.
installer/database/data_de_DE.sql (1)

7-187: Prefer explicit column lists for seed inserts instead of positional VALUES.

Many statements rely on table column order (INSERT ... VALUES (...)). That makes this seed brittle if schema changes (added/reordered columns) and can silently break installs/upgrades.

Also applies to: 290-347

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/database/data_de_DE.sql` around lines 7 - 187, The INSERTs into
table `generi` use positional VALUES which is brittle; update every "INSERT
IGNORE INTO `generi` VALUES (...)" (including the batch near lines 290-347) to
use an explicit column list matching the table schema (for example: (id, name,
description, created_at, updated_at, parent_id)) and then supply the VALUES in
the same order, ensuring NULLs map to the correct columns; apply this change
consistently across all INSERT IGNORE INTO `generi` statements in the file.
app/Views/layout.php (1)

1436-1443: Add dialog semantics + focus lifecycle for the shortcuts modal.

At Line 1606 and nearby markup, the modal should expose dialog semantics (role="dialog", aria-modal, aria-labelledby).
At Line 1436-1443, opening/closing should also manage focus (focus close button on open, restore prior focus on close) for full keyboard/screen-reader usability.

Suggested patch
- function openShortcutsModal() {
-   var modal = document.getElementById('shortcuts-modal');
-   if (modal) modal.classList.remove('hidden');
- }
- function closeShortcutsModal() {
-   var modal = document.getElementById('shortcuts-modal');
-   if (modal) modal.classList.add('hidden');
- }
+ var lastFocusedElement = null;
+ function openShortcutsModal() {
+   var modal = document.getElementById('shortcuts-modal');
+   if (!modal) return;
+   lastFocusedElement = document.activeElement;
+   modal.classList.remove('hidden');
+   var close = document.getElementById('close-shortcuts');
+   if (close) close.focus();
+ }
+ function closeShortcutsModal() {
+   var modal = document.getElementById('shortcuts-modal');
+   if (!modal) return;
+   modal.classList.add('hidden');
+   if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
+     lastFocusedElement.focus();
+   }
+ }

-<div id="shortcuts-modal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
+<div id="shortcuts-modal" role="dialog" aria-modal="true" aria-labelledby="shortcuts-title" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
...
-      <h3 class="font-semibold text-gray-900 flex items-center gap-2">
+      <h3 id="shortcuts-title" class="font-semibold text-gray-900 flex items-center gap-2">

Also applies to: 1606-1616

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/layout.php` around lines 1436 - 1443, Update the modal markup with
dialog semantics by adding role="dialog", aria-modal="true", and aria-labelledby
referencing the modal title element (add an id to the title) for the element
with id 'shortcuts-modal'; in the JavaScript functions openShortcutsModal and
closeShortcutsModal, implement focus lifecycle: when opening, save
document.activeElement to a module-scoped variable (e.g.,
previousActiveElement), remove the 'hidden' class on 'shortcuts-modal', then
move focus to the modal's close control (ensure you have a consistent identifier
like id="shortcuts-close" or data attribute); when closing, add the 'hidden'
class back and restore focus to previousActiveElement (null-check before calling
focus), and ensure the saved variable is cleared; also wire Escape key handling
within the modal to call closeShortcutsModal for keyboard users.
vendor/tecnickcom/tcpdf/tcpdf.php (3)

4440-4440: Minor: unnecessary empty string concatenation.

The trailing '' in the error message serves no purpose.

♻️ Proposed fix
-			$this->Error('Unknown font type: '.$type.'');
+			$this->Error('Unknown font type: '.$type);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php` at line 4440, The error call unnecessarily
concatenates an empty string; update the $this->Error invocation to remove the
trailing ''. Replace "$this->Error('Unknown font type: '.$type.'');" with
"$this->Error('Unknown font type: '.$type);" in the same method so the message
remains identical but without the redundant concatenation.

23272-23276: Redundant function_exists() check.

The function_exists('xml_parser_free') guard is unnecessary since xml_parser_free() exists in all PHP versions. The PHP_VERSION_ID < 80000 check alone is sufficient.

♻️ Proposed simplification
-		if (function_exists('xml_parser_free') && PHP_VERSION_ID < 80000) {
+		if (PHP_VERSION_ID < 80000) {
 		    xml_parser_free($parser);
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php` around lines 23272 - 23276, The code
redundantly checks function_exists('xml_parser_free') before calling
xml_parser_free($parser); remove the unnecessary function_exists() guard and
retain only the PHP version check (PHP_VERSION_ID < 80000) around the
xml_parser_free($parser) call so that xml_parser_free is invoked when running on
PHP versions prior to 8.0; update the block that references xml_parser_free and
PHP_VERSION_ID accordingly.

1-13: Modifying vendor files directly is not recommended.

This file is managed by Composer and any local changes will be lost when running composer update. Consider one of these alternatives:

  1. Fork the library and reference your fork in composer.json
  2. Submit upstream PRs for bug fixes to the TCPDF repository
  3. Use Composer patches (e.g., cweagans/composer-patches) to apply fixes declaratively
  4. Override via inheritance where possible

If these modifications are intentional workarounds, document them and add a composer.json script or CI check to prevent accidental overwrites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php` around lines 1 - 13, The PR modifies a
vendor-managed file (tcpdf.php) which will be overwritten by composer updates;
instead of editing tcpdf.php directly, revert the vendor change and adopt one of
the supported approaches: fork the TCPDF repo and point composer.json to your
fork, submit an upstream PR to TCPDF, apply the change via a composer patch
(e.g., cweagans/composer-patches) referenced in composer.json, or implement the
behavior via subclassing/extension rather than editing tcpdf.php; if this edit
is a deliberate temporary workaround, document it in the repo and add a
composer.json script or CI check to detect/restore vendor modifications to avoid
accidental overwrites.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Controllers/LibriController.php`:
- Around line 2534-2555: The code currently treats an invalid or empty `ids`
input as "no filter" and exports all books; change to fail-closed: detect when
`ids` was provided (check original $params['ids'] presence) but parsing yields
an empty $selectedIds, and in that case ensure the export query produces no
results (e.g. set a flag or inject a guaranteed-false condition) or return a 400
error; update the parsing block around $idsParam/$selectedIds (and the later
IN(...) filter logic that uses $selectedIds) so that when $params contains 'ids'
but $selectedIds === [] you either add a no-op false clause to the query (so
IN(...) yields zero rows) or short-circuit and return an error response.

In `@app/Views/partials/scroll-to-top.php`:
- Around line 10-31: The scroll-to-top button (btn) is visually hidden via
opacity/pointer-events but can still receive keyboard focus; in the
window.scroll event handler (and when creating btn) toggle btn.tabIndex and
btn.setAttribute('aria-hidden') alongside opacity: set tabIndex = -1 and
aria-hidden = "true" when hidden, and tabIndex = 0 (or remove attribute) and
aria-hidden = "false" when visible; update the same logic that flips the visible
variable in the scroll listener so keyboard users cannot tab to the invisible
control and screen readers get correct state.

In `@scripts/manual-upgrade.php`:
- Around line 569-572: The manual-upgrade path uses a narrower $ignorableErrors
array which can abort harmless re-run migrations; update the $ignorableErrors
array in scripts/manual-upgrade.php (and its surrounding comment) to match the
same ignorable MySQL errno list used by the updater path so the same “already
exists / missing object” cases are treated idempotently (i.e., copy the exact
errno values from the updater implementation into $ignorableErrors), and keep
the existing $lastError handling unchanged.

In `@tests/full-test.spec.js`:
- Around line 598-600: finalTitle is interpolated into the SQL LIKE clause in
the dbQuery call (producing bookId), which allows quotes and LIKE wildcards
(%)/_ to break the query or cause false matches; fix by escaping user/scraped
input for LIKE (escape % and _ and backslash or quotes) or, better, convert the
dbQuery to a parameterized query that binds an escaped finalTitle with
surrounding % (e.g. bind value = `%${escapedFinalTitle}%`), and update both
occurrences that use finalTitle in dbQuery (the bookId lookup and the similar
call at lines ~709-710) to use the escape-and-bind approach or a helper like
escapeLike(finalTitle) before building the parameter.
- Around line 2173-2175: The assertion treats the SELECTed value stored in pages
incorrectly because dbQuery(`SELECT IFNULL(numero_pagine, 'NULL') ...`) can
yield the string 'NULL' which Number('NULL') => NaN; update the test to handle
'NULL' explicitly: fetch pages via dbQuery into pages, then if pages === 'NULL'
treat it as a valid null/zero case (or coerce to 0) before running the numeric
assertion; modify the expect to assert on the normalized numeric value (e.g.,
convert 'NULL' to 0 or check for pages === 'NULL' || Number(pages) <= 0) so both
0 and 'NULL' pass.
- Around line 2254-2255: The test currently asserts
expect(resp.status()).toBeGreaterThanOrEqual(400) which rejects valid redirects;
update the assertion to accept redirects and error statuses by changing the
check on resp.status() to toBeGreaterThanOrEqual(300) (or otherwise assert
resp.status() !== 200) so 3xx and 4xx/5xx responses pass; locate the assertion
using resp.status() in full-test.spec.js to apply the change.
- Around line 2206-2223: The test "19.2 XSS in book title is escaped on display"
currently uses window.__xss_triggered (never set) which gives a false-negative;
replace that check with Playwright's dialog interception by adding a dialog
listener or using page.waitForEvent('dialog') set up before navigating to
`${BASE}/libro/${bookId}`, fail the test if any dialog is received, and remove
the window.__xss_triggered evaluation; keep the existing page.goto,
page.waitForLoadState, and bodyHtml assertion but ensure the dialog listener is
registered prior to navigation so any alert() invocation is reliably detected.
- Around line 50-53: The dbQuery function currently always passes `-p${DB_PASS}`
(causing an interactive prompt when DB_PASS is empty) and never uses DB_HOST;
update dbQuery to build the mysql args array conditionally: only push
`-p${DB_PASS}` when DB_PASS is non-empty, always include `-h`, DB_HOST (so the
configured host is used), and still insert `-S`, DB_SOCKET when DB_SOCKET is set
(ensure socket ordering matches existing splice logic), then call
execFileSync('mysql', args, ...) as before; reference symbols: function dbQuery,
variables DB_PASS, DB_HOST, DB_SOCKET, and the execFileSync call.

In `@vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php`:
- Around line 137-142: The inode-change logic in StreamHandler is conflating two
concerns by setting $retrying when hasUrlInodeWasChanged() detects rotation;
instead, change the inode-rotation branch inside StreamHandler::write() so it
clears the inode tracking state (set $this->inodeUrl = null) and calls
$this->close(); $this->write($record); return; without toggling $this->retrying,
and remove any assignment of $this->retrying from hasUrlInodeWasChanged() so
that $this->retrying remains exclusively for write-failure retry logic (the
later check around !$this->retrying should therefore behave only for actual
write errors).

In `@vendor/tecnickcom/tcpdf/include/tcpdf_colors.php`:
- Line 10: Update the malformed copyright year text in the header string
"Copyright (C) 2002-2013 2026 Nicola Asuni - Tecnick.com LTD" to a single
continuous range by replacing "2002-2013 2026" with "2002-2026" so the header
reads "Copyright (C) 2002-2026 Nicola Asuni - Tecnick.com LTD".

---

Duplicate comments:
In `@app/Support/Updater.php`:
- Around line 587-601: The auth-retry in makeGitHubRequest is too restrictive
because it only retries when the response message contains specific text; change
it to mirror getAllReleases()/downloadUpdate by retrying on status codes alone:
when $allowAuthRetry is true, $this->githubToken is non-empty and $statusCode is
in [401, 403], skip the json message checks and perform the same saved-token
swap and recursive call to $this->makeGitHubRequest($url, false) (preserving the
finally block that restores $this->githubToken) so any 401/403 triggers a retry
without token.

In `@public/assets/main.css`:
- Around line 4662-4689: The generated CSS contains Stylelint violations for
declaration-empty-line-before across utility rules (e.g., .border-amber-300,
.border-blue-100, .border-blue-200, .border-blue-300, .border-blue-600,
.border-blue-700, .border-cyan-200); regenerate or re-run your build step that
produces public/assets/main.css (or run stylelint --fix) to insert the required
empty lines before declarations and auto-fix similar blocks (also address the
same violations in the other ranges noted: 4853-4973, 5460-5626, 6398-6605,
6798-7071) so the produced file passes declaration-empty-line-before rules
before merging.

In `@vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php`:
- Around line 1381-1387: The parameter parser in verifyCandidateSignature
misclassifies closures when default parameter expressions contain parentheses
because the tokens loop flips $state from 'params' to 'body' on any ')'
regardless of nesting; update the tokens iteration in verifyCandidateSignature
to track parenthesis nesting (e.g., introduce a $depth counter incrementing on
'(' and decrementing on ')') and only transition from 'params' to 'body' when
encountering a ')' while $depth is zero; adjust the checks that set $state ('('
-> 'params' and ')' -> 'body') to use the $depth guard so default-call
expressions like fn($a = foo(), $b) are parsed correctly.

In `@vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php`:
- Around line 48-54: The constructor computes $this->nextRotation by calling
getNextRotation() before assigning $this->timezone, causing rotation cutoff to
ignore the configured timezone; fix it by assigning $this->timezone (the
DateTimeZone|null parameter) before calling $this->getNextRotation() in
__construct() (or alternatively change getNextRotation() to accept a timezone
argument), ensuring getNextRotation() uses the configured timezone when
calculating rotation boundaries and keeping filename dating in
setFilenameFormat()/getDatedFilename() consistent.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php`:
- Line 1387: The current write to the TTF head table uses the wrong byte window
and overwrites fontRevision; locate the assignment that builds $font (the line
using $table['head']['offset'], $offset and $checkSumAdjustment) and change the
substr boundaries so checkSumAdjustment is written at head offset + $offset + 8
.. +12 (i.e. slice up to offset+8, insert pack('N',$checkSumAdjustment), then
append from offset+12) instead of the current +4..+8 to ensure
checkSumAdjustment is written to the correct head field.

In `@vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php`:
- Line 157: The PDF header constant PDF_HEADER_STRING contains a typo "by2026"
that will appear in generated PDFs; update the constant definition for
PDF_HEADER_STRING (the define call) to use "by" instead of "by2026" so the
string reads "by Nicola Asuni - Tecnick.com\nwww.tcpdf.org". Ensure you update
the define('PDF_HEADER_STRING', ...) entry where it's declared.

---

Nitpick comments:
In `@app/Views/layout.php`:
- Around line 1436-1443: Update the modal markup with dialog semantics by adding
role="dialog", aria-modal="true", and aria-labelledby referencing the modal
title element (add an id to the title) for the element with id
'shortcuts-modal'; in the JavaScript functions openShortcutsModal and
closeShortcutsModal, implement focus lifecycle: when opening, save
document.activeElement to a module-scoped variable (e.g.,
previousActiveElement), remove the 'hidden' class on 'shortcuts-modal', then
move focus to the modal's close control (ensure you have a consistent identifier
like id="shortcuts-close" or data attribute); when closing, add the 'hidden'
class back and restore focus to previousActiveElement (null-check before calling
focus), and ensure the saved variable is cleared; also wire Escape key handling
within the modal to call closeShortcutsModal for keyboard users.

In `@installer/database/data_de_DE.sql`:
- Around line 7-187: The INSERTs into table `generi` use positional VALUES which
is brittle; update every "INSERT IGNORE INTO `generi` VALUES (...)" (including
the batch near lines 290-347) to use an explicit column list matching the table
schema (for example: (id, name, description, created_at, updated_at, parent_id))
and then supply the VALUES in the same order, ensuring NULLs map to the correct
columns; apply this change consistently across all INSERT IGNORE INTO `generi`
statements in the file.

In `@vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php`:
- Around line 1-130: The PR accidentally includes a third-party vendor file (the
Monolog class SlackWebhookHandler / Monolog\Handler\SlackWebhookHandler) which
should not be committed; remove this file from version control (or revert the
change) and ensure vendor/ is ignored by adding it to .gitignore, then restore
dependencies via Composer (composer install) so the project uses the package
from composer.lock; if your repo intentionally vendors dependencies, instead
keep only the upstream package bump in composer.json/composer.lock and avoid raw
edits to vendor files.

In `@vendor/tecnickcom/tcpdf/fonts/freefont-20120503/README`:
- Line 33: Revert direct edits to the vendor file README in freefont-20120503
(the modified vendor/tecnickcom/tcpdf/fonts/freefont-20120503/README) and
instead either (a) submit the HTTPS URL correction as a PR to the upstream TCPDF
repository, or (b) if you need the change applied in this project immediately,
create a persistent patch via composer-patch or maintain a small fork of
tecnickcom/tcpdf and update composer.json to require that fork; ensure the
vendor README remains untouched in the committed tree and document the upstream
PR or patch entry in the project changelog.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_filters.php`:
- Around line 8-10: This change modifies upstream vendor files
(tecnickcom/tcpdf) directly; instead revert these committed vendor edits, remove
the vendor snapshot from the repo (undo or delete the tracked vendor/ files),
add vendor/ to .gitignore, and manage the dependency via Composer by ensuring
tecnickcom/tcpdf is declared in composer.json and running composer
install/update (e.g., composer update tecnickcom/tcpdf or composer require
tecnickcom/tcpdf) so the correct upstream 6.11.0 files are pulled and the repo
no longer contains committed vendor code.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_static.php`:
- Line 495: The author docblock on the tcpdf_static.php header contains a typo
"Klemen Vodopivec,2026 Nicola Asuni"; update the `@author` line in the file's
docblock to correctly separate authors and the year (e.g., "Klemen Vodopivec,
2004; Nicola Asuni, 2026" or "Klemen Vodopivec and Nicola Asuni, 2026") so the
authors and year are properly formatted and readable; locate the `@author` tag in
tcpdf_static.php and replace the malformed string accordingly.

In `@vendor/tecnickcom/tcpdf/tcpdf.php`:
- Line 4440: The error call unnecessarily concatenates an empty string; update
the $this->Error invocation to remove the trailing ''. Replace
"$this->Error('Unknown font type: '.$type.'');" with "$this->Error('Unknown font
type: '.$type);" in the same method so the message remains identical but without
the redundant concatenation.
- Around line 23272-23276: The code redundantly checks
function_exists('xml_parser_free') before calling xml_parser_free($parser);
remove the unnecessary function_exists() guard and retain only the PHP version
check (PHP_VERSION_ID < 80000) around the xml_parser_free($parser) call so that
xml_parser_free is invoked when running on PHP versions prior to 8.0; update the
block that references xml_parser_free and PHP_VERSION_ID accordingly.
- Around line 1-13: The PR modifies a vendor-managed file (tcpdf.php) which will
be overwritten by composer updates; instead of editing tcpdf.php directly,
revert the vendor change and adopt one of the supported approaches: fork the
TCPDF repo and point composer.json to your fork, submit an upstream PR to TCPDF,
apply the change via a composer patch (e.g., cweagans/composer-patches)
referenced in composer.json, or implement the behavior via subclassing/extension
rather than editing tcpdf.php; if this edit is a deliberate temporary
workaround, document it in the repo and add a composer.json script or CI check
to detect/restore vendor modifications to avoid accidental overwrites.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and d06c11d.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (110)
  • .htaccess
  • README.md
  • app/Controllers/FrontendController.php
  • app/Controllers/LibriController.php
  • app/Controllers/UpdateController.php
  • app/Models/BookRepository.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • app/Views/autori/index.php
  • app/Views/editori/index.php
  • app/Views/events/index.php
  • app/Views/frontend/event-detail.php
  • app/Views/frontend/events.php
  • app/Views/frontend/home-sections/events.php
  • app/Views/frontend/home-sections/genre_carousel.php
  • app/Views/frontend/home.php
  • app/Views/frontend/layout.php
  • app/Views/layout.php
  • app/Views/libri/index.php
  • app/Views/libri/partials/book_form.php
  • app/Views/libri/scheda_libro.php
  • app/Views/partials/scroll-to-top.php
  • app/Views/prestiti/index.php
  • app/Views/utenti/index.php
  • frontend/js/vendor.js
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/database/data_en_US.sql
  • installer/database/data_it_IT.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • public/assets/main.css
  • public/assets/vendor.bundle.js
  • scripts/.user.ini
  • scripts/create-release.sh
  • scripts/manual-update.php
  • scripts/manual-upgrade.php
  • tests/full-test.spec.js
  • tests/genre-bugs.spec.js
  • vendor/composer/autoload_classmap.php
  • vendor/composer/autoload_files.php
  • vendor/composer/autoload_static.php
  • vendor/composer/installed.json
  • vendor/composer/installed.php
  • vendor/graham-campbell/result-type/composer.json
  • vendor/laravel/serializable-closure/composer.json
  • vendor/laravel/serializable-closure/src/Serializers/Native.php
  • vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php
  • vendor/monolog/monolog/README.md
  • vendor/monolog/monolog/composer.json
  • vendor/monolog/monolog/src/Monolog/Formatter/GelfMessageFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php
  • vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php
  • vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/Handler.php
  • vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php
  • vendor/slim/csrf/composer.json
  • vendor/slim/psr7/composer.json
  • vendor/slim/psr7/src/Cookies.php
  • vendor/slim/slim/MAINTAINERS.md
  • vendor/slim/slim/Slim/App.php
  • vendor/slim/slim/composer.json
  • vendor/slim/slim/psalm.xml
  • vendor/tecnickcom/tcpdf/CHANGELOG.TXT
  • vendor/tecnickcom/tcpdf/LICENSE.TXT
  • vendor/tecnickcom/tcpdf/README.md
  • vendor/tecnickcom/tcpdf/VERSION
  • vendor/tecnickcom/tcpdf/composer.json
  • vendor/tecnickcom/tcpdf/config/tcpdf_config.php
  • vendor/tecnickcom/tcpdf/fonts/ae_fonts_2.0/README
  • vendor/tecnickcom/tcpdf/fonts/freefont-20100919/COPYING
  • vendor/tecnickcom/tcpdf/fonts/freefont-20100919/CREDITS
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/COPYING
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/CREDITS
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/README
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/TROUBLESHOOTING
  • vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php
  • vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php
  • vendor/tecnickcom/tcpdf/include/barcodes/qrcode.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_colors.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_filters.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_font_data.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_images.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_static.php
  • vendor/tecnickcom/tcpdf/tcpdf.php
  • vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php
  • vendor/tecnickcom/tcpdf/tcpdf_barcodes_1d.php
  • vendor/tecnickcom/tcpdf/tcpdf_barcodes_2d.php
  • vendor/tecnickcom/tcpdf/tools/tcpdf_addfont.php
  • vendor/vlucas/phpdotenv/composer.json
  • vendor/vlucas/phpdotenv/src/Parser/EntryParser.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php
💤 Files with no reviewable changes (7)
  • vendor/vlucas/phpdotenv/src/Parser/EntryParser.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php
  • vendor/slim/slim/psalm.xml
  • vendor/slim/slim/MAINTAINERS.md
  • scripts/manual-update.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php

Comment on lines +137 to +142
if ($this->hasUrlInodeWasChanged()) {
$this->close();
$this->write($record);

return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php"

echo "Inspecting retry-related control flow..."
rg -n -C4 '\$retrying|hasUrlInodeWasChanged|inodeUrl' "$FILE"

echo
echo "Focused excerpt: write() method"
sed -n '130,210p' "$FILE"

echo
echo "Focused excerpt: inode helpers"
sed -n '258,282p' "$FILE"

Repository: fabiodalez-dev/Pinakes

Length of output: 5054


Decouple inode-reopen state from write-retry state.

The $retrying flag is reused for two distinct purposes: inode-rotation detection and write-failure retry control. In hasUrlInodeWasChanged(), when an inode change is detected, $retrying is set to true. However, when the recursive write() call that follows (line 139) encounters a write error, the check at line 183 (if (!$this->retrying ...)) skips the write-failure retry because $retrying was already set by the inode handler. This suppresses the intended one-time retry after an inode-triggered reopen.

The fix decouples these concerns by:

  • Resetting $inodeUrl = null; in the inode-change handler (line 138), which naturally prevents re-entry into hasUrlInodeWasChanged() without needing $retrying
  • Removing $retrying from the inode detection logic (line 272)
  • Allowing $retrying to be used exclusively for write-failure retry control
Proposed fix
 protected function write(LogRecord $record): void
 {
     if ($this->hasUrlInodeWasChanged()) {
+        $this->inodeUrl = null;
         $this->close();
         $this->write($record);

         return;
     }
@@
 private function hasUrlInodeWasChanged(): bool
 {
-    if ($this->inodeUrl === null || $this->retrying || $this->inodeUrl === $this->getInodeFromUrl()) {
+    if ($this->inodeUrl === null || $this->inodeUrl === $this->getInodeFromUrl()) {
         return false;
     }
-
-    $this->retrying = true;

     return true;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php` around lines
137 - 142, The inode-change logic in StreamHandler is conflating two concerns by
setting $retrying when hasUrlInodeWasChanged() detects rotation; instead, change
the inode-rotation branch inside StreamHandler::write() so it clears the inode
tracking state (set $this->inodeUrl = null) and calls $this->close();
$this->write($record); return; without toggling $this->retrying, and remove any
assignment of $this->retrying from hasUrlInodeWasChanged() so that
$this->retrying remains exclusively for write-failure retry logic (the later
check around !$this->retrying should therefore behave only for actual write
errors).

// License : GNU-LGPL v3 (https://www.gnu.org/copyleft/lesser.html)
// -------------------------------------------------------------------
// Copyright (C) 2002-2013 Nicola Asuni - Tecnick.com LTD
// Copyright (C) 2002-2013 2026 Nicola Asuni - Tecnick.com LTD
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Malformed copyright year range.

The copyright line 2002-2013 2026 is malformed. It should be 2002-2026 (single continuous range).

Suggested fix
-// Copyright (C) 2002-2013 2026 Nicola Asuni - Tecnick.com LTD
+// Copyright (C) 2002-2026 Nicola Asuni - Tecnick.com LTD
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_colors.php` at line 10, Update the
malformed copyright year text in the header string "Copyright (C) 2002-2013 2026
Nicola Asuni - Tecnick.com LTD" to a single continuous range by replacing
"2002-2013 2026" with "2002-2026" so the header reads "Copyright (C) 2002-2026
Nicola Asuni - Tecnick.com LTD".

- Updater: simplify auth retry to status-code-only (match other paths)
- LibriController: fail-closed when ids param yields no valid IDs
- scroll-to-top: add aria-hidden/tabIndex toggling for a11y
- layout shortcuts modal: add role=dialog, aria-modal, focus lifecycle
- manual-upgrade: align ignorable MySQL errors with Updater (add 1022/1826/1146)
- tests: fix dbQuery to handle empty password and use DB_HOST
- tests: add escapeSqlLike helper for LIKE queries
- tests: fix numero_pagine NULL assertion (handle 'NULL' string)
- tests: use Playwright dialog interception for XSS test
- tests: accept redirects (3xx) for soft-deleted book assertion
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
installer/classes/Installer.php (1)

1023-1036: ⚠️ Potential issue | 🟡 Minor

IPv6 host parsing can still produce an invalid canonical URL.

At Line 1023, splitting $host with explode(':', $host, 2) breaks bracketed IPv6 literals (for example [2001:db8::1]:8080), resulting in malformed APP_CANONICAL_URL.

🔧 Suggested fix
-        if (str_contains($host, ':')) {
-            [$hostOnly, $portPart] = explode(':', $host, 2);
-            $host = $hostOnly;
-            $port = is_numeric($portPart) ? (int)$portPart : null;
+        if (preg_match('/^\[(.+)\](?::(\d+))?$/', $host, $matches)) {
+            // Bracketed IPv6 literal, optionally with port
+            $host = '[' . $matches[1] . ']';
+            $port = isset($matches[2]) ? (int) $matches[2] : null;
+        } elseif (substr_count($host, ':') === 1) {
+            // hostname:port or IPv4:port
+            [$hostOnly, $portPart] = explode(':', $host, 2);
+            $host = $hostOnly;
+            $port = is_numeric($portPart) ? (int) $portPart : null;
         } elseif (!empty($_SERVER['HTTP_X_FORWARDED_PORT'])) {
             $port = (int)$_SERVER['HTTP_X_FORWARDED_PORT'];
         } elseif (isset($_SERVER['SERVER_PORT']) && is_numeric((string)$_SERVER['SERVER_PORT'])) {
             $port = (int)$_SERVER['SERVER_PORT'];
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/classes/Installer.php` around lines 1023 - 1036, The current
parsing in Installer.php uses str_contains($host, ':') and explode(':', $host,
2) which breaks bracketed IPv6 literals like [2001:db8::1]:8080; update the
logic around $host/$port extraction to first detect bracketed IPv6 (host starts
with '[' and contains a ']') and if so extract the text inside brackets as
$hostOnly and anything after the closing ']' (after a colon) as $portPart,
otherwise split on the last colon (use strrpos semantics instead of explode) to
avoid breaking IPv6 addresses without brackets; then set $host and $port as
before and keep the later base and defaultPorts handling unchanged.
♻️ Duplicate comments (9)
vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php (1)

137-142: ⚠️ Potential issue | 🟠 Major

Decouple inode-rotation state from write-retry state.

At Line 272 and Line 276, $retrying is being used for inode-rotation control, but Line 187 uses the same flag to allow one write-failure retry. After an inode-triggered reopen, a subsequent write error can skip the intended retry path.

Suggested fix
 protected function write(LogRecord $record): void
 {
     if ($this->hasUrlInodeWasChanged()) {
+        $this->inodeUrl = null;
         $this->close();
         $this->write($record);

         return;
     }
@@
 private function hasUrlInodeWasChanged(): bool
 {
-    if ($this->inodeUrl === null || $this->retrying || $this->inodeUrl === $this->getInodeFromUrl()) {
+    if ($this->inodeUrl === null || $this->inodeUrl === $this->getInodeFromUrl()) {
         return false;
     }
-
-    $this->retrying = true;

     return true;
 }
#!/bin/bash
set -euo pipefail

FILE="vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php"

echo "Inspect retry/inode interactions:"
rg -n -C3 '\$retrying|hasUrlInodeWasChanged|inodeUrl' "$FILE"

echo
echo "write() focus:"
sed -n '135,205p' "$FILE"

echo
echo "inode helpers focus:"
sed -n '259,279p' "$FILE"

Also applies to: 272-277

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php` around lines
137 - 142, The inode-rotation logic in write() is reusing the $retrying flag so
an inode-triggered reopen can consume the one-time write-retry; introduce a
separate boolean (e.g. $inodeReopened or $reopenedForInode) and use it for the
hasUrlInodeWasChanged() / close() path instead of mutating or relying on
$retrying, or explicitly restore/reset $retrying after the inode reopen, so the
original write-failure retry behavior remains intact; update references in
write(), hasUrlInodeWasChanged() handling, and any code paths that check
$retrying to use the new flag where appropriate.
vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php (1)

53-54: ⚠️ Potential issue | 🟠 Major

Align rotation-boundary timezone with configured filename timezone.

Line 53 computes $this->nextRotation before Line 54 sets $this->timezone, and Line 226-232 still uses default-timezone DateTimeImmutable instances. This can rotate on a different boundary than the filename date (Line 183).

Proposed fix
-        $this->nextRotation = $this->getNextRotation();
-        $this->timezone = $timezone;
+        $this->timezone = $timezone;
+        $this->nextRotation = $this->getNextRotation();
     protected function getNextRotation(): \DateTimeImmutable
     {
+        $now = new \DateTimeImmutable('now', $this->timezone);
+
         return match (str_replace(['/','_','.'], '-', $this->dateFormat)) {
-            self::FILE_PER_MONTH => (new \DateTimeImmutable('first day of next month'))->setTime(0, 0, 0),
-            self::FILE_PER_YEAR => (new \DateTimeImmutable('first day of January next year'))->setTime(0, 0, 0),
-            default => (new \DateTimeImmutable('tomorrow'))->setTime(0, 0, 0),
+            self::FILE_PER_MONTH => $now->modify('first day of next month')->setTime(0, 0, 0),
+            self::FILE_PER_YEAR => $now->modify('first day of January next year')->setTime(0, 0, 0),
+            default => $now->modify('tomorrow')->setTime(0, 0, 0),
         };
     }
#!/bin/bash
set -euo pipefail

file="vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php"

echo "Constructor ordering (expect timezone assignment before getNextRotation):"
nl -ba "$file" | sed -n '48,58p'

echo
echo "Rotation boundary calculation (expect DateTimeImmutable initialized with configured timezone):"
nl -ba "$file" | sed -n '226,234p'

Also applies to: 127-127, 226-232

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php` around
lines 53 - 54, The constructor in RotatingFileHandler sets $this->nextRotation
before assigning $this->timezone which can cause rotation boundaries to use the
default timezone; move the assignment of $this->timezone so it occurs before
calling $this->getNextRotation() (i.e., ensure $this->timezone is set prior to
computing $this->nextRotation in the constructor). Additionally, update the
rotation-boundary calculations inside getNextRotation() (and any
DateTimeImmutable instantiations used for rotation boundary or filename
generation) to construct DateTimeImmutable objects with $this->timezone (e.g.,
new DateTimeImmutable('now', $this->timezone)) so the rotation boundary aligns
with the configured filename timezone.
vendor/tecnickcom/tcpdf/include/tcpdf_colors.php (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Malformed copyright year range remains unresolved.

Line 10 still uses 2002-2013 2026; this should be a single continuous range (e.g., 2002-2026).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_colors.php` at line 10, Update the
malformed copyright year range in the header string "Copyright (C) 2002-2013
2026 Nicola Asuni - Tecnick.com LTD" to a single continuous range (for example
"Copyright (C) 2002-2026 Nicola Asuni - Tecnick.com LTD") in tcpdf_colors.php so
the year sequence is correct and no duplicate years remain.
vendor/tecnickcom/tcpdf/include/tcpdf_images.php (1)

277-277: ⚠️ Potential issue | 🟡 Minor

Fix typo in PNG parser comments (UnknownnUnknown).

Line 277, Line 282, and Line 334 still contain Unknownn (double n) in comments.

✏️ Proposed fix
-			// Unknownn compression method
+			// Unknown compression method
...
-			// Unknownn filter method
+			// Unknown filter method
...
-					// Unknownn filter method
+					// Unknown filter method

Also applies to: 282-282, 334-334

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_images.php` at line 277, Fix the typo
"Unknownn" → "Unknown" in the PNG parser comments in tcpdf_images.php: locate
the PNG parsing section (the PNG parse function / PNG parser comment blocks) and
replace all occurrences of the misspelled "Unknownn" (e.g., in comment strings
like "Unknownn compression method") with "Unknown" so the comments read
correctly; ensure the three occurrences noted in the review are corrected.
app/Views/partials/scroll-to-top.php (1)

5-6: ⚠️ Potential issue | 🟠 Major

Escape translated text before embedding it in JavaScript.

Line 5 and Line 6 inject localized text directly into single-quoted JS strings. A translation containing quotes or </script> can break script context.

🛡️ Proposed fix
-  btn.setAttribute('aria-label', '<?= __('Torna su') ?>');
-  btn.setAttribute('title', '<?= __('Torna su') ?>');
+  var scrollToTopLabel = <?= json_encode(__('Torna su'), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>;
+  btn.setAttribute('aria-label', scrollToTopLabel);
+  btn.setAttribute('title', scrollToTopLabel);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/partials/scroll-to-top.php` around lines 5 - 6, The translated
string is injected directly into JS strings via btn.setAttribute('aria-label',
'<?= __('Torna su') ?>') and btn.setAttribute('title', '<?= __('Torna su') ?>'),
which can break the script if the translation contains quotes or script-close
sequences; fix by encoding/escaping the translation for JavaScript output (emit
the translation as a JSON-safe JS string using PHP's json_encode or an
equivalent JS-escaping helper) so the second argument to btn.setAttribute
receives a properly escaped string.
vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php (1)

1387-1387: ⚠️ Potential issue | 🔴 Critical

Fix checkSumAdjustment write offset in the head table.

Line 1387 currently writes at +4..+8; checkSumAdjustment must be written at +8..+12. The current slicing can corrupt subsetted TTF metadata.

🔧 Proposed fix
-$font = substr($font, 0, $table['head']['offset'] + $offset + 4).pack('N', $checkSumAdjustment).substr($font, $table['head']['offset'] + $offset + 8);
+$font = substr($font, 0, $table['head']['offset'] + $offset + 8).pack('N', $checkSumAdjustment).substr($font, $table['head']['offset'] + $offset + 12);
According to the TrueType/OpenType `head` table specification, what byte offset stores `checkSumAdjustment`?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php` at line 1387, The head
table's checkSumAdjustment is being written at bytes +4..+8 but per the
TrueType/OpenType spec it must be at +8..+12; update the write in the code that
manipulates $font (the expression using $table['head']['offset'], $offset and
$checkSumAdjustment) so it inserts pack('N', $checkSumAdjustment) starting at
$table['head']['offset'] + $offset + 8 and continues to +12 (i.e., use substr
boundaries +8 and +12 instead of +4 and +8) to avoid corrupting subsetted TTF
metadata.
tests/full-test.spec.js (1)

2269-2273: ⚠️ Potential issue | 🟡 Minor

Guard goto response before reading status.

If page.goto returns null, resp.status() throws and hides the actual assertion intent.

🛠️ Suggested patch
-    expect(resp.status()).not.toBe(200);
+    const status = resp?.status() ?? 0;
+    expect(status).not.toBe(200);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/full-test.spec.js` around lines 2269 - 2273, Guard the result of
page.goto before calling resp.status() to avoid errors when it returns null:
check the returned value from page.goto (resp) and treat a null response as a
non-200 result (or extract status via a safe check), then perform the assertion
using that guarded status; update the code around page.goto / resp.status() so
it uses a conditional check on resp (or a safe accessor) before calling
status().
app/Views/libri/partials/book_form.php (1)

1378-1417: ⚠️ Potential issue | 🟠 Major

Do not override Choices.js private _onEnterKey API.

This is still relying on an internal method and remains version-fragile (already flagged in prior review, still unresolved).

🔧 Suggested direction (replace private API patch)
-        if (typeof authorsChoice._onEnterKey === 'function') {
-            const originalOnEnterKey = authorsChoice._onEnterKey.bind(authorsChoice);
-            authorsChoice._onEnterKey = function(event, hasActiveDropdown) {
-                ...
-                return originalOnEnterKey(event, hasActiveDropdown);
-            };
-        } else {
-            console.warn('Choices.js _onEnterKey not available; author creation on Enter disabled.');
-        }
+        const handleAuthorEnter = (event) => {
+            if (event.key !== 'Enter' || !internalInput) return;
+            const inputValue = internalInput.value.trim();
+            if (!inputValue) return;
+
+            const dd = wrapper ? wrapper.querySelector('.choices__list--dropdown') : null;
+            const highlighted = dd ? dd.querySelector('.choices__item--selectable.is-highlighted') : null;
+
+            if (!highlighted) {
+                event.preventDefault();
+                event.stopPropagation();
+                createAuthorFromInputWithValue(inputValue);
+                return;
+            }
+
+            const highlightedText = (highlighted.textContent || '').trim().toLowerCase();
+            const currentText = inputValue.toLowerCase();
+            if (highlightedText !== currentText && !highlightedText.startsWith(currentText)) {
+                event.preventDefault();
+                event.stopPropagation();
+                createAuthorFromInputWithValue(inputValue);
+            }
+        };
+
+        document.addEventListener('keydown', handleAuthorEnter, true);

Use this script to verify where private Choices.js APIs are patched and what version is pinned:

#!/bin/bash
set -euo pipefail

echo "== Private Choices.js API usages =="
rg -n "_onEnterKey" app/Views/libri/partials/book_form.php

echo
echo "== choices.js version declarations =="
python - <<'PY'
import json, pathlib
for p in pathlib.Path('.').rglob('package.json'):
    try:
        data = json.loads(p.read_text())
    except Exception:
        continue
    deps = {}
    deps.update(data.get('dependencies', {}))
    deps.update(data.get('devDependencies', {}))
    if 'choices.js' in deps:
        print(f"{p}: {deps['choices.js']}")
PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/libri/partials/book_form.php` around lines 1378 - 1417, You are
overriding a private Choices.js method (authorsChoice._onEnterKey) which is
fragile; instead attach a stable public handler: listen to the actual input
element (internalInput) keydown/keypress or Choices public events (e.g.
'addItem' / 'choice' / 'search') and implement the create-on-Enter logic there
by detecting Enter, reading internalInput.value, checking the dropdown
highlighted/choices via public DOM queries (wrapper / .choices__list--dropdown /
.choices__item--selectable) and calling createAuthorFromInputWithValue when
appropriate; remove the override of authorsChoice._onEnterKey and ensure the new
handler prevents default and stops propagation only when creating a new author.
vendor/tecnickcom/tcpdf/tcpdf.php (1)

23733-23737: ⚠️ Potential issue | 🟠 Major

SVG path parameter splitting retains delimiters due to capturing group in preg_split.

Line 23734 uses a capturing group '/([\,\s]+)/si' which includes the matched delimiters (commas, spaces) as separate array elements in the result. The subsequent array_filter removes empty strings but leaves delimiters intact, corrupting SVG coordinate parsing.

Use a non-capturing group with PREG_SPLIT_NO_EMPTY instead:

Proposed fix
-				$rawparams = preg_split('/([\,\s]+)/si', trim($val[2]));
-				$rawparams = array_filter($rawparams, function($p) {
-					return trim($p) != '';
-				});
+				$rawparams = preg_split('/[\,\s]+/si', trim($val[2]), -1, PREG_SPLIT_NO_EMPTY);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php` around lines 23733 - 23737, The preg_split
call that builds $rawparams is using a capturing group '/([\,\s]+)/si' which
causes delimiters to appear in the result; update the preg_split invocation in
the code that populates $rawparams to use a non‑capturing group and the
PREG_SPLIT_NO_EMPTY flag (e.g. '/(?:[,\s]+)/si' with PREG_SPLIT_NO_EMPTY) so
delimiters are not returned and you can remove the subsequent array_filter step
that tries to drop empty strings.
🧹 Nitpick comments (2)
installer/database/data_de_DE.sql (1)

189-191: Deduplicate cms.events_page_enabled upsert to keep one source of truth.

Line 190 and Line 282 seed the same setting key; the later write silently overrides the first. Keeping a single insert improves maintainability.

♻️ Suggested cleanup
-INSERT INTO `system_settings` (`category`, `setting_key`, `setting_value`, `description`) VALUES
-('cms', 'events_page_enabled', '1', 'Veranstaltungsseite im Frontend aktivieren oder deaktivieren')
-ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), description = VALUES(description);

Also applies to: 281-286

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/database/data_de_DE.sql` around lines 189 - 191, The seed contains
duplicate upserts for the same setting key 'cms.events_page_enabled' which
causes the later INSERT to silently override the earlier one; remove the
redundant INSERT/ON DUPLICATE KEY UPDATE block so there is a single canonical
upsert for 'cms.events_page_enabled' (keep the preferred values/description and
delete the other occurrence), ensuring only one source of truth for that setting
in the SQL seed.
vendor/tecnickcom/tcpdf/tcpdf.php (1)

4-6: Consider contributing fixes upstream or maintaining patches separately.

This vendor file contains local modifications that may be overwritten when updating via Composer. If these changes fix real issues, consider:

  1. Contributing the fixes upstream to tecnickcom/TCPDF
  2. Using a patch file applied during build (e.g., cweagans/composer-patches)
  3. Forking the repository and requiring your fork in composer.json
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php` around lines 4 - 6, This vendor file
(tcpdf.php) has local modifications that will be lost on Composer updates; stop
editing vendor/tecnickcom/tcpdf/tcpdf.php directly and instead either submit the
fixes upstream to tecnickcom/TCPDF (open a PR referencing the exact changes in
tcpdf.php), maintain the fixes as a project patch applied during install (e.g.,
add a patch entry using cweagans/composer-patches referencing the modified
tcpdf.php hunks), or fork the TCPDF repo and require your fork in composer.json
so the changes to tcpdf.php persist across updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Controllers/FrontendController.php`:
- Around line 1305-1307: The prepare() failure for the SQL statement (e.g. when
$descStmt === false) should be treated as a server error, not a missing
resource: replace the current return $this->render404($response) calls (seen
around the $descStmt/$metaStmt/$topicStmt checks) with server-error handling —
either return $this->render500($response) if a render500 helper exists, or
throw/return an appropriate 500-level exception/response after logging the PDO
error (use the PDO connection's errorInfo() or exception message) so failures in
prepare() are surfaced as 5xx and logged for debugging.

In `@app/Controllers/LibriController.php`:
- Line 640: The current coercion for $fields['numero_pagine'] casts any
non-empty string (e.g. "abc") to int and then to 1; change the logic to first
check that the value is numeric (use is_numeric or filter_var with
FILTER_VALIDATE_INT) and only then cast and apply max(1, (int)...); if the input
is empty or not a valid number, set the field to null (do not coerce invalid
strings to 1). Apply the same fix to the other occurrence referenced around the
second location (the similar $fields['numero_pagine'] handling at the other
spot).

In `@scripts/manual-upgrade.php`:
- Around line 260-268: Add brute-force protection around the POST password check
by tracking and enforcing attempt limits in session or persistent store: before
validating UPGRADE_PASSWORD, check a counter like $_SESSION['upgrade_attempts']
and a timestamp like $_SESSION['upgrade_last_attempt']; if attempts exceed a
threshold (e.g., 5) within a time window (e.g., 15 minutes) set $error to a
lockout message and refuse further checks; on each failed hash_equals increment
the counter and update the timestamp, and on successful authentication reset the
counter and timestamp before calling session_regenerate_id(true) and setting
$_SESSION['upgrade_auth']; also consider adding a short delay or logging remote
IP for repeated failures.

In `@tests/full-test.spec.js`:
- Around line 2002-2009: The test is passing a genre id to the catalog filter
but the app compares by genre name (g.nome); update the test in
tests/full-test.spec.js so the page.goto call uses the genre's name
(genres[0].nome) when building `${BASE}/catalogo?genere=...` (instead of
genres[0].id) so the request matches the FrontendController.php filtering logic.

In `@vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php`:
- Around line 306-310: The current LineFormatter (class LineFormatter) returns
the full stacktrace and normalizes directory separators by calling strtr on the
entire $trace which also converts namespace backslashes; update the logic in the
method that builds the stacktrace output (the block that returns
"\n{$this->indentStacktraces}[stacktrace]\n...") to avoid calling strtr on the
whole $trace — instead split the $trace into frames/lines, detect file path
segments in each frame (e.g., the "in /path/to/file.php:line" or quoted file
path portions), normalize DIRECTORY_SEPARATOR to '/' only for those file-path
substrings, and then rejoin the frames so class names and namespaces
(backslashes) remain intact while file paths are normalized. Ensure you touch
the code around the return that currently uses strtr($trace,
DIRECTORY_SEPARATOR, '/') and reference class LineFormatter and the $trace
variable when making the fix.

In `@vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php`:
- Around line 151-153: In collectLogs() ensure the opened file handle is closed
if flock($handle, LOCK_EX) fails: before the early return that currently happens
when false === flock($handle, LOCK_EX) call (in
DeduplicationHandler::collectLogs), call fclose($handle) (or otherwise release
the resource) so the file descriptor is not leaked; alternatively restructure
the locking path to use a try/finally or cleanup block that always calls
fclose($handle) after opening.

In `@vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php`:
- Around line 46-49: You modified a vendored class (ProcessHandler) by changing
documentation/formatting around the private float $timeout property; do not edit
vendor code directly—either revert this file to the Composer-managed version and
remove it from version control, submit your doc/formatting changes as a PR to
the Monolog upstream repository (referencing class ProcessHandler and property
$timeout), or apply the change via a Composer patch (e.g., using
cweagans/composer-patches) so it’s reproducible; restore the original vendor
file in the repo and implement whichever of the three workflows you choose.

In `@vendor/tecnickcom/tcpdf/config/tcpdf_config.php`:
- Line 11: Do not edit the vendor file in-tree; revert the corrupted change in
vendor/tecnickcom/tcpdf/config/tcpdf_config.php (the altered header line
containing "Copyright (C) 2004-2014 2026 Nicola Asuni") so the repository
matches the installed Composer package, then open an upstream issue/PR with the
TCPDF maintainers describing the malformed year and the related corruption seen
in PDF_HEADER_STRING so they can fix the package; reference tcpdf_config.php and
the PDF_HEADER_STRING constant in your report and attach the exact corrupted
line.

---

Outside diff comments:
In `@installer/classes/Installer.php`:
- Around line 1023-1036: The current parsing in Installer.php uses
str_contains($host, ':') and explode(':', $host, 2) which breaks bracketed IPv6
literals like [2001:db8::1]:8080; update the logic around $host/$port extraction
to first detect bracketed IPv6 (host starts with '[' and contains a ']') and if
so extract the text inside brackets as $hostOnly and anything after the closing
']' (after a colon) as $portPart, otherwise split on the last colon (use strrpos
semantics instead of explode) to avoid breaking IPv6 addresses without brackets;
then set $host and $port as before and keep the later base and defaultPorts
handling unchanged.

---

Duplicate comments:
In `@app/Views/libri/partials/book_form.php`:
- Around line 1378-1417: You are overriding a private Choices.js method
(authorsChoice._onEnterKey) which is fragile; instead attach a stable public
handler: listen to the actual input element (internalInput) keydown/keypress or
Choices public events (e.g. 'addItem' / 'choice' / 'search') and implement the
create-on-Enter logic there by detecting Enter, reading internalInput.value,
checking the dropdown highlighted/choices via public DOM queries (wrapper /
.choices__list--dropdown / .choices__item--selectable) and calling
createAuthorFromInputWithValue when appropriate; remove the override of
authorsChoice._onEnterKey and ensure the new handler prevents default and stops
propagation only when creating a new author.

In `@app/Views/partials/scroll-to-top.php`:
- Around line 5-6: The translated string is injected directly into JS strings
via btn.setAttribute('aria-label', '<?= __('Torna su') ?>') and
btn.setAttribute('title', '<?= __('Torna su') ?>'), which can break the script
if the translation contains quotes or script-close sequences; fix by
encoding/escaping the translation for JavaScript output (emit the translation as
a JSON-safe JS string using PHP's json_encode or an equivalent JS-escaping
helper) so the second argument to btn.setAttribute receives a properly escaped
string.

In `@tests/full-test.spec.js`:
- Around line 2269-2273: Guard the result of page.goto before calling
resp.status() to avoid errors when it returns null: check the returned value
from page.goto (resp) and treat a null response as a non-200 result (or extract
status via a safe check), then perform the assertion using that guarded status;
update the code around page.goto / resp.status() so it uses a conditional check
on resp (or a safe accessor) before calling status().

In `@vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php`:
- Around line 53-54: The constructor in RotatingFileHandler sets
$this->nextRotation before assigning $this->timezone which can cause rotation
boundaries to use the default timezone; move the assignment of $this->timezone
so it occurs before calling $this->getNextRotation() (i.e., ensure
$this->timezone is set prior to computing $this->nextRotation in the
constructor). Additionally, update the rotation-boundary calculations inside
getNextRotation() (and any DateTimeImmutable instantiations used for rotation
boundary or filename generation) to construct DateTimeImmutable objects with
$this->timezone (e.g., new DateTimeImmutable('now', $this->timezone)) so the
rotation boundary aligns with the configured filename timezone.

In `@vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php`:
- Around line 137-142: The inode-rotation logic in write() is reusing the
$retrying flag so an inode-triggered reopen can consume the one-time
write-retry; introduce a separate boolean (e.g. $inodeReopened or
$reopenedForInode) and use it for the hasUrlInodeWasChanged() / close() path
instead of mutating or relying on $retrying, or explicitly restore/reset
$retrying after the inode reopen, so the original write-failure retry behavior
remains intact; update references in write(), hasUrlInodeWasChanged() handling,
and any code paths that check $retrying to use the new flag where appropriate.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_colors.php`:
- Line 10: Update the malformed copyright year range in the header string
"Copyright (C) 2002-2013 2026 Nicola Asuni - Tecnick.com LTD" to a single
continuous range (for example "Copyright (C) 2002-2026 Nicola Asuni -
Tecnick.com LTD") in tcpdf_colors.php so the year sequence is correct and no
duplicate years remain.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php`:
- Line 1387: The head table's checkSumAdjustment is being written at bytes
+4..+8 but per the TrueType/OpenType spec it must be at +8..+12; update the
write in the code that manipulates $font (the expression using
$table['head']['offset'], $offset and $checkSumAdjustment) so it inserts
pack('N', $checkSumAdjustment) starting at $table['head']['offset'] + $offset +
8 and continues to +12 (i.e., use substr boundaries +8 and +12 instead of +4 and
+8) to avoid corrupting subsetted TTF metadata.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_images.php`:
- Line 277: Fix the typo "Unknownn" → "Unknown" in the PNG parser comments in
tcpdf_images.php: locate the PNG parsing section (the PNG parse function / PNG
parser comment blocks) and replace all occurrences of the misspelled "Unknownn"
(e.g., in comment strings like "Unknownn compression method") with "Unknown" so
the comments read correctly; ensure the three occurrences noted in the review
are corrected.

In `@vendor/tecnickcom/tcpdf/tcpdf.php`:
- Around line 23733-23737: The preg_split call that builds $rawparams is using a
capturing group '/([\,\s]+)/si' which causes delimiters to appear in the result;
update the preg_split invocation in the code that populates $rawparams to use a
non‑capturing group and the PREG_SPLIT_NO_EMPTY flag (e.g. '/(?:[,\s]+)/si' with
PREG_SPLIT_NO_EMPTY) so delimiters are not returned and you can remove the
subsequent array_filter step that tries to drop empty strings.

---

Nitpick comments:
In `@installer/database/data_de_DE.sql`:
- Around line 189-191: The seed contains duplicate upserts for the same setting
key 'cms.events_page_enabled' which causes the later INSERT to silently override
the earlier one; remove the redundant INSERT/ON DUPLICATE KEY UPDATE block so
there is a single canonical upsert for 'cms.events_page_enabled' (keep the
preferred values/description and delete the other occurrence), ensuring only one
source of truth for that setting in the SQL seed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php`:
- Around line 4-6: This vendor file (tcpdf.php) has local modifications that
will be lost on Composer updates; stop editing vendor/tecnickcom/tcpdf/tcpdf.php
directly and instead either submit the fixes upstream to tecnickcom/TCPDF (open
a PR referencing the exact changes in tcpdf.php), maintain the fixes as a
project patch applied during install (e.g., add a patch entry using
cweagans/composer-patches referencing the modified tcpdf.php hunks), or fork the
TCPDF repo and require your fork in composer.json so the changes to tcpdf.php
persist across updates.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and 5e4ea3f.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (110)
  • .htaccess
  • README.md
  • app/Controllers/FrontendController.php
  • app/Controllers/LibriController.php
  • app/Controllers/UpdateController.php
  • app/Models/BookRepository.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • app/Views/autori/index.php
  • app/Views/editori/index.php
  • app/Views/events/index.php
  • app/Views/frontend/event-detail.php
  • app/Views/frontend/events.php
  • app/Views/frontend/home-sections/events.php
  • app/Views/frontend/home-sections/genre_carousel.php
  • app/Views/frontend/home.php
  • app/Views/frontend/layout.php
  • app/Views/layout.php
  • app/Views/libri/index.php
  • app/Views/libri/partials/book_form.php
  • app/Views/libri/scheda_libro.php
  • app/Views/partials/scroll-to-top.php
  • app/Views/prestiti/index.php
  • app/Views/utenti/index.php
  • frontend/js/vendor.js
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/database/data_en_US.sql
  • installer/database/data_it_IT.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • public/assets/main.css
  • public/assets/vendor.bundle.js
  • scripts/.user.ini
  • scripts/create-release.sh
  • scripts/manual-update.php
  • scripts/manual-upgrade.php
  • tests/full-test.spec.js
  • tests/genre-bugs.spec.js
  • vendor/composer/autoload_classmap.php
  • vendor/composer/autoload_files.php
  • vendor/composer/autoload_static.php
  • vendor/composer/installed.json
  • vendor/composer/installed.php
  • vendor/graham-campbell/result-type/composer.json
  • vendor/laravel/serializable-closure/composer.json
  • vendor/laravel/serializable-closure/src/Serializers/Native.php
  • vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php
  • vendor/monolog/monolog/README.md
  • vendor/monolog/monolog/composer.json
  • vendor/monolog/monolog/src/Monolog/Formatter/GelfMessageFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php
  • vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php
  • vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/Handler.php
  • vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php
  • vendor/slim/csrf/composer.json
  • vendor/slim/psr7/composer.json
  • vendor/slim/psr7/src/Cookies.php
  • vendor/slim/slim/MAINTAINERS.md
  • vendor/slim/slim/Slim/App.php
  • vendor/slim/slim/composer.json
  • vendor/slim/slim/psalm.xml
  • vendor/tecnickcom/tcpdf/CHANGELOG.TXT
  • vendor/tecnickcom/tcpdf/LICENSE.TXT
  • vendor/tecnickcom/tcpdf/README.md
  • vendor/tecnickcom/tcpdf/VERSION
  • vendor/tecnickcom/tcpdf/composer.json
  • vendor/tecnickcom/tcpdf/config/tcpdf_config.php
  • vendor/tecnickcom/tcpdf/fonts/ae_fonts_2.0/README
  • vendor/tecnickcom/tcpdf/fonts/freefont-20100919/COPYING
  • vendor/tecnickcom/tcpdf/fonts/freefont-20100919/CREDITS
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/COPYING
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/CREDITS
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/README
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/TROUBLESHOOTING
  • vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php
  • vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php
  • vendor/tecnickcom/tcpdf/include/barcodes/qrcode.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_colors.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_filters.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_font_data.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_images.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_static.php
  • vendor/tecnickcom/tcpdf/tcpdf.php
  • vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php
  • vendor/tecnickcom/tcpdf/tcpdf_barcodes_1d.php
  • vendor/tecnickcom/tcpdf/tcpdf_barcodes_2d.php
  • vendor/tecnickcom/tcpdf/tools/tcpdf_addfont.php
  • vendor/vlucas/phpdotenv/composer.json
  • vendor/vlucas/phpdotenv/src/Parser/EntryParser.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php
💤 Files with no reviewable changes (7)
  • scripts/manual-update.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php
  • vendor/vlucas/phpdotenv/src/Parser/EntryParser.php
  • vendor/slim/slim/psalm.xml
  • vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php
  • vendor/slim/slim/MAINTAINERS.md

Comment on lines +306 to +310
if (trim($trace) === '') {
return '';
}

return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}" . strtr($trace, DIRECTORY_SEPARATOR, '/') . "\n";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the exact code context in LineFormatter.php
cd vendor/monolog/monolog/src/Monolog/Formatter && head -320 LineFormatter.php | tail -30

Repository: fabiodalez-dev/Pinakes

Length of output: 1051


🏁 Script executed:

# Let's get more context around the problematic code
cat -n vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php | sed -n '290,320p'

Repository: fabiodalez-dev/Pinakes

Length of output: 1240


🏁 Script executed:

# Check what method this code is in and understand the full context
cat -n vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php | sed -n '280,320p'

Repository: fabiodalez-dev/Pinakes

Length of output: 1571


🌐 Web query:

PHP Throwable getTraceAsString() format namespace separators backslash Windows

💡 Result:

Throwable::getTraceAsString() returns a plain PHP string containing the stack frames (e.g. #0 ... func()), with function/class names and file paths exactly as PHP records them. It does not have a special “Windows formatting” mode. [1]

So, on Windows you can naturally end up with lots of \ characters for two unrelated reasons:

  1. Namespaces in PHP use \ as the separator (e.g. Vendor\Package\Class::method). [2]
  2. Windows file paths often contain \ (e.g. C:\path\to\file.php).

Why you might see double backslashes (\\)

If you pass the trace string through an escaping representation, backslashes will be shown doubled because backslash is the escape character in many output formats:

  • var_dump($trace) / var_export($trace, true) show an escaped representation of the string (so a single \ is displayed as \\).
  • json_encode($trace) must escape backslashes to produce valid JSON, so you’ll see \\ in the JSON text. [3]

In all of those cases, the underlying string still contains single backslashes; you’re just viewing an escaped rendering.

What to do

  • If you want “human readable” output: echo $e->getTraceAsString(); [1]
  • If you want full control over formatting (including normalizing slashes): use getTrace() (array form) and format it yourself. [4]

Sources:
[1] PHP manual: Throwable::getTraceAsString() [1]
[2] PHP RFC discussing \ as the namespace separator [2]
[3] PHP manual: json_encode() (escaping in JSON strings) [3]
[4] PHP manual: Throwable::getTrace() [4]


🌐 Web query:

PHP strtr() DIRECTORY_SEPARATOR Windows namespace corruption stacktrace

💡 Result:

On Windows, DIRECTORY_SEPARATOR is \ (backslash) [1]. In PHP, backslash is also the namespace separator in fully-qualified class names like Foo\Bar [2][3].

So if you do something like:

$trace = strtr($trace, ['/' => DIRECTORY_SEPARATOR, '\\' => DIRECTORY_SEPARATOR]);

(or any “normalize slashes” transform applied to the entire stacktrace string), you can accidentally rewrite namespaced identifiers inside the trace, making class names look like paths (or otherwise “corrupting” how the trace reads).

How to avoid namespace corruption

  • Do not run slash normalization on the whole stacktrace text. Only normalize actual filesystem paths.
  • Prefer forward slashes (/) as your internal path separator (PHP on Windows accepts / in paths), and only deal with \ when you receive Windows paths from APIs like realpath() / __FILE__ / __DIR__ [2].

Practical pattern:

$bt = debug_backtrace();
foreach ($bt as &$f) {
  if (isset($f['file'])) {
    $f['file'] = str_replace('\\', '/', $f['file']); // normalize path only
  }
}

Sources: DIRECTORY_SEPARATOR meaning [1]; Windows accepts / and \ but \ is also namespaces [2]; PHP namespace separator is \ [3].

[1] https://www.php.net/manual/en/dir.constants.php
[2] https://stackoverflow.com/questions/32035756/what-is-the-difference-between-and-in-php
[3] https://wiki.php.net/rfc/backslashnamespaces


Line 310: Avoid normalizing the entire stacktrace string with strtr(DIRECTORY_SEPARATOR, '/').

On Windows, this replaces all backslashes including namespace separators in class names (e.g., Vendor\ClassVendor/Class), corrupting stacktrace readability. Normalize only file-path portions per frame.

🔧 Suggested fix
-        return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}" . strtr($trace, DIRECTORY_SEPARATOR, '/') . "\n";
+        $normalizedTrace = preg_replace_callback(
+            '/^(#\d+\s+)(.+?)(\(\d+\):)/m',
+            static fn (array $m): string => $m[1] . str_replace(DIRECTORY_SEPARATOR, '/', $m[2]) . $m[3],
+            $trace
+        ) ?? $trace;
+
+        return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}{$normalizedTrace}\n";
📝 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.

Suggested change
if (trim($trace) === '') {
return '';
}
return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}" . strtr($trace, DIRECTORY_SEPARATOR, '/') . "\n";
if (trim($trace) === '') {
return '';
}
$normalizedTrace = preg_replace_callback(
'/^(#\d+\s+)(.+?)(\(\d+\):)/m',
static fn (array $m): string => $m[1] . str_replace(DIRECTORY_SEPARATOR, '/', $m[2]) . $m[3],
$trace
) ?? $trace;
return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}{$normalizedTrace}\n";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php` around lines
306 - 310, The current LineFormatter (class LineFormatter) returns the full
stacktrace and normalizes directory separators by calling strtr on the entire
$trace which also converts namespace backslashes; update the logic in the method
that builds the stacktrace output (the block that returns
"\n{$this->indentStacktraces}[stacktrace]\n...") to avoid calling strtr on the
whole $trace — instead split the $trace into frames/lines, detect file path
segments in each frame (e.g., the "in /path/to/file.php:line" or quoted file
path portions), normalize DIRECTORY_SEPARATOR to '/' only for those file-path
substrings, and then rejoin the frames so class names and namespaces
(backslashes) remain intact while file paths are normalized. Ensure you touch
the code around the return that currently uses strtr($trace,
DIRECTORY_SEPARATOR, '/') and reference class LineFormatter and the $trace
variable when making the fix.

Comment on lines +151 to +153
if (false === flock($handle, LOCK_EX)) {
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file with context around the flagged lines
head -200 vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php | tail -100

Repository: fabiodalez-dev/Pinakes

Length of output: 3246


🏁 Script executed:

# Get a broader view: read the collectLogs method completely
# First, find the line range of the method
rg -n "function collectLogs|private function|protected function" vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php

Repository: fabiodalez-dev/Pinakes

Length of output: 271


🏁 Script executed:

# Look at lines 130-170 to get full context of the flock logic
sed -n '130,170p' vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php

Repository: fabiodalez-dev/Pinakes

Length of output: 1269


Close the file handle before returning on lock acquisition failure.

In collectLogs(), the file handle opened at line 148 is not closed when flock() fails at line 151–153. The early return bypasses the fclose($handle) at line 170, leaving the file descriptor open. Under lock contention, this accumulates resource leaks.

Proposed fix
        if (false === flock($handle, LOCK_EX)) {
+           fclose($handle);
            return;
        }
📝 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.

Suggested change
if (false === flock($handle, LOCK_EX)) {
return;
}
if (false === flock($handle, LOCK_EX)) {
fclose($handle);
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php` around
lines 151 - 153, In collectLogs() ensure the opened file handle is closed if
flock($handle, LOCK_EX) fails: before the early return that currently happens
when false === flock($handle, LOCK_EX) call (in
DeduplicationHandler::collectLogs), call fclose($handle) (or otherwise release
the resource) so the file descriptor is not leaked; alternatively restructure
the locking path to use a try/finally or cleanup block that always calls
fclose($handle) after opening.

Comment on lines +46 to +49
private float $timeout;

/**
* @var array<int, string[]>
* @var array<int, list<string>>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid modifying vendor files directly.

This file is part of the monolog/monolog package managed by Composer. Direct modifications to vendor/ files will be overwritten on the next composer install or composer update, causing these changes to be silently lost.

If these formatting/documentation improvements are desired:

  1. Contribute upstream: Submit a PR to the Monolog repository with these changes.
  2. Revert locally: Remove this file from version control and let Composer manage it.

If the vendor/ directory is intentionally committed, consider using Composer patches or excluding vendor from the repository entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php` around lines
46 - 49, You modified a vendored class (ProcessHandler) by changing
documentation/formatting around the private float $timeout property; do not edit
vendor code directly—either revert this file to the Composer-managed version and
remove it from version control, submit your doc/formatting changes as a PR to
the Monolog upstream repository (referencing class ProcessHandler and property
$timeout), or apply the change via a Composer patch (e.g., using
cweagans/composer-patches) so it’s reproducible; restore the original vendor
file in the repo and implement whichever of the three workflows you choose.

// License : GNU-LGPL v3 (https://www.gnu.org/copyleft/lesser.html)
// -------------------------------------------------------------------
// Copyright (C) 2004-2014 Nicola Asuni - Tecnick.com LTD
// Copyright (C) 2004-2014 2026 Nicola Asuni - Tecnick.com LTD
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Corrupted copyright year format in vendor file header.

Line 11 shows Copyright (C) 2004-2014 2026 Nicola Asuni which has malformed year formatting (likely should be 2004-2026 or 2004-2014, 2026). This appears to be part of the same corruption that affected PDF_HEADER_STRING.

Since this is a vendor file managed by Composer, these issues should be reported upstream to TCPDF maintainers rather than modified locally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/config/tcpdf_config.php` at line 11, Do not edit the
vendor file in-tree; revert the corrupted change in
vendor/tecnickcom/tcpdf/config/tcpdf_config.php (the altered header line
containing "Copyright (C) 2004-2014 2026 Nicola Asuni") so the repository
matches the installed Composer package, then open an upstream issue/PR with the
TCPDF maintainers describing the malformed year and the related corruption seen
in PDF_HEADER_STRING so they can fix the package; reference tcpdf_config.php and
the PDF_HEADER_STRING constant in your report and attach the exact corrupted
line.

- Installer: handle bracketed IPv6 literals in host parsing
- FrontendController: return 500 (not 404) for prepare() failures
- LibriController: validate numero_pagine with filter_var, reject non-numeric
- scroll-to-top: escape translated text with json_encode for JS safety
- manual-upgrade: add brute-force protection (5 attempts / 5-min lockout)
- book_form: replace private Choices.js _onEnterKey with public keydown handler
- data_de_DE: remove duplicate events_page_enabled upsert
- tests: use genre name (not ID) for catalog filter, guard resp?.status()
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
vendor/slim/psr7/composer.json (1)

1-73: ⚠️ Potential issue | 🟠 Major

Avoid direct edits to vendor/slim/psr7/composer.json.

While vendor/ is tracked in this repository, slim/psr7 is installed as a standard upstream dependency from GitHub. Direct edits circumvent Composer's dependency management and will conflict with future composer install/update operations. If you need to modify this package, use a path repository in the root composer.json or depend on a forked version instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/slim/psr7/composer.json` around lines 1 - 73, You edited the upstream
dependency slim/psr7's composer.json directly in vendor, which bypasses Composer
and will break future installs; revert that file and instead manage changes via
Composer. Restore vendor/slim/psr7/composer.json to the original upstream state
(undo changes to keys like "require", "provide", "autoload", etc.), then
implement your intended modifications by adding a path repository or a forked
package entry in the root composer.json and update composer.json to require your
fork/path for the "slim/psr7" package so Composer manages the changed package
correctly.
app/Support/Updater.php (1)

994-1026: ⚠️ Potential issue | 🟡 Minor

Treat non-2xx file_get_contents download responses as failures.

Because ignore_errors is enabled, Line 994 can return an error body for HTTP 4xx/5xx. In this fallback branch, non-auth HTTP errors are not currently forcing failure, so HTML/JSON error payloads may continue through ZIP validation instead of failing early with clearer handling.

🔧 Proposed fix
                 $fileContent = `@file_get_contents`($downloadUrl, false, $context);

                 // Log response headers (magic variable set by file_get_contents)
                 /** `@var` array<int, string> $http_response_header */
@@
                 $dlStatus = 0;
                 if (!empty($http_response_header[0]) && preg_match('/HTTP\/\d\.\d\s+(\d+)/', $http_response_header[0], $dlMatch)) {
                     $dlStatus = (int) $dlMatch[1];
                 }
+                if ($dlStatus >= 400) {
+                    $fileContent = false;
+                }
                 if (in_array($dlStatus, [401, 403], true) && $this->githubToken !== '') {
                     $savedToken = $this->githubToken;
                     $this->githubToken = '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 994 - 1026, The download logic in
Updater.php currently leaves error bodies from file_get_contents (with
ignore_errors=true) to proceed; after each file_get_contents call (the initial
fetch that sets $fileContent and the retry branch that reassigns $fileContent
after clearing $this->githubToken), inspect the HTTP status from
$http_response_header (as parsed into $dlStatus) and treat any non-2xx status as
a failure: set $fileContent to false or throw/return an error so ZIP validation
is not attempted on an HTML/JSON error payload. Ensure this check uses the same
parsing logic (preg_match on $http_response_header[0] to derive $dlStatus) and
add a debugLog call (use $this->debugLog) with the headers and status when
rejecting the response; preserve resetting $this->githubToken after the retry by
keeping the existing try/finally.
installer/classes/Installer.php (1)

1023-1036: ⚠️ Potential issue | 🟡 Minor

Bracketed IPv6 host parsing can drop a valid forwarded/server port

At Line [1023], when host matches bracketed IPv6 without inline :port, $port stays null and the elseif chain skips HTTP_X_FORWARDED_PORT / SERVER_PORT. This can produce a wrong APP_CANONICAL_URL on non-default ports.

🛠️ Proposed fix
-        if (preg_match('/^\[(.+)\](?::(\d+))?$/', $host, $matches)) {
+        if (preg_match('/^\[(.+)\](?::(\d+))?$/', $host, $matches)) {
             // Bracketed IPv6 literal, optionally with port
             $host = '[' . $matches[1] . ']';
             $port = isset($matches[2]) ? (int) $matches[2] : null;
         } elseif (substr_count($host, ':') === 1) {
             // hostname:port or IPv4:port
             [$hostOnly, $portPart] = explode(':', $host, 2);
             $host = $hostOnly;
             $port = is_numeric($portPart) ? (int)$portPart : null;
-        } elseif (!empty($_SERVER['HTTP_X_FORWARDED_PORT'])) {
-            $port = (int)$_SERVER['HTTP_X_FORWARDED_PORT'];
-        } elseif (isset($_SERVER['SERVER_PORT']) && is_numeric((string)$_SERVER['SERVER_PORT'])) {
-            $port = (int)$_SERVER['SERVER_PORT'];
         }
+        if ($port === null && !empty($_SERVER['HTTP_X_FORWARDED_PORT'])) {
+            $port = (int)$_SERVER['HTTP_X_FORWARDED_PORT'];
+        } elseif ($port === null && isset($_SERVER['SERVER_PORT']) && is_numeric((string)$_SERVER['SERVER_PORT'])) {
+            $port = (int)$_SERVER['SERVER_PORT'];
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@installer/classes/Installer.php` around lines 1023 - 1036, The bracketed IPv6
branch in Installer:: (the preg_match('/^\[(.+)\](?::(\d+))?$/', $host,
$matches)) handler sets $host and $port to null when no inline port is present,
but uses elseif so the subsequent HTTP_X_FORWARDED_PORT / SERVER_PORT checks are
skipped; adjust the logic so that after handling a bracketed IPv6 literal you
still populate $port from $_SERVER['HTTP_X_FORWARDED_PORT'] or
$_SERVER['SERVER_PORT'] when an inline port was not found (either by converting
the elseif chain into separate ifs or by only setting $port when $matches[2]
exists and leaving $port unset otherwise), ensuring $host remains '[...]' and
$port is derived from forwarded/server values when needed.
♻️ Duplicate comments (13)
vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php (1)

48-54: ⚠️ Potential issue | 🟠 Major

Align rotation boundary with the configured timezone.

$this->nextRotation is computed before $this->timezone is assigned, and getNextRotation() still uses the server default timezone. Filename generation (Line 183) is timezone-aware, so rotation cutoff and filename date can diverge around midnight.

Suggested fix
-        $this->nextRotation = $this->getNextRotation();
         $this->timezone = $timezone;
+        $this->nextRotation = $this->getNextRotation();
     protected function getNextRotation(): \DateTimeImmutable
     {
+        $now = new \DateTimeImmutable('now', $this->timezone);
+
         return match (str_replace(['/','_','.'], '-', $this->dateFormat)) {
-            self::FILE_PER_MONTH => (new \DateTimeImmutable('first day of next month'))->setTime(0, 0, 0),
-            self::FILE_PER_YEAR => (new \DateTimeImmutable('first day of January next year'))->setTime(0, 0, 0),
-            default => (new \DateTimeImmutable('tomorrow'))->setTime(0, 0, 0),
+            self::FILE_PER_MONTH => $now->modify('first day of next month')->setTime(0, 0, 0),
+            self::FILE_PER_YEAR => $now->modify('first day of January next year')->setTime(0, 0, 0),
+            default => $now->modify('tomorrow')->setTime(0, 0, 0),
         };
     }

Also applies to: 226-232

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php` around
lines 48 - 54, The constructor sets $this->nextRotation by calling
getNextRotation() before assigning $this->timezone, so getNextRotation() uses
the server default timezone and can disagree with timezone-aware filename
generation; move the assignment of $this->timezone (and any timezone-dependent
setup) before calling $this->getNextRotation() (affecting the __construct in
RotatingFileHandler and the same pattern around lines 226-232), or modify
getNextRotation() to accept a timezone parameter and pass $this->timezone to it
after setting it; ensure calls to setFilenameFormat(...) remain unchanged so
filenames and rotation cutoff use the same timezone.
vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php (1)

137-142: ⚠️ Potential issue | 🟠 Major

Retry state is still coupled between inode-rotation and write-failure paths.

$this->retrying is set for inode-change handling (Line 276), then the recursive call at Line 139 can no longer take the write-failure retry branch at Line 187. This suppresses the intended one-time retry after reopen.

Suggested fix
 protected function write(LogRecord $record): void
 {
     if ($this->hasUrlInodeWasChanged()) {
+        $this->inodeUrl = null;
         $this->close();
         $this->write($record);

         return;
     }
@@
 private function hasUrlInodeWasChanged(): bool
 {
-    if ($this->inodeUrl === null || $this->retrying || $this->inodeUrl === $this->getInodeFromUrl()) {
+    if ($this->inodeUrl === null || $this->inodeUrl === $this->getInodeFromUrl()) {
         return false;
     }
-
-    $this->retrying = true;

     return true;
 }

Also applies to: 270-277

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php` around lines
137 - 142, The inode-rotation path sets the instance-level flag $this->retrying
(in methods like hasUrlInodeWasChanged()/the inode-change block) and then calls
$this->write($record) recursively, which prevents the write-failure retry branch
from executing; update the inode-rotation flow (the block that calls close()
then write($record)) to clear or localize the retry state before the recursive
call (e.g., save $prevRetry = $this->retrying; $this->retrying = false;
$this->close(); $this->write($record); $this->retrying = $prevRetry) so that
write() can independently perform its one-time retry on failure; apply the same
change where retrying is set in the inode-change handling to avoid coupling the
two paths.
vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php (1)

1381-1387: ⚠️ Potential issue | 🟠 Major

verifyCandidateSignature misparses parameter lists with nested parentheses.

The state machine exits params on the first ) token, so defaults like fn($a = foo(), $b) => ... can be parsed incorrectly and rejected.

💡 Proposed fix
         $params = [];
         $vars = [];
         $state = 'start';
+        $depth = 0;

         foreach ($tokens as $token) {
             if (! is_array($token)) {
                 if ($token === '(' && $state === 'start') {
                     $state = 'params';
-                } elseif ($token === ')' && $state === 'params') {
-                    $state = 'body';
+                    $depth = 1;
+                } elseif ($token === '(' && $state === 'params') {
+                    $depth++;
+                } elseif ($token === ')' && $state === 'params') {
+                    $depth--;
+                    if ($depth === 0) {
+                        $state = 'body';
+                    }
                 }

                 continue;
             }

Use this read-only verification to confirm the missing depth tracking:

#!/bin/bash
set -euo pipefail

FILE="vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php"

echo "=== verifyCandidateSignature block ==="
sed -n '1363,1405p' "$FILE"

echo
echo "=== Look for params->body transition without depth handling ==="
rg -n "token === '\\)' && \\$state === 'params'" "$FILE"
rg -n "\\$depth" "$FILE" || true

Expected result: the transition exists, and no local $depth handling is present in this method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php` around
lines 1381 - 1387, The state machine in verifyCandidateSignature iterates tokens
and switches from 'params' to 'body' on any ')' token, which misparses nested
parentheses in parameter defaults (e.g., fn($a = foo(), $b)); update the logic
in ReflectionClosure::verifyCandidateSignature to track a depth counter while in
the 'params' state (increment on '(' and decrement on ')') and only transition
to 'body' when you see a ')' and depth is zero, ensuring nested parentheses do
not prematurely end parameter parsing; adjust any variables/conditions around
the foreach over $tokens to use this $depth check and initialize/reset $depth
appropriately.
vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Correct malformed copyright year range.

Line 10 currently has an invalid format (2010-2013 2026).

Suggested fix
-// Copyright (C) 2010-2013 2026 Nicola Asuni - Tecnick.com LTD
+// Copyright (C) 2010-2026 Nicola Asuni - Tecnick.com LTD
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php` at line 10, Update the
malformed copyright header string "2010-2013 2026" in the file's top copyright
comment to a valid format (for example "2010-2013, 2026") so the year range is
correctly punctuated; locate the copyright comment containing "2010-2013 2026"
and replace it with the corrected text.
vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Fix malformed copyright year range.

Line 10 has an invalid year format (2010-2014 2026).

Suggested fix
-// Copyright (C) 2010-2014 2026 Nicola Asuni - Tecnick.com LTD
+// Copyright (C) 2010-2026 Nicola Asuni - Tecnick.com LTD
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php` at line 10, The
copyright header contains a malformed year token "2010-2014 2026"; locate that
exact string in vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php and
replace it with a valid format such as "2010-2014, 2026" (or a single continuous
range like "2010-2026" if that's intended) so the year range/token is
syntactically correct.
vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php (1)

151-153: ⚠️ Potential issue | 🟠 Major

Close the file handle before returning on lock failure.

At Line 151–Line 153, the early return bypasses fclose($handle), causing a resource leak when flock fails.

🔧 Suggested fix
         if (false === flock($handle, LOCK_EX)) {
+            fclose($handle);
             return;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php` around
lines 151 - 153, The early return after flock($handle, LOCK_EX) failing leaks
the open resource; update the code in DeduplicationHandler (the block using
$handle and flock) to call fclose($handle) before returning on lock failure
(i.e., replace "if (false === flock($handle, LOCK_EX)) { return; }" with a
branch that calls fclose($handle) then returns) so the file handle is always
closed when the function exits early.
vendor/tecnickcom/tcpdf/include/tcpdf_images.php (1)

277-277: ⚠️ Potential issue | 🟡 Minor

Fix repeated typo in PNG diagnostics (UnknownnUnknown).

At Line 277, Line 282, and Line 334, diagnostic comments contain a misspelling that should be corrected for clarity and consistency.

Also applies to: 282-282, 334-334

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_images.php` at line 277, Replace the
repeated typo in the PNG diagnostic comments where the message reads "Unknownn
compression method" to the correct spelling "Unknown compression method"; locate
the occurrences of that string in tcpdf_images.php (the PNG diagnostics branch
handled in the image parsing logic) and update the comment/text at each
occurrence (previously at the lines containing "Unknownn compression method") to
the corrected form to ensure consistency.
vendor/tecnickcom/tcpdf/config/tcpdf_config.php (1)

11-11: ⚠️ Potential issue | 🟡 Minor

Revert malformed TCPDF header text values.

Line 11 has a corrupted copyright year format, and Line 117 has malformed PDF_HEADER_STRING text (by2026).

🔧 Suggested fix
-// Copyright (C) 2004-2014 2026 Nicola Asuni - Tecnick.com LTD
+// Copyright (C) 2004-2026 Nicola Asuni - Tecnick.com LTD
...
-define ('PDF_HEADER_STRING', "by2026 Nicola Asuni - Tecnick.com\nwww.tcpdf.org");
+define ('PDF_HEADER_STRING', "by Nicola Asuni - Tecnick.com\nwww.tcpdf.org");

Also applies to: 117-117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/config/tcpdf_config.php` at line 11, The file has
corrupted header text and a malformed PDF header string; restore the original
header comment to "Copyright (C) 2004-2014 Nicola Asuni - Tecnick.com LTD" (fix
the corrupted year/text around the author) and update the PDF_HEADER_STRING
constant (symbol PDF_HEADER_STRING) to remove the stray "2026" so it reads the
intended phrase (e.g., "by Tecnick.com LTD" or simply "by"). Locate the header
comment and the PDF_HEADER_STRING definition in tcpdf_config.php and replace the
malformed values with the correct original texts.
vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php (1)

310-310: ⚠️ Potential issue | 🟠 Major

Avoid normalizing the entire stacktrace string.

At Line 310, strtr($trace, DIRECTORY_SEPARATOR, '/') rewrites non-path backslashes too (notably PHP namespaces on Windows), which degrades stacktrace readability.

🔧 Suggested fix
-        return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}" . strtr($trace, DIRECTORY_SEPARATOR, '/') . "\n";
+        return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}{$trace}\n";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php` at line 310,
The code in Monolog\Formatter\LineFormatter currently normalizes all backslashes
in the $trace string (strtr($trace, DIRECTORY_SEPARATOR, '/')), which also
converts PHP namespace separators and harms readability; update the logic in
Monolog\Formatter\LineFormatter (the stacktrace formatting code that builds
$trace) to stop running a global strtr on the whole stacktrace and instead
normalize directory separators only inside actual file path substrings—e.g., use
a preg_replace_callback to find file path patterns (Windows drive paths or UNC
paths and the file:line fragments produced by backtraces) and replace
backslashes with '/' only within those matches, leaving namespace backslashes
untouched.
vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php (1)

1387-1387: ⚠️ Potential issue | 🔴 Critical

Fix checkSumAdjustment write offset (reintroduced regression).

Line 1387 writes the checksum at +4..+8, which targets the wrong head field. This should be written at +8..+12.

🔧 Proposed fix
-$font = substr($font, 0, $table['head']['offset'] + $offset + 4).pack('N', $checkSumAdjustment).substr($font, $table['head']['offset'] + $offset + 8);
+$font = substr($font, 0, $table['head']['offset'] + $offset + 8).pack('N', $checkSumAdjustment).substr($font, $table['head']['offset'] + $offset + 12);
#!/bin/bash
# Verify current checkSumAdjustment write location in subset rebuild logic.
rg -n "set checkSumAdjustment on head table|pack\\('N', \\$checkSumAdjustment\\)" vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php -C3
sed -n '1383,1389p' vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php` at line 1387, The
checkSumAdjustment is currently written at the wrong offset in the head table:
update the write location so the packed 4-byte checkSumAdjustment is inserted at
+8..+12 instead of +4..+8; specifically modify the expression that builds $font
(the line using substr($font, 0, $table['head']['offset'] + $offset +
4).pack('N', $checkSumAdjustment).substr(...)) to use +8 (i.e.
$table['head']['offset'] + $offset + 8) for the start of the pack insertion so
the packed value lands at bytes +8..+12 relative to the head table.
vendor/tecnickcom/tcpdf/tcpdf.php (3)

2909-2914: ⚠️ Potential issue | 🟠 Major

setCompression() is bypassed by an unconditional early return.

Line 2910 returns immediately, so $compress and gzcompress checks are never used, and the rest of the method is dead code.

🔧 Proposed fix
 public function setCompression($compress=true) {
-		$this->compress = false; return;
-		if (function_exists('gzcompress')) {
-			if ($compress) {
-                $this->compress = true;
-			}
-		}
+		$this->compress = false;
+		if (function_exists('gzcompress') && $compress) {
+			if (!$this->pdfa_mode) {
+				$this->compress = true;
+			}
+		}
 	}
#!/bin/bash
set -euo pipefail

FILE="vendor/tecnickcom/tcpdf/tcpdf.php"

echo "[local]"
rg -n -C3 'public function setCompression|\$this->compress = false; return;|\$this->compress = true;|pdfa_mode' "$FILE"

echo
echo "[upstream 6.11.0]"
curl -fsSL https://raw.githubusercontent.com/tecnickcom/TCPDF/6.11.0/tcpdf.php \
  | rg -n -C3 'public function setCompression|\$this->compress = false; return;|\$this->compress = true;|pdfa_mode'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php` around lines 2909 - 2914, The
setCompression method currently unconditionally sets $this->compress = false and
returns, making the rest of the function dead; remove the early unconditional
assignment and return so the method honors the $compress parameter and the
gzcompress availability check, i.e., in function setCompression(...) use
function_exists('gzcompress') and then set $this->compress based on the incoming
$compress value (and pdfa_mode if needed) rather than always forcing false and
returning early.

23733-23737: ⚠️ Potential issue | 🟠 Major

SVG path tokenization keeps delimiters and can skew parameter indexing.

Line 23734 uses a capturing group in preg_split, so delimiters may be retained in $rawparams before numeric conversion.

🔧 Proposed fix
-				$rawparams = preg_split('/([\,\s]+)/si', trim($val[2]));
-				$rawparams = array_filter($rawparams, function($p) {
-					return trim($p) != '';
-				});
+				$rawparams = preg_split('/[\,\s]+/si', trim($val[2]), -1, PREG_SPLIT_NO_EMPTY);
#!/bin/bash
set -euo pipefail

FILE="vendor/tecnickcom/tcpdf/tcpdf.php"

echo "[local svg split code]"
rg -n -C2 "preg_split\\('/\\(\\[\\\\,\\\\s\\]\\+\\)/si'|PREG_SPLIT_NO_EMPTY|array_filter\\(\\$rawparams" "$FILE"

echo
echo "[upstream 6.11.0 svg split code]"
curl -fsSL https://raw.githubusercontent.com/tecnickcom/TCPDF/6.11.0/tcpdf.php \
  | rg -n -C2 "preg_split\\('/\\(\\[\\\\,\\\\s\\]\\+\\)/si'|PREG_SPLIT_NO_EMPTY|array_filter\\(\\$rawparams"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php` around lines 23733 - 23737, The preg_split
call that populates $rawparams currently uses a capturing group, causing
delimiters to be kept and skew parameter positions; change the split to avoid
capturing (use a non-capturing group) or pass the PREG_SPLIT_NO_EMPTY flag so
delimiters are not returned, then remove the array_filter cleanup; update the
preg_split invocation that fills $rawparams (and subsequent handling of
$rawparams) to rely on the cleaned tokens only so numeric parameter indexing is
correct.

6925-6927: ⚠️ Potential issue | 🟠 Major

fitBlock() bottom clamp can exceed the page-break boundary.

Line 6925-Line 6926 clamps against PageBreakTrigger + bMargin, which can place content into the footer margin when fitonpage is true.

🔧 Proposed fix
-			if (($y + $h) > $this->PageBreakTrigger + $this->bMargin) {
-				$h = $this->PageBreakTrigger + $this->bMargin - $y;
+			if (($y + $h) > $this->PageBreakTrigger) {
+				$h = $this->PageBreakTrigger - $y;
 				$w = ($h * $ratio_wh);
 			}
#!/bin/bash
set -euo pipefail

FILE="vendor/tecnickcom/tcpdf/tcpdf.php"

echo "[local fitBlock clamp]"
rg -n -C3 'protected function fitBlock|PageBreakTrigger \+ \$this->bMargin|PageBreakTrigger - \$y' "$FILE"

echo
echo "[upstream 6.11.0 fitBlock clamp]"
curl -fsSL https://raw.githubusercontent.com/tecnickcom/TCPDF/6.11.0/tcpdf.php \
  | rg -n -C3 'protected function fitBlock|\(\$y \+ \$h\) > \$this->PageBreakTrigger(\s*\+\s*\$this->bMargin)?'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/tcpdf.php` around lines 6925 - 6927, The fitBlock()
bottom-clamp currently uses $this->PageBreakTrigger + $this->bMargin which can
push content into the footer margin when fitonpage is true; change the clamp so
$h is calculated as $this->PageBreakTrigger - $y (i.e. $h =
$this->PageBreakTrigger - $y) and then recompute $w using the existing $ratio_wh
logic (variables: fitBlock, PageBreakTrigger, bMargin, fitonpage, $y, $h, $w,
$ratio_wh).
🧹 Nitpick comments (6)
app/Models/BookRepository.php (1)

345-366: Extract shared metadata normalization to reduce create/update drift.

These new blocks are mirrored in both methods; extracting one shared normalizer/mapping would make future field changes safer.

♻️ Refactor sketch
-        if ($this->hasColumn('lingua')) {
-            $addField('lingua', 's', $data['lingua'] ?? null);
-        }
-        if ($this->hasColumn('anno_pubblicazione')) {
-            $annoRaw = $data['anno_pubblicazione'] ?? null;
-            $anno = filter_var($annoRaw, FILTER_VALIDATE_INT);
-            $addField('anno_pubblicazione', 'i', $anno === false ? null : $anno);
-        }
-        if ($this->hasColumn('edizione')) {
-            $addField('edizione', 's', $data['edizione'] ?? null);
-        }
-        if ($this->hasColumn('traduttore')) {
-            $addField('traduttore', 's', $data['traduttore'] ?? null);
-        }
-        if ($this->hasColumn('illustratore')) {
-            $addField('illustratore', 's', $data['illustratore'] ?? null);
-        }
-        if ($this->hasColumn('numero_pagine')) {
-            $numPagineRaw = $data['numero_pagine'] ?? null;
-            $numPagine = filter_var($numPagineRaw, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
-            $addField('numero_pagine', 'i', $numPagine === false ? null : $numPagine);
-        }
+        foreach ($this->normalizedBookMeta($data) as [$column, $type, $value]) {
+            if ($this->hasColumn($column)) {
+                $addField($column, $type, $value);
+            }
+        }
+    /**
+     * `@return` array<int, array{0:string,1:string,2:mixed}>
+     */
+    private function normalizedBookMeta(array $data): array
+    {
+        $annoRaw = $data['anno_pubblicazione'] ?? null;
+        $anno = filter_var($annoRaw, FILTER_VALIDATE_INT);
+        $numPagineRaw = $data['numero_pagine'] ?? null;
+        $numPagine = filter_var($numPagineRaw, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
+
+        return [
+            ['lingua', 's', $data['lingua'] ?? null],
+            ['anno_pubblicazione', 'i', $anno === false ? null : $anno],
+            ['edizione', 's', $data['edizione'] ?? null],
+            ['traduttore', 's', $data['traduttore'] ?? null],
+            ['illustratore', 's', $data['illustratore'] ?? null],
+            ['numero_pagine', 'i', $numPagine === false ? null : $numPagine],
+        ];
+    }

Also applies to: 671-692

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/BookRepository.php` around lines 345 - 366, The repeated
field-normalization block (using hasColumn, addField, and validations for
anno_pubblicazione and numero_pagine) should be extracted into a single private
helper (e.g., normalizeBookMetadata or prepareBookFields) that accepts the
incoming $data and either returns a normalized map of field => [type, value] or
accepts the $addField callable to invoke directly; move the filter_var logic for
anno_pubblicazione and numero_pagine into that helper (preserve min_range
validation), include the string defaults for
lingua/edizione/traduttore/illustratore, and then call this helper from both
create and update to iterate and apply addField so future field changes stay in
one place.
tests/full-test.spec.js (1)

49-49: Clarify the SQL-safety claim in the helper comment.

Line 49 says “injection-safe”, but the file still builds many SQL statements via interpolation. This helper is shell-safe, not SQL-injection-safe.

✏️ Suggested wording
-/** Execute a MySQL query and return trimmed output (injection-safe, no shell). */
+/** Execute a MySQL query and return trimmed output (shell-safe; caller must escape SQL values). */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/full-test.spec.js` at line 49, Update the helper's comment to remove
the misleading "injection-safe" claim and clarify that it is safe from shell
injection but not safe from SQL injection; replace the phrase in the JSDoc on
the helper in tests/full-test.spec.js (the comment that begins "/** Execute a
MySQL query and return trimmed output (injection-safe, no shell). */") with
wording such as "shell-safe (does not use the shell), but not SQL-injection-safe
— callers must use parameterized queries or proper escaping when constructing
SQL."
vendor/slim/csrf/composer.json (1)

39-50: Move CI/test scripts from vendored package to root composer.json.

Lines 39-50 add sniffer and phpunit scripts directly to vendor/slim/csrf/composer.json. These edits will be lost when the slim/csrf dependency is updated or reinstalled. Move these scripts to the repository root composer.json instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/slim/csrf/composer.json` around lines 39 - 50, The composer.json in
the vendored package incorrectly adds project CI/test scripts ("sniffer:check",
"sniffer:fix", "test", "check", "test:coverage"); remove these script entries
from vendor/slim/csrf/composer.json and instead add the same script definitions
to your repository root composer.json "scripts" section so they persist across
dependency updates; update any script references (e.g., "@sniffer:check",
"@test:coverage") in the root file as needed and run composer dump-autoload /
composer update locally to ensure the root scripts are registered.
app/Support/Updater.php (1)

748-751: Centralize retry headers to avoid drift across retry paths.

The unauthenticated retry header arrays are duplicated in multiple places. Consider deriving them via a single helper (e.g., getGitHubHeaders(...) with an explicit withAuth flag) to keep User-Agent/Accept behavior consistent over time.

Also applies to: 948-951

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 748 - 751, Duplicate unauthenticated
retry header arrays ($retryHeaders) are causing drift; create a single helper
(e.g., getGitHubHeaders(bool $withAuth = false, ?string $token = null)) in
Updater.php that returns the canonical array including 'User-Agent' and
'Accept', and conditionally adds an Authorization header when $withAuth is true,
then replace the inline $retryHeaders definitions (the occurrences around the
current block and the ones at the later occurrence) with calls to
getGitHubHeaders(...) so all retry paths use the same header set.
app/Controllers/FrontendController.php (1)

1299-1322: Consider replacing per-level descendant queries with a single tree load + shared traversal helper

Current BFS performs one SQL query per depth level and duplicates traversal logic already present in collectGenreTreeIds(). A single SELECT id,parent_id FROM generi + in-memory traversal would reduce DB round-trips and keep one traversal implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/FrontendController.php` around lines 1299 - 1322, The
per-level BFS SQL loop using repeated prepares (variables $queue, $visited and
the descendant query) should be replaced by a single SELECT id,parent_id FROM
generi to load the tree into memory and then perform the traversal in PHP
(reusing or delegating to the existing collectGenreTreeIds() helper) to collect
the requested genre plus all descendants; remove the repeated prepare/execute
block and instead build an adjacency map from the single result set and run a
local BFS/DFS to populate $visited (or return the id list) so you eliminate
multiple DB round-trips and centralize traversal logic.
app/Routes/web.php (1)

257-282: Hybrid profile-update routes are now harder to scale with dynamic locales.

The if/elseif per-locale block will require code changes for each new language. Consider centralizing hybrid aliases in a small map to keep this maintainable.

♻️ Suggested refactor
-        if ($locale === 'it_IT') {
-            // Italian base + English action
-            $registerRouteIfUnique('POST', '/profilo/update', function (...) { ... }, [...]);
-        } elseif ($locale === 'en_US') {
-            // English base + Italian action
-            $registerRouteIfUnique('POST', '/profile/aggiorna', function (...) { ... }, [...]);
-        } elseif ($locale === 'de_DE') {
-            // German base + English/Italian action
-            $registerRouteIfUnique('POST', '/profil/update', function (...) { ... }, [...]);
-            $registerRouteIfUnique('POST', '/profil/aggiorna', function (...) { ... }, [...]);
-        }
+        $profileHybridRoutes = [
+            'it_IT' => ['/profilo/update'],
+            'en_US' => ['/profile/aggiorna'],
+            'de_DE' => ['/profil/update', '/profil/aggiorna'],
+        ];
+        foreach ($profileHybridRoutes[$locale] ?? [] as $hybridRoute) {
+            $registerRouteIfUnique('POST', $hybridRoute, function ($request, $response) use ($app) {
+                $db = $app->getContainer()->get('db');
+                $controller = new ProfileController();
+                return $controller->update($request, $response, $db);
+            }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Routes/web.php` around lines 257 - 282, The current per-locale if/elseif
block around $locale with multiple calls to $registerRouteIfUnique for
ProfileController->update duplicates middleware and is unscalable; replace it by
defining a centralized map of locale => array of route aliases (e.g.,
$routeAliasesByLocale) and iterate over that map to call $registerRouteIfUnique
for each alias, reusing the same middleware array [new CsrfMiddleware(), new
AuthMiddleware(['admin','staff','standard','premium'])] and the same controller
instantiation (ProfileController) and $db retrieval, so adding a new language
only requires updating the map instead of editing the if/elseif logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Controllers/LibriController.php`:
- Around line 640-642: The BookRepository::updateOptionals method currently
accepts raw $data which allows invalid numero_pagine through; either
validate/sanitize numero_pagine inside updateOptionals (apply the same
filter_var(..., FILTER_VALIDATE_INT, ['options'=>['min_range'=>1]]) and set null
on invalid before casting) or change LibriController to pass the
already-validated $fields array to updateOptionals instead of raw request data;
updateOptionals should enforce the min_range constraint on numero_pagine (and
mirror the controller’s validation logic) so callers cannot bypass it.

In `@app/Views/partials/scroll-to-top.php`:
- Around line 25-37: The scroll visibility logic currently only runs on the
scroll event so when the page loads already scrolled past 400px the button (btn)
remains hidden; extract or replicate the same logic that computes var show =
window.scrollY > 400 and updates visible, btn.style.opacity,
btn.style.pointerEvents, btn.setAttribute('aria-hidden', ...) and btn.tabIndex,
and invoke that update once immediately after setup (either by extracting it
into an updateVisibility function and calling it, or by duplicating the logic
right after window.addEventListener) so the initial state reflects the current
scroll position.

In `@scripts/manual-upgrade.php`:
- Around line 291-292: The POST upgrade handler begins privileged state changes
when $authenticated && $requestMethod === 'POST' && isset($_FILES['zipfile'])
without CSRF protection; add CSRF validation by requiring a server-stored token
(e.g., in $_SESSION['csrf_token']) and verifying it against a POSTed token
(e.g., $_POST['csrf_token']) before proceeding with the upgrade logic in that
block, and on mismatch reject the request, log the event (append to $log), and
do not process the uploaded file; ensure the token is generated and embedded in
the upgrade form output so legitimate requests include it.

---

Outside diff comments:
In `@app/Support/Updater.php`:
- Around line 994-1026: The download logic in Updater.php currently leaves error
bodies from file_get_contents (with ignore_errors=true) to proceed; after each
file_get_contents call (the initial fetch that sets $fileContent and the retry
branch that reassigns $fileContent after clearing $this->githubToken), inspect
the HTTP status from $http_response_header (as parsed into $dlStatus) and treat
any non-2xx status as a failure: set $fileContent to false or throw/return an
error so ZIP validation is not attempted on an HTML/JSON error payload. Ensure
this check uses the same parsing logic (preg_match on $http_response_header[0]
to derive $dlStatus) and add a debugLog call (use $this->debugLog) with the
headers and status when rejecting the response; preserve resetting
$this->githubToken after the retry by keeping the existing try/finally.

In `@installer/classes/Installer.php`:
- Around line 1023-1036: The bracketed IPv6 branch in Installer:: (the
preg_match('/^\[(.+)\](?::(\d+))?$/', $host, $matches)) handler sets $host and
$port to null when no inline port is present, but uses elseif so the subsequent
HTTP_X_FORWARDED_PORT / SERVER_PORT checks are skipped; adjust the logic so that
after handling a bracketed IPv6 literal you still populate $port from
$_SERVER['HTTP_X_FORWARDED_PORT'] or $_SERVER['SERVER_PORT'] when an inline port
was not found (either by converting the elseif chain into separate ifs or by
only setting $port when $matches[2] exists and leaving $port unset otherwise),
ensuring $host remains '[...]' and $port is derived from forwarded/server values
when needed.

In `@vendor/slim/psr7/composer.json`:
- Around line 1-73: You edited the upstream dependency slim/psr7's composer.json
directly in vendor, which bypasses Composer and will break future installs;
revert that file and instead manage changes via Composer. Restore
vendor/slim/psr7/composer.json to the original upstream state (undo changes to
keys like "require", "provide", "autoload", etc.), then implement your intended
modifications by adding a path repository or a forked package entry in the root
composer.json and update composer.json to require your fork/path for the
"slim/psr7" package so Composer manages the changed package correctly.

---

Duplicate comments:
In `@vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php`:
- Around line 1381-1387: The state machine in verifyCandidateSignature iterates
tokens and switches from 'params' to 'body' on any ')' token, which misparses
nested parentheses in parameter defaults (e.g., fn($a = foo(), $b)); update the
logic in ReflectionClosure::verifyCandidateSignature to track a depth counter
while in the 'params' state (increment on '(' and decrement on ')') and only
transition to 'body' when you see a ')' and depth is zero, ensuring nested
parentheses do not prematurely end parameter parsing; adjust any
variables/conditions around the foreach over $tokens to use this $depth check
and initialize/reset $depth appropriately.

In `@vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php`:
- Line 310: The code in Monolog\Formatter\LineFormatter currently normalizes all
backslashes in the $trace string (strtr($trace, DIRECTORY_SEPARATOR, '/')),
which also converts PHP namespace separators and harms readability; update the
logic in Monolog\Formatter\LineFormatter (the stacktrace formatting code that
builds $trace) to stop running a global strtr on the whole stacktrace and
instead normalize directory separators only inside actual file path
substrings—e.g., use a preg_replace_callback to find file path patterns (Windows
drive paths or UNC paths and the file:line fragments produced by backtraces) and
replace backslashes with '/' only within those matches, leaving namespace
backslashes untouched.

In `@vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php`:
- Around line 151-153: The early return after flock($handle, LOCK_EX) failing
leaks the open resource; update the code in DeduplicationHandler (the block
using $handle and flock) to call fclose($handle) before returning on lock
failure (i.e., replace "if (false === flock($handle, LOCK_EX)) { return; }" with
a branch that calls fclose($handle) then returns) so the file handle is always
closed when the function exits early.

In `@vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php`:
- Around line 48-54: The constructor sets $this->nextRotation by calling
getNextRotation() before assigning $this->timezone, so getNextRotation() uses
the server default timezone and can disagree with timezone-aware filename
generation; move the assignment of $this->timezone (and any timezone-dependent
setup) before calling $this->getNextRotation() (affecting the __construct in
RotatingFileHandler and the same pattern around lines 226-232), or modify
getNextRotation() to accept a timezone parameter and pass $this->timezone to it
after setting it; ensure calls to setFilenameFormat(...) remain unchanged so
filenames and rotation cutoff use the same timezone.

In `@vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php`:
- Around line 137-142: The inode-rotation path sets the instance-level flag
$this->retrying (in methods like hasUrlInodeWasChanged()/the inode-change block)
and then calls $this->write($record) recursively, which prevents the
write-failure retry branch from executing; update the inode-rotation flow (the
block that calls close() then write($record)) to clear or localize the retry
state before the recursive call (e.g., save $prevRetry = $this->retrying;
$this->retrying = false; $this->close(); $this->write($record); $this->retrying
= $prevRetry) so that write() can independently perform its one-time retry on
failure; apply the same change where retrying is set in the inode-change
handling to avoid coupling the two paths.

In `@vendor/tecnickcom/tcpdf/config/tcpdf_config.php`:
- Line 11: The file has corrupted header text and a malformed PDF header string;
restore the original header comment to "Copyright (C) 2004-2014 Nicola Asuni -
Tecnick.com LTD" (fix the corrupted year/text around the author) and update the
PDF_HEADER_STRING constant (symbol PDF_HEADER_STRING) to remove the stray "2026"
so it reads the intended phrase (e.g., "by Tecnick.com LTD" or simply "by").
Locate the header comment and the PDF_HEADER_STRING definition in
tcpdf_config.php and replace the malformed values with the correct original
texts.

In `@vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php`:
- Line 10: The copyright header contains a malformed year token "2010-2014
2026"; locate that exact string in
vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php and replace it with a
valid format such as "2010-2014, 2026" (or a single continuous range like
"2010-2026" if that's intended) so the year range/token is syntactically
correct.

In `@vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php`:
- Line 10: Update the malformed copyright header string "2010-2013 2026" in the
file's top copyright comment to a valid format (for example "2010-2013, 2026")
so the year range is correctly punctuated; locate the copyright comment
containing "2010-2013 2026" and replace it with the corrected text.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php`:
- Line 1387: The checkSumAdjustment is currently written at the wrong offset in
the head table: update the write location so the packed 4-byte
checkSumAdjustment is inserted at +8..+12 instead of +4..+8; specifically modify
the expression that builds $font (the line using substr($font, 0,
$table['head']['offset'] + $offset + 4).pack('N',
$checkSumAdjustment).substr(...)) to use +8 (i.e. $table['head']['offset'] +
$offset + 8) for the start of the pack insertion so the packed value lands at
bytes +8..+12 relative to the head table.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_images.php`:
- Line 277: Replace the repeated typo in the PNG diagnostic comments where the
message reads "Unknownn compression method" to the correct spelling "Unknown
compression method"; locate the occurrences of that string in tcpdf_images.php
(the PNG diagnostics branch handled in the image parsing logic) and update the
comment/text at each occurrence (previously at the lines containing "Unknownn
compression method") to the corrected form to ensure consistency.

In `@vendor/tecnickcom/tcpdf/tcpdf.php`:
- Around line 2909-2914: The setCompression method currently unconditionally
sets $this->compress = false and returns, making the rest of the function dead;
remove the early unconditional assignment and return so the method honors the
$compress parameter and the gzcompress availability check, i.e., in function
setCompression(...) use function_exists('gzcompress') and then set
$this->compress based on the incoming $compress value (and pdfa_mode if needed)
rather than always forcing false and returning early.
- Around line 23733-23737: The preg_split call that populates $rawparams
currently uses a capturing group, causing delimiters to be kept and skew
parameter positions; change the split to avoid capturing (use a non-capturing
group) or pass the PREG_SPLIT_NO_EMPTY flag so delimiters are not returned, then
remove the array_filter cleanup; update the preg_split invocation that fills
$rawparams (and subsequent handling of $rawparams) to rely on the cleaned tokens
only so numeric parameter indexing is correct.
- Around line 6925-6927: The fitBlock() bottom-clamp currently uses
$this->PageBreakTrigger + $this->bMargin which can push content into the footer
margin when fitonpage is true; change the clamp so $h is calculated as
$this->PageBreakTrigger - $y (i.e. $h = $this->PageBreakTrigger - $y) and then
recompute $w using the existing $ratio_wh logic (variables: fitBlock,
PageBreakTrigger, bMargin, fitonpage, $y, $h, $w, $ratio_wh).

---

Nitpick comments:
In `@app/Controllers/FrontendController.php`:
- Around line 1299-1322: The per-level BFS SQL loop using repeated prepares
(variables $queue, $visited and the descendant query) should be replaced by a
single SELECT id,parent_id FROM generi to load the tree into memory and then
perform the traversal in PHP (reusing or delegating to the existing
collectGenreTreeIds() helper) to collect the requested genre plus all
descendants; remove the repeated prepare/execute block and instead build an
adjacency map from the single result set and run a local BFS/DFS to populate
$visited (or return the id list) so you eliminate multiple DB round-trips and
centralize traversal logic.

In `@app/Models/BookRepository.php`:
- Around line 345-366: The repeated field-normalization block (using hasColumn,
addField, and validations for anno_pubblicazione and numero_pagine) should be
extracted into a single private helper (e.g., normalizeBookMetadata or
prepareBookFields) that accepts the incoming $data and either returns a
normalized map of field => [type, value] or accepts the $addField callable to
invoke directly; move the filter_var logic for anno_pubblicazione and
numero_pagine into that helper (preserve min_range validation), include the
string defaults for lingua/edizione/traduttore/illustratore, and then call this
helper from both create and update to iterate and apply addField so future field
changes stay in one place.

In `@app/Routes/web.php`:
- Around line 257-282: The current per-locale if/elseif block around $locale
with multiple calls to $registerRouteIfUnique for ProfileController->update
duplicates middleware and is unscalable; replace it by defining a centralized
map of locale => array of route aliases (e.g., $routeAliasesByLocale) and
iterate over that map to call $registerRouteIfUnique for each alias, reusing the
same middleware array [new CsrfMiddleware(), new
AuthMiddleware(['admin','staff','standard','premium'])] and the same controller
instantiation (ProfileController) and $db retrieval, so adding a new language
only requires updating the map instead of editing the if/elseif logic.

In `@app/Support/Updater.php`:
- Around line 748-751: Duplicate unauthenticated retry header arrays
($retryHeaders) are causing drift; create a single helper (e.g.,
getGitHubHeaders(bool $withAuth = false, ?string $token = null)) in Updater.php
that returns the canonical array including 'User-Agent' and 'Accept', and
conditionally adds an Authorization header when $withAuth is true, then replace
the inline $retryHeaders definitions (the occurrences around the current block
and the ones at the later occurrence) with calls to getGitHubHeaders(...) so all
retry paths use the same header set.

In `@tests/full-test.spec.js`:
- Line 49: Update the helper's comment to remove the misleading "injection-safe"
claim and clarify that it is safe from shell injection but not safe from SQL
injection; replace the phrase in the JSDoc on the helper in
tests/full-test.spec.js (the comment that begins "/** Execute a MySQL query and
return trimmed output (injection-safe, no shell). */") with wording such as
"shell-safe (does not use the shell), but not SQL-injection-safe — callers must
use parameterized queries or proper escaping when constructing SQL."

In `@vendor/slim/csrf/composer.json`:
- Around line 39-50: The composer.json in the vendored package incorrectly adds
project CI/test scripts ("sniffer:check", "sniffer:fix", "test", "check",
"test:coverage"); remove these script entries from
vendor/slim/csrf/composer.json and instead add the same script definitions to
your repository root composer.json "scripts" section so they persist across
dependency updates; update any script references (e.g., "@sniffer:check",
"@test:coverage") in the root file as needed and run composer dump-autoload /
composer update locally to ensure the root scripts are registered.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and e976cb1.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (110)
  • .htaccess
  • README.md
  • app/Controllers/FrontendController.php
  • app/Controllers/LibriController.php
  • app/Controllers/UpdateController.php
  • app/Models/BookRepository.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • app/Views/autori/index.php
  • app/Views/editori/index.php
  • app/Views/events/index.php
  • app/Views/frontend/event-detail.php
  • app/Views/frontend/events.php
  • app/Views/frontend/home-sections/events.php
  • app/Views/frontend/home-sections/genre_carousel.php
  • app/Views/frontend/home.php
  • app/Views/frontend/layout.php
  • app/Views/layout.php
  • app/Views/libri/index.php
  • app/Views/libri/partials/book_form.php
  • app/Views/libri/scheda_libro.php
  • app/Views/partials/scroll-to-top.php
  • app/Views/prestiti/index.php
  • app/Views/utenti/index.php
  • frontend/js/vendor.js
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/database/data_en_US.sql
  • installer/database/data_it_IT.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • public/assets/main.css
  • public/assets/vendor.bundle.js
  • scripts/.user.ini
  • scripts/create-release.sh
  • scripts/manual-update.php
  • scripts/manual-upgrade.php
  • tests/full-test.spec.js
  • tests/genre-bugs.spec.js
  • vendor/composer/autoload_classmap.php
  • vendor/composer/autoload_files.php
  • vendor/composer/autoload_static.php
  • vendor/composer/installed.json
  • vendor/composer/installed.php
  • vendor/graham-campbell/result-type/composer.json
  • vendor/laravel/serializable-closure/composer.json
  • vendor/laravel/serializable-closure/src/Serializers/Native.php
  • vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php
  • vendor/monolog/monolog/README.md
  • vendor/monolog/monolog/composer.json
  • vendor/monolog/monolog/src/Monolog/Formatter/GelfMessageFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php
  • vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php
  • vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/Handler.php
  • vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php
  • vendor/slim/csrf/composer.json
  • vendor/slim/psr7/composer.json
  • vendor/slim/psr7/src/Cookies.php
  • vendor/slim/slim/MAINTAINERS.md
  • vendor/slim/slim/Slim/App.php
  • vendor/slim/slim/composer.json
  • vendor/slim/slim/psalm.xml
  • vendor/tecnickcom/tcpdf/CHANGELOG.TXT
  • vendor/tecnickcom/tcpdf/LICENSE.TXT
  • vendor/tecnickcom/tcpdf/README.md
  • vendor/tecnickcom/tcpdf/VERSION
  • vendor/tecnickcom/tcpdf/composer.json
  • vendor/tecnickcom/tcpdf/config/tcpdf_config.php
  • vendor/tecnickcom/tcpdf/fonts/ae_fonts_2.0/README
  • vendor/tecnickcom/tcpdf/fonts/freefont-20100919/COPYING
  • vendor/tecnickcom/tcpdf/fonts/freefont-20100919/CREDITS
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/COPYING
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/CREDITS
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/README
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/TROUBLESHOOTING
  • vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php
  • vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php
  • vendor/tecnickcom/tcpdf/include/barcodes/qrcode.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_colors.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_filters.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_font_data.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_images.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_static.php
  • vendor/tecnickcom/tcpdf/tcpdf.php
  • vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php
  • vendor/tecnickcom/tcpdf/tcpdf_barcodes_1d.php
  • vendor/tecnickcom/tcpdf/tcpdf_barcodes_2d.php
  • vendor/tecnickcom/tcpdf/tools/tcpdf_addfont.php
  • vendor/vlucas/phpdotenv/composer.json
  • vendor/vlucas/phpdotenv/src/Parser/EntryParser.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php
💤 Files with no reviewable changes (7)
  • vendor/vlucas/phpdotenv/src/Parser/EntryParser.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php
  • scripts/manual-update.php
  • vendor/slim/slim/MAINTAINERS.md
  • vendor/slim/slim/psalm.xml

- Updater: treat non-2xx file_get_contents download as failure
- Installer: fix IPv6 port fallback (separate if block for forwarded/server port)
- BookRepository: validate numero_pagine in updateOptionals with filter_var
- scroll-to-top: extract updateVisibility function and call on page load
- manual-upgrade: add CSRF token protection for upgrade POST
- tests: fix dbQuery comment (shell-safe, not injection-safe)
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php (1)

1-280: ⚠️ Potential issue | 🔴 Critical

Do not modify vendor files directly.

This file belongs to the monolog/monolog Composer package. Direct modifications to vendor files:

  1. Will be overwritten on the next composer update or composer install
  2. Break the dependency management workflow and reproducibility
  3. Are unrelated to this PR's stated objectives (German locale, dynamic routes, GitHub token, manual upgrade)

If log rotation inode detection is needed, create a custom handler that extends StreamHandler in your application code instead:

// app/Support/Logging/RotationAwareStreamHandler.php
namespace App\Support\Logging;

use Monolog\Handler\StreamHandler;
use Monolog\LogRecord;

class RotationAwareStreamHandler extends StreamHandler
{
    private ?int $cachedInode = null;
    
    protected function write(LogRecord $record): void
    {
        // Custom inode detection logic here
        parent::write($record);
    }
}

Alternatively, consider using Monolog's built-in RotatingFileHandler which handles log rotation natively.

Please revert this vendor file modification and implement the functionality properly in application code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php` around lines 1
- 280, The PR directly modified a vendor file (Monolog\Handler\StreamHandler)
which must not be changed; revert the vendor edits and implement the inode-aware
behavior in app code instead. Revert changes to StreamHandler in vendor, then
add a new class (e.g. App\Support\Logging\RotationAwareStreamHandler) that
extends Monolog\Handler\StreamHandler and override the protected write(LogRecord
$record) method to implement your log-rotation/inode detection using or
delegating to the existing private helpers (mirror behavior of getInodeFromUrl
and hasUrlInodeWasChanged logic in your subclass) or simply switch to
Monolog\Handler\RotatingFileHandler if that meets requirements; ensure all
references use the new RotationAwareStreamHandler in your logging config.
vendor/slim/psr7/composer.json (1)

1-73: ⚠️ Potential issue | 🔴 Critical

Remove the entire vendor/ directory from git tracking.

The vendor directory is currently being tracked by git (2,400+ files), which violates standard dependency management practices. Update .gitignore to include:

/vendor/

The current .gitignore only excludes specific files within vendor directories but does not prevent vendor files from being tracked. Vendor files should be regenerated locally via composer install based on composer.lock, not committed to the repository.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/slim/psr7/composer.json` around lines 1 - 73, The repo is tracking the
vendor/ directory; stop tracking it and add a gitignore rule. Add "/vendor/" to
.gitignore, remove the tracked files from the index (git rm -r --cached vendor)
and commit that change so vendor/ is no longer in git history going forward, and
ensure consumers regenerate dependencies with composer install using
composer.lock and composer.json (refer to composer.json in the diff to confirm
dependency management).
vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php (1)

226-233: ⚠️ Potential issue | 🟠 Major

getNextRotation() ignores the configured timezone.

getTimedFilename() correctly uses $this->timezone (line 183), but getNextRotation() creates DateTimeImmutable objects without passing the timezone. This means rotation boundaries are computed in the server's default timezone while filenames use the configured timezone, leading to incorrect rotation timing.

Suggested fix
     protected function getNextRotation(): \DateTimeImmutable
     {
+        $now = new \DateTimeImmutable('now', $this->timezone);
         return match (str_replace(['/','_','.'], '-', $this->dateFormat)) {
-            self::FILE_PER_MONTH => (new \DateTimeImmutable('first day of next month'))->setTime(0, 0, 0),
-            self::FILE_PER_YEAR => (new \DateTimeImmutable('first day of January next year'))->setTime(0, 0, 0),
-            default => (new \DateTimeImmutable('tomorrow'))->setTime(0, 0, 0),
+            self::FILE_PER_MONTH => $now->modify('first day of next month')->setTime(0, 0, 0),
+            self::FILE_PER_YEAR => $now->modify('first day of January next year')->setTime(0, 0, 0),
+            default => $now->modify('tomorrow')->setTime(0, 0, 0),
         };
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php` around
lines 226 - 233, getNextRotation() builds DateTimeImmutable instances without
using the configured timezone, causing rotation times to differ from
getTimedFilename(); update getNextRotation() to construct the DateTimeImmutable
objects with $this->timezone (e.g. new \DateTimeImmutable('first day of next
month', $this->timezone)) before calling setTime so FILE_PER_MONTH,
FILE_PER_YEAR and default branch use the same timezone as getTimedFilename().
app/Views/frontend/events.php (1)

360-380: ⚠️ Potential issue | 🟠 Major

Use localized route helper for event links instead of hardcoded /events/.

Line 360, Line 374, and Line 379 hardcode the path and directly concatenate the slug. This bypasses translated routes and can break locale-specific URLs.

🔧 Proposed fix
-                        <a href="<?= htmlspecialchars(url('/events/' . $event['slug']), ENT_QUOTES, 'UTF-8') ?>" class="event-card__thumb">
+                        <a href="<?= htmlspecialchars(route_path('events') . '/' . rawurlencode($event['slug']), ENT_QUOTES, 'UTF-8') ?>" class="event-card__thumb">
@@
-                                <a href="<?= htmlspecialchars(url('/events/' . $event['slug']), ENT_QUOTES, 'UTF-8') ?>">
+                                <a href="<?= htmlspecialchars(route_path('events') . '/' . rawurlencode($event['slug']), ENT_QUOTES, 'UTF-8') ?>">
@@
-                                <a href="<?= htmlspecialchars(url('/events/' . $event['slug']), ENT_QUOTES, 'UTF-8') ?>" class="event-card__button">
+                                <a href="<?= htmlspecialchars(route_path('events') . '/' . rawurlencode($event['slug']), ENT_QUOTES, 'UTF-8') ?>" class="event-card__button">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/frontend/events.php` around lines 360 - 380, The links currently
build URLs by concatenating '/events/' with $event['slug'] using url('/events/'
. $event['slug']) which bypasses translated/localized routes; replace those
occurrences (the three url(...) calls used for the thumbnail link, title link,
and action button) with your app's localized/named route helper (e.g.,
route('events.show', ['slug' => $event['slug']]) or the project-specific
localized_route equivalent) so locale-aware URLs are generated while keeping
HtmlHelper::e(...) and the translation call __("Scopri l'evento") intact.
app/Views/layout.php (1)

1491-1535: ⚠️ Potential issue | 🟡 Minor

Reset pending g combo state on Escape.

After pressing g, hitting Escape does not clear gPrefixActive; a subsequent key can still trigger navigation. Line 1492 branch should reset the combo state.

🩹 Proposed fix
         if (e.key === 'Escape') {
+          gPrefixActive = false;
+          clearTimeout(gPrefixTimer);
+
           // Close shortcuts modal
           closeShortcutsModal();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/layout.php` around lines 1491 - 1535, The Escape key handler fails
to reset the pending 'g' combo state, so update the block handling e.key ===
'Escape' to clear that state: set gPrefixActive = false, clear and null-out any
timer variable used for the combo (e.g., gPrefixTimer) and remove any UI
indicator tied to the prefix if present (e.g., a CSS class or element shown for
the 'g' prefix). Locate the variables/handlers named gPrefixActive and
gPrefixTimer (or similar) in the surrounding code, call
clearTimeout(gPrefixTimer) if defined, set gPrefixTimer = null, and ensure
gPrefixActive = false inside the Escape branch so subsequent keys won't trigger
navigation.
♻️ Duplicate comments (11)
vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php (1)

46-49: ⚠️ Potential issue | 🟠 Major

Revert direct edits under vendor/ and keep Monolog Composer-managed.

Line 46 and Line 49 are edits inside a vendored dependency (monolog/monolog). These changes are not durable and will be overwritten by composer install/update. Please revert this file in-repo and apply the change via either an upstream PR or a Composer patch workflow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php` around lines
46 - 49, This file shows local edits to the vendored Monolog class
ProcessHandler (e.g., the private property $timeout and nearby docblock changes)
which must not be modified directly; revert any changes made under
vendor/monolog (restore the original committed vendor file or run composer
install to reset it) and instead implement the intended fix by creating an
upstream PR to monolog/monolog or by applying the change via a Composer patch
(e.g., using cweagans/composer-patches) so the modification to ProcessHandler is
durable and managed by Composer.
app/Models/BookRepository.php (1)

965-967: ⚠️ Potential issue | 🟠 Major

Validate anno_pubblicazione in updateOptionals() before integer cast.

anno_pubblicazione still enters $cols as raw input and is later cast with (int), so invalid non-empty values (e.g. "abc") can become 0 silently.

🔧 Proposed fix
         foreach (['numero_pagine', 'ean', 'data_pubblicazione', 'anno_pubblicazione', 'traduttore', 'illustratore', 'collana', 'edizione'] as $c) {
             if ($this->hasColumn($c) && array_key_exists($c, $data) && $data[$c] !== '' && $data[$c] !== null) {
                 if ($c === 'numero_pagine') {
                     $validated = filter_var($data[$c], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
                     if ($validated !== false) {
                         $cols[$c] = $validated;
                     }
+                } elseif ($c === 'anno_pubblicazione') {
+                    $validated = filter_var($data[$c], FILTER_VALIDATE_INT);
+                    if ($validated !== false) {
+                        $cols[$c] = $validated;
+                    }
                 } else {
                     $cols[$c] = $data[$c];
                 }
             }
         }

Also applies to: 999-1002

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/BookRepository.php` around lines 965 - 967, In updateOptionals()
validate the anno_pubblicazione input before it is placed into $cols to avoid
non-numeric strings being silently cast to 0; specifically, before assigning
$cols['anno_pubblicazione'] (or importing via the generic $cols[$c] = $data[$c]
branch) check that $data['anno_pubblicazione'] is a valid integer string (e.g.
ctype_digit or filter_var with FILTER_VALIDATE_INT and optional range check) and
only set $cols['anno_pubblicazione'] = (int)$data['anno_pubblicazione'] when
valid, otherwise skip setting or set null/error; apply the same validation fixes
to the similar handling around the other block (lines 999-1002) that also casts
anno_pubblicazione.
vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php (1)

1363-1387: ⚠️ Potential issue | 🟠 Major

verifyCandidateSignature() still misparses nested parentheses in parameter parsing.

At Line 1385, any ) while in params flips to body. Nested parentheses inside parameter expressions can prematurely end parameter parsing and break candidate matching.

🐛 Proposed fix
 protected function verifyCandidateSignature($candidate)
 {
     $code = $candidate['code'];
     $use = $candidate['use'];
     $isShortClosure = $candidate['isShortClosure'];
@@
     $tokens = token_get_all('<?php '.$code);
     $params = [];
     $vars = [];
     $state = 'start';
+    $depth = 0;
 
     foreach ($tokens as $token) {
         if (! is_array($token)) {
-            if ($token === '(' && $state === 'start') {
-                $state = 'params';
-            } elseif ($token === ')' && $state === 'params') {
-                $state = 'body';
+            if ($token === '(') {
+                if ($state === 'start') {
+                    $state = 'params';
+                    $depth = 1;
+                } elseif ($state === 'params') {
+                    $depth++;
+                }
+            } elseif ($token === ')' && $state === 'params') {
+                $depth--;
+                if ($depth === 0) {
+                    $state = 'body';
+                }
             }
 
             continue;
         }
#!/bin/bash
set -euo pipefail

file="vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php"

echo "=== verifyCandidateSignature token-state block ==="
nl -ba "$file" | sed -n '1368,1402p'

echo
echo "=== Direct params->body transition on ')' ==="
rg -n "\$token === '\)' && \$state === 'params'|\$state = 'body'" "$file"

echo
echo "=== Depth tracking variables in verifyCandidateSignature ==="
if rg -n "\$depth|\$parenDepth|\$parenthesisDepth" "$file"; then
  echo "Depth tracking present."
else
  echo "No depth tracking variable found."
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php` around
lines 1363 - 1387, The parameter parser in verifyCandidateSignature incorrectly
transitions from params to body on any ')' token, which breaks on nested
parentheses; fix it by introducing a parentheses depth counter (e.g.,
$parenDepth) inside the foreach over $tokens and increment when encountering '('
and decrement on ')' while in the 'params' state, only flip $state to 'body'
when $state === 'params' AND $parenDepth returns to zero (handle non-array
tokens and ignore paren changes outside 'params'); update references in the
tokens loop and any logic that tests $state so nested parameter expressions are
parsed correctly.
vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Fix malformed copyright year range on Line 10.

Use a single valid range; 2010-2014 2026 is malformed.

Suggested fix
-// Copyright (C) 2010-2014 2026 Nicola Asuni - Tecnick.com LTD
+// Copyright (C) 2010-2026 Nicola Asuni - Tecnick.com LTD
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php` at line 10, The
copyright header string "Copyright (C) 2010-2014 2026 Nicola Asuni - Tecnick.com
LTD" is malformed; replace the duplicate year tokens with a single valid range,
e.g. change that header to "Copyright (C) 2010-2026 Nicola Asuni - Tecnick.com
LTD" so the years form one continuous range and the header is syntactically
correct.
vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Fix malformed copyright year formatting on Line 10.

2010-2013 2026 is malformed and should be a single coherent range.

Proposed fix
-// Copyright (C) 2010-2013 2026 Nicola Asuni - Tecnick.com LTD
+// Copyright (C) 2010-2026 Nicola Asuni - Tecnick.com LTD
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php` at line 10, The
copyright header string "Copyright (C) 2010-2013 2026 Nicola Asuni - Tecnick.com
LTD" is malformed; replace it with a single coherent year range such as
"Copyright (C) 2010-2026 Nicola Asuni - Tecnick.com LTD" by editing the header
line in vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php (the line containing
that copyright string) so the years are a single contiguous range.
vendor/tecnickcom/tcpdf/include/tcpdf_images.php (1)

277-277: ⚠️ Potential issue | 🟡 Minor

Fix diagnostic typo regressions in PNG parser comments.

Unknownn appears in three comments and should be corrected to Unknown.

Proposed fix
-			// Unknownn compression method
+			// Unknown compression method
...
-			// Unknownn filter method
+			// Unknown filter method
...
-					// Unknownn filter method
+					// Unknown compression method

Also applies to: 282-282, 334-334

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_images.php` at line 277, There are
three comment typos in tcpdf_images.php where "Unknownn" is misspelled; locate
occurrences of the string "Unknownn" (e.g., the comment "Unknownn compression
method") and correct them to "Unknown" in the PNG parser comments so all three
instances read "Unknown" (search for "Unknownn" to find and replace in the
relevant comment blocks).
vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php (1)

310-310: ⚠️ Potential issue | 🟠 Major

Avoid normalizing the entire stacktrace string.

Line 310 still normalizes the whole trace text, which can rewrite namespace separators in class names on Windows. Normalize only path fragments, not full frames.

🔧 Suggested fix
-        return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}" . strtr($trace, DIRECTORY_SEPARATOR, '/') . "\n";
+        $normalizedTrace = preg_replace_callback(
+            '/^(#\d+\s+)(.+?)(\(\d+\):)/m',
+            static fn (array $m): string => $m[1] . str_replace(DIRECTORY_SEPARATOR, '/', $m[2]) . $m[3],
+            $trace
+        ) ?? $trace;
+
+        return "\n{$this->indentStacktraces}[stacktrace]\n{$this->indentStacktraces}{$normalizedTrace}\n";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php` at line 310,
The current return normalizes the entire $trace string with strtr which also
alters namespace separators on Windows; change the logic in LineFormatter (the
method that builds this return using $this->indentStacktraces and $trace) to
only normalize filesystem path fragments inside each stack frame instead of the
whole $trace (e.g. detect file path segments within frames and replace
DIRECTORY_SEPARATOR with '/' only for those matches), preserving backslashes
used in PHP namespaces and class names.
vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php (1)

1387-1387: ⚠️ Potential issue | 🔴 Critical

Restore checkSumAdjustment write position to head+8.

Line 1387 writes checksum bytes at +4..+8, which targets the wrong head field. This should be +8..+12; otherwise subsetted TTF metadata is corrupted.

🐛 Proposed fix
-$font = substr($font, 0, $table['head']['offset'] + $offset + 4).pack('N', $checkSumAdjustment).substr($font, $table['head']['offset'] + $offset + 8);
+$font = substr($font, 0, $table['head']['offset'] + $offset + 8).pack('N', $checkSumAdjustment).substr($font, $table['head']['offset'] + $offset + 12);
#!/bin/bash
# Verify where checkSumAdjustment is written in TCPDF_FONTS::_getTrueTypeFontSubset
rg -n "set checkSumAdjustment|pack\('N', \$checkSumAdjustment\)" vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php -C2

python - <<'PY'
import re, pathlib
p = pathlib.Path("vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php")
t = p.read_text()
m = re.search(r"\$font = substr\(\$font, 0, \$table\['head'\]\['offset'\] \+ \$offset \+ (\d+)\)\.pack\('N', \$checkSumAdjustment\)\.substr\(\$font, \$table\['head'\]\['offset'\] \+ \$offset \+ (\d+)\);", t)
print("found offsets:", m.groups() if m else "pattern not found")
print("expected offsets: ('8', '12')")
PY
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php` at line 1387, In
TCPDF_FONTS::_getTrueTypeFontSubset the checksum bytes are written at the wrong
position: the current expression uses $table['head']['offset'] + $offset + 4 and
+8, corrupting subsetted TTF metadata; update the write position to use +8 and
+12 so the pack('N', $checkSumAdjustment) replaces bytes at head+8..+11 (i.e.
change both occurrences of "+ 4" -> "+ 8" and "+ 8" -> "+ 12" around the
substr/pack that writes $checkSumAdjustment for the $font buffer).
scripts/manual-upgrade.php (1)

298-300: ⚠️ Potential issue | 🟠 Major

Cast CSRF token and session values to strings before hash_equals() to prevent fatal TypeError.

Lines 298-300 contain CSRF validation outside the try/catch block. $_POST['csrf_token'] is not type-normalized. If an attacker sends csrf_token[]=x, the null coalescing operator returns an array instead of a string. In PHP 8.1 (required by this project), hash_equals() is strictly typed and throws a fatal TypeError when given non-string arguments. This exception occurs before the try/catch block (which starts at line 307), resulting in an unhandled 500 error instead of a graceful CSRF rejection.

Cast both values to strings:

Suggested patch
-    $submittedToken = $_POST['csrf_token'] ?? '';
-    if (!hash_equals($_SESSION['upgrade_csrf'] ?? '', $submittedToken)) {
+    $submittedToken = (string) ($_POST['csrf_token'] ?? '');
+    $sessionToken = (string) ($_SESSION['upgrade_csrf'] ?? '');
+    if (!hash_equals($sessionToken, $submittedToken)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/manual-upgrade.php` around lines 298 - 300, The CSRF check uses
hash_equals() with unvalidated inputs which can be arrays and cause a TypeError;
convert both operands to strings before calling hash_equals so it never receives
non-string types (cast the $_POST['csrf_token'] result into a string for
$submittedToken and cast the $_SESSION['upgrade_csrf'] fallback to string when
passed to hash_equals) while preserving the existing logic and error assignment
in the block that performs the comparison.
vendor/tecnickcom/tcpdf/config/tcpdf_config.php (1)

11-11: ⚠️ Potential issue | 🟡 Minor

Possible malformed TCPDF metadata/header text in vendored file.

Line 11 (2004-2014 2026) and Line 117 (by2026 ...) look corrupted. If this differs from upstream 6.11.0, PDFs may show unintended header text. This matches a previously raised concern.

#!/bin/bash
set -euo pipefail

TAG="6.11.0"
UPSTREAM_URL="https://raw.githubusercontent.com/tecnickcom/TCPDF/${TAG}/config/tcpdf_config.php"

echo "=== Upstream (${TAG}) relevant lines ==="
curl -fsSL "$UPSTREAM_URL" | sed -n '9,13p;115,118p'

echo
echo "=== Local relevant lines ==="
sed -n '9,13p;115,118p' vendor/tecnickcom/tcpdf/config/tcpdf_config.php

Expected result: local lines should match upstream exactly. If they do not, sync the vendored files to the official release artifacts.

Also applies to: 117-117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/config/tcpdf_config.php` at line 11, The vendored
TCPDF config file contains corrupted header metadata (e.g., the strings
"2004-2014 2026" and "by2026" in
vendor/tecnickcom/tcpdf/config/tcpdf_config.php) that likely differs from
upstream 6.11.0; replace or restore these header lines to exactly match the
official upstream release (tag 6.11.0) by fetching the canonical file from
https://raw.githubusercontent.com/tecnickcom/TCPDF/6.11.0/config/tcpdf_config.php
and syncing the vendored file so the header/comment text and any other differing
lines match upstream exactly.
public/assets/main.css (1)

4662-4665: ⚠️ Potential issue | 🟡 Minor

Stylelint declaration-empty-line-before violations still present in newly added utility blocks.

These blocks still violate the lint rule and can keep CI red. Please regenerate this CSS or run auto-fix so declaration spacing matches repo formatting.

🔧 Example fix pattern
 .border-amber-300 {
   --tw-border-opacity: 1;
+
   border-color: rgb(252 211 77 / var(--tw-border-opacity, 1));
 }

Also applies to: 4686-4689, 4853-4868, 4891-4894, 4950-4973, 5460-5499, 5528-5531, 5556-5559, 5623-5626, 6398-6401, 6450-6453, 6490-6493, 6506-6517, 6526-6529, 6574-6577, 6602-6605, 6798-6801, 6858-6864, 6903-6909, 6996-6999, 7048-7071

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@public/assets/main.css` around lines 4662 - 4665, The added utility blocks
(for example the .border-amber-300 block and the other newly added classes noted
in the comment) violate the stylelint rule declaration-empty-line-before; to
fix, regenerate the CSS or run the repository's Stylelint autofix (e.g.,
stylelint --fix) so declaration spacing matches the repo formatting, or manually
adjust each utility block to insert/remove the required empty line before
declarations to match the existing pattern; search for the class identifiers
like ".border-amber-300" to locate the offending blocks and re-run the
build/asset generation to ensure all ranges listed are corrected.
🧹 Nitpick comments (8)
vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php (1)

162-172: Directory cleanup path comparison may be fragile.

The comparison $dir !== $basePath (line 164) uses strict string inequality. If paths are canonicalized differently (symlinks, relative vs absolute, trailing slashes), the loop could either:

  • Stop prematurely (no harm)
  • Continue beyond intended scope (unlikely given rmdir fails on non-empty dirs, but still a risk)

Consider using realpath() for comparison or adding a depth limit as a safeguard.

Suggested improvement
         $basePath = dirname($this->filename);
+        $basePath = realpath($basePath) ?: $basePath;
 
         foreach (\array_slice($logFiles, $this->maxFiles) as $file) {
             if (is_writable($file)) {
                 // suppress errors here as unlink() might fail if two processes
                 // are cleaning up/rotating at the same time
                 set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool {
                     return true;
                 });
                 unlink($file);
 
                 $dir = dirname($file);
+                $dir = realpath($dir) ?: $dir;
                 while ($dir !== $basePath) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php` around
lines 162 - 172, The directory cleanup loop in RotatingFileHandler that compares
$dir !== $basePath is fragile to path canonicalization differences; update the
loop to compare normalized real paths (e.g., compute $dirReal = realpath($dir)
and $baseReal = realpath($basePath) or normalize with rtrim and
DIRECTORY_SEPARATOR) and use those for the equality check, and add a simple
depth/safety counter (max iterations) as a fallback to prevent accidental
traversal beyond the intended base; locate the loop using the $dir and $basePath
variables in the RotatingFileHandler cleanup logic and replace the comparison
and loop guard accordingly.
tests/full-test.spec.js (1)

167-2359: Consider splitting this suite into smaller spec files by phase.

The single-file, cross-phase shared state design makes triage and reruns expensive when one early phase fails. Breaking phases into smaller specs with scoped fixtures will improve maintainability and failure isolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/full-test.spec.js` around lines 167 - 2359, Single-line: The huge
all-phases single spec file makes failures hard to triage; split phases into
smaller spec files and replace global shared state with scoped fixtures/helpers.
Create separate spec files per phase (e.g. phase1.install.spec.js,
phase2.login.spec.js) by extracting each test.describe.serial('Phase X: ...')
block into its own file, move shared helpers (loginAsAdmin, dbQuery,
requestLoanViaSwal, dismissSwal, escapeSqlLike, RUN_ID, state) into a
tests/utils.js or into Playwright fixtures, and implement fixtures (using
test.extend) to provide per-spec scoped state (createdBookIds, authorIds,
userId, eventId) and common setup/teardown so tests no longer rely on a single
global state object; ensure cleanup logic from the Phase 20 block is adapted
into fixture teardown or per-spec cleanup to keep tests isolated and rerunnable.
app/Views/events/index.php (1)

36-40: Consider centralizing date/time fallback formatting in a shared helper.

This logic now appears in multiple views; extracting it would reduce drift and make locale fallback changes easier to maintain.

Also applies to: 42-55, 57-70

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/events/index.php` around lines 36 - 40, The date fallback logic
duplicated as $fallbackDateFormat = match (strtolower(substr($locale, 0, 2))) {
... } should be extracted into a single shared helper (e.g.,
getFallbackDateFormat($locale) in a view/helper class or global helper) and all
occurrences (including the other blocks mentioned) should call that helper
instead of reimplementing the match; implement getFallbackDateFormat to
normalize locale (lowercase + substr first 2 chars) and return the same mappings
for 'de' and 'it' with the default, then update the views to use the helper
(replace uses of $fallbackDateFormat with the helper call) and register/import
the helper where needed.
app/Views/layout.php (1)

1412-1420: Prefer route-driven shortcuts instead of hardcoded URLs.

Hardcoded destinations can drift from real nav links/routes over time. Consider building gNavMap from existing nav anchors (or server-rendered route values) to keep shortcuts aligned automatically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/layout.php` around lines 1412 - 1420, Replace the hardcoded gNavMap
object with values derived from actual route anchors or server-rendered route
variables so shortcuts stay in sync: locate the gNavMap declaration and basePath
usage in layout.php, query the DOM for the admin nav anchors (e.g.,
document.querySelectorAll on the admin nav links) or consume server-provided
route values (e.g., a routes array injected into the template), and build
gNavMap by mapping the shortcut keys ('d','b','a','e','p','u','s') to the hrefs
from those anchors or route values instead of concatenating basePath + literal
strings.
app/Views/admin/updates.php (1)

562-632: Deduplicate token mutation flow to reduce maintenance drift.

saveGitHubToken() and removeGitHubToken() duplicate most of the same request/result/error handling. Consider extracting a single helper for mutation + toast + reload flow.

♻️ Proposed refactor
+async function mutateGitHubToken(tokenValue, successTitle) {
+    const data = await postTokenRequest(tokenValue);
+    if (data.success) {
+        await Swal.fire({
+            icon: 'success',
+            title: successTitle,
+            text: data.message,
+            timer: 1500,
+            showConfirmButton: false
+        });
+        location.reload();
+        return;
+    }
+    throw new Error(data.error || 'Operazione non riuscita');
+}
+
 async function saveGitHubToken() {
@@
-    try {
-        const data = await postTokenRequest(token);
-
-        if (data.success) {
-            Swal.fire({
-                icon: 'success',
-                title: <?= json_encode(__("Salvato"), JSON_HEX_TAG) ?>,
-                text: data.message,
-                timer: 1500,
-                showConfirmButton: false
-            }).then(() => location.reload());
-        } else {
-            Swal.fire({ icon: 'error', title: <?= json_encode(__("Errore"), JSON_HEX_TAG) ?>, text: data.error });
-        }
-    } catch (error) {
+    try {
+        await mutateGitHubToken(token, <?= json_encode(__("Salvato"), JSON_HEX_TAG) ?>);
+    } catch (error) {
         Swal.fire({ icon: 'error', title: <?= json_encode(__("Errore"), JSON_HEX_TAG) ?>, text: error.message });
     } finally {
         tokenRequestInFlight = false;
     }
 }
@@
-    try {
-        const data = await postTokenRequest('');
-
-        if (data.success) {
-            Swal.fire({
-                icon: 'success',
-                title: <?= json_encode(__("Rimosso"), JSON_HEX_TAG) ?>,
-                text: data.message,
-                timer: 1500,
-                showConfirmButton: false
-            }).then(() => location.reload());
-        } else {
-            Swal.fire({ icon: 'error', title: <?= json_encode(__("Errore"), JSON_HEX_TAG) ?>, text: data.error });
-        }
-    } catch (error) {
+    try {
+        await mutateGitHubToken('', <?= json_encode(__("Rimosso"), JSON_HEX_TAG) ?>);
+    } catch (error) {
         Swal.fire({ icon: 'error', title: <?= json_encode(__("Errore"), JSON_HEX_TAG) ?>, text: error.message });
     } finally {
         tokenRequestInFlight = false;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/admin/updates.php` around lines 562 - 632, saveGitHubToken and
removeGitHubToken duplicate the same request/result/error/toast/reload flow;
extract a helper (e.g., performTokenMutation(token)) that checks and sets
tokenRequestInFlight, calls postTokenRequest(token), shows success/error Swal
messages and triggers location.reload() on success, and resets
tokenRequestInFlight in a finally block; keep callers (saveGitHubToken: read
input value and validate; removeGitHubToken: show confirmation) but replace
their request logic with a single call to performTokenMutation('token' or '') to
avoid duplication and maintenance drift.
app/Routes/web.php (2)

93-123: Reduce duplicated events handlers by reusing shared closures.

The same handler logic is repeated for fallback and localized variants, which increases maintenance cost.

♻️ Proposed refactor
-    $registerRouteIfUnique('GET', '/events', function ($request, $response) use ($app) {
+    $eventsListHandler = function ($request, $response) use ($app) {
         $container = $app->getContainer();
         $controller = new \App\Controllers\FrontendController($container);
         $db = $container->get('db');
         return $controller->events($request, $response, $db);
-    });
-    $registerRouteIfUnique('GET', '/events/{slug}', function ($request, $response, $args) use ($app) {
+    };
+    $eventDetailHandler = function ($request, $response, $args) use ($app) {
         $container = $app->getContainer();
         $controller = new \App\Controllers\FrontendController($container);
         $db = $container->get('db');
         return $controller->event($request, $response, $db, $args);
-    });
+    };
+
+    $registerRouteIfUnique('GET', '/events', $eventsListHandler);
+    $registerRouteIfUnique('GET', '/events/{slug}', $eventDetailHandler);
@@
-        $registerRouteIfUnique('GET', RouteTranslator::getRouteForLocale('events', $locale), function ($request, $response) use ($app) {
-            $container = $app->getContainer();
-            $controller = new \App\Controllers\FrontendController($container);
-            $db = $container->get('db');
-            return $controller->events($request, $response, $db);
-        });
+        $registerRouteIfUnique('GET', RouteTranslator::getRouteForLocale('events', $locale), $eventsListHandler);
@@
-        $registerRouteIfUnique('GET', RouteTranslator::getRouteForLocale('events', $locale) . '/{slug}', function ($request, $response, $args) use ($app) {
-            $container = $app->getContainer();
-            $controller = new \App\Controllers\FrontendController($container);
-            $db = $container->get('db');
-            return $controller->event($request, $response, $db, $args);
-        });
+        $registerRouteIfUnique('GET', RouteTranslator::getRouteForLocale('events', $locale) . '/{slug}', $eventDetailHandler);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Routes/web.php` around lines 93 - 123, Duplicate route handlers for
events can be replaced by shared closures to avoid repetition: create a single
$eventsHandler closure that instantiates new
\App\Controllers\FrontendController($container) and calls its
events($request,$response,$db), and a single $eventHandler closure that calls
event($request,$response,$db,$args); then use those two closures with
$registerRouteIfUnique for both the English fallback routes and inside the
foreach over $supportedLocales using
RouteTranslator::getRouteForLocale('events', $locale) (and the '/{slug}'
variant) so you reuse the same handler logic instead of duplicating it.

257-281: Make hybrid profile-update aliases data-driven.

The locale-specific if/elseif chain will keep growing as new languages are added via Admin → Languages.

♻️ Proposed refactor
-        if ($locale === 'it_IT') {
-            // Italian base + English action
-            $registerRouteIfUnique('POST', '/profilo/update', function ($request, $response) use ($app) {
-                $db = $app->getContainer()->get('db');
-                $controller = new ProfileController();
-                return $controller->update($request, $response, $db);
-            }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
-        } elseif ($locale === 'en_US') {
-            // English base + Italian action
-            $registerRouteIfUnique('POST', '/profile/aggiorna', function ($request, $response) use ($app) {
-                $db = $app->getContainer()->get('db');
-                $controller = new ProfileController();
-                return $controller->update($request, $response, $db);
-            }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
-        } elseif ($locale === 'de_DE') {
-            // German base + English/Italian action
-            $registerRouteIfUnique('POST', '/profil/update', function ($request, $response) use ($app) {
-                $db = $app->getContainer()->get('db');
-                $controller = new ProfileController();
-                return $controller->update($request, $response, $db);
-            }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
-            $registerRouteIfUnique('POST', '/profil/aggiorna', function ($request, $response) use ($app) {
-                $db = $app->getContainer()->get('db');
-                $controller = new ProfileController();
-                return $controller->update($request, $response, $db);
-            }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
-        }
+        $hybridProfileAliases = [
+            'it_IT' => ['/profilo/update'],
+            'en_US' => ['/profile/aggiorna'],
+            'de_DE' => ['/profil/update', '/profil/aggiorna'],
+        ];
+        foreach ($hybridProfileAliases[$locale] ?? [] as $aliasPath) {
+            $registerRouteIfUnique('POST', $aliasPath, function ($request, $response) use ($app) {
+                $db = $app->getContainer()->get('db');
+                $controller = new ProfileController();
+                return $controller->update($request, $response, $db);
+            }, [new CsrfMiddleware(), new AuthMiddleware(['admin', 'staff', 'standard', 'premium'])]);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Routes/web.php` around lines 257 - 281, The repeated locale-specific
if/elseif blocks registering POST profile update routes should be replaced with
a data-driven mapping: create a map (e.g., $localeToPaths) keyed by $locale with
arrays of URI aliases, then loop over $localeToPaths[$locale] and call
$registerRouteIfUnique for each path; reuse the same closure that resolves $db
and instantiates ProfileController and keep the same middleware list ([new
CsrfMiddleware(), new AuthMiddleware(['admin','staff','standard','premium'])])
so you remove hard-coded branches (the blocks using registerRouteIfUnique,
ProfileController::update, CsrfMiddleware, AuthMiddleware) and make adding new
language aliases a simple data change.
app/Support/Updater.php (1)

748-760: Deduplicate anonymous-header construction.

Anonymous retry headers are manually duplicated in two places. Consider a small helper (or a getGitHubHeaders(..., withAuth:false) pattern) to keep behavior aligned.

Also applies to: 948-951

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Support/Updater.php` around lines 748 - 760, The duplicate anonymous
header array ($retryHeaders) used when building curl requests (seen around
$retryHeaders and the curl_setopt_array for $ch2) should be centralized: add a
small helper method (e.g., getGitHubHeaders(bool $withAuth = true) or
getGitHubRetryHeaders()) in Updater.php that returns the headers array
(User-Agent and Accept and optionally Authorization), then replace both manual
$retryHeaders definitions and any direct header arrays (including the use around
$ch2 and the other occurrence at lines ~948-951) with calls to that helper so
all curl requests use the same canonical header builder.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae2e4c9 and 825a934.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (110)
  • .htaccess
  • README.md
  • app/Controllers/FrontendController.php
  • app/Controllers/LibriController.php
  • app/Controllers/UpdateController.php
  • app/Models/BookRepository.php
  • app/Routes/web.php
  • app/Support/I18n.php
  • app/Support/Updater.php
  • app/Views/admin/updates.php
  • app/Views/autori/index.php
  • app/Views/editori/index.php
  • app/Views/events/index.php
  • app/Views/frontend/event-detail.php
  • app/Views/frontend/events.php
  • app/Views/frontend/home-sections/events.php
  • app/Views/frontend/home-sections/genre_carousel.php
  • app/Views/frontend/home.php
  • app/Views/frontend/layout.php
  • app/Views/layout.php
  • app/Views/libri/index.php
  • app/Views/libri/partials/book_form.php
  • app/Views/libri/scheda_libro.php
  • app/Views/partials/scroll-to-top.php
  • app/Views/prestiti/index.php
  • app/Views/utenti/index.php
  • frontend/js/vendor.js
  • installer/classes/Installer.php
  • installer/database/data_de_DE.sql
  • installer/database/data_en_US.sql
  • installer/database/data_it_IT.sql
  • installer/index.php
  • installer/steps/step0.php
  • locale/de_DE.json
  • locale/routes_de_DE.json
  • public/assets/main.css
  • public/assets/vendor.bundle.js
  • scripts/.user.ini
  • scripts/create-release.sh
  • scripts/manual-update.php
  • scripts/manual-upgrade.php
  • tests/full-test.spec.js
  • tests/genre-bugs.spec.js
  • vendor/composer/autoload_classmap.php
  • vendor/composer/autoload_files.php
  • vendor/composer/autoload_static.php
  • vendor/composer/installed.json
  • vendor/composer/installed.php
  • vendor/graham-campbell/result-type/composer.json
  • vendor/laravel/serializable-closure/composer.json
  • vendor/laravel/serializable-closure/src/Serializers/Native.php
  • vendor/laravel/serializable-closure/src/Support/ReflectionClosure.php
  • vendor/monolog/monolog/README.md
  • vendor/monolog/monolog/composer.json
  • vendor/monolog/monolog/src/Monolog/Formatter/GelfMessageFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/JsonFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/LineFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php
  • vendor/monolog/monolog/src/Monolog/Formatter/WildfireFormatter.php
  • vendor/monolog/monolog/src/Monolog/Handler/BrowserConsoleHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/CubeHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/Curl/Util.php
  • vendor/monolog/monolog/src/Monolog/Handler/DeduplicationHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/Handler.php
  • vendor/monolog/monolog/src/Monolog/Handler/LogglyHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/MongoDBHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/ProcessHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/RotatingFileHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/SlackWebhookHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/StreamHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/TelegramBotHandler.php
  • vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php
  • vendor/slim/csrf/composer.json
  • vendor/slim/psr7/composer.json
  • vendor/slim/psr7/src/Cookies.php
  • vendor/slim/slim/MAINTAINERS.md
  • vendor/slim/slim/Slim/App.php
  • vendor/slim/slim/composer.json
  • vendor/slim/slim/psalm.xml
  • vendor/tecnickcom/tcpdf/CHANGELOG.TXT
  • vendor/tecnickcom/tcpdf/LICENSE.TXT
  • vendor/tecnickcom/tcpdf/README.md
  • vendor/tecnickcom/tcpdf/VERSION
  • vendor/tecnickcom/tcpdf/composer.json
  • vendor/tecnickcom/tcpdf/config/tcpdf_config.php
  • vendor/tecnickcom/tcpdf/fonts/ae_fonts_2.0/README
  • vendor/tecnickcom/tcpdf/fonts/freefont-20100919/COPYING
  • vendor/tecnickcom/tcpdf/fonts/freefont-20100919/CREDITS
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/COPYING
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/CREDITS
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/README
  • vendor/tecnickcom/tcpdf/fonts/freefont-20120503/TROUBLESHOOTING
  • vendor/tecnickcom/tcpdf/include/barcodes/datamatrix.php
  • vendor/tecnickcom/tcpdf/include/barcodes/pdf417.php
  • vendor/tecnickcom/tcpdf/include/barcodes/qrcode.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_colors.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_filters.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_font_data.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_fonts.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_images.php
  • vendor/tecnickcom/tcpdf/include/tcpdf_static.php
  • vendor/tecnickcom/tcpdf/tcpdf.php
  • vendor/tecnickcom/tcpdf/tcpdf_autoconfig.php
  • vendor/tecnickcom/tcpdf/tcpdf_barcodes_1d.php
  • vendor/tecnickcom/tcpdf/tcpdf_barcodes_2d.php
  • vendor/tecnickcom/tcpdf/tools/tcpdf_addfont.php
  • vendor/vlucas/phpdotenv/composer.json
  • vendor/vlucas/phpdotenv/src/Parser/EntryParser.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php
💤 Files with no reviewable changes (7)
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/EnvConstAdapter.php
  • vendor/vlucas/phpdotenv/src/Parser/EntryParser.php
  • vendor/slim/slim/psalm.xml
  • vendor/slim/slim/MAINTAINERS.md
  • vendor/vlucas/phpdotenv/src/Repository/Adapter/ServerConstAdapter.php
  • scripts/manual-update.php
  • vendor/monolog/monolog/src/Monolog/Formatter/MongoDBFormatter.php

use Monolog\Logger;
use Psr\Log\LogLevel;
use Monolog\LogRecord;
use NoDiscard;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find root dependency manifests
echo "== composer manifests =="
fd -a '^composer\.json$'
fd -a '^composer\.lock$'

# 2) Print declared PHP constraint(s)
echo
echo "== declared PHP constraints =="
rg -n '"php"\s*:\s*"' $(fd -a '^composer\.json$' 2>/dev/null || true)

# 3) Print locked monolog version(s), if present
echo
echo "== locked monolog versions =="
python - <<'PY'
import json, pathlib
for lock_path in pathlib.Path('.').rglob('composer.lock'):
    try:
        data = json.loads(lock_path.read_text(encoding='utf-8'))
    except Exception:
        continue
    pkgs = data.get('packages', []) + data.get('packages-dev', [])
    for p in pkgs:
        if p.get('name') == 'monolog/monolog':
            print(f"{lock_path}: monolog/monolog {p.get('version')}")
PY

# 4) Confirm where NoDiscard is used in vendor copy
echo
echo "== NoDiscard usages in monolog vendor file =="
rg -n 'use NoDiscard|#\[NoDiscard\]' vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php 2>/dev/null || echo "File not found or no matches"

Repository: fabiodalez-dev/Pinakes

Length of output: 12751


🌐 Web query:

#[NoDiscard] PHP attribute introduction version when released

💡 Result:

  • #[\NoDiscard] (the built-in attribute) was introduced in PHP 8.5, first released in PHP 8.5.0 (Nov 20, 2025). [1][2]
  • PHP attributes syntax (#[] in general) were introduced in PHP 8.0, first released in PHP 8.0.0 (Nov 26, 2020). [3][4]

Update minimum PHP version or remove #[NoDiscard] attributes for compatibility.

The #[NoDiscard] attribute was introduced in PHP 8.5 (released Nov 2025), but your project requires ^8.1, which includes PHP 8.1–8.4 where this attribute does not exist. On older PHP versions, the attributes will parse but provide no engine behavior, defeating their purpose of warning when return values are discarded. Either update composer.json to require php: ">=8.5" or remove the #[NoDiscard] annotations from the vendored Monolog code.

Reference: https://www.php.net/manual/en/migration85.new-features.php

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/monolog/monolog/src/Monolog/Handler/TestHandler.php` at line 18, The
vendored Monolog code uses the PHP 8.5 attribute #[NoDiscard] which is
incompatible with our declared php constraint (^8.1); either raise the minimum
PHP requirement to >=8.5 in composer.json or remove the #[NoDiscard] annotations
from the Monolog handler(s) (e.g., the TestHandler class where #[NoDiscard]
appears) so the code is parseable and behaves consistently on PHP 8.1–8.4; if
choosing removal, delete the #[NoDiscard] attribute tokens (and any related
use/import like NoDiscard) from the relevant classes/methods (e.g., TestHandler)
in the vendor files, or alternatively update composer.json's php version
constraint to ">=8.5" and run composer update to enforce the new requirement.

* @return string encrypted text
* @since 2.0.000 (2008-01-02)
* @author Klemen Vodopivec, Nicola Asuni
* @author Klemen Vodopivec,2026 Nicola Asuni
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix author docblock punctuation.

Klemen Vodopivec,2026 is missing a space after the comma.

✏️ Suggested fix
-	 * `@author` Klemen Vodopivec,2026 Nicola Asuni
+	 * `@author` Klemen Vodopivec, 2026 Nicola Asuni
📝 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.

Suggested change
* @author Klemen Vodopivec,2026 Nicola Asuni
* `@author` Klemen Vodopivec, 2026 Nicola Asuni
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/include/tcpdf_static.php` at line 495, Fix the author
docblock punctuation by inserting a space after the comma in the author line:
change "Klemen Vodopivec,2026 Nicola Asuni" to "Klemen Vodopivec, 2026 Nicola
Asuni" in the tcpdf_static.php author docblock (search for the "author Klemen
Vodopivec,2026 Nicola Asuni" string to locate the exact line).

## Developer(s) Contact

* Nicola Asuni <info@tecnick.com>
*2026 Nicola Asuni <info@tecnick.com>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix malformed developer contact entry on Line 83.

The current Markdown entry is malformed and likely unintended.

Proposed fix
-*2026 Nicola Asuni <info@tecnick.com>
+* Nicola Asuni <info@tecnick.com>
📝 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.

Suggested change
*2026 Nicola Asuni <info@tecnick.com>
* Nicola Asuni <info@tecnick.com>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vendor/tecnickcom/tcpdf/README.md` at line 83, The README entry "*2026 Nicola
Asuni <info@tecnick.com>" is malformed; update the line in README.md (the
developer contact entry) to be a properly formatted Markdown list/item by adding
the missing space and punctuation — for example change "*2026 Nicola Asuni
<info@tecnick.com>" to "* 2026 Nicola Asuni <info@tecnick.com>" or to a clearer
form like "2026 — Nicola Asuni <info@tecnick.com>" so the contact renders
correctly.

- events.php: use route_path('events') instead of hardcoded /events/ path
- layout.php: reset gPrefixActive on Escape, add aria-label to icon-only buttons
- BookRepository: validate anno_pubblicazione in updateOptionals with filter_var
- Updater: validate retry HTTP status before accepting fallback download content
- scroll-to-top: respect prefers-reduced-motion for smooth scrolling
- Installer: handle unbracketed IPv6 host literals
- manual-upgrade: cast CSRF token to string, add ZIP uncompressed size check
- tests: fix merge selector to target submit button, not form element
@fabiodalez-dev
Copy link
Owner Author

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

✅ Actions performed

Full review triggered.

…#81)

- Add do_action('assets.head') and do_action('assets.footer') hooks to
  frontend layout so plugins can inject CSS/JS on public pages
- Replace admin target="_blank" audio link with inline toggle player
- Fix GAP stale DOM reference: re-query audio element after GAP.init()
  which replaces innerHTML and destroys the original node
- Add global keyboard shortcuts (←→ seek, ↑↓ volume, Space play/pause)
  active when player is visible, regardless of focus
- Switch audio player theme from green to black & white
- Bump digital-library plugin to v1.2.0
- Add version sync for bundled plugins in PluginManager so disk version
  updates propagate to DB automatically on boot
- Add appReady flag set by Phase 1 on completion or when app already installed
- Add beforeEach skip guard to Phase 2-20: skips all tests if Phase 1 failed
- Add beforeAll guard to Phase 2-19: prevents browser context creation
- Make afterAll close() calls safe with optional chaining
- Fix installer radio selector: value="it" → value="it_IT"
- Test GAP CSS/JS assets load on frontend book detail
- Test player toggle button and container render
- Test admin inline audio player (no target="_blank")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant