Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
node_modules
npm-debug.log*
.git
.github
.gitignore
.env

# Output di compilazione di installazioni precedenti: non servono a runtime.
backend/compiled
backend/static
esempi/*.pdf
esempi/*.html
latex-source/*.html

# Non serve a runtime (frontend/ e docs/ invece sì: li serve server.js)
deploy
api
vercel.json
CHANGELOG.md
CONTRIBUTING.md
.vscode
.idea
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# .env.example
# Copia questo file in .env e modifica i valori se serve.
# Nessuna variabile è obbligatoria: i default bastano per lo sviluppo.

# Porta su cui il server Express si mette in ascolto (default: 3000)
PORT=3000

# production disattiva i log di sviluppo di Express
NODE_ENV=development
183 changes: 156 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,180 @@ on:
branches: [main, master]

jobs:
test:
server:
name: Server Node + smoke test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
- name: Setup Node
uses: actions/setup-node@v4
with:
python-version: "3.11"
node-version: "22"

- name: Install dependencies
run: npm install

- name: Avvia il server
run: |
node server.js &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -sf -o /dev/null http://localhost:3000/; then
echo "server pronto dopo ${i}s"
exit 0
fi
sleep 1
done
echo "il server non ha risposto entro 30s"
exit 1

- name: Il frontend viene servito
run: |
curl -sf http://localhost:3000/ | grep -q 'id="dropzone"'
curl -sf -o /dev/null http://localhost:3000/app.js
curl -sf -o /dev/null http://localhost:3000/app.css
curl -sf -o /dev/null http://localhost:3000/manifest.json
echo "Frontend OK"

- name: La documentazione viene servita
run: |
curl -sf http://localhost:3000/docs/ | grep -q 'id="navLinks"'
echo "Docs OK"

- name: Gli endpoint di download rispondono
run: |
curl -sf http://localhost:3000/api/template/quiz.tex | grep -q 'documentclass'
curl -sf -o /dev/null http://localhost:3000/api/download/file/quizstruct.sty
curl -sf -o /dev/null http://localhost:3000/api/download/package.zip
echo "Download OK"

- name: Compilazione di un singolo .tex
run: |
curl -sf -X POST http://localhost:3000/compile \
-F "files=@latex-source/quiz.tex" -o out.json
node -e "
const fs = require('fs');
const d = require('./out.json');
if (!d.success) { console.error(d.log); process.exit(1); }
if (!d.pdf || !d.html) { console.error('output mancanti', d); process.exit(1); }
// gli output arrivano nella risposta, non da un URL sul server
fs.writeFileSync('out.pdf', Buffer.from(d.pdf.base64, 'base64'));
fs.writeFileSync('out.html', Buffer.from(d.html.base64, 'base64'));
console.log('Compile OK');
"
head -c 4 out.pdf | grep -q '%PDF'
grep -qi '<html' out.html
echo "Output OK"

- name: Gli output non vengono salvati sul server
run: |
# /static non deve più esistere: nessun documento resta raggiungibile
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/static/out.pdf)
test "$code" = "404" || { echo "atteso 404, ricevuto $code"; exit 1; }
node -e "
const d = require('./out.json');
if (d.pdfUrl || d.htmlUrl) { console.error('la risposta espone ancora URL'); process.exit(1); }
console.log('Nessuna persistenza OK');
"

- name: L'HTML generato non esegue script iniettati
run: |
cat > evil.tex <<'TEX'
\documentclass{article}
\usepackage{enumitem}
\usepackage{quizstruct}
\begin{document}
\begin{quiz}{T<script>window.PWNED=1</script>}
\domanda[1]{D<img src=x onerror="window.PWNED=1">?}
\begin{opzioni}
\rispostacorretta{ok}
\end{opzioni}
\end{quiz}
\end{document}
TEX
curl -sf -X POST http://localhost:3000/compile -F "files=@evil.tex" -o evil.json
node -e "
const d = require('./evil.json');
const html = Buffer.from(d.html.base64, 'base64').toString();
if (/<script>window\.PWNED/.test(html)) { console.error('XSS: script iniettato'); process.exit(1); }
if (/onerror=\"window\.PWNED/.test(html)) { console.error('XSS: handler iniettato'); process.exit(1); }
if (!/&lt;script&gt;/.test(html)) { console.error('atteso payload escapato'); process.exit(1); }
console.log('XSS OK');
"

- name: L'HTML generato non contatta terze parti
run: |
node -e "
const d = require('./out.json');
const html = Buffer.from(d.html.base64, 'base64').toString();
const ext = html.match(/https?:\/\/[a-zA-Z0-9.-]+/g) || [];
if (ext.length) { console.error('richieste esterne:', ext); process.exit(1); }
console.log('Nessuna dipendenza esterna OK');
"

- name: Le intestazioni di sicurezza sono presenti
run: |
h=$(curl -sf -o /dev/null -D - http://localhost:3000/)
for header in "content-security-policy" "x-content-type-options" "x-frame-options" "referrer-policy"; do
echo "$h" | grep -qi "^$header:" || { echo "manca $header"; exit 1; }
done
echo "Header OK"

- name: I file non ammessi vengono rifiutati
run: |
echo "x" > payload.exe
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost:3000/compile -F "files=@payload.exe")
test "$code" = "415" || { echo "atteso 415, ricevuto $code"; exit 1; }
echo "Validazione upload OK"

- name: Compilazione multipla produce uno ZIP
run: |
curl -sf -X POST http://localhost:3000/compile \
-F "files=@latex-source/quiz.tex" \
-F "files=@esempi/matematica.tex" -o multi.json
node -e "
const d = require('./multi.json');
if (!d.success) { console.error(d.log); process.exit(1); }
if (!d.pdf.zip || !d.html.zip) { console.error('atteso uno ZIP', d); process.exit(1); }
console.log('ZIP OK');
"

- name: Ferma il server
if: always()
run: kill "$(cat server.pid)" || true

latex:
name: Compilazione LaTeX reale
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install TeX Live + tex4ht
run: |
sudo apt-get update
sudo apt-get install -y texlive-latex-base texlive-latex-extra \
texlive-fonts-recommended texlive-fonts-extra tex4ht

- name: Install Flask
run: pip install flask
sudo apt-get install -y --no-install-recommends \
texlive-latex-base texlive-latex-extra \
texlive-fonts-recommended tex4ht

- name: Test PDF compilation (quiz.tex)
- name: PDF — quiz.tex
working-directory: latex-source
run: |
cd latex-source
pdflatex -interaction=nonstopmode -halt-on-error quiz.tex
pdflatex -interaction=nonstopmode -halt-on-error quiz.tex
test -f quiz.pdf && echo "PDF OK"

- name: Test PDF compilation (matematica.tex)
- name: PDF — esempi
run: |
cp esempi/matematica.tex latex-source/
cp esempi/*.tex latex-source/
cd latex-source
pdflatex -interaction=nonstopmode -halt-on-error matematica.tex
test -f matematica.pdf && echo "Matematica PDF OK"
for f in matematica lezione fisica-lezione; do
pdflatex -interaction=nonstopmode -halt-on-error "$f.tex"
test -f "$f.pdf" && echo "$f PDF OK"
done

- name: Test HTML compilation (quiz.tex)
- name: HTML — quiz.tex
working-directory: latex-source
run: |
cd latex-source
htlatex quiz quiz.cfg
test -f quiz.html && echo "HTML OK"

- name: Backend smoke test
run: |
cd backend
python3 app.py &
sleep 2
curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/ | grep 200
curl -s -X POST http://localhost:5000/compile \
-F "files=@../latex-source/quiz.tex" |
python3 -c "import sys,json; d=json.load(sys.stdin); assert d['success'], d['log']; print('Backend OK')"
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
latex-source/quiz.cfg
latex-source/quiz.js
latex-source/quiz.css
esempi/quiz.tex
latex-source/quiz.tex
esempi/matematica.tex
esempi/lezione.tex
esempi/fisica-lezione.tex
16 changes: 12 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
# Node
node_modules/
npm-debug.log*
.env

# Python
__pycache__/
*.pyc
venv/
.venv/

# Backend output
# Backend output (il server ora scrive solo sotto la temp di sistema,
# queste voci restano per ripulire installazioni precedenti)
backend/compiled/
backend/static/*.pdf
backend/static/*.html
backend/static/

# LaTeX temp files
*.aux
Expand All @@ -21,7 +26,10 @@ backend/static/*.html
*.lg
*.tmp
*.xref
*.html

# HTML generato da htlatex (NON ignorare *.html: mangerebbe frontend/index.html)
latex-source/*.html
esempi/*.html

# OS
.DS_Store
Expand Down
Binary file removed 5b22338b5a6f.pdf
Binary file not shown.
94 changes: 90 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,105 @@
# Changelog

## [Non rilasciato]

### Sicurezza
- **XSS nell'HTML generato.** Titolo, domande, opzioni e celle di tabella
venivano interpolati grezzi: un `.tex` contenente `<script>` produceva
JavaScript eseguito sull'origine dell'app. Ora ogni testo proveniente dal
sorgente passa da un escaping sistematico, e i frammenti generati dal server
sono tenuti separati da quelli non fidati.
- **Output non più esposti sul server.** Erano salvati in `backend/static` con
un nome derivato da `Math.random()`: indovinabile ed enumerabile, quindi i
documenti di un utente erano potenzialmente raggiungibili da altri. Ora gli
output viaggiano dentro la risposta di `/compile`, la cartella di lavoro
viene cancellata prima dell'invio e la rotta `/static` non esiste più.
- **Anteprima isolata.** L'HTML generato non viene più aperto sull'origine
dell'app: si prova dentro un iframe `sandbox="allow-scripts"`, quindi con
un'origine opaca che non può accedere alla pagina ospite.
- **Nessuna richiesta a terze parti.** L'HTML generato caricava MathJax da
`cdn.jsdelivr.net`: ogni apertura del file rivelava l'IP di chi lo leggeva a
un soggetto esterno, e senza rete le formule restavano testo grezzo.
- Aggiunte intestazioni di sicurezza: `Content-Security-Policy`,
`X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`,
`Cross-Origin-Opener-Policy`, `Permissions-Policy`.
- Aggiunti rate limiting per IP su `/compile` (12 al minuto), allowlist delle
estensioni caricabili e tetto complessivo di 20 MB per richiesta.
- `/api/download/package.zip` scriveva sempre sullo stesso file: due download
simultanei potevano corrompersi a vicenda. Ora ogni richiesta usa una
cartella propria.

### Aggiunto — formule
- Renderer matematico lato server, senza dipendenze esterne: frazioni, radici,
apici e pedici, lettere greche, operatori grandi (∫ ∑ ∏), funzioni e circa
cento simboli. Funziona offline e non contatta nessuno.
- Il PDF usa ora il font Unicode DejaVu (già presente nel repo ma mai
utilizzato, come `@pdf-lib/fontkit`): i simboli matematici compaiono come
tali invece di essere degradati in ASCII. Se il font manca, il fallback ASCII
resta attivo.

### Aggiunto — deploy
- Configurazione Vercel: `vercel.json` e `api/index.js`. Nessuna scrittura
avviene più nella cartella del progetto (su Vercel è di sola lettura): i
temporanei stanno sotto `/tmp`.

### Aggiunto — legale
- Pagine `/termini` e `/privacy`, collegate dai footer, scritte per
corrispondere al comportamento effettivo del codice.

### Corretto
- **Il frontend non era nel repository**: la regola `*.html` in `.gitignore`,
pensata per gli output di `htlatex`, escludeva anche `frontend/index.html` e
`docs/index.html`. Da un clone pulito la homepage rispondeva 404. Ora la
regola è mirata (`latex-source/*.html`, `esempi/*.html`) e le due pagine sono
tracciate.
- **CI**: il workflow testava un backend Flask (`backend/app.py`) che non
esiste più, e falliva sempre. Riscritto per lo stack Node/Express, con smoke
test su frontend, documentazione, API, compilazione singola e multipla.
- **Deploy**: `deploy-vps.sh` invocava `docker compose` senza che esistessero
`Dockerfile` e `docker-compose.yml`. Ora ci sono entrambi.
- **Porta**: nginx faceva proxy verso la 5000, ma il server ascolta sulla 3000.
- **`quizstruct.sty`** usa `itemize[label=...]` senza dichiarare `enumitem`:
funzionava solo perché ogni esempio lo importava a mano. Ora il pacchetto
è autosufficiente.
- **Service worker**: precacheava `/script.js`, che nel frontend non è il file
dell'app (è `app.js`), e `/`, che non esisteva. Il precache falliva in blocco,
quindi la modalità offline non funzionava.
- **`release.yml`** pubblicava `esempi/quiz.tex`, che si trova in `latex-source/`.
- Escape rotti nei nomi dei comandi in questo changelog (`\rispostacorretta`,
`\tendina`, `\numerica`, `\risultato`).

### Aggiunto
- `Dockerfile` (Node 22 + TeX Live + tex4ht, utente non-root, healthcheck) e
`docker-compose.yml`
- `.dockerignore`
- Sito di documentazione su `/docs`: comandi di `quizstruct.sty`, i quattro tipi
di domanda, esempi copiabili, istruzioni di installazione
- Sezione API nel README
- La variabile d'ambiente `PORT` è ora rispettata dal server

### Modificato
- README e CONTRIBUTING allineati allo stack reale (Node/Express sulla 3000,
non Flask sulla 5000): installazione, struttura, deploy e flusso
- Rimossa da `metadata.json` la capability `SERVER_SIDE_GEMINI_API`, che il
codice non usa
- `.gitignore`: aggiunte le voci Node
- Rimossi dal repository file di scarto: un PDF compilato, ZIP di output,
risposte API salvate (`test_fisica*.json`) e uno script di prova la cui
logica è già in `server.js`

## [1.0.0] — 2026-07-11

### Aggiunto
- Backend Flask con endpoint `POST /compile`
- Backend con endpoint `POST /compile`
- Compilazione PDF via `pdflatex` (2 passate)
- Compilazione HTML via `htlatex` + post-processing (UTF-8, CSS/JS inline)
- Frontend drag&drop con accumulo file e rimozione singola
- Pacchetto `quizstruct.sty`: `\domanda`, `\opzione`, `ispostacorretta`, ` endina`, `
umerica`, `isultato`
- Pacchetto `quizstruct.sty`: `\domanda`, `\opzione`, `\rispostacorretta`,
`\tendina`, `\numerica`, `\risultato`
- Config `quiz.cfg` per conversione LaTeX→HTML interattiva
- `quiz.js` per validazione risposte e calcolo punteggio
- `quiz.css` con stile tipografico moderno
- Auto-load di `quizstruct.sty`/`quiz.cfg`/`quiz.js`/`quiz.css` se non caricati
- Dockerfile per ambiente isolato (TeX Live + tex4ht + Flask)
- Esempi: `quiz.tex`, `matematica.tex`, `lezione.tex`
- PWA: manifest, service worker, icone, installabile offline

Expand Down
Loading
Loading