中級 jQuery ajax PHP ファイル操作

WYSIWYGエディターに「画像の削除」機能を付ける

見たまま編集できる文章エディター(WYSIWYG)に、アップロードした画像を一覧から削除する機能を追加します。ajax でサーバーの PHP を呼び出し、ファイルを安全に削除する方法を学びます。

このサンプルは2017〜2018年ごろに作成したものです。PHPのバージョンが上がり、そのままでは動かなくなっていたため、2026年に PHP 8.3 で動くように書き直し、解説も新しくしました。

DEMO 実際に動かしてみよう

文字を太字にしたり、画像を入れたりしながら文章を書けるエディターです。ツールバーの「画像アップロード」で画像を送り、「画像管理」タブで一覧を表示すると、画像ごとの「削除」ボタンで消せます。「POST送信テスト」を押すと、書いた内容が HTML として送信されます。
※ 公開デモのため、アップロードした画像は新しい30枚だけ残り、古いものから自動で削除されます。

解説コードを読んでみよう

このサンプルでわかること

  • 配布されている部品(エディター)に、自分で機能を書き足す方法
  • ボタンを押したら 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({ ... }) を実行すると、ツールバー付きのエディターに変わります。

index.php(9〜12行目) PHP
    <!-- エディター本体と、ボタンなどの日本語表示 -->
    <link rel="stylesheet" href="Editor/skeditor/skeditor.css">
    <script src="Editor/skeditor/editor.min.js"></script>
    <script src="Editor/skeditor/language.js"></script>
index.php(18〜39行目) PHP
<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/年/月/日/ のフォルダに保存します。保存する前に、拡張子と中身が本当に画像かを確かめています。

studiokeyEditorImageUploader.php(60〜75行目) PHP
$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)を入れておきます。

editor.min.js(326〜340行目) JavaScript
                        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 を呼ぶ

editor.min.js(345〜364行目) JavaScript
                    }).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(); 
                        })


                       });
                    })
  1. 348行目:画像一覧の中にある削除ボタンがクリックされたら動きます。
  2. 349行目:押されたボタンの id(=画像の場所)を取り出します。
  3. 351〜353行目:$.ajax() で wysiwyg_delet.php に、画像の場所を data という名前で POST 送信します。
  4. 359行目:通信が終わったら、一覧からその画像の行を隠します。

ajax(エイジャックス)を使うと、ページを切り替えずに裏側でサーバーと通信できます。削除のたびにページ全体を読み込み直さなくてよいので、操作が軽快になります。

5. 画像を削除する PHP(いちばん大事なところ)

wysiwyg_delet.php 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つでも当てはまらなければ削除しません。

  1. realpath() で、../ などを取り除いた「本当の場所」に直す
  2. そのファイルが実際に存在するか(is_file())
  3. 本当の場所が wysiwygUpload フォルダの中にあるか
  4. 拡張子が画像(jpg / png / gif)か

ブラウザから送られてくる値は、画面のボタンを使わなくても自由に書き換えて送れます。「ボタンの id には画像の場所しか入らないはず」と考えず、受け取る PHP 側で必ずチェックしましょう。

6. 送信された HTML を表示する

「POST送信テスト」の送信先では、受け取った HTML をそのまま表示せず、h()(htmlspecialchars())を通して、ソースとして見せています。

wysiwyg_post.php(13〜20行目) PHP
<?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; を入れてみましょう。

↑