stevegrunwell/revision-strike

View on GitHub
includes/class-revision-strike.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php
/**
 * Primary plugin functionality.
 *
 * @package Revision Strike
 * @author  Steve Grunwell
 */

/**
 * Class that hooks into WordPress, determines revision IDs, and strikes them from the database.
 */
class RevisionStrike {

    /**
     * The plugin settings.
     *
     * @var RevisionStrikeSettings $settings
     */
    public $settings;

    /**
     * The canonical source for default settings.
     *
     * @var array $defaults
     */
    protected $defaults;

    /**
     * Information about Revision Strike's current state.
     *
     * @var array $statistics
     */
    protected $statistics;

    /**
     * The batch size when striking revisions.
     */
    const BATCH_SIZE = 50;

    /**
     * The action called to trigger the clean-up process.
     */
    const STRIKE_ACTION = 'revisionstrike_strike_old_revisions';

    /**
     * Class constructor.
     */
    public function __construct() {
        $this->settings   = new RevisionStrikeSettings( $this );
        $this->defaults   = array(
            'days'      => 30,
            'limit'     => 50,
            'post_type' => 'post',
        );
        $this->statistics = array(
            'count'   => 0, // Number of revision IDs found.
            'deleted' => 0, // Number of revisions deleted.
        );

        $this->add_hooks();
    }

    /**
     * Register hooks within WordPress.
     */
    public function add_hooks() {
        add_action( self::STRIKE_ACTION, array( $this, 'strike' ) );
        add_action( 'admin_init', array( $this->settings, 'add_settings_section' ) );
        add_action( 'admin_menu', array( $this->settings, 'add_tools_page' ) );
        add_action( 'wp_delete_post_revision', array( $this, 'count_deleted_revision' ) );
    }

    /**
     * Increment $this->statistics['deleted'] by one every time a post revision is removed.
     */
    public function count_deleted_revision() {
        $this->statistics['deleted']++;
    }

    /**
     * Count the number of post revisions that are eligible to be struck for the given threshold and
     * post types.
     *
     * @global $wpdb
     *
     * @param int    $days      The number of days old a post must be in order for its revisions to
     *                          be struck.
     * @param string $post_type Post types for which revisions should be struck.
     * @return int The number of matching post revisions in the database.
     */
    public function count_eligible_revisions( $days, $post_type ) {
        global $wpdb;

        $post_type_in_string = $this->get_slug_in_string( $post_type );
        // @codingStandardsIgnoreStart
        $count     = $wpdb->get_var( $wpdb->prepare(
            "
            SELECT COUNT(r.ID) FROM $wpdb->posts r
            LEFT JOIN $wpdb->posts p ON r.post_parent = p.ID
            WHERE r.post_type = 'revision' AND p.post_type IN ($post_type_in_string) AND p.post_date < %s
            ",
            date( 'Y-m-d', time() - ( absint( $days ) * DAY_IN_SECONDS ) )
        ) );
        // @codingStandardsIgnoreEnd

        return absint( $count );
    }

    /**
     * Return the current default Revision Strike settings.
     *
     * @return array An array of default settings.
     */
    public function get_defaults() {
        return $this->defaults;
    }

    /**
     * Return the current statistics for this RevisionStrike instance.
     *
     * The statistics array contains the following keys:
     * - count: The number of revision IDs found, up to the limit that was passed to strike().
     * - deleted: The number of revisions that have been deleted. This number should always be <= the
     *            value of "count".
     *
     * @return array An array of statistics.
     */
    public function get_stats() {
        return $this->statistics;
    }

    /**
     * Clean up ("strike") post revisions for posts that have been published for at least $days days.
     *
     * @param array $args {
     *   Optional. An array of arguments.
     *
     *   @type int    $days      The number of days old a post must be in order for its revisions to
     *                           be struck. Default is 30.
     *   @type int    $limit     The maximum number of posts to delete in a single pass (when run as
     *                           cron, per day). Default is 50.
     *   @type string $post_type A comma-separated list of post types for which the revisions should
     *                           be purged. By default, only revisions for posts will be purged.
     * }
     */
    public function strike( $args = array() ) {
        $default_args = array(
            'days'      => $this->settings->get_option( 'days' ),
            'limit'     => $this->settings->get_option( 'limit' ),
            'post_type' => $this->settings->get_option( 'post_type' ),
        );
        $args         = wp_parse_args( $args, $default_args );

        /**
         * Set the post type(s) for which revisions should be struck.
         *
         * @param string $post_type A comma-separated list of post types.
         */
        $args['post_type'] = apply_filters( 'revisionstrike_post_types', $args['post_type'] );

        // Calculate the number of batches to run.
        $limit       = self::BATCH_SIZE >= $args['limit'] ? $args['limit'] : self::BATCH_SIZE;
        $batch_count = ceil( $args['limit'] / $limit );

        for ( $i = 0; $i < $batch_count; $i++ ) {
            $revision_ids = $this->get_revision_ids( $args['days'], $limit, $args['post_type'] );

            if ( ! empty( $revision_ids ) ) {
                array_map( 'wp_delete_post_revision', $revision_ids );
            }
        }
    }

    /**
     * Converts a comma-delimited list of slugs into a string usable
     * as with an SQL IN statement.
     *
     * This mimics the functionality in core for building IN strings.
     *
     * @see get_page_by_path()
     *
     * @param  string $slugs Comma-delimited list of slugs (post,page).
     * @return string List of slugs for IN statement ('post','page').
     */
    protected function get_slug_in_string( $slugs ) {

        // Split the list into an array.
        $slugs = explode( ',', $slugs );

        // Run esc_sql on the array of slugs.
        $slugs = esc_sql( $slugs );

        // Return a string usable in an IN statement.
        return "'" . implode( "','", $slugs ) . "'";
    }

    /**
     * Find revisions eligible to be removed from the database.
     *
     * @global $wpdb
     *
     * @param int   $days      The number of days since a post's publish date that must pass before
     *                         we can purge the post revisions.
     * @param int   $limit     The maximum number of revision IDs to retrieve.
     * @param array $post_type The post types for which revisions should be located.
     *
     * @return array An array of post IDs (unless 'fields' is manipulated in $args).
     */
    protected function get_revision_ids( $days, $limit, $post_type ) {
        global $wpdb;

        // Return early if we don't have any eligible post types.
        if ( ! $post_type ) {
            return array();
        }

        $post_type_in_string = $this->get_slug_in_string( $post_type );
        // @codingStandardsIgnoreStart
        $revision_ids        = $wpdb->get_col( $wpdb->prepare(
            "
            SELECT r.ID FROM $wpdb->posts r
            LEFT JOIN $wpdb->posts p ON r.post_parent = p.ID
            WHERE r.post_type = 'revision' AND p.post_type IN ($post_type_in_string) AND p.post_date < %s
            ORDER BY p.post_date ASC
            LIMIT %d
            ",
            date( 'Y-m-d', time() - ( absint( $days ) * DAY_IN_SECONDS ) ),
            absint( $limit )
        ) );
        // @codingStandardsIgnoreEnd

        /**
         * Filter the list of eligible revision IDs.
         *
         * @since 0.3.0
         *
         * @param array $revision_ids Revision IDs to be struck.
         * @param int   $days      The number of days since a post's publish date that must pass before
         *                         we can purge the post revisions.
         * @param int   $limit     The maximum number of revision IDs to retrieve.
         * @param array $post_type The post types for which revisions should be located.
         */
        $revision_ids = apply_filters( 'revisionstrike_get_revision_ids', $revision_ids, $days, $limit, $post_type );

        $this->statistics['count'] = count( $revision_ids );

        return $revision_ids;
    }
}