Commit 7b5f845a authored by Erick Hitter's avatar Erick Hitter

Merge branch 'add/1-bulk-action' into 'master'

Add bulk actions

Closes #1 and #4

See merge request !4
parents a5fcb932 99a4ca3e
Pipeline #1066 passed with stages
in 2 minutes and 32 seconds
......@@ -6,7 +6,6 @@
<file>.</file>
<exclude-pattern>/vendor/</exclude-pattern>
<exclude-pattern>/node_modules/</exclude-pattern>
<exclude-pattern>/tests/*</exclude-pattern>
<!-- How to scan -->
<!-- Usage instructions: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage -->
......
......@@ -36,6 +36,7 @@ Navigate to **Settings > Writing** in your WordPress Dashboard, and look for the
## Changelog ##
### 1.3 ###
* Add bulk actions to purge excess or all revisions.
* Introduce unit tests.
* Conform to coding standards.
......@@ -50,6 +51,9 @@ Navigate to **Settings > Writing** in your WordPress Dashboard, and look for the
## Upgrade Notice ##
### 1.3 ###
Introduces bulk actions for purging revisions, along with unit tests. The plugin also conforms to coding standards.
### 1.2.1 ###
Introduces Spanish translation thanks to Maria Ramos at [WebHostingHub](http://www.webhostinghub.com/).
......
<?php
/**
* Bulk actions.
*
* @package WP_Revisions_Control
*/
/**
* Class WP_Revisions_Control_Bulk_Actions.
*/
class WP_Revisions_Control_Bulk_Actions {
/**
* Singleton.
*
* @var static
*/
private static $__instance;
/**
* Supported post types.
*
* @var array
*/
protected $post_types;
/**
* Base for bulk action names.
*
* @var string
*/
protected $action_base = 'wp_rev_ctl_bulk_';
/**
* Custom bulk actions.
*
* @var array
*/
protected $actions;
/**
* Silence is golden!
*/
private function __construct() {}
/**
* Singleton implementation.
*
* @param array $post_types Supported post types, used only on instantiation.
* @return static
*/
public static function get_instance( $post_types = array() ) {
if ( ! is_a( static::$__instance, __CLASS__ ) ) {
static::$__instance = new self();
static::$__instance->setup( $post_types );
}
return static::$__instance;
}
/**
* One-time actions.
*
* @param array $post_types Supported post types.
*/
public function setup( $post_types ) {
if ( empty( $post_types ) || ! is_array( $post_types ) ) {
return;
}
$this->post_types = $post_types;
$this->register_actions();
add_action( 'load-edit.php', array( $this, 'register_admin_hooks' ) );
add_filter( 'removable_query_args', array( $this, 'remove_message_query_args' ) );
}
/**
* Register custom actions.
*/
protected function register_actions() {
$actions = array();
$actions[ $this->action_base . 'purge_excess' ] = __(
'Purge excess revisions',
'wp_revisions_control'
);
$actions[ $this->action_base . 'purge_all' ] = __(
'Purge ALL revisions',
'wp_revisions_control'
);
$this->actions = $actions;
}
/**
* Register various hooks.
*/
public function register_admin_hooks() {
$screen = get_current_screen();
if ( null === $screen ) {
return;
}
$post_types = array_keys( $this->post_types );
if ( ! in_array( $screen->post_type, $post_types, true ) ) {
return;
}
$post_type_caps = get_post_type_object( $screen->post_type )->cap;
$user_can = (
current_user_can( $post_type_caps->edit_posts ) &&
current_user_can( $post_type_caps->edit_published_posts ) &&
current_user_can( $post_type_caps->edit_others_posts )
);
$user_can = apply_filters(
'wp_revisions_control_current_user_can_bulk_actions',
$user_can,
$screen->post_type
);
if ( ! $user_can ) {
return;
}
if ( 'edit' !== $screen->base ) {
return;
}
add_filter( 'bulk_actions-' . $screen->id, array( $this, 'add_actions' ) );
add_filter( 'handle_bulk_actions-' . $screen->id, array( $this, 'handle_action' ), 10, 3 );
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
}
/**
* Remove message query arguments to prevent re-display.
*
* @param array $args Array of query variables to remove from URL.
* @return array
*/
public function remove_message_query_args( $args ) {
return array_merge( $args, $this->get_message_query_args() );
}
/**
* Return array of supported query args that trigger admin notices.
*
* @return array
*/
protected function get_message_query_args() {
$args = array_keys( $this->actions );
$args[] = $this->action_base . 'missing';
$args[] = $this->action_base . 'nonce';
return $args;
}
/**
* Add our actions.
*
* @param string[] $actions Array of available actions.
* @return array
*/
public function add_actions( $actions ) {
return array_merge( $actions, $this->actions );
}
/**
* Handle our bulk actions.
*
* @param string $redirect_to Redirect URL.
* @param string $action Bulk action being taken.
* @param array $ids Object IDs to manipulate.
* @return string
*/
public function handle_action( $redirect_to, $action, $ids ) {
if ( ! array_key_exists( $action, $this->actions ) ) {
return $redirect_to;
}
$response = array_fill_keys( $this->get_message_query_args(), 0 );
switch ( str_replace( $this->action_base, '', $action ) ) {
case 'purge_all':
$this->purge_all( $ids );
$response[ $action ] = 1;
break;
case 'purge_excess':
$this->purge_excess( $ids );
$response[ $action ] = 1;
break;
case 'nonce':
break;
default:
$response[ $this->action_base . 'missing' ] = 1;
break;
}
if ( is_array( $response ) ) {
$response[ $this->action_base . 'nonce' ] = wp_create_nonce( $this->action_base );
$redirect_to = add_query_arg( $response, $redirect_to );
}
return $redirect_to;
}
/**
* Remove all revisions from the given IDs.
*
* @param array $ids Object IDs.
*/
protected function purge_all( $ids ) {
$plugin = WP_Revisions_Control::get_instance();
foreach ( $ids as $id ) {
$plugin->do_purge_all( $id );
}
}
/**
* Remove excess revisions from the given IDs.
*
* @param array $ids Object IDs.
*/
protected function purge_excess( $ids ) {
$plugin = WP_Revisions_Control::get_instance();
foreach ( $ids as $id ) {
$plugin->do_purge_excess( $id );
}
}
/**
* Render admin notices.
*/
public function admin_notices() {
$message = null;
$nonce_key = $this->action_base . 'nonce';
if (
! isset( $_GET[ $nonce_key ] ) ||
! wp_verify_nonce( sanitize_text_field( $_GET[ $nonce_key ] ), $this->action_base )
) {
return;
}
foreach ( $this->get_message_query_args() as $arg ) {
if ( isset( $_GET[ $arg ] ) && 1 === (int) $_GET[ $arg ] ) {
$message = $arg;
break;
}
}
if ( null === $message ) {
return;
}
$type = 'updated';
switch ( str_replace( $this->action_base, '', $message ) ) {
case 'purge_all':
$message = __(
'Purged all revisions.',
'wp_revisions_control'
);
break;
case 'purge_excess':
$message = __(
'Purged excess revisions.',
'wp_revisions_control'
);
break;
case 'nonce':
break;
default:
case 'missing':
$message = __(
'WP Revisions Control encountered an unspecified error.',
'wp_revisions_control'
);
$type = 'error';
break;
}
if ( ! isset( $message, $type ) ) {
return;
}
?>
<div class="notice is-dismissible <?php echo esc_attr( $type ); ?>">
<p><?php echo esc_html( $message ); ?></p>
</div>
<?php
}
}
......@@ -103,7 +103,11 @@ class WP_Revisions_Control {
* Load plugin translations.
*/
public function action_plugins_loaded() {
load_plugin_textdomain( 'wp_revisions_control', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
load_plugin_textdomain(
'wp_revisions_control',
false,
dirname( dirname( plugin_basename( __FILE__ ) ) ) . '/languages/'
);
}
/**
......@@ -121,12 +125,14 @@ class WP_Revisions_Control {
* Plugin title is intentionally not translatable.
*/
public function action_admin_init() {
$post_types = $this->get_post_types();
// Plugin setting section.
register_setting( $this->settings_page, $this->settings_section, array( $this, 'sanitize_options' ) );
add_settings_section( $this->settings_section, 'WP Revisions Control', array( $this, 'settings_section_intro' ), $this->settings_page );
foreach ( $this->get_post_types() as $post_type => $name ) {
foreach ( $post_types as $post_type => $name ) {
add_settings_field( $this->settings_section . '-' . $post_type, $name, array( $this, 'field_post_type' ), $this->settings_page, $this->settings_section, array( 'post_type' => $post_type ) );
}
......@@ -134,6 +140,9 @@ class WP_Revisions_Control {
add_action( 'add_meta_boxes', array( $this, 'action_add_meta_boxes' ), 10, 2 );
add_action( 'wp_ajax_' . $this->settings_section . '_purge', array( $this, 'ajax_purge' ) );
add_action( 'save_post', array( $this, 'action_save_post' ) );
// Bulk actions.
WP_Revisions_Control_Bulk_Actions::get_instance( $post_types );
}
/**
......@@ -332,7 +341,7 @@ class WP_Revisions_Control {
'Limit this post to %1$s revisions. Leave this field blank for default behavior.',
'wp_revisions_control'
),
'<input type="text" name="' . esc_attr( $this->settings_section ) . '_qty" value="' . (int) $this->get_post_revisions_to_keep( $post->ID ) . '" id="' . esc_attr( $this->settings_section ) . '_qty" size="2" />'
'<input type="text" name="' . esc_attr( $this->settings_section ) . '_qty" value="' . esc_attr( $this->get_post_revisions_to_keep( $post->ID ) ) . '" id="' . esc_attr( $this->settings_section ) . '_qty" size="2" />'
);
?>
......@@ -401,6 +410,50 @@ class WP_Revisions_Control {
return $response;
}
/**
* Remove any revisions in excess of a post's limit.
*
* @param int $post_id Post ID to purge of excess revisions.
* @return array
*/
public function do_purge_excess( $post_id ) {
$response = array(
'count' => 0,
);
$to_keep = wp_revisions_to_keep( get_post( $post_id ) );
if ( $to_keep < 0 ) {
$response['success'] = __(
'No revisions to remove.',
'wp_revisions_control'
);
return $response;
}
$revisions = wp_get_post_revisions( $post_id );
$starting_count = count( $revisions );
if ( $starting_count <= $to_keep ) {
$response['success'] = __(
'No revisions to remove.',
'wp_revisions_control'
);
return $response;
}
$to_remove = array_slice( $revisions, $to_keep, null, true );
$response['count'] = count( $to_remove );
foreach ( $to_remove as $revision ) {
wp_delete_post_revision( $revision->ID );
}
return $response;
}
/**
* Sanitize and store post-specifiy revisions quantity.
*
......@@ -452,10 +505,12 @@ class WP_Revisions_Control {
* @return array
*/
private function get_settings() {
if ( empty( self::$settings ) ) {
$post_types = $this->get_post_types();
static $hash = null;
$settings = get_option( $this->settings_section, array() );
$settings = get_option( $this->settings_section, array() );
if ( empty( self::$settings ) || $hash !== $this->hash_settings( $settings ) ) {
$post_types = $this->get_post_types();
if ( ! is_array( $settings ) ) {
$settings = array();
......@@ -472,11 +527,23 @@ class WP_Revisions_Control {
}
self::$settings = $merged_settings;
$hash = $this->hash_settings( self::$settings );
}
return self::$settings;
}
/**
* Hash settings to limit re-parsing.
*
* @param array $settings Settings array.
* @return string
*/
private function hash_settings( $settings ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
return md5( serialize( $settings ) );
}
/**
* Retrieve array of supported post types and their labels.
*
......@@ -519,7 +586,7 @@ class WP_Revisions_Control {
$_post = new WP_Post( (object) array( 'post_type' => $post_type ) );
$to_keep = wp_revisions_to_keep( $_post );
if ( $blank_for_all && -1 === $to_keep ) {
if ( $blank_for_all && ( -1 === $to_keep || '-1' === $to_keep ) ) {
return '';
} else {
return (int) $to_keep;
......@@ -535,7 +602,7 @@ class WP_Revisions_Control {
private function get_post_revisions_to_keep( $post_id ) {
$to_keep = get_post_meta( $post_id, $this->meta_key_limit, true );
if ( -1 === $to_keep || empty( $to_keep ) ) {
if ( empty( $to_keep ) || -1 === $to_keep || '-1' === $to_keep ) {
$to_keep = '';
} else {
$to_keep = (int) $to_keep;
......
......@@ -5,7 +5,7 @@ msgstr ""
"Project-Id-Version: WP Revisions Control 1.3\n"
"Report-Msgid-Bugs-To: "
"https://wordpress.org/support/plugin/wp-revisions-control\n"
"POT-Creation-Date: 2019-05-26 17:40:54+00:00\n"
"POT-Creation-Date: 2019-05-26 23:37:00+00:00\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
......@@ -25,17 +25,37 @@ msgstr ""
"X-Poedit-Bookmarks: \n"
"X-Textdomain-Support: yes\n"
#: inc/class-wp-revisions-control.php:149
#: inc/class-wp-revisions-control-bulk-actions.php:84
msgid "Purge excess revisions"
msgstr ""
#: inc/class-wp-revisions-control-bulk-actions.php:89
msgid "Purge ALL revisions"
msgstr ""
#: inc/class-wp-revisions-control-bulk-actions.php:269
msgid "Purged all revisions."
msgstr ""
#: inc/class-wp-revisions-control-bulk-actions.php:276
msgid "Purged excess revisions."
msgstr ""
#: inc/class-wp-revisions-control-bulk-actions.php:287
msgid "WP Revisions Control encountered an unspecified error."
msgstr ""
#: inc/class-wp-revisions-control.php:158
msgid ""
"Set the number of revisions to save for each post type listed. To retain "
"all revisions for a given post type, leave the field empty."
msgstr ""
#: inc/class-wp-revisions-control.php:150
#: inc/class-wp-revisions-control.php:159
msgid "If a post type isn't listed, revisions are not enabled for that post type."
msgstr ""
#: inc/class-wp-revisions-control.php:161
#: inc/class-wp-revisions-control.php:170
#. translators: 1. Filter tag.
msgid ""
"A local change is causing this plugin's functionality to run at a priority "
......@@ -43,58 +63,63 @@ msgid ""
"please unhook any functions from the %1$s filter."
msgstr ""
#: inc/class-wp-revisions-control.php:278
#: inc/class-wp-revisions-control.php:287
msgid "Revisions"
msgstr ""
#: inc/class-wp-revisions-control.php:300
#: inc/class-wp-revisions-control.php:309
msgid "Processing&hellip;"
msgstr ""
#: inc/class-wp-revisions-control.php:301
#: inc/class-wp-revisions-control.php:310
msgid "Are you sure you want to remove revisions from this post?"
msgstr ""
#: inc/class-wp-revisions-control.php:302
#: inc/class-wp-revisions-control.php:311
msgid "Autosave"
msgstr ""
#: inc/class-wp-revisions-control.php:303
#: inc/class-wp-revisions-control.php:312
msgid "There are no revisions to remove."
msgstr ""
#: inc/class-wp-revisions-control.php:304
#: inc/class-wp-revisions-control.php:313
msgid "An error occurred. Please refresh the page and try again."
msgstr ""
#: inc/class-wp-revisions-control.php:325
#: inc/class-wp-revisions-control.php:334
msgid "Purge these revisions"
msgstr ""
#: inc/class-wp-revisions-control.php:331
#: inc/class-wp-revisions-control.php:340
#. translators: 1. Text input field.
msgid ""
"Limit this post to %1$s revisions. Leave this field blank for default "
"behavior."
msgstr ""
#: inc/class-wp-revisions-control.php:356
#: inc/class-wp-revisions-control.php:365
msgid "No post ID was provided. Please refresh the page and try again."
msgstr ""
#: inc/class-wp-revisions-control.php:358
#: inc/class-wp-revisions-control.php:367
msgid "Invalid request. Please refresh the page and try again."
msgstr ""
#: inc/class-wp-revisions-control.php:360
#: inc/class-wp-revisions-control.php:369
msgid "You are not allowed to edit this post."
msgstr ""
#: inc/class-wp-revisions-control.php:392
#: inc/class-wp-revisions-control.php:401
#. translators: 1. Number of removed revisions, already formatted for locale.
msgid "Removed %1$s revisions associated with this post."
msgstr ""
#: inc/class-wp-revisions-control.php:427
#: inc/class-wp-revisions-control.php:439
msgid "No revisions to remove."
msgstr ""
#. Plugin Name of the plugin/theme
msgid "WP Revisions Control"
msgstr ""
......
......@@ -36,6 +36,7 @@ Navigate to **Settings > Writing** in your WordPress Dashboard, and look for the
== Changelog ==
= 1.3 =
* Add bulk actions to purge excess or all revisions.
* Introduce unit tests.
* Conform to coding standards.
......@@ -50,6 +51,9 @@ Navigate to **Settings > Writing** in your WordPress Dashboard, and look for the
== Upgrade Notice ==
= 1.3 =
Introduces bulk actions for purging revisions, along with unit tests. The plugin also conforms to coding standards.
= 1.2.1 =
Introduces Spanish translation thanks to Maria Ramos at [WebHostingHub](http://www.webhostinghub.com/).
......
......@@ -5,27 +5,39 @@
* @package WP_Revisions_Control
*/
$_tests_dir = getenv( 'WP_TESTS_DIR' );
$wp_revisions_control_tests_tests_dir = getenv( 'WP_TESTS_DIR' );
if ( ! $_tests_dir ) {
$_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib';
if ( ! $wp_revisions_control_tests_tests_dir ) {
$wp_revisions_control_tests_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib';
}
if ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) {
echo "Could not find $_tests_dir/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // WPCS: XSS ok.
if ( ! file_exists( $wp_revisions_control_tests_tests_dir . '/includes/functions.php' ) ) {
echo "Could not find $wp_revisions_control_tests_tests_dir/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // WPCS: XSS ok.
exit( 1 );
}
// Give access to tests_add_filter() function.
require_once $_tests_dir . '/includes/functions.php';
require_once $wp_revisions_control_tests_tests_dir . '/includes/functions.php';
/**
* Stub admin-only function not needed for testing.
*/
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound
if ( ! function_exists( 'post_revisions_meta_box' ) ) {
/**
* Stub for Core's revisions meta box.
*/
function post_revisions_meta_box() {}
}
// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound
/**
* Manually load the plugin being tested.
*/
function _manually_load_plugin() {
function wp_revisions_control_tests_manually_load_plugin() {
require dirname( dirname( __FILE__ ) ) . '/wp-revisions-control.php';
}
tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
tests_add_filter( 'muplugins_loaded', 'wp_revisions_control_tests_manually_load_plugin' );
// Start up the WP testing environment.
require $_tests_dir . '/includes/bootstrap.php';
require $wp_revisions_control_tests_tests_dir . '/includes/bootstrap.php';
......@@ -62,49 +62,4 @@ class TestHooks extends WP_UnitTestCase {
wp_revisions_to_keep( get_post( $post_id_unlimited ) )
);
}
/**
* Test revision purging.
*/
public function test_purge_all() {
$post_id = $this->factory->post->create();
$iterations = 10;
for ( $i = 0; $i < $iterations; $i++ ) {
wp_update_post(
array(
'ID' => $post_id,
'post_content' => wp_rand(),
)
);
}
$revisions_to_purge = count( wp_get_post_revisions( $post_id ) );
$this->assertEquals(
$iterations,
$revisions_to_purge,
'Failed to assert that there are revisions to purge.'
);
$purge = WP_Revisions_Control::get_instance()->do_purge_all( $post_id );
$revisions_remaining = count( wp_get_post_revisions( $post_id ) );
$this->assertEquals(
0,
$revisions_remaining,
'Failed to assert that all revisions were purged.'
);
$this->assertEquals(
10,
$purge['count'],
'Failed to assert that response includes expected count of purged revisions.'
);
$this->assertEquals(
'Removed 10 revisions associated with this post.',
$purge['success'],
'Failed to assert that response includes expected success message.'
);
}
}
<?php
/**
* Test miscellaneous methods.
*
* @package WP_Revisions_Control
*/
/**
* Class TestMisc.