今回は投稿のREST APIに疑問があったので調べてみました。
例えば投稿のtitle
はスキーマ定義ではtitle.raw
およびtitle.rendered
が定義してあります。
以下はREST APIからスキーマの一部を表示したものです。
以下はtitle
のスキーマを定義しているWP_REST_Posts_Controller#get_item_schema()からの抜粋です。
case 'title':
$schema['properties']['title'] = array(
'description' => __( 'The title for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Title for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML title for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
title.rendered
は読み込み専用です。
なのでtitle.raw
だけが使えるはずです。
ところが以下のコードでも投稿できてしまいます。
wp.apiFetch({ path: '/wp/v2/posts', method: 'POST', data: { title: 'Hello Title', status: 'publish' }})
では新規投稿のルート定義を見ます(WP_REST_Posts_Controller#register_route()から抜粋です)。
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => $this->get_collection_params(),
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
),
'allow_batch' => $this->allow_batch,
'schema' => array( $this, 'get_public_item_schema' ),
)
);
get_endpoint_args_for_item_schema()
が重要です。
このメソッドは内部でrest_get_endpoint_args_for_schema()
を呼び出しますが、
以下を見るとvalidate_callback
およびsanitize_callback
が初期値として定義されています。
function rest_get_endpoint_args_for_schema( $schema, $method = WP_REST_Server::CREATABLE ) {
$schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
$endpoint_args = array();
$valid_schema_properties = rest_get_allowed_schema_keywords();
$valid_schema_properties = array_diff( $valid_schema_properties, array( 'default', 'required' ) );
foreach ( $schema_properties as $field_id => $params ) {
// Arguments specified as `readonly` are not allowed to be set.
if ( ! empty( $params['readonly'] ) ) {
continue;
}
//
// ほらほらほら!
//
// ↓ ここを見てみてみて! すべてのargsに二つのコールバックが自動的に追加されるよ!
//
$endpoint_args[ $field_id ] = array(
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'rest_sanitize_request_arg',
);
if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
$endpoint_args[ $field_id ]['default'] = $params['default'];
}
if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
$endpoint_args[ $field_id ]['required'] = true;
}
foreach ( $valid_schema_properties as $schema_prop ) {
if ( isset( $params[ $schema_prop ] ) ) {
$endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
}
}
// Merge in any options provided by the schema property.
if ( isset( $params['arg_options'] ) ) {
// Only use required / default from arg_options on CREATABLE endpoints.
if ( WP_REST_Server::CREATABLE !== $method ) {
$params['arg_options'] = array_diff_key(
$params['arg_options'],
array(
'required' => '',
'default' => '',
)
);
}
//
//
// でもね、でもね、args_optionsでマージされるんだねー!
//
$endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
}
}
return $endpoint_args;
}
ところが、これらのコールバックを上書きする箇所があります。
$endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
arg_options
の部分は上書きされるんですね。
もう一度title
のスキーマ定義を見てみます。
$schema['properties']['title'] = array(
'description' => __( 'The title for the post.' ),
'type' => 'object',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
),
'properties' => array(
'raw' => array(
'description' => __( 'Title for the post, as it exists in the database.' ),
'type' => 'string',
'context' => array( 'edit' ),
),
'rendered' => array(
'description' => __( 'HTML title for the post, transformed for display.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);
はい、arg_options
があればその値は上書きされます。
validate_callback
およびsanitize_callback
はともにnull
になってしまいます。
実際にtitle
の定義を見てみましょう。
試しにformat
を見るとデフォルトの値が設定されていることもわかります。
次に、
このスキーマが実際に使用されるのはどのタイミングを知る必要があります。
REST APIが実行されるタイミングで真っ先に見るメソッドがWP_REST_Server#dispatch()
です。
このメソッドはリクエストをバリデートおよびサニタイズした後、RESTコントローラのアクションメソッドを実行するようになってます。
バリデートして、サニタイズしますが、もし失敗したらエラーになります。
成功したらコントローラのアクションを実行します。
大まかには以下のようになってます。
WP_REST_Server
// REST APIを叩くと実行されるメソッド
dispatch( request )
WP_REST_Request#has_valid_params()
WP_REST_Request#sanitize_params()
this#render_to_request(request, ...)
// RESTコントローラのアクションを実行(register_route()で設定したルートを実行)
respond_to_request( request, ... )
WP_REST_Posts_Controller#create_item()
WP_REST_Request
// バリデートする
has_valid_params()
// サニタイズする
sanitize_params()
以下はWP_REST_Server#dispatch()
を省略したものです。
public function dispatch( $request ) {
if ( ! is_wp_error( $error ) ) {
$check_required = $request->has_valid_params();
if ( is_wp_error( $check_required ) ) {
$error = $check_required;
} else {
$check_sanitized = $request->sanitize_params();
if ( is_wp_error( $check_sanitized ) ) {
$error = $check_sanitized;
}
}
}
$response = $this->respond_to_request( $request, $route, $handler, $error );
array_pop( $this->dispatching_requests );
return $response;
}
バリデートするのが以下の部分です。
$request->has_valid_params();
このメソッドはWP_REST_Request#has_valid_params()
を実行してます。
実はここでargs
を取り出してバリデーションを行っているのですが、
validate_callback
のコールバックにargs
を渡してバリデートしているのです。
つまりこのコールバックがrest_validate_request_args
であれば期待通りの動きをするのですが、
コールバックが先ほどのtitle
のようにnull
だとargs
によるバリデーションはされないんです。
独自のバリデーションをしたければデフォルトのコールバックでもnullでもなく独自のコールバックを設定することもできます。
今回のことからtitle
はバリデーションされません。
RESTコントローラを見てみます。
なんとリクエストの情報をそのまま取得してます。
WP_REST_Posts_Controller#prepare_item_for_database()
if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
if ( is_string( $request['title'] ) ) {
$prepared_post->post_title = $request['title'];
} elseif ( ! empty( $request['title']['raw'] ) ) {
$prepared_post->post_title = $request['title']['raw'];
}
}
見た感じ、title
が文字列型かチェック、違えばtitle.raw
を無条件に取得してます。
title.raw
に数値やオブジェクトを渡してもそのまま有効になってしまいます。
取得した情報はwp_insert_post()
によってDBに追加されます、
スキーマによるバリデーションはされてませんが、wp_indert_post()は内部で最低限のサニタイズはしてます。
以下の実行例ではtitle.raw
の値は123
という数値として渡しますが、
wp.apiFetch({ path: '/wp/v2/posts', method: 'POST', data: { title: { raw: 123 }, status: 'publish' }})
DBに追加するころにはwp_insert_post()
が内部的に以下のサニタイズをするため文字列に変換されるようになってます。
$postarr = sanitize_post( $postarr, 'db' );
というように、コールバックをnullにすることでバリデーション回避ができるんですね。
ところが以下のコードを実行するとエラーが発生します。
wp.apiFetch({ path: '/wp/v2/posts', method: 'POST', data: { title: { raw: { msg: 'Hello' } }, status: 'publish' }})
title.raw
にオブジェクトを渡すと、trim()
関数にそのオブジェクトを渡してエラーになってます。
この辺もうちょっとスマートに設計できなかったんやろか・・・。