← Toute la documentation

Configurer le chemin de la galerie d’images

La boîte de dialogue Insert Gallery affiche les images déjà présentes sur votre serveur, pour qu’un rédacteur puisse en réutiliser une au lieu de l’envoyer de nouveau. Elle les obtient de l’une de deux façons : une liste fixe que vous placez dans la configuration, ou un point de terminaison qui vous appartient et qu’elle appelle à mesure que l’utilisateur passe d’un dossier à l’autre.

Une liste fixe

galleryImages prend un simple tableau. Chaque entrée peut être une URL, un tableau URL, nom, légende, ou un objet. Aucun code serveur n’intervient, et la boîte de dialogue n’affiche aucun dossier.

var config = {};

// The simplest gallery: a fixed list, no server code at all.
config.galleryImages = [
  "/imageuploads/roof.jpg",                                  // just the URL
  ["/imageuploads/atrium.jpg", "Atrium", "1600 x 900"],      // URL, name, caption
  {
    url: "/imageuploads/stair.jpg",
    thumbnail: "/imageuploads/thumbs/stair.jpg",             // optional, for a faster dialog
    name: "Stair core",
    meta: "440 KB",
    alt: "Timber stair core, looking up"         // optional; core 2.6.5+ writes it as the alt text
  }
];

var editor = new RichTextEditor("#div_editor", config);

Par défaut, cette propriété pointe vers quelques images de démonstration hébergées sur notre propre site. Remplacez-la : vos utilisateurs ne doivent pas se voir proposer les nôtres, et la boîte de dialogue ne doit pas dépendre de la disponibilité d’un autre domaine.

Définissez galleryEndpoint sur une URL qui vous appartient. La boîte de dialogue l’appelle à son ouverture, puis chaque fois que l’utilisateur ouvre un dossier, et elle affiche ce que renvoie cette URL.

var config = {};

// Browsable: the dialog asks this URL for each folder as the user opens it.
config.galleryEndpoint = "/rte/gallery";

// Keep galleryImages as well - it is what the dialog falls back to if the
// endpoint is unreachable, so the user sees a gallery rather than an error.
config.galleryImages = ["/imageuploads/roof.jpg"];

var editor = new RichTextEditor("#div_editor", config);

Ce que renvoie le point de terminaison

Un GET avec un paramètre folder renvoie les sous-dossiers et les images de ce dossier :

GET /rte/gallery?folder=2026/site-photos

{
  "currentFolder": "2026/site-photos",
  "currentFolderDisplay": "/2026/site-photos",
  "parentFolder": "2026",
  "folders": [
    { "name": "week-1", "folder": "2026/site-photos/week-1" }
  ],
  "images": [
    {
      "name": "roof.jpg",
      "folder": "2026/site-photos",
      "size": 448123,
      "url": "/imageuploads/2026/site-photos/roof.jpg",
      "alt": "Roof with new solar panels"
    }
  ]
}
  • currentFolder est le chemin qui vous a été demandé, tel que votre point de terminaison le comprend. Il est opaque pour l’éditeur : la boîte de dialogue se contente de vous le renvoyer.
  • currentFolderDisplay est ce que l’utilisateur voit dans la boîte de dialogue : ce peut donc être un nom convivial plutôt qu’un chemin.
  • parentFolder détermine l’entrée « remonter ». Utilisez null à la racine, et une chaîne vide un niveau en dessous — une chaîne vide signifie la racine, et non aucun parent.
  • url est le seul champ que conserve le document. C’est ce que demandera le navigateur du lecteur : elle doit donc fonctionner pour un visiteur qui n’est pas connecté à votre espace d’administration, et continuer à fonctionner une fois la page enregistrée puis rouverte ailleurs.
  • size est en octets et s’affiche à côté de l’image ; omettez-le si vous ne voulez pas interroger chaque fichier.

Si la requête échoue, la boîte de dialogue se rabat sur galleryImages et n’affiche aucun dossier — une faute de frappe dans l’URL du point de terminaison ressemble donc à une galerie étrangement courte plutôt qu’à une erreur. Consultez l’onglet Réseau du navigateur si vous voyez les images de démonstration.

Le bouton New Folder envoie un formulaire à la même URL :

POST /rte/gallery
Content-Type: application/x-www-form-urlencoded

action=create-folder&folder=2026%2Fsite-photos&name=week-2

-> the same JSON as a GET, listing the folder that was just created

Transformer un dossier en URL

C’est la partie qu’il vaut la peine de bien faire, et celle sur laquelle les deux moitiés doivent s’accorder : le gestionnaire d’envoi décide où un fichier est écrit, et la galerie décide à partir de quelle URL il est lu. Trois règles les gardent alignés.

  • Gardez une seule racine, avec un chemin physique et un préfixe d’URL. Chaque URL que vous renvoyez est composée du préfixe, du dossier et du nom de fichier. Déterminez le préfixe à l’exécution — en ASP.NET, VirtualPathUtility.ToAbsolute("~/imageuploads") — pour que la galerie fonctionne encore une fois déployée dans un répertoire virtuel.
  • Échappez chaque segment, et seulement les segments. Sinon, un dossier nommé Site photos ou un fichier nommé roof #2.jpg produit une URL cassée. Échappez les parties, puis joignez-les avec / — échapper tout le chemin échapperait aussi les séparateurs.
  • Résolvez le dossier et vérifiez qu’il se trouve dans la racine, avant de l’utiliser. La valeur folder vient du navigateur : traitez-la comme hostile. Rejetez tout segment vide, égal à . ou .., ou contenant un caractère invalide dans un nom de fichier, puis résolvez le chemin complet et vérifiez qu’il commence toujours par la racine. Sans cette dernière vérification, ?folder=../../App_Data liste un répertoire que vous n’aviez jamais l’intention de publier.

Tout cela vaut aussi pour le gestionnaire d’envoi : il reçoit la même valeur folder et doit la résoudre de la même façon. Partagez une seule fonction utilitaire entre les deux plutôt que d’écrire deux fois la correspondance.

ASP.NET Core

Deux points de terminaison, et les quatre fonctions utilitaires qu’ils partagent avec le point de terminaison d’envoi. Le dossier d’envoi se trouve sous wwwroot : UseStaticFiles sert donc les images, et aucun code ne s’exécute lorsqu’une image est affichée.

// ASP.NET Core minimal API. Add .RequireAuthorization() to both endpoints.
const string UploadUrl = "/imageuploads";               // served by UseStaticFiles
string uploadRoot = Path.GetFullPath(Path.Combine(app.Environment.WebRootPath, "imageuploads"));
Directory.CreateDirectory(uploadRoot);

app.MapGet("/rte/gallery", (string? folder) =>
{
    string relative = Normalize(folder);
    if (ResolveFolder(relative) is null) relative = "";   // fall back to the root
    return Results.Json(BuildPayload(relative));
});

app.MapPost("/rte/gallery", async (HttpRequest request) =>
{
    var form = await request.ReadFormAsync();
    if (form["action"] != "create-folder") return Results.BadRequest(new { error = "unsupported action" });

    string parent = Normalize(form["folder"]);
    string name = form["name"].ToString().Trim();
    if (ResolveFolder(parent) is null || !IsValidName(name))
        return Results.BadRequest(new { error = "invalid folder name" });

    string relative = parent.Length == 0 ? name : parent + "/" + name;
    Directory.CreateDirectory(Path.Combine(uploadRoot, relative));
    return Results.Json(BuildPayload(relative));
});

string Normalize(string? folder) => (folder ?? "").Replace('\\', '/').Trim('/');

bool IsValidName(string part) =>
    part.Length > 0 && part != "." && part != ".." && part.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;

// The physical directory for "a/b" inside uploadRoot, or null if it is invalid,
// missing, or outside it. This is the check that stops ?folder=../../web.config.
string? ResolveFolder(string relative)
{
    if (relative.Length == 0) return uploadRoot;
    if (relative.Split('/').Any(part => !IsValidName(part))) return null;
    string full = Path.GetFullPath(Path.Combine(uploadRoot, relative));
    bool inside = full.StartsWith(uploadRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
    return inside && Directory.Exists(full) ? full : null;
}

// folder + file name -> the URL the browser will request
string BuildUrl(string relative, string fileName) =>
    UploadUrl + "/" + string.Join("/",
        (relative.Length == 0 ? new[] { fileName } : relative.Split('/').Append(fileName))
        .Select(Uri.EscapeDataString));

object BuildPayload(string relative)
{
    string dir = ResolveFolder(relative)!;
    int slash = relative.LastIndexOf('/');
    return new
    {
        currentFolder = relative,
        currentFolderDisplay = "/" + relative,
        parentFolder = relative.Length == 0 ? null : (slash < 0 ? "" : relative.Substring(0, slash)),
        folders = Directory.GetDirectories(dir).Select(Path.GetFileName)
            .Select(n => new { name = n, folder = relative.Length == 0 ? n : relative + "/" + n }),
        images = Directory.GetFiles(dir)
            .Where(f => imageExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
            .Select(f => new FileInfo(f))
            .Select(f => new { name = f.Name, folder = relative, size = f.Length, url = BuildUrl(relative, f.Name) })
    };
}

ASP.NET Web Forms et MVC

Un gestionnaire générique, qui utilise une classe utilitaire partagée RteImageFolder contenant les méthodes de normalisation, de résolution, de construction d’URL et de construction de la charge utile. Pour MVC, placez les deux mêmes actions sur un contrôleur — /RTE/Gallery — et gardez la classe utilitaire telle quelle.

// RTEGallery.ashx - Web Forms. Put RteImageFolder.cs in App_Code.
//   GET  RTEGallery.ashx?folder=a/b                       list that folder
//   POST action=create-folder, folder=a/b, name=New       create it, then list it
public class RTEGallery : IHttpHandler
{
    const string RootVirtual = "~/imageuploads";

    public void ProcessRequest(HttpContext context)
    {
        // TODO: check that the current user may browse images and create folders.
        context.Response.ContentType = "application/json";
        string root = Path.GetFullPath(context.Server.MapPath(RootVirtual));
        Directory.CreateDirectory(root);

        string folder;
        if (context.Request.HttpMethod == "POST")
        {
            if (context.Request.Form["action"] != "create-folder") { Fail(context, "unsupported action"); return; }
            string parent = RteImageFolder.Normalize(context.Request.Form["folder"]);
            string name = (context.Request.Form["name"] ?? "").Trim();
            if (RteImageFolder.Resolve(root, parent) == null || !RteImageFolder.IsValidName(name))
            { Fail(context, "invalid folder name"); return; }
            folder = parent.Length == 0 ? name : parent + "/" + name;
            Directory.CreateDirectory(Path.Combine(root, folder.Replace('/', Path.DirectorySeparatorChar)));
        }
        else
        {
            folder = RteImageFolder.Normalize(context.Request.QueryString["folder"]);
        }

        // ToAbsolute turns "~/imageuploads" into the URL prefix, so the gallery
        // keeps working when the site is deployed under a virtual directory.
        string baseUrl = VirtualPathUtility.ToAbsolute(RootVirtual);
        context.Response.Write(new JavaScriptSerializer().Serialize(
            RteImageFolder.BuildPayload(root, baseUrl, folder)));
    }

    static void Fail(HttpContext context, string message)
    {
        context.Response.StatusCode = 400;
        context.Response.TrySkipIisCustomErrors = true;
        context.Response.Write(new JavaScriptSerializer().Serialize(new { error = message }));
    }

    public bool IsReusable { get { return false; } }
}

PHP

Le même contrat. realpath se charge de la résolution, et c’est la vérification avec strpos qui garde un folder forgé à l’intérieur de la racine d’envoi.

<?php
// rte-gallery.php
//   GET  rte-gallery.php?folder=a/b                      list that folder
//   POST action=create-folder, folder=a/b, name=New      create it, then list it
header("Content-Type: application/json; charset=utf-8");

// TODO: check that the current user may browse images and create folders.

$rootDir   = __DIR__ . DIRECTORY_SEPARATOR . "imageuploads";  // same folder as rte-upload.php
$rootUrl   = "/imageuploads";                                  // the URL that folder is served at
$imageExts = array("jpg", "jpeg", "jfif", "png", "gif", "webp", "bmp", "avif");

function gallery_valid_name($part) {
    return $part !== "" && $part !== "." && $part !== ".." && strpbrk($part, "/\\:*?\"<>|") === false;
}

// The real directory for "a/b", or false if it is invalid, missing, or outside the root.
function gallery_dir($rootReal, $folder) {
    if ($folder === "") return $rootReal;
    foreach (explode("/", $folder) as $part) {
        if (!gallery_valid_name($part)) return false;
    }
    $dir = realpath($rootReal . DIRECTORY_SEPARATOR . str_replace("/", DIRECTORY_SEPARATOR, $folder));
    return ($dir !== false && is_dir($dir) && strpos($dir, $rootReal . DIRECTORY_SEPARATOR) === 0) ? $dir : false;
}

// folder + file name -> the URL the browser will request
function gallery_url($rootUrl, $folder, $name) {
    $parts = $folder === "" ? array() : explode("/", $folder);
    $parts[] = $name;
    return $rootUrl . "/" . implode("/", array_map("rawurlencode", $parts));
}

if (!is_dir($rootDir)) mkdir($rootDir, 0755, true);
$rootReal = realpath($rootDir);

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    if ((isset($_POST["action"]) ? $_POST["action"] : "") !== "create-folder") {
        http_response_code(400); echo json_encode(array("error" => "unsupported action")); exit;
    }
    $parent    = trim(isset($_POST["folder"]) ? (string)$_POST["folder"] : "", "/");
    $name      = trim(isset($_POST["name"]) ? (string)$_POST["name"] : "");
    $parentDir = gallery_dir($rootReal, $parent);
    if ($parentDir === false || !gallery_valid_name($name)) {
        http_response_code(400); echo json_encode(array("error" => "invalid folder name")); exit;
    }
    $newDir = $parentDir . DIRECTORY_SEPARATOR . $name;
    if (!is_dir($newDir)) mkdir($newDir, 0755);
    $folder = $parent === "" ? $name : $parent . "/" . $name;
} else {
    $folder = trim(isset($_GET["folder"]) ? (string)$_GET["folder"] : "", "/");
}

$dir = gallery_dir($rootReal, $folder);
if ($dir === false) { $folder = ""; $dir = $rootReal; }   // fall back to the root

$folders = array();
$images  = array();
foreach (scandir($dir) as $entry) {
    if ($entry === "." || $entry === "..") continue;
    $path = $dir . DIRECTORY_SEPARATOR . $entry;
    if (is_dir($path)) {
        $folders[] = array("name" => $entry, "folder" => $folder === "" ? $entry : $folder . "/" . $entry);
    } elseif (in_array(strtolower(pathinfo($entry, PATHINFO_EXTENSION)), $imageExts, true)) {
        $images[] = array(
            "name"   => $entry,
            "folder" => $folder,
            "size"   => filesize($path),
            "url"    => gallery_url($rootUrl, $folder, $entry),
        );
    }
}

$slash = strrpos($folder, "/");
echo json_encode(array(
    "currentFolder"        => $folder,
    "currentFolderDisplay" => "/" . $folder,
    "parentFolder"         => $folder === "" ? null : ($slash === false ? "" : substr($folder, 0, $slash)),
    "folders"              => $folders,
    "images"               => $images,
));

Envoyer dans le dossier parcouru

La galerie a son propre bouton Upload, et il n’utilise pas config.file_upload_handler. Il cherche d’abord window.richTextBoxUploadFile, puis se rabat sur window.rte_file_upload_handler. Seule la première forme est informée du dossier ouvert : définissez-la si vous voulez que les envois arrivent là où regarde l’utilisateur.

// Lets the gallery's Upload button put the file in the folder being browsed.
// Without this the gallery falls back to rte_file_upload_handler, which knows
// nothing about folders, and every upload lands in the top-level folder.
window.richTextBoxUploadFile = function (file, callback, options) {
  var folder = (options && options.folder) || "";
  var data = new FormData();
  data.append("fileforphp", file);
  var xh = new XMLHttpRequest();
  xh.open("POST", "/rte-upload.php?folder=" + encodeURIComponent(folder), true);
  xh.onload = function () {
    var text = xh.responseText || "";
    if (xh.status === 200 && text.indexOf("READY:") === 0) callback(text.substring(6));
    else callback(null, text.indexOf("ERROR:") === 0 ? text.substring(6) : "http-" + xh.status);
  };
  xh.onerror = function () { callback(null, "network-error"); };
  xh.send(data);
};

Votre point de terminaison d’envoi doit résoudre folder exactement comme le fait la galerie, et renvoyer READY: suivi de l’URL construite par le même code que celui qui construit les URL de la galerie. Si les deux divergent, une image qui vient d’être envoyée apparaît dans le document mais pas dans la galerie à côté.

Avant de la mettre entre les mains des utilisateurs

  • Exigez un utilisateur connecté sur les deux points de terminaison. La galerie liste des noms de fichiers et crée des répertoires. Laissée anonyme, c’est une liste du contenu de votre dossier d’envoi que n’importe qui peut lire, et un moyen pour n’importe qui d’y créer des dossiers.
  • Limitez la racine par utilisateur ou par locataire lorsque les documents ne sont pas partagés. Prenez le client ou l’utilisateur dans la session, jamais dans un paramètre de requête, et intégrez-le à la racine physique plutôt qu’au chemin navigable.
  • Ne renvoyez que des images. Filtrez par extension lors de la liste, et faites vérifier au point de terminaison d’envoi les premiers octets du fichier en plus de son extension, pour qu’un script renommé en .png ne soit jamais stocké. Servez le dossier d’envoi avec X-Content-Type-Options: nosniff et sans exécution de scripts.
  • Ne réutilisez jamais le nom envoyé par le navigateur. Générez le nom du fichier stocké ; conservez l’original uniquement comme légende si vous en avez besoin.

Vous venez du contrôle ASP.NET ?

Dans l’ancien contrôle serveur, ces chemins venaient de SetSecurity et des fichiers de politique .config, qui définissaient aussi des galeries séparées pour la vidéo, les documents et les modèles. L’éditeur JavaScript n’a pas de fichiers de politique ni de partie serveur pour les lire : c’est le point de terminaison ci-dessus qui porte désormais cette configuration, et la galerie d’images est la seule navigable — les vidéos et les documents sont insérés par URL ou via le gestionnaire d’envoi.

Pour aller plus loin