Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
41.76% covered (danger)
41.76%
71 / 170
33.33% covered (danger)
33.33%
10 / 30
CRAP
n/a
0 / 0
jetpack_boost_minify_cache_buster
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
jetpack_boost_page_optimize_is_already_minified
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
jetpack_boost_minify_concat_max_files
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
jetpack_boost_should_run_daily_network_cron_job
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
jetpack_boost_minify_cron_cache_cleanup
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
jetpack_boost_legacy_minify_cache_cleanup
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
jetpack_boost_minify_cache_cleanup
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
jetpack_boost_delete_expired_files
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
jetpack_boost_minify_clear_scheduled_events
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_boost_page_optimize_deactivate
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_boost_enqueued_to_absolute_url
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
jetpack_boost_page_optimize_js_exclude_list
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
jetpack_boost_page_optimize_css_exclude_list
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
jetpack_boost_page_optimize_merge_debug_excludes
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
10
jetpack_boost_page_optimize_starts_with
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
jetpack_boost_page_optimize_use_concat_base_dir
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
jetpack_boost_page_optimize_remove_concat_base_prefix
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
jetpack_boost_page_optimize_schedule_404_tester
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
jetpack_boost_page_optimize_schedule_cache_cleanup
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
jetpack_boost_page_optimize_bail
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
132
jetpack_boost_page_optimize_cache_bust_mtime
75.00% covered (warning)
75.00%
18 / 24
0.00% covered (danger)
0.00%
0 / 1
11.56
jetpack_boost_get_static_prefix
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
jetpack_boost_minify_use_static_cache_urls
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
jetpack_boost_get_minify_url
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
jetpack_boost_get_minify_file_path
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_boost_minify_serve_concatenated
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
jetpack_boost_minify_activation
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
jetpack_boost_minify_is_enabled
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
jetpack_boost_minify_init
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
jetpack_boost_page_optimize_generate_concat_path
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
1<?php
2
3use Automattic\Jetpack_Boost\Admin\Config as Boost_Admin_Config;
4use Automattic\Jetpack_Boost\Lib\Minify\Cleanup_Stored_Paths;
5use Automattic\Jetpack_Boost\Lib\Minify\Config;
6use Automattic\Jetpack_Boost\Lib\Minify\Dependency_Path_Mapping;
7use Automattic\Jetpack_Boost\Lib\Minify\File_Paths;
8use Automattic\Jetpack_Boost\Modules\Module;
9use Automattic\Jetpack_Boost\Modules\Optimizations\Minify\Minify_CSS;
10use Automattic\Jetpack_Boost\Modules\Optimizations\Minify\Minify_JS;
11
12/**
13 * Get an extra cache key for requests. We can manually bump this when we want
14 * to ensure a new version of Jetpack Boost never reuses old cached URLs.
15 */
16function jetpack_boost_minify_cache_buster() {
17    // Bumped to 2 so the JS truncation fix actually reaches already-affected sites:
18    // the cached concat bundle is served before any rebuild, so a site that cached a
19    // corrupted bundle would keep shipping it (until its source mtime changed) even
20    // after upgrade. Changing the buster invalidates those cache keys and forces a
21    // rebuild through the new structural-validation/fallback path.
22    return 2;
23}
24
25/**
26 * Whether a file should be treated as already minified (so the serving path must
27 * not re-minify it).
28 *
29 * This is the load-bearing skip for assets shipped as `.min.js` / `.min.css`
30 * (e.g. Boost's own image-guide bundle, which webpack/Terser has already
31 * minified): re-running the ES5-era PHP minifier over modern, already-minified
32 * JS can silently corrupt it. The mime type already routes JS and CSS down
33 * separate branches, so matching both extensions here is safe for either.
34 *
35 * @since 4.6.0
36 *
37 * @param string $fullpath Absolute path to the file being served.
38 *
39 * @return bool True if the filename indicates already-minified content.
40 */
41function jetpack_boost_page_optimize_is_already_minified( $fullpath ) {
42    return (bool) preg_match( '/\.min\.(css|js)$/', (string) $fullpath );
43}
44
45/**
46 * Get the number of maximum files that can be concatenated in a group.
47 */
48function jetpack_boost_minify_concat_max_files() {
49
50    /**
51     * Filter the number of maximum files that can be concatenated in a group.
52     *
53     * @param int $max_files The maximum number of files that can be concatenated.
54     */
55    return apply_filters( 'jetpack_boost_minify_concat_max_files', 150 );
56}
57
58/**
59 * This ensures that the cache cleanup cron job is only run once per day, espicially for multisite.
60 */
61function jetpack_boost_should_run_daily_network_cron_job( $hook ) {
62    // If we see it's been executed within 24 hours, don't run
63    if ( get_site_option( 'jetpack_boost_' . $hook . '_last_run', 0 ) > time() - DAY_IN_SECONDS ) {
64        return false;
65    }
66
67    update_site_option( 'jetpack_boost_' . $hook . '_last_run', time() );
68
69    return true;
70}
71
72/**
73 * This ensures that the cache cleanup cron job is only run once per day, espicially for multisite.
74 */
75function jetpack_boost_minify_cron_cache_cleanup() {
76    if ( ! jetpack_boost_should_run_daily_network_cron_job( 'minify_cron_cache_cleanup' ) ) {
77        return;
78    }
79
80    jetpack_boost_legacy_minify_cache_cleanup();
81    jetpack_boost_minify_cache_cleanup();
82}
83
84/**
85 * Cleanup minify cache files stored using the legacy method.
86 *
87 * @param int $file_age The age of files to purge, in seconds.
88 */
89function jetpack_boost_legacy_minify_cache_cleanup( $file_age = DAY_IN_SECONDS ) {
90    // If file_age is not an int, set it to the default.
91    // $file_age can be an empty string if calling this from an action: https://core.trac.wordpress.org/ticket/14881
92    $file_age = is_int( $file_age ) ? $file_age : DAY_IN_SECONDS;
93
94    $cache_folder = Config::get_legacy_cache_dir_path();
95
96    if ( ! is_dir( $cache_folder ) ) {
97        return;
98    }
99
100    $cache_files = glob( $cache_folder . '/page-optimize-cache-*' );
101
102    jetpack_boost_delete_expired_files( $cache_files, $file_age );
103}
104
105/**
106 * Cleanup obsolete files in static cache folder.
107 *
108 * @param int $file_age The age of files to purge, in seconds.
109 */
110function jetpack_boost_minify_cache_cleanup( $file_age = DAY_IN_SECONDS ) {
111    // Explicitly cast to int as do_action() can pass a non-int value. https://core.trac.wordpress.org/ticket/14881
112    $file_age = is_int( $file_age ) ? $file_age : DAY_IN_SECONDS;
113
114    /*
115     * Cleanup obsolete files in static cache folder.
116     * If $file_age is 0, we can skip this as we will delete all files anyway.
117     */
118    if ( $file_age !== 0 ) {
119        // Cleanup obsolete files in static cache folder
120        jetpack_boost_minify_remove_stale_static_files();
121    }
122
123    $cache_files = glob( Config::get_static_cache_dir_path() . '/*.min.*' );
124
125    jetpack_boost_delete_expired_files( $cache_files, $file_age );
126}
127
128/**
129 * Delete expired files based on access time or file age.
130 *
131 * Only delete files which are either 2x $file_age or have not been accessed in $file_age seconds.
132 *
133 * @param array $files The files to delete.
134 * @param int   $file_age The age of files to purge, in seconds.
135 */
136function jetpack_boost_delete_expired_files( $files, $file_age ) {
137    foreach ( $files as $file ) {
138        if ( ! is_file( $file ) ) {
139            continue;
140        }
141
142        if ( $file_age === 0 ) {
143            wp_delete_file( $file );
144            continue;
145        }
146
147        // Delete files that haven't been accessed in $file_age seconds,
148        // or files that are older than 2 * $file_age regardless of access time
149        if ( ( time() - $file_age ) > fileatime( $file ) ) {
150            wp_delete_file( $file );
151        } elseif ( ( time() - ( 2 * $file_age ) ) > filemtime( $file ) ) {
152            wp_delete_file( $file );
153        }
154    }
155}
156
157/**
158 * Clear scheduled cron events for minification.
159 *
160 * Removes the cache cleanup cron job and the 404 tester cron job.
161 */
162function jetpack_boost_minify_clear_scheduled_events() {
163    wp_unschedule_hook( 'jetpack_boost_minify_cron_cache_cleanup' );
164    wp_unschedule_hook( 'jetpack_boost_404_tester_cron' );
165    Cleanup_Stored_Paths::clear_schedules();
166}
167
168/**
169 * Plugin deactivation hook - unschedule cronjobs and purge cache.
170 */
171function jetpack_boost_page_optimize_deactivate() {
172    // Cleanup all cache files even if they are 0 seconds old
173    jetpack_boost_legacy_minify_cache_cleanup( 0 );
174    jetpack_boost_minify_cache_cleanup( 0 );
175
176    jetpack_boost_minify_clear_scheduled_events();
177}
178
179/**
180 * Convert enqueued home-relative URLs to absolute ones.
181 *
182 * Enqueued script URLs which start with / are relative to WordPress' home URL.
183 * i.e.: "/wp-includes/x.js" should be "WP_HOME/wp-includes/x.js".
184 *
185 * Note: this method uses home_url, so should only be used plugin-side when
186 * generating concatenated URLs.
187 */
188function jetpack_boost_enqueued_to_absolute_url( $url ) {
189    if ( str_starts_with( $url, '/' ) ) {
190        return home_url( $url );
191    }
192
193    return $url;
194}
195
196/**
197 * Get the list of JS handles to exclude from minification.
198 *
199 * Administrators can append extra handles for the current request only via the
200 * `jb-minify-js-excludes` GET parameter (comma-separated handles). See
201 * jetpack_boost_page_optimize_merge_debug_excludes() for details.
202 */
203function jetpack_boost_page_optimize_js_exclude_list() {
204    return jetpack_boost_page_optimize_merge_debug_excludes(
205        jetpack_boost_ds_get( 'minify_js_excludes' ),
206        'jb-minify-js-excludes'
207    );
208}
209
210/**
211 * Get the list of CSS handles to exclude from minification.
212 *
213 * Administrators can append extra handles for the current request only via the
214 * `jb-minify-css-excludes` GET parameter (comma-separated handles). See
215 * jetpack_boost_page_optimize_merge_debug_excludes() for details.
216 */
217function jetpack_boost_page_optimize_css_exclude_list() {
218    return jetpack_boost_page_optimize_merge_debug_excludes(
219        jetpack_boost_ds_get( 'minify_css_excludes' ),
220        'jb-minify-css-excludes'
221    );
222}
223
224/**
225 * Debugging aid: merge per-request exclude handles from a GET parameter into a
226 * saved concatenate/minify exclude list.
227 *
228 * Lets administrators test "would excluding handle X fix this page?" by loading
229 * the page with e.g. `?jb-minify-js-excludes=jquery-core,my-plugin-script`
230 * without editing the saved settings, and without needing a staging site.
231 *
232 * Safety properties:
233 * - Only honored for logged-in users with the `manage_options` capability.
234 *   current_user_can() is available during normal page rendering (the exclude
235 *   lists are read while scripts/styles are printed at wp_head/wp_footer); the
236 *   function_exists() guard defensively no-ops in any pre-WordPress context
237 *   (e.g. the page-cache layer) where it is not yet loaded.
238 * - Each handle is validated against a strict allowlist (alphanumerics, dash,
239 *   underscore and dot); anything else is ignored entirely. Case is preserved
240 *   because script/style handles are matched case-sensitively downstream.
241 * - The number of handles processed is capped to bound the work this debug aid
242 *   can be made to do per request.
243 * - The merged list is never persisted â€” nothing is written to the data sync
244 *   store or options; the merge only affects the current request.
245 * - No Page Cache interaction: logged-in users always bypass Boost's page cache
246 *   (Request::is_cacheable() returns false), so pages rendered with these debug
247 *   parameters are neither served from nor written to the cache.
248 *
249 * Residual note: changing the exclude list changes concatenate-bundle membership,
250 * so each distinct combination an admin renders generates its own concat-path
251 * transient (and static file when static minification is on), just like any new
252 * page does. These are reclaimed by the normal Cleanup_Stored_Paths schedule.
253 * The parameter is intentionally nonce-free so it can be shared as a plain URL,
254 * so this stays capability-gated rather than CSRF-gated.
255 *
256 * @since 4.6.2
257 *
258 * @param mixed  $excludes  The saved exclude list; non-array values are treated as an empty list.
259 * @param string $param     The GET parameter to read extra handles from.
260 *
261 * @return array The exclude list, with any valid per-request handles appended.
262 */
263function jetpack_boost_page_optimize_merge_debug_excludes( $excludes, $param ) {
264    if ( ! is_array( $excludes ) ) {
265        $excludes = array();
266    }
267
268    // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only debugging parameter; capability-gated below and never persisted.
269    if ( ! isset( $_GET[ $param ] ) || ! is_string( $_GET[ $param ] ) ) {
270        return $excludes;
271    }
272
273    // Only administrators may use the debug parameters.
274    if ( ! function_exists( 'current_user_can' ) || ! current_user_can( 'manage_options' ) ) {
275        return $excludes;
276    }
277
278    // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each handle is validated against a strict allowlist below.
279    $raw_value = wp_unslash( $_GET[ $param ] );
280
281    // Bound the work: a real debug session never needs more than a handful of
282    // handles, so process at most the first 100. The excess is dropped rather
283    // than discarding the whole value, so a partial list still applies.
284    $raw_handles = array_slice( explode( ',', $raw_value ), 0, 100 );
285
286    $extra_handles = array();
287    foreach ( $raw_handles as $handle ) {
288        $handle = trim( $handle );
289
290        /*
291         * Allowlist of characters valid in script/style handles. Case is preserved
292         * (not lowercased) because handles are matched case-sensitively downstream,
293         * so lowercasing would silently fail to exclude a handle registered with
294         * uppercase letters. Handles containing any other character are ignored.
295         */
296        if ( $handle !== '' && preg_match( '/^[a-zA-Z0-9_.\-]+$/', $handle ) ) {
297            $extra_handles[] = $handle;
298        }
299    }
300
301    if ( empty( $extra_handles ) ) {
302        return $excludes;
303    }
304
305    return array_values( array_unique( array_merge( $excludes, $extra_handles ) ) );
306}
307
308/**
309 * Determines whether a string starts with another string.
310 */
311function jetpack_boost_page_optimize_starts_with( $prefix, $str ) {
312    $prefix_length = strlen( $prefix );
313    if ( strlen( $str ) < $prefix_length ) {
314        return false;
315    }
316
317    return substr( $str, 0, $prefix_length ) === $prefix;
318}
319
320/**
321 * Answers whether the plugin should provide concat resource URIs
322 * that are relative to a common ancestor directory. Assuming a common ancestor
323 * allows us to skip resolving resource URIs to filesystem paths later on.
324 */
325function jetpack_boost_page_optimize_use_concat_base_dir() {
326    return defined( 'PAGE_OPTIMIZE_CONCAT_BASE_DIR' ) && file_exists( PAGE_OPTIMIZE_CONCAT_BASE_DIR );
327}
328
329/**
330 * Get a filesystem path relative to a configured base path for resources
331 * that will be concatenated. Assuming a common ancestor allows us to skip
332 * resolving resource URIs to filesystem paths later on.
333 */
334function jetpack_boost_page_optimize_remove_concat_base_prefix( $original_fs_path ) {
335    $abspath = Config::get_abspath();
336
337    // Always check longer path first
338    if ( strlen( $abspath ) > strlen( PAGE_OPTIMIZE_CONCAT_BASE_DIR ) ) {
339        $longer_path  = $abspath;
340        $shorter_path = PAGE_OPTIMIZE_CONCAT_BASE_DIR;
341    } else {
342        $longer_path  = PAGE_OPTIMIZE_CONCAT_BASE_DIR;
343        $shorter_path = $abspath;
344    }
345
346    $prefix_abspath = trailingslashit( $longer_path );
347    if ( jetpack_boost_page_optimize_starts_with( $prefix_abspath, $original_fs_path ) ) {
348        return substr( $original_fs_path, strlen( $prefix_abspath ) );
349    }
350
351    $prefix_basedir = trailingslashit( $shorter_path );
352    if ( jetpack_boost_page_optimize_starts_with( $prefix_basedir, $original_fs_path ) ) {
353        return substr( $original_fs_path, strlen( $prefix_basedir ) );
354    }
355
356    // If we end up here, this is a resource we shouldn't have tried to concat in the first place
357    return '/page-optimize-resource-outside-base-path/' . basename( $original_fs_path );
358}
359
360/**
361 * Schedule a cronjob for the 404 tester, if one isn't already scheduled.
362 */
363function jetpack_boost_page_optimize_schedule_404_tester() {
364    if ( false === wp_next_scheduled( 'jetpack_boost_404_tester_cron' ) ) {
365        wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'jetpack_boost_404_tester_cron' );
366
367        // Run the test immediately, so the settings page can show the result.
368        jetpack_boost_404_tester();
369    }
370}
371
372/**
373 * Schedule a cronjob for cache cleanup, if one isn't already scheduled.
374 */
375function jetpack_boost_page_optimize_schedule_cache_cleanup() {
376    // If caching is on, and job isn't queued for current cache folder
377    if ( false === wp_next_scheduled( 'jetpack_boost_minify_cron_cache_cleanup' ) ) {
378        wp_schedule_event( time(), 'daily', 'jetpack_boost_minify_cron_cache_cleanup' );
379    }
380}
381
382/**
383 * Check whether it's safe to minify for the duration of this HTTP request. Checks
384 * for things like page-builder editors, etc.
385 *
386 * @return bool True if we don't want to minify/concatenate CSS/JS for this request.
387 */
388function jetpack_boost_page_optimize_bail() {
389    static $should_bail = null;
390    if ( null !== $should_bail ) {
391        return $should_bail;
392    }
393
394    $should_bail = false;
395
396    // Bail if this is an admin page
397    if ( is_admin() ) {
398        $should_bail = true;
399        return true;
400    }
401
402    // Bail if we're in customizer
403    global $wp_customize;
404    if ( isset( $wp_customize ) ) {
405        $should_bail = true;
406        return true;
407    }
408
409    // Bail if Divi theme is active, and we're in the Divi Front End Builder
410    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
411    if ( ! empty( $_GET['et_fb'] ) && 'Divi' === wp_get_theme()->get_template() ) {
412        $should_bail = true;
413        return true;
414    }
415
416    // Bail if we're editing pages in Brizy Editor
417    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
418    if ( class_exists( 'Brizy_Editor' ) && method_exists( 'Brizy_Editor', 'prefix' ) && ( isset( $_GET[ Brizy_Editor::prefix( '-edit-iframe' ) ] ) || isset( $_GET[ Brizy_Editor::prefix( '-edit' ) ] ) ) ) {
419        $should_bail = true;
420        return true;
421    }
422
423    // Bail in elementor preview
424    // phpcs:ignore WordPress.Security.NonceVerification.Recommended
425    if ( isset( $_GET['elementor-preview'] ) ) {
426        $should_bail = true;
427        return true;
428    }
429
430    return $should_bail;
431}
432
433/**
434 * Return a URL with a cache-busting query string based on the file's mtime.
435 */
436function jetpack_boost_page_optimize_cache_bust_mtime( $path, $siteurl ) {
437    static $dependency_path_mapping;
438
439    // Absolute paths should dump the path component of siteurl.
440    if ( str_starts_with( $path, '/' ) ) {
441        $parts   = wp_parse_url( $siteurl );
442        $siteurl = $parts['scheme'] . '://' . $parts['host'];
443    }
444
445    $url = $siteurl . $path;
446
447    if ( str_contains( $url, '?m=' ) ) {
448        return $url;
449    }
450
451    $parts = wp_parse_url( $url );
452    if ( ! isset( $parts['path'] ) || empty( $parts['path'] ) ) {
453        return $url;
454    }
455
456    if ( empty( $dependency_path_mapping ) ) {
457        $dependency_path_mapping = new Dependency_Path_Mapping();
458    }
459
460    $file = $dependency_path_mapping->dependency_src_to_fs_path( $url );
461
462    $mtime = false;
463    if ( file_exists( $file ) ) {
464        $mtime = filemtime( $file );
465    }
466
467    if ( ! $mtime ) {
468        return $url;
469    }
470
471    if ( ! str_contains( $url, '?' ) ) {
472        $q = '';
473    } else {
474        list( $url, $q ) = explode( '?', $url, 2 );
475        if ( strlen( $q ) ) {
476            $q = '&amp;' . $q;
477        }
478    }
479
480    return "$url?m={$mtime}{$q}";
481}
482
483/**
484 * Get the URL prefix for static minify/concat resources. Defaults to /_jb_static/, but can be
485 * overridden by defining JETPACK_BOOST_STATIC_PREFIX.
486 */
487function jetpack_boost_get_static_prefix() {
488    $prefix = defined( 'JETPACK_BOOST_STATIC_PREFIX' ) ? JETPACK_BOOST_STATIC_PREFIX : '/_jb_static/';
489
490    if ( ! str_starts_with( $prefix, '/' ) ) {
491        $prefix = '/' . $prefix;
492    }
493
494    return trailingslashit( $prefix );
495}
496
497/**
498 * Whether concatenated files should be linked from the static cache directory.
499 *
500 * Static cache URLs work only where WordPress sees the 404 for a missing wp-content file: the
501 * first request 404s, and Boost builds the file while answering it. On Atomic and WP Cloud, a
502 * per-site platform setting that Boost cannot read decides whether such a 404 reaches WordPress.
503 * Those hosts get the /_jb_static/ fallback unless the filter below says otherwise. Everywhere
504 * else the 404 tester's verdict decides. But jetpack_boost_static_minification travels with the
505 * database, and a migrated site arrives with its previous host's `1`, so this helper asks the
506 * host first.
507 *
508 * Admin\Config::get_hosting_provider() returns 'other' for any host it does not name, so a host it
509 * does not recognize keeps the static cache. A new named value there opts that provider out.
510 *
511 * This is distinct from Minify\Config::can_use_static_cache(), which ensures the cache directory
512 * exists and is writable.
513 *
514 * @since 4.7.0
515 *
516 * @return bool True if static cache URLs should be used, false to fall back to /_jb_static/.
517 */
518function jetpack_boost_minify_use_static_cache_urls() {
519    $use_static_cache_urls = 'other' === Boost_Admin_Config::get_hosting_provider()
520        && get_site_option( 'jetpack_boost_static_minification' );
521
522    /**
523     * Filter whether concatenated CSS and JS are linked from the static cache directory.
524     *
525     * Return true on an excluded host only if that host routes missing wp-content file requests
526     * through WordPress; otherwise every bundle URL 404s at the web server. A true value also
527     * overrides the 404 tester's verdict, and Boost does not schedule the tester on excluded hosts
528     * (a cron event inherited from a previous host can still fire until the next release). Boost
529     * evaluates this filter once per concat group, so return one stable value for the whole render.
530     *
531     * @since 4.7.0
532     *
533     * @param bool $use_static_cache_urls Whether static cache URLs will be used.
534     */
535    return (bool) apply_filters( 'jetpack_boost_minify_use_static_cache_urls', $use_static_cache_urls );
536}
537
538function jetpack_boost_get_minify_url( $file_name = '' ) {
539    return content_url( '/boost-cache/static/' . $file_name );
540}
541
542function jetpack_boost_get_minify_file_path( $file_name = '' ) {
543    return WP_CONTENT_DIR . '/boost-cache/static/' . $file_name;
544}
545
546/**
547 * Detects requests within the `/_jb_static/` directory, and serves minified content.
548 *
549 * @return void
550 */
551function jetpack_boost_minify_serve_concatenated() {
552    // Potential improvement: Make concat URL dir configurable
553    if ( isset( $_SERVER['REQUEST_URI'] ) ) {
554        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
555        $request_path = explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) )[0];
556        $prefix       = jetpack_boost_get_static_prefix();
557        if ( $prefix === substr( $request_path, -strlen( $prefix ), strlen( $prefix ) ) ) {
558            require_once __DIR__ . '/functions-service-fallback.php';
559            jetpack_boost_page_optimize_service_request();
560            exit( 0 ); // @phan-suppress-current-line PhanPluginUnreachableCode -- Safer to include it even though jetpack_boost_page_optimize_service_request() itself never returns.
561        }
562    }
563}
564
565/**
566 * Run during activation of any minify module.
567 *
568 * This handles scheduling cache cleanup, and setting up the cronjob to periodically test for the 404 handler.
569 *
570 * @return void
571 */
572function jetpack_boost_minify_activation() {
573    // Schedule a cronjob for cache cleanup, if one isn't already scheduled.
574    jetpack_boost_page_optimize_schedule_cache_cleanup();
575
576    Cleanup_Stored_Paths::setup_schedule();
577
578    // Setup the cronjob to periodically test for the 404 handler.
579    jetpack_boost_404_setup();
580}
581
582function jetpack_boost_minify_is_enabled() {
583    $minify_css = new Module( new Minify_CSS() );
584    $minify_js  = new Module( new Minify_JS() );
585
586    return $minify_css->is_enabled() || $minify_js->is_enabled();
587}
588
589/**
590 * Run during initialization of any minify module.
591 *
592 * Run during every page load if any minify module is active.
593 */
594function jetpack_boost_minify_init() {
595    add_action( 'jetpack_boost_minify_cron_cache_cleanup', 'jetpack_boost_minify_cron_cache_cleanup' );
596    Cleanup_Stored_Paths::add_cleanup_actions();
597
598    if ( jetpack_boost_page_optimize_bail() ) {
599        return;
600    }
601
602    // Disable Jetpack Site Accelerator CDN for static JS/CSS, if we're minifying this page.
603    add_filter( 'jetpack_force_disable_site_accelerator', '__return_true' );
604}
605
606function jetpack_boost_page_optimize_generate_concat_path( $url_paths, $dependency_path_mapping ) {
607    $fs_paths = array();
608    foreach ( $url_paths as $url_path ) {
609        $fs_paths[] = $dependency_path_mapping->uri_path_to_fs_path( $url_path );
610    }
611
612    $mtime = max( array_map( 'filemtime', $fs_paths ) );
613    if ( jetpack_boost_page_optimize_use_concat_base_dir() ) {
614        $paths = array_map( 'jetpack_boost_page_optimize_remove_concat_base_prefix', $fs_paths );
615    } else {
616        $paths = $url_paths;
617    }
618
619    $file_paths = new File_Paths();
620    $file_paths->set( $paths, $mtime, jetpack_boost_minify_cache_buster() );
621    $file_paths->store();
622
623    return $file_paths->get_cache_id();
624}