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

Razor Pages のサンプルページの RichTextEditor。アップロードされた夜の街の写真が本文に挿入されている

Razor Pages アプリでは、エディターの周りに 2 種類の POST があります。1 つ目は、画像が挿入された時点でその画像を、URL を返すエンドポイントにアップロードするものです。2 つ目は、その後でページ自体のフォームが、それらの URL を含む完成した HTML を OnPost に POST するものです。このページでは、その両方と、両者を分けておく理由を紹介します。フォームは偽造防止トークンをそのまま使え、HTML は Razor Pages のフォームの制限に収まる小ささを保てるからです。

フォーム

Razor · Pages/Index.cshtml
<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() をフィールドにコピーします。

ページモデル

C# · Pages/Index.cshtml.cs
// 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 のエンドポイントです。

C# · RteEndpoints.cs
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 のアップロードのガイドと同じです。

Razor Pages のサンプルの「Insert Gallery」ダイアログ。blog、products、team フォルダーと、アップロードされた 4 枚の写真が一覧表示され、1 枚が選択されている
4 回アップロードした後の、Razor Page 上のギャラリー。

テストする

このガイドのサンプルプロジェクトを dotnet run で実行し、http://localhost:5303 を開きます。ギャラリーから画像を挿入し、必要ならギャラリーの「Upload」ボタンで画像をアップロードしてから、「Save」をクリックします。メッセージに HTML の長さが表示され、ページを再読み込みしても画像は残っています。シェルからは次のように確認できます。

Shell
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

次に読む