Skip to content

fix(mobile-api): Swagger UI self-hosted (niente CDN) — /api/v1/docs era vuoto#197

Open
fabiodalez-dev wants to merge 4 commits into
mainfrom
fix/swagger-ui-vendor-no-cdn
Open

fix(mobile-api): Swagger UI self-hosted (niente CDN) — /api/v1/docs era vuoto#197
fabiodalez-dev wants to merge 4 commits into
mainfrom
fix/swagger-ui-vendor-no-cdn

Conversation

@fabiodalez-dev

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

Copy link
Copy Markdown
Owner

/api/v1/docs (la Swagger UI dell'API mobile) era vuoto sui server dove il CDN jsDelivr è bloccato (egress firewall): nel repo c'era solo public/assets/swagger-ui/.gitkeep, quindi SwaggerUiController ripiegava su cdn.jsdelivr.net per swagger-ui-bundle.js/.css e il bundle non veniva mai caricato.

Fix

  • Vendorizzati swagger-ui-dist 5.18.2 (swagger-ui-bundle.js + swagger-ui.css) in public/assets/swagger-ui/ → spediti nello ZIP di release, la pagina funziona offline / dietro firewall.
  • SwaggerUiController ora serve sempre la copia locale: nessun riferimento CDN, rimosso il fallback jsDelivr.
  • Lo spec openapi.json era già a posto.

Verifica

  • Locale: /api/v1/docs referenzia /assets/swagger-ui/* (niente jsdelivr), gli asset servono 200, Swagger UI renderizza le 29 operazioni.
  • PHPStan L5 verde.

Parte dell'audit "niente CDN": l'app è CDN-free a runtime tranne questo punto (ora risolto). Restano due CDN minori dentro librerie terze (twemoji negli emoticon di TinyMCE, una stringa pdfobject in vendor.bundle.js) e le allowance CDN nella CSP — da valutare a parte.

Summary by CodeRabbit

  • New Features

    • Aggiunta una suite E2E Playwright per la pagina di documentazione Swagger della Mobile API, verificando raggiungibilità dello spec, uso di asset locali e rendering effettivo in browser.
  • Bug Fixes

    • Swagger UI ora usa esclusivamente risorse locali vendorizzate nell’app, senza fallback a CDN.
    • Migliorata la generazione e l’encoding delle URL (incluse quelle per openapi e asset) e aggiunta una diagnostica in pagina se il bundle non risulta disponibile.
  • Chores

    • Aggiornata la verifica dei contenuti del ZIP di release includendo i file di Swagger UI critici.

GET /api/v1/docs rendered blank wherever the jsDelivr CDN is blocked (egress
firewall): only public/assets/swagger-ui/.gitkeep shipped, so the controller
fell back to cdn.jsdelivr.net for swagger-ui-bundle.js/css and the bundle never
loaded. Vendor swagger-ui-dist 5.18.2 (bundle + css) into the repo so it ships
in the release ZIP, and make SwaggerUiController always serve the local copy —
no CDN reference at all. The openapi.json spec was already fine.

Verified locally: /api/v1/docs references /assets/swagger-ui/* (no jsdelivr),
the assets serve 200, and Swagger UI renders the 29 operations.
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 35199c14-0f05-4c81-a3c4-68d14d6ac1ff

📥 Commits

Reviewing files that changed from the base of the PR and between 2376fea and e05e8ff.

📒 Files selected for processing (1)
  • storage/plugins/mobile-api/src/Controllers/SwaggerUiController.php

📝 Walkthrough

Walkthrough

SwaggerUiController usa asset Swagger UI locali, aggiorna l’escaping delle URL e aggiunge verifiche di release e test per gli asset vendorizzati.

Changes

Asset Swagger UI locali

Layer / File(s) Summary
Doc e base URL locale
storage/plugins/mobile-api/src/Controllers/SwaggerUiController.php
La doc comment descrive asset Swagger UI self-hosted e assetsBaseUrl() restituisce sempre il percorso locale senza fallback CDN.
Escaping e fallback JS
storage/plugins/mobile-api/src/Controllers/SwaggerUiController.php
buildHtml() escape le URL degli asset, serializza correttamente l’URL OpenAPI per JavaScript e mostra un messaggio se SwaggerUIBundle non è disponibile.
Verifiche di release e docs
scripts/create-release.sh, tests/swagger-docs.spec.js
Lo script di release include gli asset Swagger UI tra i file critici e il test Playwright verifica endpoint OpenAPI, asset locali e rendering della UI.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Il titolo descrive correttamente il fix della Swagger UI self-hosted senza CDN e il problema della pagina vuota su /api/v1/docs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/swagger-ui-vendor-no-cdn

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.

tests/swagger-docs.spec.js asserts /api/v1/docs is self-hosted: openapi.json is
a valid OpenAPI 3 spec, the docs page references /assets/swagger-ui/* and never
a CDN (jsdelivr/cdnjs/unpkg), the local bundle+css are actually served, and
Swagger UI renders the operations from the local bundle. Skips cleanly when the
Mobile API plugin is not active.
@fabiodalez-dev

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@fabiodalez-dev

fabiodalez-dev commented Jun 25, 2026

Copy link
Copy Markdown
Owner Author

Code review

Branch: fix/swagger-ui-vendor-no-cdnmain
PR: #197 (open)

Revisione automatica a 6 lenti (correttezza, struttura/blast-radius, compliance CLAUDE.md, commenti, UX, sicurezza). Il cuore del PR — rimuovere il fallback CDN jsDelivr e servire Swagger UI solo da asset locali vendored — è corretto. Sotto, i rilievi sul codice già esistente che la rimozione del fallback rende più rilevanti.

🔧 Da sistemare (confermato — escaping/XSS, regola #3)

SwaggerUiController.php:62-74,104 · $cssUrl/$jsUrl (e quindi $assetsBaseUrl, derivato dall'header Host via baseUrl()) sono interpolati in href="{$cssUrl}" e <script src="{$jsUrl}"> senza htmlspecialchars(ENT_QUOTES,'UTF-8'), mentre $title e $docUrl nello stesso metodo SONO escapati — incoerenza reale e violazione della regola #3 di CLAUDE.md.

  • L'Host non è normalizzato su questa rotta: la canonical-host in public/index.php salta esplicitamente i path /api/ → un Host arbitrario arriva al controller intatto. Un valore con " o > può uscire dall'attributo (reflected XSS).
  • Fix: $cssUrl = htmlspecialchars($assetsBaseUrl.'/swagger-ui.css', ENT_QUOTES, 'UTF-8'); e idem per $jsUrl (riga 62-63). Behavior-preserving per host legittimi.

ℹ️ Da valutare (reali ma sotto la soglia di conferma)

  • scripts/create-release.sh:156-166 — tolto il fallback CDN, /api/v1/docs dipende ora in modo rigido dai due asset vendored public/assets/swagger-ui/*, ma non sono in CRITICAL_FILES (TinyMCE sì). Oggi git archive li include (sono tracciati, nessun export-ignore), quindi nessuna rottura immediata; ma un futuro export-ignore/rimozione produrrebbe una release con la docs page bianca e build verde, scopribile solo via E2E. Consiglio: aggiungere i due path a CRITICAL_FILES, come già fatto per TinyMCE.
  • SwaggerUiController.php:110$docUrl è escapato HTML (htmlspecialchars) ma usato dentro una stringa JS (url: "{$docUrl}"): escaper sbagliato per il contesto. Rischio basso (valore scheme/host/path-derived, non input utente), ma CLAUDE.md regola Add Z39.50/SRU Server Plugin and Auto-Install System #3 prescrive json_encode(..., JSON_HEX_TAG) per PHP→JS.

Senza rilievi

  • UX — un solo rilievo minore (pagina vuota senza diagnostica se il bundle non carica), classificato sotto soglia su una docs page developer-facing.
  • I 2 asset vendored (swagger-ui-bundle.js, swagger-ui.css) sono libreria upstream minificata, fuori scope per review riga-per-riga. docRoot() e SWAGGER_UI_VERSION rimossi senza altri riferimenti (verificato). Il nuovo test E2E copre il caso "self-hosted, niente CDN".

…red assets in release

Two follow-ups from the self-hosted Swagger UI change, both low-risk:

- SwaggerUiController::buildHtml() now wraps $cssUrl/$jsUrl (built from the
  Host-derived $assetsBaseUrl) in htmlspecialchars(ENT_QUOTES,'UTF-8'), matching
  the sibling $title/$docUrl. The /api/v1/docs route is exempt from the
  canonical-host redirect, so an arbitrary Host header reaches the page; without
  escaping a Host containing `"`/`>` could break out of the href/src attribute
  (reflected injection) and it violated the "escape all HTML attributes" rule.
  Legitimate hosts are unaffected (htmlspecialchars is identity on a clean URL).

- create-release.sh: add public/assets/swagger-ui/swagger-ui-bundle.js and
  swagger-ui.css to CRITICAL_FILES. With the CDN fallback gone, /api/v1/docs hard-
  depends on these vendored files; the release gate guarded TinyMCE but not these,
  so a future export-ignore/removal could ship a blank docs page with a green build.

Verified: php -l + PHPStan level 5 clean; bash -n clean; both assets present (1.4 MB
/ 152 KB) and git-tracked so the new gate entries pass; escaping confirmed to
neutralize a malicious Host while leaving normal asset URLs unchanged.
@fabiodalez-dev

Copy link
Copy Markdown
Owner Author

Fix applicati (commit 2376fea0):

  • 🔧 Escaping/XSSSwaggerUiController::buildHtml() ora avvolge $cssUrl/$jsUrl (derivati dall'Host) in htmlspecialchars(ENT_QUOTES,'UTF-8'), come $title/$docUrl. Host malevolo neutralizzato, URL legittimi invariati.
  • 🔧 Releasecreate-release.sh: aggiunti public/assets/swagger-ui/swagger-ui-bundle.js e swagger-ui.css a CRITICAL_FILES (erano già nel ZIP via git archive, ora il gate li verifica).

Verificato: php -l ok, PHPStan livello 5 pulito, bash -n ok, asset presenti+tracciati (1.4 MB / 152 KB).

Restano aperti, non richiesti in questo giro: $docUrl in contesto JS (json_encode(JSON_HEX_TAG) invece di htmlspecialchars) e la pagina vuota senza diagnostica se il bundle non carica — entrambi a basso rischio.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/swagger-docs.spec.js`:
- Around line 15-26: The skip gate in the swagger-docs Playwright spec currently
depends on probing GET /api/v1/openapi.json in test.beforeAll, which can hide
regressions by skipping the whole spec when that endpoint breaks. Remove this
availability probe from the beforeAll/beforeEach flow and make the guard rely
only on CI-injected E2E environment signals, consistent with the other
Playwright specs using E2E_ADMIN_EMAIL and E2E_ADMIN_PASS; keep the logic in
test.beforeEach and the apiAvailable-related setup aligned with that pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5c54c20-6cf2-4a66-8ca2-241d2abbbf66

📥 Commits

Reviewing files that changed from the base of the PR and between e2d710f and 2376fea.

📒 Files selected for processing (3)
  • scripts/create-release.sh
  • storage/plugins/mobile-api/src/Controllers/SwaggerUiController.php
  • tests/swagger-docs.spec.js

Comment on lines +15 to +26
test.beforeAll(async ({ request }) => {
try {
const res = await request.get(`${BASE}/api/v1/openapi.json`);
apiAvailable = res.status() === 200;
} catch {
apiAvailable = false;
}
});

test.beforeEach(() => {
test.skip(!apiAvailable, 'Mobile API plugin not active / app not installed');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Non usare l’endpoint sotto test come condizione di skip.

Se la regressione rompe GET /api/v1/openapi.json su Line 17, apiAvailable diventa false e da Line 25 in poi l’intero spec viene saltato invece di fallire. Così il test non protegge più proprio il bug che dovrebbe intercettare. Il gate deve dipendere solo dai segnali d’ambiente che CI inietta per gli E2E, oppure questo probe va rimosso del tutto.

Diff proposto
 const { test, expect } = require('`@playwright/test`');

 const BASE = process.env.E2E_BASE_URL || 'http://localhost:8081';
-
-// Public endpoints (no bearer token needed) but they only exist when the
-// bundled Mobile API plugin is active. Probe once and skip cleanly otherwise.
-let apiAvailable = true;
+const ADMIN_EMAIL = process.env.E2E_ADMIN_EMAIL;
+const ADMIN_PASS = process.env.E2E_ADMIN_PASS;

 test.describe.serial('Mobile API — Swagger UI docs (self-hosted, no CDN)', () => {
-  test.beforeAll(async ({ request }) => {
-    try {
-      const res = await request.get(`${BASE}/api/v1/openapi.json`);
-      apiAvailable = res.status() === 200;
-    } catch {
-      apiAvailable = false;
-    }
-  });
-
   test.beforeEach(() => {
-    test.skip(!apiAvailable, 'Mobile API plugin not active / app not installed');
+    test.skip(!ADMIN_EMAIL || !ADMIN_PASS, 'Credenziali E2E admin non configurate');
   });

Based on learnings, i Playwright E2E in questo repo devono essere gated da variabili d’ambiente che CI inietta, e negli UI-only spec il guard deve usare solo E2E_ADMIN_EMAIL e E2E_ADMIN_PASS.

📝 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
test.beforeAll(async ({ request }) => {
try {
const res = await request.get(`${BASE}/api/v1/openapi.json`);
apiAvailable = res.status() === 200;
} catch {
apiAvailable = false;
}
});
test.beforeEach(() => {
test.skip(!apiAvailable, 'Mobile API plugin not active / app not installed');
});
const { test, expect } = require('`@playwright/test`');
const BASE = process.env.E2E_BASE_URL || 'http://localhost:8081';
const ADMIN_EMAIL = process.env.E2E_ADMIN_EMAIL;
const ADMIN_PASS = process.env.E2E_ADMIN_PASS;
test.describe.serial('Mobile API — Swagger UI docs (self-hosted, no CDN)', () => {
test.beforeEach(() => {
test.skip(!ADMIN_EMAIL || !ADMIN_PASS, 'Credenziali E2E admin non configurate');
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/swagger-docs.spec.js` around lines 15 - 26, The skip gate in the
swagger-docs Playwright spec currently depends on probing GET
/api/v1/openapi.json in test.beforeAll, which can hide regressions by skipping
the whole spec when that endpoint breaks. Remove this availability probe from
the beforeAll/beforeEach flow and make the guard rely only on CI-injected E2E
environment signals, consistent with the other Playwright specs using
E2E_ADMIN_EMAIL and E2E_ADMIN_PASS; keep the logic in test.beforeEach and the
apiAvailable-related setup aligned with that pattern.

Source: Learnings

…asset-load failure

Two remaining Swagger UI docs-page follow-ups, both low-risk:

- buildHtml() now JSON-encodes $openApiUrl with json_encode(JSON_HEX_TAG |
  JSON_UNESCAPED_SLASHES) instead of htmlspecialchars(). The value is consumed
  inside a JS string literal (SwaggerUIBundle url:), where HTML escaping is the
  wrong escaper (it leaves backslashes untouched); json_encode is the correct
  JS-context escaper and JSON_HEX_TAG blocks a </script> breakout. It already
  emits the surrounding quotes, so the heredoc drops them (url: {$docUrl}).

- The inline script now guards `typeof SwaggerUIBundle === 'undefined'` and
  renders a diagnostic (with a link to the raw openapi.json) into #swagger-ui
  instead of leaving a blank page. Self-hosted with no CDN fallback, a missing
  vendored bundle would otherwise show only the header bar with no explanation.

Verified: php -l + PHPStan level 5 clean; the generated inline JS is well-formed
for normal URLs and neutralizes a malicious Host (\" and < escapes); browser
check on /api/v1/docs — SwaggerUIBundle boots, 29 operation blocks render, no JS
errors.
@fabiodalez-dev

Copy link
Copy Markdown
Owner Author

Sistemati anche gli ultimi due (commit e05e8ffb):

  • 🔧 $docUrl in contesto JS — ora json_encode($openApiUrl, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES) invece di htmlspecialchars (escaper giusto per una stringa JS; JSON_HEX_TAG blocca un breakout </script>). L'heredoc usa url: {$docUrl} (le virgolette le mette già json_encode).
  • 🔧 Pagina vuota → diagnostica — se SwaggerUIBundle non è definito (bundle non caricato), lo script mostra ora un messaggio con link alla spec grezza openapi.json, invece di lasciare #swagger-ui vuoto.

Verificato: php -l ok, PHPStan L5 pulito, e prova in browser su /api/v1/docs → SwaggerUIBundle si avvia, 29 operation block renderizzati, 0 errori JS. Input Host malevolo correttamente neutralizzato (\", <).

Tutti e 4 i rilievi della review sono ora chiusi.

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