在 Web Forms 中为富文本编辑器添加图片库

Insert Gallery 对话框显示 Web Forms 示例中的 blog、products 和 team 文件夹以及四张图片,其中一张处于选中状态

RichTextEditor 的 Insert Gallery 对话框可以列出服务器上的图片、打开文件夹,并上传到当前打开的文件夹。它需要一个返回 JSON 的端点。在 ASP.NET Web Forms 中,这个端点是 App_Code 中的一个 IHttpHandler。本页介绍这个处理程序、它用 .NET Framework 自带的序列化程序输出的 JSON,以及两个 IIS 行为——它们会让图片库对远程访问者或 .webp 文件失效,而在您自己的机器上看起来却一切正常。

启用图片库

galleryEndpoint 选项让对话框指向服务器。maxUploadFileSize 使用与服务器相同的限制,因此对话框可以在发送之前拒绝过大的文件:

ASP.NET · Default.aspx
<script>
    var editor = new RichTextEditor("#editor", {
        toolbar: "full",
        galleryEndpoint: "<%= ResolveUrl("~/rte/gallery") %>",
        maxUploadFileSize: 4000000   // same limit as the server
    });
</script>

对话框发送 GET /rte/gallery?folder=blog,并期望得到该文件夹的子文件夹和图片。该路径在 web.config 中映射到处理程序,方式与 Web Forms 中的图片上传里的上传路由相同。

处理程序

C# · App_Code/RteGalleryHandler.cs
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);
    }
}

它读取的是 Request.Unvalidated.QueryString,而不是 Request.QueryString。ASP.NET 请求验证同样适用于一般处理程序。使用 folder=<b> 时,经过验证的集合抛出了 "A potentially dangerous Request.QueryString value was detected",对话框收到一个 HTML 500。改为不经验证地读取后,处理程序自己的文件夹规则就能以 400 {"error":"Invalid folder."} 作出响应。

列表

C# · App_Code/RteFiles.cs
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
    };
}

只列出 .jpg、.jpeg、.png、.gif 和 .webp 文件,文件夹和图片按名称排序。排序时先把两个名称都转为小写,再按序号比较,因为这与 JavaScript 中 toLowerCase() 比较得到的顺序一致。StringComparer.OrdinalIgnoreCase 则是转为大写后比较,这会把 _ 排在字母之后而不是之前。

Web Forms 示例对 GET /rte/gallery?folder=blog 返回的 JSON,包含 currentFolder、parentFolder、folders 以及其中两张图片
对话框为 blog 文件夹收到的内容。

不使用 NuGet 包生成 JSON

C# · App_Code/RteFiles.cs
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));
}
  • JavaScriptSerializer 随 .NET Framework 一起提供,位于 System.Web.Extensions 中,因此 App_Code 无需 bin\ 文件夹就能使用它。它按声明的原样输出属性名。匿名对象使用 camelCase 名称,因此无需任何设置,JSON 就是 camelCase。对于根目录的 parentFolder,它输出 null;它会把 ' 转义为 \u0027,这仍然是有效的 JSON。
  • MaxJsonLength 默认为 2,097,152 个字符,而 web.config 中的 jsonSerialization 设置不适用于 new JavaScriptSerializer()。包含成千上万张图片的文件夹会导致异常,因此示例提高了这个限制。
  • TrySkipIisCustomErrors。IIS Express 默认使用 errorMode="DetailedLocalOnly",因此在您自己的机器上,JSON 400 无论如何都能完整到达。我们设置了 <httpErrors errorMode="Custom">,也就是远程访问者得到的模式。此时,设置了状态 400 但没有设置这个标志的处理程序,其响应体被替换成了 IIS 的 "Bad Request" HTML 页面。设置该标志后,JSON 能正常返回。
  • 不缓存。每次上传后列表都会变化,因此响应告诉浏览器不要缓存它。

IIS Express 上的 .webp

IIS Express 的 applicationhost.config 中没有 .webp 的 MIME 类型。.webp 上传会被保存并列出,但其 URL 会返回 404.3,因此缩略图一直是空的。示例添加了这个类型:

XML · web.config
<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>

它在 <mimeMap> 之前使用 <remove>,因为在已经识别 .webp 的服务器上,重复的条目会导致 500.19 配置错误。

测试

用 run.ps1 启动本指南的示例项目(端口 5401)。用对话框的 New folder 按钮创建一个名为 team 的文件夹,然后请求它:

Shell
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."}

第一个响应是 HTTP 200,第二个是 HTTP 400。不存在的文件夹返回 404。在浏览器中点击工具栏上的 Insert Gallery,对话框会显示 uploads/ 中的文件夹和图片。

其他平台的同一指南

JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · Blazor · MVC 5

延伸阅读