Progetto Guidato — Integrare un Editor WYSIWYG in PHP

Dal semplice textarea all'HTML formattato e sanificato, passo dopo passo · Marco Consiglio
Volta Institute · Napoli
A.A. 2026
0

Punto di partenza — cosa costruiamo

🎯 Obiettivo del progetto
Un editor WYSIWYG ("What You See Is What You Get") permette a chi scrive di formattare il testo (grassetto, elenchi, link) senza scrivere HTML a mano, vedendo subito il risultato finale. Partiremo da un semplice <textarea> che salva testo grezzo, poi lo sostituiremo con un editor visuale (TinyMCE, via CDN), imparando perché l'HTML che arriva dall'editor va sempre sanificato prima di essere salvato e mostrato.
📁 Struttura delle cartelle del progetto
editor-wysiwyg/ ├── config.php // connessione al database (Passo 2) ├── nuovo.php // form di scrittura (Passo 3, poi con editor al Passo 4) └── visualizza.php // mostra il contenuto salvato (Passo 3)
1

Il database — una tabella per gli articoli

🗄️ Schema minimo
CREATE TABLE articoli ( id INT AUTO_INCREMENT PRIMARY KEY, titolo VARCHAR(200) NOT NULL, contenuto MEDIUMTEXT NOT NULL, creato_il TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
💡 MEDIUMTEXT invece di TEXT: un editor WYSIWYG genera HTML con molti tag e attributi, potenzialmente più voluminoso del testo semplice equivalente — MEDIUMTEXT supporta fino a 16MB contro i ~64KB di TEXT.
✅ Checkpoint — a questo punto hai
Una tabella pronta a ricevere contenuto HTML, nessun dato ancora.
2

Connessione e prima pagina di test

config.php
<?php $host = 'localhost'; $db = 'editor_volta'; $user = 'root'; $password = getenv('DB_PASSWORD'); try { $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $password); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { die("Connessione fallita: " . $e->getMessage()); } ?>
visualizza.php (versione di TEST, verrà estesa al Passo 3)
<?php require 'config.php'; echo "Connessione riuscita al database $db!"; ?>
✅ Checkpoint — a questo punto hai: apri visualizza.php. Messaggio di conferma = si può proseguire.
3

Funzionalità core — salvare e mostrare testo semplice

🎯 Cosa costruiamo in questo passo
Prima dell'editor visuale, verifichiamo che il meccanismo di base funzioni con un semplice <textarea>: nessuna formattazione, solo testo grezzo salvato ed escapato correttamente in visualizzazione.
nuovo.php
<?php require 'config.php'; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $titolo = $_POST['titolo'] ?? ''; $contenuto = $_POST['contenuto'] ?? ''; $stmt = $pdo->prepare("INSERT INTO articoli (titolo, contenuto) VALUES (:titolo, :contenuto)"); $stmt->execute(['titolo' => $titolo, 'contenuto' => $contenuto]); header('Location: visualizza.php?id=' . $pdo->lastInsertId()); exit; } ?> <form method="post"> <input type="text" name="titolo" placeholder="Titolo" required> <textarea name="contenuto" rows="10" required></textarea> <button type="submit">Salva</button> </form>
visualizza.php (sostituisce la versione di test del Passo 2)
<?php require 'config.php'; $id = (int) ($_GET['id'] ?? 0); $stmt = $pdo->prepare("SELECT titolo, contenuto FROM articoli WHERE id = :id"); $stmt->execute(['id' => $id]); $articolo = $stmt->fetch(PDO::FETCH_ASSOC); ?> <h1><?= htmlspecialchars($articolo['titolo']) ?></h1> <p><?= nl2br(htmlspecialchars($articolo['contenuto'])) ?></p>
💡 htmlspecialchars() qui è corretto e sufficiente: il contenuto è testo semplice, quindi ogni carattere HTML digitato per errore va mostrato letteralmente, non interpretato — è esattamente il comportamento visto nella guida PHP Form/Superglobali.
✅ Checkpoint — a questo punto hai: un articolo con testo semplice, salvato e mostrato correttamente. Nessuna formattazione (grassetto, elenchi) possibile ancora — il Passo 4 introduce l'editor visuale che cambia questa situazione.
4

Integrare l'editor — da textarea a WYSIWYG

🎨 Sostituire il textarea con TinyMCE
TinyMCE è una libreria JavaScript che trasforma un <textarea> esistente in un editor visuale con toolbar, senza modificare come i dati arrivano al server: il nome del campo POST resta lo stesso.
nuovo.php (MODIFICA rispetto al Passo 3)
<!-- nell'HEAD della pagina, aggiungiamo lo script della libreria: --> <script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js"></script> <script> tinymce.init({ selector: '#contenuto', plugins: 'lists link', toolbar: 'bold italic | bullist numlist | link' }); </script> <!-- il form resta identico, solo il textarea riceve un id --> <form method="post"> <input type="text" name="titolo" placeholder="Titolo" required> <textarea id="contenuto" name="contenuto" rows="10" required></textarea> <button type="submit">Salva</button> </form>
💡 La logica PHP di nuovo.php scritta al Passo 3 NON cambia: $_POST['contenuto'] continua a funzionare identicamente, perché TinyMCE si limita a sostituire visivamente il textarea, sincronizzando il suo contenuto HTML nel campo originale al momento dell'invio del form.
visualizza.php (MODIFICA: mostrare l'HTML, non più testo semplice)
<?php // il contenuto ora contiene HTML generato dall'editor (es. <p>, <strong>, <ul>) ?> <h1><?= htmlspecialchars($articolo['titolo']) ?></h1> <div><?= $articolo['contenuto'] ?></div> // NOTA: qui NON usiamo più htmlspecialchars() — vedremo perché al Passo 5
⚠️ Rimuovendo htmlspecialchars() per permettere la formattazione, abbiamo aperto una falla di sicurezza: qualsiasi HTML (incluso <script>) verrebbe ora eseguito dal browser. Il Passo 5 risolve questo problema.
5

Sanificazione e sicurezza — permettere formattazione, bloccare script

🛡️ Il problema: HTML "voluto" contro HTML pericoloso
A differenza di htmlspecialchars() (che neutralizza OGNI tag), qui vogliamo permettere alcuni tag (<strong>, <ul>, <a>) e bloccarne altri (<script>, gestori di eventi come onclick). Questo si chiama sanificazione con whitelist.
nuovo.php (MODIFICA: sanificare PRIMA di salvare)
<?php require 'config.php'; function sanificaHtml(string $html): string { $tagConsentiti = '<p><br><strong><em><ul><ol><li><a><h1><h2><h3>'; return strip_tags($html, $tagConsentiti); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $titolo = $_POST['titolo'] ?? ''; $contenuto = sanificaHtml($_POST['contenuto'] ?? ''); $stmt = $pdo->prepare("INSERT INTO articoli (titolo, contenuto) VALUES (:titolo, :contenuto)"); $stmt->execute(['titolo' => $titolo, 'contenuto' => $contenuto]); header('Location: visualizza.php?id=' . $pdo->lastInsertId()); exit; } ?> // il resto del file (query INSERT già vista, form con TinyMCE) resta invariato
💡 strip_tags($html, $tagConsentiti) rimuove OGNI tag non presente nella whitelist, incluso <script> — a differenza di htmlspecialchars() che neutralizza tutto indiscriminatamente, questa funzione permette selettivamente la formattazione desiderata.
⚠️ Il limite di strip_tags() — attributi pericolosi nei tag consentiti
strip_tags() da sola NON rimuove attributi pericolosi da un tag consentito: <a href="javascript:alert(1)"> o <p onclick="..."> passerebbero comunque, perché <a> e <p> sono nella whitelist.
⚠️ Per una sanificazione realmente robusta in produzione, servirebbe una libreria dedicata (es. HTML Purifier) che controlla anche gli attributi consentiti per ciascun tag — strip_tags() da sola è un primo livello di difesa, utile didatticamente ma insufficiente da sola in un progetto reale esposto a utenti non fidati.
6

Funzionalità avanzate — upload immagini e conteggio parole

nuovo.php (ESTENSIONE: plugin immagini nella toolbar)
<?php // il file HTML/JS: aggiungiamo il plugin 'image' alla configurazione TinyMCE del Passo 4 ?> <script> tinymce.init({ selector: '#contenuto', plugins: 'lists link image', toolbar: 'bold italic | bullist numlist | link image' }); </script>
💡 Aggiungere una funzionalità all'editor (qui: inserimento immagini) richiede una sola riga aggiuntiva nella configurazione JavaScript — il resto della pipeline (salvataggio, sanificazione) del Passo 5 resta invariato, perché <img> andrebbe semplicemente aggiunto alla whitelist di sanificaHtml().
visualizza.php (ESTENSIONE: conteggio parole)
<?php // aggiunta dopo il recupero dell'articolo (Passo 3-4): $testoSenzaTag = strip_tags($articolo['contenuto']); $numeroParole = str_word_count($testoSenzaTag); ?> <p>Tempo di lettura stimato: <?= ceil($numeroParole / 200) ?> min</p>
✅ Checkpoint — a questo punto hai: strip_tags() qui usata senza whitelist (secondo argomento omesso) per un obiettivo diverso da quello del Passo 5: contare le parole del testo puro, ignorando completamente i tag HTML — stessa funzione, scopo diverso.
7

Integrazione finale — come tutto si tiene insieme

🧩 Il flusso completo, dall'inizio alla fine
PassoCosa abbiamo costruito
1Tabella articoli con MEDIUMTEXT per contenuto HTML voluminoso
2config.php: connessione condivisa
3nuovo.php/visualizza.php con textarea semplice, htmlspecialchars() in output
4TinyMCE sostituisce il textarea; visualizza.php mostra HTML grezzo (falla di sicurezza aperta)
5sanificaHtml() con whitelist di tag, applicata prima del salvataggio; falla chiusa
6Plugin immagini nell'editor; conteggio parole con strip_tags() senza whitelist
Nota il cambio di strategia tra Passo 3 e Passo 5: htmlspecialchars() (blocca tutto) è corretto per testo semplice, strip_tags() con whitelist (blocca selettivamente) è necessario quando si vuole permettere HTML formattato — usare la funzione sbagliata in uno dei due contesti produrrebbe o testo non formattato o una falla XSS.
🚀 Da qui in avanti
Estensioni naturali non trattate qui: salvataggio automatico (autosave) mentre si scrive, cronologia delle revisioni di un articolo, sanificazione più robusta con una libreria dedicata invece del solo strip_tags().
🎓 Esercitazioni pratiche
1
Aggiungi il tag immagine alla whitelist
Estendi sanificaHtml() del Passo 5 aggiungendo <img> alla whitelist di tag consentiti, coerentemente con il plugin immagini introdotto al Passo 6.
⏱ 15 min📦 1 funzione modificata💻 Individuale
2
Contatore caratteri in tempo reale
Aggiungi JavaScript a nuovo.php che mostra il conteggio caratteri aggiornato mentre si scrive nell'editor TinyMCE (Passo 4), usando l'evento onKeyUp dell'API TinyMCE.
⏱ 30 min📦 1 file modificato💻 Individuale
3
Confronta l'HTML prima e dopo la sanificazione
Crea una pagina di debug che mostra fianco a fianco l'HTML grezzo inviato da TinyMCE e il risultato di sanificaHtml() (Passo 5), per verificare visivamente quali tag vengono effettivamente rimossi.
⏱ 20 min📦 1 nuovo file💻 Individuale