Ajouter une galerie d’images à un éditeur de texte enrichi avec Web Forms

La boîte de dialogue Insert Gallery de RichTextEditor peut lister les images de votre serveur, ouvrir des dossiers et téléverser dans le dossier ouvert. Il lui faut un point de terminaison qui répond en JSON. Sous ASP.NET Web Forms, ce point de terminaison est un IHttpHandler dans App_Code. Cette page présente le gestionnaire, le JSON qu’il écrit avec le sérialiseur dont dispose déjà .NET Framework, et deux comportements d’IIS qui cassent la galerie pour les visiteurs distants ou pour les fichiers .webp alors que tout semble fonctionner sur votre machine.
Activer la galerie
L’option galleryEndpoint dirige la boîte de dialogue vers le serveur. maxUploadFileSize utilise la même limite que le serveur, pour que la boîte de dialogue puisse refuser un fichier trop volumineux avant de l’envoyer :
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "<%= ResolveUrl("~/rte/gallery") %>",
maxUploadFileSize: 4000000 // same limit as the server
});
</script>La boîte de dialogue envoie GET /rte/gallery?folder=blog et attend les sous-dossiers et les images du dossier. Le chemin est associé au gestionnaire dans web.config, de la même façon que la route d’envoi décrite dans l’envoi d’images avec Web Forms.
Le gestionnaire
public void ProcessRequest(HttpContext context)
{
try
{
// Request.Unvalidated: a value such as "<b" would otherwise make ASP.NET request
// validation throw (HTTP 500) before our own rules can answer with a 400.
if (context.Request.HttpMethod == "GET")
{
string folder = context.Request.Unvalidated.QueryString["folder"];
RteFiles.WriteJson(context, 200, RteFiles.Listing(context, folder));
}
else if (context.Request.HttpMethod == "POST")
{
string action = context.Request.Unvalidated.Form["action"];
string folder = context.Request.Unvalidated.Form["folder"] ?? "";
if (action != "create-folder")
throw new RteException(400, "Unknown action.");
RteFiles.CreateFolder(context, folder, context.Request.Unvalidated.Form["name"]);
RteFiles.WriteJson(context, 200, RteFiles.Listing(context, folder));
}
else
{
context.Response.AppendHeader("Allow", "GET, POST");
throw new RteException(405, "Use GET or POST.");
}
}
catch (RteException ex)
{
RteFiles.WriteError(context, ex.Status, ex.Message);
}
}Il lit Request.Unvalidated.QueryString, et non Request.QueryString. La validation des requêtes d’ASP.NET s’applique aussi aux gestionnaires génériques. Avec folder=<b>, la collection validée a levé « A potentially dangerous Request.QueryString value was detected », et la boîte de dialogue a reçu une erreur HTML 500. La lecture non validée laisse les règles de dossier propres au gestionnaire répondre avec 400 {"error":"Invalid folder."}.
La liste
public static object Listing(HttpContext context, string folder)
{
folder = folder ?? "";
StorageRoot root = GetRoot(context);
string dir = ResolveFolder(root, folder);
List<string> folderNames = new List<string>();
foreach (string path in Directory.GetDirectories(dir))
{
string name = Path.GetFileName(path);
// Skip anything the dialog could not open again (hidden, odd names).
if (IsValidName(name) && (File.GetAttributes(path) & FileAttributes.Hidden) == 0)
folderNames.Add(name);
}
folderNames.Sort(CompareNames);
List<FileInfo> files = new List<FileInfo>();
foreach (FileInfo file in new DirectoryInfo(dir).GetFiles())
{
if (Array.IndexOf(ImageExtensions, file.Extension.ToLowerInvariant()) >= 0
&& (file.Attributes & FileAttributes.Hidden) == 0)
files.Add(file);
}
files.Sort(delegate (FileInfo a, FileInfo b) { return CompareNames(a.Name, b.Name); });
List<object> folders = new List<object>();
foreach (string name in folderNames)
folders.Add(new { folder = Combine(folder, name), name = name });
List<object> images = new List<object>();
foreach (FileInfo file in files)
images.Add(new { url = UrlFor(root, folder, file.Name), name = file.Name, folder = folder, size = file.Length });
string parent = null;
if (folder.Length > 0)
{
int slash = folder.LastIndexOf('/');
parent = slash < 0 ? "" : folder.Substring(0, slash);
}
return new
{
currentFolder = folder,
currentFolderDisplay = "/" + folder,
parentFolder = parent,
folders = folders,
images = images
};
}Seuls les fichiers .jpg, .jpeg, .png, .gif et .webp sont listés, et les dossiers comme les images sont triés par nom. Le tri met les deux noms en minuscules puis les compare de façon ordinale, car c’est l’ordre que donne une comparaison JavaScript avec toLowerCase(). StringComparer.OrdinalIgnoreCase passe au contraire en majuscules, ce qui place _ après les lettres au lieu d’avant.

Du JSON sans package NuGet
public static void WriteJson(HttpContext context, int status, object value)
{
HttpResponse response = context.Response;
response.Clear();
response.StatusCode = status;
// Without this, IIS replaces 4xx/5xx bodies with its own error page when
// <httpErrors errorMode="Custom"> is in effect (the default for remote users).
response.TrySkipIisCustomErrors = true;
// FormsAuthenticationModule turns every 401 into a 302 to the login page, which an
// XMLHttpRequest follows silently and then fails to parse. Keep the 401.
response.SuppressFormsAuthenticationRedirect = true;
response.ContentType = "application/json";
response.Cache.SetCacheability(HttpCacheability.NoCache);
response.Cache.SetNoStore();
// JavaScriptSerializer ships with .NET Framework (System.Web.Extensions) and writes
// property names exactly as declared, so the anonymous objects above are camelCase.
// Its default MaxJsonLength (2,097,152 chars) would fail a huge folder; lift it.
JavaScriptSerializer serializer = new JavaScriptSerializer();
serializer.MaxJsonLength = int.MaxValue;
response.Write(serializer.Serialize(value));
}JavaScriptSerializerest fourni avec .NET Framework dans System.Web.Extensions : App_Code peut donc l’utiliser sans dossierbin\. Il écrit les noms de propriétés exactement tels qu’ils sont déclarés. Les objets anonymes utilisent des noms en camelCase : le JSON est donc en camelCase sans aucun réglage. Il écritnullpour leparentFolderde la racine, et il échappe'en\u0027, ce qui reste du JSON valide.MaxJsonLengthvaut par défaut 2,097,152 caractères, et le réglagejsonSerializationde web.config ne s’applique pas ànew JavaScriptSerializer(). Un dossier contenant plusieurs milliers d’images lèverait une exception : l’exemple lève donc la limite.TrySkipIisCustomErrors. IIS Express utilise par défauterrorMode="DetailedLocalOnly": sur votre propre machine, un 400 JSON arrive donc intact dans tous les cas. Nous avons défini<httpErrors errorMode="Custom">, ce que reçoivent les visiteurs distants. Un gestionnaire qui définissait le statut 400 sans cet indicateur a alors vu son corps remplacé par la page HTML « Bad Request » d’IIS. Avec l’indicateur, le JSON est passé.- Pas de mise en cache. La liste change après chaque envoi : la réponse indique donc au navigateur de ne pas la conserver.
.webp sous IIS Express
Le fichier applicationhost.config d’IIS Express n’a pas de type MIME pour .webp. Un envoi .webp serait stocké et listé, mais son URL répondrait 404.3, et la vignette resterait vide. L’exemple ajoute le type :
<staticContent>
<!-- IIS Express (and older IIS) has no MIME type for .webp, so it would answer 404.3. -->
<remove fileExtension=".webp" />
<mimeMap fileExtension=".webp" mimeType="image/webp" />
</staticContent>Il utilise <remove> avant <mimeMap>, car une entrée en double sur un serveur qui connaît déjà .webp provoque une erreur de configuration 500.19.
Tester
Démarrez le projet d’exemple de ce guide avec run.ps1 (port 5401). Créez un dossier nommé team avec le bouton New folder de la boîte de dialogue, puis demandez-le :
curl "http://localhost:5401/rte/gallery?folder=team"
{"currentFolder":"team","currentFolderDisplay":"/team","parentFolder":"","folders":[],"images":[]}
curl "http://localhost:5401/rte/gallery?folder=..%2F"
{"error":"Invalid folder."}La première réponse est un HTTP 200 et la seconde un HTTP 400. Un dossier qui n’existe pas donne un 404. Dans le navigateur, cliquez sur Insert Gallery dans la barre d’outils. La boîte de dialogue affiche les dossiers et les images de uploads/.
Le même guide pour d’autres plateformes
JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · Blazor · MVC 5