Ajouter une galerie d’images à un éditeur de texte enrichi avec ASP.NET MVC 5

La boîte de dialogue Insert Gallery de RichTextEditor liste les images de votre serveur, ouvre des dossiers et téléverse dans le dossier ouvert. Sous ASP.NET MVC 5, elle a besoin d’une action de contrôleur qui renvoie du JSON pour GET /rte/gallery?folder=.... Deux comportements par défaut de MVC gênent : Json() refuse les requêtes GET, et la validation des requêtes transforme les noms de dossiers inhabituels en pages HTML d’erreur 500. Cette page présente l’action avec ces deux points réglés.
Activer la galerie
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "@Url.Content("~/rte/gallery")",
maxUploadFileSize: 4000000 // same limit as the server
});
</script>galleryEndpoint est l’URL qu’interroge la boîte de dialogue. maxUploadFileSize reprend la limite de 4,000,000 octets du serveur, pour que la boîte de dialogue refuse un fichier trop volumineux avant de l’envoyer. La route rte/gallery mène à l’action Gallery de RteController. Elle est déclarée dans Global.asax.cs, à côté de la route d’envoi décrite dans l’envoi d’images avec MVC 5.
L’action
[HttpGet]
public ActionResult Gallery(string folder)
{
try
{
return JsonStatus(200, RteFiles.Listing(HttpContext, folder));
}
catch (RteException ex)
{
return JsonStatus(ex.Status, new { error = ex.Message });
}
}RteFiles.Listing résout le dossier virtuel à l’intérieur de uploads/. Il renvoie les sous-dossiers et les fichiers .jpg, .jpeg, .png, .gif et .webp, triés par nom :
public static object Listing(HttpContextBase 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
};
}Json() et GET
Json(data) utilise par défaut JsonRequestBehavior.DenyGet. Nous l’avons essayé avec une action jetable. Un GET a renvoyé une erreur HTTP 500 avec « This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet. ». La galerie doit passer par un GET : le contrôleur construit donc son JsonResult à un seul endroit :
// Json() refuses GET unless told otherwise (JsonRequestBehavior.DenyGet is the default)
// and caps output at 2,097,152 characters; both are lifted here. Property names are
// written as declared, so the anonymous objects in RteFiles produce camelCase JSON.
JsonResult JsonStatus(int status, object data)
{
Response.StatusCode = status;
// Without this, IIS replaces 4xx bodies with its own error page for remote users.
Response.TrySkipIisCustomErrors = true;
// Forms authentication would turn a 401 into a 302 to the login page; keep the 401.
Response.SuppressFormsAuthenticationRedirect = true;
Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.SetNoStore();
return new JsonResult
{
Data = data,
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
MaxJsonLength = int.MaxValue
};
}AllowGet: le message de MVC nomme le risque, à savoir qu’un autre site lise une réponse GET. Ici, la réponse est toujours un objet, jamais un tableau au premier niveau. Elle ne liste que ce que la boîte de dialogue affiche de toute façon. Si cette liste est privée, placez la route derrière une connexion, comme pour les dossiers par utilisateur ci-dessous.MaxJsonLength:JsonResultutiliseJavaScriptSerializer. D’après la documentation de MVC, une valeur nulle correspond à la valeur par défaut du sérialiseur, 2,097,152 caractères, qu’un dossier contenant des milliers d’images pourrait dépasser. Nous n’avons pas mesuré cette limite.JavaScriptSerializerécrit les noms exactement tels qu’ils sont déclarés. Les objets anonymes deRteFilessont en camelCase : le JSON correspond donc à ce qu’attend la boîte de dialogue sans aucun réglage.TrySkipIisCustomErrorsempêche IIS de remplacer le corps d’une réponse 4xx par sa propre page d’erreur pour les visiteurs distants (avecerrorMode="Custom").SuppressFormsAuthenticationRedirectcompte dès que vous ajoutez des dossiers par utilisateur.

[ValidateInput(false)] et la valeur folder
MVC exécute la validation des requêtes d’ASP.NET pendant qu’il lie string folder. Nous avons retiré [ValidateInput(false)] du contrôleur pour un test. GET /rte/gallery?folder=<b> a alors renvoyé une erreur 500 en HTML (« A potentially dangerous Request.QueryString value was detected from the client ») avant même l’exécution de l’action. Avec l’attribut, la valeur atteint RteFiles, qui n’accepte que des noms de dossiers simples, et la réponse est 400 {"error":"Invalid folder."}. Désactiver la validation est sans risque ici, car rien de ce qui vient de la requête n’est utilisé sans passer ces règles.
Les URL des images
L'url de chaque image est /uploads/<folder>/<file>, construite avec VirtualPathUtility.ToAbsolute, et IIS la sert comme fichier statique. Le routage MVC ne gêne pas : RouteExistingFiles vaut false par défaut, si bien que les fichiers existants sous /uploads/, /rte-files.js et le dossier /richtexteditor/ de l’éditeur échappent aux routes. Les noms de dossiers peuvent contenir des espaces et des lettres non ASCII, et chaque segment de chemin est encodé en pourcentage. IIS Express n’a pas de type MIME pour .webp : le web.config de l’exemple en ajoute un. Sans lui, une image .webp est listée, mais son URL répond 404.3.
Tester
Démarrez le projet d’exemple de ce guide avec run.ps1 (port 5402). Créez un dossier team avec le bouton New folder de la boîte de dialogue, puis :
curl "http://localhost:5402/rte/gallery?folder=team"
{"currentFolder":"team","currentFolderDisplay":"/team","parentFolder":"","folders":[],"images":[]}
curl "http://localhost:5402/rte/gallery?folder=..%2F"
{"error":"Invalid folder."}HTTP 200, puis HTTP 400. Dans le navigateur, Insert Gallery 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 · Web Forms