Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 186
0.00% covered (danger)
0.00%
0 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Page_Cache_Setup
0.00% covered (danger)
0.00%
0 / 186
0.00% covered (danger)
0.00%
0 / 21
4556
0.00% covered (danger)
0.00%
0 / 1
 run_setup
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 get_notices
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 add_notice
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 run_step
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 enable_caching
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 verify_wp_content_writable
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 verify_permalink_setting
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 create_settings_file
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_advanced_cache_path
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 create_advanced_cache
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
72
 add_wp_cache_define
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 can_run_cache
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 deactivate
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 uninstall
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 delete_advanced_cache
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 delete_wp_cache_constant
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 cleanup_wp_cache_constant
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 find_wp_config
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 clear_opcache
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 write_to_file_direct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_wp_filesystem
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache;
4
5use Automattic\Jetpack_Boost\Lib\Analytics;
6use Automattic\Jetpack_Boost\Lib\Super_Cache_Config_Compatibility;
7use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Error;
8use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache_Settings;
9use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Filesystem_Utils;
10use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Logger;
11use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Simple_Delete;
12
13class Page_Cache_Setup {
14
15    private static $notices = array();
16
17    /**
18     * Runs setup steps and returns whether setup was successful or not.
19     *
20     * @return bool|\WP_Error
21     */
22    public static function run_setup() {
23        // Steps that are only for cache system verification. They don't change anything.
24        $verification_steps = array(
25            'verify_wp_content_writable',
26            'verify_permalink_setting',
27        );
28
29        foreach ( $verification_steps as $step ) {
30            $result = self::run_step( $step );
31
32            if ( is_wp_error( $result ) ) {
33                return $result;
34            }
35        }
36
37        /*
38         * Steps that may change something to setup the cache system.
39         * Each of them should return the result in following format:
40         * - true if the step was successful and changes were made
41         * - false if the step was successful but no changes were made
42         * - WP_Error if the step failed
43         */
44        $setup_steps = array(
45            'create_settings_file',
46            'create_advanced_cache',
47            'add_wp_cache_define',
48            'enable_caching',
49        );
50
51        $changes_made = false;
52        foreach ( $setup_steps as $step ) {
53            $result = self::run_step( $step );
54
55            if ( is_wp_error( $result ) ) {
56                return $result;
57            }
58
59            if ( $result === true ) {
60                $changes_made = true;
61            }
62        }
63
64        if ( $changes_made ) {
65            Analytics::record_user_event( 'page_cache_setup_succeeded' );
66        }
67        return true;
68    }
69
70    public static function get_notices() {
71        return self::$notices;
72    }
73
74    private static function add_notice( $title, $message ) {
75        self::$notices[] = array(
76            'title'   => $title,
77            'message' => $message,
78        );
79    }
80
81    private static function run_step( $step ) {
82        $result = self::$step();
83
84        if ( $result instanceof Boost_Cache_Error ) {
85            Analytics::record_user_event( 'page_cache_setup_failed', array( 'error_code' => $result->get_error_code() ) );
86            return $result->to_wp_error();
87        }
88
89        if ( is_wp_error( $result ) ) {
90            Analytics::record_user_event( 'page_cache_setup_failed', array( 'error_code' => $result->get_error_code() ) );
91            return $result;
92        }
93    }
94
95    /**
96     * Enable caching step of setup.
97     *
98     * @return Boost_Cache_Error|bool - True on success, false if it was already enabled, error otherwise.
99     */
100    private static function enable_caching() {
101        $settings       = Boost_Cache_Settings::get_instance();
102        $previous_value = $settings->get_enabled();
103
104        if ( $previous_value === true ) {
105            return false;
106        }
107
108        $enabled_result = $settings->set( array( 'enabled' => true ) );
109
110        if ( $enabled_result === true ) {
111            Logger::debug( 'Caching enabled in cache config' );
112        }
113
114        return $enabled_result;
115    }
116
117    /**
118     * Returns true if the wp-content directory is writeable.
119     */
120    private static function verify_wp_content_writable() {
121        $filename = WP_CONTENT_DIR . '/' . uniqid() . '.txt';
122        $result   = @file_put_contents( $filename, 'test' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, WordPress.PHP.NoSilencedErrors.Discouraged
123        wp_delete_file( $filename );
124
125        if ( $result === false ) {
126            return new \WP_Error( 'wp-content-not-writable' );
127        }
128
129        return true;
130    }
131
132    /**
133     * Returns true if WordPress is using a proper permalink setup. WP_Error if not.
134     */
135    private static function verify_permalink_setting() {
136        global $wp_rewrite;
137
138        if ( ! $wp_rewrite || ! $wp_rewrite->using_permalinks() ) {
139            return new \WP_Error( 'not-using-permalinks' );
140        }
141    }
142
143    /**
144     * Create a settings file, if one does not already exist.
145     *
146     * @return bool|\WP_Error - True if the file was created, WP_Error if there was a problem, or false if the file already exists.
147     */
148    private static function create_settings_file() {
149        $result = Boost_Cache_Settings::get_instance()->create_settings_file();
150        return $result;
151    }
152
153    /**
154     * Get the path to the advanced-cache.php file.
155     *
156     * @return string The full path to the advanced-cache.php file.
157     */
158    public static function get_advanced_cache_path() {
159        return WP_CONTENT_DIR . '/advanced-cache.php';
160    }
161
162    /**
163     * Creates the advanced-cache.php file.
164     *
165     * Returns true if the files were setup correctly, or WP_Error if there was a problem.
166     *
167     * @return bool|\WP_Error
168     */
169    private static function create_advanced_cache() {
170        $advanced_cache_filename = self::get_advanced_cache_path();
171
172        if ( file_exists( $advanced_cache_filename ) ) {
173            $content = file_get_contents( $advanced_cache_filename ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
174
175            if ( strpos( $content, 'WP SUPER CACHE' ) !== false ) {
176                // advanced-cache.php is already in use by WP Super Cache.
177
178                if ( Super_Cache_Config_Compatibility::is_compatible() ) {
179                    $deactivation = new Data_Sync_Actions\Deactivate_WPSC();
180                    $deactivation->handle();
181                    self::add_notice(
182                        __( 'WP Super Cache Has Been Deactivated', 'jetpack-boost' ),
183                        __( 'To ensure optimal performance, WP Super Cache has been automatically deactivated because Jetpack Boost\'s Cache is now active. Only one caching system can be used at a time.', 'jetpack-boost' )
184                    );
185
186                    Analytics::record_user_event(
187                        'switch_to_boost_cache',
188                        array(
189                            'type'   => 'silent',
190                            'reason' => 'super_cache_compatible',
191                        )
192                    );
193                } else {
194                    return new \WP_Error( 'advanced-cache-for-super-cache' );
195                }
196            } elseif ( strpos( $content, Page_Cache::ADVANCED_CACHE_SIGNATURE ) === false ) {
197                // advanced-cache.php is in use by another plugin.
198                return new \WP_Error( 'advanced-cache-incompatible' );
199            }
200
201            if ( strpos( $content, Page_Cache::ADVANCED_CACHE_VERSION ) !== false ) {
202                // The advanced-cache.php file belongs to current version of Boost Cache.
203                return false;
204            }
205        }
206
207        $plugin_dir_name      = untrailingslashit( str_replace( JETPACK_BOOST_PLUGIN_FILENAME, '', JETPACK_BOOST_PLUGIN_BASE ) );
208        $boost_cache_filename = WP_CONTENT_DIR . '/plugins/' . $plugin_dir_name . '/app/modules/optimizations/page-cache/pre-wordpress/class-boost-cache.php';
209        if ( ! file_exists( $boost_cache_filename ) ) {
210            return new \WP_Error( 'boost-cache-file-not-found' );
211        }
212        $contents = '<?php
213// ' . Page_Cache::ADVANCED_CACHE_SIGNATURE . ' - ' . Page_Cache::ADVANCED_CACHE_VERSION . '
214if ( ! file_exists( \'' . $boost_cache_filename . '\' ) ) {
215return;
216}
217require_once( \'' . $boost_cache_filename . '\');
218$boost_cache = new Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache();
219$boost_cache->init_actions();
220$boost_cache->serve();
221';
222
223        $write_advanced_cache = Filesystem_Utils::write_to_file( $advanced_cache_filename, $contents );
224        if ( $write_advanced_cache instanceof Boost_Cache_Error ) {
225            return new \WP_Error( 'unable-to-write-to-advanced-cache', $write_advanced_cache->get_error_code() );
226        }
227        self::clear_opcache( $advanced_cache_filename );
228
229        Logger::debug( 'Advanced cache file created' );
230
231        return true;
232    }
233
234    /**
235     * Adds the WP_CACHE define to wp-config.php
236     */
237    private static function add_wp_cache_define() {
238        $config_file = self::find_wp_config();
239        if ( $config_file === false ) {
240            return new \WP_Error( 'wp-config-not-found' );
241        }
242
243        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
244        $content = file_get_contents( $config_file );
245        if ( preg_match( '#^\s*(define\s*\(\s*[\'"]WP_CACHE[\'"]|const\s+WP_CACHE\s*=)#m', $content ) === 1 ) {
246            /*
247             * wp-settings.php checks "if ( WP_CACHE )" so it may be truthy and
248             * not === true to pass that check.
249             * Later, it is defined as false in default-constants.php, but
250             * it may have been defined manually as true using "true", 1, or "1"
251             * in wp-config.php.
252             */
253            if ( defined( 'WP_CACHE' ) && ! WP_CACHE ) {
254                return new \WP_Error( 'wp-cache-defined-not-true' );
255            }
256
257            return false; // WP_CACHE already added.
258        }
259        $content = preg_replace(
260            '#^<\?php#',
261            '<?php
262define( \'WP_CACHE\', true ); // ' . Page_Cache::ADVANCED_CACHE_SIGNATURE,
263            $content
264        );
265
266        $result = self::write_to_file_direct( $config_file, $content );
267        if ( $result === false ) {
268            return new \WP_Error( 'wp-config-not-writable' );
269        }
270        self::clear_opcache( $config_file );
271
272        Logger::debug( 'WP_CACHE constant added to wp-config.php' );
273
274        return true;
275    }
276
277    /**
278     * Checks if page cache can be run or not.
279     *
280     * @return bool True if the advanced-cache.php file doesn't exist or belongs to Boost, false otherwise.
281     */
282    public static function can_run_cache() {
283        $advanced_cache_path = self::get_advanced_cache_path();
284        if ( ! file_exists( $advanced_cache_path ) ) {
285            return true;
286        }
287
288        $content = file_get_contents( $advanced_cache_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
289        return strpos( $content, Page_Cache::ADVANCED_CACHE_SIGNATURE ) !== false;
290    }
291
292    /**
293     * Removes the advanced-cache.php file and the WP_CACHE define from wp-config.php
294     * Fired when the plugin is deactivated.
295     */
296    public static function deactivate() {
297        $advanced_cache_deleted = self::delete_advanced_cache();
298        // Only remove constant if Boost was the last to run caching.
299        // This is to avoid breaking caching for other plugins.
300        if ( $advanced_cache_deleted ) {
301            self::delete_wp_cache_constant();
302        } else {
303            self::cleanup_wp_cache_constant();
304        }
305
306        return true;
307    }
308
309    /**
310     * Removes the boost-cache directory, removing all cached files and the config file.
311     * Fired when the plugin is uninstalled.
312     */
313    public static function uninstall() {
314        self::deactivate();
315        // Call the Cache Preload module deactivation here to ensure it's cleaned up properly.
316        Cache_Preload::deactivate();
317        $result = Filesystem_Utils::iterate_directory( WP_CONTENT_DIR . '/boost-cache', new Simple_Delete() );
318        if ( $result instanceof Boost_Cache_Error ) {
319            return $result->to_wp_error();
320        }
321
322        return true;
323    }
324
325    /**
326     * Deletes the file advanced-cache.php if it exists.
327     */
328    public static function delete_advanced_cache() {
329        $advanced_cache_filename = self::get_advanced_cache_path();
330
331        if ( ! file_exists( $advanced_cache_filename ) ) {
332            return false;
333        }
334
335        $deleted_file = false;
336        $content      = file_get_contents( $advanced_cache_filename ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
337        if ( strpos( $content, Page_Cache::ADVANCED_CACHE_SIGNATURE ) !== false ) {
338            wp_delete_file( $advanced_cache_filename );
339
340            // wp_delete_file doesn't return anything
341            // so we manually check if the file was deleted.
342            $deleted_file = ! file_exists( $advanced_cache_filename );
343        }
344
345        self::clear_opcache( $advanced_cache_filename );
346
347        return $deleted_file;
348    }
349
350    /**
351     * Deletes the WP_CACHE define from wp-config.php
352     *
353     * @return \WP_Error if an error occurred.
354     */
355    public static function delete_wp_cache_constant() {
356        $config_file = self::find_wp_config();
357        if ( $config_file === false ) {
358            return;
359        }
360
361        $lines = file( $config_file );
362        $found = false;
363        foreach ( $lines as $key => $line ) {
364            if ( preg_match( '#define\s*\(\s*[\'"]WP_CACHE[\'"]#', $line ) === 1 && strpos( $line, Page_Cache::ADVANCED_CACHE_SIGNATURE ) !== false ) {
365                unset( $lines[ $key ] );
366                $found = true;
367            }
368        }
369        if ( ! $found ) {
370            return;
371        }
372        $content = implode( '', $lines );
373        Filesystem_Utils::write_to_file( $config_file, $content );
374        self::clear_opcache( $config_file );
375    }
376
377    /**
378     * Removes the comment after WP_CACHE defined in wp-config.php (if any).
379     *
380     * @return void
381     */
382    public static function cleanup_wp_cache_constant() {
383        $config_file = self::find_wp_config();
384        if ( $config_file === false ) {
385            return;
386        }
387
388        $lines = file( $config_file );
389        $found = false;
390        foreach ( $lines as $key => $line ) {
391            if ( preg_match( '#define\s*\(\s*[\'"]WP_CACHE[\'"]#', $line ) === 1 && strpos( $line, Page_Cache::ADVANCED_CACHE_SIGNATURE ) !== false ) {
392                $lines[ $key ] = preg_replace( '#\s*?\/\/.*$#', '', $line );
393                $found         = true;
394            }
395        }
396        if ( ! $found ) {
397            return;
398        }
399        $content = implode( '', $lines );
400        Filesystem_Utils::write_to_file( $config_file, $content );
401        self::clear_opcache( $config_file );
402    }
403
404    /**
405     * Find location of wp-config.php file.
406     *
407     * @return string|false - The path to the wp-config.php file, or false if it was not found.
408     */
409    private static function find_wp_config() {
410        if ( file_exists( ABSPATH . 'wp-config.php' ) ) {
411            return ABSPATH . 'wp-config.php';
412        } elseif ( file_exists( dirname( ABSPATH ) . '/wp-config.php' ) && ! file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) {
413            // While checking one directory up, check for wp-settings.php as well similar to WordPress core, to avoid nested WordPress installations.
414            return dirname( ABSPATH ) . '/wp-config.php';
415        }
416
417        return false;
418    }
419
420    /**
421     * Clear opcache for a file.
422     */
423    private static function clear_opcache( $file ) {
424        // If API functions are restricted, we can't do anything.
425        if ( ini_get( 'opcache.restrict_api' ) ) {
426            return;
427        }
428
429        if ( function_exists( 'opcache_invalidate' ) ) {
430            opcache_invalidate( $file, true );
431        }
432    }
433
434    private static function write_to_file_direct( $file, $content ) {
435        $filesystem = self::get_wp_filesystem();
436        $chmod      = $filesystem->getchmod( $file );
437        if ( $chmod === false ) {
438            $chmod = 0644; // Default to a common permission for files
439        } else {
440            $chmod = intval( '0' . $chmod, 8 ); // Ensure leading zero
441        }
442        return $filesystem->put_contents( $file, $content, $chmod );
443    }
444
445    private static function get_wp_filesystem() {
446        require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
447        require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
448        return new \WP_Filesystem_Direct( null );
449    }
450}