0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WordPressで外部からデータを安全に受け取るREST APIの最小構成(hash_equals・書ける場所の限定・array_replace_recursive)

0
Posted at

手元の 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

実際の表示例:https://www.blstweb.jp/network/esim/korea-esim/

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?