diff --git a/admin/apple-actions/index/class-get.php b/admin/apple-actions/index/class-get.php index e1c64895..c4ccbabf 100644 --- a/admin/apple-actions/index/class-get.php +++ b/admin/apple-actions/index/class-get.php @@ -52,16 +52,7 @@ public function perform() { } // Get the article from the API. - try { - $article = $this->get_api()->get_article( $apple_id ); - } catch ( \Apple_Push_API\Request\Request_Exception $e ) { - $article = $e->getMessage(); - - // Reset the API postmeta if the article is deleted in Apple News. - if ( is_string( $article ) && str_contains( $article, 'NOT_FOUND (keyPath articleId)' ) ) { - $this->delete_post_meta( $this->id ); - } - } + $article = $this->get_api()->get_article( $apple_id ); if ( empty( $article->data ) ) { return null; diff --git a/admin/apple-actions/index/class-push.php b/admin/apple-actions/index/class-push.php index bed4261a..2f7782a0 100644 --- a/admin/apple-actions/index/class-push.php +++ b/admin/apple-actions/index/class-push.php @@ -14,10 +14,10 @@ use Admin_Apple_Async; use Admin_Apple_Notice; use Admin_Apple_Sections; -use Apple_Actions\Action_Exception; -use Apple_Actions\API_Action; use Apple_Exporter\Exporter; use Apple_Exporter\Settings; +use Apple_Actions\API_Action; +use Apple_Actions\Action_Exception; use Apple_Push_API\Request\Request_Exception; /** @@ -212,11 +212,12 @@ private function get(): void { /** * Push the post using the API data. * - * @param int $user_id Optional. The ID of the user performing the push. Defaults to current user. + * @param int $user_id Optional. The ID of the user performing the push. Defaults to current user. + * @param bool $display_notices Optional. Whether to display notices. Defaults to true. * * @throws Action_Exception If unable to push. */ - private function push( $user_id = null ): void { + private function push( $user_id = null, $display_notices = true ): void { if ( ! $this->is_api_configuration_valid() ) { throw new Action_Exception( esc_html__( 'Your Apple News API settings seem to be empty. Please fill in the API key, API secret and API channel fields in the plugin configuration page.', 'apple-news' ) ); } @@ -387,6 +388,9 @@ private function push( $user_id = null ): void { ); } + $original_error_message = null; + $error_message = null; + try { if ( $remote_id ) { // Update the current article from the API in case the revision changed. @@ -450,40 +454,61 @@ private function push( $user_id = null ): void { } else { $error_message = __( 'There has been an error with the Apple News API: ', 'apple-news' ) . esc_html( $original_error_message ); } - + } finally { /** - * Actions to be taken after an article failed to be pushed to Apple News. + * Reindex the article if it was deleted in the iCloud News Publisher dashboard. * - * @param int $post_id The ID of the post. - * @param string $original_error_message The original error message. + * @see https://github.com/alleyinteractive/apple-news/issues/1154 */ - do_action( 'apple_news_after_push_failure', $this->id, $original_error_message ); + if ( $original_error_message && str_contains( $original_error_message, 'NOT_FOUND (keyPath articleId)' ) ) { + try { + self::push( + user_id: $user_id, + display_notices: false + ); + } catch ( Action_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + // Do nothing, even if the second push fails. + } + } + + if ( $error_message ) { + /** + * Actions to be taken after an article failed to be pushed to Apple News. + * + * @param int $post_id The ID of the post. + * @param string|null $original_error_message The original error message, if available. + * @param string $error_message The error message to be displayed. + */ + do_action( 'apple_news_after_push_failure', $this->id, $original_error_message, $error_message ); + + throw new Action_Exception( esc_html( $error_message ) ); + } + } - throw new Action_Exception( esc_html( $error_message ) ); + // If we're not supposed to display notices, bail out. + if ( false === $display_notices ) { + return; } // Print success message. $post = get_post( $this->id ); + + $success_message = sprintf( + // translators: token is the post title. + __( 'Article %s has been pushed successfully to Apple News!', 'apple-news' ), + $post->post_title + ); + if ( $remote_id ) { - Admin_Apple_Notice::success( - sprintf( + $success_message = sprintf( // translators: token is the post title. - __( 'Article %s has been successfully updated on Apple News!', 'apple-news' ), - $post->post_title - ), - $user_id - ); - } else { - Admin_Apple_Notice::success( - sprintf( - // translators: token is the post title. - __( 'Article %s has been pushed successfully to Apple News!', 'apple-news' ), - $post->post_title - ), - $user_id + __( 'Article %s has been successfully updated on Apple News!', 'apple-news' ), + $post->post_title ); } + Admin_Apple_Notice::success( $success_message, $user_id ); + $this->clean_workspace(); } diff --git a/admin/settings/class-admin-apple-settings-section-developer-tools.php b/admin/settings/class-admin-apple-settings-section-developer-tools.php index 77db19a5..7e372794 100644 --- a/admin/settings/class-admin-apple-settings-section-developer-tools.php +++ b/admin/settings/class-admin-apple-settings-section-developer-tools.php @@ -41,10 +41,11 @@ public function __construct( $page ) { 'type' => [ 'no', 'yes' ], ], 'apple_news_admin_email' => [ - 'label' => __( 'Administrator Email', 'apple-news' ), + 'label' => __( 'Email(s)', 'apple-news' ), 'required' => false, - 'type' => 'string', + 'type' => 'email', 'size' => 40, + 'multiple' => true, ], ]; @@ -70,7 +71,7 @@ public function __construct( $page ) { */ public function get_section_info() { return __( - 'If debugging is enabled, emails will be sent to an administrator for every publish, update or delete action with a detailed API response.', + 'If debugging is enabled (and valid emails are provided), emails will be sent for every publish, update or delete action with a detailed API response.', 'apple-news' ); } diff --git a/admin/settings/class-admin-apple-settings-section.php b/admin/settings/class-admin-apple-settings-section.php index 5ad1c7d6..a96aab80 100644 --- a/admin/settings/class-admin-apple-settings-section.php +++ b/admin/settings/class-admin-apple-settings-section.php @@ -126,6 +126,7 @@ class Admin_Apple_Settings_Section extends Apple_News { 'max' => [], 'step' => [], 'type' => [], + 'multiple' => [], 'required' => [], 'size' => [], 'id' => [], @@ -324,6 +325,14 @@ public function render_field( $args ) { $field = ''; } elseif ( 'number' === $type ) { $field = ''; + } elseif ( 'email' === $type ) { + $field = 'is_multiple( $name ) ) { + $field .= ' multiple %s>'; + } else { + $field .= ' %s>'; + } } else { // If nothing else matches, it's a string. $field = ''; @@ -403,9 +412,9 @@ public function render_field( $args ) { protected function get_type_for( $name ) { if ( $this->hidden ) { return 'hidden'; - } else { - return empty( $this->settings[ $name ]['type'] ) ? 'string' : $this->settings[ $name ]['type']; } + + return empty( $this->settings[ $name ]['type'] ) ? 'string' : $this->settings[ $name ]['type']; } /** @@ -643,5 +652,13 @@ public function save_settings() { // Save to options. update_option( self::$section_option_name, $settings, 'no' ); + + /** + * Update the cached settings with new one after an update. + * + * The `self::get_value` method uses this cached data. By resetting it, we ensure + * that the new value is used after an update instead of the old value. + */ + self::$loaded_settings = $settings; } } diff --git a/includes/REST/apple-news-delete.php b/includes/REST/apple-news-delete.php index 3be0bdd5..f6a99811 100644 --- a/includes/REST/apple-news-delete.php +++ b/includes/REST/apple-news-delete.php @@ -9,17 +9,8 @@ use WP_Error; use WP_REST_Request; - -/** - * Handle a REST POST request to the /apple-news/v1/delete endpoint. - * - * @param WP_REST_Request $data Data from query args. - * - * @return array|WP_Error Response to the request - either data about a successfully deleted article, or error. - */ -function rest_post_delete( $data ) { - return modify_post( (int) $data->get_param( 'id' ), 'delete' ); -} +use WP_REST_Response; +use WP_REST_Server; /** * Initialize this REST Endpoint. @@ -27,15 +18,30 @@ function rest_post_delete( $data ) { add_action( 'rest_api_init', function () { - // Register route count argument. register_rest_route( 'apple-news/v1', '/delete', [ - 'methods' => 'POST', + 'methods' => WP_REST_Server::CREATABLE, 'callback' => __NAMESPACE__ . '\rest_post_delete', 'permission_callback' => '__return_true', ] ); } ); + +/** + * Handle a REST POST request to the /apple-news/v1/delete endpoint. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ +function rest_post_delete( $request ): WP_REST_Response|WP_Error { + $post = modify_post( (int) $request->get_param( 'id' ), 'delete' ); + + if ( is_wp_error( $post ) ) { + return $post; + } + + return rest_ensure_response( $post ); +} diff --git a/includes/REST/apple-news-get-published-state.php b/includes/REST/apple-news-get-published-state.php index 35ef147e..32603dec 100644 --- a/includes/REST/apple-news-get-published-state.php +++ b/includes/REST/apple-news-get-published-state.php @@ -44,7 +44,7 @@ function () { * Get the published state of a post. * * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + * @return WP_REST_Response|WP_Error */ function get_published_state_response( $request ): WP_REST_Response|WP_Error { $id = $request->get_param( 'id' ); diff --git a/includes/REST/apple-news-get-settings.php b/includes/REST/apple-news-get-settings.php index fedaeff4..ade4fcc7 100644 --- a/includes/REST/apple-news-get-settings.php +++ b/includes/REST/apple-news-get-settings.php @@ -1,6 +1,6 @@ WP_REST_Server::READABLE, + 'callback' => __NAMESPACE__ . '\get_settings_response', + 'permission_callback' => '__return_true', + ] + ); + } +); /** * Get API response. * - * @param array $data data from query args. - * @return array updated response. + * @return WP_REST_Response|WP_Error */ -function get_settings_response( $data ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found +function get_settings_response(): WP_REST_Response|WP_Error { + // Ensure Apple News is first initialized. - \Apple_News::has_uninitialized_error(); + $retval = \Apple_News::has_uninitialized_error(); + + if ( is_wp_error( $retval ) ) { + return $retval; + } if ( empty( get_current_user_id() ) ) { - return []; + return rest_ensure_response( [] ); } // Compile non-sensitive plugin settings into a JS-friendly format and return. $admin_settings = new \Admin_Apple_Settings(); $settings = $admin_settings->fetch_settings(); $default_settings = ( new Settings() )->all(); - return [ + + $response = [ 'adminUrl' => esc_url_raw( admin_url( 'admin.php?page=apple-news-options' ) ), 'automaticAssignment' => ! empty( Automation::get_automation_rules() ), 'apiAsync' => 'yes' === $settings->api_async, @@ -42,23 +68,6 @@ function get_settings_response( $data ) { // phpcs:ignore Generic.CodeAnalysis.U 'showMetabox' => 'yes' === $settings->show_metabox, 'useRemoteImages' => 'yes' === $settings->use_remote_images, ]; -} -/** - * Initialize this REST Endpoint. - */ -add_action( - 'rest_api_init', - function () { - // Register route count argument. - register_rest_route( - 'apple-news/v1', - '/get-settings', - [ - 'methods' => 'GET', - 'callback' => __NAMESPACE__ . '\get_settings_response', - 'permission_callback' => '__return_true', - ] - ); - } -); + return rest_ensure_response( $response ); +} diff --git a/includes/REST/apple-news-modify-post.php b/includes/REST/apple-news-modify-post.php index f8cdb840..f1ac20a9 100644 --- a/includes/REST/apple-news-modify-post.php +++ b/includes/REST/apple-news-modify-post.php @@ -23,9 +23,13 @@ * * @return array|WP_Error Response to the request - either data about a successful operation, or error. */ -function modify_post( $post_id, $operation ) { +function modify_post( $post_id, $operation ): array|WP_Error { // Ensure Apple News is first initialized. - \Apple_News::has_uninitialized_error(); + $retval = \Apple_News::has_uninitialized_error(); + + if ( is_wp_error( $retval ) ) { + return $retval; + } // Ensure there is a post ID provided in the data. if ( empty( $post_id ) ) { @@ -85,6 +89,7 @@ function modify_post( $post_id, $operation ) { ] ); } + try { $action->perform(); diff --git a/includes/REST/apple-news-publish.php b/includes/REST/apple-news-publish.php index ea609506..762a99cb 100644 --- a/includes/REST/apple-news-publish.php +++ b/includes/REST/apple-news-publish.php @@ -9,17 +9,8 @@ use WP_Error; use WP_REST_Request; - -/** - * Handle a REST POST request to the /apple-news/v1/publish endpoint. - * - * @param WP_REST_Request $data Data from query args. - * - * @return array|WP_Error Response to the request - either data about a successfully published article, or error. - */ -function rest_post_publish( $data ) { - return modify_post( (int) $data->get_param( 'id' ), 'publish' ); -} +use WP_REST_Response; +use WP_REST_Server; /** * Initialize this REST Endpoint. @@ -27,15 +18,30 @@ function rest_post_publish( $data ) { add_action( 'rest_api_init', function () { - // Register route count argument. register_rest_route( 'apple-news/v1', '/publish', [ - 'methods' => 'POST', + 'methods' => WP_REST_Server::CREATABLE, 'callback' => __NAMESPACE__ . '\rest_post_publish', 'permission_callback' => '__return_true', ] ); } ); + +/** + * Handle a REST POST request to the /apple-news/v1/publish endpoint. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ +function rest_post_publish( $request ): WP_REST_Response|WP_Error { + $post = modify_post( (int) $request->get_param( 'id' ), 'publish' ); + + if ( is_wp_error( $post ) ) { + return $post; + } + + return rest_ensure_response( $post ); +} diff --git a/includes/REST/apple-news-sections.php b/includes/REST/apple-news-sections.php index 5e959019..113bc5c3 100644 --- a/includes/REST/apple-news-sections.php +++ b/includes/REST/apple-news-sections.php @@ -7,14 +7,40 @@ namespace Apple_News\REST; +use WP_Error; +use WP_REST_Response; +use WP_REST_Server; + +/** + * Initialize this REST Endpoint. + */ +add_action( + 'rest_api_init', + function () { + register_rest_route( + 'apple-news/v1', + '/sections', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => __NAMESPACE__ . '\get_sections_response', + 'permission_callback' => '__return_true', + ] + ); + } +); + /** * Get API response. * - * @return array An array of information about sections. + * @return WP_REST_Response|WP_Error */ -function get_sections_response() { +function get_sections_response(): WP_REST_Response|WP_Error { // Ensure Apple News is first initialized. - \Apple_News::has_uninitialized_error(); + $retval = \Apple_News::has_uninitialized_error(); + + if ( is_wp_error( $retval ) ) { + return $retval; + } $sections = \Admin_Apple_Sections::get_sections(); $response = []; @@ -28,24 +54,5 @@ function get_sections_response() { } } - return $response; + return rest_ensure_response( $response ); } - -/** - * Initialize this REST Endpoint. - */ -add_action( - 'rest_api_init', - function () { - // Register route count argument. - register_rest_route( - 'apple-news/v1', - '/sections', - [ - 'methods' => 'GET', - 'callback' => __NAMESPACE__ . '\get_sections_response', - 'permission_callback' => '__return_true', - ] - ); - } -); diff --git a/includes/REST/apple-news-update.php b/includes/REST/apple-news-update.php index 49c4e6ee..9cc1ed55 100644 --- a/includes/REST/apple-news-update.php +++ b/includes/REST/apple-news-update.php @@ -9,17 +9,8 @@ use WP_Error; use WP_REST_Request; - -/** - * Handle a REST POST request to the /apple-news/v1/update endpoint. - * - * @param WP_REST_Request $data Data from query args. - * - * @return array|WP_Error Response to the request - either data about a successfully updated article, or error. - */ -function rest_post_update( $data ) { - return modify_post( (int) $data->get_param( 'id' ), 'update' ); -} +use WP_REST_Response; +use WP_REST_Server; /** * Initialize this REST Endpoint. @@ -27,15 +18,30 @@ function rest_post_update( $data ) { add_action( 'rest_api_init', function () { - // Register route count argument. register_rest_route( 'apple-news/v1', '/update', [ - 'methods' => 'POST', + 'methods' => WP_REST_Server::CREATABLE, 'callback' => __NAMESPACE__ . '\rest_post_update', 'permission_callback' => '__return_true', ] ); } ); + +/** + * Handle a REST POST request to the /apple-news/v1/update endpoint. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ +function rest_post_update( $request ): WP_REST_Response|WP_Error { + $post = modify_post( (int) $request->get_param( 'id' ), 'update' ); + + if ( is_wp_error( $post ) ) { + return $post; + } + + return rest_ensure_response( $post ); +} diff --git a/includes/REST/apple-news-user-can-publish.php b/includes/REST/apple-news-user-can-publish.php index c7393103..8310123d 100644 --- a/includes/REST/apple-news-user-can-publish.php +++ b/includes/REST/apple-news-user-can-publish.php @@ -8,34 +8,57 @@ namespace Apple_News\REST; use Apple_News; +use WP_Error; +use WP_REST_Response; +use WP_REST_Server; + +/** + * Initialize this REST Endpoint. + */ +add_action( + 'rest_api_init', + function () { + register_rest_route( + 'apple-news/v1', + '/user-can-publish/(?P\d+)', + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => __NAMESPACE__ . '\get_user_can_publish', + 'permission_callback' => '__return_true', + ] + ); + } +); /** * Get API response. * - * @param array $args Args present in the URL. - * - * @return array An array that contains the key 'userCanPublish' which is true if the user can publish, false if not. + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error */ -function get_user_can_publish( $args ) { +function get_user_can_publish( $request ): WP_REST_Response|WP_Error { + // Ensure Apple News is first initialized. + $retval = Apple_News::has_uninitialized_error(); + + if ( is_wp_error( $retval ) ) { + return $retval; + } // Ensure there is a post ID provided in the data. - $id = ! empty( $args['id'] ) ? (int) $args['id'] : 0; + $id = (int) $request->get_param( 'id' ); + if ( empty( $id ) ) { - return [ - 'userCanPublish' => false, - ]; + return rest_ensure_response( [ 'userCanPublish' => false ] ); } // Try to get the post by ID. $post = get_post( $id ); if ( empty( $post ) ) { - return [ - 'userCanPublish' => false, - ]; + return rest_ensure_response( [ 'userCanPublish' => false ] ); } // Ensure the user is authorized to make changes to Apple News posts. - return [ + $response = [ 'userCanPublish' => current_user_can( /** This filter is documented in admin/class-admin-apple-post-sync.php */ apply_filters( @@ -44,23 +67,6 @@ function get_user_can_publish( $args ) { ) ), ]; -} -/** - * Initialize this REST Endpoint. - */ -add_action( - 'rest_api_init', - function () { - // Register route count argument. - register_rest_route( - 'apple-news/v1', - '/user-can-publish/(?P\d+)', - [ - 'methods' => 'GET', - 'callback' => __NAMESPACE__ . '\get_user_can_publish', - 'permission_callback' => '__return_true', - ] - ); - } -); + return rest_ensure_response( $response ); +} diff --git a/includes/apple-push-api/request/class-request.php b/includes/apple-push-api/request/class-request.php index 844904cb..ee44a57d 100644 --- a/includes/apple-push-api/request/class-request.php +++ b/includes/apple-push-api/request/class-request.php @@ -171,10 +171,17 @@ private function parse_response( $response, $json = true, $type = 'post', $meta && 'yes' === $settings['apple_news_enable_debugging'] && 'get' !== $type ) { - // Get the admin email. - $admin_email = filter_var( $settings['apple_news_admin_email'], FILTER_VALIDATE_EMAIL ); + $emails = $settings['apple_news_admin_email'] ?? ''; - if ( empty( $admin_email ) ) { + if ( str_contains( $emails, ',' ) ) { + $emails = array_map( 'trim', explode( ',', $emails ) ); + } else { + $emails = [ $emails ]; + } + + $to = array_filter( $emails, 'is_email' ); + + if ( empty( $to ) ) { return; // TODO Fix inconsistent return value. } @@ -218,7 +225,7 @@ private function parse_response( $response, $json = true, $type = 'post', $meta // Send the email. if ( ! empty( $body ) ) { wp_mail( // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail - $admin_email, + $to, esc_html__( 'Apple News Notification', 'apple-news' ), $body, $headers diff --git a/tests/admin/apple-actions/index/test-class-get.php b/tests/admin/apple-actions/index/test-class-get.php index 5164e95f..a10bc914 100644 --- a/tests/admin/apple-actions/index/test-class-get.php +++ b/tests/admin/apple-actions/index/test-class-get.php @@ -57,43 +57,4 @@ public function test_get_action(): void { $this->assertSame( $response['data']['revision'], $data->data->revision ); $this->assertSame( $response['data']['type'], $data->data->type ); } - - /** - * Test the behavior of the get action with a deleted Apple News article assigned to the post. - */ - public function test_get_deleted_article(): void { - $api_id = 'def456'; - $post_id = self::factory()->post->create(); - $action = new Apple_Actions\Index\Get( $this->settings, $post_id ); - - $this->assertNull( $action->perform() ); - - add_post_meta( $post_id, 'apple_news_api_id', $api_id ); - - // Fake the API response for the GET request. - $this->add_http_response( - verb: 'GET', - url: 'https://news-api.apple.com/articles/' . $api_id, - body: wp_json_encode( - [ - 'errors' => [ - [ - 'code' => 'NOT_FOUND', - 'keyPath' => [ 'articleId' ], - 'value' => $api_id, - ], - ], - ] - ), - response: [ - 'code' => 404, - 'message' => 'Not Found', - ] - ); - - $action = new Apple_Actions\Index\Get( $this->settings, $post_id ); - - $this->assertNull( $action->perform() ); - $this->assertEmpty( get_post_meta( $post_id, 'apple_news_api_id', true ) ); - } } diff --git a/tests/rest/test-class-rest-post-published-state.php b/tests/rest/test-class-rest-post-published-state.php index 183dcff3..944fbdaf 100644 --- a/tests/rest/test-class-rest-post-published-state.php +++ b/tests/rest/test-class-rest-post-published-state.php @@ -92,13 +92,12 @@ public function test_get_post_published_state_of_an_invalid_id_when_authenticate $this->get( rest_url( '/apple-news/v1/get-published-state/' . $post_id ) ) ->assertOk() - ->assertJsonPath( 'publishState', 'N/A' ); + ->assertJsonPath( 'publishState', 'NOT_FOUND (keyPath articleId)' ); - // Ensure that the API postmeta _was_ reset. - $this->assertEmpty( get_post_meta( $post_id, 'apple_news_api_created_at', true ) ); - $this->assertEmpty( get_post_meta( $post_id, 'apple_news_api_id', true ) ); - $this->assertEmpty( get_post_meta( $post_id, 'apple_news_api_modified_at', true ) ); - $this->assertEmpty( get_post_meta( $post_id, 'apple_news_api_revision', true ) ); - $this->assertEmpty( get_post_meta( $post_id, 'apple_news_api_share_url', true ) ); + $this->assertEquals( 'abc123', get_post_meta( $post_id, 'apple_news_api_created_at', true ) ); + $this->assertEquals( $api_id, get_post_meta( $post_id, 'apple_news_api_id', true ) ); + $this->assertEquals( 'ghi789', get_post_meta( $post_id, 'apple_news_api_modified_at', true ) ); + $this->assertEquals( 'jkl123', get_post_meta( $post_id, 'apple_news_api_revision', true ) ); + $this->assertEquals( 'mno456', get_post_meta( $post_id, 'apple_news_api_share_url', true ) ); } }