Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.47% covered (warning)
89.47%
51 / 57
80.00% covered (warning)
80.00%
12 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Cache_Preload
89.47% covered (warning)
89.47%
51 / 57
80.00% covered (warning)
80.00%
12 / 15
25.73
0.00% covered (danger)
0.00%
0 / 1
 setup
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 get_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_available
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 activate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 deactivate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 schedule_cornerstone_cronjob
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 schedule_cornerstone
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 schedule
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 preload_cornerstone
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 preload
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 request_pages
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 request_page
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
3.09
 handle_post_update
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 handle_cache_invalidation
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 get_parent_features
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache;
4
5use Automattic\Jetpack_Boost\Contracts\Has_Activate;
6use Automattic\Jetpack_Boost\Contracts\Has_Deactivate;
7use Automattic\Jetpack_Boost\Contracts\Is_Always_On;
8use Automattic\Jetpack_Boost\Contracts\Sub_Feature;
9use Automattic\Jetpack_Boost\Lib\Cornerstone\Cornerstone_Utils;
10use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache;
11use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Logger;
12
13/**
14 * Class Cache_Preload
15 *
16 * Handles the rebuilding/preloading of cache for pages, currently only for Cornerstone Pages.
17 * This module automagically preloads the cache after cache invalidation events such as rebuilds, or when
18 * Cornerstone Pages are updated, to ensure that important pages always have a cache.
19 *
20 * @since 3.11.0
21 * @package Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache
22 */
23class Cache_Preload implements Sub_Feature, Has_Activate, Has_Deactivate, Is_Always_On {
24
25    /**
26     * @since 3.11.0
27     */
28    public function setup() {
29        add_action( 'update_option_jetpack_boost_ds_cornerstone_pages_list', array( $this, 'schedule_cornerstone' ) );
30        add_action( 'jetpack_boost_preload_cornerstone', array( $this, 'preload_cornerstone' ) );
31        add_action( 'jetpack_boost_preload', array( $this, 'preload' ) );
32
33        add_action( 'post_updated', array( $this, 'handle_post_update' ) );
34        add_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ), 10, 3 );
35    }
36
37    /**
38     * @since 3.11.0
39     */
40    public static function get_slug() {
41        return 'cache_preload';
42    }
43
44    /**
45     * @since 3.11.0
46     */
47    public static function is_available() {
48        return true;
49    }
50
51    /**
52     * As this is a submodule, this activate is triggered when the parent module is activated,
53     * despite the module having Is_Always_On.
54     *
55     * @since 3.12.0
56     */
57    public static function activate() {
58        $instance = new self();
59        $instance->schedule_cornerstone_cronjob();
60    }
61
62    /**
63     *
64     * @since 3.12.0
65     */
66    public static function deactivate() {
67        wp_unschedule_hook( 'jetpack_boost_preload_cornerstone' );
68    }
69
70    /**
71     * Schedule the cronjob to preload the cache for Cornerstone Pages.
72     *
73     * @since 3.12.0
74     * @return void
75     */
76    public function schedule_cornerstone_cronjob() {
77        if ( ! wp_next_scheduled( 'jetpack_boost_preload_cornerstone' ) ) {
78            wp_schedule_event( time(), 'twicehourly', 'jetpack_boost_preload_cornerstone' );
79        }
80    }
81
82    /**
83     * Schedule Preload for all Cornerstone Pages.
84     *
85     * This method is triggered when the Cornerstone Pages list is updated,
86     * ensuring all Cornerstone Pages have their cache rebuilt.
87     *
88     * @since 3.12.0
89     * @return void
90     */
91    public function schedule_cornerstone() {
92        wp_schedule_single_event( time(), 'jetpack_boost_preload_cornerstone' );
93    }
94
95    /**
96     * Schedule a rebuild for the given URLs.
97     *
98     * @since 3.12.0
99     * @param array $urls The URLs of the Cornerstone Pages to rebuild.
100     * @return void
101     */
102    public function schedule( array $urls ) {
103        Logger::debug( sprintf( 'Scheduling preload for %d pages', count( $urls ) ) );
104        wp_schedule_single_event( time(), 'jetpack_boost_preload', array( $urls ) );
105    }
106
107    /**
108     * Rebuild the cache for all Cornerstone Pages.
109     *
110     * @since 3.12.0
111     * @return void
112     */
113    public function preload_cornerstone() {
114        $urls = Cornerstone_Utils::get_list();
115        $this->preload( $urls );
116    }
117
118    /**
119     * Rebuild the cache for the given URLs.
120     *
121     * @since 3.12.0
122     * @param array $urls The URLs of the pages to preload.
123     * @return void
124     */
125    public function preload( array $urls ) {
126        Logger::debug( sprintf( 'Preload started for %d pages', count( $urls ) ) );
127
128        // Pause the hook here to avoid an infinite loop of: invalidation → preload → invalidation.
129        remove_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ) );
130        $boost_cache = new Boost_Cache();
131        foreach ( $urls as $url ) {
132            $boost_cache->rebuild_page( $url );
133            $this->request_page( $url );
134        }
135        add_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ), 10, 3 );
136
137        Logger::debug( sprintf( 'Preload completed for %d pages', count( $urls ) ) );
138    }
139
140    /**
141     * Requests the pages scheduled for preload.
142     *
143     * @since 3.11.0
144     * @param array $posts The posts to preload.
145     * @return void
146     */
147    public function request_pages( $posts ) {
148        if ( empty( $posts ) ) {
149            return;
150        }
151
152        foreach ( $posts as $url ) {
153            $this->request_page( $url );
154        }
155    }
156
157    /**
158     * Make an HTTP request to the specified URL to generate a fresh cache entry.
159     *
160     * @since 3.11.0
161     * @param string $page The URL of the page to preload.
162     * @return void
163     */
164    private function request_page( string $page ) {
165        // Add a cache-busting header to ensure our response is fresh.
166        $args = array(
167            'headers' => array(
168                'Cache-Control' => 'no-cache, no-store, must-revalidate, max-age=0',
169                'Pragma'        => 'no-cache',
170                'Expires'       => '0',
171            ),
172        );
173
174        $response = wp_remote_get( $page, $args );
175
176        if ( is_wp_error( $response ) ) {
177            Logger::debug( 'Error preloading page: ' . $response->get_error_message() );
178            return;
179        }
180
181        $status_code = wp_remote_retrieve_response_code( $response );
182        if ( $status_code !== 200 ) {
183            Logger::debug( sprintf( 'Error preloading page %s: HTTP status code %d', $page, $status_code ) );
184        }
185    }
186
187    /**
188     * Handle post updates to check if the post is a cornerstone page and schedule preload if needed.
189     *
190     * @since 3.11.0
191     * @param int $post_id The ID of the post being updated.
192     * @return void
193     */
194    public function handle_post_update( int $post_id ) {
195        if ( Cornerstone_Utils::is_cornerstone_page( $post_id ) ) {
196            $this->schedule( array( get_permalink( $post_id ) ) );
197        }
198    }
199
200    /**
201     * Handle cache invalidation events to schedule preloading for affected pages.
202     *
203     * If cache for Cornerstone Pages is invalidated, this method schedules those pages
204     * for preloading to ensure they have fresh cache.
205     *
206     * @since 3.11.0
207     * @param string $path The path that was invalidated.
208     * @param string $type The type of invalidation that occurred (rebuild or delete).
209     * @param string $scope The scope of the invalidation (page or recursive).
210     * @return void
211     */
212    public function handle_cache_invalidation( string $path, string $type, string $scope ) {
213        if ( $path === home_url() && $scope === 'recursive' ) {
214            Logger::debug( 'Invalidating all files, scheduling preload for all Cornerstone Pages.' );
215            // If the cache is invalidated for all files, schedule preload for all Cornerstone Pages.
216            $this->schedule_cornerstone();
217            return;
218        }
219
220        // Otherwise identify if a Cornerstone Page cache file is being deleted and schedule preload that page if it is.
221        Logger::debug( 'Invalidating a specific page, scheduling preload for that page if it is a Cornerstone Page.' );
222        $cornerstone_pages = Cornerstone_Utils::get_list();
223        $cornerstone_pages = array_map( 'untrailingslashit', $cornerstone_pages );
224        // If the $path is in the Cornerstone Page list, add it to the preload list.
225        if ( in_array( untrailingslashit( $path ), $cornerstone_pages, true ) ) {
226            $this->schedule( array( $path ) );
227        }
228    }
229
230    public static function get_parent_features(): array {
231        return array(
232            Page_Cache::class,
233        );
234    }
235}