Skip to main content

Summernote

image.png

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
<?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 &lt;script&gt; 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:

HTML
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:

  1. El navegador intentarà carregar la imatge "x".

  2. Fallarà.

  3. Executarà l'esdeveniment onerror.

  4. El codi JavaScript s'executarà al navegador de tots els visitants que vegin el post, permetent robar sessions.


Solució: Sanitization


Opció A: Sanejament Natiu (Dèbil)

PHP té strip_tags(), però és massa radical o difícil de configurar per permetre atributs complexos (com style o src). CI4 té esc(), però serveix per mostrar text pla, no HTML ric.

 

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:

Bash
composer require ezyang/htmlpurifier

Controlador + HTMLPurifier

Fitxer: app/Controllers/PostController.php

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.

HTML
<!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.

HTML
<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

PHP
$routes->get('posts/create', 'PostController::create');
$routes->post('posts/store', 'PostController::store');
$routes->get('posts', 'PostController::index');


  1. Entrada: L'usuari posa un <script> maliciós al Summernote.

  2. Enviament: El navegador envia el text amb el virus al PostController.

  3. Filtrat: HTMLPurifier analitza el text. Veu l'etiqueta <script>, sap que no està permesa a la seva configuració per defecte, i l'esborra completament.

  4. Emmagatzematge: A la base de dades es guarda només el text net i segur.

  5. Sortida: Quan mostres <?= $post['body'] ?>, el codi és segur i no s'executa res estrany.