Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
Cleanup_Transients
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
2 / 2
14
100.00% covered (success)
100.00%
1 / 1
 run_cleanup
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
10
 is_expired
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3namespace Automattic\Jetpack_Boost\Lib;
4
5use Automattic\Jetpack\Boost_Core\Lib\Transient;
6
7/**
8 * Deletes expired Boost options without caching every live payload in the request.
9 *
10 * @since $$next-version$$
11 */
12class Cleanup_Transients {
13
14    const FOLLOWUP_HOOK = 'jetpack_boost_general_cleanup_followup';
15
16    // Larger than the stored-paths batch so a large backlog clears in few runs; rows are small.
17    private const BATCH_SIZE    = 1000;
18    private const CURSOR_OPTION = 'jetpack_boost_general_cleanup_last_processed_option_id';
19
20    /**
21     * Processes one batch and schedules the rest of the pass.
22     *
23     * @return void
24     */
25    public static function run_cleanup() {
26        global $wpdb;
27
28        $cursor = (int) get_option( self::CURSOR_OPTION, 0 );
29        $rows   = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
30            $wpdb->prepare(
31                "SELECT option_id, option_name, option_value FROM {$wpdb->options}
32                WHERE option_name LIKE %s AND option_id > %d
33                ORDER BY option_id ASC LIMIT %d",
34                $wpdb->esc_like( Transient::OPTION_PREFIX ) . '%',
35                $cursor,
36                self::BATCH_SIZE + 1
37            ),
38            ARRAY_A
39        );
40
41        if ( $wpdb->last_error || ! is_array( $rows ) ) {
42            return;
43        }
44
45        $has_more = count( $rows ) > self::BATCH_SIZE;
46        if ( $has_more ) {
47            array_pop( $rows );
48        }
49
50        $now = time();
51        foreach ( $rows as $row ) {
52            if ( ! self::is_expired( $row['option_value'], $now ) ) {
53                continue;
54            }
55
56            // A render may have renewed this option since the batch was read.
57            $current_value = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
58                $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_id = %d", (int) $row['option_id'] )
59            );
60            if ( $wpdb->last_error ) {
61                return;
62            }
63            if ( self::is_expired( $current_value, $now ) ) {
64                delete_option( $row['option_name'] );
65            }
66        }
67
68        if ( ! $has_more ) {
69            delete_option( self::CURSOR_OPTION );
70            return;
71        }
72
73        $last_row = end( $rows );
74        update_option( self::CURSOR_OPTION, (int) $last_row['option_id'], false );
75
76        if ( false === wp_next_scheduled( self::FOLLOWUP_HOOK ) ) {
77            wp_schedule_single_event( time(), self::FOLLOWUP_HOOK );
78        }
79    }
80
81    /**
82     * Treats nonnumeric expiry values as unexpired.
83     *
84     * @param mixed $raw_value Stored option value.
85     * @param int   $now Current timestamp.
86     * @return bool Whether the expiry is numeric and in the past.
87     */
88    private static function is_expired( $raw_value, $now ) {
89        $value = maybe_unserialize( $raw_value );
90        return is_array( $value ) && isset( $value['expire'] ) && is_numeric( $value['expire'] ) && $value['expire'] < $now;
91    }
92}