Añadir una galería de imágenes a un editor de texto enriquecido en ASP.NET MVC 5

El cuadro de diálogo Insert Gallery de RichTextEditor enumera las imágenes de tu servidor, abre carpetas y sube archivos a la carpeta abierta. En ASP.NET MVC 5 necesita una acción de controlador que devuelva JSON para GET /rte/gallery?folder=.... Dos valores predeterminados de MVC estorban: Json() rechaza las solicitudes GET, y la validación de solicitudes convierte los nombres de carpeta raros en páginas HTML de error 500. Esta página muestra la acción con ambas cosas resueltas.
Activa la galería
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "@Url.Content("~/rte/gallery")",
maxUploadFileSize: 4000000 // same limit as the server
});
</script>galleryEndpoint es la URL a la que pregunta el cuadro de diálogo. maxUploadFileSize repite el límite de 4,000,000 bytes del servidor, para que el cuadro de diálogo rechace un archivo demasiado grande antes de enviarlo. La ruta rte/gallery va a la acción Gallery de RteController. Se configura en Global.asax.cs, junto a la ruta de carga de carga de imágenes en MVC 5.
La acción
[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 resuelve la carpeta virtual dentro de uploads/. Devuelve las subcarpetas y los archivos .jpg, .jpeg, .png, .gif y .webp, ordenados por nombre:
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() y GET
Json(data) usa de forma predeterminada JsonRequestBehavior.DenyGet. Lo probamos con una acción desechable. Un GET devolvió HTTP 500 con "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 galería tiene que ser un GET, así que el controlador construye su JsonResult en un solo lugar:
// 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: el mensaje de MVC nombra el riesgo, que es que otro sitio lea una respuesta GET. Aquí la respuesta es siempre un objeto, nunca un array de nivel superior. Solo enumera lo que el cuadro de diálogo muestra de todos modos. Si esa lista es privada, pon la ruta detrás de un inicio de sesión, como en las carpetas por usuario de más abajo.MaxJsonLength:JsonResultusaJavaScriptSerializer. Según la documentación de MVC, un valor null significa el valor predeterminado del serializador, 2,097,152 caracteres, que una carpeta con miles de imágenes podría superar. No medimos ese límite.JavaScriptSerializerescribe los nombres exactamente como están declarados. Los objetos anónimos deRteFilesusan camelCase, así que el JSON coincide con lo que espera el cuadro de diálogo sin ningún ajuste.TrySkipIisCustomErrorsimpide que IIS sustituya el cuerpo de un 4xx por su propia página de error para los visitantes remotos (conerrorMode="Custom").SuppressFormsAuthenticationRedirectimporta en cuanto añades carpetas por usuario.

[ValidateInput(false)] y el valor de folder
MVC ejecuta la validación de solicitudes de ASP.NET mientras enlaza string folder. Para una prueba, quitamos [ValidateInput(false)] del controlador. GET /rte/gallery?folder=<b> devolvió entonces una página HTML de error 500 ("A potentially dangerous Request.QueryString value was detected from the client") antes de que se ejecutara la acción. Con el atributo, el valor llega a RteFiles, que solo acepta nombres de carpeta simples, y la respuesta es 400 {"error":"Invalid folder."}. Desactivar la validación es seguro aquí porque nada de la solicitud se usa sin pasar esas reglas.
Las URL de las imágenes
La url de cada imagen es /uploads/<folder>/<file>, construida con VirtualPathUtility.ToAbsolute, e IIS la sirve como archivo estático. El enrutamiento de MVC no estorba: RouteExistingFiles es false de forma predeterminada, así que los archivos existentes bajo /uploads/, /rte-files.js y la carpeta /richtexteditor/ del editor se saltan las rutas. Los nombres de carpeta pueden contener espacios y letras no ASCII, y cada segmento de la ruta se codifica con porcentajes. IIS Express no tiene tipo MIME para .webp, así que el web.config del ejemplo añade uno. Sin él, una imagen .webp aparece en la lista, pero su URL responde 404.3.
Pruébalo
Inicia el proyecto de ejemplo de esta guía con run.ps1 (puerto 5402). Crea una carpeta team con el botón New folder del cuadro de diálogo y luego:
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 y luego HTTP 400. En el navegador, Insert Gallery 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 · Web Forms