本ページは コンテンツ更新管理(プラグインスラッグ content-maintenance-plugin・名前空間 ContentMaintenancePlugin・prefix plg_cmp_)を拡張・調査・移行するための技術情報です。記載はすべて v1.0.1 の実装に基づきます。
ブロック「コンテンツ更新管理セクション」
本プラグインが登録するブロックは 1 つだけです。ショートコードは 1 つも登録せず、カスタム投稿タイプ・カスタムタクソノミーも追加しません。
| 項目 | 値 |
|---|---|
| ブロック名 | content-maintenance-plugin/section(PHP 側の定数は Block::NAME) |
| インサーターの表示名 | コンテンツ更新管理セクション |
apiVersion / category / icon | 3 / design / clipboard(Dashicon) |
| 登録元 | ビルド成果物 block/build/content-maintenance-section/。block.json が無ければブロックを登録せず、activate_plugins を持つユーザーにだけ管理画面通知を出す |
| 資産 | editorScript / editorStyle のみ。フロント用の CSS / JS を 1 本も持たない |
attributes は managedId 1 個だけ
| 属性 | 型 / 既定値 | 内容 |
|---|---|---|
managedId | string / "" | 管理 ID。書式は blk + 16 進 12 桁(例 blk3f9a1c04b7de)。PHP 側の定数は Block::ATTR_ID |
担当者・承認者・次回確認日・更新頻度・重要度・通知・期限切れ処理といった管理設定は、ブロック属性に一切入りません。正本はカスタムテーブル plg_cmp_targets の行で、ブロックが持つのは行を引くための管理 ID だけです。したがって post_content をパースしても管理設定は取り出せませんし、逆に post_content を書き戻しても管理設定は変わりません。エディタのサイドバーの「管理設定を保存」は投稿の「更新」とは別の REST 書き込みです(次章)。
supports(4 キー)
| キー | 値 | 意味 |
|---|---|---|
html | false | 「HTML として編集」を出さない |
anchor | true | HTML アンカー(id)を付けられる |
align | [ "wide", "full" ] | 幅広・全幅の 2 段階のみ |
reusable | false | 同期パターンに変換できない |
色・タイポグラフィ・スペーシングの 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_content | 0 | 本文フィルタは 1 つも登録していない |
wp_enqueue_scripts / wp_head / wp_footer | 0 | フロントへ CSS・JS・マークアップを足さない |
render_block | 1 | 期限切れの管理ブロックだけ、出力を空文字(ブロックを非表示)または代替メッセージの <div class="plg-cmp-block-alt-message"> に差し替える |
template_redirect | 1 | 期限切れ処理「指定 URL へリダイレクト」の実行(wp_redirect()・301 / 302) |
pre_get_posts | 1 | リダイレクト中の投稿をメインクエリの検索・フィード・アーカイブ・ホームから post__not_in で外す |
render_block の分岐条件は is_admin() だけです。したがってページ表示・フィード・抜粋の生成・公開 REST の content.rendered の 4 経路には差し替えが効き、ブロックエディタのキャンバスには効きません。事前に本文を書き換えていないので、期限が来た瞬間から効き、確認完了で即座に元へ戻ります。代替メッセージは esc_html() を通すプレーンテキストで、専用の CSS は出力しません(クラス名だけ付けてテーマに任せる設計)。リダイレクト対象のマップは 300 秒の transient にキャッシュし、確認完了・確認結果登録・期限切れ処理実行・save_post で破棄します。
ブロックの期限切れ処理は 4 種(none warn hide alt_message)で、フロント出力が変わるのは hide と alt_message の 2 つだけです。表示ラベルは画面によって異なり、ブロックのサイドバー(JS)は フロントで非表示、管理画面(PHP)は ブロックを非表示 です。
REST API
名前空間は content-maintenance-plugin/v1。register_rest_route() の呼び出しは 3 本で、メソッド×パスの組み合わせは 4 エンドポイントです。認証は WordPress コア標準の cookie 認証 + nonce(wp_rest / X-WP-Nonce)で、独自のトークン発行はありません。4 エンドポイントすべてに permission_callback が設定されています(コアの rest_cookie_check_errors() が nonce を検証する前提で、プラグイン側では二重検証しません)。いずれも管理画面・ブロックエディタ向けで、フロント公開用の API ではありません。
| エンドポイント | メソッド / 権限 | 内容 |
|---|---|---|
/content-maintenance-plugin/v1/block-target | GET / plg_cmp_review かつ 対象投稿の edit_post | 管理ブロックの管理設定と選択肢の取得 |
/content-maintenance-plugin/v1/block-target | POST / plg_cmp_edit_targets かつ 対象投稿の edit_post | 管理設定の保存(投稿の「更新」とは別経路) |
/content-maintenance-plugin/v1/block-target/owned-elsewhere | GET / 対象投稿の edit_post のみ | 「その管理 ID を別の投稿が持っているか」だけを返す。独自 capability を要求しない意図的な緩和(寄稿者のブロック複製を壊さないため) |
/content-maintenance-plugin/v1/editor-notices | GET / 対象投稿の edit_post のみ | メタボックスの保存結果(検証エラー・警告・成功メッセージ)をブロックエディタへ渡す。GET だが読んだら消える one-shot |
パラメータ
| 名前 | 型 / 必須 | 対象 | 備考 |
|---|---|---|---|
post_id | integer / 必須 | 4 エンドポイント共通 | /editor-notices では absint でサニタイズ |
object_key | string / 必須 | /block-target 系 3 つ | 管理 ID。サーバ側で sanitize_key() を通す |
settings | object / POST のみ | POST /block-target | args に宣言が無く、get_param() で受けて内部の parse() が全項目を検証する |
post_id / object_key は args で必須宣言しているため、欠けたリクエストは WordPress コアが 400 で弾きます。
GET /block-target のレスポンス(6 キー)
| キー | 型 | 意味 |
|---|---|---|
exists | bool | 管理対象の行があるか |
settings | object \| null | 管理設定(行が無ければ null)。下記 22 キー |
choices | object | 選択肢一式 8 キー(assignees / approvers / escalation / units / priorities / actions / channels / connections) |
editable | bool | 保存できるか(plg_cmp_edit_targets + edit_post) |
canAssign | bool | 担当者・承認者・エスカレーション先を変更できるか(plg_cmp_manage_targets) |
ownedElsewhere | bool | その管理 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 / コード | 応答 |
|---|---|---|
| 成功 | 200 | exists / 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->prefix + plg_cmp_ + 論理名です。wp_plg_cmp_targets と決め打ちできません(サイトの DB プレフィックスに従います)。作成は dbDelta()、削除は DROP TABLE IF EXISTS(アンインストールの「すべて削除」のみ)。
| 論理名 | 役割(1 行 = 何か) | 列数 | 索引数 |
|---|---|---|---|
targets | 管理対象の本体。種類・紐づく投稿・対象キー・共通設定・運用ステータス・確認基準値・確認サイクル・期限状態キャッシュ・レコードの状態 | 39 | 8 |
history | 操作履歴(追記型・操作種別 17 種) | 14 | 4 |
notification_log | 通知ログ。重複防止キー・送信結果・試行回数・失敗理由・接続テスト履歴 | 16 | 5 |
rules | 運用ルール(適用条件と初期管理設定を JSON 3 列で持つ) | 11 | 3 |
日時カラムはすべて 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_type | VARCHAR(20) / 'post' | 種類。post / block / acf / custom_field の 4 種 |
post_id / object_key | BIGINT / VARCHAR(191) | 紐づく投稿と対象キー(投稿は空文字、ブロックは管理 ID、ACF はフィールド名、カスタムフィールドはメタキー)。191 文字超は保存前に弾く |
name | VARCHAR(191) / '' | 管理対象名。191 文字へ丸めるのは運用ルールの自動適用だけで、投稿・フィールド・ブロックの手入力経路は丸めない(超過すると保存自体が失敗しうる) |
assignee_id / approver_id / escalation_user_id | BIGINT / 0 | 担当者 / 承認者 / エスカレーション先(0 = 未設定) |
next_review_date | DATE NULL | 次回確認日 |
interval_value / interval_unit | SMALLINT / VARCHAR(10) 'none' | 更新頻度(1〜999 + 単位 5 種) |
priority | VARCHAR(10) / 'normal' | 重要度 4 段 |
workflow_status | VARCHAR(20) / 'pending' | 運用ステータス 8 種(うち完了 3 種) |
notify_channels / notify_connections / notify_before / notify_after | VARCHAR / '' | 通知チャンネル・送信先(接続 ID)・期限前日数・期限超過日数(いずれもカンマ区切り) |
notify_on_due | TINYINT / -1 | 期限当日通知。-1 = 全体設定に従う / 0 = 送らない / 1 = 送る |
escalation_days | SMALLINT / 0 | エスカレーションまでの超過日数(0 = 送らない・上限 65535) |
expiration_action / expiration_args | VARCHAR(20) 'none' / LONGTEXT | 期限切れ処理と追加引数(JSON。リダイレクト URL・代替メッセージ) |
expired_done_cycle | INT / 0 | 期限切れ処理を実行済みの確認サイクル番号(冪等性のキー) |
previous_post_status | VARCHAR(20) / '' | 下書き・非公開へ変える前の公開状態(「以前の公開状態へ戻す」で使い、復元後は空に戻る) |
baseline_value / baseline_hash / baseline_saved_at | LONGTEXT / CHAR(40) / DATETIME | 確認基準値。baseline_hash が空 = 未確認。ACF・カスタムフィールドだけ baseline_value に正規化済みの値そのものも入る |
has_changes | TINYINT(1) / 0 | 前回確認後に変更あり |
cycle | INT / 1 | 確認サイクル番号 |
deadline_state / deadline_state_at | VARCHAR(20) / DATETIME | 期限状態の検索用キャッシュ。画面の値は読み出しのたびに現在日で算出し直すので、この列を直接読むと古い可能性がある |
record_status | VARCHAR(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 本です。
actionは 17 種の許可リストで、そこに無い値は書き込み時点で捨てられます(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_typeはuser/systemの 2 値。明示しなかった場合はactor_id→get_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_at。dedup_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_init の maybe_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_action(post / 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_groups | ACF 設定 |
plg_cmp_setting_custom_field_keys / _custom_field_excluded | カスタムフィールド設定 |
期限切れ処理の既定値は 4 系統(投稿・ブロック・ACF・カスタムフィールド)すべて none = 何もしない、期限当日通知の全体既定は OFF、履歴保持期間は 0 = 無期限、アンインストールは keep = すべてのデータを保持です。つまり有効化しただけでは、公開中の投稿のステータスが変わることも、履歴が消えることも、削除でデータが消えることもありません。移行スクリプトで既定値を上書きするときは、この安全側の初期状態を意図的に外していることになります。
設定 UI を持たないオプション・transient
| キー | 内容 |
|---|---|
plg_cmp_caps_granted | capability を初回付与済みかのフラグ(次章) |
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_prefix が plg_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.php の define( 'PLG_API_URL', … ) で上書きできます。
post meta / user meta
| キー | 内容 |
|---|---|
post meta _plg_cmp_baseline_pending | ACF 連携が止まっている間に確認完了したとき「確認基準値を取り直す必要がある」印だけを立てる。停止中に基準を取ると「値なし」を基準にしてしまうため。連携が戻った最初の保存で取り直す |
user meta plg_cmp_slack_member_id | Slack メンション用のメンバー ID。保存時に英数字以外を落とす。未設定でも通知は届く(表示名だけになる) |
user meta plg_cmp_chatwork_account_id | Chatwork メンション用のアカウント ID。保存時に数字以外を落とす。同上 |
user meta の入力欄はユーザープロフィール画面の見出し コンテンツ更新管理の通知 にあります(設定画面ではありません)。編集に必要なのは edit_user だけで、独自 capability は要りません。post meta はこの 1 キーのみで、管理設定を post meta に持たせている箇所はありません(正本はすべてカスタムテーブル)。
独自 capability(5 種)とロール既定
| capability(機械名) | 用途 | administrator | editor | author | contributor | subscriber |
|---|---|---|---|---|---|---|
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_sweep | hourly | 本編のスイープ(期限状態キャッシュ更新・新サイクル開始・期限切れ処理・通知・通知の自動再送・履歴の刈り込み) |
plg_cmp_daily_sweep | daily | ACF のフィールド定義が消えていないかの点検だけ。ACF 連携が止まっていれば即 return |
- 登録は有効化フックに加えて
admin_initのたびに再試行します(wp_next_scheduled()で守られているため冪等)。日次イベントを後から追加した際に既存サイトへ追随させるための措置で、「有効化時にしか登録されない」わけではありません。 - 無効化時は 2 本とも
wp_clear_scheduled_hook()で解除します。データは消えません。 DISABLE_WP_CRONでコアの cron を止めているサイトでは、サーバーの cron からwp cron event run --due-nowを実行してください。通知と期限切れ処理は毎時の定期処理で走ります。
スイープ 1 回の処理順
- ロック取得。取れなければ即 return(
skipped = true)。 - 基準日(サイトタイムゾーンの今日)と期限接近閾値(既定 7 日)を決める。
- カーソル(前回の続き)を
plg_cmp_sweep_cursorから読み、管理対象を 200 件取得(record_status = 'active'かつnext_review_date IS NOT NULL・next_review_date ASC, id ASC順)。 - 各件の期限状態キャッシュを最新化(値が変わったときだけ DB を書く)。
- 完了 3 ステータス かつ 期限状態が
approaching/due_today/overdueの行について新サイクルを開始(cycle+1・workflow_statusをpendingへ)。 - 各件について
do_action( 'plg_cmp_sweep_target', $target, $today )を発火。priority 10 で投稿 / ブロック / フィールドの期限切れ処理が、priority 20 で通知の送信判定がぶら下がる。 - カーソルを進める(取得件数が上限に満たなければ末尾なのでオプションを削除=次回は先頭から)。
- 履歴の刈り込み(保持期間が 1 日以上のときだけ・1 回あたり最大 200 件・古い順)。
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::PREFIX は plg_cmp_)。JavaScript 側(@wordpress/hooks)のフックはありません。
アクション(do_action・7 種)
| フック名 | 引数 | いつ発火するか |
|---|---|---|
plg_cmp_sweep_target | array $target, string $today | 毎時スイープが管理対象 1 件を処理するたび。期限切れ処理(priority 10)と通知(priority 20)がここにぶら下がる |
plg_cmp_daily_check_target | array $target | 日次点検が ACF 型の管理対象 1 件を見るたび |
plg_cmp_target_review_registered | int $target_id, array $target | 確認結果を登録した直後(承認者の有無に関わらず)。確認基準値の取り直しはここにぶら下がる |
plg_cmp_target_completed | int $target_id, array $target(完了前の行) | 確認が完了したとき(承認者なし=確認結果の登録時 / 承認者あり=承認時)。確認基準値の保存と期限切れ処理の解除がここにぶら下がる |
plg_cmp_notify_event | string $event, int $target_id | ワークフロー起因の通知イベント(承認依頼・承認完了・差し戻し)。送るかどうか・どこへ送るかは通知側が決める |
plg_cmp_expiration_executed | int $target_id, array $target(実行前の行), string $outcome | 期限切れ処理を実行したとき。投稿 / ブロック / フィールドの 3 か所から発火(= 9 か所のうち 3 か所)。$outcome は executed / not_needed(投稿のみ) |
plg_cmp_block_target_created | int $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_size | 200 | 1 回のスイープで処理する管理対象の件数 |
plg_cmp_daily_check_batch_size | 200 | 日次 ACF 点検で見る件数 |
plg_cmp_history_prune_batch_size | 200 | 1 回で刈る履歴の件数 |
plg_cmp_bulk_apply_max_posts | 2000 | 運用ルール一括適用 1 ジョブの上限投稿数 |
plg_cmp_bulk_apply_batch_size | 50 | 一括適用の 1 バッチの投稿数 |
plg_cmp_bulk_apply_query_args | array $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_mode / plg_cmp_db_version / plg_cmp_caps_granted / plg_cmp_installed_version / plg_cmp_sweep_cursor / plg_cmp_rule_job_* / post meta _plg_cmp_* / user meta plg_cmp_* |
all管理設定と履歴をすべて削除 | 上に加えて: カスタムテーブル 4 本を DROP / 全ロールから独自 capability 5 種を remove_cap / plg_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.css・assets/js/plg_cmp_admin.js・assets/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_loadedでload_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 が無くても投稿単位・ブロック単位・カスタムフィールド単位の管理は動作します。