Eine Bildergalerie für einen Rich-Text-Editor in Web Forms

Der Dialog Insert Gallery von RichTextEditor kann die Bilder auf Ihrem Server auflisten, Ordner öffnen und in den geöffneten Ordner hochladen. Er braucht dafür einen Endpoint, der mit JSON antwortet. In ASP.NET Web Forms ist dieser Endpoint ein IHttpHandler in App_Code. Diese Seite behandelt den Handler, das JSON, das er mit dem bereits in .NET Framework enthaltenen Serializer schreibt, und zwei Verhaltensweisen von IIS, die die Galerie für entfernte Besucher oder für .webp-Dateien unbrauchbar machen, während auf Ihrem Rechner alles in Ordnung aussieht.
Die Galerie einschalten
Die Option galleryEndpoint richtet den Dialog auf den Server. maxUploadFileSize verwendet dasselbe Limit wie der Server, sodass der Dialog eine zu große Datei ablehnen kann, bevor er sie sendet:
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "<%= ResolveUrl("~/rte/gallery") %>",
maxUploadFileSize: 4000000 // same limit as the server
});
</script>Der Dialog sendet GET /rte/gallery?folder=blog und erwartet die Unterordner und Bilder des Ordners. Der Pfad wird in web.config auf den Handler abgebildet, auf dieselbe Weise wie die Upload-Route in Bild-Upload in Web Forms.
Der Handler
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);
}
}Er liest Request.Unvalidated.QueryString, nicht Request.QueryString. Die ASP.NET-Anforderungsüberprüfung gilt auch für generische Handler. Mit folder=<b> warf die validierte Collection „A potentially dangerous Request.QueryString value was detected“, und der Dialog erhielt eine HTML-Seite mit 500. Durch das Lesen ohne Überprüfung können die eigenen Ordnerregeln des Handlers mit 400 {"error":"Invalid folder."} antworten.
Die 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
};
}Aufgeführt werden nur Dateien mit .jpg, .jpeg, .png, .gif und .webp, und Ordner und Bilder werden nach Namen sortiert. Die Sortierung schreibt beide Namen klein und vergleicht sie dann ordinal, weil das die Reihenfolge ist, die ein Vergleich mit toLowerCase() in JavaScript ergibt. StringComparer.OrdinalIgnoreCase wandelt dagegen in Großbuchstaben um und setzt _ damit hinter die Buchstaben statt davor.

JSON ohne NuGet-Paket
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));
}JavaScriptSerializerwird mit .NET Framework in System.Web.Extensions ausgeliefert, sodass App_Code ihn ohne Ordnerbin\verwenden kann. Er schreibt Eigenschaftsnamen genau so, wie sie deklariert sind. Die anonymen Objekte verwenden Namen in camelCase, also ist das JSON ohne weitere Einstellungen camelCase. Er schreibtnullfürparentFolderdes Stammordners und maskiert'als\u0027, was weiterhin gültiges JSON ist.MaxJsonLengthist standardmäßig 2,097,152 Zeichen, und die EinstellungjsonSerializationin web.config gilt nicht fürnew JavaScriptSerializer(). Ein Ordner mit vielen tausend Bildern würde eine Ausnahme werfen, deshalb hebt das Beispiel das Limit an.TrySkipIisCustomErrors. IIS Express verwendet standardmäßigerrorMode="DetailedLocalOnly", auf Ihrem eigenen Rechner kommt ein JSON-400 also in jedem Fall unverändert an. Wir haben<httpErrors errorMode="Custom">gesetzt, was entfernte Besucher erhalten. Bei einem Handler, der ohne dieses Flag den Status 400 setzte, ersetzte IIS den Body dann durch seine HTML-Seite „Bad Request“. Mit dem Flag kam das JSON durch.- Kein Caching. Die Liste ändert sich nach jedem Upload, deshalb weist die Antwort den Browser an, sie nicht zu speichern.
.webp unter IIS Express
Die applicationhost.config von IIS Express hat keinen MIME-Typ für .webp. Ein .webp-Upload würde gespeichert und aufgeführt, aber seine URL würde mit 404.3 antworten, sodass das Vorschaubild leer bleibt. Das Beispiel ergänzt den Typ:
<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>Es verwendet <remove> vor <mimeMap>, weil ein doppelter Eintrag auf einem Server, der .webp bereits kennt, ein Konfigurationsfehler 500.19 ist.
Testen
Starten Sie das Beispielprojekt zu dieser Anleitung mit run.ps1 (Port 5401). Legen Sie mit der Schaltfläche New folder des Dialogs einen Ordner namens team an und fragen Sie ihn dann ab:
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."}Die erste Antwort ist HTTP 200, die zweite HTTP 400. Ein Ordner, der nicht existiert, ergibt ein 404. Klicken Sie im Browser in der Symbolleiste auf Insert Gallery. Der Dialog zeigt die Ordner und Bilder von uploads/.
Dieselbe Anleitung für andere Plattformen
JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · Blazor · MVC 5