Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Cleanup_Stored_Paths
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 7
380
0.00% covered (danger)
0.00%
0 / 1
 setup_schedule
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 add_cleanup_actions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 clear_schedules
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 run_cleanup
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 cleanup_stored_paths_batch
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 delete_static_file_by_hash
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 get_stored_paths
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace Automattic\Jetpack_Boost\Lib\Minify;
4
5/**
6 * Takes care of cleaning up options created during concatenation.
7 *
8 * @since 4.1.2
9 */
10class Cleanup_Stored_Paths {
11
12    /**
13     * The maximum number of options to process in a single batch.
14     *
15     * @var int
16     */
17    private $max_options_to_process = 50;
18
19    /**
20     * The key of the option that stores the ID of the last processed option.
21     *
22     * @var string
23     */
24    private $last_processed_option_key = 'jetpack_boost_cleanup_concat_paths_last_processed_option_id';
25
26    /**
27     * Whether to schedule a followup cleanup.
28     *
29     * @var bool
30     */
31    private $should_schedule_followup = false;
32
33    /**
34     * Schedules the start of the cleanup.
35     */
36    public static function setup_schedule() {
37        if ( false === wp_next_scheduled( 'jetpack_boost_minify_cron_cleanup_concat_paths' ) ) {
38            wp_schedule_event( time(), 'daily', 'jetpack_boost_minify_cron_cleanup_concat_paths' );
39        }
40    }
41
42    /**
43     * Hooks the callbacks for the cleanup.
44     */
45    public static function add_cleanup_actions() {
46        add_action( 'jetpack_boost_minify_cron_cleanup_concat_paths', array( __CLASS__, 'run_cleanup' ) );
47        add_action( 'jetpack_boost_minify_cron_cleanup_concat_paths_followup', array( __CLASS__, 'run_cleanup' ) );
48    }
49
50    /**
51     * Clears the cleanup schedules.
52     */
53    public static function clear_schedules() {
54        wp_unschedule_hook( 'jetpack_boost_minify_cron_cleanup_concat_paths' );
55        wp_unschedule_hook( 'jetpack_boost_minify_cron_cleanup_concat_paths_followup' );
56    }
57
58    /**
59     * Runs the cleanup for the first batch,
60     * and if there are more entries to process,
61     * schedules the cleanup for the next batch.
62     */
63    public static function run_cleanup() {
64        $cleanup      = new Cleanup_Stored_Paths();
65        $can_continue = $cleanup->cleanup_stored_paths_batch();
66        if ( ! $can_continue ) {
67            return;
68        }
69
70        if ( ! $cleanup->should_schedule_followup ) {
71            return;
72        }
73
74        // 'batch' arg is necessary, to tell WP that this is a unique event
75        // and allow it to be run within 10 minutes of the last.
76        // See https://developer.wordpress.org/reference/functions/wp_schedule_single_event/#description
77        if ( ! wp_next_scheduled( 'jetpack_boost_minify_cron_cleanup_concat_paths_followup', array( 'batch' ) ) ) {
78            wp_schedule_single_event( time(), 'jetpack_boost_minify_cron_cleanup_concat_paths_followup', array( 'batch' ) );
79        }
80    }
81
82    /**
83     * Cleans up expired stored paths.
84     *
85     * @return bool True if there are more entries to process, false if not.
86     */
87    public function cleanup_stored_paths_batch() {
88        $stored_paths = $this->get_stored_paths();
89        if ( ! $stored_paths ) {
90            // Cleanup after the cleanup.
91            delete_option( $this->last_processed_option_key );
92            return false;
93        }
94
95        // Used to tell the cleanup to skip the entries that were checked in the previous run.
96        // Avoids processing the same entries over and over again.
97        $last_processed_option_id = false;
98
99        foreach ( $stored_paths as $option ) {
100            $value = maybe_unserialize( $option['option_value'] );
101            if ( ! is_array( $value ) ) {
102                continue;
103            }
104
105            if ( $value['expire'] <= time() ) {
106                $this->delete_static_file_by_hash( str_replace( 'jb_transient_concat_paths_', '', $option['option_name'] ) );
107                delete_option( $option['option_name'] );
108            }
109
110            $last_processed_option_id = $option['option_id'];
111        }
112
113        update_option( $this->last_processed_option_key, $last_processed_option_id, false );
114
115        return true;
116    }
117
118    /**
119     * Deletes the static files by hash.
120     *
121     * @param string $hash The hash of the file.
122     * @return void
123     */
124    private function delete_static_file_by_hash( $hash ) {
125        // Since we don't have a way to know if this file is JS or CSS,
126        // we delete both.
127
128        $js_file_path = jetpack_boost_get_minify_file_path( $hash . '.min.js' );
129        if ( file_exists( $js_file_path ) ) {
130            wp_delete_file( $js_file_path );
131        }
132
133        $css_file_path = jetpack_boost_get_minify_file_path( $hash . '.min.css' );
134        if ( file_exists( $css_file_path ) ) {
135            wp_delete_file( $css_file_path );
136        }
137    }
138
139    /**
140     * Gets the stored paths to process, and skips the ones that were checked in the previous run.
141     * Also sets a flag that determines if there should be a followup cleanup or not.
142     *
143     * @return array The stored paths.
144     */
145    private function get_stored_paths() {
146        $last_processed_option_id = get_option( $this->last_processed_option_key );
147
148        global $wpdb;
149        $query = "SELECT * FROM {$wpdb->options} WHERE option_name LIKE 'jb_transient_concat_paths_%'";
150        if ( $last_processed_option_id ) {
151            $query .= $wpdb->prepare( ' AND option_id > %d', $last_processed_option_id );
152        }
153
154        // Add 1 to use it as a flag to know if there are more entries to process.
155        $max_options_to_process_offset = $this->max_options_to_process + 1;
156        $query                        .= $wpdb->prepare( ' ORDER BY option_id ASC LIMIT %d', $max_options_to_process_offset );
157
158        $stored_paths = $wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
159
160        // If the number of stored paths is equal to the offset of max options to process,
161        // it means that there are more entries to process, so a followup cleanup is needed.
162        if ( count( $stored_paths ) === $max_options_to_process_offset ) {
163            $this->should_schedule_followup = true;
164
165            // Since 1 was added to the limit, it needs to be removed from the list.
166            array_pop( $stored_paths );
167        }
168
169        return $stored_paths;
170    }
171}