PHP でリッチテキストエディターに画像ギャラリーを追加する

エディターの「Insert Gallery」ダイアログには、すでにサーバーにある画像を表示できます。そのため、同じ画像をもう一度アップロードしなくても、既存の画像から選べます。必要なのは、JSON で応答する URL 1 つだけです。このページでは、そのエンドポイントを素の PHP 8 で紹介します。scandir() で uploads/ 以下のフォルダーを読み取り、そのサブフォルダーと画像を一覧にするスクリプトです。
ギャラリーを有効にする
エディターの作成時に galleryEndpoint を設定します。すると、ダイアログは開くたび、またはフォルダーを切り替えるたびに、その URL に一覧を要求します。
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css">
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<script src="/rte-files.js"></script>
<script>
new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "/rte/gallery",
maxUploadFileSize: 4000000 // same limit as rte/upload.php, checked before sending
});
</script>rte-files.js は、ダイアログからのアップロードを /rte/upload に送信する小さなブラウザー用ファイルです。そのサーバー側については、画像アップロードのガイドで紹介しています。
ルート
GET /rte/gallery?folder=blog は、1 つのフォルダーの一覧を返します。処理は 2 つのヘルパーが担っているので、スクリプトは短くなっています。
if ($method === 'GET') {
return rte_listing(rte_resolve_folder($_GET['folder'] ?? ''));
}rte_resolve_folder() は、クエリ文字列の仮想フォルダーをストレージルート内の実際のディレクトリに変換するか、400 または 404 を返します。これについてはフォルダーのガイドで説明しています。rte_listing() は JSON を組み立てます。
ギャラリーが受け取るもの
ダイアログに必要なのは、現在のフォルダー、その親フォルダー(ルートでは null)、サブフォルダー、そして URL とサイズを含む画像の情報です。

これを生成するコードは次のとおりです。
// The listing for one folder, in the shape the gallery dialog expects.
function rte_listing(array $folder): array
{
$folders = [];
$images = [];
$names = scandir($folder['dir']);
if ($names === false) {
throw new RteHttpError(500, 'The folder cannot be read.');
}
foreach ($names as $name) {
if ($name === '' || $name[0] === '.') {
continue; // ".", "..", and hidden files such as .htaccess
}
$path = $folder['dir'] . DIRECTORY_SEPARATOR . $name;
// Skip symlinks and Windows junctions. is_link() alone is not enough: PHP 8.2 on
// Windows returns false for a junction, so also require realpath() to be unchanged.
$real = realpath($path);
$same = DIRECTORY_SEPARATOR === '\\' ? strcasecmp($real ?: '', $path) === 0 : $real === $path;
if (is_link($path) || !$same) {
continue;
}
$segments = [...$folder['segments'], $name];
if (is_dir($path)) {
if (rte_is_valid_name($name) && !rte_is_reserved($segments)) { // only folders the routes accept back
$folders[] = ['folder' => implode('/', $segments), 'name' => $name];
}
} elseif (is_file($path) && in_array(rte_extension($name), RTE_IMAGE_EXTENSIONS, true)) {
$images[] = [
'url' => rte_url($folder['root'], $segments),
'name' => $name,
'folder' => $folder['virtual'],
'size' => (int)filesize($path),
];
}
}
$byName = fn(array $a, array $b): int => strcasecmp($a['name'], $b['name']) ?: strcmp($a['name'], $b['name']);
usort($folders, $byName);
usort($images, $byName);
$virtual = $folder['virtual'];
$parent = null;
if ($virtual !== '') {
$cut = strrpos($virtual, '/');
$parent = $cut === false ? '' : substr($virtual, 0, $cut);
}
return [
'currentFolder' => $virtual,
'currentFolderDisplay' => '/' . $virtual,
'parentFolder' => $parent,
'folders' => $folders,
'images' => $images,
];
}この中には、知っておく価値のある判断がいくつかあります。
- ドットで始まる名前はスキップするため、
uploads/.htaccessが表示されることはありません。 - 一覧に含めるのは
.jpg .jpeg .png .gif .webpのファイルだけです。フォルダー内のそれ以外のものは表示されません。 - シンボリックリンクと Windows のジャンクションはスキップします。当社のテストでは、Windows 上の PHP 8.2 の
is_link()はジャンクションに対してfalseを返したため、コードではrealpath()でパスが変わらないことも確認しています。 - 画像の URL の各部分は
rawurlencode()を通すので、my photo.jpgは/uploads/products/my%20photo.jpgとして一覧に表示されます。 - フォルダーと画像は、
strcasecmp()を使い、大文字と小文字を区別せずに名前順で並べます。
ダイアログが提示する形式とサーバーが受け付ける形式
リリース版のギャラリーダイアログは、受け付ける形式として「JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF」を表示し、ファイル選択画面でもそれらを選べるようにしています。このサーバーが受け付けるのは、そのうち .jpg .jpeg .png .gif .webp の 5 つです。.bmp ファイルを選ぶと、ダイアログはそれを送信し、サーバーは 400 {"error":"Only .jpg, .jpeg, .png, .gif and .webp images can be uploaded."} を返し、ダイアログには「Upload failed for photo.bmp.」と表示されます。サーバーが示した理由はレスポンスの本文に含まれているので、ブラウザーの開発者ツールで確認できます。
サイズの場合は事情が異なります。maxUploadFileSize: 4000000 を指定すると、ダイアログは大きなファイルを送信前に拒否します。4.5 MB のファイルでは「big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB.」と表示されました(ダイアログは 2 進数のメガバイトで計算します)。
画像の配信
一覧は /uploads/... を指しているので、それらの URL でファイルを返す必要があります。サンプルプロジェクトでは、PHP の組み込みサーバー向けに router.php がそれらを配信します。画像の拡張子のものだけを、固定の Content-Type と X-Content-Type-Options: nosniff を付けて送信し、そのフォルダーにあるものを実行することは決してありません。Apache や nginx では、Web サーバーが直接配信します。安全なアップロードのガイドで紹介しているとおり、実行を禁止するルールは必ず残しておいてください。
テストする
このガイドのサンプルプロジェクトを php -d upload_max_filesize=5M -d display_errors=0 -d log_errors=1 -S localhost:5201 router.php で起動し、一覧を要求します。
curl "http://localhost:5201/rte/gallery?folder=team"
{"currentFolder":"team","currentFolderDisplay":"/team","parentFolder":"","folders":[],"images":[]}
curl "http://localhost:5201/rte/gallery?folder=blog/2027"
{"error":"Folder not found."}2 つ目のリクエストは HTTP 404 になります。http://localhost:5201/ を開いて「Insert Gallery」をクリックすると、ダイアログにルートのフォルダーと画像が表示され、フォルダーをクリックするとそれが開きます。
ほかのプラットフォーム向けの同じガイド
JavaScript · Next.js · ASP.NET Core · Razor Pages · Blazor · MVC 5 · Web Forms