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