手元の Mac で毎朝取得したデータ(各社の公式サイトの料金)を、WordPress に書き込む受け口を作りました。最初は SSH で wp eval を叩いていましたが、自動処理に秘密鍵を持たせたくないので、書き込み専用の REST API に変えました。その最小構成と、運用して入れた3つの工夫をまとめます。
最小構成
<?php
/**
* Plugin Name: Example Data Intake
* mu-plugins に置く。書き込めるのは example_prices だけ。
*/
if (!defined('ABSPATH')) exit;
add_action('rest_api_init', function () {
register_rest_route('example/v1', '/prices', [
'methods' => 'POST',
'callback' => 'example_intake',
'permission_callback' => 'example_intake_auth',
]);
});
function example_intake_auth(WP_REST_Request $req) {
$given = (string) $req->get_header('x-example-token');
// トークンは wp-config.php に define('EXAMPLE_INTAKE_TOKEN', '...');
return defined('EXAMPLE_INTAKE_TOKEN') && hash_equals(EXAMPLE_INTAKE_TOKEN, $given);
}
送る側はこれだけです。
curl -s -X POST "https://example.com/wp-json/example/v1/prices" \
-H "Content-Type: application/json" \
-H "X-Example-Token: $(cat ~/.example-intake-token)" \
--data @prices.json
permission_callback を省略したり __return_true にしたりすると、誰でも書き込めるエンドポイントになります。必ず認証を返す関数を指定します。
工夫1:比較は hash_equals
$given === TOKEN だと、文字列比較にかかる時間の差から中身を推測される余地があります。hash_equals は長さが同じなら一定時間で比較します。
工夫2:書ける場所と値を絞る
受け口で触れるのは決めた option 1つだけにします。トークンが漏れても、記事や他の設定は書き換えられません。
受け取った JSON は、キーを英数字に限り、数値以外とありえない範囲の値を捨ててから保存します。
function example_clean($v, $depth = 0) {
if ($depth > 6) return null;
if (is_array($v)) {
$o = [];
foreach ($v as $k => $x) {
$k = preg_replace('/[^A-Za-z0-9_.\-]/', '', (string) $k);
$c = example_clean($x, $depth + 1);
if ($k !== '' && $c !== null && $c !== []) $o[$k] = $c;
}
return $o;
}
if (is_numeric($v)) { $n = $v + 0; return ($n > 0 && $n < 200000) ? $n : null; }
return null;
}
工夫3:上書きではなく array_replace_recursive でマージする
一部の取得に失敗した日に、届いたデータで丸ごと上書きすると、前日の正しい値が消えます。実際にそれで、記事の「いちばん安いのは〇〇」が一時的に別の会社に変わる事故を起こしました。
function example_intake(WP_REST_Request $req) {
$in = $req->get_json_params();
if (!is_array($in)) return new WP_Error('empty', '中身が空です', ['status' => 400]);
$cur = get_option('example_prices', []);
foreach ($in as $service => $rows) {
$service = preg_replace('/[^a-z]/', '', strtolower((string) $service));
if ($service === '' || !is_array($rows)) continue;
foreach ($rows as $key => $row) {
$row = example_clean($row);
if (!$row) continue;
$row['at'] = current_time('Y-m-d'); // いつの値か
$cur[$service][$key] = array_replace_recursive($cur[$service][$key] ?? [], $row);
}
}
update_option('example_prices', $cur, false);
return ['ok' => true];
}
array_merge だと数値のキー("3" "5" などの日数)が振り直されてしまいます。array_replace_recursive なら、届いた項目だけ差し替え、欠けた項目は前回の値が残ります。取得日(at)も残るので、表示側で「〇月〇日時点」と出せます。
まとめ
-
permission_callbackで認証し、比較はhash_equals - 書けるのは決めた option だけ、値は掃除してから
-
array_replace_recursiveで前回の値とマージし、取得日を残す
取得から記事の表示(ショートコード)までの全体は、Zenn に詳しく書きました。
https://zenn.dev/blstweb/articles/27e2cf79292c19