Eine Bildergalerie für einen Rich-Text-Editor in ASP.NET MVC 5

Der Dialog Insert Gallery in RichTextEditor listet die Bilder auf Ihrem Server auf, öffnet Ordner und lädt in den geöffneten Ordner hoch. In ASP.NET MVC 5 braucht er eine Controller-Action, die für GET /rte/gallery?folder=... JSON zurückgibt. Zwei Standardeinstellungen von MVC stehen im Weg: Json() lehnt GET-Requests ab, und die Anforderungsüberprüfung macht aus ungewöhnlichen Ordnernamen HTML-Seiten mit 500. Diese Seite zeigt die Action, die beides berücksichtigt.
Die Galerie einschalten
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "@Url.Content("~/rte/gallery")",
maxUploadFileSize: 4000000 // same limit as the server
});
</script>galleryEndpoint ist die URL, die der Dialog abfragt. maxUploadFileSize wiederholt das Limit des Servers von 4,000,000 Bytes, sodass der Dialog eine zu große Datei ablehnt, bevor er sie sendet. Die Route rte/gallery führt zur Action Gallery von RteController. Sie wird in Global.asax.cs eingerichtet, neben der Upload-Route aus Bild-Upload in MVC 5.
Die 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 löst den virtuellen Ordner innerhalb von uploads/ auf. Es gibt die Unterordner und die Dateien .jpg, .jpeg, .png, .gif und .webp zurück, nach Namen sortiert:
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() und GET
Json(data) verwendet standardmäßig JsonRequestBehavior.DenyGet. Wir haben das mit einer Wegwerf-Action ausprobiert. Ein GET gab HTTP 500 zurück mit „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.“ Die Galerie muss ein GET sein, deshalb baut der Controller sein JsonResult an einer Stelle:
// 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: Die Meldung von MVC benennt das Risiko, nämlich dass eine andere Website eine GET-Antwort liest. Die Antwort ist hier immer ein Objekt, nie ein Array auf oberster Ebene. Sie listet nur auf, was der Dialog ohnehin anzeigt. Ist diese Liste privat, stellen Sie die Route hinter eine Anmeldung, wie bei den Ordnern pro Benutzer unten.MaxJsonLength:JsonResultverwendetJavaScriptSerializer. Laut Dokumentation von MVC bedeutet ein Nullwert den Standard des Serializers von 2,097,152 Zeichen, den ein Ordner mit Tausenden von Bildern überschreiten könnte. Dieses Limit haben wir nicht gemessen.JavaScriptSerializerschreibt Namen genau so, wie sie deklariert sind. Die anonymen Objekte inRteFilesverwenden camelCase, sodass das JSON ohne weitere Einstellungen dem entspricht, was der Dialog erwartet.TrySkipIisCustomErrorsverhindert, dass IIS den Body einer 4xx-Antwort für entfernte Besucher durch seine eigene Fehlerseite ersetzt (miterrorMode="Custom").SuppressFormsAuthenticationRedirectwird wichtig, sobald Sie Ordner pro Benutzer hinzufügen.

[ValidateInput(false)] und der Wert von folder
MVC führt die ASP.NET-Anforderungsüberprüfung aus, während es string folder bindet. Für einen Test haben wir [ValidateInput(false)] vom Controller entfernt. GET /rte/gallery?folder=<b> gab dann eine HTML-Seite mit 500 zurück („A potentially dangerous Request.QueryString value was detected from the client“), bevor die Action lief. Mit dem Attribut erreicht der Wert RteFiles, das nur einfache Ordnernamen akzeptiert, und die Antwort lautet 400 {"error":"Invalid folder."}. Die Überprüfung abzuschalten ist hier sicher, weil nichts aus dem Request verwendet wird, ohne diese Regeln zu bestehen.
Die Bild-URLs
Die url jedes Bildes ist /uploads/<folder>/<file>, gebildet mit VirtualPathUtility.ToAbsolute, und IIS liefert sie als statische Datei aus. Das Routing von MVC stört dabei nicht: RouteExistingFiles ist standardmäßig false, daher umgehen vorhandene Dateien unter /uploads/, /rte-files.js und der Ordner /richtexteditor/ des Editors die Routen. Ordnernamen dürfen Leerzeichen und Nicht-ASCII-Buchstaben enthalten, und jedes Pfadsegment wird prozentkodiert. IIS Express hat keinen MIME-Typ für .webp, deshalb ergänzt die web.config des Beispiels einen. Ohne ihn wird ein .webp-Bild aufgeführt, aber seine URL antwortet mit 404.3.
Testen
Starten Sie das Beispielprojekt zu dieser Anleitung mit run.ps1 (Port 5402). Legen Sie mit der Schaltfläche New folder des Dialogs einen Ordner team an, dann:
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, dann HTTP 400. Im Browser zeigt Insert Gallery die Ordner und Bilder in uploads/.
Dieselbe Anleitung für andere Plattformen
JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · Blazor · Web Forms