Añadir una galería de imágenes a un editor de texto enriquecido en Web Forms

El cuadro de diálogo Insert Gallery de RichTextEditor puede enumerar las imágenes de tu servidor, abrir carpetas y subir archivos a la carpeta abierta. Necesita un endpoint que responda con JSON. En ASP.NET Web Forms, ese endpoint es un IHttpHandler en App_Code. Esta página trata el handler, el JSON que escribe con el serializador que .NET Framework ya incluye y dos comportamientos de IIS que rompen la galería para los visitantes remotos o para los archivos .webp mientras en tu máquina parece funcionar bien.
Activa la galería
La opción galleryEndpoint apunta el cuadro de diálogo al servidor. maxUploadFileSize usa el mismo límite que el servidor, para que el cuadro de diálogo pueda rechazar un archivo demasiado grande antes de enviarlo:
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "<%= ResolveUrl("~/rte/gallery") %>",
maxUploadFileSize: 4000000 // same limit as the server
});
</script>El cuadro de diálogo envía GET /rte/gallery?folder=blog y espera las subcarpetas y las imágenes de la carpeta. La ruta se asocia con el handler en web.config, igual que la ruta de carga de carga de imágenes en Web Forms.
El 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);
}
}Lee Request.Unvalidated.QueryString, no Request.QueryString. La validación de solicitudes de ASP.NET también se aplica a los generic handlers. Con folder=<b>, la colección validada lanzó "A potentially dangerous Request.QueryString value was detected" y el cuadro de diálogo recibió un HTML 500. La lectura sin validar permite que las reglas de carpetas propias del handler respondan con 400 {"error":"Invalid folder."}.
El listado
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
};
}Solo se enumeran archivos .jpg, .jpeg, .png, .gif y .webp, y las carpetas y las imágenes se ordenan por nombre. La ordenación pasa ambos nombres a minúsculas y luego los compara de forma ordinal, porque ese es el orden que da una comparación con toLowerCase() de JavaScript. StringComparer.OrdinalIgnoreCase pasa a mayúsculas, lo que coloca _ después de las letras en lugar de antes.

JSON sin un paquete 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));
}JavaScriptSerializerviene con .NET Framework, en System.Web.Extensions, así que App_Code puede usarlo sin carpetabin\. Escribe los nombres de propiedad exactamente como están declarados. Los objetos anónimos usan nombres en camelCase, así que el JSON sale en camelCase sin ningún ajuste. Escribenullpara elparentFolderde la raíz, y escapa'como\u0027, que sigue siendo JSON válido.MaxJsonLengthvale 2,097,152 caracteres de forma predeterminada, y el ajustejsonSerializationde web.config no se aplica anew JavaScriptSerializer(). Una carpeta con muchos miles de imágenes lanzaría una excepción, así que el ejemplo amplía el límite.TrySkipIisCustomErrors. IIS Express usa de forma predeterminadaerrorMode="DetailedLocalOnly", así que en tu propia máquina un 400 en JSON llega intacto en cualquier caso. Establecimos<httpErrors errorMode="Custom">, que es lo que reciben los visitantes remotos. A un handler que establecía el estado 400 sin este indicador, IIS le sustituyó entonces el cuerpo por su página HTML "Bad Request". Con el indicador, el JSON llegó.- Sin caché. El listado cambia después de cada carga, así que la respuesta le indica al navegador que no la guarde.
.webp en IIS Express
El applicationhost.config de IIS Express no tiene tipo MIME para .webp. Una carga .webp se guardaría y aparecería en la lista, pero su URL respondería 404.3, así que la miniatura se quedaría vacía. El ejemplo añade el tipo:
<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>Usa <remove> antes de <mimeMap> porque una entrada duplicada en un servidor que ya conoce .webp es un error de configuración 500.19.
Pruébalo
Inicia el proyecto de ejemplo de esta guía con run.ps1 (puerto 5401). Crea una carpeta llamada team con el botón New folder del cuadro de diálogo y luego pídela:
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 primera respuesta es HTTP 200 y la segunda HTTP 400. Una carpeta que no existe da 404. En el navegador, haz clic en Insert Gallery en la barra de herramientas. El cuadro de diálogo muestra las carpetas y las imágenes de uploads/.
La misma guía para otras plataformas
JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · Blazor · MVC 5