Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
File
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 19
3660
0.00% covered (danger)
0.00%
0 / 1
 should_enable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 exclusionChange
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 on_rewrite
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 maybeAddRules
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
20
 addRules
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 removeRules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 maybeGeneratePageCache
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 write
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 isCacheable
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
420
 shouldCache
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
 exclusions
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getExpirationTimeframe
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 purge_all
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 purge_url
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getStoragePathForRequest
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getStorageFileForRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 on_activation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 on_deactivation
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace NewfoldLabs\WP\Module\Performance\Cache\Types;
4
5use NewfoldLabs\WP\Module\Performance\Cache\Purgeable;
6use NewfoldLabs\WP\Module\Performance\OptionListener;
7use NewfoldLabs\WP\ModuleLoader\Container;
8use NewfoldLabs\WP\Module\Performance\Cache\CacheExclusion;
9use NewfoldLabs\WP\Module\Performance\Cache\CacheManager;
10use WP_Forge\WP_Htaccess_Manager\htaccess;
11use wpscholar\Url;
12
13use function NewfoldLabs\WP\Module\Performance\get_cache_level;
14use function NewfoldLabs\WP\Module\Performance\remove_directory;
15use function NewfoldLabs\WP\Module\Performance\should_cache_pages;
16use function WP_Forge\WP_Htaccess_Manager\removeMarkers;
17use function NewfoldLabs\WP\ModuleLoader\container as getContainer;
18
19/**
20 * File cache type.
21 */
22class File extends CacheBase implements Purgeable {
23    /**
24     * The directory where cached files live.
25     *
26     * @var string
27     */
28    const CACHE_DIR = WP_CONTENT_DIR . '/newfold-page-cache/';
29
30    /**
31     * The file marker name.
32     *
33     * @var string
34     */
35    const MARKER = 'Newfold File Cache';
36
37    /**
38     * Whether or not the code for this cache type should be loaded.
39     *
40     * @param Container $container Dependency injection container.
41     *
42     * @return bool
43     */
44    public static function should_enable( Container $container ) {
45        return (bool) $container->has( 'isApache' ) && $container->get( 'isApache' );
46    }
47
48    /**
49     * Constructor.
50     */
51    public function __construct() {
52        new OptionListener( CacheManager::OPTION_CACHE_LEVEL, array( __CLASS__, 'maybeAddRules' ) );
53        new OptionListener( CacheExclusion::OPTION_CACHE_EXCLUSION, array( __CLASS__, 'exclusionChange' ) );
54
55        add_action( 'init', array( $this, 'maybeGeneratePageCache' ) );
56        add_action( 'newfold_update_htaccess', array( $this, 'on_rewrite' ) );
57    }
58
59    /**
60     * Manage on exlcusion option change.
61     */
62    public static function exclusionChange() {
63        self::maybeAddRules( get_cache_level() );
64    }
65
66    /**
67     * When updating mod rewrite rules, also update our rewrites as appropriate.
68     */
69    public function on_rewrite() {
70        self::maybeAddRules( get_cache_level() );
71    }
72
73    /**
74     * Determine whether to add or remove rules based on caching level.
75     *
76     * @param  int $cacheLevel  The caching level.
77     */
78    public static function maybeAddRules( $cacheLevel ) {
79        $brand = getContainer()->plugin()->brand;
80        absint( $cacheLevel ) > 1 && 'bluehost' !== $brand && 'hostgator' !== $brand ? self::addRules() : self::removeRules();
81    }
82
83    /**
84     * Add our content to the .htaccess file.
85     *
86     * @return bool
87     */
88    public static function addRules() {
89        $base = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
90        $path = str_replace( get_home_path(), '/', self::CACHE_DIR );
91
92        $content = <<<HTACCESS
93<IfModule mod_rewrite.c>
94    RewriteEngine On
95    RewriteBase {$base}
96    RewriteRule ^{$path}/ - [L]
97    RewriteCond %{REQUEST_METHOD} !POST
98    RewriteCond %{QUERY_STRING} !.*=.*
99    RewriteCond %{HTTP_COOKIE} !(wordpress_test_cookie|comment_author|wp\-postpass|wordpress_logged_in|wptouch_switch_toggle|wp_woocommerce_session_) [NC]
100    RewriteCond %{HTTP:Cache-Control} ^((?!no-cache).)*$
101    RewriteCond %{DOCUMENT_ROOT}{$path}/$1/_index.html -f
102    RewriteRule ^(.*)\$ {$path}/$1/_index.html [L]
103</IfModule>
104HTACCESS;
105
106        $htaccess = new htaccess( self::MARKER );
107
108        return $htaccess->addContent( $content );
109    }
110
111    /**
112     * Remove our content from the .htaccess file.
113     */
114    public static function removeRules() {
115        removeMarkers( self::MARKER );
116    }
117
118    /**
119     * Initiate the generation of a page cache for a given request, if necessary.
120     */
121    public function maybeGeneratePageCache() {
122        if ( $this->isCacheable() ) {
123            if ( $this->shouldCache() ) {
124                ob_start( array( $this, 'write' ) );
125            }
126        } else {
127            nocache_headers();
128        }
129    }
130
131    /**
132     * Write page content to cache.
133     *
134     * @param  string $content  Page content to be cached.
135     *
136     * @return string
137     */
138    public function write( $content ) {
139        if ( ! empty( $content ) ) {
140
141            $path = $this->getStoragePathForRequest();
142            $file = $this->getStorageFileForRequest();
143
144            if ( false !== strpos( $content, '</html>' ) ) {
145                $content .= "\n<!--Generated by Newfold Page Cache-->";
146            }
147
148            global $wp_filesystem;
149
150            if ( ! function_exists( 'WP_Filesystem' ) ) {
151                require_once ABSPATH . 'wp-admin/includes/file.php';
152            }
153
154            WP_Filesystem();
155
156            if ( ! $wp_filesystem->is_dir( $path ) ) {
157                $wp_filesystem->mkdir( $path, 0755 );
158            }
159
160            $wp_filesystem->put_contents( $file, $content, FS_CHMOD_FILE );
161
162        }
163
164        return $content;
165    }
166
167    /**
168     * Check if the current request is cacheable.
169     *
170     * @return bool
171     */
172    public function isCacheable() {
173        // The request URI should never be empty â€“ even for the homepage it should be '/'
174        if ( empty( $_SERVER['REQUEST_URI'] ) ) {
175            return false;
176        }
177
178        // Don't cache if pretty permalinks are disabled
179        if ( false === get_option( 'permalink_structure' ) ) {
180            return false;
181        }
182
183        // Only cache front-end pages
184        if ( is_admin() || wp_doing_ajax() || wp_doing_cron() ) {
185            return false;
186        }
187
188        // Don't cache REST API requests
189        if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
190            return false;
191        }
192
193        // Never cache requests made via WP-CLI
194        if ( defined( 'WP_CLI' ) && WP_CLI ) {
195            return false;
196        }
197
198        // Don't cache if there are URL parameters present
199        if ( isset( $_GET ) && ! empty( $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification
200            return false;
201        }
202
203        // Don't cache if handling a form submission
204        if ( isset( $_POST ) && ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification
205            return false;
206        }
207
208        // Don't cache if a user is logged in.
209        if ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
210            return false;
211        }
212
213        global $wp_query;
214        if ( isset( $wp_query ) ) {
215
216            // Don't cache 404 pages or RSS feeds
217            if ( is_404() || is_feed() ) {
218                return false;
219            }
220        }
221
222        // Don't cache private pages
223        if ( 'private' === get_post_status() ) {
224            return false;
225        }
226
227        return true;
228    }
229
230    /**
231     * Check if we should cache the current request.
232     *
233     * @return bool
234     */
235    public function shouldCache() {
236
237        // If page caching is disabled, then don't cache
238        if ( ! should_cache_pages() ) {
239            return false;
240        }
241
242        // Check cache exclusion.
243        $cache_exclusion_parameters = $this->exclusions();
244
245        if ( ! empty( $cache_exclusion_parameters ) ) {
246            foreach ( $cache_exclusion_parameters as $param ) {
247                if ( stripos( $_SERVER['REQUEST_URI'], $param ) !== false ) {
248                    return false;
249                }
250            }
251        }
252
253        // Don't cache if a file exists and hasn't expired
254        $file = $this->getStorageFileForRequest();
255        if ( file_exists( $file ) && filemtime( $file ) + $this->getExpirationTimeframe() > time() ) {
256            return false;
257        }
258
259        return true;
260    }
261
262    /**
263     * Get an array of strings that should not be present in the URL for a request to be cached.
264     *
265     * @return array
266     */
267    protected function exclusions() {
268        $default                = array( 'cart', 'checkout', 'wp-admin', '@', '%', ':', ';', '&', '=', '.', rest_get_url_prefix() );
269        $cache_exclusion_option = array_map( 'trim', explode( ',', get_option( CacheExclusion::OPTION_CACHE_EXCLUSION ) ) );
270        return array_merge( $default, $cache_exclusion_option );
271    }
272
273    /**
274     * Get expiration duration.
275     *
276     * @return int
277     */
278    protected function getExpirationTimeframe() {
279        switch ( get_cache_level() ) {
280            case 2:
281                return 2 * HOUR_IN_SECONDS;
282            case 3:
283                return 8 * HOUR_IN_SECONDS;
284            default:
285                return 0;
286        }
287    }
288
289    /**
290     * Purge everything from the cache.
291     */
292    public function purge_all() {
293        remove_directory( self::CACHE_DIR );
294    }
295
296    /**
297     * Purge a specific URL from the cache.
298     *
299     * @param string $url the url to purge.
300     */
301    public function purge_url( $url ) {
302        $path = $this->getStoragePathForRequest();
303
304        if ( trailingslashit( self::CACHE_DIR ) === $path ) {
305            if ( file_exists( self::CACHE_DIR . '/_index.html' ) ) {
306                wp_delete_file( self::CACHE_DIR . '/_index.html' );
307            }
308
309            return;
310        }
311
312        remove_directory( $this->getStoragePathForRequest() );
313    }
314
315    /**
316     * Get storage path for a given request.
317     *
318     * @return string
319     */
320    protected function getStoragePathForRequest() {
321        static $path;
322
323        if ( ! isset( $path ) ) {
324            $url      = new Url();
325            $basePath = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
326            $path     = trailingslashit( self::CACHE_DIR . str_replace( $basePath, '', esc_url( $url->path ) ) );
327        }
328
329        return $path;
330    }
331
332    /**
333     * Get storage file for a given request.
334     *
335     * @return string
336     */
337    protected function getStorageFileForRequest() {
338        return $this->getStoragePathForRequest() . '_index.html';
339    }
340
341    /**
342     * Handle activation logic.
343     */
344    public static function on_activation() {
345        self::maybeAddRules( get_cache_level() );
346    }
347
348    /**
349     * Handle deactivation logic.
350     */
351    public static function on_deactivation() {
352
353        // Remove file cache rules from .htaccess
354        self::removeRules();
355
356        // Remove all statically cached files
357        remove_directory( self::CACHE_DIR );
358    }
359}