Developer

コンテンツ更新管理 — 開発者向け情報

フック・ショートコード・カスタム投稿タイプなど、本プラグインを拡張・連携するための技術情報です。

本ページは コンテンツ更新管理(プラグインスラッグ content-maintenance-plugin・名前空間 ContentMaintenancePlugin・prefix plg_cmp_)を拡張・調査・移行するための技術情報です。記載はすべて v1.0.1 の実装に基づきます。

ブロック「コンテンツ更新管理セクション」

本プラグインが登録するブロックは 1 つだけです。ショートコードは 1 つも登録せず、カスタム投稿タイプ・カスタムタクソノミーも追加しません。

項目
ブロック名content-maintenance-plugin/section(PHP 側の定数は Block::NAME
インサーターの表示名コンテンツ更新管理セクション
apiVersion / category / icon3 / design / clipboard(Dashicon)
登録元ビルド成果物 block/build/content-maintenance-section/block.json が無ければブロックを登録せずactivate_plugins を持つユーザーにだけ管理画面通知を出す
資産editorScript / editorStyle のみ。フロント用の CSS / JS を 1 本も持たない

attributes は managedId 1 個だけ

属性型 / 既定値内容
managedIdstring / ""管理 ID。書式は blk + 16 進 12 桁(例 blk3f9a1c04b7de)。PHP 側の定数は Block::ATTR_ID

担当者・承認者・次回確認日・更新頻度・重要度・通知・期限切れ処理といった管理設定は、ブロック属性に一切入りません。正本はカスタムテーブル plg_cmp_targets の行で、ブロックが持つのは行を引くための管理 ID だけです。したがって post_content をパースしても管理設定は取り出せませんし、逆に post_content を書き戻しても管理設定は変わりません。エディタのサイドバーの「管理設定を保存」は投稿の「更新」とは別の REST 書き込みです(次章)。

supports(4 キー)

キー意味
htmlfalse「HTML として編集」を出さない
anchortrueHTML アンカー(id)を付けられる
align[ "wide", "full" ]幅広・全幅の 2 段階のみ
reusablefalse同期パターンに変換できない

色・タイポグラフィ・スペーシングの supports は宣言していません。保存側は useBlockProps.save()useInnerBlocksProps.save()<div> 1 枚で、期限の状態を HTML に焼き込みません(表示のたびに判定するため)。

管理 ID の発番とネスト禁止

  • 発番はクライアント(crypto.getRandomValues の 6 バイト)とサーバ(wp_insert_post_data での保険)の 2 か所にあります。クライアント側の発番条件は「① managedId が空」「② エディタ内の自分以外のブロックが同じ ID を持つ」「③ その ID を別の投稿が持っている(REST で確認)」の 3 つで、振り直すのは新しくマウントした側(貼った側)だけです。
  • ③ は「この投稿の保存済み本文に同じ ID があるならコピーではない」と判断して通信しません。通常の編集・エディタの読み込みでは、この重複確認の REST を 1 回も呼びません(管理設定サイドバーを開いたときの取得と、保存後の通知取得は別経路です)。
  • 発番は __unstableMarkNextChangeAsNotPersistent() の直後に行うため、取り消し履歴に独立した段を作りません。
  • サーバ側の振り直しの結果本文が変わらなければ post_content に触りません(保存のたびに差分が出るのを避けるため)。逆に言うと、ブロック管理を使う投稿では管理 ID を振るために post_content が書き換わることがあります。
  • 入れ子は 3 層で塞いでいます。エディタは allowedBlocks から自分自身だけを除外し、サーバは管理ブロックの内側にある管理ブロックへ ID を振らず、収集もしません(=管理対象になりません)。
  • 同期パターン(core/block)の内側にいると判定したときは、クライアントは管理 ID を一切触りません。同期パターンの実体である投稿タイプ wp_block は管理対象にできる投稿タイプの候補からも除外されています。

フロント側に出るもの・出ないもの

フック登録数内容
the_content0本文フィルタは 1 つも登録していない
wp_enqueue_scripts / wp_head / wp_footer0フロントへ CSS・JS・マークアップを足さない
render_block1期限切れの管理ブロックだけ、出力を空文字(ブロックを非表示)または代替メッセージの <div class="plg-cmp-block-alt-message"> に差し替える
template_redirect1期限切れ処理「指定 URL へリダイレクト」の実行(wp_redirect()・301 / 302)
pre_get_posts1リダイレクト中の投稿をメインクエリの検索・フィード・アーカイブ・ホームから post__not_in で外す

render_block の分岐条件は is_admin() だけです。したがってページ表示・フィード・抜粋の生成・公開 REST の content.rendered の 4 経路には差し替えが効き、ブロックエディタのキャンバスには効きません。事前に本文を書き換えていないので、期限が来た瞬間から効き、確認完了で即座に元へ戻ります。代替メッセージは esc_html() を通すプレーンテキストで、専用の CSS は出力しません(クラス名だけ付けてテーマに任せる設計)。リダイレクト対象のマップは 300 秒の transient にキャッシュし、確認完了・確認結果登録・期限切れ処理実行・save_post で破棄します。

ブロックの期限切れ処理は 4 種none warn hide alt_message)で、フロント出力が変わるのは hidealt_message の 2 つだけです。表示ラベルは画面によって異なり、ブロックのサイドバー(JS)は フロントで非表示、管理画面(PHP)は ブロックを非表示 です。

REST API

名前空間は content-maintenance-plugin/v1register_rest_route() の呼び出しは 3 本で、メソッド×パスの組み合わせは 4 エンドポイントです。認証は WordPress コア標準の cookie 認証 + noncewp_rest / X-WP-Nonce)で、独自のトークン発行はありません。4 エンドポイントすべてに permission_callback が設定されています(コアの rest_cookie_check_errors() が nonce を検証する前提で、プラグイン側では二重検証しません)。いずれも管理画面・ブロックエディタ向けで、フロント公開用の API ではありません。

エンドポイントメソッド / 権限内容
/content-maintenance-plugin/v1/block-targetGET / plg_cmp_review かつ 対象投稿の edit_post管理ブロックの管理設定と選択肢の取得
/content-maintenance-plugin/v1/block-targetPOST / plg_cmp_edit_targets かつ 対象投稿の edit_post管理設定の保存(投稿の「更新」とは別経路)
/content-maintenance-plugin/v1/block-target/owned-elsewhereGET / 対象投稿の edit_post のみ「その管理 ID を別の投稿が持っているか」だけを返す。独自 capability を要求しない意図的な緩和(寄稿者のブロック複製を壊さないため)
/content-maintenance-plugin/v1/editor-noticesGET / 対象投稿の edit_post のみメタボックスの保存結果(検証エラー・警告・成功メッセージ)をブロックエディタへ渡す。GET だが読んだら消える one-shot

パラメータ

名前型 / 必須対象備考
post_idinteger / 必須4 エンドポイント共通/editor-notices では absint でサニタイズ
object_keystring / 必須/block-target 系 3 つ管理 ID。サーバ側で sanitize_key() を通す
settingsobject / POST のみPOST /block-targetargs に宣言が無くget_param() で受けて内部の parse() が全項目を検証する

post_id / object_keyargs で必須宣言しているため、欠けたリクエストは WordPress コアが 400 で弾きます。

GET /block-target のレスポンス(6 キー)

キー意味
existsbool管理対象の行があるか
settingsobject \| null管理設定(行が無ければ null)。下記 22 キー
choicesobject選択肢一式 8 キー(assignees / approvers / escalation / units / priorities / actions / channels / connections
editablebool保存できるか(plg_cmp_edit_targetsedit_post
canAssignbool担当者・承認者・エスカレーション先を変更できるか(plg_cmp_manage_targets
ownedElsewhereboolその管理 ID を別の投稿が持っているか

settings の 22 キーは id / name / assigneeId / approverId / escalationUserId / nextReviewDate / intervalValue / intervalUnit / priority / reviewNote / referenceUrl / notifyBefore / notifyAfter / notifyOnDue / escalationDays / notifyChannels / notifyConnections / expirationAction / altMessage / workflowStatus / recordStatus / expired です。expired は保存列ではなくサーバ側の判定結果で、エディタは次回確認日から自前で計算せずこの値だけを見ます。

候補ユーザー(assignees / approvers / escalation)は 1 回 100 件まで取得してから権限で絞るため、候補は 100 件未満になりえます。現在の担当者が候補条件から外れた場合は {表示名}(権限なし) として選択肢に残します。

POST /block-target のレスポンスとエラー

状況HTTP / コード応答
成功200exists / settings / choices / editable / warnings の 5 キー
管理対象の行がまだ無い404 plg_cmp_block_target_missingこの管理ブロックはまだ保存されていません。投稿を保存してから設定してください。
入力エラー400 plg_cmp_block_target_invalidエラー文をスペース区切りで連結したメッセージ

エラーが 1 つでもあると 1 項目も保存されません(部分保存はしません)。保存時の検証は次のとおりです。

ルールメッセージ(原文)
管理対象名は必須管理対象名を入力してください。
担当者は必須担当者を選択してください。
担当者は「確認を実行」権限が必要(値を変えたときのみ検査)担当者には「確認を実行」権限を持つユーザーを選択してください。
担当者はその投稿を編集できること(同上)担当者には、この投稿を編集できるユーザーを選択してください。
担当者と承認者の兼任禁止(無条件)担当者と承認者に同じユーザーは指定できません。
承認者は「承認を実行」権限が必要(同上)承認者には「承認を実行」権限を持つユーザーを選択してください。
承認者はその投稿を編集できること(同上)承認者には、この投稿を編集できるユーザーを選択してください。
エスカレーション先の実在(同上)エスカレーション先に存在しないユーザーが指定されています。
割り当て 3 項目の変更には plg_cmp_manage_targets担当者・承認者・エスカレーション先を変更する権限がありません。
次回確認日は必須・正しい日付次回確認日を正しい日付で入力してください。
通知イベントを 1 つでも有効にするならチャンネル必須通知を有効にする場合は通知チャンネルを 1 つ以上選択してください。
Slack / Chatwork を選ぶならそのチャンネルの送信先が必須%1$s を選ぶときは %1$s の送信先を 1 つ以上選択してください。
警告(保存は通る。warnings で返る)担当者とエスカレーション先が同じユーザーです。

期限前・期限超過の日数リストは保存時に「正の整数だけ・重複除去・降順」へ整形されます。保存が成功すると、担当者・承認者・エスカレーション先・次回確認日が変わった項目ごとに専用の履歴が残り、どれも変わっていなければ「管理設定変更」の履歴が 1 件残ります。

その他 2 エンドポイントの応答

  • GET /block-target/owned-elsewhere{ "ownedElsewhere": true|false } の 1 キーのみ。通信に失敗したときクライアントは false(=別投稿のものではない)に倒します。
  • GET /editor-notices{ "errors": [], "warnings": [], "success": [] }。投稿単位(Post\Meta_Box)とフィールド単位(Field\Meta_Box)の transient を両方 drain し、同じ文言は畳みます。ブロックエディタはメタボックスの保存 POST が返す HTML を捨てるため、保存後にこのルートへ取りに来て core/notices として表示する仕組みです。REST と画面描画のどちらか一方だけが消費します。

WP-CLI コマンドは提供していません。上記 4 本のほかに、同梱のバグ報告モジュールが別名前空間のルートを 1 本追加します(管理者専用。本プラグインの管理 API ではありません)。

カスタムテーブル(4 本)

物理テーブル名は $wpdb->prefixplg_cmp_ + 論理名です。wp_plg_cmp_targets と決め打ちできません(サイトの DB プレフィックスに従います)。作成は dbDelta()、削除は DROP TABLE IF EXISTS(アンインストールの「すべて削除」のみ)。

論理名役割(1 行 = 何か)列数索引数
targets管理対象の本体。種類・紐づく投稿・対象キー・共通設定・運用ステータス・確認基準値・確認サイクル・期限状態キャッシュ・レコードの状態398
history操作履歴(追記型・操作種別 17 種)144
notification_log通知ログ。重複防止キー・送信結果・試行回数・失敗理由・接続テスト履歴165
rules運用ルール(適用条件と初期管理設定を JSON 3 列で持つ)113

日時カラムはすべて current_time( 'mysql' ) = サイトタイムゾーンの壁時計で保存します(UTC ではありません)。履歴・通知ログ・管理対象の created_at / updated_at / deadline_state_at のいずれも同じで、履歴の刈り込み境界も「サイトタイムゾーンの今日 − N 日 00:00:00」という壁時計文字列で作られます。期限判定の基準日も wp_date( 'Y-m-d' ) です。外部から日時を突き合わせるときは gmdate() 系と混ぜないでください。

plg_cmp_targets の主要カラム

型 / 既定値意味
target_typeVARCHAR(20) / 'post'種類。post / block / acf / custom_field の 4 種
post_id / object_keyBIGINT / VARCHAR(191)紐づく投稿と対象キー(投稿は空文字、ブロックは管理 ID、ACF はフィールド名、カスタムフィールドはメタキー)。191 文字超は保存前に弾く
nameVARCHAR(191) / ''管理対象名。191 文字へ丸めるのは運用ルールの自動適用だけで、投稿・フィールド・ブロックの手入力経路は丸めない(超過すると保存自体が失敗しうる)
assignee_id / approver_id / escalation_user_idBIGINT / 0担当者 / 承認者 / エスカレーション先(0 = 未設定)
next_review_dateDATE NULL次回確認日
interval_value / interval_unitSMALLINT / VARCHAR(10) 'none'更新頻度(1〜999 + 単位 5 種)
priorityVARCHAR(10) / 'normal'重要度 4 段
workflow_statusVARCHAR(20) / 'pending'運用ステータス 8 種(うち完了 3 種)
notify_channels / notify_connections / notify_before / notify_afterVARCHAR / ''通知チャンネル・送信先(接続 ID)・期限前日数・期限超過日数(いずれもカンマ区切り)
notify_on_dueTINYINT / -1期限当日通知。-1 = 全体設定に従う / 0 = 送らない / 1 = 送る
escalation_daysSMALLINT / 0エスカレーションまでの超過日数(0 = 送らない・上限 65535)
expiration_action / expiration_argsVARCHAR(20) 'none' / LONGTEXT期限切れ処理と追加引数(JSON。リダイレクト URL・代替メッセージ)
expired_done_cycleINT / 0期限切れ処理を実行済みの確認サイクル番号(冪等性のキー)
previous_post_statusVARCHAR(20) / ''下書き・非公開へ変える前の公開状態(「以前の公開状態へ戻す」で使い、復元後は空に戻る)
baseline_value / baseline_hash / baseline_saved_atLONGTEXT / CHAR(40) / DATETIME確認基準値。baseline_hash が空 = 未確認。ACF・カスタムフィールドだけ baseline_value に正規化済みの値そのものも入る
has_changesTINYINT(1) / 0前回確認後に変更あり
cycleINT / 1確認サイクル番号
deadline_state / deadline_state_atVARCHAR(20) / DATETIME期限状態の検索用キャッシュ。画面の値は読み出しのたびに現在日で算出し直すので、この列を直接読むと古い可能性がある
record_statusVARCHAR(20) / 'active'有効 / 無効化(disabled)/ 論理削除(trashed
  • 行の同一性は uniq_target(target_type, post_id, object_key) の複合ユニークです。二重登録しようとすると作成が 0 を返します。索引は他に post_id / sweep(record_status,next_review_date) / assignee / approver / list_filter(record_status,workflow_status,deadline_state) / type_priority
  • リポジトリは列一覧に無いキーを捨て、列挙カラム(target_type / priority / workflow_status / last_result / interval_unit / record_status / expiration_action / deadline_state)と notify_channels の値を検証して、外れていれば保存そのものを拒否します。

plg_cmp_history(14 列)

列は id / target_id / cycle / action / actor_type / actor_id / value_before / value_after / comment / channel / result / log_id / rule_id / created_at。索引は PRIMARY / target_created(target_id,created_at) / created_at / action の 4 本です。

  • action17 種の許可リストで、そこに無い値は書き込み時点で捨てられます(target_created / settings_changed / assignee_changed / approver_changed / escalation_changed / next_date_changed / status_changed / review_started / review_registered / approval_requested / approved / rejected / notification_sent / notification_failed / expiration_executed / expiration_released / notification_retry)。
  • actor_typeuser / system の 2 値。明示しなかった場合は actor_idget_current_user_id() の順に解決し、それも 0 なら system になります。定期処理からの書き込みは自動的にシステム扱いです。
  • 配列・オブジェクトの値は wp_json_encode() して保存します。
  • リポジトリに update / delete メソッドがありません。クラス全体で INSERT と SELECT と DELETE 1 本だけで、その DELETE を呼ぶのは保持期間の刈り込み 1 か所です。管理画面から履歴を編集・削除する UI もありません。

plg_cmp_notification_log(16 列)と plg_cmp_rules(11 列)

  • 通知ログの列は id / kind / target_id / cycle / event / timing / channel / recipient / role / dedup_key / status / attempts / last_error / reason / created_at / updated_atdedup_key に UNIQUE 制約があり、これが二重送信の防止装置です(次章)。索引は PRIMARY / uniq_dedup / target_cycle / kind_status / created_at
  • 運用ルールの列は id / name / post_type / target_type / priority / enabled / term_conditions / object_keys / defaults / created_at / updated_at。絞り込みに使う値だけを列に出し、条件と初期管理設定は LONGTEXT の JSON 3 列で持ちます。索引は PRIMARY / match_lookup(enabled,post_type,priority) / type_priority

スキーマ版と追従

項目
スキーマ版'5'Schema::DB_VERSION
記録先オプション plg_cmp_db_version(autoload しない)
有効化時無条件で Schema::create()dbDelta() を CREATE 文ごとに実行)
更新時の追従admin_initmaybe_upgrade_on_admin()manage_options を持つユーザーのリクエストのとき、かつ版が違うときだけ走る

オプション・post meta・user meta

設定画面が書き込むオプション(16 個)

キー画面上の場所 / 内容
plg_cmp_setting_post_types基本設定。管理対象とする投稿タイプ。一度も保存していないときだけ候補すべてで、全部外して保存すると 0 件
plg_cmp_setting_approaching_days基本設定。期限接近の閾値(日)。既定 7・範囲 1〜90(保存時も読み出し時もクランプ)
plg_cmp_setting_history_retention_days基本設定。履歴保持期間(日)既定 0 = 無期限・負値は 0 へ丸め・上限なし
plg_cmp_setting_defaults基本設定 > 新規管理対象の既定値。interval_value / interval_unit / priority / notify_before / notify_after / expiration_actionpost / block / acf / custom_field の 4 系統)/ alt_message を 1 オプションにまとめる
plg_cmp_uninstall_mode基本設定 > アンインストール時のデータ削除。keep(既定)/ settings / allあえて plg_cmp_setting_ 接頭辞を付けていない(「設定のみ削除」で削除方法の指定が先に消えないため)
plg_cmp_setting_mail_enabled通知設定。未設定なら有効(無効時は送らず「設定によりスキップ」を 1 件記録)
plg_cmp_setting_notify_on_due通知設定。期限当日通知の既定。既定は '0' = 送らない(オプトイン)
plg_cmp_setting_extra_mail_recipients通知設定 > 追加のメール宛先
plg_cmp_connection_slack / plg_cmp_connection_chatwork通知設定。Slack の Incoming Webhook URL、Chatwork の API トークンとルーム ID。画面では先頭 4 文字と末尾 4 文字だけを残してマスク表示(12 文字以下は全体を伏せる)
plg_cmp_setting_acf_enabled / _acf_field_types / _acf_excluded_fields / _acf_excluded_groupsACF 設定
plg_cmp_setting_custom_field_keys / _custom_field_excludedカスタムフィールド設定

期限切れ処理の既定値は 4 系統(投稿・ブロック・ACF・カスタムフィールド)すべて none何もしない、期限当日通知の全体既定は OFF、履歴保持期間は 0 = 無期限、アンインストールは keep = すべてのデータを保持です。つまり有効化しただけでは、公開中の投稿のステータスが変わることも、履歴が消えることも、削除でデータが消えることもありません。移行スクリプトで既定値を上書きするときは、この安全側の初期状態を意図的に外していることになります。

設定 UI を持たないオプション・transient

キー内容
plg_cmp_caps_grantedcapability を初回付与済みかのフラグ(次章)
plg_cmp_db_versionスキーマ版(現在 5
plg_cmp_installed_version初回インストール時のプラグインバージョン(既存値は上書きしない)
plg_cmp_sweep_cursor毎時スイープの巡回位置({ date, id }。autoload しない)
plg_cmp_sweep_lockスイープの排他ロック(TTL 300 秒)。transient ではなく options のユニーク制約で取る
plg_cmp_rule_job_{ルールID} / plg_cmp_rule_bulk_lock_{ルールID}一括適用のジョブ状態とロック
plg_cmp_license_key / _status / _expires_at / _last_check / _max_activations / _current_activationsライセンス情報(キーは暗号化保存。option_prefixplg_cmp_
plg_cmp_pbr_disabledバグ報告ウィジェットのサイト側無効化フラグ

transient(options 上は _transient_plg_cmp_*)は plg_cmp_redirect_map(リダイレクト対象マップ・300 秒)/ plg_cmp_notice_{user}_{post}(メタボックスの保存結果・60 秒)/ plg_cmp_field_notice_*(フィールドの検証結果)/ plg_cmp_license_notice(60 秒)/ plg_cmp_update_data_{md5}plg_cmp_releases_data(更新情報・12 時間)です。ライセンス API の向き先は wp-config.phpdefine( 'PLG_API_URL', … ) で上書きできます。

post meta / user meta

キー内容
post meta _plg_cmp_baseline_pendingACF 連携が止まっている間に確認完了したとき「確認基準値を取り直す必要がある」印だけを立てる。停止中に基準を取ると「値なし」を基準にしてしまうため。連携が戻った最初の保存で取り直す
user meta plg_cmp_slack_member_idSlack メンション用のメンバー ID。保存時に英数字以外を落とす。未設定でも通知は届く(表示名だけになる)
user meta plg_cmp_chatwork_account_idChatwork メンション用のアカウント ID。保存時に数字以外を落とす。同上

user meta の入力欄はユーザープロフィール画面の見出し コンテンツ更新管理の通知 にあります(設定画面ではありません)。編集に必要なのは edit_user だけで、独自 capability は要りません。post meta はこの 1 キーのみで、管理設定を post meta に持たせている箇所はありません(正本はすべてカスタムテーブル)。

独自 capability(5 種)とロール既定

capability(機械名)用途administratoreditorauthorcontributorsubscriber
plg_cmp_manage_targets管理対象の全操作。担当者・承認者・エスカレーション先の割り当て変更、一括操作
plg_cmp_edit_targets管理対象の追加と管理情報の編集(割り当て変更は含まない)。メタボックスの保存、ブロック REST の書き込み、「以前の公開状態へ戻す」
plg_cmp_review確認作業の開始・確認結果の登録(=担当者に指定できる条件)。管理対象一覧 / 自分の担当 メニュー
plg_cmp_approve承認・差し戻し(=承認者に指定できる条件)
plg_cmp_manage_settings管理設定・通知接続設定・運用ルールの閲覧と変更。設定 / 運用ルール / 通知ログ の 3 画面

付与は初回インストール時の一度きりです。フラグオプション plg_cmp_caps_granted が立っていれば、プラグイン更新でも再有効化でも maybe_grant() は即 return します。運用側でロールから外した capability(例: 投稿者から plg_cmp_edit_targets を剥がす)を、更新のたびに黙って復元しない設計です。裏返すと、新しい capability を後から配りたいときは自分で add_cap() する必要があります。付与は role への add_cap のみで、個別ユーザーへ付与する UI はありません(権限管理プラグイン等の標準機構で行う想定)。

  • 寄稿者(contributor)・購読者(subscriber)には既定で何も付きません。capability を 1 つも持たないユーザーには、サブメニューが 1 件も登録されず、投稿編集画面のメタボックスも登録されず、管理画面の CSS/JS も読み込まれず、URL を直接叩けば 403、/block-target の読み書き 2 本は REST でも拒否されます(残る /block-target/owned-elsewhere/editor-notices は前述のとおり対象投稿の edit_post だけで通ります)。
  • 最上位メニュー(ダッシュボード)とライセンス画面だけは独自 capability ではなく manage_options を要求します。plg_cmp_manage_settings を編集者に付けてもこの 2 画面は開きません。
  • 担当者・承認者に指定できるのは「対応する capability を持ち、かつその管理対象が紐づく投稿の edit_post を持つ」ユーザーです。したがって投稿者ロールは自分が書いた投稿しか担当できず、edit_pages を持たないため固定ページは自分が作成者でも担当できません。
  • エスカレーション先だけは capability を問いません(候補一覧からは購読者ロールのみ除外)。
  • 一覧に何行見えるかを決めるのは独自 capability だけではありません。plg_cmp_manage_targets または WordPress 標準の edit_others_posts のどちらかを持っていれば全件が見えます(既定では管理者と編集者)。どちらも持たないユーザーに並ぶのは、自分が担当者・承認者・エスカレーション先のいずれかである行と、自分が作成者の投稿の行(その投稿タイプを編集できる場合に限る)だけです。担当を持つ行は開けなくても一覧には残します——本人に見えていないと、一覧の警告から直しようがないためです。
  • ワークフローの可否判定が見るのは「操作名 ・ record_status ・ 現在の運用ステータス ・ capability」の 4 つだけで、担当者への指名は条件に入りません(代行できます)。無効化された管理対象では全操作が拒否されます。
  • 剥奪(revoke())は既定付与の対象外だったロールも含めて全ロールを走査し、5 種を remove_cap します。呼び出し元はアンインストールの「すべて削除」だけです。

定期処理(WP-Cron)

本編が wp_schedule_event() で登録する繰り返しイベントは 2 本です(1 メソッドの 2 箇所)。これに加えて同梱のライセンスモジュールが日次のライセンス確認を 1 本登録するので、WP Crontrol などで見えるこのプラグイン由来の繰り返しイベントは 3 本になります。運用ルールの一括適用は繰り返しではなく、実行中だけ単発イベントを予約します。

フック間隔中身
plg_cmp_hourly_sweephourly本編のスイープ(期限状態キャッシュ更新・新サイクル開始・期限切れ処理・通知・通知の自動再送・履歴の刈り込み)
plg_cmp_daily_sweepdailyACF のフィールド定義が消えていないかの点検だけ。ACF 連携が止まっていれば即 return
  • 登録は有効化フックに加えてadmin_init のたびに再試行します(wp_next_scheduled() で守られているため冪等)。日次イベントを後から追加した際に既存サイトへ追随させるための措置で、「有効化時にしか登録されない」わけではありません。
  • 無効化時は 2 本とも wp_clear_scheduled_hook() で解除します。データは消えません。
  • DISABLE_WP_CRON でコアの cron を止めているサイトでは、サーバーの cron から wp cron event run --due-now を実行してください。通知と期限切れ処理は毎時の定期処理で走ります。

スイープ 1 回の処理順

  1. ロック取得。取れなければ即 return(skipped = true)。
  2. 基準日(サイトタイムゾーンの今日)と期限接近閾値(既定 7 日)を決める。
  3. カーソル(前回の続き)を plg_cmp_sweep_cursor から読み、管理対象を 200 件取得(record_status = 'active' かつ next_review_date IS NOT NULLnext_review_date ASC, id ASC 順)。
  4. 各件の期限状態キャッシュを最新化(値が変わったときだけ DB を書く)。
  5. 完了 3 ステータス かつ 期限状態が approaching / due_today / overdue の行について新サイクルを開始(cycle +1・workflow_statuspending へ)。
  6. 各件について do_action( 'plg_cmp_sweep_target', $target, $today ) を発火。priority 10 で投稿 / ブロック / フィールドの期限切れ処理が、priority 20 で通知の送信判定がぶら下がる。
  7. カーソルを進める(取得件数が上限に満たなければ末尾なのでオプションを削除=次回は先頭から)。
  8. 履歴の刈り込み(保持期間が 1 日以上のときだけ・1 回あたり最大 200 件・古い順)。
  9. finally でロック解放。

これとは別に、plg_cmp_hourly_sweep そのものへ priority 20 で通知の自動再送がぶら下がります。こちらは管理対象 1 件ずつではなくスイープ 1 回につき 1 度動き、試行回数が上限(通算 3 回)未満の失敗ログを最大 50 件再送します。Sweeper::run() の戻り値は { processed, cycles_started, skipped, pruned } の 4 キーです(docblock は 3 キーと書いていますが実装は 4 キー)。

抽出範囲を「期限が近いものだけ」に絞っていないのは、管理対象ごとに通知タイミングが違うためです(閾値で切ると閾値より前の段階の通知が構造的に評価されなくなる)。代わりに次回確認日の昇順で全件をカーソルで巡回します。

冪等性は 3 層で担保する

対象キー仕組み
通知の送信dedup_key = {target_id}:{cycle}:{event}:{timing}:{sha1(channel"|"recipient)}通知ログテーブルの UNIQUE 制約への INSERT が成功したときだけ送る。アプリ側のフラグではなく DB 制約
期限切れ処理管理対象の expired_done_cycle 列 === cycle実行時に現在の cycle を書き込み、同じサイクルでは以後スキップ
定期イベントの多重起動オプション plg_cmp_sweep_lock{$wpdb->options} への INSERT IGNOREアトミックに取る

ロックは transient ではなく options テーブルのユニーク制約です(要件ドキュメントとコード内コメントの一部に「transient ロック」と書かれていますが、実装は transient API を 1 行も使っていません)。wp-cron.php は非認証で誰でも叩ける公開エンドポイントで、ほぼ同時に複数リクエストが来るため「読んでから書く」方式では両方が取得できてしまう、というのが理由です。TTL は 300 秒。取り残しの掃除は「読み取った値と一致するときだけ消す」条件付き DELETE で、解放も自分が取ったトークンと一致するときだけ行います。合い札は time() . ':' . wp_generate_password( 12, false, false )。運用ルールの一括適用が別キーで同じ Lock を使います。

補足として、新しい確認サイクルに入ると cycle が変わるのでキーも変わり、通知も期限切れ処理も再び走ります。「何もしない」設定や遷移先 URL が空のリダイレクトも expired_done_cycle は消費します(消費しないと毎時同じ判定を繰り返すため)が、履歴は残しません。停止中(投稿タイプを管理対象から外した / ACF 連携が止まっている)は expired_done_cycle を進めませんが、通知側は停止中でも重複防止キーを消費します(「送らなかった」記録を 1 件残す)。これにより、停止を解除しても止まっていた期間の通知が後からまとめて飛ぶことがありません。

キャッチアップと期限状態の再計算

  • スイープは実行のたびに「いま処理すべきで、まだ処理済み記録のない対象」を抽出するので、実行時刻に依存しません。WP-Cron が遅れても次の実行で補完されます。期限切れ処理は期限状態が overdue でありさえすれば何日遅れていても実行されます。
  • 期限前通知は経過した段階を遡って送りません。いま有効な最新段階だけを送ります。
  • deadline_state 列は検索用キャッシュにすぎません。一覧の行・詳細画面のバッジ・メタボックスは読み出しのたびに現在日で算出し直し、一覧の絞り込み・並び替え・ダッシュボードの件数は SQL で次回確認日から導出します。判定規則の正本は Deadline::state() ひとつで、SQL 版はその写しです。
  • 履歴の刈り込みは毎時スイープの最後に走ります(日次ではありません)。1 回あたり最大 200 件・古い順で、残りは次のスイープに任せます。保持期間の既定は 0 = 無期限なので、そのままでは 1 件も消えません。

管理側の送信経路(admin-ajax.php は使わない)

wp_ajax_ フックの登録は 1 か所もありません。管理画面の操作はすべて通常の POST → 処理 → wp_safe_redirect() で、画面はリロードされます。「画面を離れずに保存」「Ajax で一覧を更新」といった挙動はありません。JavaScript は管理画面用の 2 ファイル(assets/js/plg_cmp_admin.js / assets/js/plg_cmp_editor_notices.js)とブロックエディタ用のビルド成果物だけで、フロントには 1 本も出ません。

観点実装
nonce経路ごとに独立した action / field を持つ。投稿メタボックス、フィールドメタボックス(種類ごと)、設定画面、運用ルール(保存 / 削除 / 一括適用の開始・実行・中止)、一覧の一括操作、通知ログ、詳細画面、ユーザープロフィール
検証の順序nonce → 対象投稿の edit_post → 独自 capability の 3 段。nonce 切れは黙って捨てず理由を残す(例: 編集画面を開いてから時間が経ちすぎたため、フィールドの管理設定を保存できませんでした。もう一度お試しください。
管理画面内のリダイレクトすべて wp_safe_redirect()
一括適用の進捗JavaScript を使わず <meta http-equiv="refresh">(1 秒間隔)で次のバッチへ進む。バッチを進める GET 経路だけは nonce が URL に載る
リダイレクト先 URL の検証保存時に esc_url_raw()自己参照と最大 10 段のチェーンのループ検査。ステータスは 301 / 302 のみ。読み出し側でも esc_url_raw() を通す多層防御。外部ドメインは意図的に許容している(wp_safe_redirect() ではなく wp_redirect()
秘密情報のマスクWebhook URL / API トークンは先頭 4 文字と末尾 4 文字だけを残して * に置換(12 文字以下は全体を伏せる)。設定画面では入力欄に value を持たせずプレースホルダにマスク値を出す
直 SQLカスタムテーブルへのクエリはリポジトリ層に集約。テーブル名と ORDER BY はホワイトリストから組み立てた固定文字列で、値は $wpdb->prepare へ渡す。LIKE を使うのは一覧のキーワード検索・通知チャンネルの部分一致絞り込み・通知ログの集計と、アンインストールの一括削除で、いずれも $wpdb->esc_like() を併用する。テーブル DROP はテーブル名を内部で組み立てた固定文字列

フック

プラグイン自身が発火するアクションは do_action() 9 か所 / 7 種、拡張用のフィルタは apply_filters() 6 か所 / 6 種です(いずれも includes/ 配下・同梱モジュール分を除く。Plugin::PREFIXplg_cmp_)。JavaScript 側(@wordpress/hooks)のフックはありません。

アクション(do_action・7 種)

フック名引数いつ発火するか
plg_cmp_sweep_targetarray $target, string $today毎時スイープが管理対象 1 件を処理するたび。期限切れ処理(priority 10)と通知(priority 20)がここにぶら下がる
plg_cmp_daily_check_targetarray $target日次点検が ACF 型の管理対象 1 件を見るたび
plg_cmp_target_review_registeredint $target_id, array $target確認結果を登録した直後(承認者の有無に関わらず)。確認基準値の取り直しはここにぶら下がる
plg_cmp_target_completedint $target_id, array $target(完了前の行)確認が完了したとき(承認者なし=確認結果の登録時 / 承認者あり=承認時)。確認基準値の保存と期限切れ処理の解除がここにぶら下がる
plg_cmp_notify_eventstring $event, int $target_idワークフロー起因の通知イベント(承認依頼・承認完了・差し戻し)。送るかどうか・どこへ送るかは通知側が決める
plg_cmp_expiration_executedint $target_id, array $target(実行前の行), string $outcome期限切れ処理を実行したとき。投稿 / ブロック / フィールドの 3 か所から発火(= 9 か所のうち 3 か所)。$outcomeexecuted / not_needed(投稿のみ)
plg_cmp_block_target_createdint $target_id, int $post_id, string $object_key本文に置かれた管理ブロックの管理対象を正規の投稿保存時に作成したとき(save_post 優先度 20)。運用ルールが初期設定を後乗せするための入口

これらはプラグイン内部でも実際に使われています。たとえばリダイレクト対象のキャッシュ破棄は plg_cmp_target_completed / plg_cmp_target_review_registered / plg_cmp_expiration_executed の 3 本を購読しています。外部から確認完了に連動させたい処理(社内チャットへの追加通知、CDN パージなど)は plg_cmp_target_completed にぶら下げるのが素直です。

フィルタ(apply_filters・6 種)

いずれも件数の上限を調整するためのもので、管理画面の設定 UI はありません。表示内容や通知本文を差し替えるフィルタは提供していません。

フック名既定値用途
plg_cmp_sweep_batch_size2001 回のスイープで処理する管理対象の件数
plg_cmp_daily_check_batch_size200日次 ACF 点検で見る件数
plg_cmp_history_prune_batch_size2001 回で刈る履歴の件数
plg_cmp_bulk_apply_max_posts2000運用ルール一括適用 1 ジョブの上限投稿数
plg_cmp_bulk_apply_batch_size50一括適用の 1 バッチの投稿数
plg_cmp_bulk_apply_query_argsarray $args, array $rule一括適用の対象を引く WP_Query 引数

いずれも戻り値は max( 1, (int) … ) でクランプされます(plg_cmp_bulk_apply_query_args は配列)。同梱モジュール由来の plugin_bug_report_is_license_active / plugin_bug_report_should_display / plugin_bug_report_allow_with_heavy_plugin はバグ報告ウィジェットのフックで、更新管理機能とは無関係です。

アンインストール時の挙動

削除範囲はオプション plg_cmp_uninstall_mode(設定画面の アンインストール時のデータ削除 > 削除範囲)で決まります。uninstall.php はオートローダより前にこのオプションをリテラルで読み、「そもそも何かを消すのか」を先に判定します。既定は keep(未設定時も同じ)で、未知の値も keep に倒します。

値 / 画面表示消えるもの残るもの
keep
すべてのデータを保持既定
何も消えない(即 return)すべて
settings
プラグイン設定のみ削除
オプション plg_cmp_setting_*(13 個)と plg_cmp_connection_slack / plg_cmp_connection_chatworkカスタムテーブル 4 本(管理対象・履歴・通知ログ・運用ルール)/独自 capability / ライセンス情報 plg_cmp_license_*plg_cmp_uninstall_modeplg_cmp_db_versionplg_cmp_caps_grantedplg_cmp_installed_versionplg_cmp_sweep_cursorplg_cmp_rule_job_* / post meta _plg_cmp_* / user meta plg_cmp_*
all
管理設定と履歴をすべて削除
上に加えて: カスタムテーブル 4 本を DROP全ロールから独自 capability 5 種を remove_capplg_cmp_ で始まるオプション全部(ライセンス情報を含む)_transient_plg_cmp_*_transient_timeout_plg_cmp_* / post meta _plg_cmp_* / user meta plg_cmp_* / 最後に wp_cache_flush()投稿本文・記事データ・他プラグインのデータ(LIKE は $wpdb->esc_like() を通すのでアンダースコアも literal 一致)
  • all のときだけ uninstall.php が自前でオートローダを読み込みます(インスタンスを作るだけで init() は呼ばず、フックは張りません)。
  • cron イベントは uninstall.php では解除していません。WordPress は削除前に必ず無効化を通るため、そこで解除済みだからです。
  • 無効化(deactivate)では何も消えません。行うのはライセンス cron の解除、plg_cmp_hourly_sweep / plg_cmp_daily_sweep の解除、進行中の一括適用の予約解除(wp_unschedule_hook( 'plg_cmp_rule_bulk_apply' )。ジョブ本体のオプションは残すので再有効化すれば画面から再開・中止できる)、flush_rewrite_rules() の 4 つだけです。
  • 有効化時に行うのは、plg_cmp_installed_version の記録(既存値は上書きしない)、テーブル作成、capability の初回付与、cron 登録、flush_rewrite_rules() の 5 つです。サンプルのルールや管理対象といった同梱データの投入はありません(シーダに相当するコードが存在しません)。

配布 zip に含まれないもの / 翻訳

配布 zip は .distignore で絞り込んでいます。含まれない主なものは次のとおりです。

種別対象
ディレクトリ(任意の深さ).git / .claude / .serena / .vscode / .md / .fixtures / .harness / .not-build / node_modules
トップレベルのパス/block/src(ブロックのソース)/ /vendor / /tests / /docs / /scripts
開発用ファイル.editorconfig / .gitignore / .distignore / phpcs.xml / package.json / package-lock.json / composer.json / composer.lock / webpack 3 種 / tsconfig.json / jest.config.js / eslint 3 種 / prettier 3 種 / .nvmrc / CLAUDE.md / AGENTS.md / SKILL.md
このプラグイン固有README.md(開発者向け。配布用は readme.txt
グロブ*.DS_Store / Thumbs.db / *.log / *.map / .gitkeep

composer.json が配布 zip に入らないため、そこに書かれた "php": ">=8.3"開発環境の指定であって利用者の動作要件ではありません。利用者要件はプラグインヘッダの WordPress 6.6 以上 / PHP 8.1 以上です。ブロックのビルド成果物が react-jsx-runtime に依存するため、WordPress 6.5 以前では動きません。

含まれる主なものは plugin-base.php / uninstall.php / readme.txt / includes/(自前クラス 63 ファイル + 同梱モジュール 2 つ)/ assets/css/plg_cmp_admin.cssassets/js/plg_cmp_admin.jsassets/js/plg_cmp_editor_notices.js / block/build/content-maintenance-section/block.json / index.js / index.css / index-rtl.css / index.asset.php)です。ブロックはビルド済みで同梱されるので、利用者側で npm install && npm run build を走らせる必要はありません。

翻訳ファイルは 0 件

  • text domain は content-maintenance-plugin で、plugins_loadedload_plugin_textdomain() を呼んでいます。しかし自プラグイン用の .mo / .po / .pot はリポジトリ全体で 0 件です。languages/ には .gitkeep しか無く、その .gitkeep 自体が配布 zip から除外されるため、配布物の languages/ は実質空です。
  • 結果として UI にはソース内の日本語リテラルがそのまま表示されます。国際化の下地(text domain)はありますが、英語 UI は提供していません。
  • wp_set_script_translations() は呼んでいません。JavaScript 側の文言も日本語リテラルです。
  • 最上位メニューのラベル(menu_title)は意図的に __() に通していません(画面の見出し page_title のほうは翻訳しています)。翻訳すると screen ID が変わり、バグ報告ウィジェットの表示許可画面と一致しなくなるためです。
  • 例外として、同梱モジュール由来の includes/plugin-client/languages/ にある .pot / .po は zip に含まれますが、そちらは別の text domain を使っており、その text domain を読み込むコードは本プラグインにありません。

※ 本ページの内容は v1.0.1 時点の実装に基づきます。マルチサイトでは検証していません(ネットワーク対応コードは実装されておらず、テーブルは有効化したサイトの $wpdb->prefix でのみ作成されます)。ACF フィールド単位の管理には Advanced Custom Fields が必要ですが、ACF が無くても投稿単位・ブロック単位・カスタムフィールド単位の管理は動作します。