Summernote
Aquest és un exemple complet d'implementació d'un sistema de "Posts" (articles) amb l'editor Summernote, posant èmfasi en la seguretat.
Migració
Necessitem una taula que pugui guardar text llarg. El tipus de dada TEXT o LONGTEXT és l'adequat per al contingut HTML que generarà l'editor.
Fitxer: app/Database/Migrations/2026-09-01-000001_CreatePostsTable.php
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreatePostsTable extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'body' => [ // Aquí es guardarà l'HTML del Summernote
'type' => 'TEXT',
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->createTable('posts');
}
public function down()
{
$this->forge->dropTable('posts');
}
}
Problema: Seguretat XSS (Cross-Site Scripting)
Abans de veure el Controlador, hem d'entendre el perill, quan utilitzes textarea normal, normalment fas esc($text) en mostrar-lo per convertir <script> en <script> i que no s'executi.
Però amb Summernote, volem que es guardi i es mostri HTML (negretes, imatges, llistes).
L'Exemple del Problema:
Imagina que un usuari malintencionat, en lloc d'escriure un article, obre la vista de codi de Summernote (botó </>) i escriu:
Hola, mira aquesta imatge:
<img src="x" onerror="alert('He robat les teves cookies: ' + document.cookie)">
Si guardes això tal qual i ho mostres a la teva web sense netejar:
Solució: Sanitization
Opció A: Sanejament Natiu (Dèbil)
Opció B: HTMLPurifier (La Solució Professional)
És una llibreria externa que reescriu l'HTML, eliminant qualsevol codi maliciós (<script>, onclick, iframe desconeguts) però mantenint el format segur (<b>, <p>, <img>).
Instal·lació:
Obre el terminal a l'arrel del teu projecte:
composer require ezyang/htmlpurifier
Controlador + HTMLPurifier
Fitxer: app/Controllers/PostController.php
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use App\Models\PostModel; // Assumeix que has creat el model bàsic
class PostController extends BaseController
{
// Mostra el formulari
public function create()
{
return view('posts/create');
}
// Guarda el post
public function store()
{
$title = $this->request->getPost('title');
$rawHtml = $this->request->getPost('body');
// --- INICI DEL SANEJAMENT ---
// 1. Configuració bàsica de HTMLPurifier
$config = \HTMLPurifier_Config::createDefault();
// Permetem youtube, imatges, negretes, etc.
// Si no posem res, la config per defecte és molt segura.
// Opcional: Permetre iframes només de Youtube
// $config->set('HTML.SafeIframe', true);
// $config->set('URI.SafeIframeRegexp', '%^https://www.youtube.com/embed/%');
$purifier = new \HTMLPurifier($config);
// 2. Netegem l'HTML brut que ve del Summernote
$cleanHtml = $purifier->purify($rawHtml);
// --- FI DEL SANEJAMENT ---
// Guardem a la BD el codi net
$model = new PostModel();
$model->save([
'title' => $title,
'body' => $cleanHtml
]);
return redirect()->to('/posts')->with('msg', 'Article publicat!');
}
public function index()
{
$model = new PostModel();
$data['posts'] = $model->findAll();
return view('posts/index', $data);
}
}
Vistes
Vista del Formulari (app/Views/posts/create.php)
Utilitzarem els CDN de Bootstrap i Summernote.
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<title>Nou Article</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<h2>Crear un nou article</h2>
<form action="<?= base_url('posts/store') ?>" method="post">
<?= csrf_field() ?>
<div class="form-group">
<label>Títol:</label>
<input type="text" name="title" class="form-control" required>
</div>
<div class="form-group">
<label>Contingut:</label>
<textarea id="summernote" name="body"></textarea>
</div>
<button type="submit" class="btn btn-primary">Publicar</button>
</form>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script>
<script>
$(document).ready(function() {
$('#summernote').summernote({
height: 300, // Alçada de l'editor
placeholder: 'Escriu aquí el teu contingut...',
tabsize: 2
});
});
</script>
</body>
</html>
Vista de llistat (app/Views/posts/index.php)
Aquí veurem el resultat.
<div class="container">
<h1>Articles recents</h1>
<?php foreach ($posts as $post): ?>
<div class="panel panel-default">
<div class="panel-heading">
<h3><?= esc($post['title']) ?></h3> </div>
<div class="panel-body">
<?= $post['body'] ?>
</div>
</div>
<?php endforeach; ?>
</div>
Rutes
Fitxer: app/Config/Routes.php
$routes->get('posts/create', 'PostController::create');
$routes->post('posts/store', 'PostController::store');
$routes->get('posts', 'PostController::index');
-
Entrada: L'usuari posa un
<script>maliciós al Summernote. -
Enviament: El navegador envia el text amb el virus al
PostController. -
Filtrat:
HTMLPurifieranalitza el text. Veu l'etiqueta<script>, sap que no està permesa a la seva configuració per defecte, i l'esborra completament. -
Emmagatzematge: A la base de dades es guarda només el text net i segur.
-
Sortida: Quan mostres
<?= $post['body'] ?>, el codi és segur i no s'executa res estrany.
