From 67d32af6b1a19501a53adea5a73ad533bc88fa0c Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:43:04 +0300 Subject: [PATCH 1/7] Add utils/post/rest-route endpoint --- src/rest-api/class-rest-api-controller.php | 2 + src/rest-api/utils/class-endpoint-post.php | 72 +++++++++++++++++++ src/rest-api/utils/class-utils-controller.php | 47 ++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 src/rest-api/utils/class-endpoint-post.php create mode 100644 src/rest-api/utils/class-utils-controller.php diff --git a/src/rest-api/class-rest-api-controller.php b/src/rest-api/class-rest-api-controller.php index de83801c8d..756ea63738 100644 --- a/src/rest-api/class-rest-api-controller.php +++ b/src/rest-api/class-rest-api-controller.php @@ -13,6 +13,7 @@ use Parsely\REST_API\Content_Helper\Content_Helper_Controller; use Parsely\REST_API\Settings\Settings_Controller; use Parsely\REST_API\Stats\Stats_Controller; +use Parsely\REST_API\Utils\Utils_Controller; /** * The REST API Controller. @@ -64,6 +65,7 @@ public function init(): void { new Content_Helper_Controller( $this->get_parsely() ), new Stats_Controller( $this->get_parsely() ), new Settings_Controller( $this->get_parsely() ), + new Utils_Controller( $this->get_parsely() ), ); // Initialize the controllers. diff --git a/src/rest-api/utils/class-endpoint-post.php b/src/rest-api/utils/class-endpoint-post.php new file mode 100644 index 0000000000..bf9af500bf --- /dev/null +++ b/src/rest-api/utils/class-endpoint-post.php @@ -0,0 +1,72 @@ +register_rest_route_with_post_id( + '/rest-route', + array( 'GET' ), + array( $this, 'get_rest_route' ) + ); + } + + /** + * API Endpoint: GET /utils/post/{post_id}/rest-route + * + * Returns the REST route of a post. + * + * @since 3.20.5 + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response The response object. + */ + public function get_rest_route( WP_REST_Request $request ) { + $post_id = $request->get_param( 'post_id' ); + $post_rest_route = rest_get_route_for_post( $post_id ); + + return new WP_REST_Response( array( 'data' => $post_rest_route ), 200 ); + } +} diff --git a/src/rest-api/utils/class-utils-controller.php b/src/rest-api/utils/class-utils-controller.php new file mode 100644 index 0000000000..056907f0e4 --- /dev/null +++ b/src/rest-api/utils/class-utils-controller.php @@ -0,0 +1,47 @@ +register_endpoints( $endpoints ); + } +} From 7f7a36cc26f530f42913053ca96010f2fad45a28 Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:44:11 +0300 Subject: [PATCH 2/7] Add utils/post/rest-route endpoint integration tests --- ...PostTest.php => EndpointStatsPostTest.php} | 2 +- .../RestAPI/Utils/EndpointUtilsPostTest.php | 425 ++++++++++++++++++ 2 files changed, 426 insertions(+), 1 deletion(-) rename tests/Integration/RestAPI/Stats/{EndpointPostTest.php => EndpointStatsPostTest.php} (99%) create mode 100644 tests/Integration/RestAPI/Utils/EndpointUtilsPostTest.php diff --git a/tests/Integration/RestAPI/Stats/EndpointPostTest.php b/tests/Integration/RestAPI/Stats/EndpointStatsPostTest.php similarity index 99% rename from tests/Integration/RestAPI/Stats/EndpointPostTest.php rename to tests/Integration/RestAPI/Stats/EndpointStatsPostTest.php index a990af6933..f94a92c69e 100644 --- a/tests/Integration/RestAPI/Stats/EndpointPostTest.php +++ b/tests/Integration/RestAPI/Stats/EndpointStatsPostTest.php @@ -23,7 +23,7 @@ * * @since 3.17.0 */ -class EndpointPostTest extends BaseEndpointTest { +class EndpointStatsPostTest extends BaseEndpointTest { /** * The endpoint instance. * diff --git a/tests/Integration/RestAPI/Utils/EndpointUtilsPostTest.php b/tests/Integration/RestAPI/Utils/EndpointUtilsPostTest.php new file mode 100644 index 0000000000..816664f61f --- /dev/null +++ b/tests/Integration/RestAPI/Utils/EndpointUtilsPostTest.php @@ -0,0 +1,425 @@ +api_controller = new Utils_Controller( $this->parsely ); + $this->endpoint = new Endpoint_Post( $this->api_controller ); + + parent::set_up(); + + TestCase::set_options( + array( + 'apikey' => 'test-api-key', + 'api_secret' => 'test-secret', + ) + ); + } + + /** + * Gets the test endpoint instance. + * + * @since 3.20.5 + * + * @return Endpoint_Post + */ + public function get_endpoint(): Base_Endpoint { + return $this->endpoint; + } + + /** + * Verifies that the route is registered. + * + * @since 3.20.5 + * + * @covers \Parsely\REST_API\Utils\Endpoint_Post::register_routes + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::get_registered_routes + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Utils\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Utils\Utils_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_route_is_registered(): void { + $routes = rest_get_server()->get_routes(); + $registered_routes = $this->get_endpoint()->get_registered_routes(); + + // Assert that the routes are registered when the filter returns true. + foreach ( $registered_routes as $route ) { + $expected_route = $this->get_endpoint()->get_full_endpoint( $route ); + $route_data = $routes[ $expected_route ]; + self::assertArrayHasKey( $expected_route, $routes ); + + // Check that the route is associated with the GET method, since all + // the routes in this endpoint are GET routes. + self::assertArrayHasKey( 'GET', $route_data[0]['methods'] ); + } + } + + /** + * Verifies that the endpoint is not available if the API Secret is not set. + * + * @covers \Parsely\REST_API\Utils\Endpoint_Post::is_available_to_current_user + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\REST_API\Utils\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Utils\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Utils\Utils_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_error_if_api_secret_is_not_set(): void { + $test_post_id = $this->create_test_post(); + TestCase::set_options( + array( + 'apikey' => 'test', + 'api_secret' => '', + ) + ); + + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/rest-route' ); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', $route ) + ); + + $error = $response->as_error(); + self::assertNotNull( $error ); + self::assertSame( 403, $response->get_status() ); + self::assertSame( 'parsely_api_secret_not_set', $error->get_error_code() ); + self::assertSame( + 'A Parse.ly API Secret must be set in site options to use this endpoint', + $error->get_error_message() + ); + } + + /** + * Verifies forbidden error when current user doesn't have proper + * capabilities. + * + * @covers \Parsely\REST_API\Utils\Endpoint_Post::is_available_to_current_user + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\REST_API\Utils\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Utils\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Utils\Utils_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_access_of_stats_post_endpoint_is_forbidden(): void { + $test_post_id = $this->create_test_post(); + $this->set_current_user_to_contributor(); + + $route = $this->get_endpoint()->get_full_endpoint( '/' . $test_post_id . '/rest-route' ); + $response = rest_get_server()->dispatch( + new WP_REST_Request( 'GET', $route ) + ); + /** + * Variable. + * + * @var WP_Error $error + */ + $error = $response->as_error(); + + self::assertSame( 403, $response->get_status() ); + self::assertSame( 'rest_forbidden', $error->get_error_code() ); + self::assertSame( + 'Sorry, you are not allowed to do that.', + $error->get_error_message() + ); + } + + /** + * Verifies that get_rest_route() works as expected with WordPress built-in + * post types. + * + * @since 3.20.5 + * + * @covers \Parsely\REST_API\Utils\Endpoint_Post::get_rest_route + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\REST_API\Utils\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Utils\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Utils\Utils_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_get_rest_route_with_built_in_post_types(): void { + self::assertSame( '/wp/v2/posts', $this->get_base_rest_route( 'post' ) ); + self::assertSame( '/wp/v2/pages', $this->get_base_rest_route( 'page' ) ); + } + + /** + * Verifies that get_rest_route() works as expected with custom post types. + * + * @since 3.20.5 + * + * @covers \Parsely\REST_API\Utils\Endpoint_Post::get_rest_route + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\REST_API\Utils\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Utils\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Utils\Utils_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_get_rest_route_with_custom_post_types(): void { + register_post_type( 'cpt_rest', array( 'show_in_rest' => true ) ); + register_post_type( 'cpt_no_rest', array( 'show_in_rest' => false ) ); + register_post_type( + 'cpt_private', + array( + 'show_in_rest' => true, + 'public' => false, + ) + ); + register_post_type( + 'cpt_public', + array( + 'show_in_rest' => true, + 'public' => true, + ) + ); + register_post_type( + 'cpt_custom_rest_base', + array( + 'show_in_rest' => true, + 'rest_base' => 'custom_rest_base', + ) + ); + + self::assertSame( '/wp/v2/cpt_rest', $this->get_base_rest_route( 'cpt_rest' ) ); + self::assertSame( '/wp/v2/cpt_private', $this->get_base_rest_route( 'cpt_private' ) ); + self::assertSame( '/wp/v2/cpt_public', $this->get_base_rest_route( 'cpt_public' ) ); + self::assertSame( '/wp/v2/custom_rest_base', $this->get_base_rest_route( 'cpt_custom_rest_base' ) ); + + // Post types that aren't registered with the REST API should return an + // empty string. + self::assertSame( '', $this->get_base_rest_route( 'cpt_no_rest' ) ); + } + + /** + * Verifies that get_rest_route() works as expected when the route has been + * modified by filters. + * + * @since 3.20.5 + * + * @covers \Parsely\REST_API\Utils\Endpoint_Post::get_rest_route + * @uses \Parsely\Parsely::api_secret_is_set + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::site_id_is_set + * @uses \Parsely\Permissions::build_pch_permissions_settings_array + * @uses \Parsely\Permissions::get_user_roles_with_edit_posts_cap + * @uses \Parsely\REST_API\Base_API_Controller::__construct + * @uses \Parsely\REST_API\Base_API_Controller::get_full_namespace + * @uses \Parsely\REST_API\Base_API_Controller::get_parsely + * @uses \Parsely\REST_API\Base_API_Controller::prefix_route + * @uses \Parsely\REST_API\Base_Endpoint::__construct + * @uses \Parsely\REST_API\Base_Endpoint::apply_capability_filters + * @uses \Parsely\REST_API\Base_Endpoint::get_default_access_capability + * @uses \Parsely\REST_API\Base_Endpoint::get_full_endpoint + * @uses \Parsely\REST_API\Base_Endpoint::init + * @uses \Parsely\REST_API\Base_Endpoint::is_available_to_current_user + * @uses \Parsely\REST_API\Base_Endpoint::register_rest_route + * @uses \Parsely\REST_API\Base_Endpoint::validate_site_id_and_secret + * @uses \Parsely\REST_API\REST_API_Controller::get_namespace + * @uses \Parsely\REST_API\REST_API_Controller::get_version + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::register_rest_route_with_post_id + * @uses \Parsely\REST_API\Use_Post_ID_Parameter_Trait::validate_post_id + * @uses \Parsely\REST_API\Utils\Endpoint_Post::get_endpoint_name + * @uses \Parsely\REST_API\Utils\Endpoint_Post::register_routes + * @uses \Parsely\REST_API\Utils\Utils_Controller::get_route_prefix + * @uses \Parsely\Utils\Utils::convert_endpoint_to_filter_key + */ + public function test_get_rest_route_with_filtered_routes(): void { + // Test `rest_route_for_post_type_items` filter. + register_post_type( 'cpt_filtered_route', array( 'show_in_rest' => true ) ); + add_filter( + 'rest_route_for_post_type_items', + function ( $route, $post_type ) { + if ( 'cpt_filtered_route' === $post_type->name ) { + $route = '/wp/v2/filtered_route'; + } + + return $route; + }, + 10, + 2 + ); + self::assertSame( '/wp/v2/filtered_route', $this->get_base_rest_route( 'cpt_filtered_route' ) ); + + // Test `rest_route_for_post` filter. + register_post_type( 'cpt_filtered_route_2', array( 'show_in_rest' => true ) ); + add_filter( + 'rest_route_for_post', + function ( $route, $post ) { + if ( '/wp/v2/cpt_filtered_route_2/' . $post->ID === $route ) { + $route = '/wp/v2/filtered_route_2'; + } + + return $route; + }, + 10, + 2 + ); + self::assertSame( '/wp/v2/filtered_route_2', $this->get_base_rest_route( 'cpt_filtered_route_2' ) ); + } + + /** + * Returns the base REST route for the passed post type. + * + * @since 3.20.5 + * + * @param string $post_type The post type for which to get the base REST route. + * @return string The base REST route for the post type. + */ + public function get_base_rest_route( string $post_type ): string { + $post_id = self::factory()->post->create( array( 'post_type' => $post_type ) ); /** @var int $post_id */ + $route = $this->get_endpoint()->get_full_endpoint( '/' . $post_id . '/rest-route' ); + + /** @var string $data */ + // @phpstan-ignore offsetAccess.nonOffsetAccessible + $data = rest_get_server()->dispatch( new WP_REST_Request( 'GET', $route ) )->get_data()['data']; + + // Return the data without the Post ID. + return str_replace( "/${post_id}", '', $data ); + } +} From fdd78b934d50a82519be14cf30c6a943cf6b796e Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:55:50 +0300 Subject: [PATCH 3/7] EndpointUtilsPostTest.php: Clean up after some tests --- .../RestAPI/Utils/EndpointUtilsPostTest.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/Integration/RestAPI/Utils/EndpointUtilsPostTest.php b/tests/Integration/RestAPI/Utils/EndpointUtilsPostTest.php index 816664f61f..b0f0b94233 100644 --- a/tests/Integration/RestAPI/Utils/EndpointUtilsPostTest.php +++ b/tests/Integration/RestAPI/Utils/EndpointUtilsPostTest.php @@ -331,6 +331,13 @@ public function test_get_rest_route_with_custom_post_types(): void { // Post types that aren't registered with the REST API should return an // empty string. self::assertSame( '', $this->get_base_rest_route( 'cpt_no_rest' ) ); + + // Clean up. + unregister_post_type( 'cpt_rest' ); + unregister_post_type( 'cpt_no_rest' ); + unregister_post_type( 'cpt_private' ); + unregister_post_type( 'cpt_public' ); + unregister_post_type( 'cpt_custom_rest_base' ); } /** @@ -374,7 +381,7 @@ public function test_get_rest_route_with_filtered_routes(): void { register_post_type( 'cpt_filtered_route', array( 'show_in_rest' => true ) ); add_filter( 'rest_route_for_post_type_items', - function ( $route, $post_type ) { + $filtered_route_callback = function ( $route, $post_type ) { if ( 'cpt_filtered_route' === $post_type->name ) { $route = '/wp/v2/filtered_route'; } @@ -390,7 +397,7 @@ function ( $route, $post_type ) { register_post_type( 'cpt_filtered_route_2', array( 'show_in_rest' => true ) ); add_filter( 'rest_route_for_post', - function ( $route, $post ) { + $filtered_route_2_callback = function ( $route, $post ) { if ( '/wp/v2/cpt_filtered_route_2/' . $post->ID === $route ) { $route = '/wp/v2/filtered_route_2'; } @@ -401,6 +408,12 @@ function ( $route, $post ) { 2 ); self::assertSame( '/wp/v2/filtered_route_2', $this->get_base_rest_route( 'cpt_filtered_route_2' ) ); + + // Clean up. + remove_filter( 'rest_route_for_post_type_items', $filtered_route_callback ); + remove_filter( 'rest_route_for_post', $filtered_route_2_callback ); + unregister_post_type( 'cpt_filtered_route' ); + unregister_post_type( 'cpt_filtered_route_2' ); } /** From 36edd69d3855574342984f9ad92604c51918946e Mon Sep 17 00:00:00 2001 From: Alex Cicovic <23142906+acicovic@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:55:37 +0300 Subject: [PATCH 4/7] Engagement Boost: Add support for custom post types --- build/content-helper/dashboard-page.asset.php | 2 +- build/content-helper/dashboard-page.js | 12 +++---- .../providers/base-wordpress-provider.tsx | 33 ++++++++++++++++++- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/build/content-helper/dashboard-page.asset.php b/build/content-helper/dashboard-page.asset.php index 2284c99dd7..eee7b05c41 100644 --- a/build/content-helper/dashboard-page.asset.php +++ b/build/content-helper/dashboard-page.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => 'cb305c11a90df2b8d9ae'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-dom-ready', 'wp-element', 'wp-escape-html', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url'), 'version' => '86966deca1032417599c'); diff --git a/build/content-helper/dashboard-page.js b/build/content-helper/dashboard-page.js index 859afd1180..4ec0b87156 100644 --- a/build/content-helper/dashboard-page.js +++ b/build/content-helper/dashboard-page.js @@ -1,9 +1,9 @@ -!function(){"use strict";var e={69:function(e,t){Object.prototype.toString},20:function(e,t,n){var r=n(609),o=Symbol.for("react.element"),i=Symbol.for("react.fragment"),a=Object.prototype.hasOwnProperty,s=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,n){var r,i={},c=null,u=null;for(r in void 0!==n&&(c=""+n),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)a.call(t,r)&&!l.hasOwnProperty(r)&&(i[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===i[r]&&(i[r]=t[r]);return{$$typeof:o,type:e,key:c,ref:u,props:i,_owner:s.current}}t.Fragment=i,t.jsx=c,t.jsxs=c},848:function(e,t,n){e.exports=n(20)},609:function(e){e.exports=window.React}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var i=t[r]={exports:{}};return e[r](i,i.exports,n),i.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e=n(848),t=n(609),r=(n(69),"popstate");function o(e={}){return function(e,t,n,o={}){let{window:a=document.defaultView,v5Compat:u=!1}=o,d=a.history,p="POP",f=null,h=g();function g(){return(d.state||{idx:null}).idx}function v(){p="POP";let e=g(),t=null==e?null:e-h;h=e,f&&f({action:p,location:y.location,delta:t})}function m(e){return function(e,t=!1){let n="http://localhost";"undefined"!=typeof window&&(n="null"!==window.location.origin?window.location.origin:window.location.href),i(n,"No window.location.(origin|href) available to create URL");let r="string"==typeof e?e:c(e);return r=r.replace(/ $/,"%20"),!t&&r.startsWith("//")&&(r=n+r),new URL(r,n)}(e)}null==h&&(h=0,d.replaceState({...d.state,idx:h},""));let y={get action(){return p},get location(){return e(a,d)},listen(e){if(f)throw new Error("A history only accepts one active listener");return a.addEventListener(r,v),f=e,()=>{a.removeEventListener(r,v),f=null}},createHref(e){return t(a,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:function(e,t){p="PUSH";let r=l(y.location,e,t);n&&n(r,e),h=g()+1;let o=s(r,h),i=y.createHref(r);try{d.pushState(o,"",i)}catch(e){if(e instanceof DOMException&&"DataCloneError"===e.name)throw e;a.location.assign(i)}u&&f&&f({action:p,location:y.location,delta:1})},replace:function(e,t){p="REPLACE";let r=l(y.location,e,t);n&&n(r,e),h=g();let o=s(r,h),i=y.createHref(r);d.replaceState(o,"",i),u&&f&&f({action:p,location:y.location,delta:0})},go(e){return d.go(e)}};return y}((function(e,t){let{pathname:n="/",search:r="",hash:o=""}=u(e.location.hash.substring(1));return n.startsWith("/")||n.startsWith(".")||(n="/"+n),l("",{pathname:n,search:r,hash:o},t.state&&t.state.usr||null,t.state&&t.state.key||"default")}),(function(e,t){let n=e.document.querySelector("base"),r="";if(n&&n.getAttribute("href")){let t=e.location.href,n=t.indexOf("#");r=-1===n?t:t.slice(0,n)}return r+"#"+("string"==typeof t?t:c(t))}),(function(e,t){a("/"===e.pathname.charAt(0),`relative pathnames are not supported in hash history.push(${JSON.stringify(t)})`)}),e)}function i(e,t){if(!1===e||null==e)throw new Error(t)}function a(e,t){if(!e){"undefined"!=typeof console&&console.warn(t);try{throw new Error(t)}catch(e){}}}function s(e,t){return{usr:e.state,key:e.key,idx:t}}function l(e,t,n=null,r){return{pathname:"string"==typeof e?e:e.pathname,search:"",hash:"",..."string"==typeof t?u(t):t,state:n,key:t&&t.key||r||Math.random().toString(36).substring(2,10)}}function c({pathname:e="/",search:t="",hash:n=""}){return t&&"?"!==t&&(e+="?"===t.charAt(0)?t:"?"+t),n&&"#"!==n&&(e+="#"===n.charAt(0)?n:"#"+n),e}function u(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function d(e,t,n="/"){return function(e,t,n,r){let o=E(("string"==typeof t?u(t):t).pathname||"/",n);if(null==o)return null;let i=p(e);!function(e){e.sort(((e,t)=>e.score!==t.score?t.score-e.score:function(e,t){return e.length===t.length&&e.slice(0,-1).every(((e,n)=>e===t[n]))?e[e.length-1]-t[t.length-1]:0}(e.routesMeta.map((e=>e.childrenIndex)),t.routesMeta.map((e=>e.childrenIndex)))))}(i);let a=null;for(let e=0;null==a&&e{let s={relativePath:void 0===a?e.path||"":a,caseSensitive:!0===e.caseSensitive,childrenIndex:o,route:e};s.relativePath.startsWith("/")&&(i(s.relativePath.startsWith(r),`Absolute route path "${s.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),s.relativePath=s.relativePath.slice(r.length));let l=C([r,s.relativePath]),c=n.concat(s);e.children&&e.children.length>0&&(i(!0!==e.index,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),p(e.children,t,c,l)),(null!=e.path||e.index)&&t.push({path:l,score:x(l,e.index),routesMeta:c})};return e.forEach(((e,t)=>{if(""!==e.path&&e.path?.includes("?"))for(let n of f(e.path))o(e,t,n);else o(e,t)})),t}function f(e){let t=e.split("/");if(0===t.length)return[];let[n,...r]=t,o=n.endsWith("?"),i=n.replace(/\?$/,"");if(0===r.length)return o?[i,""]:[i];let a=f(r.join("/")),s=[];return s.push(...a.map((e=>""===e?i:[i,e].join("/")))),o&&s.push(...a),s.map((t=>e.startsWith("/")&&""===t?"/":t))}new WeakMap;var h=/^:[\w-]+$/,g=3,v=2,m=1,y=10,w=-2,b=e=>"*"===e;function x(e,t){let n=e.split("/"),r=n.length;return n.some(b)&&(r+=w),t&&(r+=v),n.filter((e=>!b(e))).reduce(((e,t)=>e+(h.test(t)?g:""===t?m:y)),r)}function k(e,t,n=!1){let{routesMeta:r}=e,o={},i="/",a=[];for(let e=0;e(r.push({paramName:t,isOptional:null!=n}),n?"/?([^\\/]+)?":"/([^\\/]+)")));return e.endsWith("*")?(r.push({paramName:"*"}),o+="*"===e||"/*"===e?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?o+="\\/*$":""!==e&&"/"!==e&&(o+="(?:(?=\\/|$))"),[new RegExp(o,t?void 0:"i"),r]}(e.path,e.caseSensitive,e.end),o=t.match(n);if(!o)return null;let i=o[0],s=i.replace(/(.)\/+$/,"$1"),l=o.slice(1);return{params:r.reduce(((e,{paramName:t,isOptional:n},r)=>{if("*"===t){let e=l[r]||"";s=i.slice(0,i.length-e.length).replace(/(.)\/+$/,"$1")}const o=l[r];return e[t]=n&&!o?void 0:(o||"").replace(/%2F/g,"/"),e}),{}),pathname:i,pathnameBase:s,pattern:e}}function S(e){try{return e.split("/").map((e=>decodeURIComponent(e).replace(/\//g,"%2F"))).join("/")}catch(t){return a(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function E(e,t){if("/"===t)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&"/"!==r?null:e.slice(n)||"/"}function L(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function P(e){let t=function(e){return e.filter(((e,t)=>0===t||e.route.path&&e.route.path.length>0))}(e);return t.map(((e,n)=>n===t.length-1?e.pathname:e.pathnameBase))}function j(e,t,n,r=!1){let o;"string"==typeof e?o=u(e):(o={...e},i(!o.pathname||!o.pathname.includes("?"),L("?","pathname","search",o)),i(!o.pathname||!o.pathname.includes("#"),L("#","pathname","hash",o)),i(!o.search||!o.search.includes("#"),L("#","search","hash",o)));let a,s=""===e||""===o.pathname,l=s?"/":o.pathname;if(null==l)a=n;else{let e=t.length-1;if(!r&&l.startsWith("..")){let t=l.split("/");for(;".."===t[0];)t.shift(),e-=1;o.pathname=t.join("/")}a=e>=0?t[e]:"/"}let c=function(e,t="/"){let{pathname:n,search:r="",hash:o=""}="string"==typeof e?u(e):e,i=n?n.startsWith("/")?n:function(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach((e=>{".."===e?n.length>1&&n.pop():"."!==e&&n.push(e)})),n.length>1?n.join("/"):"/"}(n,t):t;return{pathname:i,search:N(r),hash:I(o)}}(o,a),d=l&&"/"!==l&&l.endsWith("/"),p=(s||"."===l)&&n.endsWith("/");return c.pathname.endsWith("/")||!d&&!p||(c.pathname+="/"),c}var C=e=>e.join("/").replace(/\/\/+/g,"/"),T=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),N=e=>e&&"?"!==e?e.startsWith("?")?e:"?"+e:"",I=e=>e&&"#"!==e?e.startsWith("#")?e:"#"+e:"";function R(e){return null!=e&&"number"==typeof e.status&&"string"==typeof e.statusText&&"boolean"==typeof e.internal&&"data"in e}var A=["POST","PUT","PATCH","DELETE"],O=(new Set(A),["GET",...A]);new Set(O),Symbol("ResetLoaderData");var D=t.createContext(null);D.displayName="DataRouter";var B=t.createContext(null);B.displayName="DataRouterState";var M=t.createContext({isTransitioning:!1});M.displayName="ViewTransition",t.createContext(new Map).displayName="Fetchers",t.createContext(null).displayName="Await";var G=t.createContext(null);G.displayName="Navigation";var F=t.createContext(null);F.displayName="Location";var U=t.createContext({outlet:null,matches:[],isDataRoute:!1});U.displayName="Route";var H=t.createContext(null);function V(){return null!=t.useContext(F)}function $(){return i(V(),"useLocation() may be used only in the context of a component."),t.useContext(F).location}H.displayName="RouteError";var W="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function z(e){t.useContext(G).static||t.useLayoutEffect(e)}function q(){let{isDataRoute:e}=t.useContext(U);return e?function(){let{router:e}=function(e){let n=t.useContext(D);return i(n,te(e)),n}("useNavigate"),n=ne("useNavigate"),r=t.useRef(!1);return z((()=>{r.current=!0})),t.useCallback((async(t,o={})=>{a(r.current,W),r.current&&("number"==typeof t?e.navigate(t):await e.navigate(t,{fromRouteId:n,...o}))}),[e,n])}():function(){i(V(),"useNavigate() may be used only in the context of a component.");let e=t.useContext(D),{basename:n,navigator:r}=t.useContext(G),{matches:o}=t.useContext(U),{pathname:s}=$(),l=JSON.stringify(P(o)),c=t.useRef(!1);return z((()=>{c.current=!0})),t.useCallback(((t,o={})=>{if(a(c.current,W),!c.current)return;if("number"==typeof t)return void r.go(t);let i=j(t,JSON.parse(l),s,"path"===o.relative);null==e&&"/"!==n&&(i.pathname="/"===i.pathname?n:C([n,i.pathname])),(o.replace?r.replace:r.push)(i,o.state,o)}),[n,r,l,s,e])}()}function Z(){let{matches:e}=t.useContext(U),n=e[e.length-1];return n?n.params:{}}function K(e,{relative:n}={}){let{matches:r}=t.useContext(U),{pathname:o}=$(),i=JSON.stringify(P(r));return t.useMemo((()=>j(e,JSON.parse(i),o,"path"===n)),[e,i,o,n])}function Y(e,n,r,o){i(V(),"useRoutes() may be used only in the context of a component.");let{navigator:s}=t.useContext(G),{matches:l}=t.useContext(U),c=l[l.length-1],p=c?c.params:{},f=c?c.pathname:"/",h=c?c.pathnameBase:"/",g=c&&c.route;{let e=g&&g.path||"";oe(f,!g||e.endsWith("*")||e.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${f}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render.\n\nPlease change the parent to .`)}let v,m=$();if(n){let e="string"==typeof n?u(n):n;i("/"===h||e.pathname?.startsWith(h),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${h}" but pathname "${e.pathname}" was given in the \`location\` prop.`),v=e}else v=m;let y=v.pathname||"/",w=y;if("/"!==h){let e=h.replace(/^\//,"").split("/");w="/"+y.replace(/^\//,"").split("/").slice(e.length).join("/")}let b=d(e,{pathname:w});a(g||null!=b,`No routes matched location "${v.pathname}${v.search}${v.hash}" `),a(null==b||void 0!==b[b.length-1].route.element||void 0!==b[b.length-1].route.Component||void 0!==b[b.length-1].route.lazy,`Matched leaf route at location "${v.pathname}${v.search}${v.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let x=function(e,n=[],r=null){if(null==e){if(!r)return null;if(r.errors)e=r.matches;else{if(0!==n.length||r.initialized||!(r.matches.length>0))return null;e=r.matches}}let o=e,a=r?.errors;if(null!=a){let e=o.findIndex((e=>e.route.id&&void 0!==a?.[e.route.id]));i(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(",")}`),o=o.slice(0,Math.min(o.length,e+1))}let s=!1,l=-1;if(r)for(let e=0;e=0?o.slice(0,l+1):[o[0]];break}}}return o.reduceRight(((e,i,c)=>{let u,d=!1,p=null,f=null;r&&(u=a&&i.route.id?a[i.route.id]:void 0,p=i.route.errorElement||J,s&&(l<0&&0===c?(oe("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,f=null):l===c&&(d=!0,f=i.route.hydrateFallbackElement||null)));let h=n.concat(o.slice(0,c+1)),g=()=>{let n;return n=u?p:d?f:i.route.Component?t.createElement(i.route.Component,null):i.route.element?i.route.element:e,t.createElement(ee,{match:i,routeContext:{outlet:e,matches:h,isDataRoute:null!=r},children:n})};return r&&(i.route.ErrorBoundary||i.route.errorElement||0===c)?t.createElement(Q,{location:r.location,revalidation:r.revalidation,component:p,error:u,children:g(),routeContext:{outlet:null,matches:h,isDataRoute:!0}}):g()}),null)}(b&&b.map((e=>Object.assign({},e,{params:Object.assign({},p,e.params),pathname:C([h,s.encodeLocation?s.encodeLocation(e.pathname).pathname:e.pathname]),pathnameBase:"/"===e.pathnameBase?h:C([h,s.encodeLocation?s.encodeLocation(e.pathnameBase).pathname:e.pathnameBase])}))),l,r,o);return n&&x?t.createElement(F.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...v},navigationType:"POP"}},x):x}function X(){let e=function(){let e=t.useContext(H),n=function(e){let n=t.useContext(B);return i(n,te(e)),n}("useRouteError"),r=ne("useRouteError");return void 0!==e?e:n.errors?.[r]}(),n=R(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),r=e instanceof Error?e.stack:null,o="rgba(200,200,200, 0.5)",a={padding:"0.5rem",backgroundColor:o},s={padding:"2px 4px",backgroundColor:o},l=null;return console.error("Error handled by React Router default ErrorBoundary:",e),l=t.createElement(t.Fragment,null,t.createElement("p",null,"đź’ż Hey developer đź‘‹"),t.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",t.createElement("code",{style:s},"ErrorBoundary")," or"," ",t.createElement("code",{style:s},"errorElement")," prop on your route.")),t.createElement(t.Fragment,null,t.createElement("h2",null,"Unexpected Application Error!"),t.createElement("h3",{style:{fontStyle:"italic"}},n),r?t.createElement("pre",{style:a},r):null,l)}t.createContext(null);var J=t.createElement(X,null),Q=class extends t.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||"idle"!==t.revalidation&&"idle"===e.revalidation?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:void 0!==e.error?e.error:t.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){console.error("React Router caught the following error during render",e,t)}render(){return void 0!==this.state.error?t.createElement(U.Provider,{value:this.props.routeContext},t.createElement(H.Provider,{value:this.state.error,children:this.props.component})):this.props.children}};function ee({routeContext:e,match:n,children:r}){let o=t.useContext(D);return o&&o.static&&o.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(o.staticContext._deepestRenderedBoundaryId=n.route.id),t.createElement(U.Provider,{value:e},r)}function te(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function ne(e){let n=function(e){let n=t.useContext(U);return i(n,te(e)),n}(e),r=n.matches[n.matches.length-1];return i(r.route.id,`${e} can only be used on routes that contain a unique "id"`),r.route.id}var re={};function oe(e,t,n){t||re[e]||(re[e]=!0,a(!1,n))}function ie({to:e,replace:n,state:r,relative:o}){i(V()," may be used only in the context of a component.");let{static:s}=t.useContext(G);a(!s," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:l}=t.useContext(U),{pathname:c}=$(),u=q(),d=j(e,P(l),c,"path"===o),p=JSON.stringify(d);return t.useEffect((()=>{u(JSON.parse(p),{replace:n,state:r,relative:o})}),[u,p,o,n,r]),null}function ae(e){i(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function se({basename:e="/",children:n=null,location:r,navigationType:o="POP",navigator:s,static:l=!1}){i(!V(),"You cannot render a inside another . You should never have more than one in your app.");let c=e.replace(/^\/*/,"/"),d=t.useMemo((()=>({basename:c,navigator:s,static:l,future:{}})),[c,s,l]);"string"==typeof r&&(r=u(r));let{pathname:p="/",search:f="",hash:h="",state:g=null,key:v="default"}=r,m=t.useMemo((()=>{let e=E(p,c);return null==e?null:{location:{pathname:e,search:f,hash:h,state:g,key:v},navigationType:o}}),[c,p,f,h,g,v,o]);return a(null!=m,` is not able to match the URL "${p}${f}${h}" because it does not start with the basename, so the won't render anything.`),null==m?null:t.createElement(G.Provider,{value:d},t.createElement(F.Provider,{children:n,value:m}))}function le({children:e,location:t}){return Y(ce(e),t)}function ce(e,n=[]){let r=[];return t.Children.forEach(e,((e,o)=>{if(!t.isValidElement(e))return;let a=[...n,o];if(e.type===t.Fragment)return void r.push.apply(r,ce(e.props.children,a));i(e.type===ae,`[${"string"==typeof e.type?e.type:e.type.name}] is not a component. All component children of must be a or `),i(!e.props.index||!e.props.children,"An index route cannot have child routes.");let s={id:e.props.id||a.join("-"),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:!0===e.props.hasErrorBoundary||null!=e.props.ErrorBoundary||null!=e.props.errorElement,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(s.children=ce(e.props.children,a)),r.push(s)})),r}t.memo((function({routes:e,future:t,state:n}){return Y(e,void 0,n,t)})),t.Component;var ue="get",de="application/x-www-form-urlencoded";function pe(e){return null!=e&&"string"==typeof e.tagName}var fe=null,he=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function ge(e){return null==e||he.has(e)?e:(a(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${de}"`),null)}function ve(e,t){if(!1===e||null==e)throw new Error(t)}function me(e){return null!=e&&(null==e.href?"preload"===e.rel&&"string"==typeof e.imageSrcSet&&"string"==typeof e.imageSizes:"string"==typeof e.rel&&"string"==typeof e.href)}function ye(e,t,n,r,o,i){let a=(e,t)=>!n[t]||e.route.id!==n[t].route.id,s=(e,t)=>n[t].pathname!==e.pathname||n[t].route.path?.endsWith("*")&&n[t].params["*"]!==e.params["*"];return"assets"===i?t.filter(((e,t)=>a(e,t)||s(e,t))):"data"===i?t.filter(((t,i)=>{let l=r.routes[t.route.id];if(!l||!l.hasLoader)return!1;if(a(t,i)||s(t,i))return!0;if(t.route.shouldRevalidate){let r=t.route.shouldRevalidate({currentUrl:new URL(o.pathname+o.search+o.hash,window.origin),currentParams:n[0]?.params||{},nextUrl:new URL(e,window.origin),nextParams:t.params,defaultShouldRevalidate:!0});if("boolean"==typeof r)return r}return!0})):[]}function we(e,t,{includeHydrateFallback:n}={}){return r=e.map((e=>{let r=t.routes[e.route.id];if(!r)return[];let o=[r.module];return r.clientActionModule&&(o=o.concat(r.clientActionModule)),r.clientLoaderModule&&(o=o.concat(r.clientLoaderModule)),n&&r.hydrateFallbackModule&&(o=o.concat(r.hydrateFallbackModule)),r.imports&&(o=o.concat(r.imports)),o})).flat(1),[...new Set(r)];var r}Object.getOwnPropertyNames(Object.prototype).sort().join("\0"),"undefined"!=typeof window?window:"undefined"!=typeof globalThis&&globalThis,Symbol("SingleFetchRedirect");function be(){let e=t.useContext(D);return ve(e,"You must render this element inside a element"),e}function xe(){let e=t.useContext(B);return ve(e,"You must render this element inside a element"),e}t.Component;var ke=t.createContext(void 0);function _e(){let e=t.useContext(ke);return ve(e,"You must render this element inside a element"),e}function Se(e,t){return n=>{e&&e(n),n.defaultPrevented||t(n)}}function Ee({page:e,...n}){let{router:r}=be(),o=t.useMemo((()=>d(r.routes,e,r.basename)),[r.routes,e,r.basename]);return o?t.createElement(Pe,{page:e,matches:o,...n}):null}function Le(e){let{manifest:n,routeModules:r}=_e(),[o,i]=t.useState([]);return t.useEffect((()=>{let t=!1;return async function(e,t,n){return function(e,t){let n=new Set,r=new Set(t);return e.reduce(((e,o)=>{if(t&&(null==(i=o)||"string"!=typeof i.page)&&"script"===o.as&&o.href&&r.has(o.href))return e;var i;let a=JSON.stringify(function(e){let t={},n=Object.keys(e).sort();for(let r of n)t[r]=e[r];return t}(o));return n.has(a)||(n.add(a),e.push({key:a,link:o})),e}),[])}((await Promise.all(e.map((async e=>{let r=t.routes[e.route.id];if(r){let e=await async function(e,t){if(e.id in t)return t[e.id];try{let n=await import(e.module);return t[e.id]=n,n}catch(t){return console.error(`Error loading route module \`${e.module}\`, reloading page...`),console.error(t),window.__reactRouterContext&&window.__reactRouterContext.isSpaMode,window.location.reload(),new Promise((()=>{}))}}(r,n);return e.links?e.links():[]}return[]})))).flat(1).filter(me).filter((e=>"stylesheet"===e.rel||"preload"===e.rel)).map((e=>"stylesheet"===e.rel?{...e,rel:"prefetch",as:"style"}:{...e,rel:"prefetch"})))}(e,n,r).then((e=>{t||i(e)})),()=>{t=!0}}),[e,n,r]),o}function Pe({page:e,matches:n,...r}){let o=$(),{manifest:i,routeModules:a}=_e(),{basename:s}=be(),{loaderData:l,matches:c}=xe(),u=t.useMemo((()=>ye(e,n,c,i,o,"data")),[e,n,c,i,o]),d=t.useMemo((()=>ye(e,n,c,i,o,"assets")),[e,n,c,i,o]),p=t.useMemo((()=>{if(e===o.pathname+o.search+o.hash)return[];let t=new Set,r=!1;if(n.forEach((e=>{let n=i.routes[e.route.id];n&&n.hasLoader&&(!u.some((t=>t.route.id===e.route.id))&&e.route.id in l&&a[e.route.id]?.shouldRevalidate||n.hasClientLoader?r=!0:t.add(e.route.id))})),0===t.size)return[];let c=function(e,t){let n="string"==typeof e?new URL(e,"undefined"==typeof window?"server://singlefetch/":window.location.origin):e;return"/"===n.pathname?n.pathname="_root.data":t&&"/"===E(n.pathname,t)?n.pathname=`${t.replace(/\/$/,"")}/_root.data`:n.pathname=`${n.pathname.replace(/\/$/,"")}.data`,n}(e,s);return r&&t.size>0&&c.searchParams.set("_routes",n.filter((e=>t.has(e.route.id))).map((e=>e.route.id)).join(",")),[c.pathname+c.search]}),[s,l,o,i,u,n,e,a]),f=t.useMemo((()=>we(d,i)),[d,i]),h=Le(d);return t.createElement(t.Fragment,null,p.map((e=>t.createElement("link",{key:e,rel:"prefetch",as:"fetch",href:e,...r}))),f.map((e=>t.createElement("link",{key:e,rel:"modulepreload",href:e,...r}))),h.map((({key:e,link:n})=>t.createElement("link",{key:e,...n}))))}ke.displayName="FrameworkContext";function je(...e){return t=>{e.forEach((e=>{"function"==typeof e?e(t):null!=e&&(e.current=t)}))}}var Ce="undefined"!=typeof window&&void 0!==window.document&&void 0!==window.document.createElement;try{Ce&&(window.__reactRouterVersion="7.6.3")}catch(e){}function Te({basename:e,children:n,window:r}){let i=t.useRef();null==i.current&&(i.current=o({window:r,v5Compat:!0}));let a=i.current,[s,l]=t.useState({action:a.action,location:a.location}),c=t.useCallback((e=>{t.startTransition((()=>l(e)))}),[l]);return t.useLayoutEffect((()=>a.listen(c)),[a,c]),t.createElement(se,{basename:e,children:n,location:s.location,navigationType:s.action,navigator:a})}var Ne=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Ie=t.forwardRef((function({onClick:e,discover:n="render",prefetch:r="none",relative:o,reloadDocument:s,replace:l,state:u,target:d,to:p,preventScrollReset:f,viewTransition:h,...g},v){let m,{basename:y}=t.useContext(G),w="string"==typeof p&&Ne.test(p),b=!1;if("string"==typeof p&&w&&(m=p,Ce))try{let e=new URL(window.location.href),t=p.startsWith("//")?new URL(e.protocol+p):new URL(p),n=E(t.pathname,y);t.origin===e.origin&&null!=n?p=n+t.search+t.hash:b=!0}catch(e){a(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}let x=function(e,{relative:n}={}){i(V(),"useHref() may be used only in the context of a component.");let{basename:r,navigator:o}=t.useContext(G),{hash:a,pathname:s,search:l}=K(e,{relative:n}),c=s;return"/"!==r&&(c="/"===s?r:C([r,s])),o.createHref({pathname:c,search:l,hash:a})}(p,{relative:o}),[k,_,S]=function(e,n){let r=t.useContext(ke),[o,i]=t.useState(!1),[a,s]=t.useState(!1),{onFocus:l,onBlur:c,onMouseEnter:u,onMouseLeave:d,onTouchStart:p}=n,f=t.useRef(null);t.useEffect((()=>{if("render"===e&&s(!0),"viewport"===e){let e=new IntersectionObserver((e=>{e.forEach((e=>{s(e.isIntersecting)}))}),{threshold:.5});return f.current&&e.observe(f.current),()=>{e.disconnect()}}}),[e]),t.useEffect((()=>{if(o){let e=setTimeout((()=>{s(!0)}),100);return()=>{clearTimeout(e)}}}),[o]);let h=()=>{i(!0)},g=()=>{i(!1),s(!1)};return r?"intent"!==e?[a,f,{}]:[a,f,{onFocus:Se(l,h),onBlur:Se(c,g),onMouseEnter:Se(u,h),onMouseLeave:Se(d,g),onTouchStart:Se(p,h)}]:[!1,f,{}]}(r,g),L=function(e,{target:n,replace:r,state:o,preventScrollReset:i,relative:a,viewTransition:s}={}){let l=q(),u=$(),d=K(e,{relative:a});return t.useCallback((t=>{if(function(e,t){return!(0!==e.button||t&&"_self"!==t||function(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}(e))}(t,n)){t.preventDefault();let n=void 0!==r?r:c(u)===c(d);l(e,{replace:n,state:o,preventScrollReset:i,relative:a,viewTransition:s})}}),[u,l,d,r,o,n,e,i,a,s])}(p,{replace:l,state:u,target:d,preventScrollReset:f,relative:o,viewTransition:h}),P=t.createElement("a",{...g,...S,href:m||x,onClick:b||s?e:function(t){e&&e(t),t.defaultPrevented||L(t)},ref:je(v,_),target:d,"data-discover":w||"render"!==n?void 0:"true"});return k&&!w?t.createElement(t.Fragment,null,P,t.createElement(Ee,{page:x})):P}));function Re(e){let n=t.useContext(D);return i(n,function(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}(e)),n}Ie.displayName="Link",t.forwardRef((function({"aria-current":e="page",caseSensitive:n=!1,className:r="",end:o=!1,style:a,to:s,viewTransition:l,children:c,...u},d){let p=K(s,{relative:u.relative}),f=$(),h=t.useContext(B),{navigator:g,basename:v}=t.useContext(G),m=null!=h&&function(e,n={}){let r=t.useContext(M);i(null!=r,"`useViewTransitionState` must be used within `react-router-dom`'s `RouterProvider`. Did you accidentally import `RouterProvider` from `react-router`?");let{basename:o}=Re("useViewTransitionState"),a=K(e,{relative:n.relative});if(!r.isTransitioning)return!1;let s=E(r.currentLocation.pathname,o)||r.currentLocation.pathname,l=E(r.nextLocation.pathname,o)||r.nextLocation.pathname;return null!=_(a.pathname,l)||null!=_(a.pathname,s)}(p)&&!0===l,y=g.encodeLocation?g.encodeLocation(p).pathname:p.pathname,w=f.pathname,b=h&&h.navigation&&h.navigation.location?h.navigation.location.pathname:null;n||(w=w.toLowerCase(),b=b?b.toLowerCase():null,y=y.toLowerCase()),b&&v&&(b=E(b,v)||b);const x="/"!==y&&y.endsWith("/")?y.length-1:y.length;let k,S=w===y||!o&&w.startsWith(y)&&"/"===w.charAt(x),L=null!=b&&(b===y||!o&&b.startsWith(y)&&"/"===b.charAt(y.length)),P={isActive:S,isPending:L,isTransitioning:m},j=S?e:void 0;k="function"==typeof r?r(P):[r,S?"active":null,L?"pending":null,m?"transitioning":null].filter(Boolean).join(" ");let C="function"==typeof a?a(P):a;return t.createElement(Ie,{...u,"aria-current":j,className:k,ref:d,style:C,to:s,viewTransition:l},"function"==typeof c?c(P):c)})).displayName="NavLink",t.forwardRef((({discover:e="render",fetcherKey:n,navigate:r,reloadDocument:o,replace:a,state:s,method:l=ue,action:u,onSubmit:d,relative:p,preventScrollReset:f,viewTransition:h,...g},v)=>{let m=function(){let{router:e}=Re("useSubmit"),{basename:n}=t.useContext(G),r=ne("useRouteId");return t.useCallback((async(t,o={})=>{let{action:i,method:a,encType:s,formData:l,body:c}=function(e,t){let n,r,o,i,a;if(pe(s=e)&&"form"===s.tagName.toLowerCase()){let a=e.getAttribute("action");r=a?E(a,t):null,n=e.getAttribute("method")||ue,o=ge(e.getAttribute("enctype"))||de,i=new FormData(e)}else if(function(e){return pe(e)&&"button"===e.tagName.toLowerCase()}(e)||function(e){return pe(e)&&"input"===e.tagName.toLowerCase()}(e)&&("submit"===e.type||"image"===e.type)){let a=e.form;if(null==a)throw new Error('Cannot submit a