解説コードを読んでみよう
このサンプルでわかること
- 配布されている部品(エディター)に、自分で機能を書き足す方法
- ボタンを押したら ajax で PHP を呼び出し、サーバー上のファイルを削除する流れ
- ファイルを削除する PHP で、消してよいファイルだけに制限する方法
WYSIWYG(ウィジウィグ)は「What You See Is What You Get = 見たままが得られる」の略で、ブログの投稿画面のように、文字の太さや画像を見た目のまま編集できるエディターのことです。ここでは配布されている WYSIWYG エディターを使い、もともと無かった「アップロードした画像を削除する」機能を追加しています。
使っているファイル
- index.phpエディターを表示するページ
- Editor/skeditor/editor.min.jsエディター本体(ここに削除ボタンの処理を追加)
- Editor/skeditor/language.jsボタンなどに表示する日本語
- Editor/imagemanager/studiokeyEditorImageUploader.php画像を受け取って保存する PHP
- Editor/imagemanager/studiokeyEditorImageList.php保存した画像の一覧を返す PHP
- wysiwyg_delet.php画像を削除する PHP(追加した部分)
- wysiwyg_post.php「POST送信テスト」の送信先
1. エディターを表示する
エディターの CSS と JavaScript を読み込み、<textarea> を囲んだ #MyEditor に対して .myEditor({ ... }) を実行すると、ツールバー付きのエディターに変わります。
<!-- エディター本体と、ボタンなどの日本語表示 -->
<link rel="stylesheet" href="Editor/skeditor/skeditor.css">
<script src="Editor/skeditor/editor.min.js"></script>
<script src="Editor/skeditor/language.js"></script>
<div id="container">
<form name="form1" method="post" action="wysiwyg_post.php" target="_blank">
<div id="MyEditor">
<textarea id="comment" name="comment"></textarea>
</div>
<p style="padding: 20px 0 0">
<input type="submit" class="btn btn-default" value="POST送信テスト">
</p>
</form>
<script>
jQuery.event.add(window, "load", function(){
jQuery('#MyEditor').myEditor({
'siteUrl' : '<?= h(url('wysiwyg-add-delete/')) ?>' // このページがあるフォルダの URL(画像の URL を作るのに使う)
,'manager' : 'Editor/imagemanager/' // アップロード・一覧を担当する PHP の場所
,'img_icon' : 'Editor/skeditor/icon.png'
,'img_loder' : 'Editor/skeditor/loader.gif'
,'ifHeight' : '400px'
,'txHeight' : '400px'
,'uploadFileMax': 1000000 // 1MB(PHP 側の上限 upmax と合わせる)
});
});
</script>
| オプション | 意味 |
|---|---|
siteUrl | このページがあるフォルダの URL。アップロードした画像の URL を組み立てるのに使います |
manager | 画像のアップロードと一覧を担当する PHP があるフォルダ |
ifHeight / txHeight | 編集画面(見たまま/HTML)の高さ |
uploadFileMax | ブラウザ側でチェックするファイルサイズの上限(バイト) |
文章を書いて「POST送信テスト」を押すと、書いた内容が HTML の文字列として $_POST['comment'] に届きます。
2. 画像を保存する PHP
「画像アップロード」で画像を送ると、studiokeyEditorImageUploader.php が受け取り、wysiwygUpload/年/月/日/ のフォルダに保存します。保存する前に、拡張子と中身が本当に画像かを確かめています。
$ext = strtolower(pathinfo($ufile_oname, PATHINFO_EXTENSION));
if ($ext === 'jpeg') { $ext = 'jpg'; }
$size = getimagesize("$ufile_tmp");
$error = 'er';
// 安全対策: 画像以外(.php など)をアップロードされるとサーバー上で実行されてしまうため、画像のみ許可
if(!in_array($ext, ['jpg','png','gif'], true) || $size === false){
$Message = '画像ファイル(JPG/PNG/GIF)のみアップロードできます';
}elseif($ufile_size > upmax ){
$Message = 'ファイルサイズが大きすぎます';
}else{
$hash = hash('SHA256',$ufile_mine.$ufile_tmp.time().$ufile_oname); //色々くっつけてハッシュに
$hash = mb_strimwidth($hash, 0, 8, "", "UTF-8"); //8文字で切り取る
$FileName = $hash.'.'.$ext; //拡張子を付けてファイル名に
$new_upload_file = $upload_Dir.$FileName; //アップロードファイル+ディレクトリ
画像のアップロード機能で拡張子を確かめずに保存すると、.php のファイルを送り込まれて、サーバー上でプログラムを実行されてしまう危険があります。getimagesize() で中身が画像かどうかも確かめ、許可する拡張子を決めておくのが基本です。
3. 画像一覧に「削除」ボタンを付ける(追加した部分)
「画像管理」タブを開くと、エディターは ajax で studiokeyEditorImageList.php を呼び、画像の一覧(XML)を受け取って並べます。そのとき、画像ごとに削除ボタンも作るようにしました。ボタンの id には画像の場所(例:wysiwygUpload/2017/02/21/97d2474e.jpg)を入れておきます。
b(a)
.find("item")
.each(function () {
var a;
a = "" + ("<h4>" + b(this).find("time").text() + "<button id ='" + b(this).find("file").text() + "'>" + editorLanguage.img_Delete + " </button></h4>");
a += '<img src="' + b(this)
.find("file")
.text() + '" data-filename="' + b(this)
.find("file")
.text() + '" title="' + editorLanguage.imgList_insert + '" alt=""/>';
a += "";
b("<li id=" + remove( b(this).find("file").text()) + "></li>")
.html(a)
.appendTo("#edt_img_list ul")
})
4. 削除ボタンが押されたら ajax で PHP を呼ぶ
}).always(function(){
var ds;
/* 画像一覧の「削除」ボタンだけに付ける(ページ内の他のボタンに反応しないように修正) */
b("#edt_img_list h4 button").off("click.imgdel").on("click.imgdel", function(){
ds = $(this).attr("id");
b.ajax({
url: 'wysiwyg_delet.php',
type: 'post',
data: {'data': ds},
timeout: 10000,
dataType: 'text'
}).done(function(data) {
}).fail(function() {
}).always(function() {
b('#edt_img_list ul li#'+ remove(ds)).hide();
})
});
})
- 348行目:画像一覧の中にある削除ボタンがクリックされたら動きます。
- 349行目:押されたボタンの
id(=画像の場所)を取り出します。 - 351〜353行目:
$.ajax()でwysiwyg_delet.phpに、画像の場所をdataという名前で POST 送信します。 - 359行目:通信が終わったら、一覧からその画像の行を隠します。
ajax(エイジャックス)を使うと、ページを切り替えずに裏側でサーバーと通信できます。削除のたびにページ全体を読み込み直さなくてよいので、操作が軽快になります。
5. 画像を削除する PHP(いちばん大事なところ)
<?php
/*
* エディターの画像一覧から画像を削除する
*
* PHP8.3対応 + 安全対策:
* 旧版は POST された data をそのまま unlink() していたため、サーバー上の任意のファイルを
* 外部から削除できてしまう状態でした。wysiwygUpload フォルダ内の画像だけ削除できるように制限しています。
*/
$file = (string)($_POST['data'] ?? '');
$base = realpath(__DIR__ . '/wysiwygUpload');
$path = realpath(__DIR__ . '/' . $file);
if ($base === false || $path === false || !is_file($path)
|| strpos($path, $base . DIRECTORY_SEPARATOR) !== 0
|| !preg_match('/\.(jpe?g|png|gif)$/i', $path)) {
http_response_code(400);
exit('invalid file');
}
unlink($path);
送られてきた場所をそのまま unlink()(ファイル削除)に渡すと、../../index.php のような指定でサイトのどのファイルでも消せてしまいます。そこで次の順に確かめ、1つでも当てはまらなければ削除しません。
realpath()で、../などを取り除いた「本当の場所」に直す- そのファイルが実際に存在するか(
is_file()) - 本当の場所が
wysiwygUploadフォルダの中にあるか - 拡張子が画像(jpg / png / gif)か
ブラウザから送られてくる値は、画面のボタンを使わなくても自由に書き換えて送れます。「ボタンの id には画像の場所しか入らないはず」と考えず、受け取る PHP 側で必ずチェックしましょう。
6. 送信された HTML を表示する
「POST送信テスト」の送信先では、受け取った HTML をそのまま表示せず、h()(htmlspecialchars())を通して、ソースとして見せています。
<?php if ($comment === ''): ?>
<p class="alert alert-warning">データが送信されていません。<a href="./">エディター</a>で文章を書いて「POST送信テスト」を押してください。</p>
<?php else: ?>
<p>エディターで書いた内容は、<code>$_POST['comment']</code> に <strong>HTML の文字列</strong>として届きます。</p>
<p>安全のため、ここでは HTML として表示せず、<code>htmlspecialchars()</code> で<strong>ソースのまま</strong>表示しています。</p>
<pre><?= h($comment) ?></pre>
<?php endif; ?>
エディターから届く値は HTML なので、そのまま echo すると <script> も実行されてしまいます。本当に HTML として表示したい場合は、使ってよいタグだけを残す「サニタイズ」の処理が別に必要です。
書き直したポイント
- 削除の PHP:以前は送られた場所をそのまま削除していたため、サイト内の任意のファイルを消せる危険な状態でした。上の手順5のチェックを追加しています。
- アップロードの PHP:
date(Y)のようにクォートのない書き方が PHP 8 でエラーになるためdate('Y')に直し、画像以外のファイルを受け付けないようにしました。公開デモのため、新しい画像を30枚だけ残して古いものを自動で削除しています。 - JavaScript:以前は削除の処理をページ内のすべてのボタンに付けていたため、関係ないボタンを押しても削除の通信が起きていました。画像一覧の削除ボタンだけに付けるように直しています。
siteUrlにドメインを直接書いていたのをやめ、サイトの場所から自動で作るようにしました。
削除する前に「本当に削除しますか?」と確認したい場合は、削除ボタンの処理の最初に if (!confirm('削除しますか?')) return; を入れてみましょう。