Ajouter une galerie d’images à un éditeur de texte enrichi en PHP

La boîte de dialogue Insert Gallery qui affiche les dossiers blog, products et team et quatre images du dossier uploads de l’exemple PHP, l’une d’elles sélectionnée

La boîte de dialogue Insert Gallery de l’éditeur peut afficher les images déjà présentes sur votre serveur : les utilisateurs choisissent ainsi une image au lieu d’envoyer de nouveau la même. Il lui suffit d’une URL qui répond en JSON. Cette page vous donne ce point de terminaison en PHP 8 pur : un script qui lit un dossier sous uploads/ avec scandir() et liste ses sous-dossiers et ses images.

Activer la galerie

Définissez galleryEndpoint lorsque vous créez l’éditeur. La boîte de dialogue demande alors une liste à cette URL chaque fois qu’elle s’ouvre ou change de dossier.

HTML · index.php
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css">
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<script src="/rte-files.js"></script>
<script>
    new RichTextEditor("#editor", {
        toolbar: "full",
        galleryEndpoint: "/rte/gallery",
        maxUploadFileSize: 4000000     // same limit as rte/upload.php, checked before sending
    });
</script>

rte-files.js est le petit fichier côté navigateur qui envoie les fichiers de la boîte de dialogue vers /rte/upload. Le guide sur l’envoi d’images en présente le côté serveur.

La route

GET /rte/gallery?folder=blog renvoie le contenu d’un dossier. Le script est court, car le travail est fait par deux fonctions utilitaires :

PHP · rte/gallery.php
if ($method === 'GET') {
    return rte_listing(rte_resolve_folder($_GET['folder'] ?? ''));
}

rte_resolve_folder() transforme le dossier virtuel de la chaîne de requête en un véritable répertoire à l’intérieur de la racine de stockage, ou répond 400 ou 404. Le guide sur les dossiers l’explique. rte_listing() construit le JSON.

Ce que reçoit la galerie

La boîte de dialogue a besoin du dossier courant, de son parent (null à la racine), des sous-dossiers, et des images avec leurs URL et leurs tailles :

La réponse JSON de GET /rte/gallery?folder=blog dans l’exemple PHP : currentFolder blog, parentFolder vide, aucun sous-dossier, et des images avec url, name, folder et size
La réponse réelle de l’exemple, réduite à deux images.

Voici le code qui la produit :

PHP · rte/files-lib.php
// The listing for one folder, in the shape the gallery dialog expects.
function rte_listing(array $folder): array
{
    $folders = [];
    $images = [];
    $names = scandir($folder['dir']);
    if ($names === false) {
        throw new RteHttpError(500, 'The folder cannot be read.');
    }
    foreach ($names as $name) {
        if ($name === '' || $name[0] === '.') {
            continue; // ".", "..", and hidden files such as .htaccess
        }
        $path = $folder['dir'] . DIRECTORY_SEPARATOR . $name;
        // Skip symlinks and Windows junctions. is_link() alone is not enough: PHP 8.2 on
        // Windows returns false for a junction, so also require realpath() to be unchanged.
        $real = realpath($path);
        $same = DIRECTORY_SEPARATOR === '\\' ? strcasecmp($real ?: '', $path) === 0 : $real === $path;
        if (is_link($path) || !$same) {
            continue;
        }
        $segments = [...$folder['segments'], $name];
        if (is_dir($path)) {
            if (rte_is_valid_name($name) && !rte_is_reserved($segments)) { // only folders the routes accept back
                $folders[] = ['folder' => implode('/', $segments), 'name' => $name];
            }
        } elseif (is_file($path) && in_array(rte_extension($name), RTE_IMAGE_EXTENSIONS, true)) {
            $images[] = [
                'url' => rte_url($folder['root'], $segments),
                'name' => $name,
                'folder' => $folder['virtual'],
                'size' => (int)filesize($path),
            ];
        }
    }
    $byName = fn(array $a, array $b): int => strcasecmp($a['name'], $b['name']) ?: strcmp($a['name'], $b['name']);
    usort($folders, $byName);
    usort($images, $byName);

    $virtual = $folder['virtual'];
    $parent = null;
    if ($virtual !== '') {
        $cut = strrpos($virtual, '/');
        $parent = $cut === false ? '' : substr($virtual, 0, $cut);
    }
    return [
        'currentFolder' => $virtual,
        'currentFolderDisplay' => '/' . $virtual,
        'parentFolder' => $parent,
        'folders' => $folders,
        'images' => $images,
    ];
}

Quelques choix méritent d’être connus :

  • Les noms qui commencent par un point sont ignorés : uploads/.htaccess n’apparaît donc jamais.
  • Seuls les fichiers .jpg .jpeg .png .gif .webp sont listés. Tout le reste du dossier reste invisible.
  • Les liens symboliques et les jonctions Windows sont ignorés. Sous Windows, le is_link() de PHP 8.2 a renvoyé false pour une jonction lors de notre test : le code vérifie donc aussi que realpath() laisse le chemin inchangé.
  • Chaque partie de l’URL d’une image passe par rawurlencode() : my photo.jpg est donc listé sous la forme /uploads/products/my%20photo.jpg.
  • Les dossiers et les images sont triés par nom sans tenir compte de la casse, avec strcasecmp().

Les formats proposés par la boîte de dialogue et acceptés par le serveur

La boîte de dialogue de la galerie de la version publiée indique « JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF » comme formats acceptés, et son sélecteur de fichiers les propose. Ce serveur en accepte cinq : .jpg .jpeg .png .gif .webp. Si quelqu’un choisit un fichier .bmp, la boîte de dialogue l’envoie, le serveur répond 400 {"error":"Only .jpg, .jpeg, .png, .gif and .webp images can be uploaded."}, et la boîte de dialogue affiche « Upload failed for photo.bmp. ». La raison donnée par le serveur figure dans le corps de la réponse, que vous pouvez consulter avec les outils de développement du navigateur.

La taille, c’est différent : avec maxUploadFileSize: 4000000, la boîte de dialogue refuse un gros fichier avant de l’envoyer. Un fichier de 4.5 Mo a donné « big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB. » (la boîte de dialogue compte en mégaoctets binaires).

Servir les images

La liste pointe vers /uploads/... : ces URL doivent donc renvoyer les fichiers. Dans le projet d’exemple, router.php les sert pour le serveur intégré de PHP. Il n’envoie que des extensions d’images, avec un Content-Type fixe et X-Content-Type-Options: nosniff, et n’exécute jamais rien depuis ce dossier. Sous Apache ou nginx, c’est le serveur web qui les sert directement. Conservez sa règle d’interdiction d’exécution, comme le montre le guide sur l’envoi sécurisé.

Tester

Démarrez le projet d’exemple de ce guide avec php -d upload_max_filesize=5M -d display_errors=0 -d log_errors=1 -S localhost:5201 router.php, puis demandez une liste :

Shell
curl "http://localhost:5201/rte/gallery?folder=team"
{"currentFolder":"team","currentFolderDisplay":"/team","parentFolder":"","folders":[],"images":[]}

curl "http://localhost:5201/rte/gallery?folder=blog/2027"
{"error":"Folder not found."}

La seconde requête reçoit un HTTP 404. Ouvrez http://localhost:5201/ et cliquez sur Insert Gallery : la boîte de dialogue affiche les dossiers et les images de la racine, et un clic sur un dossier l’ouvre.

Le même guide pour d’autres plateformes

JavaScript · Next.js · ASP.NET Core · Razor Pages · Blazor · MVC 5 · Web Forms

À lire ensuite