Configure o caminho da galeria de imagens
A caixa de diálogo Insert Gallery mostra as imagens que já estão no seu servidor, para que quem escreve possa reutilizar uma delas em vez de enviá-la de novo. Ela as obtém de uma de duas formas: uma lista fixa que você coloca na configuração ou um endpoint seu que ela chama conforme o usuário navega de pasta em pasta.
Uma lista fixa
galleryImages recebe um array simples. Cada entrada pode ser uma URL, um array de URL, nome, legenda ou um objeto. Não há código de servidor envolvido, e a caixa de diálogo não mostra pastas.
var config = {};
// The simplest gallery: a fixed list, no server code at all.
config.galleryImages = [
"/imageuploads/roof.jpg", // just the URL
["/imageuploads/atrium.jpg", "Atrium", "1600 x 900"], // URL, name, caption
{
url: "/imageuploads/stair.jpg",
thumbnail: "/imageuploads/thumbs/stair.jpg", // optional, for a faster dialog
name: "Stair core",
meta: "440 KB",
alt: "Timber stair core, looking up" // optional; core 2.6.5+ writes it as the alt text
}
];
var editor = new RichTextEditor("#div_editor", config);Por padrão, essa propriedade aponta para algumas imagens de demonstração no nosso próprio site. Substitua-a: os seus usuários não devem receber as nossas imagens, e a caixa de diálogo não deve depender de outro domínio estar acessível.
Uma galeria navegável
Defina galleryEndpoint como uma URL sua. A caixa de diálogo faz uma requisição a ela quando é aberta e de novo sempre que o usuário abre uma pasta, e renderiza o que essa URL retornar.
var config = {};
// Browsable: the dialog asks this URL for each folder as the user opens it.
config.galleryEndpoint = "/rte/gallery";
// Keep galleryImages as well - it is what the dialog falls back to if the
// endpoint is unreachable, so the user sees a gallery rather than an error.
config.galleryImages = ["/imageuploads/roof.jpg"];
var editor = new RichTextEditor("#div_editor", config);O que o endpoint retorna
Um GET com um parâmetro folder retorna as subpastas e as imagens dessa pasta:
GET /rte/gallery?folder=2026/site-photos
{
"currentFolder": "2026/site-photos",
"currentFolderDisplay": "/2026/site-photos",
"parentFolder": "2026",
"folders": [
{ "name": "week-1", "folder": "2026/site-photos/week-1" }
],
"images": [
{
"name": "roof.jpg",
"folder": "2026/site-photos",
"size": 448123,
"url": "/imageuploads/2026/site-photos/roof.jpg",
"alt": "Roof with new solar panels"
}
]
}currentFolderé o caminho que foi solicitado, do jeito que o seu endpoint o entende. Ele é opaco para o editor: a caixa de diálogo apenas o devolve para você.currentFolderDisplayé o que o usuário vê na caixa de diálogo, então pode ser um nome amigável em vez de um caminho.parentFoldercontrola a entrada “subir um nível”. Usenullna raiz e uma string vazia um nível abaixo dela — uma string vazia significa a raiz, e não sem pasta pai.urlé o único campo que o documento guarda. É o que o navegador do leitor vai requisitar, então precisa funcionar para um visitante que não fez login na sua área administrativa, e precisa continuar funcionando depois que a página for salva e reaberta em outro lugar.sizeé em bytes e aparece ao lado da imagem; omita-o se não quiser consultar o tamanho de cada arquivo.
Se a requisição falhar, a caixa de diálogo volta para galleryImages e não mostra pastas — então um erro de digitação na URL do endpoint parece uma galeria estranhamente pequena, e não um erro. Verifique a aba de rede do navegador se você vir as imagens de demonstração.
O botão New Folder envia um formulário para a mesma URL:
POST /rte/gallery
Content-Type: application/x-www-form-urlencoded
action=create-folder&folder=2026%2Fsite-photos&name=week-2
-> the same JSON as a GET, listing the folder that was just createdTransformando uma pasta em URL
Esta é a parte que vale a pena acertar, e a parte em que as duas metades precisam concordar: o handler de upload decide onde um arquivo é gravado, e a galeria decide de qual URL ele é lido. Três regras mantêm as duas em sincronia.
- Mantenha uma única raiz, com um caminho físico e um prefixo de URL. Toda URL que você retorna é o prefixo mais a pasta mais o nome do arquivo. Obtenha o prefixo em tempo de execução — no ASP.NET,
VirtualPathUtility.ToAbsolute("~/imageuploads")— para que a galeria continue funcionando quando for implantada em um diretório virtual. - Faça o escape de cada segmento, e só dos segmentos. Caso contrário, uma pasta chamada Site photos ou um arquivo chamado roof #2.jpg gera uma URL quebrada. Faça o escape das partes e depois junte-as com
/— fazer o escape do caminho inteiro também escaparia os separadores. - Resolva a pasta e verifique se ela está dentro da raiz antes de usá-la. O valor de
foldervem do navegador, então trate-o como hostil: rejeite qualquer segmento vazio, igual a.ou.., ou que contenha um caractere inválido em nome de arquivo; depois resolva o caminho completo e confirme que ele ainda começa com a raiz. Sem essa última verificação,?folder=../../App_Datalista um diretório que você nunca quis publicar.
Essas regras também se aplicam ao handler de upload: ele recebe o mesmo valor de folder e precisa resolvê-lo da mesma forma. Compartilhe um único helper entre os dois em vez de escrever o mapeamento duas vezes.
ASP.NET Core
Dois endpoints e os quatro helpers que eles compartilham com o endpoint de upload. A pasta de upload fica em wwwroot, então UseStaticFiles serve as imagens e nenhum código é executado quando uma delas é exibida.
// ASP.NET Core minimal API. Add .RequireAuthorization() to both endpoints.
const string UploadUrl = "/imageuploads"; // served by UseStaticFiles
string uploadRoot = Path.GetFullPath(Path.Combine(app.Environment.WebRootPath, "imageuploads"));
Directory.CreateDirectory(uploadRoot);
app.MapGet("/rte/gallery", (string? folder) =>
{
string relative = Normalize(folder);
if (ResolveFolder(relative) is null) relative = ""; // fall back to the root
return Results.Json(BuildPayload(relative));
});
app.MapPost("/rte/gallery", async (HttpRequest request) =>
{
var form = await request.ReadFormAsync();
if (form["action"] != "create-folder") return Results.BadRequest(new { error = "unsupported action" });
string parent = Normalize(form["folder"]);
string name = form["name"].ToString().Trim();
if (ResolveFolder(parent) is null || !IsValidName(name))
return Results.BadRequest(new { error = "invalid folder name" });
string relative = parent.Length == 0 ? name : parent + "/" + name;
Directory.CreateDirectory(Path.Combine(uploadRoot, relative));
return Results.Json(BuildPayload(relative));
});
string Normalize(string? folder) => (folder ?? "").Replace('\\', '/').Trim('/');
bool IsValidName(string part) =>
part.Length > 0 && part != "." && part != ".." && part.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
// The physical directory for "a/b" inside uploadRoot, or null if it is invalid,
// missing, or outside it. This is the check that stops ?folder=../../web.config.
string? ResolveFolder(string relative)
{
if (relative.Length == 0) return uploadRoot;
if (relative.Split('/').Any(part => !IsValidName(part))) return null;
string full = Path.GetFullPath(Path.Combine(uploadRoot, relative));
bool inside = full.StartsWith(uploadRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
return inside && Directory.Exists(full) ? full : null;
}
// folder + file name -> the URL the browser will request
string BuildUrl(string relative, string fileName) =>
UploadUrl + "/" + string.Join("/",
(relative.Length == 0 ? new[] { fileName } : relative.Split('/').Append(fileName))
.Select(Uri.EscapeDataString));
object BuildPayload(string relative)
{
string dir = ResolveFolder(relative)!;
int slash = relative.LastIndexOf('/');
return new
{
currentFolder = relative,
currentFolderDisplay = "/" + relative,
parentFolder = relative.Length == 0 ? null : (slash < 0 ? "" : relative.Substring(0, slash)),
folders = Directory.GetDirectories(dir).Select(Path.GetFileName)
.Select(n => new { name = n, folder = relative.Length == 0 ? n : relative + "/" + n }),
images = Directory.GetFiles(dir)
.Where(f => imageExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
.Select(f => new FileInfo(f))
.Select(f => new { name = f.Name, folder = relative, size = f.Length, url = BuildUrl(relative, f.Name) })
};
}ASP.NET Web Forms e MVC
Um generic handler que usa um helper compartilhado RteImageFolder com os métodos de normalizar, resolver, montar a URL e montar o payload. No MVC, coloque as mesmas duas actions em um controller — /RTE/Gallery — e mantenha o helper como está.
// RTEGallery.ashx - Web Forms. Put RteImageFolder.cs in App_Code.
// GET RTEGallery.ashx?folder=a/b list that folder
// POST action=create-folder, folder=a/b, name=New create it, then list it
public class RTEGallery : IHttpHandler
{
const string RootVirtual = "~/imageuploads";
public void ProcessRequest(HttpContext context)
{
// TODO: check that the current user may browse images and create folders.
context.Response.ContentType = "application/json";
string root = Path.GetFullPath(context.Server.MapPath(RootVirtual));
Directory.CreateDirectory(root);
string folder;
if (context.Request.HttpMethod == "POST")
{
if (context.Request.Form["action"] != "create-folder") { Fail(context, "unsupported action"); return; }
string parent = RteImageFolder.Normalize(context.Request.Form["folder"]);
string name = (context.Request.Form["name"] ?? "").Trim();
if (RteImageFolder.Resolve(root, parent) == null || !RteImageFolder.IsValidName(name))
{ Fail(context, "invalid folder name"); return; }
folder = parent.Length == 0 ? name : parent + "/" + name;
Directory.CreateDirectory(Path.Combine(root, folder.Replace('/', Path.DirectorySeparatorChar)));
}
else
{
folder = RteImageFolder.Normalize(context.Request.QueryString["folder"]);
}
// ToAbsolute turns "~/imageuploads" into the URL prefix, so the gallery
// keeps working when the site is deployed under a virtual directory.
string baseUrl = VirtualPathUtility.ToAbsolute(RootVirtual);
context.Response.Write(new JavaScriptSerializer().Serialize(
RteImageFolder.BuildPayload(root, baseUrl, folder)));
}
static void Fail(HttpContext context, string message)
{
context.Response.StatusCode = 400;
context.Response.TrySkipIisCustomErrors = true;
context.Response.Write(new JavaScriptSerializer().Serialize(new { error = message }));
}
public bool IsReusable { get { return false; } }
}PHP
O mesmo contrato. realpath faz a resolução, e a verificação com strpos é o que mantém um folder forjado dentro da raiz de upload.
<?php
// rte-gallery.php
// GET rte-gallery.php?folder=a/b list that folder
// POST action=create-folder, folder=a/b, name=New create it, then list it
header("Content-Type: application/json; charset=utf-8");
// TODO: check that the current user may browse images and create folders.
$rootDir = __DIR__ . DIRECTORY_SEPARATOR . "imageuploads"; // same folder as rte-upload.php
$rootUrl = "/imageuploads"; // the URL that folder is served at
$imageExts = array("jpg", "jpeg", "jfif", "png", "gif", "webp", "bmp", "avif");
function gallery_valid_name($part) {
return $part !== "" && $part !== "." && $part !== ".." && strpbrk($part, "/\\:*?\"<>|") === false;
}
// The real directory for "a/b", or false if it is invalid, missing, or outside the root.
function gallery_dir($rootReal, $folder) {
if ($folder === "") return $rootReal;
foreach (explode("/", $folder) as $part) {
if (!gallery_valid_name($part)) return false;
}
$dir = realpath($rootReal . DIRECTORY_SEPARATOR . str_replace("/", DIRECTORY_SEPARATOR, $folder));
return ($dir !== false && is_dir($dir) && strpos($dir, $rootReal . DIRECTORY_SEPARATOR) === 0) ? $dir : false;
}
// folder + file name -> the URL the browser will request
function gallery_url($rootUrl, $folder, $name) {
$parts = $folder === "" ? array() : explode("/", $folder);
$parts[] = $name;
return $rootUrl . "/" . implode("/", array_map("rawurlencode", $parts));
}
if (!is_dir($rootDir)) mkdir($rootDir, 0755, true);
$rootReal = realpath($rootDir);
if ($_SERVER["REQUEST_METHOD"] === "POST") {
if ((isset($_POST["action"]) ? $_POST["action"] : "") !== "create-folder") {
http_response_code(400); echo json_encode(array("error" => "unsupported action")); exit;
}
$parent = trim(isset($_POST["folder"]) ? (string)$_POST["folder"] : "", "/");
$name = trim(isset($_POST["name"]) ? (string)$_POST["name"] : "");
$parentDir = gallery_dir($rootReal, $parent);
if ($parentDir === false || !gallery_valid_name($name)) {
http_response_code(400); echo json_encode(array("error" => "invalid folder name")); exit;
}
$newDir = $parentDir . DIRECTORY_SEPARATOR . $name;
if (!is_dir($newDir)) mkdir($newDir, 0755);
$folder = $parent === "" ? $name : $parent . "/" . $name;
} else {
$folder = trim(isset($_GET["folder"]) ? (string)$_GET["folder"] : "", "/");
}
$dir = gallery_dir($rootReal, $folder);
if ($dir === false) { $folder = ""; $dir = $rootReal; } // fall back to the root
$folders = array();
$images = array();
foreach (scandir($dir) as $entry) {
if ($entry === "." || $entry === "..") continue;
$path = $dir . DIRECTORY_SEPARATOR . $entry;
if (is_dir($path)) {
$folders[] = array("name" => $entry, "folder" => $folder === "" ? $entry : $folder . "/" . $entry);
} elseif (in_array(strtolower(pathinfo($entry, PATHINFO_EXTENSION)), $imageExts, true)) {
$images[] = array(
"name" => $entry,
"folder" => $folder,
"size" => filesize($path),
"url" => gallery_url($rootUrl, $folder, $entry),
);
}
}
$slash = strrpos($folder, "/");
echo json_encode(array(
"currentFolder" => $folder,
"currentFolderDisplay" => "/" . $folder,
"parentFolder" => $folder === "" ? null : ($slash === false ? "" : substr($folder, 0, $slash)),
"folders" => $folders,
"images" => $images,
));Enviando para a pasta que está sendo navegada
A galeria tem um botão Upload próprio, e ele não usa config.file_upload_handler. Ele procura primeiro window.richTextBoxUploadFile e, se não encontrar, usa window.rte_file_upload_handler. Só a primeira forma recebe a informação de qual pasta está aberta, então defina-a se quiser que os uploads caiam onde o usuário está olhando.
// Lets the gallery's Upload button put the file in the folder being browsed.
// Without this the gallery falls back to rte_file_upload_handler, which knows
// nothing about folders, and every upload lands in the top-level folder.
window.richTextBoxUploadFile = function (file, callback, options) {
var folder = (options && options.folder) || "";
var data = new FormData();
data.append("fileforphp", file);
var xh = new XMLHttpRequest();
xh.open("POST", "/rte-upload.php?folder=" + encodeURIComponent(folder), true);
xh.onload = function () {
var text = xh.responseText || "";
if (xh.status === 200 && text.indexOf("READY:") === 0) callback(text.substring(6));
else callback(null, text.indexOf("ERROR:") === 0 ? text.substring(6) : "http-" + xh.status);
};
xh.onerror = function () { callback(null, "network-error"); };
xh.send(data);
};O seu endpoint de upload precisa resolver folder exatamente como a galeria faz, e retornar READY: seguido da URL montada pelo mesmo código que monta as URLs da galeria. Se os dois divergirem, uma imagem recém-enviada aparece no documento, mas não na galeria ao lado dele.
Antes de colocar na frente dos usuários
- Exija um usuário autenticado nos dois endpoints. A galeria lista nomes de arquivos e cria diretórios. Se ficar anônima, ela é uma listagem de diretório da sua pasta de upload que qualquer pessoa pode ler, e um meio para qualquer pessoa criar pastas nela.
- Defina a raiz por usuário ou por tenant quando os documentos não forem compartilhados. Obtenha o cliente ou o usuário da sessão, nunca de um parâmetro da requisição, e faça dele parte da raiz física, e não parte do caminho navegável.
- Retorne apenas imagens. Filtre por extensão ao listar e faça o endpoint de upload verificar os primeiros bytes do arquivo além da extensão, para que um script renomeado para
.pngnunca seja armazenado. Sirva a pasta de upload comX-Content-Type-Options: nosniffe sem execução de scripts. - Nunca reutilize o nome que o navegador enviou. Gere o nome do arquivo armazenado; guarde o original apenas como legenda, se precisar dele.
Vindo do controle ASP.NET?
No antigo controle de servidor, esses caminhos vinham de SetSecurity e dos arquivos de política .config, que também traziam galerias separadas para vídeo, documentos e templates. O editor JavaScript não tem arquivos de política nem uma metade no servidor para lê-los: o endpoint acima é onde essa configuração fica agora, e a galeria de imagens é a única navegável — vídeos e documentos são inseridos por URL ou pelo handler de upload.
Leitura adicional
- Enviar arquivos — o próprio handler de upload, limites de tamanho e o que acontece quando um upload falha.
- Upload de imagens e a galeria no ASP.NET Web Forms e MVC, no ASP.NET Core e no PHP — os projetos completos de onde estes trechos foram tirados.
- Referência de configuração — todas as propriedades de galeria e de upload em uma única tabela.