diff --git a/admin/apple-actions/class-api-action.php b/admin/apple-actions/class-api-action.php index fce96fbe..e73787e8 100644 --- a/admin/apple-actions/class-api-action.php +++ b/admin/apple-actions/class-api-action.php @@ -100,5 +100,6 @@ protected function delete_post_meta( $post_id ): void { delete_post_meta( $post_id, 'apple_news_api_created_at' ); delete_post_meta( $post_id, 'apple_news_api_modified_at' ); delete_post_meta( $post_id, 'apple_news_api_share_url' ); + delete_post_meta( $post_id, 'apple_news_article_checksum' ); } } diff --git a/admin/class-admin-apple-bulk-export-page.php b/admin/class-admin-apple-bulk-export-page.php index 669ebf01..91eba186 100644 --- a/admin/class-admin-apple-bulk-export-page.php +++ b/admin/class-admin-apple-bulk-export-page.php @@ -41,8 +41,8 @@ public function __construct( $settings ) { add_action( 'admin_menu', [ $this, 'register_page' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'register_assets' ] ); - add_action( 'wp_ajax_push_post', [ $this, 'ajax_push_post' ] ); - add_filter( 'admin_title', [ $this, 'set_title' ] ); + add_action( 'wp_ajax_apple_news_push_post', [ $this, 'ajax_push_post' ] ); + add_action( 'wp_ajax_apple_news_delete_post', [ $this, 'ajax_delete_post' ] ); } /** @@ -66,45 +66,52 @@ public function register_page() { ); } - /** - * Fix the title since WordPress doesn't set one. - * - * @param string $admin_title The title to be filtered. - * @access public - * @return string The title for the screen. - */ - public function set_title( $admin_title ) { - $screen = get_current_screen(); - if ( 'admin_page_apple_news_bulk_export' === $screen->base ) { - $admin_title = __( 'Bulk Export', 'apple-news' ) . $admin_title; - } - - return $admin_title; - } - /** * Builds the plugin submenu page. * * @access public */ public function build_page() { - $ids = isset( $_GET['ids'] ) ? sanitize_text_field( wp_unslash( $_GET['ids'] ) ) : null; // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.NonceVerification.Recommended - if ( ! $ids ) { + $post_ids = isset( $_GET['post_ids'] ) ? sanitize_text_field( wp_unslash( $_GET['post_ids'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( ! $post_ids ) { wp_safe_redirect( esc_url_raw( menu_page_url( $this->plugin_slug . '_index', false ) ) ); // phpcs:ignore WordPressVIPMinimum.Security.ExitAfterRedirect.NoExit + if ( ! defined( 'APPLE_NEWS_UNIT_TESTS' ) || ! APPLE_NEWS_UNIT_TESTS ) { exit; } } - // Populate $articles array with a set of valid posts. - $articles = []; - foreach ( explode( '.', $ids ) as $id ) { - $post = get_post( absint( $id ) ); + // Allow only specific actions. + if ( ! in_array( $action, [ 'apple_news_push_post', 'apple_news_delete_post' ], true ) ) { + wp_die( esc_html__( 'Invalid action.', 'apple-news' ), '', [ 'response' => 400 ] ); + } + + // Populate articles array with a set of valid posts. + $apple_posts = []; + foreach ( explode( ',', $post_ids ) as $post_id ) { + $post = get_post( (int) $post_id ); + if ( ! empty( $post ) ) { - $articles[] = $post; + $apple_posts[] = $post; } } + // Override text within the partial depending on the action. + $apple_page_title = match ( $action ) { + 'apple_news_push_post' => __( 'Bulk Export Articles', 'apple-news' ), + 'apple_news_delete_post' => __( 'Bulk Delete Articles', 'apple-news' ), + }; + $apple_page_description = match ( $action ) { + 'apple_news_push_post' => __( 'The following articles will be exported.', 'apple-news' ), + 'apple_news_delete_post' => __( 'The following articles will be deleted.', 'apple-news' ), + }; + $apple_submit_text = match ( $action ) { + 'apple_news_push_post' => __( 'Publish All', 'apple-news' ), + 'apple_news_delete_post' => __( 'Delete All', 'apple-news' ), + }; + require_once __DIR__ . '/partials/page-bulk-export.php'; } @@ -123,13 +130,7 @@ public function ajax_push_post() { // Ensure the post exists and that it's published. $post = get_post( $id ); if ( empty( $post ) ) { - echo wp_json_encode( - [ - 'success' => false, - 'error' => __( 'This post no longer exists.', 'apple-news' ), - ] - ); - wp_die(); + wp_send_json_error( __( 'This post no longer exists.', 'apple-news' ) ); } // Check capabilities. @@ -137,27 +138,17 @@ public function ajax_push_post() { /** This filter is documented in admin/class-admin-apple-post-sync.php */ apply_filters( 'apple_news_publish_capability', self::get_capability_for_post_type( 'publish_posts', $post->post_type ) ) ) ) { - echo wp_json_encode( - [ - 'success' => false, - 'error' => __( 'You do not have permission to publish to Apple News', 'apple-news' ), - ] - ); - wp_die(); + wp_send_json_error( __( 'You do not have permission to publish to Apple News', 'apple-news' ) ); } if ( 'publish' !== $post->post_status ) { - echo wp_json_encode( - [ - 'success' => false, - 'error' => sprintf( - // translators: token is a post ID. - __( 'Article %s is not published and cannot be pushed to Apple News.', 'apple-news' ), - $id - ), - ] + wp_send_json_error( + sprintf( + /* translators: %s: post ID */ + __( 'Article %s is not published and cannot be pushed to Apple News.', 'apple-news' ), + $id + ) ); - wp_die(); } $action = new Apple_Actions\Index\Push( $this->settings, $id ); @@ -168,22 +159,56 @@ public function ajax_push_post() { } if ( $errors ) { - echo wp_json_encode( - [ - 'success' => false, - 'error' => $errors, - ] - ); - } else { - echo wp_json_encode( - [ - 'success' => true, - ] - ); + wp_send_json_error( $errors ); + } + + wp_send_json_success(); + } + + /** + * Handles the ajax action to delete a post from Apple News. + * + * @access public + */ + public function ajax_delete_post() { + // Check the nonce. + check_ajax_referer( self::ACTION ); + + // Sanitize input data. + $id = isset( $_GET['id'] ) ? (int) $_GET['id'] : -1; + + $post = get_post( $id ); + + if ( empty( $post ) ) { + wp_send_json_error( __( 'This post no longer exists.', 'apple-news' ) ); + } + + /** This filter is documented in admin/class-admin-apple-post-sync.php */ + $cap = apply_filters( 'apple_news_delete_capability', self::get_capability_for_post_type( 'delete_posts', $post->post_type ) ); + + // Check capabilities. + if ( ! current_user_can( $cap ) ) { + wp_send_json_error( __( 'You do not have permission to delete posts from Apple News', 'apple-news' ) ); + } + + $errors = null; + + // Try to sync only if the post has a remote ID. Ref `Admin_Apple_Post_Sync::do_delete()`. + if ( get_post_meta( $id, 'apple_news_api_id', true ) ) { + $action = new Apple_Actions\Index\Delete( $this->settings, $id ); + + try { + $errors = $action->perform(); + } catch ( Apple_Actions\Action_Exception $e ) { + $errors = $e->getMessage(); + } + } + + if ( $errors ) { + wp_send_json_error( $errors ); } - // This is required to terminate immediately and return a valid response. - wp_die(); + wp_send_json_success(); } /** diff --git a/admin/class-admin-apple-index-page.php b/admin/class-admin-apple-index-page.php index ccef3af7..80a99e9b 100644 --- a/admin/class-admin-apple-index-page.php +++ b/admin/class-admin-apple-index-page.php @@ -110,7 +110,7 @@ public function admin_page() { * * @since 0.4.0 * @access public - * @return mixed The result of the requested action. + * @return mixed|void The result of the requested action. */ public function page_router() { $id = isset( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : null; // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.NonceVerification.Recommended @@ -130,22 +130,56 @@ public function page_router() { return $this->export_action( $id ); case self::namespace_action( 'reset' ): return $this->reset_action( $id ); - case self::namespace_action( 'push' ): // phpcs:ignore PSR2.ControlStructures.SwitchDeclaration.TerminatingComment - if ( ! $id ) { + case self::namespace_action( 'push' ): + if ( $id ) { + $this->push_action( $id ); + } else { $url = menu_page_url( $this->plugin_slug . '_bulk_export', false ); - if ( isset( $_GET['article'] ) ) { // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.NonceVerification.Recommended - $ids = is_array( $_GET['article'] ) ? array_map( 'absint', $_GET['article'] ) : absint( $_GET['article'] ); // phpcs:ignore WordPress.VIP.SuperGlobalInputUsage.AccessDetected, WordPress.Security.NonceVerification.Recommended - $url .= '&ids=' . implode( '.', $ids ); + + if ( isset( $_GET['article'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $post_ids = is_array( $_GET['article'] ) ? array_map( 'intval', $_GET['article'] ) : (int) $_GET['article']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $url = add_query_arg( + [ + 'action' => 'apple_news_push_post', + 'post_ids' => implode( ',', $post_ids ), + ], + $url, + ); } + wp_safe_redirect( esc_url_raw( $url ) ); // phpcs:ignore WordPressVIPMinimum.Security.ExitAfterRedirect.NoExit + if ( ! defined( 'APPLE_NEWS_UNIT_TESTS' ) || ! APPLE_NEWS_UNIT_TESTS ) { exit; } - } else { - return $this->push_action( $id ); } + + break; case self::namespace_action( 'delete' ): - return $this->delete_action( $id ); + if ( $id ) { + $this->delete_action( $id ); + } else { + $url = menu_page_url( $this->plugin_slug . '_bulk_export', false ); + + if ( isset( $_GET['article'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $post_ids = is_array( $_GET['article'] ) ? array_map( 'intval', $_GET['article'] ) : (int) $_GET['article']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $url = add_query_arg( + [ + 'action' => 'apple_news_delete_post', + 'post_ids' => implode( ',', $post_ids ), + ], + $url, + ); + } + + wp_safe_redirect( esc_url_raw( $url ) ); // phpcs:ignore WordPressVIPMinimum.Security.ExitAfterRedirect.NoExit + + if ( ! defined( 'APPLE_NEWS_UNIT_TESTS' ) || ! APPLE_NEWS_UNIT_TESTS ) { + exit; + } + } + + break; } } @@ -401,7 +435,7 @@ private function delete_action( $id ) { $action = new Apple_Actions\Index\Delete( $this->settings, $id ); try { $action->perform(); - $this->notice_success( __( 'Your article has been removed from apple news.', 'apple-news' ) ); + $this->notice_success( __( 'Your article has been removed from Apple News.', 'apple-news' ) ); } catch ( Apple_Actions\Action_Exception $e ) { $this->notice_error( $e->getMessage() ); } diff --git a/admin/class-admin-apple-news-list-table.php b/admin/class-admin-apple-news-list-table.php index 676c878f..2466652a 100644 --- a/admin/class-admin-apple-news-list-table.php +++ b/admin/class-admin-apple-news-list-table.php @@ -328,8 +328,9 @@ public function get_bulk_actions() { return apply_filters( 'apple_news_bulk_actions', [ - Admin_Apple_Index_Page::namespace_action( 'push' ) => __( 'Publish', 'apple-news' ), - ] + Admin_Apple_Index_Page::namespace_action( 'push' ) => __( 'Publish', 'apple-news' ), + Admin_Apple_Index_Page::namespace_action( 'delete' ) => __( 'Delete', 'apple-news' ), + ], ); } diff --git a/admin/partials/page-bulk-export.php b/admin/partials/page-bulk-export.php index de923e5d..dd5747a2 100644 --- a/admin/partials/page-bulk-export.php +++ b/admin/partials/page-bulk-export.php @@ -2,17 +2,20 @@ /** * Publish to Apple News partials: Bulk Export page template * - * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable - * - * @global array $articles - * * @package Apple_News */ +// Expect these variables to be defined in Admin_Apple_Bulk_Export_Page::build_page() but make sure they're set. +$apple_page_title ??= __( 'Bulk Export', 'apple-news' ); +$apple_page_description ??= __( 'The following articles will be affected.', 'apple-news' ); +$apple_posts ??= []; +$apple_submit_text ??= __( 'Go', 'apple-news' ); + ?>
diff --git a/assets/js/bulk-export.js b/assets/js/bulk-export.js index 85752ffc..c9d71e16 100644 --- a/assets/js/bulk-export.js +++ b/assets/js/bulk-export.js @@ -1,10 +1,13 @@ (function ( $, window, undefined ) { 'use strict'; - var started = false; + var started = false, + searchParams = new URLSearchParams( window.location.search ), + $submitButton = $( '.bulk-export-submit' ); function done() { - $( '.bulk-export-submit' ).text( 'Done' ); + $submitButton.text( 'Done' ); + $submitButton.attr( 'disabled', 'disabled' ); } function pushItem( item, next, nonce ) { @@ -12,14 +15,14 @@ var $status = $item.find( '.bulk-export-list-item-status' ); var id = +$item.data( 'post-id' ); // fetch the post-id and cast to integer - $status.removeClass( 'pending' ).addClass( 'in-progress' ).text( 'Publishing...' ); + $status.removeClass( 'pending' ).addClass( 'in-progress' ).text( 'In Progress…' ); // Send a GET request to ajaxurl, which is WordPress endpoint for AJAX // requests. Expects JSON as response. $.getJSON( ajaxurl, { - action: 'push_post', + action: searchParams.get( 'action' ), id: id, _ajax_nonce: nonce }, @@ -27,7 +30,7 @@ if ( res.success ) { $status.removeClass( 'in-progress' ).addClass( 'success' ).text( 'Success' ); } else { - $status.removeClass( 'in-progress' ).addClass( 'failed' ).text( res.error ); + $status.removeClass( 'in-progress' ).addClass( 'failed' ).text( res.data ); } next(); }, @@ -56,7 +59,7 @@ next(); } - $('.bulk-export-submit').click(function (e) { + $submitButton.click( function ( e ) { e.preventDefault(); if ( started ) { @@ -65,6 +68,6 @@ started = true; bulkPush(); - }); + } ); })( jQuery, window ); diff --git a/includes/apple-push-api/request/class-request.php b/includes/apple-push-api/request/class-request.php index ee44a57d..cf529209 100644 --- a/includes/apple-push-api/request/class-request.php +++ b/includes/apple-push-api/request/class-request.php @@ -371,6 +371,10 @@ private function request( $verb, $url, $data = [] ) { $args['timeout'] = 30; // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout } + if ( 'DELETE' === $verb ) { + $args['timeout'] = 5; // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + } + /** * Allow filtering of the default arguments for the request. * @@ -411,8 +415,8 @@ private function request( $verb, $url, $data = [] ) { } } - // NULL is a valid response for DELETE. - if ( 'DELETE' === $verb && is_null( $response ) ) { + // Successful DELETE requests have no response body. + if ( 'DELETE' === $verb && 204 === wp_remote_retrieve_response_code( $response ) ) { return null; }