diff --git a/package-lock.json b/package-lock.json index f57ec8c8b..f5c49eecf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "lodash.debounce": "^4.0.8" }, "devDependencies": { - "@playwright/test": "^1.49.1", + "@playwright/test": "^1.50.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", @@ -3984,12 +3984,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", - "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz", + "integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright": "1.49.1" + "playwright": "1.50.0" }, "bin": { "playwright": "cli.js" @@ -23218,12 +23219,13 @@ } }, "node_modules/playwright": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", - "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz", + "integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.49.1" + "playwright-core": "1.50.0" }, "bin": { "playwright": "cli.js" @@ -23236,10 +23238,11 @@ } }, "node_modules/playwright-core": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", - "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz", + "integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==", "dev": true, + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, diff --git a/package.json b/package.json index d8ae81bf4..416c763ae 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "lodash.debounce": "^4.0.8" }, "devDependencies": { - "@playwright/test": "^1.49.1", + "@playwright/test": "^1.50.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@types/jest": "^29.5.14", diff --git a/src/UI/class-settings-page.php b/src/UI/class-settings-page.php index 766568de1..89d3c8792 100644 --- a/src/UI/class-settings-page.php +++ b/src/UI/class-settings-page.php @@ -543,7 +543,6 @@ function (): void { 'title' => __( 'Track Post Types as', 'wp-parsely' ), 'option_key' => $field_id, 'help_text' => $field_help, - 'filter' => 'wp_parsely_trackable_statuses', ) ); diff --git a/src/class-parsely.php b/src/class-parsely.php index f3eecee64..4f72fd06e 100644 --- a/src/class-parsely.php +++ b/src/class-parsely.php @@ -282,7 +282,8 @@ public function run(): void { update_option( self::OPTIONS_KEY, $options ); } - add_action( 'save_post', array( $this, 'update_metadata_endpoint' ) ); + // @phpstan-ignore return.void + add_action( 'save_post', array( $this, 'call_update_metadata_endpoint' ) ); } /** @@ -397,12 +398,6 @@ public function insert_page_header_metadata(): void { * By default,only 'publish' is allowed. */ public static function post_has_trackable_status( $post ): bool { - static $cache = array(); - $post_id = is_int( $post ) ? $post : $post->ID; - if ( isset( $cache[ $post_id ] ) ) { - return $cache[ $post_id ]; - } - /** * Filters whether the post password check should be skipped when getting * the post trackable status. @@ -416,13 +411,11 @@ public static function post_has_trackable_status( $post ): bool { */ $skip_password_check = apply_filters( 'wp_parsely_skip_post_password_check', false, $post ); if ( ! $skip_password_check && post_password_required( $post ) ) { - $cache[ $post_id ] = false; return false; } - $statuses = self::get_trackable_statuses( $post ); - $cache[ $post_id ] = in_array( get_post_status( $post ), $statuses, true ); - return $cache[ $post_id ]; + $statuses = self::get_trackable_statuses( $post ); + return in_array( get_post_status( $post ), $statuses, true ); } /** @@ -444,19 +437,48 @@ public function construct_parsely_metadata( array $parsely_options, WP_Post $pos } /** - * Updates the Parsely metadata endpoint with the new metadata of the post. + * Calls Parse.ly's update metadata endpoint, sending the post's updated + * metadata. * - * @param int $post_id id of the post to update. + * @param int $post_id The ID of the post to update. + * @return bool True if the metadata endpoint was called, false otherwise. */ - public function update_metadata_endpoint( int $post_id ): void { - $parsely_options = $this->get_options(); - if ( $this->site_id_is_missing() || '' === $parsely_options['metadata_secret'] ) { - return; + public function call_update_metadata_endpoint( int $post_id ): bool { + $options = $this->get_options(); + + if ( $this->site_id_is_missing() || '' === $options['metadata_secret'] ) { + return false; + } + + $current_post_type = get_post_type( $post_id ); + if ( false === $current_post_type ) { + return false; + } + + $tracked_post_types = array_merge( + $options['track_post_types'], + $options['track_page_types'] + ); + + // Check that the post's type is trackable. + if ( ! in_array( $current_post_type, $tracked_post_types, true ) ) { + return false; + } + + // Check that the post's status is trackable. + if ( ! self::post_has_trackable_status( $post_id ) ) { + return false; } $post = get_post( $post_id ); if ( null === $post ) { - return; + return false; + } + + // Don't call the endpoint when integration tests are running, but + // signal that the above checks have passed. + if ( defined( 'INTEGRATION_TESTS_RUNNING' ) ) { + return true; } $metadata = ( new Metadata( $this ) )->construct_metadata( $post ); @@ -474,7 +496,7 @@ public function update_metadata_endpoint( int $post_id ): void { $parsely_api_base_url = Content_API_Service::get_base_url(); $parsely_api_endpoint = $parsely_api_base_url . '/metadata/posts'; - $parsely_metadata_secret = $parsely_options['metadata_secret']; + $parsely_metadata_secret = $options['metadata_secret']; $headers = array( 'Content-Type' => 'application/json' ); $body = wp_json_encode( @@ -488,9 +510,9 @@ public function update_metadata_endpoint( int $post_id ): void { /** * POST request options. * - * @var WP_HTTP_Request_Args $options + * @var WP_HTTP_Request_Args $request_options */ - $options = array( + $request_options = array( 'method' => 'POST', 'headers' => $headers, 'blocking' => false, @@ -498,12 +520,15 @@ public function update_metadata_endpoint( int $post_id ): void { 'data_format' => 'body', ); - $response = wp_remote_post( $parsely_api_endpoint, $options ); + $response = wp_remote_post( $parsely_api_endpoint, $request_options ); - if ( ! is_wp_error( $response ) ) { - $current_timestamp = time(); - update_post_meta( $post_id, 'parsely_metadata_last_updated', $current_timestamp ); + if ( is_wp_error( $response ) ) { + return false; } + + update_post_meta( $post_id, 'parsely_metadata_last_updated', time() ); + + return true; } /** diff --git a/tests/Integration/CallUpdateMetadataEndpointTest.php b/tests/Integration/CallUpdateMetadataEndpointTest.php new file mode 100644 index 000000000..f166de288 --- /dev/null +++ b/tests/Integration/CallUpdateMetadataEndpointTest.php @@ -0,0 +1,337 @@ + 'test' ) ); + + self::$parsely = new Parsely(); + } + + /** + * Verifies that the endpoint is not being called when credentials are + * insufficient. + * + * @since 3.18.0 + * + * @covers \Parsely\Parsely::call_update_metadata_endpoint + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @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::set_managed_options + * @uses \Parsely\Parsely::site_id_is_missing + * @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\Services\Content_API\Content_API_Service::get_base_url + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::get_base_url + * + * @param array $options The options to set. + * + * @dataProvider provide_data_for_test_endpoint_is_not_being_called_when_credentials_are_insufficient + */ + public function test_endpoint_is_not_being_called_when_credentials_are_insufficient( array $options ): void { + self::set_options( $options ); + + /** @var int $post_id */ + $post_id = self::factory()->post->create(); + + self::assertFalse( self::$parsely->call_update_metadata_endpoint( $post_id ) ); + } + + /** + * Provides data for test_endpoint_is_not_being_called_when_credentials_are_insufficient(). + * + * @since 3.18.0 + * + * @return array>> + */ + public function provide_data_for_test_endpoint_is_not_being_called_when_credentials_are_insufficient(): array { + return array( + 'no Site ID and Metadata Secret' => array( + 'options' => array( + 'apikey' => '', + 'metadata_secret' => '', + ), + ), + 'no Site ID' => array( + 'options' => array( + 'apikey' => '', + 'metadata_secret' => 'test', + ), + ), + 'no Metadata Secret' => array( + 'options' => array( + 'apikey' => 'test', + 'metadata_secret' => '', + ), + ), + ); + } + + /** + * Verifies that endpoint calls work as expected with default post types. + * + * @since 3.18.0 + * + * @covers \Parsely\Parsely::call_update_metadata_endpoint + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_trackable_statuses + * @uses \Parsely\Parsely::post_has_trackable_status + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_missing + * @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\Services\Content_API\Content_API_Service::get_base_url + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::get_base_url + * + * @param string $post_type The post type to create and test against. + * @param bool $expected Whether the endpoint should be called. + * + * @dataProvider provide_data_for_test_updates_metadata_endpoint_calls_work_as_expected_with_default_post_types + */ + public function test_endpoint_calls_work_as_expected_with_default_post_types( string $post_type, bool $expected ): void { + /** @var int $post_id */ + $post_id = self::factory()->post->create( array( 'post_type' => $post_type ) ); + + self::assertSame( $expected, self::$parsely->call_update_metadata_endpoint( $post_id ) ); + } + + /** + * Provides data for test_updates_metadata_endpoint_calls_work_as_expected_with_default_post_types(). + * + * @since 3.18.0 + * + * @return array> + */ + public function provide_data_for_test_updates_metadata_endpoint_calls_work_as_expected_with_default_post_types(): array { + return array( + '"post" type' => array( + 'post_type' => 'post', + 'expected' => true, + ), + '"page" type' => array( + 'post_type' => 'page', + 'expected' => true, + ), + '"attachment" type' => array( + 'post_type' => 'attachment', + 'expected' => false, + ), + '"revision" type' => array( + 'post_type' => 'revision', + 'expected' => false, + ), + ); + } + + /** + * Verifies that the endpoint isn't being called when the ID of a + * nonexistent post gets passed to the function. + * + * @since 3.18.0 + * + * @covers \Parsely\Parsely::call_update_metadata_endpoint + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @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::set_managed_options + * @uses \Parsely\Parsely::site_id_is_missing + * @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\Services\Content_API\Content_API_Service::get_base_url + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::get_base_url + */ + public function test_endpoint_is_not_being_called_when_post_does_not_exist(): void { + self::assertFalse( self::$parsely->call_update_metadata_endpoint( 0 ) ); + } + + /** + * Verifies that endpoint calls work as expected with custom post types. + * + * @since 3.18.0 + * + * @covers \Parsely\Parsely::call_update_metadata_endpoint + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_trackable_statuses + * @uses \Parsely\Parsely::post_has_trackable_status + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_missing + * @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\Services\Content_API\Content_API_Service::get_base_url + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::get_base_url + */ + public function test_endpoint_calls_work_as_expected_with_custom_post_types(): void { + register_post_type( 'test_cpt', array( 'public' => true ) ); + + /** @var int $post_id */ + $post_id = self::factory()->post->create( array( 'post_type' => 'test_cpt' ) ); + + self::assertFalse( self::$parsely->call_update_metadata_endpoint( $post_id ) ); + + // Add the custom post type to the list of tracked post types. + self::set_options( + array( + 'metadata_secret' => 'test', + 'track_post_types' => array( 'test_cpt' ), + ) + ); + + self::assertTrue( self::$parsely->call_update_metadata_endpoint( $post_id ) ); + + unregister_post_type( 'test_cpt' ); + } + + /** + * Verifies that endpoint calls work as expected for posts with trackable + * and non-trackable post statuses. + * + * @since 3.18.0 + * + * @covers \Parsely\Parsely::call_update_metadata_endpoint + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_trackable_statuses + * @uses \Parsely\Parsely::post_has_trackable_status + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_missing + * @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\Services\Content_API\Content_API_Service::get_base_url + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::get_base_url + */ + public function test_endpoint_calls_work_as_expected_with_post_statuses(): void { + /** @var int $post_id */ + $post_id = self::factory()->post->create( array( 'post_status' => 'draft' ) ); + + self::assertFalse( self::$parsely->call_update_metadata_endpoint( $post_id ) ); + + // Update to a trackable post status. + wp_update_post( + array( + 'ID' => $post_id, + 'post_status' => 'publish', + ) + ); + + self::assertTrue( self::$parsely->call_update_metadata_endpoint( $post_id ) ); + } + + /** + * Verifies that endpoint calls work as expected for posts with custom post + * statuses. + * + * @since 3.18.0 + * + * @covers \Parsely\Parsely::call_update_metadata_endpoint + * @uses \Parsely\Parsely::__construct + * @uses \Parsely\Parsely::allow_parsely_remote_requests + * @uses \Parsely\Parsely::are_credentials_managed + * @uses \Parsely\Parsely::get_managed_credentials + * @uses \Parsely\Parsely::get_options + * @uses \Parsely\Parsely::get_trackable_statuses + * @uses \Parsely\Parsely::post_has_trackable_status + * @uses \Parsely\Parsely::set_default_content_helper_settings_values + * @uses \Parsely\Parsely::set_default_full_metadata_in_non_posts + * @uses \Parsely\Parsely::set_managed_options + * @uses \Parsely\Parsely::site_id_is_missing + * @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\Services\Content_API\Content_API_Service::get_base_url + * @uses \Parsely\Services\Suggestions_API\Suggestions_API_Service::get_base_url + */ + public function test_endpoint_calls_work_as_expected_with_custom_post_statuses(): void { + /** @var int $post_id */ + $post_id = self::factory()->post->create( array( 'post_status' => 'active' ) ); + + self::assertFalse( self::$parsely->call_update_metadata_endpoint( $post_id ) ); + + add_filter( + 'wp_parsely_trackable_statuses', + array( $this, 'add_active_status_to_trackable_post_statuses' ), + 10, + 2 + ); + + self::assertTrue( self::$parsely->call_update_metadata_endpoint( $post_id ) ); + + remove_filter( + 'wp_parsely_trackable_statuses', + array( $this, 'add_active_status_to_trackable_post_statuses' ), + 10 + ); + } + + /** + * Adds an "active" post status to the array of trackable post statuses. + * + * @since 3.18.0 + * + * @param array $statuses The array of trackable post statuses. + * @param WP_Post|int|null $post The post being checked. + * @return array The updated array of trackable post statuses. + */ + public function add_active_status_to_trackable_post_statuses( array $statuses, $post ): array { + $statuses[] = 'active'; + + return $statuses; + } +}