WordPressで投稿の同時編集による上書き事故を防ぐ方法【プラグイン無料配布】

,

複数人での同時編集、こんな事故はありませんか?

複数人で運用しているWordPressサイトで、同じ投稿をほぼ同時に編集して保存したら、後から保存した方の内容で上書きされてしまい、先に保存した方の変更がまるごと消えてしまった——そんな経験はありませんか?

WordPressには標準で「他のユーザーが現在この投稿を編集中です」という表示があります。ただしこれはHeartbeat APIによる注意喚起の表示だけで、実際に2人がほぼ同時に保存ボタンを押した場合の二重保存そのものは防いでくれません。表示に気づかずに保存してしまえば、事故は普通に起こります。

この記事では、投稿単位の排他ロックによって保存そのものを確実にブロックする自作WordPressプラグイン「EditLock」を紹介します。プラグインは無料でダウンロードできます。

EditLockについて

EditLockは、先に編集を始めたユーザーがロックを保持している間、他のユーザーによる保存を実際にブロックする投稿単位の排他ロックプラグインです。

主な機能:

  • 投稿の編集画面を開くと同時にロックを取得(先に開いた人が保持)
  • 編集画面を開いている間はHeartbeat APIに乗せてロックの有効期限(TTL、デフォルト120秒)を自動延長。タブを閉じる・ブラウザがクラッシュした場合は自然に失効
  • 他のユーザーが保存しようとすると、保存ボタンを押した瞬間にモーダルで警告し、保存を確実にブロック(fail-close方式)
  • クラシックエディタ・Gutenberg(ブロックエディタ)の両方に対応
  • 対応する投稿タイプはデフォルトで全て。管理画面の設定で投稿タイプごとに除外可能
  • ロックが残ってしまった場合に備え、管理画面から手動で強制解除できるツールも用意
  • オプションで、ゴミ箱への移動もロック対象にできる(デフォルトOFF)

インストール手順

  1. EditLockのZIPファイルをダウンロード(記事末尾のリンク)
  2. WordPressの「プラグイン」→「新規追加」→「プラグインのアップロード」からZIPをインストール・有効化
  3. 有効化すると、ロック管理用のテーブルが自動的に作成されます
  4. 管理画面左メニューの「設定」→「EditLock」で、必要に応じて設定を確認してください

使い方

1. 保存が競合したときの動作

誰かが投稿を編集中に、別の人が同じ投稿を開いて保存(クラシックエディタの「更新」、Gutenbergの「更新」/「公開」)しようとすると、保存はブロックされ、モーダルでロックを保持しているユーザー名が表示されます。

判定はあくまで保存ボタンをクリックした瞬間に行われるため、編集画面を開いているだけの状態では何も表示されません。

2. 設定画面

管理画面の「設定」→「EditLock」では、以下を設定できます。

  • ロックの有効期限(TTL、秒)
  • 対象から除外する投稿タイプ
  • ゴミ箱への移動もロック対象にするかどうか(デフォルトOFF)
  • 現在ロック中の投稿の一覧

3. ロックが残ってしまった場合の強制解除

通常はTTL(デフォルト120秒)で自動的に失効するため手動介入は基本的に不要ですが、万一ロックが残ってしまった場合は、設定画面の「現在ロック中」一覧から管理者が対象投稿を選んで強制解除できます。

技術的な実装について

プラグインの核心部分を簡単に紹介します。

ロックの取得はUPSERT一文でアトミックに

ロックの取得(acquire)は、SELECTしてからINSERTする2ステップにすると、その間に別のリクエストが割り込む競合状態(レースコンディション)が起こり得ます。EditLockではINSERT ... ON DUPLICATE KEY UPDATEのUPSERT一文にまとめ、InnoDBの行ロックに競合解消を委ねています。

public static function acquire( $post_id, $session_id, $user_id, $ttl_seconds ) {
    global $wpdb;
    $table = self::table_name();

    $wpdb->query( $wpdb->prepare(
        "INSERT INTO {$table} (post_id, session_id, user_id, locked_at, expires_at)
        VALUES (%d, %s, %d, NOW(), DATE_ADD(NOW(), INTERVAL %d SECOND))
        ON DUPLICATE KEY UPDATE
            session_id = IF(expires_at < NOW() OR session_id = VALUES(session_id), VALUES(session_id), session_id),
            user_id    = IF(expires_at < NOW() OR session_id = VALUES(session_id), VALUES(user_id), user_id),
            locked_at  = IF(expires_at < NOW() OR session_id = VALUES(session_id), VALUES(locked_at), locked_at),
            expires_at = IF(expires_at < NOW() OR session_id = VALUES(session_id), VALUES(expires_at), expires_at)",
        $post_id, $session_id, $user_id, (int) $ttl_seconds
    ) );

    return self::status( $post_id );
}

条件は「既存ロックが期限切れ」または「自分自身のセッションによる再取得」の場合のみ上書きする、というシンプルなものです。これにより、他人が有効なロックを保持している間は何回リクエストが来ても奪えません。

保存ブロックの「実効性」を巡って見つかったバグ

実装中、興味深いバグに遭遇しました。クラシックエディタの保存を確実にブロックするサーバー側のゲートとして、最初はadmin_action_editpostフックを使っていました。しかし実機で生のHTTPリクエストを送って確認したところ、このフックは実際には一度も発火していませんでした

原因はWordPressコア側にあります。wp-admin/post.phpcase 'editpost':edit_post()を直接呼び出すだけで、do_action( 'admin_action_editpost' )のようなdo_action()を経由しません。つまり、admin_action_{$action}という命名から「発火するはず」と思い込みやすいものの、editpostというアクション名に限っては罠だった、ということです。

修正では、投稿更新の直前に必ず発火するpre_post_updateアクションに差し替えました。

/*
 * admin_action_editpost はWordPressコアの post.php では実際には発火しないため使えない。
 * post.php の case 'editpost' は edit_post() を直接呼ぶだけで do_action() を経由しない。
 */
add_action( 'pre_post_update', 'edlk_pre_post_update_gate', 1, 2 );

function edlk_pre_post_update_gate( $post_id, $data ) {
    if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return; }
    if ( ! edlk_is_post_type_enabled( get_post_type( $post_id ) ) ) { return; }

    $session_id = sanitize_text_field( wp_unslash( $_POST['edlk_session_id'] ?? '' ) );
    edlk_current_session_id( $session_id );

    if ( '' !== $session_id && Edlk_Lock_Manager::is_holder( $post_id, $session_id ) ) { return; }

    $status = Edlk_Lock_Manager::status( $post_id );
    if ( ! $status ) { return; } // ロックが存在しない=競合なし

    $user = get_userdata( (int) $status['user_id'] );
    wp_die(
        esc_html( sprintf( '%s さんがこの投稿を編集中のため、保存できませんでした。ページを再読み込みしてやり直してください。', $user ? $user->display_name : '他のユーザー' ) ),
        '保存をブロックしました',
        [ 'response' => 409, 'back_link' => true ]
    );
}

pre_post_updateは新規投稿では発火せず既存投稿の更新時にのみ確実に発火するため、ロック判定の実効ゲートとして適しています。Gutenberg(REST経由)の保存は別途rest_pre_insert_{post_type}フィルタで判定しているため、ここではREST_REQUESTの場合を除外し、二重判定を避けています。

このように、JavaScriptによるモーダル表示は「親切な警告」に過ぎず、実際に保存を止めているのはサーバー側のこのゲートです。フロント側のJSが無効化されていたり通信エラーが起きたりしても、ここで確実にブロックされます。

注意事項

  • ブロック編集画面(Gutenberg)で「ゴミ箱へ移動」がブロックされた場合、サーバー側では正しく拒否されている(投稿は公開状態のまま)にもかかわらず、画面上には一瞬「ゴミ箱へ移動しました」という成功通知が表示されることがあります。投稿一覧を再読み込みすれば、実際には移動していないことを確認できます
  • ゴミ箱ガードをオンにした状態で投稿一覧から複数選択して一括ゴミ箱移動を行うと、対象にロック中の投稿が含まれている場合、その時点で処理が中断され、後続の投稿が未処理のまま残ることがあります
  • マルチサイトには対応していません(単一サイト想定)
  • 保存ブロックは wp_insert_post() / wp_update_post() を経由する更新であれば、呼び出し元(管理画面・フロント側フォーム・他プラグイン・REST API等)を問わず機能します。
    ゴミ箱ブロックは「ゴミ箱への移動もロック対象にする」が有効な場合のみ、wp_trash_post() を経由する処理、またはREST APIのDELETEリクエストに対して機能します。
    $wpdb->update() 等でDBを直接書き換えるプラグイン/自作コードの場合は、いずれも機能しません。

ダウンロード

EditLockは無料でダウンロードできます。

有償サポートやカスタマイズをご希望の方は、弊社サポートページ からお問い合わせください。

自己紹介

松田 大と申しますm(_ _)m
インディーズでミュージシャンをやっていたのですがいつのまにか…

とある企業でショップのアルバイトスタッフから正社員、支店長を経てシステム部門に異動するという、開発担当としては変わった経緯を持っている方だと思います。

「Excel VBA」からスタートして、Yamaha RTX シリーズで VPN環境構築、Hyper-V環境構築、Windowsアプリ開発などを経験した後、「 WordPress 」に出会い、どっぷりハマっています。

現在勤めているETBS合同会社では、「 WordPress 」を活用したWEBサイト、業務用WEBアプリケーション開発を中心に、記事の執筆代行や掲載に必要な情報のリサーチ、映像のテロップ入れや切りはりなどの簡単な動画編集なども、まとめて行なっています。

宮崎県 都城市 出身。東京都 葛飾区 在住。現在、代表兼二児のパパ。子育てを通じて、こどもたちにもプログラミングの楽しさに触れてほしいと思うようになり、「 こどもICTかつしか教室 」を開講中。最近は童心に帰り、簡単なゲーム制作なんかも楽しんでいます(^_^)。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

生成AIで作ったHTMLを、そのままWebサイトに。
ビジュアルプログラミング オンライン無料体験レッスン モニター大募集
こどもICTかつしか教室 | 幼稚園年長 小学生 中学生 スクラッチ ビジュアルプログラミング
WordPress で困ったら? | 必要なときに必要な分だけ、お助けサポート
WordPress / PHP なんでも相談OK