Razor Pages でリッチテキストエディターから画像をアップロードする

Razor Pages アプリでは、エディターの周りに 2 種類の POST があります。1 つ目は、画像が挿入された時点でその画像を、URL を返すエンドポイントにアップロードするものです。2 つ目は、その後でページ自体のフォームが、それらの URL を含む完成した HTML を OnPost に POST するものです。このページでは、その両方と、両者を分けておく理由を紹介します。フォームは偽造防止トークンをそのまま使え、HTML は Razor Pages のフォームの制限に収まる小ささを保てるからです。
フォーム
<form method="post" id="post">
<input type="hidden" asp-for="Html" />
<div id="editor"></div>
<p><button type="submit">Save</button> @Model.Message</p>
</form>
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "/rte/gallery",
maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
});
var html = document.getElementById("Html");
editor.setHTMLCode(html.value);
document.getElementById("post").addEventListener("submit", function () {
html.value = editor.getHTMLCode();
});
</script><input type="hidden" asp-for="Html" />が HTML を運びます。<form method="post">タグヘルパーは、非表示の__RequestVerificationTokenフィールドを自動的に追加します。- エディターは非表示フィールドの値から始まるので、POST の後にはページに保存済みのコンテンツが再び表示されます。
- submit ハンドラーは、フォームが送信される直前に
editor.getHTMLCode()をフィールドにコピーします。
ページモデル
// Razor Pages validates the antiforgery token on this POST by default; the <form> tag helper
// in Index.cshtml adds the hidden __RequestVerificationToken field. Save Html to your database here.
public void OnPost() => Message = $"Saved {Html?.Length ?? 0} characters of HTML.";Html に付けた [BindProperty] は、POST 時にフィールドをバインドします。Razor Pages は OnPost の実行前に偽造防止トークンを検証します。トークンのない POST は 400 になりました。HTML は、保存したりほかのユーザーに表示したりする前に、サーバーでサニタイズしてください。エディターが送信するものも、ブラウザーからの入力であることに変わりはありません。
画像を先にアップロードする理由
アップロードハンドラーがない場合、エディターは貼り付けられた画像を base64 の data: URL として HTML に埋め込みます。これは長いテキストになります。Razor Pages のフォームの値は FormOptions.ValueLengthLimit によって制限されており、既定値は 4,194,304 文字です。有効なトークンを付けた場合、100,000 文字のフォームの POST は保存され、4,300,000 文字のものは OnPost が実行される前に 400 になりました。アップロード用のルートを使えば、HTML には画像ごとに短い URL が含まれるだけです。空のエディターにアップロードした画像を挿入して「Save」をクリックすると、ページには「Saved 108 characters of HTML.」と表示されました。
アップロード用のルート
ページで読み込む rte-files.js は、ドラッグ&ドロップ、貼り付け、「Insert Image」ダイアログ、ギャラリーの「Upload」ボタンからのすべての画像を /rte/upload に送信します。このルートは、Program.cs で app.MapRteFiles() によってマップされる最小 API のエンドポイントです。
rte.MapPost("/upload", async (HttpContext http, IFormFile? file, [FromForm] string? folder) =>
Json(await RteFiles.Save(http, file, folder)))
.DisableAntiforgery()
// Kestrel accepts 30,000,000-byte bodies by default; this route needs ~4 MB.
.WithMetadata(new RequestSizeLimitAttribute(RteFiles.MaxRequestBytes))
.WithFormOptions(multipartBodyLengthLimit: RteFiles.MaxRequestBytes);これをページハンドラーにすることはできません。ハンドラーは /?handler=Upload として指定され、すべての POST ハンドラーで偽造防止トークンが必要になりますが、エディターの XHR はそれを送信できないからです。この選択については Razor Pages でリッチテキストエディターに画像ギャラリーを追加するで説明しています。2 つの制限によって、リクエストは 4,065,536 バイト未満に抑えられます。これがないと、Kestrel は 30,000,000 バイトまで受け付けてしまいます。その後 RteFiles.Save が、オリジン、フォルダー、サイズ、拡張子、先頭バイトをチェックし、新しい名前でファイルを保存します。これらのチェックは ASP.NET Core のアップロードのガイドと同じです。

テストする
このガイドのサンプルプロジェクトを dotnet run で実行し、http://localhost:5303 を開きます。ギャラリーから画像を挿入し、必要ならギャラリーの「Upload」ボタンで画像をアップロードしてから、「Save」をクリックします。メッセージに HTML の長さが表示され、ページを再読み込みしても画像は残っています。シェルからは次のように確認できます。
curl -F "file=@photo.jpg" http://localhost:5303/rte/upload
{"url":"/uploads/photo-4cdaeb3e.jpg"}
curl -s -o /dev/null -w "%{http_code}\n" -d "Html=x" http://localhost:5303/
400アップロードはトークンなしで動作します。単純なフォームの POST は、トークンがないため拒否されます。
ほかのプラットフォーム向けの同じガイド
JavaScript · Next.js · PHP · ASP.NET Core · Blazor · MVC 5 · Web Forms