Adicionar uma galeria de imagens a um editor de texto rico no ASP.NET MVC 5

A caixa de diálogo Insert Gallery do RichTextEditor lista as imagens do seu servidor, abre pastas e envia arquivos para a pasta que está aberta. No ASP.NET MVC 5, ela precisa de uma action de controller que retorne JSON para GET /rte/gallery?folder=.... Dois padrões do MVC atrapalham: Json() recusa requisições GET, e a validação de requisições transforma nomes de pasta incomuns em páginas HTML de erro 500. Esta página mostra a action com os dois problemas resolvidos.
Ative a galeria
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "@Url.Content("~/rte/gallery")",
maxUploadFileSize: 4000000 // same limit as the server
});
</script>galleryEndpoint é a URL que a caixa de diálogo consulta. maxUploadFileSize repete o limite de 4,000,000 bytes do servidor, para que a caixa de diálogo recuse um arquivo grande demais antes de enviá-lo. A rota rte/gallery leva à action Gallery do RteController. Ela é configurada em Global.asax.cs, ao lado da rota de upload do guia upload de imagens no MVC 5.
A 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 resolve a pasta virtual dentro de uploads/. Ele retorna as subpastas e os arquivos .jpg, .jpeg, .png, .gif e .webp, ordenados por nome:
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() e GET
Por padrão, Json(data) usa JsonRequestBehavior.DenyGet. Testamos isso com uma action descartável. Um GET retornou HTTP 500 com "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." A galeria precisa ser um GET, então o controller monta seu JsonResult em um único 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: a mensagem do MVC aponta o risco, que é outro site ler uma resposta de GET. Aqui a resposta é sempre um objeto, nunca um array de nível superior. Ela lista apenas o que a caixa de diálogo mostra de qualquer forma. Se essa lista for privada, coloque a rota atrás de autenticação, como nas pastas por usuário abaixo.MaxJsonLength: oJsonResultusa oJavaScriptSerializer. Segundo a documentação do MVC, um valor nulo significa o padrão do serializador, de 2,097,152 caracteres, que uma pasta com milhares de imagens poderia ultrapassar. Não medimos esse limite.- O
JavaScriptSerializergrava os nomes exatamente como foram declarados. Os objetos anônimos emRteFilesusam camelCase, então o JSON corresponde ao que a caixa de diálogo espera sem nenhuma configuração. TrySkipIisCustomErrorsimpede que o IIS substitua o corpo de uma resposta 4xx pela própria página de erro para visitantes remotos (comerrorMode="Custom").SuppressFormsAuthenticationRedirectpassa a importar quando você adiciona pastas por usuário.

[ValidateInput(false)] e o valor de folder
O MVC executa a validação de requisições do ASP.NET enquanto vincula string folder. Removemos [ValidateInput(false)] do controller para um teste. GET /rte/gallery?folder=<b> então retornou um HTML 500 ("A potentially dangerous Request.QueryString value was detected from the client") antes de a action rodar. Com o atributo, o valor chega ao RteFiles, que só aceita nomes de pasta simples, e a resposta é 400 {"error":"Invalid folder."}. Desativar a validação é seguro aqui porque nada da requisição é usado sem passar por essas regras.
As URLs das imagens
A url de cada imagem é /uploads/<folder>/<file>, montada com VirtualPathUtility.ToAbsolute, e o IIS a serve como arquivo estático. O roteamento do MVC não atrapalha: RouteExistingFiles é false por padrão, então os arquivos existentes em /uploads/, /rte-files.js e a pasta /richtexteditor/ do editor não passam pelas rotas. Os nomes de pasta podem conter espaços e letras não ASCII, e cada segmento do caminho é codificado com percent-encoding. O IIS Express não tem um tipo MIME para .webp, então o web.config do exemplo adiciona um. Sem ele, uma imagem .webp aparece na lista, mas sua URL responde 404.3.
Teste
Inicie o projeto de exemplo deste guia com run.ps1 (porta 5402). Crie uma pasta team com o botão New folder da caixa de diálogo e depois:
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 e depois HTTP 400. No navegador, Insert Gallery mostra as pastas e imagens em uploads/.
O mesmo guia para outras plataformas
JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · Blazor · Web Forms