Make WordPress Core

Opened 3 years ago

Last modified 3 years ago

#56483 new defect (bug)

Weird oneOf behaviour upon validation of post meta value upon GET retrieval via REST API

Reported by: joeyojoeyo12's profile joeyojoeyo12 Owned by:
Milestone: Awaiting Review Priority: normal
Severity: major Version: 6.0.1
Component: REST API Keywords: reporter-feedback
Focuses: rest-api Cc:

Description

I've been checking for a significant amount of time with some other developers, and we can't find reasonable explanations for the behavior described here: https://wordpress.stackexchange.com/questions/409131/oneof-json-schema-validation-not-properly-working-for-custom-post-meta-value

Change History (6)

#1 @joeyojoeyo12
3 years ago

Another example, eventually illustrating some deeper rooted problems:

<?php
add_action(
        'rest_api_init',
        function () {
                
                register_post_meta(
                        post_type: 'my_custom_post',
                        meta_key:  'purchase_details',
                        args:      [
                                           'type'         => 'object',
                                           'single'       => true,
                                           'show_in_rest' => [
                                                   'schema' => [
                                                           'oneOf' => [
                                                                   [
                                                                           'title'                => 'first_possibility',
                                                                           'type'                 => 'object',
                                                                           'properties'           => [
                                                                                   'first_name' => [
                                                                                           'type'     => 'string',
                                                                                           'required' => true
                                                                                   ],
                                                                                   'age'        => [
                                                                                           'type'     => 'integer',
                                                                                           'required' => true
                                                                                   ],
                                                                                   'gender'     => [
                                                                                           'type'     => 'string',
                                                                                           'enum'     => [
                                                                                                   'm',
                                                                                                   'w',
                                                                                                   'f'
                                                                                           ],
                                                                                           'required' => true
                                                                                   ],
                                                                                   'purchase_comment'      => [
                                                                                           'type'     => 'string',
                                                                                           'required' => true
                                                                                   ],
                                                                                   'products'  => [
                                                                                           'type'        => 'array',
                                                                                           'uniqueItems' => true,
                                                                                           'items'       => [
                                                                                                   'type'    => 'string',
                                                                                                   'pattern' => '[A-z]'
                                                                                           ],
                                                                                           'minItems'    => 1,
                                                                                           'required'    => true
                                                                                   ],
                                                                                   'online_id'     => [
                                                                                           'type'     => 'string',
                                                                                           'pattern'  => '[0-9]_[A-z]{10}',
                                                                                           'required' => true
                                                                                   ],
                                                                                   'shop_location'  => [
                                                                                           'type'                 => 'object',
                                                                                           'patternProperties'    => [
                                                                                                   '[A-z]' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type'    => 'string',
                                                                                                                   'pattern' => '[A-z]'
                                                                                                           ],
                                                                                                           'minItems'    => 0
                                                                                                   ]
                                                                                           ],
                                                                                           'minProperties'        => 0,
                                                                                           'additionalProperties' => false,
                                                                                           'required'             => true
                                                                                   ],
                                                                                   'feedbacks'  => [
                                                                                           'type'                 => [
                                                                                                   'object',
                                                                                                   'null'
                                                                                           ],
                                                                                           'properties'           => [
                                                                                                   'intros' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required'    => true
                                                                                                   ],
                                                                                                   'mains' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required'    => true
                                                                                                   ],
                                                                                                   'submains' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required'    => true
                                                                                                   ],
                                                                                                   'footers' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required'    => true
                                                                                                   ],
                                                                                                   'signatures' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required'    => true
                                                                                                   ],
                                                                                                   'addresses' => [
                                                                                                           'type'     => 'array',
                                                                                                           'items'    => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required' => true
                                                                                                   ]
                                                                                           ],
                                                                                           'additionalProperties' => false,
                                                                                           'required'             => true
                                                                                   ]
                                                                           ],
                                                                           'additionalProperties' => false
                                                                   ],
                                                                   [
                                                                           'title'                => 'second_possibility',
                                                                           'type'                 => 'object',
                                                                           'properties'           => [
                                                                                   'first_name'    => [
                                                                                           'type'     => 'string',
                                                                                           'required' => true
                                                                                   ],
                                                                                   'order_name'     => [
                                                                                           'type'     => 'string',
                                                                                           'required' => true
                                                                                   ],
                                                                                   'product_name'     => [
                                                                                           'type'        => 'array',
                                                                                           'uniqueItems' => true,
                                                                                           'items'       => [
                                                                                                   'type'    => 'string',
                                                                                                   'pattern' => '[A-z]{5}'
                                                                                           ],
                                                                                           'minItems'    => 1,
                                                                                           'required'    => true
                                                                                   ],
                                                                                   'product_category'      => [
                                                                                           'type'                 => 'object',
                                                                                           'patternProperties'    => [
                                                                                                   '[A-z]{5}' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'minItems'    => 0
                                                                                                   ]
                                                                                           ],
                                                                                           'minProperties'        => 1,
                                                                                           'additionalProperties' => false,
                                                                                           'required'             => true
                                                                                   ],
                                                                                   'online_name'        => [
                                                                                           'type'     => 'string',
                                                                                           'pattern'  => '[A-z]{10}',
                                                                                           'required' => true
                                                                                   ],
                                                                                   'shop_location'  => [
                                                                                           'type'                 => 'object',
                                                                                           'patternProperties'    => [
                                                                                                   '[A-z]' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type'    => 'string',
                                                                                                                   'pattern' => '[A-z]'
                                                                                                           ],
                                                                                                           'minItems'    => 0
                                                                                                   ]
                                                                                           ],
                                                                                           'minProperties'        => 0,
                                                                                           'additionalProperties' => false,
                                                                                           'required'             => true
                                                                                   ],
                                                                                   'feedbacks'  => [
                                                                                           'type'                 => [
                                                                                                   'object',
                                                                                                   'null'
                                                                                           ],
                                                                                           'properties'           => [
                                                                                                   'intros' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required'    => true
                                                                                                   ],
                                                                                                   'mains' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required'    => true
                                                                                                   ],
                                                                                                   'submains' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required'    => true
                                                                                                   ],
                                                                                                   'footers' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required'    => true
                                                                                                   ],
                                                                                                   'signatures' => [
                                                                                                           'type'        => 'array',
                                                                                                           'uniqueItems' => true,
                                                                                                           'items'       => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required'    => true
                                                                                                   ],
                                                                                                   'addresses' => [
                                                                                                           'type'     => 'array',
                                                                                                           'items'    => [
                                                                                                                   'type' => 'string'
                                                                                                           ],
                                                                                                           'required' => true
                                                                                                   ]
                                                                                           ],
                                                                                           'additionalProperties' => false,
                                                                                           'required'             => true
                                                                                   ],
                                                                                   'profile_doc' => [
                                                                                           'type'     => 'string',
                                                                                           'pattern'  => '^[0-9]+\.(jpg|jpeg|png)$',
                                                                                           'required' => true
                                                                                   ]
                                                                           ],
                                                                           'additionalProperties' => false
                                                                   ]
                                                           ]
                                                   ]
                                           ]
                                   ]
                );
  
                register_post_meta(
                        post_type: 'my_custom_post',
                        meta_key:  'product_category_id',
                        args:      [
                                           'type'          => 'integer',
                                           'single'        => true,
                                           'auth_callback' => function (
                                                   $allowed,
                                                   $meta_key,
                                                   $object_id,
                                                   $user_id,
                                                   $cap,
                                                   $caps
                                           ) {
                                                   return current_user_can( 'edit_dashboard' );
                                           },
                                           'show_in_rest'  => [
                                                   'schema' => [
                                                           'type'    => 'integer',
                                                           'minimum' => 1
                                                   ]
                                           ]
                                   ]
                );
  
                register_post_meta(
                        post_type: 'my_custom_post',
                        meta_key:  'product_shop_id',
                        args:      [
                                           'type'          => 'integer',
                                           'single'        => true,
                                           'auth_callback' => function (
                                                   $allowed,
                                                   $meta_key,
                                                   $object_id,
                                                   $user_id,
                                                   $cap,
                                                   $caps
                                           ) {
                                                   return current_user_can( 'edit_dashboard' );
                                           },
                                           'show_in_rest'  => [
                                                   'schema' => [
                                                           'type'    => 'integer',
                                                           'minimum' => 0
                                                   ]
                                           ]
                                   ]
                );
                
        }
);

With this setting, try to add a new

purchase_details

meta value via

add_post_meta

, and then to try to retrieve it via

REST

without its value being

null

.

Version 0, edited 3 years ago by joeyojoeyo12 (next)

#2 @anna.bansaghi
3 years ago

Hi, I was able to reproduce the response having null with your workflow, but do not know if the cause is the same as in your case. I have misspelled a value which was defined with pattern in the input meta. Would you mind sharing your input meta here?

So in my case I had inserted an invalid meta with add_post_meta(), and wanted the fetch it with REST API. But REST API validates the value against the schema, and because it was invalid, so the response was null.

One might argue that a detailed error response would be more useful, however a better argue would be to use REST API for all operations (create, read, update, delete), so all database operations will be validated against the schema.

So my next try was using REST API instead of add_post_meta(), and the response immediately was an error message with detailed information about the misspelled value. It was not possible to create an invalid meta at the first place!

As I said before, it is possible that your issue is caused by something else! It would be nice :

  • to see your input meta here, so we can rule out the invalid meta issue, and
  • if you could try to use REST API for creating the meta, and see what would be the response to that.
Last edited 3 years ago by anna.bansaghi (previous) (diff)

#3 @anna.bansaghi
3 years ago

  • Keywords reporter-feedback added; needs-patch removed

#4 @TimothyBlynJacobs
3 years ago

I'm sorry you're experiencing issues @joeyojoeyo12. Could you try and simplify your replication example to the bare minimum required to trigger the error? It is a bit difficult to follow such a large example.

To clarify a couple of things. Yes, when using oneOf, the REST API ensures that exactly one schema matches the input value. If more than one schema matches successfully, then the validation will fail. If this isn't the behavior you are looking for, try using the anyOf keyword. This indicates that you want to match any of the provided schemas.

Can you clarify where you saw oneOf overwritten to anyOf? As far as I known, we never assign a value of anyOf to a schema.

Omitting type when setting your schema in show_in_rest won't actually remove the type definition because WP_REST_Meta_Fields::get_registered_fields defaults the type keyword to the top-level type you set in register_meta.

While not required, specifying a type in addition to a oneOf or anyOf keyword is supported. However, that type must then be valid for all schemas used in oneOf or anyOf.

Validation is supposed to continue after the oneOf or anyOf keywords are processed successfully. So if you are registering a meta key where we default additionalProperties to be false, then it would be expected behavior that your schema validation fails. I believe you should be able to work around this by also setting additionalProperties to true at the same level that you set oneOf.

#5 @joeyojoeyo12
3 years ago

Hey there; apologies on my late reply and thanks for your help; I've added a simpler example above that highlights the issues I'm having.

#6 @TimothyBlynJacobs
3 years ago

In the future @joeyojoeyo12, it'd be best to add the details in the follow up comment instead of editing the first one. It makes things easier to track that way.

A) The consequence of this is that I cannot block incoming requests that have more data in the payload than needed, correct?

You can using additionalProperties.

B) So I figured you have to specify it at both levels; can you confirm that?

You can omit it, but you have to then specify your schema callbacks manually, see my next reply.

C) do I still have to provide it explicitly to every argument?

It depends on how you register your route. If you use WP_REST_Controller::get_item_schema combined with WP_REST_Controller::get_endpoint_args_for_item_schema or simply rest_get_endpoint_args_for_schema, the args definition for your route will have the default JSON Schema validation and sanitization callbacks automatically applied.

Otherwise, WP_REST_Request will automatically apply the schema sanitization via rest_parse_request_arg but only if the arg has a specified type.

D) can you in WP JSON Schema maybe directly tell the args that the payload is a simply object; basically under no key?

No. The REST API doesn't support this behavior. A lot of the APIs are formulated around request objects being a dictionary with parameter keys that each have a set of validation and sanitization bits attached to it. This may be supported officially in the future, but it would take some doing.

In the mean time, you could set a validate_callback when you register your REST API route alongside callback and permission_callback. It is passed the entire WP_REST_Request object. Then you can call the sanitize and validate callbacks directly.

Here is a corrected schema.

[
	'data' => [
		'sanitize_callback' => 'rest_sanitize_request_arg',
		'validate_callback' => 'rest_validate_request_arg',
		'required'          => true,
		'oneOf'             => [
			[
				'title'                => 'only_banana',
				'type'                 => 'object',
				'additionalProperties' => false,
				'properties'           => $banana_attributes
			],
			[
				'title'                => 'banana_and_apple',
				'type'                 => 'object',
				'additionalProperties' => false,
				'properties'           => array_merge(
					$banana_attributes,
					$apple_attributes
				)
			],
			[
				'title'                => 'banana_and_apple_and_pear',
				'type'                 => 'object',
				'additionalProperties' => false,
				'properties'           => array_merge(
					$banana_attributes,
					$apple_attributes,
					$pear_attributes
				)
			]
		]
	]
]
Note: See TracTickets for help on using tickets.