diff --git a/bulk-edit-cron-offload.php b/bulk-edit-cron-offload.php index 3610dc89521a217146aa55d36689a32ef5b56833..ccfb942f93e8729e001dbb3b0ee1e64da7bb147d 100644 --- a/bulk-edit-cron-offload.php +++ b/bulk-edit-cron-offload.php @@ -20,3 +20,4 @@ require __DIR__ . '/includes/utils.php'; // Plugin functionality. require __DIR__ . '/includes/class-main.php'; require __DIR__ . '/includes/class-delete-all.php'; +require __DIR__ . '/includes/class-move-to-trash.php'; diff --git a/includes/class-delete-all.php b/includes/class-delete-all.php index 3f683f4ff424c7a86cd2ab8fae0fe5483a6dedef..dc540b562f8cf40ff73e5ede0018735b675c34e2 100644 --- a/includes/class-delete-all.php +++ b/includes/class-delete-all.php @@ -14,16 +14,14 @@ class Delete_All { /** * Class constants */ - const CRON_EVENT = 'a8c_bulk_edit_delete_all'; - - const ADMIN_NOTICE_KEY = 'a8c_bulk_edit_deleted_all'; + const ADMIN_NOTICE_KEY = 'bulk_edit_cron_offload_deleted_all'; /** * Register this bulk process' hooks */ public static function register_hooks() { add_action( Main::build_hook( 'delete_all' ), array( __CLASS__, 'process' ) ); - add_action( self::CRON_EVENT, array( __CLASS__, 'process_via_cron' ) ); + add_action( Main::build_cron_hook( 'delete_all' ), array( __CLASS__, 'process_via_cron' ) ); add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) ); add_filter( 'posts_where', array( __CLASS__, 'hide_posts_pending_delete' ), 999, 2 ); @@ -43,11 +41,10 @@ class Delete_All { // Special keys are used to trigger this request, and we need to remove them on redirect. $extra_keys = array( 'delete_all', 'delete_all2' ); - $action_scheduled = self::action_next_scheduled( self::CRON_EVENT, $vars->post_type ); + $action_scheduled = self::action_next_scheduled( $vars->post_type ); if ( empty( $action_scheduled ) ) { - wp_schedule_single_event( time(), self::CRON_EVENT, array( $vars ) ); - + Main::schedule_processing( $vars ); Main::do_admin_redirect( self::ADMIN_NOTICE_KEY, true, $extra_keys ); } else { Main::do_admin_redirect( self::ADMIN_NOTICE_KEY, false, $extra_keys ); @@ -115,31 +112,25 @@ class Delete_All { public static function admin_notices() { $screen = get_current_screen(); + $type = ''; + $message = ''; + if ( isset( $_REQUEST[ self::ADMIN_NOTICE_KEY ] ) ) { if ( 1 === (int) $_REQUEST[ self::ADMIN_NOTICE_KEY ] ) { - $class = 'notice-success'; - $message = __( 'Success! The trash will be emptied soon.', 'bulk-edit-cron-offload' ); + $type = 'success'; + $message = __( 'Success! The trash will be emptied shortly.', 'bulk-edit-cron-offload' ); } else { - $class = 'notice-error'; + $type = 'error'; $message = __( 'A request to empty the trash is already pending for this post type.', 'bulk-edit-cron-offload' ); } } elseif ( 'edit' === $screen->base && isset( $_REQUEST['post_status'] ) && 'trash' === $_REQUEST['post_status'] ) { - if ( self::action_next_scheduled( self::CRON_EVENT, $screen->post_type ) ) { - $class = 'notice-warning'; + if ( self::action_next_scheduled( $screen->post_type ) ) { + $type = 'warning'; $message = __( 'A pending request to empty the trash will be processed soon.', 'bulk-edit-cron-offload' ); } } - // Nothing to display. - if ( ! isset( $class ) || ! isset( $message ) ) { - return; - } - - ?> - <div class="notice <?php echo esc_attr( $class ); ?>"> - <p><?php echo esc_html( $message ); ?></p> - </div> - <?php + Main::render_admin_notice( $type, $message ); } /** @@ -162,7 +153,7 @@ class Delete_All { return $where; } - if ( self::action_next_scheduled( self::CRON_EVENT, $q->get( 'post_type' ) ) ) { + if ( self::action_next_scheduled( $q->get( 'post_type' ) ) ) { $where .= ' AND 0=1'; } @@ -196,7 +187,7 @@ class Delete_All { } // There isn't a pending purge, so one should be permitted. - if ( ! self::action_next_scheduled( self::CRON_EVENT, $screen->post_type ) ) { + if ( ! self::action_next_scheduled( $screen->post_type ) ) { return $caps; } @@ -209,11 +200,10 @@ class Delete_All { /** * Find the next scheduled instance of a given action, regardless of arguments * - * @param string $action_to_check Hook to search for. - * @param string $post_type Post type hook is scheduled for. + * @param string $post_type Post type hook is scheduled for. * @return array */ - private static function action_next_scheduled( $action_to_check, $post_type ) { + private static function action_next_scheduled( $post_type ) { $events = get_option( 'cron' ); if ( ! is_array( $events ) ) { @@ -227,14 +217,14 @@ class Delete_All { } foreach ( $timestamp_events as $action => $action_instances ) { - if ( $action !== $action_to_check ) { + if ( Main::CRON_EVENT !== $action ) { continue; } foreach ( $action_instances as $instance => $instance_args ) { $vars = array_shift( $instance_args['args'] ); - if ( $post_type === $vars->post_type ) { + if ( 'delete_all' === $vars->action && $post_type === $vars->post_type ) { return array( 'timestamp' => $timestamp, 'args' => $vars, diff --git a/includes/class-main.php b/includes/class-main.php index aa0d618d084a96a8970b5bb3e8efe01b47a96614..1a477327800daa97a31fd669039702d4dcb2d617 100644 --- a/includes/class-main.php +++ b/includes/class-main.php @@ -17,12 +17,28 @@ class Main { const ACTION = 'a8c_bulk_edit_cron_'; /** - * Register action + * Common cron action + */ + const CRON_EVENT = 'bulk_edit_cron_offload'; + + /** + * Register actions */ public static function load() { + add_action( self::CRON_EVENT, array( __CLASS__, 'do_cron' ) ); + add_action( 'load-edit.php', array( __CLASS__, 'intercept' ) ); } + /** + * Run appropriate cron callback + * + * @param object $vars Bulk-request variables. + */ + public static function do_cron( $vars ) { + do_action( self::build_cron_hook( $vars->action ), $vars ); + } + /** * Call appropriate handler */ @@ -77,8 +93,6 @@ class Main { if ( isset( $_REQUEST['delete_all'] ) || isset( $_REQUEST['delete_all2'] ) ) { $vars->action = 'delete_all'; - - $vars->post_status = $_REQUEST['post_status']; } elseif ( isset( $_REQUEST['action'] ) && '-1' !== $_REQUEST['action'] ) { $vars->action = $_REQUEST['action']; } elseif ( isset( $_REQUEST['action2'] ) && '-1' !== $_REQUEST['action2'] ) { @@ -121,6 +135,11 @@ class Main { $vars->post_format = $_REQUEST['post_format']; } + // Post status is special. + if ( is_null( $vars->post_status ) && isset( $_REQUEST['post_status'] ) && ! empty( $_REQUEST['post_status'] ) ) { + $vars->post_status = $_REQUEST['post_status']; + } + return $vars; } @@ -152,6 +171,16 @@ class Main { return self::ACTION . $action; } + /** + * Build a cron hook specific to a bulk request + * + * @param string $action Bulk action to register cron callback for. + * @return string + */ + public static function build_cron_hook( $action ) { + return self::ACTION . $action . '_callback'; + } + /** * Unset flags Core uses to trigger bulk processing */ @@ -162,6 +191,26 @@ class Main { unset( $_REQUEST['delete_all2'] ); } + /** + * Create cron event + * + * @param object $vars Bulk-request variables. + * @return bool + */ + public static function schedule_processing( $vars ) { + return false !== wp_schedule_single_event( time(), self::CRON_EVENT, array( $vars ) ); + } + + /** + * Retrieve timestamp for next scheduled event with given vars + * + * @param object $vars Bulk-request variables. + * @return int + */ + public static function next_scheduled( $vars ) { + return (int) wp_next_scheduled( self::CRON_EVENT, array( $vars ) ); + } + /** * Redirect, including a flag to indicate if the bulk process was scheduled successfully * @@ -184,6 +233,26 @@ class Main { wp_safe_redirect( $redirect ); exit; } + + /** + * Render an admin message of a given type + * + * @param string $type Message type. + * @param string $message Message to output. + * @return void + */ + public static function render_admin_notice( $type, $message ) { + // Lacking what's required. + if ( empty( $type ) || empty( $message ) ) { + return; + } + + ?> + <div class="notice <?php echo esc_attr( 'notice-' . $type ); ?>"> + <p><?php echo esc_html( $message ); ?></p> + </div> + <?php + } } Main::load(); diff --git a/includes/class-move-to-trash.php b/includes/class-move-to-trash.php new file mode 100644 index 0000000000000000000000000000000000000000..edcd11eed855a9833ba16d1c60c369c08e4a74a9 --- /dev/null +++ b/includes/class-move-to-trash.php @@ -0,0 +1,231 @@ +<?php +/** + * Offload "Move to Trash" + * + * @package Bulk_Edit_Cron_Offload + */ + +namespace Automattic\WP\Bulk_Edit_Cron_Offload; + +/** + * Class Move_To_Trash + */ +class Move_To_Trash { + /** + * Class constants + */ + const ADMIN_NOTICE_KEY = 'bulk_edit_cron_offload_move_to_trash'; + + /** + * Register this bulk process' hooks + */ + public static function register_hooks() { + add_action( Main::build_hook( 'trash' ), array( __CLASS__, 'process' ) ); + add_action( Main::build_cron_hook( 'trash' ), array( __CLASS__, 'process_via_cron' ) ); + + add_action( 'admin_notices', array( __CLASS__, 'admin_notices' ) ); + add_filter( 'posts_where', array( __CLASS__, 'hide_posts_pending_move' ), 999, 2 ); + } + + /** + * Handle a request to move some posts to the trash + * + * @param object $vars Bulk-request variables. + */ + public static function process( $vars ) { + $action_scheduled = Main::next_scheduled( $vars ); + + if ( empty( $action_scheduled ) ) { + Main::schedule_processing( $vars ); + Main::do_admin_redirect( self::ADMIN_NOTICE_KEY, true ); + } else { + Main::do_admin_redirect( self::ADMIN_NOTICE_KEY, false ); + } + } + + /** + * Cron callback to move requested items to trash + * + * @param object $vars Bulk-request variables. + */ + public static function process_via_cron( $vars ) { + $count = 0; + + if ( is_array( $vars->posts ) && ! empty( $vars->posts ) ) { + require_once ABSPATH . '/wp-admin/includes/post.php'; + + $trashed = array(); + $locked = array(); + $auth_error = array(); + $error = array(); + + foreach ( $vars->posts as $post_id ) { + // Can the user trash this post? + if ( ! user_can( $vars->user_id, 'delete_post', $post_id ) ) { + $auth_error[] = $post_id; + continue; + } + + // Post is locked by someone, so leave it alone. + if ( false !== wp_check_post_lock( $post_id ) ) { + $locked[] = $post_id; + continue; + } + + // Try trashing. + $post_trashed = wp_trash_post( $post_id ); + if ( $post_trashed ) { + $trashed[] = $post_id; + } else { + $error[] = $post_id; + } + + // Take a break periodically. + if ( 0 === $count++ % 50 ) { + stop_the_insanity(); + sleep( 3 ); + } + } + + $results = compact( 'trashed', 'locked', 'auth_error', 'error' ); + do_action( 'bulk_edit_cron_offload_move_to_trash_request_completed', $results, $vars ); + } else { + do_action( 'bulk_edit_cron_offload_move_to_trash_request_no_posts', $vars->posts, $vars ); + } + } + + /** + * Let the user know what's going on + */ + public static function admin_notices() { + $screen = get_current_screen(); + + $type = ''; + $message = ''; + + if ( isset( $_REQUEST[ self::ADMIN_NOTICE_KEY ] ) ) { + if ( 1 === (int) $_REQUEST[ self::ADMIN_NOTICE_KEY ] ) { + $type = 'success'; + $message = __( 'Success! The selected posts will be moved to the trash shortly.', 'bulk-edit-cron-offload' ); + } else { + $type = 'error'; + $message = __( 'The selected posts are already scheduled to be moved to the trash.', 'bulk-edit-cron-offload' ); + } + } elseif ( 'edit' === $screen->base ) { + if ( isset( $_REQUEST['post_status'] ) && 'trash' === $_REQUEST['post_status'] ) { + return; + } + + $status = isset( $_REQUEST['post_status'] ) ? $_REQUEST['post_status'] : 'all'; + + if ( self::get_all_pending_actions( $screen->post_type, $status ) ) { + $type = 'warning'; + $message = __( 'Some items that would normally be shown here are waiting to be moved to the trash. These items are hidden until they are moved.', 'bulk-edit-cron-offload' ); + } + } + + Main::render_admin_notice( $type, $message ); + } + + /** + * When a move is pending for a given post type, hide those posts in the admin + * + * @param string $where Posts' WHERE clause. + * @param object $q WP_Query object. + * @return string + */ + public static function hide_posts_pending_move( $where, $q ) { + if ( ! is_admin() || ! $q->is_main_query() ) { + return $where; + } + + if ( 'edit' !== get_current_screen()->base ) { + return $where; + } + + if ( 'trash' === $q->get( 'post_status' ) ) { + return $where; + } + + $post__not_in = self::get_post_ids_pending_move( $q->get( 'post_type' ), $q->get( 'post_status' ) ); + + if ( ! empty( $post__not_in ) ) { + $post__not_in = implode( ',', $post__not_in ); + $where .= ' AND ID NOT IN(' . $post__not_in . ')'; + } + + return $where; + } + + /** + * Gather all pending events for a given post type + * + * @param string $post_type Post type needing exclusion. + * @param string $post_status Post status to filter by. + * @return array + */ + private static function get_all_pending_actions( $post_type, $post_status ) { + $events = get_option( 'cron' ); + + if ( ! is_array( $events ) ) { + return array(); + } + + $ids = array(); + + foreach ( $events as $timestamp => $timestamp_events ) { + // Skip non-event data that Core includes in the option. + if ( ! is_numeric( $timestamp ) ) { + continue; + } + + foreach ( $timestamp_events as $action => $action_instances ) { + if ( Main::CRON_EVENT !== $action ) { + continue; + } + + foreach ( $action_instances as $instance => $instance_args ) { + $vars = array_shift( $instance_args['args'] ); + + if ( 'trash' === $vars->action && $post_type === $vars->post_type ) { + if ( $post_status === $vars->post_status || 'all' === $vars->post_status || 'all' === $post_status ) { + $ids[] = array( + 'timestamp' => $timestamp, + 'args' => $vars, + ); + } + } + } + } + } + + return $ids; + } + + /** + * Gather IDs of objects pending move to trash, with given post type + * + * @param string $post_type Post type needing exclusion. + * @param string $post_status Post status to filter by. + * @return array + */ + private static function get_post_ids_pending_move( $post_type, $post_status ) { + $events = wp_list_pluck( self::get_all_pending_actions( $post_type, $post_status ), 'args' ); + $events = wp_list_pluck( $events, 'posts' ); + + $ids = array(); + + foreach ( $events as $ids_to_merge ) { + $ids = array_merge( $ids, $ids_to_merge ); + } + + if ( ! empty( $ids ) ) { + $ids = array_map( 'absint', $ids ); + $ids = array_unique( $ids ); + } + + return $ids; + } +} + +Move_To_Trash::register_hooks(); diff --git a/languages/bulk-edit-cron-offload.pot b/languages/bulk-edit-cron-offload.pot index ba8d037a21ee446652f324099a4d8509acb919b8..00f39df29866b2f7d3de38def2161bca21a7ddcc 100644 --- a/languages/bulk-edit-cron-offload.pot +++ b/languages/bulk-edit-cron-offload.pot @@ -5,7 +5,7 @@ msgstr "" "Project-Id-Version: Bulk Edit Cron Offload 1.0\n" "Report-Msgid-Bugs-To: " "https://wordpress.org/support/plugin/bulk-edit-cron-offload\n" -"POT-Creation-Date: 2017-09-13 01:24:54+00:00\n" +"POT-Creation-Date: 2017-09-13 05:28:23+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -25,18 +25,32 @@ msgstr "" "X-Poedit-Bookmarks: \n" "X-Textdomain-Support: yes\n" -#: includes/class-delete-all.php:124 -msgid "Success! The trash will be emptied soon." +#: includes/class-delete-all.php:121 +msgid "Success! The trash will be emptied shortly." msgstr "" -#: includes/class-delete-all.php:127 +#: includes/class-delete-all.php:124 msgid "A request to empty the trash is already pending for this post type." msgstr "" -#: includes/class-delete-all.php:132 +#: includes/class-delete-all.php:129 msgid "A pending request to empty the trash will be processed soon." msgstr "" +#: includes/class-move-to-trash.php:109 +msgid "Success! The selected posts will be moved to the trash shortly." +msgstr "" + +#: includes/class-move-to-trash.php:112 +msgid "The selected posts are already scheduled to be moved to the trash." +msgstr "" + +#: includes/class-move-to-trash.php:123 +msgid "" +"Some items that would normally be shown here are waiting to be moved to the " +"trash. These items are hidden until they are moved." +msgstr "" + #. Plugin Name of the plugin/theme msgid "Bulk Edit Cron Offload" msgstr ""