From 7b8e4451f4d28102585bdf9573ce0fb193a917f9 Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Tue, 17 Sep 2024 21:50:38 +0000 Subject: [PATCH 1/7] REST API: Automatically populate targetHints for the Allow header. The REST API uses the "Allow" header to communicate what methods a user is authorized to perform on a resource. This works great when operating on a single item route, but can break down when needing to determine authorization over a collection of items. This commit uses the "targetHints" property of JSON Hyper Schema to provide access to the "allow" header for "self" links. This alleviates needing to make a separate network request for each item in a collection. Props mamaduka, noisysocks, peterwilsoncc, spacedmonkey, swissspidy, timothyblynjacobs, tyxla, youknowriad. Fixes #61739. git-svn-id: https://develop.svn.wordpress.org/trunk@59032 602fd350-edb4-49c9-b593-d223f7449a82 --- .../rest-api/class-wp-rest-server.php | 64 +++++++- tests/phpunit/tests/rest-api/rest-server.php | 155 +++++++++++++++++- tests/qunit/fixtures/wp-api-generated.js | 87 +++++++++- 3 files changed, 295 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 0eedb0396bcc4..17d620f6fb6e0 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -636,13 +636,75 @@ public static function get_response_links( $response ) { foreach ( $items as $item ) { $attributes = $item['attributes']; $attributes['href'] = $item['href']; - $data[ $rel ][] = $attributes; + + if ( 'self' !== $rel ) { + $data[ $rel ][] = $attributes; + continue; + } + + $target_hints = self::get_target_hints_for_link( $attributes ); + if ( $target_hints ) { + $attributes['targetHints'] = $target_hints; + } + + $data[ $rel ][] = $attributes; } } return $data; } + /** + * Gets the target links for a REST API Link. + * + * @since 6.7.0 + * + * @param array $link + * + * @return array|null + */ + protected static function get_target_hints_for_link( $link ) { + // Prefer targetHints that were specifically designated by the developer. + if ( isset( $link['targetHints']['allow'] ) ) { + return null; + } + + $request = WP_REST_Request::from_url( $link['href'] ); + if ( ! $request ) { + return null; + } + + $server = rest_get_server(); + $match = $server->match_request_to_handler( $request ); + + if ( is_wp_error( $match ) ) { + return null; + } + + if ( is_wp_error( $request->has_valid_params() ) ) { + return null; + } + + if ( is_wp_error( $request->sanitize_params() ) ) { + return null; + } + + $target_hints = array(); + + $response = new WP_REST_Response(); + $response->set_matched_route( $match[0] ); + $response->set_matched_handler( $match[1] ); + $headers = rest_send_allow_header( $response, $server, $request )->get_headers(); + + foreach ( $headers as $name => $value ) { + $name = WP_REST_Request::canonicalize_header_name( $name ); + + $target_hints[ $name ] = array_map( 'trim', explode( ',', $value ) ); + } + + return $target_hints; + } + /** * Retrieves the CURIEs (compact URIs) used for relations. * diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 7bcc6d68e7497..378b51f606cc9 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -9,6 +9,8 @@ */ class Tests_REST_Server extends WP_Test_REST_TestCase { protected static $icon_id; + protected static $admin_id; + protected static $post_id; /** * Called before setting up all tests. @@ -21,12 +23,20 @@ public static function set_up_before_class() { } public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { - $filename = DIR_TESTDATA . '/images/test-image-large.jpg'; - self::$icon_id = $factory->attachment->create_upload_object( $filename ); + $filename = DIR_TESTDATA . '/images/test-image-large.jpg'; + self::$icon_id = $factory->attachment->create_upload_object( $filename ); + self::$admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + self::$post_id = $factory->post->create(); } public static function tear_down_after_class() { wp_delete_attachment( self::$icon_id, true ); + self::delete_user( self::$admin_id ); + wp_delete_post( self::$post_id ); parent::tear_down_after_class(); } @@ -2431,6 +2441,147 @@ public function test_rest_allowed_cors_headers_filter_receives_request_object() $this->assertSame( '/test-allowed-cors-headers', $mock_hook->get_events()[0]['args'][1]->get_route() ); } + /** + * @ticket 61739 + */ + public function test_validates_request_when_building_target_hints() { + register_rest_route( + 'test-ns/v1', + '/test/(?P\d+)', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => static function () { + return new \WP_REST_Response(); + }, + 'permission_callback' => '__return_true', + 'args' => array( + 'id' => array( + 'type' => 'integer', + ), + ), + ), + ) + ); + + $response = new WP_REST_Response(); + $response->add_link( 'self', rest_url( 'test-ns/v1/test/garbage' ) ); + + $links = rest_get_server()::get_response_links( $response ); + + $this->assertArrayHasKey( 'self', $links ); + $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] ); + } + + /** + * @ticket 61739 + */ + public function test_sanitizes_request_when_building_target_hints() { + $validated_param = null; + register_rest_route( + 'test-ns/v1', + '/test/(?P\d+)', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => static function () { + return new \WP_REST_Response(); + }, + 'permission_callback' => function ( WP_REST_Request $request ) use ( &$validated_param ) { + $validated_param = $request['id']; + + return true; + }, + 'args' => array( + 'id' => array( + 'type' => 'integer', + ), + ), + ), + ) + ); + + $response = new WP_REST_Response(); + $response->add_link( 'self', rest_url( 'test-ns/v1/test/5' ) ); + + $links = rest_get_server()::get_response_links( $response ); + + $this->assertArrayHasKey( 'self', $links ); + $this->assertArrayHasKey( 'targetHints', $links['self'][0] ); + $this->assertIsInt( $validated_param ); + } + + /** + * @ticket 61739 + */ + public function test_populates_target_hints_for_administrator() { + wp_set_current_user( self::$admin_id ); + $response = rest_do_request( '/wp/v2/posts' ); + $post = $response->get_data()[0]; + + $link = $post['_links']['self'][0]; + $this->assertArrayHasKey( 'targetHints', $link ); + $this->assertArrayHasKey( 'allow', $link['targetHints'] ); + $this->assertSame( array( 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ), $link['targetHints']['allow'] ); + } + + /** + * @ticket 61739 + */ + public function test_populates_target_hints_for_logged_out_user() { + $response = rest_do_request( '/wp/v2/posts' ); + $post = $response->get_data()[0]; + + $link = $post['_links']['self'][0]; + $this->assertArrayHasKey( 'targetHints', $link ); + $this->assertArrayHasKey( 'allow', $link['targetHints'] ); + $this->assertSame( array( 'GET' ), $link['targetHints']['allow'] ); + } + + /** + * @ticket 61739 + */ + public function test_does_not_error_on_invalid_urls() { + $response = new WP_REST_Response(); + $response->add_link( 'self', 'this is not a real URL' ); + + $links = rest_get_server()::get_response_links( $response ); + $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] ); + } + + /** + * @ticket 61739 + */ + public function test_does_not_error_on_bad_rest_api_routes() { + $response = new WP_REST_Response(); + $response->add_link( 'self', rest_url( '/this/is/not/a/real/route' ) ); + + $links = rest_get_server()::get_response_links( $response ); + $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] ); + } + + /** + * @ticket 61739 + */ + public function test_prefers_developer_defined_target_hints() { + $response = new WP_REST_Response(); + $response->add_link( + 'self', + '/wp/v2/posts/' . self::$post_id, + array( + 'targetHints' => array( + 'allow' => array( 'GET', 'PUT' ), + ), + ) + ); + + $links = rest_get_server()::get_response_links( $response ); + $link = $links['self'][0]; + $this->assertArrayHasKey( 'targetHints', $link ); + $this->assertArrayHasKey( 'allow', $link['targetHints'] ); + $this->assertSame( array( 'GET', 'PUT' ), $link['targetHints']['allow'] ); + } + public function _validate_as_integer_123( $value, $request, $key ) { if ( ! is_int( $value ) ) { return new WP_Error( 'some-error', 'This is not valid!' ); diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 7e97f19b18588..b3917a35eee93 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -12334,7 +12334,16 @@ mockedApiResponse.PostsCollection = [ "_links": { "self": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/posts/4" + "href": "http://example.org/index.php?rest_route=/wp/v2/posts/4", + "targetHints": { + "allow": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ] + } } ], "collection": [ @@ -12641,7 +12650,16 @@ mockedApiResponse.PagesCollection = [ "_links": { "self": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/pages/7" + "href": "http://example.org/index.php?rest_route=/wp/v2/pages/7", + "targetHints": { + "allow": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ] + } } ], "collection": [ @@ -12932,7 +12950,16 @@ mockedApiResponse.MediaCollection = [ "_links": { "self": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/media/10" + "href": "http://example.org/index.php?rest_route=/wp/v2/media/10", + "targetHints": { + "allow": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ] + } } ], "collection": [ @@ -13629,7 +13656,15 @@ mockedApiResponse.CategoriesCollection = [ "_links": { "self": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/categories/1" + "href": "http://example.org/index.php?rest_route=/wp/v2/categories/1", + "targetHints": { + "allow": [ + "GET", + "POST", + "PUT", + "PATCH" + ] + } } ], "collection": [ @@ -13694,7 +13729,16 @@ mockedApiResponse.TagsCollection = [ "_links": { "self": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/tags/2" + "href": "http://example.org/index.php?rest_route=/wp/v2/tags/2", + "targetHints": { + "allow": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ] + } } ], "collection": [ @@ -13758,7 +13802,16 @@ mockedApiResponse.UsersCollection = [ "_links": { "self": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/users/1" + "href": "http://example.org/index.php?rest_route=/wp/v2/users/1", + "targetHints": { + "allow": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ] + } } ], "collection": [ @@ -13786,7 +13839,16 @@ mockedApiResponse.UsersCollection = [ "_links": { "self": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/users/2" + "href": "http://example.org/index.php?rest_route=/wp/v2/users/2", + "targetHints": { + "allow": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ] + } } ], "collection": [ @@ -13859,7 +13921,16 @@ mockedApiResponse.CommentsCollection = [ "_links": { "self": [ { - "href": "http://example.org/index.php?rest_route=/wp/v2/comments/2" + "href": "http://example.org/index.php?rest_route=/wp/v2/comments/2", + "targetHints": { + "allow": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ] + } } ], "collection": [ From af0437b080a1c512816883728bde9a75f70081bf Mon Sep 17 00:00:00 2001 From: David Baumwald Date: Tue, 17 Sep 2024 21:52:54 +0000 Subject: [PATCH 2/7] Script Loader: Remove unused array_merge. This change removes an unused `array_merge` that was added in [44265]. Props kkmuffme, SergeyBiryukov, akshat2802. Fixes #61754. git-svn-id: https://develop.svn.wordpress.org/trunk@59033 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/script-loader.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index c9eb40d1c5b50..9adfb379316dd 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -609,10 +609,6 @@ function wp_tinymce_inline_scripts() { $tinymce_settings['wpeditimage_disable_captions'] = true; } - if ( ! empty( $editor_settings['tinymce'] ) && is_array( $editor_settings['tinymce'] ) ) { - array_merge( $tinymce_settings, $editor_settings['tinymce'] ); - } - /** This filter is documented in wp-includes/class-wp-editor.php */ $tinymce_settings = apply_filters( 'tiny_mce_before_init', $tinymce_settings, 'classic-block' ); From a9d76fab5641acdd3724a25989979cc51f22b953 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Tue, 17 Sep 2024 21:56:18 +0000 Subject: [PATCH 3/7] REST API: Support exact search in the REST API posts endpoint. This changeset adds support for a new `search_semantics` enum query parameter that can be passed alongside the `search` string parameter. At this point, it only supports "exact" as possible value, but an enum is used for forward compatibility with potential enhancements like "sentence" search support. If `search_semantics=exact` is passed, it will look for an exact match rather than do a full text search, which for some use-cases is more appropriate and more performant. Props mehulkaklotar, timothyblynjacobs, jimmyh61, ironprogrammer, johnregan3, mukesh27, costdev. Fixes #56350. git-svn-id: https://develop.svn.wordpress.org/trunk@59034 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-rest-posts-controller.php | 13 ++++ .../rest-api/rest-attachments-controller.php | 1 + .../tests/rest-api/rest-pages-controller.php | 1 + .../tests/rest-api/rest-posts-controller.php | 59 +++++++++++++++++ tests/qunit/fixtures/wp-api-generated.js | 64 +++++++++++++++++++ 5 files changed, 138 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index d6afdef470af2..11bc499fc6720 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -337,6 +337,13 @@ public function get_items( $request ) { } } + if ( + isset( $registered['search_semantics'], $request['search_semantics'] ) + && 'exact' === $request['search_semantics'] + ) { + $args['exact'] = true; + } + $args = $this->prepare_tax_query( $args, $request ); // Force the post_type argument, since it's not a user input variable. @@ -2886,6 +2893,12 @@ public function get_collection_params() { ); } + $query_params['search_semantics'] = array( + 'description' => __( 'How to interpret the search input.' ), + 'type' => 'string', + 'enum' => array( 'exact' ), + ); + $query_params['offset'] = array( 'description' => __( 'Offset the result set by a specific number of items.' ), 'type' => 'integer', diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 737c32b8e1ab1..55ea686d22f25 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -229,6 +229,7 @@ public function test_registered_query_params() { 'per_page', 'search', 'search_columns', + 'search_semantics', 'slug', 'status', ), diff --git a/tests/phpunit/tests/rest-api/rest-pages-controller.php b/tests/phpunit/tests/rest-api/rest-pages-controller.php index 209229256a11f..9717a7fcda1c6 100644 --- a/tests/phpunit/tests/rest-api/rest-pages-controller.php +++ b/tests/phpunit/tests/rest-api/rest-pages-controller.php @@ -85,6 +85,7 @@ public function test_registered_query_params() { 'per_page', 'search', 'search_columns', + 'search_semantics', 'slug', 'status', ), diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index 8b81b9f648651..3085d066fc6ae 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -206,6 +206,7 @@ public function test_registered_query_params() { 'per_page', 'search', 'search_columns', + 'search_semantics', 'slug', 'status', 'sticky', @@ -765,6 +766,64 @@ public function test_get_items_status_without_permissions() { } } + /** + * @ticket 56350 + * + * @dataProvider data_get_items_exact_search + * + * @param string $search_term The search term. + * @param bool $exact_search Whether the search is an exact or general search. + * @param int $expected The expected number of matching posts. + */ + public function test_get_items_exact_search( $search_term, $exact_search, $expected ) { + self::factory()->post->create( + array( + 'post_title' => 'Rye', + 'post_content' => 'This is a post about Rye Bread', + ) + ); + + self::factory()->post->create( + array( + 'post_title' => 'Types of Bread', + 'post_content' => 'Types of bread are White and Rye Bread', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request['search'] = $search_term; + if ( $exact_search ) { + $request['search_semantics'] = 'exact'; + } + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( $expected, $response->get_data() ); + } + + /** + * Data provider for test_get_items_exact_search(). + * + * @return array[] + */ + public function data_get_items_exact_search() { + return array( + 'general search, one exact match and one partial match' => array( + 'search_term' => 'Rye', + 'exact_search' => false, + 'expected' => 2, + ), + 'exact search, one exact match and one partial match' => array( + 'search_term' => 'Rye', + 'exact_search' => true, + 'expected' => 1, + ), + 'exact search, no match and one partial match' => array( + 'search_term' => 'Rye Bread', + 'exact_search' => true, + 'expected' => 0, + ), + ); + } + public function test_get_items_order_and_orderby() { self::factory()->post->create( array( diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index b3917a35eee93..d0d0a24a73e6e 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -362,6 +362,14 @@ mockedApiResponse.Schema = { "default": [], "required": false }, + "search_semantics": { + "description": "How to interpret the search input.", + "type": "string", + "enum": [ + "exact" + ], + "required": false + }, "offset": { "description": "Offset the result set by a specific number of items.", "type": "integer", @@ -1719,6 +1727,14 @@ mockedApiResponse.Schema = { "type": "integer", "required": false }, + "search_semantics": { + "description": "How to interpret the search input.", + "type": "string", + "enum": [ + "exact" + ], + "required": false + }, "offset": { "description": "Offset the result set by a specific number of items.", "type": "integer", @@ -2820,6 +2836,14 @@ mockedApiResponse.Schema = { "default": [], "required": false }, + "search_semantics": { + "description": "How to interpret the search input.", + "type": "string", + "enum": [ + "exact" + ], + "required": false + }, "offset": { "description": "Offset the result set by a specific number of items.", "type": "integer", @@ -3571,6 +3595,14 @@ mockedApiResponse.Schema = { "default": [], "required": false }, + "search_semantics": { + "description": "How to interpret the search input.", + "type": "string", + "enum": [ + "exact" + ], + "required": false + }, "offset": { "description": "Offset the result set by a specific number of items.", "type": "integer", @@ -4382,6 +4414,14 @@ mockedApiResponse.Schema = { "default": [], "required": false }, + "search_semantics": { + "description": "How to interpret the search input.", + "type": "string", + "enum": [ + "exact" + ], + "required": false + }, "offset": { "description": "Offset the result set by a specific number of items.", "type": "integer", @@ -6995,6 +7035,14 @@ mockedApiResponse.Schema = { "default": [], "required": false }, + "search_semantics": { + "description": "How to interpret the search input.", + "type": "string", + "enum": [ + "exact" + ], + "required": false + }, "offset": { "description": "Offset the result set by a specific number of items.", "type": "integer", @@ -7812,6 +7860,14 @@ mockedApiResponse.Schema = { "default": [], "required": false }, + "search_semantics": { + "description": "How to interpret the search input.", + "type": "string", + "enum": [ + "exact" + ], + "required": false + }, "offset": { "description": "Offset the result set by a specific number of items.", "type": "integer", @@ -8017,6 +8073,14 @@ mockedApiResponse.Schema = { "default": [], "required": false }, + "search_semantics": { + "description": "How to interpret the search input.", + "type": "string", + "enum": [ + "exact" + ], + "required": false + }, "offset": { "description": "Offset the result set by a specific number of items.", "type": "integer", From d3d02c44ebdddc11b5c53b6fc6938e801b3d41f8 Mon Sep 17 00:00:00 2001 From: Anthony Burchell Date: Tue, 17 Sep 2024 21:56:43 +0000 Subject: [PATCH 4/7] Media: Add Ctrl/Command + Enter shortcut to insert selected Media Library items. Adds a Ctrl/Command + Enter keyboard shortcut to insert the currently selected single media or multiple media items when selecting in the Media Library modal. Props poena, hirschferkel, antpb, joedolson, skobe, rcreators, plaidharper. Fixes #60369. git-svn-id: https://develop.svn.wordpress.org/trunk@59035 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/media/views/attachment.js | 5 +++++ src/js/media/views/modal.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/js/media/views/attachment.js b/src/js/media/views/attachment.js index 6f9f7f5001640..7359570b3fb2f 100644 --- a/src/js/media/views/attachment.js +++ b/src/js/media/views/attachment.js @@ -199,6 +199,11 @@ Attachment = View.extend(/** @lends wp.media.view.Attachment.prototype */{ method = 'toggle'; } + // Avoid toggles when the command or control key is pressed with the enter key to prevent deselecting the last selected attachment. + if ( ( event.metaKey || event.ctrlKey ) && ( 13 === event.keyCode || 10 === event.keyCode ) ) { + return; + } + this.toggleSelection({ method: method }); diff --git a/src/js/media/views/modal.js b/src/js/media/views/modal.js index 8ef4fbb6e1eac..e3d9ad1701c86 100644 --- a/src/js/media/views/modal.js +++ b/src/js/media/views/modal.js @@ -180,6 +180,29 @@ Modal = wp.media.View.extend(/** @lends wp.media.view.Modal.prototype */{ this.escape(); }, + /** + * Handles the selection of attachments when the command or control key is pressed with the enter key. + * + * @since 6.7 + * + * @param {Object} event The keydown event object. + */ + selectHandler: function( event ) { + var selection = this.controller.state().get( 'selection' ); + + if ( ! selection.length > 0 ) { + return; + } + + if ( 'insert' === this.controller.options.state ) { + this.controller.trigger( 'insert', selection ); + } else { + this.controller.trigger( 'select', selection ); + event.preventDefault(); + this.escape(); + } + }, + /** * @param {Array|Object} content Views to register to '.media-modal-content' * @return {wp.media.view.Modal} Returns itself to allow chaining. @@ -214,6 +237,13 @@ Modal = wp.media.View.extend(/** @lends wp.media.view.Modal.prototype */{ this.escape(); event.stopImmediatePropagation(); } + + // Select the attachment when command or control and enter are pressed. + if ( ( 13 === event.which || 10 === event.which ) && ( event.metaKey || event.ctrlKey ) ) { + this.selectHandler( event ); + event.stopImmediatePropagation(); + } + } }); From 15b7d2a86885a6c83b520277f97b1debe31048fb Mon Sep 17 00:00:00 2001 From: "K. Adam White" Date: Tue, 17 Sep 2024 22:17:41 +0000 Subject: [PATCH 5/7] REST API: Only check password value in query parameters while checking post permissions. The `password` property which gets sent as part of a request POST body while setting a post's password should not be checked when calculating post visibility permissions. That value in the request body is intended to update the post, not to authenticate, and may be malformed or an invalid non-string type which would cause a fatal when checking against the hashed post password value. Query parameter `?password=` values are the correct interface to check, and are also guaranteed to be strings. Props mlf20, devansh016, antonvlasenko, TimothyBlynJacobs, kadamwhite. Fixes #61837. git-svn-id: https://develop.svn.wordpress.org/trunk@59036 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-rest-posts-controller.php | 4 +- .../tests/rest-api/rest-posts-controller.php | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 11bc499fc6720..8aec375bc8bb8 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -504,9 +504,9 @@ public function get_item_permissions_check( $request ) { ); } - if ( $post && ! empty( $request['password'] ) ) { + if ( $post && ! empty( $request->get_query_params()['password'] ) ) { // Check post password, and return error if invalid. - if ( ! hash_equals( $post->post_password, $request['password'] ) ) { + if ( ! hash_equals( $post->post_password, $request->get_query_params()['password'] ) ) { return new WP_Error( 'rest_post_incorrect_password', __( 'Incorrect post password.' ), diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index 3085d066fc6ae..9b697fe2efd3d 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -2232,6 +2232,51 @@ public function test_get_post_with_password_without_permission() { $this->assertTrue( $data['excerpt']['protected'] ); } + /** + * @ticket 61837 + */ + public function test_get_item_permissions_check_while_updating_password() { + $endpoint = new WP_REST_Posts_Controller( 'post' ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_url_params( array( 'id' => self::$post_id ) ); + $request->set_body_params( + $this->set_post_data( + array( + 'id' => self::$post_id, + 'password' => '123', + ) + ) + ); + $permission = $endpoint->get_item_permissions_check( $request ); + + // Password provided in POST data, should not be used as authentication. + $this->assertNotWPError( $permission, 'Password in post body should be ignored by permissions check.' ); + $this->assertTrue( $permission ); + } + + /** + * @ticket 61837 + */ + public function test_get_item_permissions_check_while_updating_password_with_invalid_type() { + $endpoint = new WP_REST_Posts_Controller( 'post' ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_url_params( array( 'id' => self::$post_id ) ); + $request->set_body_params( + $this->set_post_data( + array( + 'id' => self::$post_id, + 'password' => 123, + ) + ) + ); + $permission = $endpoint->get_item_permissions_check( $request ); + + $this->assertNotWPError( $permission, 'Password in post body should be ignored by permissions check even when it is an invalid type.' ); + $this->assertTrue( $permission ); + } + /** * The post response should not have `block_version` when in view context. * From 98a9f6481afe3dc764f0ca5bb4f108cefe7203e0 Mon Sep 17 00:00:00 2001 From: Anthony Burchell Date: Tue, 17 Sep 2024 22:24:43 +0000 Subject: [PATCH 6/7] Coding Standards: Avoid using confusing `!` condition in Media Library selection check. Checks that value is now equal or less than or equal to 0 which has the same result as the previous confusing `!` usage. Props kadamwhite, drjosh07. See #60369. git-svn-id: https://develop.svn.wordpress.org/trunk@59037 602fd350-edb4-49c9-b593-d223f7449a82 --- src/js/media/views/modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/media/views/modal.js b/src/js/media/views/modal.js index e3d9ad1701c86..cfc396bf8039b 100644 --- a/src/js/media/views/modal.js +++ b/src/js/media/views/modal.js @@ -190,7 +190,7 @@ Modal = wp.media.View.extend(/** @lends wp.media.view.Modal.prototype */{ selectHandler: function( event ) { var selection = this.controller.state().get( 'selection' ); - if ( ! selection.length > 0 ) { + if ( selection.length <= 0 ) { return; } From 3cd3a00c76102913590ade96a44003deb01664c3 Mon Sep 17 00:00:00 2001 From: Timothy Jacobs Date: Tue, 17 Sep 2024 22:25:03 +0000 Subject: [PATCH 7/7] Build Tools: Allow easier customization of the .env file. The .env file allows for configuring how the WordPress Local environment should be configured. However, because the file is version controlled, developers must be careful not to commit their modifications. This commit renames the .env file to be .env.example. During env start, the .env.example file is copied to .env if it does not exist. This allows for contributors to continue using the project without thinking about .env and to make changes when needed. This brings WordPress Core into the dotenv project guidelines. Props johnbillion, afragen, h71, desrosj. Fixes #52668. git-svn-id: https://develop.svn.wordpress.org/trunk@59038 602fd350-edb4-49c9-b593-d223f7449a82 --- .env => .env.example | 0 .gitignore | 1 + tools/local-env/scripts/start.js | 14 ++++++++++++++ 3 files changed, 15 insertions(+) rename .env => .env.example (100%) diff --git a/.env b/.env.example similarity index 100% rename from .env rename to .env.example diff --git a/.gitignore b/.gitignore index 44c3769ee314d..f35d743ea866f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # gitignore file for WordPress Core # Configuration files with possibly sensitive information +.env wp-config.php wp-tests-config.php .htaccess diff --git a/tools/local-env/scripts/start.js b/tools/local-env/scripts/start.js index b929dce0b69a8..22bb65ca8426d 100644 --- a/tools/local-env/scripts/start.js +++ b/tools/local-env/scripts/start.js @@ -2,6 +2,20 @@ const dotenv = require( 'dotenv' ); const dotenvExpand = require( 'dotenv-expand' ); const { execSync } = require( 'child_process' ); +try { + execSync( 'test -f .env', { stdio: 'inherit' } ); +} catch ( e ) { + // test exits with a status code of 1 if the test fails. + // Alert the user on any other failure. + if ( e.status !== 1 ) { + throw e; + } + + // The file does not exist, copy over the default example file. + execSync( 'cp .env.example .env', { stdio: 'inherit' } ); +} + + dotenvExpand.expand( dotenv.config() ); // Check if the Docker service is running.