🎯 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)
🗄️ 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.
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.
🎯 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.
🎨 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.
🛡️ 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.
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.
🧩 Il flusso completo, dall'inizio alla fine
| Passo | Cosa abbiamo costruito |
| 1 | Tabella articoli con MEDIUMTEXT per contenuto HTML voluminoso |
| 2 | config.php: connessione condivisa |
| 3 | nuovo.php/visualizza.php con textarea semplice, htmlspecialchars() in output |
| 4 | TinyMCE sostituisce il textarea; visualizza.php mostra HTML grezzo (falla di sicurezza aperta) |
| 5 | sanificaHtml() con whitelist di tag, applicata prima del salvataggio; falla chiusa |
| 6 | Plugin 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().
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