diff --git a/.editorconfig b/.editorconfig index 79207a40cb9326b8c6b8c958fa864b5345f94e68..0fcdf7fd4fc5705384ea40f93be21e0fc85ac557 100755 --- a/.editorconfig +++ b/.editorconfig @@ -17,6 +17,3 @@ indent_size = 4 [{.jshintrc,*.json,*.yml}] indent_style = space indent_size = 2 - -[{*.txt,wp-config-sample.php}] -end_of_line = crlf diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 40c53b76bbfcdb3b614b4ff21a13d2a52de56d50..a4d480111b33701c1f5cd7785d3f506e883a7b21 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ variables: # Configure mysql service (https://hub.docker.com/_/mysql/) MYSQL_DATABASE: wordpress_tests MYSQL_ROOT_PASSWORD: mysql + WP_VERSION: latest cache: paths: @@ -10,7 +11,7 @@ cache: before_script: # Set up WordPress tests - - bash bin/install-wp-tests.sh $MYSQL_DATABASE root $MYSQL_ROOT_PASSWORD mysql latest true + - bash bin/install-wp-tests.sh $MYSQL_DATABASE root $MYSQL_ROOT_PASSWORD mysql $WP_VERSION true # PHPUnit - | @@ -20,10 +21,43 @@ before_script: composer global require "phpunit/phpunit=4.8.*" fi - # Install PHPCS and WPCS - - composer global require automattic/vipwpcs - - composer global require phpcompatibility/phpcompatibility-wp - - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/automattic/vipwpcs,$HOME/.composer/vendor/phpcompatibility/php-compatibility,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-paragonie,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-wp +PHPunit:PHP5.3:MySQL: + stage: test + variables: + WP_VERSION: 5.1.1 + image: containers.ethitter.com:443/docker/images/php:5.3 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP5.6:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:5.6 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP7.0:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.0 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP7.1:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.1 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit PHPunit:PHP7.2:MySQL: stage: test @@ -32,9 +66,7 @@ PHPunit:PHP7.2:MySQL: - mysql:5.6 script: - find . -type "f" -iname "*.php" | xargs -L "1" php -l - - phpcs -n - phpunit - allow_failure: true PHPunit:PHP7.3:MySQL: stage: test @@ -43,13 +75,21 @@ PHPunit:PHP7.3:MySQL: - mysql:5.6 script: - find . -type "f" -iname "*.php" | xargs -L "1" php -l - - phpcs -n - phpunit - allow_failure: true + +PHPCS: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.3 + before_script: + - composer global require automattic/vipwpcs + - composer global require phpcompatibility/phpcompatibility-wp + - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/automattic/vipwpcs,$HOME/.composer/vendor/phpcompatibility/php-compatibility,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-paragonie,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-wp + script: + - phpcs -n PluginSVN: stage: deploy - image: containers.ethitter.com:443/docker/images/php:7.3 + image: containers.ethitter.com:443/docker/wp-org-plugin-deploy:latest before_script: - curl -o ./bin/deploy.sh https://git-cdn.e15r.co/open-source/wp-org-plugin-deploy/raw/master/scripts/deploy.sh - chmod +x ./bin/deploy.sh diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 0c031ac22c1fc4ce8e1529485409ecb9a643dca7..0717712f19a0fcd0a4dd366ba27a86737f0dccdd 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -19,7 +19,7 @@ <!-- Rules: Check PHP version compatibility --> <!-- https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions --> - <config name="testVersion" value="7.2-"/> + <config name="testVersion" value="5.3-"/> <!-- https://github.com/PHPCompatibility/PHPCompatibilityWP --> <rule ref="PHPCompatibilityWP"/> diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..6c5b18cf143be14531cdd46247bfbd4cfe08bf04 --- /dev/null +++ b/LICENSE @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md index 38832a9c3aedd0d6076e4832f044c8fd23d55178..090e14d5620d72726e36d31dde61ffe03accdb18 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Tags:** revision, revisions, admin **Requires at least:** 3.6 **Tested up to:** 5.2 -**Stable tag:** 1.2.1 +**Stable tag:** 1.3 **License:** GPLv2 or later **License URI:** http://www.gnu.org/licenses/gpl-2.0.html @@ -35,6 +35,10 @@ Navigate to **Settings > Writing** in your WordPress Dashboard, and look for the ## Changelog ## +### 1.3 ### +* Introduce unit tests. +* Conform to coding standards. + ### 1.2.1 ### * Introduce Spanish translation thanks to Maria Ramos at [WebHostingHub](http://www.webhostinghub.com/). diff --git a/inc/class-wp-revisions-control.php b/inc/class-wp-revisions-control.php new file mode 100644 index 0000000000000000000000000000000000000000..87c8967ae384c693ab18bff7492fe53e0882e29a --- /dev/null +++ b/inc/class-wp-revisions-control.php @@ -0,0 +1,548 @@ +<?php +/** + * Main plugin functionality. + * + * @package WP_Revisions_Control + */ + +/** + * Class WP_Revisions_Control. + */ +class WP_Revisions_Control { + /** + * Singleton. + * + * @var static + */ + private static $__instance; + + /** + * Filter priority. + * + * @see $this->filter_priority() + * + * @var int + */ + private static $priority = null; + + /** + * Default filter priority. + * + * @var int + */ + private $priority_default = 50; + + /** + * Supported post types. + * + * @see $this->get_post_types() + * + * @var array + */ + private static $post_types = array(); + + /** + * Plugin settings. + * + * @see $this->get_settings() + * + * @var array + */ + private static $settings = array(); + + /** + * WordPress options page to display settings on. + * + * @var string + */ + private $settings_page = 'writing'; + + /** + * Name of custom settings sections. + * + * @var string + */ + private $settings_section = 'wp_revisions_control'; + + /** + * Meta key holding post's revisions limit. + * + * @var string + */ + private $meta_key_limit = '_wp_rev_ctl_limit'; + + /** + * Silence is golden! + */ + private function __construct() {} + + /** + * Singleton implementation. + * + * @return static + */ + public static function get_instance() { + if ( ! is_a( static::$__instance, __CLASS__ ) ) { + static::$__instance = new self(); + + static::$__instance->setup(); + } + + return static::$__instance; + } + + /** + * Register actions and filters at `init` so others can interact, if desired. + */ + private function setup() { + add_action( 'plugins_loaded', array( $this, 'action_plugins_loaded' ) ); + add_action( 'init', array( $this, 'action_init' ) ); + } + + /** + * Load plugin translations. + */ + public function action_plugins_loaded() { + load_plugin_textdomain( 'wp_revisions_control', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' ); + } + + /** + * Register actions and filters. + */ + public function action_init() { + add_action( 'admin_init', array( $this, 'action_admin_init' ) ); + + add_filter( 'wp_revisions_to_keep', array( $this, 'filter_wp_revisions_to_keep' ), $this->plugin_priority(), 2 ); + } + + /** + * Register plugin's admin-specific elements. + * + * Plugin title is intentionally not translatable. + */ + public function action_admin_init() { + // 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 ) { + 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 ) ); + } + + // Post-level functionality. + 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' ) ); + } + + /** + * PLUGIN SETTINGS SECTION + * FOUND UNDER SETTINGS > WRITING + */ + + /** + * Display assistive text in settings section. + */ + public function settings_section_intro() { + ?> + <p><?php esc_html_e( '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.', 'wp_revisions_control' ); ?></p> + <p><?php esc_html_e( 'If a post type isn\'t listed, revisions are not enabled for that post type.', 'wp_revisions_control' ); ?></p> + <?php + + // Display a note if the plugin priority is other than the default. + // Will be useful when debugging issues later. + if ( $this->plugin_priority() !== $this->priority_default ) : + ?> + <p> + <?php + printf( + /* translators: 1. Filter tag. */ + esc_html__( + 'A local change is causing this plugin\'s functionality to run at a priority other than the default. If you experience difficulties with the plugin, please unhook any functions from the %1$s filter.', + 'wp_revisions_control' + ), + '<code>wp_revisions_control_priority</code>' + ); + ?> + </p> + <?php + endif; + } + + /** + * Render field for each post type. + * + * @param array $args Field arguments. + */ + public function field_post_type( $args ) { + $revisions_to_keep = $this->get_revisions_to_keep( $args['post_type'], true ); + ?> + <input type="text" name="<?php echo esc_attr( $this->settings_section . '[' . $args['post_type'] . ']' ); ?>" value="<?php echo esc_attr( $revisions_to_keep ); ?>" class="small-text" /> + <?php + } + + /** + * Sanitize plugin settings. + * + * @param array $options Unsanitized settings. + * @return array + */ + public function sanitize_options( $options ) { + $options_sanitized = array(); + + if ( is_array( $options ) ) { + foreach ( $options as $post_type => $to_keep ) { + $type_length = strlen( $to_keep ); + + if ( 0 === $type_length ) { + $to_keep = -1; + } else { + $to_keep = (int) $to_keep; + } + + // Lowest possible value is -1, used to indicate infinite revisions are stored. + if ( -1 > $to_keep ) { + $to_keep = -1; + } + + $options_sanitized[ $post_type ] = $to_keep; + } + } + + return $options_sanitized; + } + + /** + * REVISIONS QUANTITY OVERRIDES. + */ + + /** + * Allow others to change the priority this plugin's functionality runs at + * + * @uses apply_filters + * @return int + */ + private function plugin_priority() { + if ( is_null( self::$priority ) ) { + $plugin_priority = apply_filters( 'wp_revisions_control_priority', $this->priority_default ); + + self::$priority = is_numeric( $plugin_priority ) ? (int) $plugin_priority : $this->priority_default; + } + + return self::$priority; + } + + /** + * Override number of revisions to keep using plugin's settings. + * + * Can either be post-specific or universal. + * + * @param int $qty Number of revisions to keep. + * @param WP_Post $post Post object. + * @return int + */ + public function filter_wp_revisions_to_keep( $qty, $post ) { + $post_limit = get_post_meta( $post->ID, $this->meta_key_limit, true ); + + if ( 0 < strlen( $post_limit ) ) { + $qty = $post_limit; + } else { + $post_type = get_post_type( $post ) ? get_post_type( $post ) : $post->post_type; + $settings = $this->get_settings(); + + if ( array_key_exists( $post_type, $settings ) ) { + $qty = $settings[ $post_type ]; + } + } + + return $qty; + } + + /** + * POST-LEVEL FUNCTIONALITY. + */ + + /** + * Override Core's revisions metabox. + * + * @param string $post_type Post type. + * @param object $post Post object. + */ + public function action_add_meta_boxes( $post_type, $post ) { + if ( post_type_supports( $post_type, 'revisions' ) && 'auto-draft' !== get_post_status() && count( wp_get_post_revisions( $post ) ) > 1 ) { + // Replace the metabox. + remove_meta_box( 'revisionsdiv', null, 'normal' ); + add_meta_box( + 'revisionsdiv-wp-rev-ctl', + __( + 'Revisions', + 'wp_revisions_control' + ), + array( + $this, + 'revisions_meta_box', + ), + null, + 'normal', + 'core' + ); + + // A bit of JS for us. + $handle = 'wp-revisions-control-post'; + wp_enqueue_script( $handle, plugins_url( 'js/post.js', __DIR__ ), array( 'jquery' ), '20131205', true ); + wp_localize_script( + $handle, + $this->settings_section, + array( + 'namespace' => $this->settings_section, + 'action_base' => $this->settings_section, + 'processing_text' => __( 'Processing…', 'wp_revisions_control' ), + 'ays' => __( 'Are you sure you want to remove revisions from this post?', 'wp_revisions_control' ), + 'autosave' => __( 'Autosave', 'wp_revisions_control' ), + 'nothing_text' => wpautop( __( 'There are no revisions to remove.', 'wp_revisions_control' ) ), + 'error' => __( 'An error occurred. Please refresh the page and try again.', 'wp_revisions_control' ), + ) + ); + + // Add some styling to our metabox additions. + add_action( 'admin_head', array( $this, 'action_admin_head' ), 999 ); + } + } + + /** + * Render Revisions metabox with plugin's additions + * + * @param WP_Post $post Post object. + */ + public function revisions_meta_box( $post ) { + post_revisions_meta_box( $post ); + + ?> + <div id="<?php echo esc_attr( $this->settings_section ); ?>"> + <h4>WP Revisions Control</h4> + + <p class="button purge" data-postid="<?php the_ID(); ?>" data-nonce="<?php echo esc_attr( wp_create_nonce( $this->settings_section . '_purge' ) ); ?>"><?php _e( 'Purge these revisions', 'wp_revisions_control' ); ?></p> + + <p> + <?php + printf( + /* translators: 1. Text input field. */ + esc_html__( + '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" />' + ); + ?> + + <?php wp_nonce_field( $this->settings_section . '_limit', $this->settings_section . '_limit_nonce', false ); ?> + </p> + </div><!-- #<?php echo esc_attr( $this->settings_section ); ?> --> + <?php + } + + /** + * Process a post-specific request to purge revisions. + */ + public function ajax_purge() { + $post_id = isset( $_REQUEST['post_id'] ) ? (int) $_REQUEST['post_id'] : false; + + // Hold the current state of this Ajax request. + $response = array(); + + // Check for necessary data and capabilities. + if ( ! $post_id ) { + $response['error'] = __( 'No post ID was provided. Please refresh the page and try again.', 'wp_revisions_control' ); + } elseif ( ! check_ajax_referer( $this->settings_section . '_purge', 'nonce', false ) ) { + $response['error'] = __( 'Invalid request. Please refresh the page and try again.', 'wp_revisions_control' ); + } elseif ( ! current_user_can( 'edit_post', $post_id ) ) { + $response['error'] = __( 'You are not allowed to edit this post.', 'wp_revisions_control' ); + } + + // Request is valid if $response is still empty, as no errors arose above. + if ( empty( $response ) ) { + $response = $this->do_purge_all( $post_id ); + } + + // Pass the response back to JS. + echo json_encode( $response ); + exit; + } + + /** + * Remove all revisions from a given post ID. + * + * @param int $post_id Post ID to purge of revisions. + * @return array + */ + public function do_purge_all( $post_id ) { + $response = array(); + + $revisions = wp_get_post_revisions( $post_id ); + + $count = count( $revisions ); + + foreach ( $revisions as $revision ) { + wp_delete_post_revision( $revision->ID ); + } + + $response['success'] = sprintf( + /* translators: 1. Number of removed revisions, already formatted for locale. */ + esc_html__( + 'Removed %1$s revisions associated with this post.', + 'wp_revisions_control' + ), + number_format_i18n( $count, 0 ) + ); + + $response['count'] = $count; + + return $response; + } + + /** + * Sanitize and store post-specifiy revisions quantity. + * + * @param int $post_id Post ID. + */ + public function action_save_post( $post_id ) { + $nonce = $this->settings_section . '_limit_nonce'; + $qty = $this->settings_section . '_qty'; + + if ( isset( $_POST[ $nonce ], $_POST[ $qty ] ) && wp_verify_nonce( sanitize_text_field( $_POST[ $nonce ] ), $this->settings_section . '_limit' ) ) { + $limit = (int) $_POST[ $qty ]; + + if ( -1 === $limit || empty( $limit ) ) { + delete_post_meta( $post_id, $this->meta_key_limit ); + } else { + update_post_meta( $post_id, $this->meta_key_limit, absint( $limit ) ); + } + } + } + + /** + * Add a border between the regular revisions list and this plugin's additions. + */ + public function action_admin_head() { + ?> + <style type="text/css"> + #revisionsdiv-wp-rev-ctl #<?php echo esc_attr( $this->settings_section ); ?> { + border-top: 1px solid #dfdfdf; + padding-top: 0; + margin-top: 20px; + } + + #revisionsdiv-wp-rev-ctl #<?php echo esc_attr( $this->settings_section ); ?> h4 { + border-top: 1px solid #fff; + padding-top: 1.33em; + margin-top: 0; + } + </style> + <?php + } + + /** + * PLUGIN UTILITIES. + */ + + /** + * Retrieve plugin settings. + * + * @return array + */ + private function get_settings() { + if ( empty( self::$settings ) ) { + $post_types = $this->get_post_types(); + + $settings = get_option( $this->settings_section, array() ); + + if ( ! is_array( $settings ) ) { + $settings = array(); + } + + $merged_settings = array(); + + foreach ( $post_types as $post_type => $name ) { + if ( array_key_exists( $post_type, $settings ) ) { + $merged_settings[ $post_type ] = (int) $settings[ $post_type ]; + } else { + $merged_settings[ $post_type ] = - 1; + } + } + + self::$settings = $merged_settings; + } + + return self::$settings; + } + + /** + * Retrieve array of supported post types and their labels. + * + * @return array + */ + private function get_post_types() { + if ( empty( self::$post_types ) ) { + foreach ( get_post_types() as $type ) { + if ( post_type_supports( $type, 'revisions' ) ) { + $object = get_post_type_object( $type ); + + if ( null === $object ) { + continue; + } + + if ( property_exists( $object, 'labels' ) && property_exists( $object->labels, 'name' ) ) { + $name = $object->labels->name; + } else { + $name = $object->name; + } + + self::$post_types[ $type ] = $name; + } + } + } + + return self::$post_types; + } + + /** + * Retrieve number of revisions to keep for a given post type. + * + * @param string $post_type Post type. + * @param bool $blank_for_all Should blank value be used to indicate all are kept. + * @return int|string + */ + private function get_revisions_to_keep( $post_type, $blank_for_all = false ) { + // wp_revisions_to_keep() accepts a post object, not just the post type. + // We construct a new WP_Post object to ensure anything hooked to the wp_revisions_to_keep filter has the same basic data WP provides. + $_post = new WP_Post( (object) array( 'post_type' => $post_type ) ); + $to_keep = wp_revisions_to_keep( $_post ); + + if ( $blank_for_all && -1 === $to_keep ) { + return ''; + } else { + return (int) $to_keep; + } + } + + /** + * Retrieve number of revisions to keep for a give post. + * + * @param int $post_id Post ID. + * @return int|string + */ + 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 ) ) { + $to_keep = ''; + } else { + $to_keep = (int) $to_keep; + } + + return $to_keep; + } +} + +WP_Revisions_Control::get_instance(); diff --git a/languages/wp-revisions-control.pot b/languages/wp-revisions-control.pot index d271808ce3c82bb36a18c491bd1d7efd6c923eb7..f865ce422da4bd3827d768da24b48b20af8a5a9e 100644 --- a/languages/wp-revisions-control.pot +++ b/languages/wp-revisions-control.pot @@ -2,10 +2,10 @@ # This file is distributed under the same license as the WP Revisions Control package. msgid "" msgstr "" -"Project-Id-Version: WP Revisions Control 1.2.1\n" +"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-04-14 04:52:10+00:00\n" +"POT-Creation-Date: 2019-05-26 17:40:54+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -25,71 +25,74 @@ msgstr "" "X-Poedit-Bookmarks: \n" "X-Textdomain-Support: yes\n" -#: wp-revisions-control.php:148 +#: inc/class-wp-revisions-control.php:149 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 "" -#: wp-revisions-control.php:149 +#: inc/class-wp-revisions-control.php:150 msgid "If a post type isn't listed, revisions are not enabled for that post type." msgstr "" -#: wp-revisions-control.php:155 +#: inc/class-wp-revisions-control.php:161 +#. translators: 1. Filter tag. msgid "" "A local change is causing this plugin's functionality to run at a priority " "other than the default. If you experience difficulties with the plugin, " -"please unhook any functions from the %s filter." +"please unhook any functions from the %1$s filter." msgstr "" -#: wp-revisions-control.php:274 +#: inc/class-wp-revisions-control.php:278 msgid "Revisions" msgstr "" -#: wp-revisions-control.php:282 +#: inc/class-wp-revisions-control.php:300 msgid "Processing…" msgstr "" -#: wp-revisions-control.php:283 +#: inc/class-wp-revisions-control.php:301 msgid "Are you sure you want to remove revisions from this post?" msgstr "" -#: wp-revisions-control.php:284 +#: inc/class-wp-revisions-control.php:302 msgid "Autosave" msgstr "" -#: wp-revisions-control.php:285 +#: inc/class-wp-revisions-control.php:303 msgid "There are no revisions to remove." msgstr "" -#: wp-revisions-control.php:286 +#: inc/class-wp-revisions-control.php:304 msgid "An error occurred. Please refresh the page and try again." msgstr "" -#: wp-revisions-control.php:312 +#: inc/class-wp-revisions-control.php:325 msgid "Purge these revisions" msgstr "" -#: wp-revisions-control.php:315 +#: inc/class-wp-revisions-control.php:331 +#. translators: 1. Text input field. msgid "" -"Limit this post to %s revisions. Leave this field blank for default " +"Limit this post to %1$s revisions. Leave this field blank for default " "behavior." msgstr "" -#: wp-revisions-control.php:341 +#: inc/class-wp-revisions-control.php:356 msgid "No post ID was provided. Please refresh the page and try again." msgstr "" -#: wp-revisions-control.php:343 +#: inc/class-wp-revisions-control.php:358 msgid "Invalid request. Please refresh the page and try again." msgstr "" -#: wp-revisions-control.php:345 +#: inc/class-wp-revisions-control.php:360 msgid "You are not allowed to edit this post." msgstr "" -#: wp-revisions-control.php:357 -msgid "Removed %s revisions associated with this post." +#: inc/class-wp-revisions-control.php:392 +#. translators: 1. Number of removed revisions, already formatted for locale. +msgid "Removed %1$s revisions associated with this post." msgstr "" #. Plugin Name of the plugin/theme diff --git a/readme.txt b/readme.txt index df46245421b2ddad74ec4a7f90dc5d11d0770de7..d1cd518e69ff10dbc0da8a16812968bc21976734 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Donate link: https://ethitter.com/donate/ Tags: revision, revisions, admin Requires at least: 3.6 Tested up to: 5.2 -Stable tag: 1.2.1 +Stable tag: 1.3 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -35,6 +35,10 @@ Navigate to **Settings > Writing** in your WordPress Dashboard, and look for the == Changelog == += 1.3 = +* Introduce unit tests. +* Conform to coding standards. + = 1.2.1 = * Introduce Spanish translation thanks to Maria Ramos at [WebHostingHub](http://www.webhostinghub.com/). diff --git a/tests/test-hooks.php b/tests/test-hooks.php new file mode 100755 index 0000000000000000000000000000000000000000..7b6cdae66cf6b8978f1de696b705883cfdc2b2d2 --- /dev/null +++ b/tests/test-hooks.php @@ -0,0 +1,110 @@ +<?php +/** + * Test WP hooks. + * + * @package WP_Revisions_Control + */ + +/** + * Class TestHooks. + */ +class TestHooks extends WP_UnitTestCase { + /** + * Plugin slug used in many settings etc. + * + * @var string + */ + protected static $settings_section = 'wp_revisions_control'; + + /** + * Plugin's limit meta key. + * + * @var string + */ + protected static $meta_key = '_wp_rev_ctl_limit'; + + /** + * Test saving post's revisions limit. + */ + public function test_save_post() { + $post_id = $this->factory->post->create(); + $expected = 92; + + $_POST[ static::$settings_section . '_limit_nonce' ] = wp_create_nonce( static::$settings_section . '_limit' ); + $_POST[ static::$settings_section . '_qty' ] = $expected; + + WP_Revisions_Control::get_instance()->action_save_post( $post_id ); + + $to_keep = (int) get_post_meta( $post_id, static::$meta_key, true ); + $to_keep_filtered = wp_revisions_to_keep( get_post( $post_id ) ); + + $this->assertEquals( $expected, $to_keep ); + $this->assertEquals( $expected, $to_keep_filtered ); + } + + /** + * Test limits, ensuring no leakage. + */ + public function test_limits() { + $post_id_limited = $this->factory->post->create(); + $post_id_unlimited = $this->factory->post->create(); + $expected = 47; + + update_post_meta( $post_id_limited, static::$meta_key, $expected ); + + $this->assertEquals( + $expected, + wp_revisions_to_keep( get_post( $post_id_limited ) ) + ); + + $this->assertEquals( + -1, + 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.' + ); + } +} diff --git a/tests/test-sample.php b/tests/test-sample.php deleted file mode 100755 index 6c88ac8560879088d217111b2b4bab2227b6331f..0000000000000000000000000000000000000000 --- a/tests/test-sample.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php -/** - * Class SampleTest - * - * @package WP_Revisions_Control - */ - -/** - * Sample test case. - */ -class SampleTest extends WP_UnitTestCase { - - /** - * A single example test. - */ - public function test_sample() { - // Replace this with some actual testing code. - $this->assertTrue( true ); - } -} diff --git a/wp-revisions-control.php b/wp-revisions-control.php index 4049892213802e5fd3a4cffa0c2958a2aa53080e..1b09aca69b2c8dcfd30a41d5711e2e2f5a7e3dc3 100644 --- a/wp-revisions-control.php +++ b/wp-revisions-control.php @@ -1,508 +1,33 @@ <?php -/* -Plugin Name: WP Revisions Control -Plugin URI: https://ethitter.com/plugins/wp-revisions-control/ -Description: Control how many revisions are stored for each post type -Author: Erick Hitter -Version: 1.2.1 -Author URI: https://ethitter.com/ -Text Domain: wp_revisions_control -Domain Path: /languages/ - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -class WP_Revisions_Control { - /** - * Singleton - */ - private static $__instance = null; - - /** - * Class variables - */ - private static $priority = null; // use $this->plugin_priority() - private $priority_default = 50; - - private static $post_types = array(); // use $this->get_post_types() - private static $settings = array(); // use $this->get_settings() - - private $settings_page = 'writing'; - private $settings_section = 'wp_revisions_control'; - - private $meta_key_limit = '_wp_rev_ctl_limit'; - - /** - * Silence is golden! - */ - private function __construct() {} - - /** - * Singleton implementation - * - * @uses self::setup - * @return object - */ - public static function get_instance() { - if ( ! is_a( self::$__instance, __CLASS__ ) ) { - self::$__instance = new self; - - self::$__instance->setup(); - } - - return self::$__instance; - } - - /** - * Register actions and filters at `init` so others can interact, if desired. - * - * @uses add_action - * @return null - */ - private function setup() { - add_action( 'plugins_loaded', array( $this, 'action_plugins_loaded' ) ); - add_action( 'init', array( $this, 'action_init' ) ); - } - - /** - * Load plugin translations - * - * @uses load_plugin_textdomain - * @uses plugin_basename - * @action plugins_loaded - * @return null - */ - public function action_plugins_loaded() { - load_plugin_textdomain( 'wp_revisions_control', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' ); - } - - /** - * Register actions and filters - * - * @uses add_action - * @uses add_filter - * @uses this::plugin_priority - * @return null - */ - public function action_init() { - add_action( 'admin_init', array( $this, 'action_admin_init' ) ); - - add_filter( 'wp_revisions_to_keep', array( $this, 'filter_wp_revisions_to_keep' ), $this->plugin_priority(), 2 ); - } - - /** - * Register plugin's admin-specific elements - * - * Plugin title is intentionally not translatable. - * - * @uses register_setting - * @uses add_settings_section - * @uses __ - * @uses this::get_post_types - * @uses add_settings_field - * @action admin_init - * @return null - */ - public function action_admin_init() { - // 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 ) { - 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 ) ); - } - - // Post-level functionality - 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' ) ); - } - - /** - ** PLUGIN SETTINGS SECTION - ** FOUND UNDER SETTINGS > WRITING - **/ - - /** - * Display assistive text in settings section - * - * @uses _e - * @uses this::plugin_priority - * @return string - */ - public function settings_section_intro() { - ?> - <p><?php _e( '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.', 'wp_revisions_control' ); ?></p> - <p><?php _e( "If a post type isn't listed, revisions are not enabled for that post type.", 'wp_revisions_control' ); ?></p> - <?php - - // Display a note if the plugin priority is other than the default. - // Will be useful when debugging issues later. - if ( $this->plugin_priority() !== $this->priority_default ) : ?> - <p><?php printf( __( "A local change is causing this plugin's functionality to run at a priority other than the default. If you experience difficulties with the plugin, please unhook any functions from the %s filter.", 'wp_revisions_control' ), '<code>wp_revisions_control_priority</code>' ); ?></p> - <?php endif; - } - - /** - * Render field for each post type - * - * @param array $args - * @uses this::get_revisions_to_keep - * @uses esc_attr - * @return string - */ - public function field_post_type( $args ) { - $revisions_to_keep = $this->get_revisions_to_keep( $args['post_type'], true ); - ?> - <input type="text" name="<?php echo esc_attr( $this->settings_section . '[' . $args['post_type'] . ']' ); ?>" value="<?php echo esc_attr( $revisions_to_keep ); ?>" class="small-text" /> - <?php - } - - /** - * Sanitize plugin settings - * - * @param array $options - * @return array - */ - public function sanitize_options( $options ) { - $options_sanitized = array(); - - if ( is_array( $options ) ) { - foreach ( $options as $post_type => $to_keep ) { - if ( 0 === strlen( $to_keep ) ) - $to_keep = -1; - else - $to_keep = intval( $to_keep ); - - // Lowest possible value is -1, used to indicate infinite revisions are stored - if ( -1 > $to_keep ) - $to_keep = -1; - - $options_sanitized[ $post_type ] = $to_keep; - } - } - - return $options_sanitized; - } - - /** - ** REVISIONS QUANTITY OVERRIDES - **/ - - /** - * Allow others to change the priority this plugin's functionality runs at - * - * @uses apply_filters - * @return int - */ - private function plugin_priority() { - if ( is_null( self::$priority ) ) { - $plugin_priority = apply_filters( 'wp_revisions_control_priority', $this->priority_default ); - - self::$priority = is_numeric( $plugin_priority ) ? (int) $plugin_priority : $this->priority_default; - } - - return self::$priority; - } - - /** - * Override number of revisions to keep using plugin's settings - * - * Can either be post-specific or universal - * - * @uses get_post_meta - * @uses get_post_type - * @uses this::get_settings - * @filter wp_revisions_to_keep - * @return mixed - */ - public function filter_wp_revisions_to_keep( $qty, $post ) { - $post_limit = get_post_meta( $post->ID, $this->meta_key_limit, true ); - - if ( 0 < strlen( $post_limit ) ) { - $qty = $post_limit; - } else { - $post_type = get_post_type( $post ) ? get_post_type( $post ) : $post->post_type; - $settings = $this->get_settings(); - - if ( array_key_exists( $post_type, $settings ) ) - $qty = $settings[ $post_type ]; - } - - return $qty; - } - - /** - ** POST-LEVEL FUNCTIONALITY - **/ - - /** - * Override Core's revisions metabox - * - * @param string $post_type - * @param object $post - * @uses post_type_supports - * @uses get_post_status - * @uses wp_get_post_revisions - * @uses remove_meta_box - * @uses add_meta_box - * @uses wp_enqueue_script - * @uses plugins_url - * @uses wp_localize_script - * @uses wpautop - * @uses add_action - * @action add_meta_boxes - * @return null - */ - public function action_add_meta_boxes( $post_type, $post ) { - if ( post_type_supports( $post_type, 'revisions' ) && 'auto-draft' != get_post_status() && count( wp_get_post_revisions( $post ) ) > 1 ) { - // Replace the metabox - remove_meta_box( 'revisionsdiv', null, 'normal' ); - add_meta_box( 'revisionsdiv-wp-rev-ctl', __('Revisions', 'wp_revisions_control'), array( $this, 'revisions_meta_box' ), null, 'normal', 'core' ); - - // A bit of JS for us - $handle = 'wp-revisions-control-post'; - wp_enqueue_script( $handle, plugins_url( 'js/post.js', __FILE__ ), array( 'jquery' ), '20131205', true ); - wp_localize_script( $handle, $this->settings_section, array( - 'namespace' => $this->settings_section, - 'action_base' => $this->settings_section, - 'processing_text' => __( 'Processing…', 'wp_revisions_control' ), - 'ays' => __( 'Are you sure you want to remove revisions from this post?', 'wp_revisions_control' ), - 'autosave' => __( 'Autosave', 'wp_revisions_control' ), - 'nothing_text' => wpautop( __( 'There are no revisions to remove.', 'wp_revisions_control' ) ), - 'error' => __( 'An error occurred. Please refresh the page and try again.', 'wp_revisions_control' ) - ) ); - - // Add some styling to our metabox additions - add_action( 'admin_head', array( $this, 'action_admin_head' ), 999 ); - } - } - - /** - * Render Revisions metabox with plugin's additions - * - * @uses post_revisions_meta_box - * @uses the_ID - * @uses esc_attr - * @uses wp_create_nonce - * @uses this::get_post_revisions_to_keep - * @uses wp_nonce_field - * @return string - */ - public function revisions_meta_box( $post ) { - post_revisions_meta_box( $post ); - - ?> - <div id="<?php echo esc_attr( $this->settings_section ); ?>"> - <h4>WP Revisions Control</h4> - - <p class="button purge" data-postid="<?php the_ID(); ?>" data-nonce="<?php echo esc_attr( wp_create_nonce( $this->settings_section . '_purge' ) ); ?>"><?php _e( 'Purge these revisions', 'wp_revisions_control' ); ?></p> - - <p> - <?php printf( __( 'Limit this post to %s revisions. Leave this field blank for default behavior.', 'wp_revisions_control' ), '<input type="text" name="' . $this->settings_section . '_qty" value="' . $this->get_post_revisions_to_keep( $post->ID ) . '" id="' . $this->settings_section . '_qty" size="2" />' ); ?> - - <?php wp_nonce_field( $this->settings_section . '_limit', $this->settings_section . '_limit_nonce', false ); ?> - </p> - </div><!-- #<?php echo esc_attr( $this->settings_section ); ?> --> - <?php - } - - /** - * Process a post-specific request to purge revisions - * - * @uses __ - * @uses check_ajax_referer - * @uses current_user_can - * @uses wp_get_post_revisions - * @uses number_format_i18n - * @return string - */ - public function ajax_purge() { - $post_id = isset( $_REQUEST['post_id'] ) ? (int) $_REQUEST['post_id'] : false; - - // Hold the current state of this Ajax request - $response = array(); - - // Check for necessary data and capabilities - if ( ! $post_id ) - $response['error'] = __( 'No post ID was provided. Please refresh the page and try again.', 'wp_revisions_control' ); - elseif ( ! check_ajax_referer( $this->settings_section . '_purge', 'nonce', false ) ) - $response['error'] = __( 'Invalid request. Please refresh the page and try again.', 'wp_revisions_control' ); - elseif ( ! current_user_can( 'edit_post', $post_id ) ) - $response['error'] = __( 'You are not allowed to edit this post.', 'wp_revisions_control' ); - - // Request is valid if $response is still empty, as no errors arose above - if ( empty( $response ) ) { - $revisions = wp_get_post_revisions( $post_id ); - - $count = count( $revisions ); - - foreach ( $revisions as $revision ) { - wp_delete_post_revision( $revision->ID ); - } - - $response['success'] = sprintf( __( 'Removed %s revisions associated with this post.', 'wp_revisions_control' ), number_format_i18n( $count, 0 ) ); - $response['count'] = $count; - } - - // Pass the response back to JS - echo json_encode( $response ); - exit; - } - - /** - * Sanitize and store post-specifiy revisions quantity - * - * @uses wp_verify_nonce - * @uses update_post_meta - * @action save_post - * @return null - */ - public function action_save_post( $post_id ) { - if ( isset( $_POST[ $this->settings_section . '_limit_nonce' ] ) && wp_verify_nonce( $_POST[ $this->settings_section . '_limit_nonce' ], $this->settings_section . '_limit' ) && isset( $_POST[ $this->settings_section . '_qty' ] ) ) { - $limit = $_POST[ $this->settings_section . '_qty' ]; - - if ( -1 == $limit || empty( $limit ) ) - delete_post_meta( $post_id, $this->meta_key_limit ); - else - update_post_meta( $post_id, $this->meta_key_limit, absint( $limit ) ); - } - } - - /** - * Add a border between the regular revisions list and this plugin's additions - * - * @uses esc_attr - * @action admin_head - * @return string - */ - public function action_admin_head() { - ?> - <style type="text/css"> - #revisionsdiv-wp-rev-ctl #<?php echo esc_attr( $this->settings_section ); ?> { - border-top: 1px solid #dfdfdf; - padding-top: 0; - margin-top: 20px; - } - - #revisionsdiv-wp-rev-ctl #<?php echo esc_attr( $this->settings_section ); ?> h4 { - border-top: 1px solid #fff; - padding-top: 1.33em; - margin-top: 0; - } - </style> - <?php - } - - /** - ** PLUGIN UTILITIES - **/ - - /** - * Retrieve plugin settings - * - * @uses this::get_post_types - * @uses get_option - * @return array - */ - private function get_settings() { - if ( empty( self::$settings ) ) { - $post_types = $this->get_post_types(); - - $settings = get_option( $this->settings_section, array() ); - - $merged_settings = array(); - - foreach ( $post_types as $post_type => $name ) { - if ( array_key_exists( $post_type, $settings ) ) - $merged_settings[ $post_type ] = (int) $settings[ $post_type ]; - else - $merged_settings[ $post_type ] = -1; - } - - self::$settings = $merged_settings; - } - - return self::$settings; - } - - /** - * Retrieve array of supported post types and their labels - * - * @uses get_post_types - * @uses post_type_supports - * @uses get_post_type_object - * @return array - */ - private function get_post_types() { - if ( empty( self::$post_types ) ) { - $types = get_post_types(); - - foreach ( $types as $type ) { - if ( post_type_supports( $type, 'revisions' ) ) { - $object = get_post_type_object( $type ); - - if ( property_exists( $object, 'labels' ) && property_exists( $object->labels, 'name' ) ) - $name = $object->labels->name; - else - $name = $object->name; - - self::$post_types[ $type ] = $name; - } - } - } - - return self::$post_types; - } - - /** - * Retrieve number of revisions to keep for a given post type - * - * @uses WP_Post - * @uses wp_revisions_to_keep - * @return mixed - */ - private function get_revisions_to_keep( $post_type, $blank_for_all = false ) { - // wp_revisions_to_keep() accepts a post object, not just the post type - // We construct a new WP_Post object to ensure anything hooked to the wp_revisions_to_keep filter has the same basic data WP provides. - $_post = new WP_Post( (object) array( 'post_type' => $post_type ) ); - $to_keep = wp_revisions_to_keep( $_post ); - - if ( $blank_for_all && -1 == $to_keep ) - return ''; - else - return (int) $to_keep; - } - - /** - * Retrieve number of revisions to keep for a give post - * - * @param int $post_id - * @uses get_post_meta - * @return mixed - */ - 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 ) ) - $to_keep = ''; - else - $to_keep = (int) $to_keep; - - return $to_keep; - } -} -WP_Revisions_Control::get_instance(); +/** + * Load plugin. + * + * @package WP_Revisions_Control + */ + +/** + * Plugin Name: WP Revisions Control + * Plugin URI: https://ethitter.com/plugins/wp-revisions-control/ + * Description: Control how many revisions are stored for each post type + * Author: Erick Hitter + * Version: 1.3 + * Author URI: https://ethitter.com/ + * Text Domain: wp_revisions_control + * Domain Path: /languages/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +require_once __DIR__ . '/inc/class-wp-revisions-control.php';