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