Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Menu items REST API] Perform validation and sanitization separately from saving #23474

Closed

Conversation

adamziel
Copy link
Contributor

@adamziel adamziel commented Jun 25, 2020

This PR removes the prepare_item_for_database method in favor of more specific validation and sanitization callbacks, with the ultimate goal being supporting batch requests. Let's discuss in comments!

Test plan:

  1. Apply First pass at batch controller wordpress-develop#311 to your local WordPress.
  2. Manually add support for sanitize_callback (see comments on the PR above).
  3. Run phpunit tests, confirm they pass.

@github-actions
Copy link

github-actions bot commented Jun 25, 2020

Size Change: +1.68 kB (0%)

Total Size: 1.13 MB

Filename Size Change
build/a11y/index.js 1.14 kB -1 B
build/annotations/index.js 3.62 kB +2 B (0%)
build/autop/index.js 2.82 kB +1 B
build/block-directory/index.js 7.39 kB +22 B (0%)
build/block-editor/index.js 109 kB -36 B (0%)
build/block-editor/style-rtl.css 10.7 kB -40 B (0%)
build/block-editor/style.css 10.7 kB -37 B (0%)
build/block-library/editor-rtl.css 7.6 kB +1 B
build/block-library/index.js 130 kB +84 B (0%)
build/block-serialization-default-parser/index.js 1.88 kB -1 B
build/blocks/index.js 48.2 kB +1 B
build/components/index.js 198 kB +1.33 kB (0%)
build/components/style-rtl.css 15.9 kB +131 B (0%)
build/components/style.css 15.9 kB +138 B (0%)
build/compose/index.js 9.64 kB +25 B (0%)
build/core-data/index.js 11.4 kB +1 B
build/data/index.js 8.44 kB +4 B (0%)
build/dom/index.js 3.19 kB +1 B
build/edit-navigation/index.js 9.87 kB -1 B
build/edit-post/index.js 303 kB -14 B (0%)
build/edit-site/index.js 16.6 kB -1 B
build/edit-site/style-rtl.css 3.03 kB +9 B (0%)
build/edit-site/style.css 3.03 kB +11 B (0%)
build/editor/index.js 44.8 kB -63 B (0%)
build/editor/style-rtl.css 3.86 kB +52 B (1%)
build/editor/style.css 3.86 kB +52 B (1%)
build/element/index.js 4.65 kB +2 B (0%)
build/format-library/index.js 7.72 kB -2 B (0%)
build/is-shallow-equal/index.js 711 B +1 B
build/keyboard-shortcuts/index.js 2.51 kB +1 B
build/list-reusable-blocks/index.js 3.12 kB -4 B (0%)
build/plugins/index.js 2.56 kB +2 B (0%)
build/priority-queue/index.js 788 B -1 B
build/redux-routine/index.js 2.85 kB +3 B (0%)
build/rich-text/index.js 14 kB +2 B (0%)
build/token-list/index.js 1.28 kB -1 B
ℹ️ View Unchanged
Filename Size Change
build/api-fetch/index.js 3.4 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/style-rtl.css 941 B 0 B
build/block-directory/style.css 942 B 0 B
build/block-library/editor.css 7.6 kB 0 B
build/block-library/style-rtl.css 8.04 kB 0 B
build/block-library/style.css 8.04 kB 0 B
build/block-library/theme-rtl.css 730 B 0 B
build/block-library/theme.css 732 B 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/data-controls/index.js 1.29 kB 0 B
build/date/index.js 5.47 kB 0 B
build/deprecated/index.js 772 B 0 B
build/dom-ready/index.js 569 B 0 B
build/edit-navigation/style-rtl.css 1.02 kB 0 B
build/edit-navigation/style.css 1.02 kB 0 B
build/edit-post/style-rtl.css 5.51 kB 0 B
build/edit-post/style.css 5.5 kB 0 B
build/edit-widgets/index.js 9.32 kB 0 B
build/edit-widgets/style-rtl.css 2.42 kB 0 B
build/edit-widgets/style.css 2.42 kB 0 B
build/editor/editor-styles-rtl.css 537 B 0 B
build/editor/editor-styles.css 539 B 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/style-rtl.css 547 B 0 B
build/format-library/style.css 548 B 0 B
build/hooks/index.js 2.13 kB 0 B
build/html-entities/index.js 622 B 0 B
build/i18n/index.js 3.56 kB 0 B
build/keycodes/index.js 1.94 kB 0 B
build/list-reusable-blocks/style-rtl.css 450 B 0 B
build/list-reusable-blocks/style.css 451 B 0 B
build/media-utils/index.js 5.29 kB 0 B
build/notices/index.js 1.79 kB 0 B
build/nux/index.js 3.4 kB 0 B
build/nux/style-rtl.css 663 B 0 B
build/nux/style.css 660 B 0 B
build/primitives/index.js 1.5 kB 0 B
build/server-side-render/index.js 2.68 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/url/index.js 4.06 kB 0 B
build/viewport/index.js 1.85 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

@adamziel
Copy link
Contributor Author

This is not ready for a review yet and needs to be rebased on top of WordPress/wordpress-develop#311 first - validating without using a controller-level validate_callback is really far from perfect.

@adamziel adamziel marked this pull request as ready for review June 29, 2020 11:56
@adamziel adamziel requested a review from spacedmonkey as a code owner June 29, 2020 11:56
@adamziel adamziel requested a review from TimothyBJacobs as a code owner June 29, 2020 11:56
@adamziel adamziel self-assigned this Jun 29, 2020
@adamziel adamziel changed the title Use JSON schema for menu items validation and sanitization (instead of prepare_item_for_db) [Menu items REST API] Perform validation and sanitization separately from saving Jun 29, 2020
@adamziel
Copy link
Contributor Author

adamziel commented Jun 29, 2020

This is now ready for discussion. I moved any field-specific validation and sanitization to the schema. This left me with some code that took both the user input and the database data and transformed it into a saveable $prepared_nav_item. This resembled a sanitization to me, so I moved it to a sanitize_callback (it would be great to add support for it to WordPress/wordpress-develop#311, I got all tests to pass by adding it locally).

Code structure wise, I don't love how it creates a synthetic sanitized request property $request['prepared_nav_item'] = $prepared_nav_item;. An alternative solution would be producing $prepared_nav_item using get_nav_menu_item and $request during the sanitization, sanitizing the request params, and then computing $prepared_nav_item for the second time in the actual handler method (if the sanitization succeeded) - I don't like this idea either. Any thoughts appreciated @TimothyBJacobs

@adamziel
Copy link
Contributor Author

adamziel commented Jun 30, 2020

I also think menu position needs to be handled differently to work with batch processing. It would neatly fit a follow-up PR with some additional test cases once the batch processing support is merged into the core.

@TimothyBJacobs
Copy link
Member

This is looking good overall, I'm going to hold off on detailed feedback for a bit while focusing on Beta 1.

Copy link
Member

@TimothyBJacobs TimothyBJacobs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for starting work on this! I've left some feedback.

Now that we have #25096, we could refactor this to be based off that branch for the moment. We will need to do a version check to make sure the user is running at least r48945 which adds support for a route-level validate_callback and if the user isn't, manually calling our validate_callback in the route callback.

What would this look like if we kept prepare_item_for_database and made the validate_callback only do validation? I understand there will be some duplicate work, but how much of an impact would that actually have?

Another thing we need to keep in mind, is that the REST API applies validation before doing permission checks. So we're currently leaking some data at the moment. For the moment, I think we can manually call our permission callbacks before doing the more sensitive validation. But I'm thinking thru what a Core solution might look like.

@@ -368,64 +478,51 @@ protected function prepare_item_for_database( $request ) {
$check = rest_validate_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this double schema validation while we're here. The request data has already been validated and sanitized according to the schema.

$title = $value['raw'];
}

$title = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $title ) ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fairly certain this should be removed: #25095.

'sanitize_callback' => static function ( $value ) {
$sanitized = sanitize_text_field( $value );
/** This filter is documented in wp-includes/post.php */
$sanitized = wp_unslash(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this, #25095.

'sanitize_callback' => static function ( $value, $request ) {
$sanitized = sanitize_text_field( $value );
/** This filter is documented in wp-includes/post.php */
$sanitized = wp_unslash(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this #25095.

@@ -813,7 +891,18 @@ public function get_item_schema() {
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
'sanitize_callback' => static function ( $value ) {
$sanitized = sanitize_text_field( $value );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this function should be called, it was the previous sanitize_callback, but AFAICT the title attr is not sanitized with this in Core.

if ( '' === $validated ) {
// Fail sanitization if URL is invalid.
return new WP_Error( 'invalid_url', __( 'Invalid URL.', 'gutenberg' ), array( 'status' => 400 ) );
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}
return true;

* @param WP_REST_Request $request Request object.
*/
return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request );
$request['prepared_nav_item'] = $prepared_nav_item;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really not a fan of us passing data thru the request object like this. What does it look like to stick with a prepare_item_for_database method that does this with already validated data.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could not agree more just put the return and filter back. This change is completely unnessary

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line seems to completely overwrite the existing request. Please remove as it breaks things.

Comment on lines +1040 to +1043
$sanitized = $value;
$sanitized = array_map( 'sanitize_html_class', wp_parse_list( $sanitized ) );

return array_map( 'sanitize_html_class', $sanitized );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need change?

@@ -745,13 +789,33 @@ public function get_item_schema() {

$schema['properties']['title'] = array(
'description' => __( 'The title for the object.', 'gutenberg' ),
'type' => 'object',
'type' => array( 'string', 'object' ),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this change?

Comment on lines +931 to +942
'sanitize_callback' => static function ( $value, $request ) {
$sanitized = sanitize_text_field( $value );
/** This filter is documented in wp-includes/post.php */
$sanitized = wp_unslash(
apply_filters(
'content_save_pre',
wp_slash( $sanitized )
)
);

return $sanitized;
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code has been repeated. Let's just make it a method so we can we use it.

Comment on lines 917 to 922
'sanitize_callback' => function ( $value ) {
return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
$sanitized = $value;
$sanitized = array_map( 'sanitize_html_class', wp_parse_list( $sanitized ) );

return array_map( 'sanitize_html_class', $sanitized );
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeated code again.

Comment on lines +986 to +988
'sanitize_callback' => static function ( $value ) {
return intval( $value );
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Comment on lines +1025 to +1027
'sanitize_callback' => static function ( $value ) {
return esc_url_raw( $value );
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this required, shouldn't format: uri cover this?


return true;
},
'sanitize_callback' => static function ( $value ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this required?

@@ -186,15 +281,25 @@ public function update_item( $request ) {
return $valid_check;
}

$prepared_nav_item = $this->prepare_item_for_database( $request );
$prepared_nav_item = $request['prepared_nav_item'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$prepared_nav_item = $request['prepared_nav_item'];
$prepared_nav_item = $this->sanitize( $request );

@@ -97,7 +181,18 @@ public function create_item( $request ) {
return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.', 'gutenberg' ), array( 'status' => 400 ) );
}

$prepared_nav_item = $this->prepare_item_for_database( $request );
$prepared_nav_item = $request['prepared_nav_item'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$prepared_nav_item = $request['prepared_nav_item'];
$prepared_nav_item = $this->sanitize( $request );

This is a bad pattern to put an object into state like this. Just run the method again.

$prepared_nav_item['menu-id'] = absint( $request[ $base ] );
}

// Nav menu title.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title no longer works without this code. Why was this removed?

@adamziel
Copy link
Contributor Author

Closing this stale PR, we may need to find another way forward.

@adamziel adamziel closed this Nov 27, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Core REST API Task Task for Core REST API efforts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants