Den Pfad der Bildergalerie einrichten
Der Dialog „Insert Gallery“ zeigt die Bilder, die bereits auf Ihrem Server liegen, sodass ein Autor eines wiederverwenden kann, statt es erneut hochzuladen. Er erhält sie auf einem von zwei Wegen: aus einer festen Liste in der Konfiguration oder von einem Endpoint Ihrer Anwendung, den er aufruft, während der Benutzer von Ordner zu Ordner wechselt.
Eine feste Liste
galleryImages nimmt ein einfaches Array. Jeder Eintrag kann eine URL sein, ein Array aus URL, Name, Beschriftung oder ein Objekt. Es ist kein Servercode beteiligt, und der Dialog zeigt keine Ordner.
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);Ab Werk verweist diese Eigenschaft auf eine Handvoll Demobilder auf unserer eigenen Website. Ersetzen Sie sie: Ihren Benutzern sollen nicht unsere Bilder angeboten werden, und der Dialog sollte nicht davon abhängen, dass eine andere Domain erreichbar ist.
Eine durchsuchbare Galerie
Setzen Sie galleryEndpoint auf eine eigene URL. Der Dialog fordert sie beim Öffnen an und erneut jedes Mal, wenn der Benutzer einen Ordner öffnet, und stellt dar, was diese URL zurückgibt.
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);Was der Endpoint zurückgibt
Ein GET mit einem Parameter folder gibt die Unterordner und Bilder dieses Ordners zurück:
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"
}
]
}currentFolderist der angefragte Pfad, so wie Ihr Endpoint ihn versteht. Für den Editor ist er undurchsichtig: Der Dialog schickt ihn nur an Sie zurück.currentFolderDisplayist das, was der Benutzer im Dialog sieht, und kann daher ein lesbarer Name statt eines Pfads sein.parentFoldersteuert den Eintrag „nach oben“. Verwenden Sienullim Stammordner und einen leeren String eine Ebene darunter – ein leerer String bedeutet der Stammordner, nicht kein übergeordneter Ordner.urlist das einzige Feld, das das Dokument behält. Diese Adresse fordert der Browser des Lesers an, daher muss sie für einen Besucher funktionieren, der nicht in Ihrem Admin-Bereich angemeldet ist, und auch dann noch, wenn die Seite gespeichert und anderswo wieder geöffnet wird.sizewird in Bytes angegeben und neben dem Bild angezeigt; lassen Sie es weg, wenn Sie nicht jede Datei per stat abfragen wollen.
Schlägt der Request fehl, weicht der Dialog auf galleryImages aus und zeigt keine Ordner – ein Tippfehler in der URL des Endpoints sieht also aus wie eine seltsam kurze Galerie statt wie ein Fehler. Sehen Sie die Demobilder, prüfen Sie den Netzwerk-Tab des Browsers.
Die Schaltfläche „New Folder“ sendet ein Formular an dieselbe 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 createdAus einem Ordner eine URL machen
Diesen Teil sollten Sie richtig machen, und in diesem Teil müssen sich beide Hälften einig sein: Der Upload-Handler entscheidet, wohin eine Datei geschrieben wird, und die Galerie entscheidet, von welcher URL sie gelesen wird. Drei Regeln halten beide im Gleichschritt.
- Verwenden Sie einen Stammordner, mit physischem Pfad und URL-Präfix. Jede URL, die Sie zurückgeben, ist das Präfix plus der Ordner plus der Dateiname. Ermitteln Sie das Präfix zur Laufzeit – in ASP.NET mit
VirtualPathUtility.ToAbsolute("~/imageuploads")–, damit die Galerie auch in einem virtuellen Verzeichnis funktioniert. - Maskieren Sie jedes Segment, und nur die Segmente. Ein Ordner namens Site photos oder eine Datei namens roof #2.jpg ergibt sonst eine defekte URL. Maskieren Sie die Teile und verbinden Sie sie dann mit
/– würden Sie den ganzen Pfad maskieren, würden auch die Trennzeichen maskiert. - Lösen Sie den Ordner auf und prüfen Sie, ob er im Stammordner liegt, bevor Sie ihn verwenden. Der Wert
folderkommt aus dem Browser, behandeln Sie ihn also als feindlich: Lehnen Sie jedes Segment ab, das leer ist,.oder..ist oder ein in Dateinamen ungültiges Zeichen enthält, lösen Sie dann den vollständigen Pfad auf und bestätigen Sie, dass er noch mit dem Stammordner beginnt. Ohne diese letzte Prüfung listet?folder=../../App_Dataein Verzeichnis auf, das Sie nie veröffentlichen wollten.
Diese Regeln gelten auch für den Upload-Handler: Er erhält denselben Wert folder und muss ihn auf dieselbe Weise auflösen. Teilen Sie sich eine Hilfsfunktion, statt die Zuordnung zweimal zu schreiben.
ASP.NET Core
Zwei Endpoints und die vier Hilfsfunktionen, die sie mit dem Upload-Endpoint teilen. Der Upload-Ordner liegt unter wwwroot, daher liefert UseStaticFiles die Bilder aus, und beim Anzeigen eines Bildes läuft kein Code.
// 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 und MVC
Ein generischer Handler mit einer gemeinsamen Hilfsklasse RteImageFolder, die die Methoden zum Normalisieren, Auflösen, Bilden der URL und Bilden der Daten enthält. Für MVC legen Sie dieselben zwei Actions auf einen Controller – /RTE/Gallery – und lassen die Hilfsklasse unverändert.
// 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
Derselbe Vertrag. realpath übernimmt das Auflösen, und die Prüfung mit strpos hält einen präparierten Wert folder innerhalb des Upload-Stammordners.
<?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,
));In den gerade durchsuchten Ordner hochladen
Die Galerie hat eine eigene Upload-Schaltfläche, und diese verwendet nicht config.file_upload_handler. Sie sucht zuerst nach window.richTextBoxUploadFile und weicht auf window.rte_file_upload_handler aus. Nur die erste Variante erfährt, welcher Ordner geöffnet ist; definieren Sie sie also, wenn Uploads dort landen sollen, wo der Benutzer gerade hinschaut.
// 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);
};Ihr Upload-Endpoint muss folder genau so auflösen wie die Galerie und READY: zurückgeben, gefolgt von der URL, gebildet von demselben Code, der die URLs der Galerie bildet. Sind sich beide uneinig, erscheint ein frisch hochgeladenes Bild im Dokument, aber nicht in der Galerie daneben.
Bevor Sie es Benutzern zur Verfügung stellen
- Verlangen Sie für beide Endpoints einen angemeldeten Benutzer. Die Galerie listet Dateinamen auf und legt Verzeichnisse an. Bleibt sie anonym, ist sie eine Verzeichnisliste Ihres Upload-Ordners, die jeder lesen kann, und ein Weg für jeden, darin Ordner anzulegen.
- Grenzen Sie den Stammordner pro Benutzer oder pro Mandant ein, wenn Dokumente nicht geteilt werden. Entnehmen Sie Kunde oder Benutzer der Session, nie einem Request-Parameter, und machen Sie ihn zum Teil des physischen Stammordners statt des durchsuchbaren Pfads.
- Geben Sie nur Bilder zurück. Filtern Sie beim Auflisten nach Dateiendung, und lassen Sie den Upload-Endpoint neben der Dateiendung auch die ersten Bytes der Datei prüfen, damit ein in
.pngumbenanntes Skript nie gespeichert wird. Liefern Sie den Upload-Ordner mitX-Content-Type-Options: nosniffund ohne Skriptausführung aus. - Verwenden Sie nie den Namen, den der Browser gesendet hat. Generieren Sie den gespeicherten Dateinamen; behalten Sie den ursprünglichen höchstens als Beschriftung, wenn Sie ihn brauchen.
Sie kommen vom ASP.NET-Steuerelement?
Im alten Server-Steuerelement stammten diese Pfade aus SetSecurity und den .config-Richtliniendateien, die auch eigene Galerien für Videos, Dokumente und Vorlagen enthielten. Der JavaScript-Editor hat keine Richtliniendateien und keine Serverhälfte, die sie lesen würde: Diese Konfiguration steckt jetzt im Endpoint oben, und die Bildergalerie ist die einzige durchsuchbare Galerie – Videos und Dokumente werden per URL oder über den Upload-Handler eingefügt.
Weiterführende Literatur
- Dateien hochladen – der Upload-Handler selbst, Größenlimits und was passiert, wenn ein Upload fehlschlägt.
- Bild-Uploads und die Galerie in ASP.NET Web Forms und MVC, in ASP.NET Core und in PHP – die vollständigen Projekte, aus denen diese Auszüge stammen.
- Konfigurationsreferenz – jede Eigenschaft für Galerie und Upload in einer Tabelle.