PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.5.4
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.5.4
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / minify / class-wp-optimize-minify-front-end.php

class-wp-optimize-minify-front-end.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.5.4, at minify/class-wp-optimize-minify-front-end.php

2,778 lines 98.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) die('No direct access allowed');
4
5 class WP_Optimize_Minify_Front_End {
6
7 private $collect_preload_js = array();
8
9 private $collect_preload_css = array();
10
11 private $collect_google_fonts = array();
12
13 private $minify_cache_incremented = false;
14
15 private $options = array();
16
17 /**
18 * Initialize actions and filters
19 *
20 * @return void
21 */
22 public function __construct() {
23 $this->include_dependencies();
24 $this->options = wp_optimize_minify_config()->get();
25
26 // Main process
27 add_action('wp', array($this, 'init'));
28
29 // Beaver builder
30 add_action('fl_builder_after_save_layout', array('WP_Optimize_Minify_Cache_Functions', 'reset'));
31
32 // extra_preload_headers is currently not available to users
33 // add_action('send_headers', array($this, 'extra_preload_headers'));
34
35 if (!is_admin()) {
36 add_action('wp_headers', array($this, 'add_caching_headers'));
37 }
38 }
39
40 /**
41 * Fired on wp init
42 *
43 * @return void
44 */
45 public function init() {
46 /**
47 * Check whether Minify is run on the current page
48 */
49 if (!$this->run_on_page()) return;
50
51 if ($this->options['emoji_removal']) {
52 $this->disable_emojis();
53 }
54
55 if ($this->options['clean_header_one']) {
56 $this->remove_header_meta_info();
57 }
58
59 // Headers & Preload JS/CSS/Extra
60 if ($this->options['enabled_css_preload'] || $this->options['enabled_js_preload']) {
61 add_action('wp_footer', array($this, 'generate_preload_headers'), PHP_INT_MAX);
62 }
63
64 if ($this->options['enable_js']) {
65 $this->process_js();
66 }
67
68 if ($this->options['host_local_google_fonts']) {
69 // Add filter to replace Goole fonts urls to local if needed
70 add_filter('wpo_minify_css_string', array($this, 'replace_gfont_urls_with_local'));
71 add_filter('wpo_minify_get_css', array($this, 'replace_gfont_urls_with_local'));
72 }
73
74 if ($this->options['enable_css']) {
75 $this->process_css();
76 }
77
78 // Preload tags
79 if (trim($this->options['hpreload'])) {
80 add_action('wp_head', array($this, 'add_assets_preload'), 2);
81 }
82
83 if ($this->should_process_html()) {
84 add_action('template_redirect', array('WP_Optimize_Minify_Functions', 'html_compression_start'));
85 }
86
87 if ($this->should_use_loadCSS()) {
88 add_action('wp_footer', array('WP_Optimize_Minify_Print', 'add_load_css'), PHP_INT_MAX);
89 }
90
91 $this->remove_query_string_from_static_assets();
92 }
93
94 /**
95 * Replace Google fonts urls to local versions
96 *
97 * @param string $content
98 * @return string
99 */
100 public function replace_gfont_urls_with_local($content): string {
101 return WP_Optimize_Host_Google_Fonts::instance()->replace_gfont_urls_with_local($content);
102 }
103
104 /**
105 * Detects whether cache preloading is running or not
106 *
107 * @return bool
108 */
109 private function is_cache_preload() {
110 return isset($_SERVER['HTTP_X_WP_OPTIMIZE_CACHE_PRELOAD']) && 0 === strcmp(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_WP_OPTIMIZE_CACHE_PRELOAD'])), 'Yes');
111 }
112
113 /**
114 * Set initial values for the meta log object
115 *
116 * @param string $header - Info on time the log was processed
117 * @return array
118 */
119 private function meta_log_initialization($header) {
120 return array(
121 'header' => $header,
122 'invalidation_reason' => "",
123 'files_that_changed' => array()
124 );
125 }
126
127 /**
128 * Whether to run the feature on a page or not
129 *
130 * @param string $context - Optional, The context where the check is done
131 * @return boolean
132 */
133 public function run_on_page($context = 'default') {
134 /**
135 * Filters whether the functionality is run on the current page.
136 *
137 * @param boolean $run_on_page
138 * @param string $context - Optional, The feature where the check is done
139 */
140 $filtered_value = apply_filters(
141 'wpo_minify_run_on_page',
142 !is_admin()
143 && (!defined('SCRIPT_DEBUG') || !SCRIPT_DEBUG)
144 && !is_preview()
145 && (!function_exists('is_customize_preview') || !is_customize_preview())
146 && !($this->options['disable_when_logged_in'] && is_user_logged_in())
147 && !(function_exists('is_amp_endpoint') && is_amp_endpoint())
148 && !WP_Optimize_Minify_Functions::exclude_contents(),
149 $context
150 );
151 return is_bool($filtered_value) ? $filtered_value : false;
152 }
153
154 /**
155 * Inline css in place, instead of inlining the large file
156 *
157 * @param string $html
158 * @param string $handle
159 * @param string $href
160 * @param mixed $media
161 *
162 * @return string|boolean
163 */
164 public function inline_css($html, $handle, $href, $media) {
165 $exclude_css = $this->get_excluded_css_list();
166 $ignore_list = WP_Optimize_Minify_Functions::compile_ignore_list($exclude_css);
167 $blacklist = WP_Optimize_Minify_Functions::get_ie_blacklist();
168 $async_css = $this->get_async_css_list();
169 $master_ignore = array_merge($ignore_list, $blacklist);
170
171 // make sure href is complete
172 $href = WP_Optimize_Minify_Functions::get_hurl($href);
173
174 if ($this->options['debug']) {
175 echo "<!-- wpo_min DEBUG: Inline CSS processing start ". esc_html($handle) . " / " . esc_html($href) . " -->\n";
176 }
177
178
179 // skip all this, if the async css option is enabled
180 if ($this->options['loadcss']) return $html;
181 // remove all css?
182 if ($this->options['remove_css']) return false;
183 // leave conditionals alone
184 if (wp_styles()->get_data($handle, 'conditional')) return $html;
185
186 // mediatype fix for some plugins + remove print mediatypes
187 if (!is_string($media)
188 || '' === $media
189 || 'screen' === $media
190 || 'screen, print' === $media
191 ) {
192 $media = 'all';
193 }
194 if (!empty($this->options['remove_print_mediatypes']) && 'print' === $media) {
195 return false;
196 }
197
198 // Exclude specific CSS files from PageSpeedInsights?
199 if (WP_Optimize_Minify_Functions::in_arrayi($href, $async_css)) {
200 WP_Optimize_Minify_Print::exclude_style($href);
201 return false;
202 }
203
204 // remove wpo_min from the ignore list
205 $ignore_list = array_filter($ignore_list, array($this, 'check_wpo'));
206
207 // return if in any ignore or black list
208 if (count($master_ignore) > 0 && WP_Optimize_Minify_Functions::in_arrayi($href, $master_ignore)) {
209 return $html;
210 }
211
212 // return if already minified
213 if (WP_Optimize_Minify_Functions::is_minified_css_js_filename($href)) {
214 return $html;
215 }
216
217 // check if working with a font awesome link
218 if (WP_Optimize_Minify_Functions::is_font_awesome($href)) {
219 // font awesome processing, async css
220 if ('async' === $this->options['fawesome_method']) {
221 WP_Optimize_Minify_Print::async_style($href, $media);
222 return false;
223 } elseif ('exclude' === $this->options['fawesome_method']) {
224 // font awesome processing, async and exclude from PageSpeedIndex
225 WP_Optimize_Minify_Print::exclude_style($href);
226 return false;
227 } elseif ('inline' === $this->options['fawesome_method']) {
228 WP_Optimize_Minify_Print::inline_style($handle, $href);
229 return false;
230 }
231 }
232
233 // Check if working with Google font url
234
235 if ('fonts.googleapis.com' === wp_parse_url($href, PHP_URL_HOST)) {
236 // if option disabled google fonts processing
237 if ($this->options['disable_google_fonts_processing']) return $html;
238 // check if google fonts should be removed
239 if ($this->options['remove_googlefonts']) return false;
240 // check if google fonts should be merged
241 if ($this->options['merge_google_fonts']) {
242 if (WP_Optimize_Minify_Functions::is_flatsome_handle($handle)) {
243 $href = WP_Optimize_Minify_Functions::fix_flatsome_google_fonts_url($href);
244 $this->collect_google_fonts[$handle] = $href;
245 } else {
246 $this->collect_google_fonts[$handle] = $href;
247 }
248 return false;
249 } else {
250 if ('inline' === $this->options['gfonts_method']) {
251 if (WP_Optimize_Minify_Functions::is_flatsome_handle($handle)) {
252 $href = WP_Optimize_Minify_Functions::fix_flatsome_google_fonts_url($href);
253 }
254 // download, minify, cache
255 $tkey = 'css-'.hash('adler32', $handle.$href).'.css';
256 $json = false;
257 $json = WP_Optimize_Minify_Cache_Functions::get_transient($tkey);
258 if (false === $json) {
259 $json = WP_Optimize_Minify_Functions::download_and_minify($href, null, $this->options['enable_css_minification'], 'css', $handle);
260 if ($this->options['debug']) {
261 echo "<!-- wpo_min DEBUG: Uncached file processing now for " . esc_html($handle) . " / " . esc_html($href) . " -->\n";
262 }
263 WP_Optimize_Minify_Cache_Functions::set_transient($tkey, $json);
264 }
265
266 // decode
267 $res = json_decode($json, true);
268
269 // inline css or fail
270 if (null !== $res && false !== $res['status']) {
271 // add font-display
272 // https://developers.google.com/web/updates/2016/02/font-display
273 $res['code'] = str_ireplace('font-style:normal;', 'font-display:block;font-style:normal;', $res['code']);
274
275 echo '<style class="optimize_css_1" type="text/css" media="all">';
276 echo $res['code']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is already escaped
277 echo '</style>' . "\n";
278 return false;
279 } else {
280 if ($this->options['debug']) {
281 echo "<!-- wpo_min DEBUG: Google fonts request failed for " . esc_html($href) . " -->\n";
282 }
283 return $html;
284 }
285 } elseif ('async' === $this->options['gfonts_method']) {
286 WP_Optimize_Minify_Print::async_style($href);
287 return false;
288 } elseif ('exclude' === $this->options['gfonts_method']) {
289 WP_Optimize_Minify_Print::exclude_style($href);
290 return false;
291 }
292 }
293 }
294
295 // skip external scripts that are not specifically allowed
296 if (false === WP_Optimize_Minify_Functions::internal_url($href, site_url()) || empty($href)) {
297 if ($this->options['debug']) {
298 echo "<!-- wpo_min DEBUG: Skipped the next external enqueued CSS -->\n";
299 }
300 return $html;
301 }
302
303 $file_size = WP_Optimize_Minify_Functions::get_file_size($href);
304
305 // If we can't determine file size, then we still need to proceed with normal minify process
306 if (apply_filters('wp_optimize_skip_inlining', false === $file_size || $file_size > 20480, $file_size, $href)) return $html;
307
308 // download, minify, cache
309 $tkey = 'css-'.hash('adler32', $handle.$href).'.css';
310 $json = false;
311 $json = WP_Optimize_Minify_Cache_Functions::get_transient($tkey);
312 if (false === $json) {
313 $json = WP_Optimize_Minify_Functions::download_and_minify($href, null, $this->options['enable_css_minification'], 'css', $handle);
314 if ($this->options['debug']) {
315 echo "<!-- wpo_min DEBUG: Uncached file processing now for " . esc_html($handle) . " / " . esc_html($href) . " -->" . "\n";
316 }
317 WP_Optimize_Minify_Cache_Functions::set_transient($tkey, $json);
318 }
319
320 // decode
321 $res = json_decode($json, true);
322
323 // inline it + other inlined children styles
324 if (null !== $res && $res['status']) {
325 echo '<style class="optimize_css_2" type="text/css" media="'.esc_attr($media).'">';
326 echo $res['code']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is already escaped
327 echo '</style>' . "\n";
328
329 // get inline_styles for this handle, minify and print
330 $inline_styles = array();
331 $inline_styles = wp_styles()->get_data($handle, 'after');
332 if ($inline_styles) {
333
334 // string type
335 if (is_string($inline_styles)) {
336 $code = WP_Optimize_Minify_Functions::get_css($href, $inline_styles, $this->options['enable_css_minification']);
337 if (!empty($code)) {
338 echo '<style class="optimize_css_3" type="text/css" media="'.esc_attr($media).'">';
339 echo $code; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is already escaped
340 echo '</style>' . "\n";
341 }
342 }
343
344 // array type
345 if (is_array($inline_styles)) {
346 foreach ($inline_styles as $st) {
347 $code = WP_Optimize_Minify_Functions::get_css($href, $st, $this->options['enable_css_minification']);
348 if (!empty($code)) {
349 echo '<style class="optimize_css_4" type="text/css" media="'.esc_attr($media).'">';
350 echo $code; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is already escaped
351 echo '</style>' . "\n";
352 }
353 }
354 }
355 }
356
357 // prevent default
358 return false;
359 } else {
360 if ($this->options['debug']) {
361 echo "<!-- wpo_min DEBUG: " . esc_html($handle) . " / " . esc_html($href) . " returned an empty from minification -->" . "\n";
362 }
363 return $html;
364 }
365 echo "<!-- ERROR: WPO-Minify couldn't catch the CSS file below. Please report this on https://wordpress.org/support/plugin/wp-optimize/ -->\n";
366 return $html;
367 }
368
369 /**
370 * Enable defer for JavaScript and remove query strings for ignored and all non-minified files
371 *
372 * @param string $tag
373 * @param string $handle
374 * @param string $src
375 *
376 * @return string
377 */
378 public function defer_js($tag, $handle, $src) {
379 $wp_domain = trim(str_ireplace(array('http://', 'https://'), '', trim(site_url(), '/')));
380 $exclude_js = $this->get_excluded_js_list();
381 $ignore_list = WP_Optimize_Minify_Functions::compile_ignore_list($exclude_js);
382 // Should this defer the Poly fills for IE?
383 $blacklist = WP_Optimize_Minify_Functions::get_ie_blacklist();
384
385 // no query strings
386 if (false !== stripos($src, '?ver') && WP_Optimize_Minify_Functions::is_already_minified($src)) {
387 $srcf = stristr($src, '?ver', true);
388 $tag = str_ireplace($src, $srcf, $tag);
389 $src = $srcf;
390 }
391
392 // return if defer option is set to individual
393 if ('individual' === $this->options['enable_defer_js']) {
394 return $tag;
395 }
396
397 // return the tag if it's a polyfill
398 if (count($blacklist) > 0 && WP_Optimize_Minify_Functions::in_arrayi($src, $blacklist)) {
399 return $tag;
400 }
401
402 // Skip deferring the jQuery library option, if the defer jQuery option is disabled
403 if (!$this->options['defer_jquery']
404 && (false !== stripos($tag, '/jquery.js')
405 || false !== stripos($tag, '/jquery.min.js')
406 || (false !== stripos($tag, '/jquery-') && false !== stripos($tag, '.js')))
407 ) {
408 return $tag;
409 }
410
411 // return if external script url https://www.chromestatus.com/feature/5718547946799104
412 if (WP_Optimize_Minify_Functions::is_local_domain($src) !== true) {
413 return $tag;
414 }
415
416 // bypass if already optimized
417 if (false !== stripos($tag, 'navigator.userAgent.match')) {
418 return $tag;
419 }
420
421 // should we exclude defer on the login page?
422 if ($this->options['exclude_defer_login']
423 && isset($_SERVER['SCRIPT_NAME']) && false !== stripos(sanitize_text_field(wp_unslash($_SERVER["SCRIPT_NAME"])), strrchr(wp_login_url(), '/'))
424 ) {
425 return $tag;
426 }
427
428 // add defer attribute, but only if not having async or defer already
429 if (stripos($tag, 'defer') === false && stripos($tag, 'async') === false) {
430
431 // add cdn for PageSpeedIndex
432 if (!empty($this->options['cdn_url'])) {
433 $cdn_url = trim(trim(str_ireplace(array('http://', 'https://'), '', trim($this->options['cdn_url'], '/'))), '/');
434 $src = str_ireplace($wp_domain, $cdn_url, $src);
435 }
436 if ('async_using_js' === $this->options['defer_js_type']) {
437 return WP_Optimize_Minify_Print::async_script($src, false);
438 }
439
440 // remove wpo_min from the ignore list
441 $ignore_list = array_filter($ignore_list, array($this, 'check_wpo'));
442 if (count($ignore_list) > 0 && WP_Optimize_Minify_Functions::in_arrayi($src, $ignore_list)) {
443 return $tag;
444 } else {
445 /**
446 * Filters whether to use the defer attribute or async.
447 *
448 * @default string 'defer' (any other value will be set to async)
449 * @param string $value - 'defer' or 'async'
450 * @param string $tag - The <script> tag
451 * @param string $handle - The WP handle for the script
452 * @param string $src - The SRC for the script
453 * @return string - 'defer' or 'async'
454 */
455 $defer_or_async = apply_filters('wpo_minify_defer_or_async', 'defer', $tag, $handle, $src);
456 if ('defer' === $defer_or_async) {
457 return str_ireplace('<script ', '<script defer ', $tag);
458 } else {
459 return str_ireplace('<script ', '<script async ', $tag);
460 }
461 }
462 }
463
464 // fallback
465 return $tag;
466 }
467
468 /**
469 * Add inline CSS code / Critical Path
470 *
471 * @return void
472 */
473 public function add_critical_path() {
474 if (is_front_page() && !empty($this->options['critical_path_css_is_front_page'])) {
475 echo '<style id="critical-path-is-front-page" type="text/css" media="all">' . "\n";
476 echo $this->options['critical_path_css_is_front_page'] . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is already escaped
477 echo '</style>' . "\n";
478 } elseif (!empty($this->options['critical_path_css'])) {
479 echo '<style id="critical-path-global" type="text/css" media="all">' . "\n";
480 echo $this->options['critical_path_css'] . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is already escaped
481 echo '</style>' . "\n";
482 }
483 }
484
485 /**
486 * Add critical assets preload
487 *
488 * @return void
489 */
490 public function add_assets_preload() {
491 $preload = json_decode($this->options['hpreload']);
492 if (is_array($preload)) {
493 foreach ($preload as $asset) {
494 if (!empty($asset)) {
495 echo '<link rel="preload" href="'.esc_url($asset->href).'" as="'.esc_attr($asset->type).'"'.($asset->crossorigin ? ' crossorigin' : '').'>';
496 }
497 }
498 }
499 }
500
501 /**
502 * Process header CSS
503 *
504 * @return boolean
505 */
506 public function process_header_css() {
507 global $wp_styles;
508 if (!is_object($wp_styles)) return false;
509
510 $cache_path = WP_Optimize_Minify_Cache_Functions::cache_path();
511 $cache_base_dir = $cache_path['cachebasedir'];
512 $cache_dir = $cache_path['cachedir'];
513 $cache_dir_url = $cache_path['cachedirurl'];
514 $exclude_css = $this->get_excluded_css_list();
515 $ignore_list = WP_Optimize_Minify_Functions::compile_ignore_list($exclude_css);
516 $async_css = $this->get_async_css_list();
517
518 $minify_css = $this->options['enable_css_minification'];
519 $merge_css = $this->options['enable_merging_of_css'];
520 $merge_inline_extra_css_js = $this->options['merge_inline_extra_css_js'];
521 $process_css = $minify_css || $merge_css;
522 $disable_google_fonts_processing = $this->options['disable_google_fonts_processing'];
523 $styles = clone $wp_styles;
524 $styles->all_deps($styles->queue);
525 $done = $styles->done;
526 $header = array();
527 $google_fonts = array();
528 $process = array();
529 $inline_css = array();
530 $log = '';
531
532 // dequeue all styles
533 if (isset($this->options['remove_css']) && $this->options['remove_css']) {
534 foreach ($styles->to_do as $handle) {
535 $done = array_merge($done, array($handle));
536 }
537 // remove from queue
538 $wp_styles->done = $done;
539 return false;
540 }
541
542 // get list of handles to process, dequeue duplicate css urls and keep empty source handles (for dependencies)
543 $uniq = array();
544 foreach ($styles->to_do as $handle) {
545
546 // conditionals
547 $conditional = null; if (isset($wp_styles->registered[$handle]->extra["conditional"])) {
548 $conditional = $wp_styles->registered[$handle]->extra["conditional"]; // such as ie7, ie8, ie9, etc
549 }
550
551 // mediatype
552 $mt = isset($wp_styles->registered[$handle]->args) ? $wp_styles->registered[$handle]->args : 'all';
553 if ('screen' === $mt || 'screen, print' === $mt || empty($mt) || is_null($mt) || false === $mt) {
554 $mt = 'all';
555 }
556 $mediatype = $mt;
557
558 // full url or empty
559 $href = WP_Optimize_Minify_Functions::get_hurl($wp_styles->registered[$handle]->src);
560 $version = $wp_styles->registered[$handle]->ver;
561
562 // inlined scripts without file
563 if (empty($href)) continue;
564
565
566 // mark duplicates as done and remove from the queue
567 if (!empty($href)) {
568 $version = is_array($version) ? '' : $version;
569 $key = hash('adler32', $href . $version);
570 if (isset($uniq[$key])) {
571 $done = array_merge($done, array($handle));
572 continue;
573 } else {
574 $uniq[$key] = $handle;
575 }
576 }
577
578 $version = '?ver=' . (!empty($version) ? $version : $wp_styles->default_version);
579 $href_versioned = $href . $version;
580
581 // Exclude specific CSS files from PageSpeedIndex?
582 if (WP_Optimize_Minify_Functions::in_arrayi($href, $async_css)) {
583 WP_Optimize_Minify_Print::exclude_style($href_versioned);
584 $done = array_merge($done, array($handle));
585 continue;
586 }
587 // Fonts Awesome Processing
588 if (WP_Optimize_Minify_Functions::is_font_awesome($href)) {
589 if ('inline' === $this->options['fawesome_method']) {
590 WP_Optimize_Minify_Print::inline_style($handle, $href_versioned);
591 $done = array_merge($done, array($handle));
592 continue;
593 } elseif ('async' === $this->options['fawesome_method']) {
594 WP_Optimize_Minify_Print::async_style($href_versioned, $mediatype);
595 $done = array_merge($done, array($handle));
596 continue;
597 } elseif ('exclude' === $this->options['fawesome_method']) {
598 WP_Optimize_Minify_Print::exclude_style($href_versioned);
599 $done = array_merge($done, array($handle));
600 continue;
601 }
602 }
603
604 // Exclude Print mediatype
605 if (!empty($this->options['remove_print_mediatypes']) && 'print' === $mediatype) {
606 $done = array_merge($done, array($handle));
607 continue;
608 }
609
610 // array of info to save
611 $arr = array(
612 'handle' => $handle,
613 'url' => $href,
614 'conditional' => $conditional,
615 'mediatype' => $mediatype
616 );
617
618 // google fonts to the top (collect and skip process array)
619 if (!$disable_google_fonts_processing && WP_Optimize_Minify_Functions::is_google_font($href)) {
620 if ($this->options['remove_googlefonts']) {
621 $done = array_merge($done, array($handle));
622 continue;
623 }
624 if (WP_Optimize_Minify_Functions::is_flatsome_handle($handle)) {
625 $href = WP_Optimize_Minify_Functions::fix_flatsome_google_fonts_url($href);
626 $google_fonts[$handle] = $href;
627 } else {
628 $google_fonts[$handle] = $href;
629 }
630 }
631 $process[$handle] = $arr;
632 }
633
634 // Process Google fonts
635 if (count($google_fonts) > 0) {
636 // merge google fonts if force inlining is enabled?
637 $nfonts = array();
638 if ($this->options['merge_google_fonts']) {
639 $nfonts[] = WP_Optimize_Minify_Fonts::concatenate_google_fonts($google_fonts);
640 // mark the Google fonts as done so they don't get processed
641 if ('inherit' !== $this->options['gfonts_method']) {
642 $done = array_merge($done, array_keys($google_fonts));
643 }
644 } else {
645 foreach ($google_fonts as $h => $a) {
646 if (!empty($a)) {
647 $nfonts[$h] = $a;
648 }
649 }
650 }
651
652 // foreach google font (will be one if merged is not disabled)
653 foreach ($nfonts as $handle => $href) {
654 if ('inline' === $this->options['gfonts_method']) {
655 // download, minify, cache
656 $tkey = 'css-'.hash('adler32', $href).'.css';
657 // this returns false if the cache is empty! but it doesn't check for failed
658 $json = WP_Optimize_Minify_Cache_Functions::get_transient($tkey);
659 $json = json_decode($json, true);
660 // check if the cache is empty or has no code
661 if (null === $json || false === $json || empty($json['code'])) {
662 $res = WP_Optimize_Minify_Functions::download_and_minify($href, null, $minify_css, 'css', null);
663 if ($this->options['debug']) {
664 echo "<!-- wpo_min DEBUG: Uncached file processing now for " . esc_html($href) . " -->\n";
665 }
666 WP_Optimize_Minify_Cache_Functions::set_transient($tkey, $res);
667 // decode
668 $json = json_decode($res, true);
669 }
670
671 // inline css or fail
672 if (null !== $json && !empty($json['code'])) {
673 // add font-display
674 // https://developers.google.com/web/updates/2016/02/font-display
675 $json['code'] = str_ireplace('font-style:normal;', 'font-display:block;font-style:normal;', $json['code']);
676 echo '<style type="text/css" media="all">';
677 echo $json['code']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is already escaped
678 echo '</style>' . "\n";
679 $done = array_merge($done, array($handle));
680 } else {
681 echo "<!-- GOOGLE FONTS REQUEST FAILED for " . esc_html($href) . " -->" . "\n";
682 // inlining failed, so enqueue again
683 wp_enqueue_style($handle, $href, array(), null); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Not needed as this is a google font and from cache
684 }
685 } elseif ('async' === $this->options['gfonts_method']) {
686 WP_Optimize_Minify_Print::async_style($href);
687 $done = array_merge($done, array($handle));
688 } elseif ('exclude' === $this->options['gfonts_method']) {
689 // make a stylesheet, hide from PageSpeedIndex
690 WP_Optimize_Minify_Print::exclude_style($href);
691 $done = array_merge($done, array($handle));
692 }
693 }
694 }
695
696 // get groups of handles
697 foreach ($styles->to_do as $handle) {
698 // skip already processed google fonts and empty dependencies
699 if (isset($google_fonts[$handle]) && 'inherit' !== $this->options['gfonts_method']) {
700 continue;
701 }
702 if (empty($wp_styles->registered[$handle]->src)) {
703 continue;
704 }
705 if (WP_Optimize_Minify_Functions::in_arrayi($handle, $done)) {
706 continue;
707 }
708 if (!isset($process[$handle])) {
709 continue;
710 }
711
712 // get full url
713 $href = $process[$handle]['url'];
714 $conditional = $process[$handle]['conditional'];
715 $mediatype = $process[$handle]['mediatype'];
716
717 // IE only files don't increment things
718 $ieonly = WP_Optimize_Minify_Functions::is_url_in_ie_blacklist($href);
719 if ($ieonly) {
720 continue;
721 }
722
723 $file_size = WP_Optimize_Minify_Functions::get_file_size($href);
724
725 // If we can't determine file size, then we still need to proceed with normal minify process
726 if (!apply_filters('wp_optimize_skip_inlining', false === $file_size || $file_size > 20480 || !$this->options['inline_css'], $file_size, $href)) continue;
727
728 // check if the current URL points to a minified file.
729 $exclude_minified_css = WP_Optimize_Minify_Functions::is_minified_css_js_filename($href);
730 $normal_processing = $exclude_minified_css && !$merge_css;
731 // skip ignore list, conditional css, external css, font-awesome merge and already minified js
732 if (($process_css && !$normal_processing && !WP_Optimize_Minify_Functions::in_arrayi($href, $ignore_list) && !isset($conditional) && WP_Optimize_Minify_Functions::internal_url($href, site_url()))
733 || empty($href)
734 || ($process_css && 'inherit' === $this->options['fawesome_method'] && WP_Optimize_Minify_Functions::is_font_awesome($href))
735 || ($process_css && !$disable_google_fonts_processing && 'inherit' === $this->options['gfonts_method'] && WP_Optimize_Minify_Functions::is_google_font($href))
736 ) {
737 // collect inline css for this handle
738 if (isset($wp_styles->registered[$handle]->extra['after']) && is_array($wp_styles->registered[$handle]->extra['after'])) {
739 $inline_css[$handle] = WP_Optimize_Minify_Functions::minify_css_string(implode('', $wp_styles->registered[$handle]->extra['after'])); // save
740 $wp_styles->registered[$handle]->extra['after'] = null; // dequeue
741 }
742
743 // process
744 if (isset($header[count($header)-1]['handle']) || count($header) === 0 || $header[count($header)-1]['media'] !== $mediatype || !$merge_css) {
745 array_push($header, array('handles' => array(), 'media' => $mediatype, 'versions' => array()));
746 }
747
748 // push it to the array
749 array_push($header[count($header)-1]['handles'], $handle);
750
751 // We need this because some plugins/theme use empty array as version
752 // https://plugins.trac.wordpress.org/browser/mailin/tags/3.1.54/sendinblue.php#L456
753 $version_str = is_array($wp_styles->registered[$handle]->ver) ? $this->array_to_string_conversion($wp_styles->registered[$handle]->ver) : $wp_styles->registered[$handle]->ver;
754 array_push($header[count($header)-1]['versions'], $version_str);
755
756 $header[count($header) - 1]['last_modified'][] = WP_Optimize_Minify_Functions::get_modification_time($wp_styles->registered[$handle]->src);
757
758 // external and ignored css
759 } else {
760 // normal enqueuing
761 array_push($header, array('handle' => $handle));
762 }
763 }
764
765 /**
766 * Filters the array of stylesheets before processing them
767 *
768 * @param array $list - The list of items filtered
769 * @param string $location - The location of the list (footer or header)
770 * @return array
771 */
772 $header = apply_filters('wpo_minify_stylesheets', $header, 'header');
773
774 // loop through header css and merge
775 for ($i=0,$l=count($header); $i<$l; $i++) {
776
777 if (!isset($header[$i]['handle'])) {
778
779 $handle_str = is_array($header[$i]['handles']) ? $this->array_to_string_conversion($header[$i]['handles']) : strval($header[$i]['handles']);
780 $last_modified_str = empty($header[$i]['last_modified']) ? '' : (is_array($header[$i]['last_modified']) ? $this->array_to_string_conversion($header[$i]['last_modified']) : strval($header[$i]['last_modified']));
781
782 if ($merge_css) {
783 // get hash for the inline css in this group
784 $inline_css_group = array();
785
786 if (is_array($header[$i]['handles'])) {
787 foreach ($header[$i]['handles'] as $h) {
788 if (!empty($inline_css) && isset($inline_css[$h]) && !empty($inline_css[$h])) {
789 $inline_css_group[] = $inline_css[$h];
790 }
791 }
792 }
793 $inline_css_hash = md5(implode('', $inline_css_group));
794 $hash = hash('adler32', $handle_str . $inline_css_hash . $last_modified_str);
795 } else {
796 $hash = sanitize_file_name($handle_str . $last_modified_str);
797 }
798
799 // static cache file info
800 $file_name = 'wpo-minify-header-'.$hash.($minify_css ? '.min' : '');
801
802 // create cache files and urls
803 $file = $cache_dir.'/'.$file_name.'.css';
804 $meta_json_file = $cache_base_dir.'/'.'meta.json';
805 $file_url = WP_Optimize_Minify_Functions::get_protocol("$cache_dir_url/$file_name.css");
806
807 // generate a new cache file
808 clearstatcache();
809 if (!file_exists($file)) {
810
811 // code and log initialization
812 $log_header = $this->get_log_header();
813 $log = array(
814 'header' => $log_header,
815 'files' => array()
816 );
817 $meta_log = $this->meta_log_initialization($log_header);
818 $code = '';
819
820 // minify and write to file
821 foreach ($header[$i]['handles'] as $handle) {
822 if (!empty($wp_styles->registered[$handle]->src)) {
823
824 // get href per handle
825 $handle_src = $wp_styles->registered[$handle]->src;
826 $href = WP_Optimize_Minify_Functions::get_hurl($handle_src);
827 $version = $wp_styles->registered[$handle]->ver;
828 $last_modified = WP_Optimize_Minify_Functions::get_modification_time($handle_src);
829
830 // inlined scripts without file
831 if (empty($href)) continue;
832
833 // download, minify, cache
834 $tkey = 'css-'.hash('adler32', $handle . $href).'.css';
835 $json = false;
836 $json = WP_Optimize_Minify_Cache_Functions::get_transient($tkey);
837 if (false === $json) {
838 $enable_minification = $this->options['enable_css_minification'] && !WP_Optimize_Minify_Functions::is_minified_css_js_filename($href);
839 $json = WP_Optimize_Minify_Functions::download_and_minify($href, null, $enable_minification, 'css', $handle, $version);
840 if ($this->options['debug']) {
841 echo "<!-- wpo_min DEBUG: Uncached file processing now for " . esc_html($handle) . " / " . esc_html($href) . " / " . esc_html($version) . " -->" . "\n";
842 }
843 WP_Optimize_Minify_Cache_Functions::set_transient($tkey, $json);
844 }
845
846 // decode
847 $res = json_decode($json, true);
848
849 if (null === $res) continue;
850
851 if ($this->should_reset_minify_assets($res, $href, 'css', $handle, $version)) {
852 WP_Optimize_Minify_Cache_Functions::reset();
853 $this->minify_cache_incremented = true;
854 $meta_log['invalidation_reason'] = 'Checksum Mismatch and version changed from ' . $res['request']['version'] . ' to ' . $version;
855 $meta_log['files_that_changed'][$handle] = $res['log'];
856 }
857
858 // response has failed
859 if (true !== $res['status']) {
860 $log['files'][$handle] = $res['log'];
861 $log['files'][$handle]['version'] = $version;
862 $log['files'][$handle]['last_modified'] = $last_modified;
863 continue;
864 }
865
866 // Only add the $handle to $done if it was successfully downloaded
867 $done[] = $handle;
868 // append code to merged file
869 $code .= $res['code'] ?? '';
870 $log['files'][$handle] = $res['log'];
871 $log['files'][$handle]['version'] = $version;
872 $log['files'][$handle]['last_modified'] = $last_modified;
873
874 // append inlined styles
875 if ($merge_inline_extra_css_js && isset($inline_css[$handle]) && !empty($inline_css[$handle])) {
876 $code.= $inline_css[$handle];
877 }
878
879 // consider dependencies on handles with an empty src
880 } else {
881 wp_dequeue_script($handle);
882 wp_enqueue_script($handle);
883 }
884 };
885
886 // generate cache, write log
887 if (!empty($code)) {
888 // Only generate meta.json file if cache is reset
889 if (!empty($meta_log['invalidation_reason'])) {
890 file_put_contents($meta_json_file, wp_json_encode($meta_log));
891 }
892 WP_Optimize_Minify_Print::write_combined_asset($file, $code, $log);
893 }
894 } else {
895 $log_file = $file.'.json';
896 if (file_exists($log_file)) {
897 $saved_log = json_decode(file_get_contents($log_file));
898 if (is_object($saved_log) && property_exists($saved_log, 'files')) {
899 $files = (array) $saved_log->files;
900 foreach ($header[$i]['handles'] as $handle) {
901 $handle = is_array($handle) ? '' : $handle;
902 if (isset($files[$handle]) && $files[$handle]->success) {
903 $done[] = $handle;
904 }
905 }
906 } else {
907 // The merged file already exists, so add all files to $done.
908 $done = array_merge($done, $header[$i]['handles']);
909 }
910 } else {
911 // The merged file already exists, so add all files to $done.
912 $done = array_merge($done, $header[$i]['handles']);
913 }
914 }
915
916 // the developers tab, takes precedence
917 // Async CSS with loadCSS ?
918 if ($this->options['loadcss'] && empty($this->options['remove_css'])) {
919 $mt = $header[$i]['media'];
920 WP_Optimize_Minify_Print::async_style($file_url, $mt);
921 // enqueue file, if not empty
922 } else {
923 if (file_exists($file) && filesize($file) > 0) {
924 // enqueue it
925 wp_enqueue_style("wpo_min-header-$i", $file_url, array(), 'mycoolversion', $header[$i]['media']);
926 foreach ($header[$i]['handles'] as $h) {
927 if (!$merge_inline_extra_css_js && isset($inline_css[$h]) && !empty($inline_css[$h])) {
928 wp_add_inline_style("wpo_min-header-$i", $inline_css[$h]);
929 }
930 }
931 } else {
932 // file could not be generated, output something meaningful
933 echo "<!-- ERROR: WP-Optimize Minify was not allowed to save its cache on - ".esc_html(str_replace(ABSPATH, '', $file))." -->";
934 echo "<!-- Please check if the path above is correct and ensure your server has write permission there! -->";
935 }
936 }
937 // other css need to be requeued for the order of files to be kept
938 } else {
939 wp_dequeue_style($header[$i]['handle']);
940 wp_enqueue_style($header[$i]['handle']);
941 }
942 }
943
944 // remove from queue
945 $wp_styles->done = $done;
946 return true;
947 }
948
949 /**
950 * Process JS in the footer
951 *
952 * @return void
953 */
954 public function process_footer_scripts() {
955 global $wp_scripts;
956 if (!is_object($wp_scripts)) {
957 return;
958 }
959 $cache_path = WP_Optimize_Minify_Cache_Functions::cache_path();
960 $cache_base_dir = $cache_path['cachebasedir'];
961 $cache_dir = $cache_path['cachedir'];
962 $cache_dir_url = $cache_path['cachedirurl'];
963
964
965 $exclude_js = $this->get_excluded_js_list();
966 $ignore_list = WP_Optimize_Minify_Functions::compile_ignore_list($exclude_js);
967 $async_js = $this->get_async_js_list();
968 $scripts = clone $wp_scripts;
969 $scripts->all_deps($scripts->queue);
970 $footer = array();
971 $minify_js = $this->options['enable_js_minification'];
972 $merge_js = $this->options['enable_merging_of_js'];
973 $process_js = $minify_js || $merge_js;
974 $merge_inline_extra_css_js = $this->options['merge_inline_extra_css_js'];
975
976 // mark as done (as we go)
977 $done = $scripts->done;
978
979 $previous_load_strategy = null;
980
981 // get groups of handles
982 foreach ($scripts->to_do as $handle) :
983
984 // get full url
985 $href = WP_Optimize_Minify_Functions::get_hurl($wp_scripts->registered[$handle]->src);
986
987 // inlined scripts without file
988 if (empty($href)) {
989 continue;
990 }
991
992 // Exclude JS files from PageSpeedIndex (Async) takes priority over the ignore list
993 if (!empty($async_js)) {
994
995 // check for string match
996 $skipjs = false;
997 foreach ($async_js as $l) {
998 if (stripos($href, $l) !== false) {
999 // print code if there are no linebreaks, or return
1000 $href_versioned = $href . (!empty($wp_scripts->registered[$handle]->ver) ? '?ver=' . $wp_scripts->registered[$handle]->ver : '');
1001 WP_Optimize_Minify_Print::async_script($href_versioned);
1002 $skipjs = true;
1003 $done = array_merge($done, array($handle));
1004 break;
1005 }
1006 }
1007 if (false !== $skipjs) {
1008 continue;
1009 }
1010 }
1011
1012 // IE only files don't increment things
1013 $ieonly = WP_Optimize_Minify_Functions::is_url_in_ie_blacklist($href);
1014 if ($ieonly) {
1015 continue;
1016 }
1017
1018 // check if the current URL points to a minified file.
1019 $exclude_minified_js = WP_Optimize_Minify_Functions::is_minified_css_js_filename($href);
1020 $normal_processing = $exclude_minified_js && !$merge_js;
1021
1022 // skip ignore list, scripts with conditionals, external scripts
1023 if (($process_js && !$normal_processing && !WP_Optimize_Minify_Functions::in_arrayi($href, $ignore_list) && !isset($wp_scripts->registered[$handle]->extra["conditional"]) && WP_Optimize_Minify_Functions::internal_url($href, site_url()))
1024 || empty($href)
1025 ) {
1026
1027 // process
1028 if (isset($footer[count($footer)-1]['handle'])
1029 || !count($footer)
1030 || !$merge_js
1031 || (isset($scripts->registered[$handle]->extra['strategy']) && $previous_load_strategy !== $scripts->registered[$handle]->extra['strategy'])
1032 || (!isset($scripts->registered[$handle]->extra['strategy']) && null !== $previous_load_strategy)
1033 ) {
1034 array_push($footer, array('handles' => array(), 'versions' => array()));
1035 }
1036
1037 if (isset($wp_scripts->registered[$handle]->extra['before'])) {
1038 if (!empty($footer[count($footer)-1]['handles'])) {
1039 array_push($footer, array('handles' => array(), 'versions' => array()));
1040 }
1041 }
1042
1043 // push it to the array
1044 array_push($footer[count($footer)-1]['handles'], $handle);
1045 array_push($footer[count($footer)-1]['versions'], $wp_scripts->registered[$handle]->ver);
1046
1047 // Register strategy if it is used
1048 if (isset($scripts->registered[$handle]->extra['strategy'])) $footer[count($footer)-1]['strategy'] = $scripts->registered[$handle]->extra['strategy'];
1049
1050 $footer[count($footer) - 1]['last_modified'][] = WP_Optimize_Minify_Functions::get_modification_time($wp_scripts->registered[$handle]->src);
1051 // external and ignored scripts
1052
1053 $previous_load_strategy = $scripts->registered[$handle]->extra['strategy'] ?? null;
1054
1055 } else {
1056 array_push($footer, array('handle' => $handle));
1057 }
1058 endforeach;
1059
1060 // loop through footer scripts and merge
1061 for ($i=0,$l=count($footer); $i<$l; $i++) {
1062
1063 if (!isset($footer[$i]['handle'])) {
1064
1065 $handles_str = is_array($footer[$i]['handles']) ? $this->array_to_string_conversion($footer[$i]['handles']) : strval($footer[$i]['handles']);
1066 $last_modified_str = empty($footer[$i]['last_modified']) ? '' : (is_array($footer[$i]['last_modified']) ? $this->array_to_string_conversion($footer[$i]['last_modified']) : strval($footer[$i]['last_modified']));
1067
1068 if ($merge_js) {
1069 // Change the hash based on last modified timestamp
1070 $hash = hash('adler32', $handles_str . $last_modified_str);
1071 } else {
1072 $hash = sanitize_file_name($handles_str . $last_modified_str);
1073 }
1074
1075 // static cache file info
1076 $file_name = 'wpo-minify-footer-'.$hash.($minify_js ? '.min' : '');
1077
1078 // create cache files and urls
1079 $file = $cache_dir.'/'.$file_name.'.js';
1080 $meta_json_file = $cache_base_dir.'/'.'meta.json';
1081 $file_url = WP_Optimize_Minify_Functions::get_protocol($cache_dir_url.'/'.$file_name.'.js');
1082
1083 // generate a new cache file
1084 clearstatcache();
1085 if (!file_exists($file)) {
1086
1087 // code and log initialization
1088 $log_header = $this->get_log_header();
1089 $log = array(
1090 'header' => $log_header,
1091 'files' => array()
1092 );
1093 $meta_log = $this->meta_log_initialization($log_header);
1094 $before_code = '';
1095 $code = '';
1096 $after_code = '';
1097
1098 // minify and write to file
1099 foreach ($footer[$i]['handles'] as $handle) :
1100 if (!empty($wp_scripts->registered[$handle]->src)) {
1101 // only add minified scripts once to the bundle
1102 if (in_array($handle, $done)) continue;
1103
1104 // get href per handle
1105 $handle_src = $wp_scripts->registered[$handle]->src;
1106 $href = WP_Optimize_Minify_Functions::get_hurl($handle_src);
1107 $version = $wp_scripts->registered[$handle]->ver;
1108 $last_modified = WP_Optimize_Minify_Functions::get_modification_time($handle_src);
1109 // inlined scripts without file
1110 if (empty($href)) {
1111 continue;
1112 }
1113 // download, minify, cache
1114 $tkey = 'js-'.hash('adler32', $handle . $href).'.js';
1115 $json = false;
1116 $json = WP_Optimize_Minify_Cache_Functions::get_transient($tkey);
1117 if (false === $json) {
1118 $enable_minification = $minify_js && !WP_Optimize_Minify_Functions::is_minified_css_js_filename($href);
1119 $json = WP_Optimize_Minify_Functions::download_and_minify($href, null, $enable_minification, 'js', $handle, $version);
1120 if ($this->options['debug']) {
1121 echo "<!-- wpo_min DEBUG: Uncached file processing now for " . esc_html($handle) . " / " . esc_html($href) . " / " . esc_html($version) . " -->\n";
1122 }
1123 WP_Optimize_Minify_Cache_Functions::set_transient($tkey, $json);
1124 }
1125
1126 // decode
1127 $res = json_decode($json, true);
1128
1129 if (null === $res) continue;
1130
1131 if ($this->should_reset_minify_assets($res, $href, 'js', $handle, $version)) {
1132 WP_Optimize_Minify_Cache_Functions::reset();
1133 $this->minify_cache_incremented = true;
1134 $meta_log['invalidation_reason'] = 'Checksum Mismatch and version changed from ' . $res['request']['version'] . ' to ' . $version;
1135 $meta_log['files_that_changed'][$handle] = $res['log'];
1136 }
1137
1138 // response has failed
1139 if (true !== $res['status']) {
1140 $log['files'][$handle] = $res['log'];
1141 $log['files'][$handle]['uses_loading_strategy'] = isset($footer[$i]['strategy']);
1142 $log['files'][$handle]['version'] = $version;
1143 $log['files'][$handle]['last_modified'] = $last_modified;
1144 continue;
1145 }
1146
1147 $done[] = $handle;
1148 // Add extra data from wp_add_inline_script before
1149 if (!empty($wp_scripts->registered[$handle]->extra)) {
1150 if (!empty($wp_scripts->registered[$handle]->extra['before']) && is_array($wp_scripts->registered[$handle]->extra['before'])) {
1151 if ($merge_inline_extra_css_js) {
1152 $before_code .= "\n" . implode("\n", array_filter($wp_scripts->registered[$handle]->extra['before']));
1153 }
1154 }
1155 }
1156
1157 // Add translation
1158 if (!empty($wp_scripts->registered[$handle]->textdomain)) {
1159 $code .= "\n" . $wp_scripts->print_translations($handle, false);
1160 }
1161
1162 // Add extra data from wp_add_inline_script after
1163 if (!empty($wp_scripts->registered[$handle]->extra)) {
1164 if (!empty($wp_scripts->registered[$handle]->extra['after']) && is_array($wp_scripts->registered[$handle]->extra['after'])) {
1165 if ($merge_inline_extra_css_js) {
1166 $after_code .= "\n" . implode("\n", array_filter($wp_scripts->registered[$handle]->extra['after']));
1167 }
1168 }
1169 }
1170
1171 // append code to merged file
1172 $code .= $before_code . "\n";
1173 $code .= $res['code'] ?? '';
1174 $code .= "\n" . $after_code . "\n";
1175 $code = WP_Optimize_Minify_Functions::prepare_merged_js($code, $href);
1176 $log['files'][$handle] = $res['log'];
1177 $log['files'][$handle]['uses_loading_strategy'] = isset($footer[$i]['strategy']);
1178 $log['files'][$handle]['version'] = $version;
1179 $log['files'][$handle]['last_modified'] = $last_modified;
1180
1181 // consider dependencies on handles with an empty src
1182 } else {
1183 wp_dequeue_script($handle);
1184 wp_enqueue_script($handle);
1185 }
1186 endforeach;
1187
1188 // generate cache, write log
1189 if (!empty($code)) {
1190 // Only generate meta.json file if cache is reset
1191 if (!empty($meta_log['invalidation_reason'])) {
1192 file_put_contents($meta_json_file, wp_json_encode($meta_log));
1193 }
1194 WP_Optimize_Minify_Print::write_combined_asset($file, $code, $log);
1195 }
1196 } else {
1197 $log_file = $file.'.json';
1198 if (file_exists($log_file)) {
1199 $saved_log = json_decode(file_get_contents($log_file));
1200 if (is_object($saved_log) && property_exists($saved_log, 'files')) {
1201 $files = (array) $saved_log->files;
1202 foreach ($footer[$i]['handles'] as $handle) {
1203 if (isset($files[$handle]) && $files[$handle]->success) {
1204 $done[] = $handle;
1205 }
1206 }
1207 } else {
1208 // The merged file already exists, so add all files to $done.
1209 $done = array_merge($done, $footer[$i]['handles']);
1210 }
1211 } else {
1212 // The merged file already exists, so add all files to $done.
1213 $done = array_merge($done, $footer[$i]['handles']);
1214 }
1215 }
1216
1217 // register minified file
1218 wp_register_script("wpo_min-footer-$i", $file_url, array(), null, false); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Not needed as this is a timestamp based cache
1219
1220 // add all extra data from wp_localize_script
1221 $before_code = '';
1222 $data = array();
1223 $after_code = '';
1224
1225 foreach ($footer[$i]['handles'] as $handle) {
1226 if (isset($wp_scripts->registered[$handle]->extra['data'])) {
1227 $data[] = $wp_scripts->registered[$handle]->extra['data'];
1228 }
1229 // Add extra data from wp_add_inline_script before
1230 if (!empty($wp_scripts->registered[$handle]->extra)) {
1231 if (!empty($wp_scripts->registered[$handle]->extra['before']) && is_array($wp_scripts->registered[$handle]->extra['before'])) {
1232 if (!$merge_inline_extra_css_js) {
1233 $before_code.= "\n" . implode("\n", array_filter($wp_scripts->registered[$handle]->extra['before'])) . "\n";
1234 }
1235 }
1236 if (!empty($wp_scripts->registered[$handle]->extra['after']) && is_array($wp_scripts->registered[$handle]->extra['after'])) {
1237 if (!$merge_inline_extra_css_js) {
1238 $after_code.= "\n" . implode("\n", array_filter($wp_scripts->registered[$handle]->extra['after'])) . "\n";
1239 }
1240 }
1241 }
1242 }
1243 if (count($data) > 0) {
1244 $wp_scripts->registered["wpo_min-footer-$i"]->extra['data'] = implode("\n", $data);
1245 }
1246
1247 // enqueue file, if not empty
1248 if (file_exists($file) && (filesize($file) > 0 || count($data) > 0)) {
1249 if (!empty($before_code)) {
1250 wp_add_inline_script("wpo_min-footer-$i", $before_code, 'before');
1251 }
1252 wp_enqueue_script("wpo_min-footer-$i");
1253 if (!empty($after_code)) {
1254 wp_add_inline_script("wpo_min-footer-$i", $after_code, 'after');
1255 }
1256
1257 if (isset($footer[$i]['strategy'])) {
1258 // Backward compatible way of doing this
1259 wp_script_add_data( "wpo_min-footer-$i", 'strategy', $footer[$i]['strategy']);
1260 }
1261 } else {
1262 // file could not be generated, output something meaningful
1263 echo "<!-- ERROR: WP-Optimize Minify was not allowed to save its cache on - ".esc_html(str_replace(ABSPATH, '', $file))." -->";
1264 echo "<!-- Please check if the path above is correct and ensure your server has write permission there! -->";
1265 }
1266
1267 // other scripts need to be requeued for the order of files to be kept
1268 } else {
1269 wp_dequeue_script($footer[$i]['handle']);
1270 wp_enqueue_script($footer[$i]['handle']);
1271 }
1272 }
1273
1274 // remove from queue
1275 $wp_scripts->done = $done;
1276 }
1277
1278 /**
1279 * Process header JavaScript
1280 * Dependent on 'enable_js' option
1281 *
1282 * @return void
1283 */
1284 public function process_header_scripts() {
1285 global $wp_scripts;
1286 if (!is_object($wp_scripts)) return;
1287 $cache_path = WP_Optimize_Minify_Cache_Functions::cache_path();
1288 $cache_base_dir = $cache_path['cachebasedir'];
1289 $cache_dir = $cache_path['cachedir'];
1290 $cache_dir_url = $cache_path['cachedirurl'];
1291 $exclude_js = $this->get_excluded_js_list();
1292 $ignore_list = WP_Optimize_Minify_Functions::compile_ignore_list($exclude_js);
1293 $async_js = $this->get_async_js_list();
1294 $scripts = clone $wp_scripts;
1295 $scripts->all_deps($scripts->queue);
1296 $minify_js = $this->options['enable_js_minification'];
1297 $merge_js = $this->options['enable_merging_of_js'];
1298 $process_js = $minify_js || $merge_js;
1299 $merge_inline_extra_css_js = $this->options['merge_inline_extra_css_js'];
1300 $header = array();
1301 // mark as done (as we go)
1302 $done = $scripts->done;
1303 $excluded_dependencies = array();
1304
1305 $previous_load_strategy = null;
1306
1307 // Prepare and separate assets (get groups of handles)
1308 foreach ($scripts->to_do as $handle) {
1309 // get full url
1310 $href = WP_Optimize_Minify_Functions::get_hurl($wp_scripts->registered[$handle]->src);
1311 // inlined scripts without file
1312 if (empty($href)) {
1313 wp_enqueue_script($handle, false); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Not needed
1314 continue;
1315 }
1316
1317 // Only go through items without a group
1318 if (!$scripts->groups[$handle]) {
1319 // Exclude JS files from PageSpeedIndex (Async) takes priority over the ignore list
1320 // check for string match
1321 $skipjs = false;
1322 foreach ($async_js as $l) {
1323 if (stripos($href, $l) !== false) {
1324 $href_versioned = $href . (!empty($wp_scripts->registered[$handle]->ver) ? '?ver=' . $wp_scripts->registered[$handle]->ver : '');
1325 WP_Optimize_Minify_Print::async_script($href_versioned);
1326 $skipjs = true;
1327 $done = array_merge($done, array($handle));
1328 break;
1329 }
1330 }
1331 // IE only files don't increment things
1332 if ($skipjs
1333 || WP_Optimize_Minify_Functions::is_url_in_ie_blacklist($href)
1334 ) {
1335 continue;
1336 }
1337
1338 // Skip jQuery from the merged files if deferring is ENABLED and defer_jquery is DISABLED
1339 if ('all' === $this->options['enable_defer_js']
1340 && !$this->options['defer_jquery']
1341 && (false !== stripos($href, '/jquery.js')
1342 || false !== stripos($href, '/jquery.min.js')
1343 || (false !== stripos($href, '/jquery-') && false !== stripos($href, '.js')))
1344 ) {
1345 continue;
1346 }
1347
1348 // check if the current URL points to a minified file.
1349 $exclude_minified_js = WP_Optimize_Minify_Functions::is_minified_css_js_filename($href);
1350 $normal_processing = $exclude_minified_js && !$merge_js;
1351
1352 // Group handles - skip ignore list, scripts with conditionals, external scripts
1353 if (($process_js && !$normal_processing && !WP_Optimize_Minify_Functions::in_arrayi($href, $ignore_list) && !isset($wp_scripts->registered[$handle]->extra["conditional"]) && WP_Optimize_Minify_Functions::internal_url($href, site_url()))
1354 || empty($href)
1355 ) {
1356 // process
1357 if (isset($header[count($header)-1]['handle'])
1358 || !count($header)
1359 || !$merge_js
1360 || (isset($scripts->registered[$handle]->extra['strategy']) && $previous_load_strategy !== $scripts->registered[$handle]->extra['strategy'])
1361 || (!isset($scripts->registered[$handle]->extra['strategy']) && null !== $previous_load_strategy)
1362 ) {
1363 array_push($header, array('handles' => array(), 'versions' => array()));
1364 }
1365
1366 // Force loading of dependencies
1367 foreach ($wp_scripts->registered[$handle]->deps as $dep) {
1368 // If the handle is not present in $done yet, or excluded, enqueue it.
1369 $dep_href = WP_Optimize_Minify_Functions::get_hurl($wp_scripts->registered[$dep]->src);
1370 if (!in_array($dep, $done) && !WP_Optimize_Minify_Functions::in_arrayi($dep_href, $ignore_list)) {
1371 // Include any dependency
1372 array_push($header[count($header)-1]['handles'], $dep);
1373 array_push($header[count($header)-1]['versions'], $wp_scripts->registered[$dep]->ver);
1374 } elseif (!in_array($dep, $done) && WP_Optimize_Minify_Functions::in_arrayi($dep_href, $ignore_list)) {
1375 // The dependency is in the exclude list
1376 array_push($header, array('handle' => $dep));
1377 // Record dependency to be added to the minified script as dependency array
1378 if (isset($excluded_dependencies[count($header)])) {
1379 $excluded_dependencies[count($header)][] = $dep;
1380 } else {
1381 $excluded_dependencies[count($header)] = array($dep);
1382 }
1383 // Adds the 'handles' record for the main script
1384 array_push($header, array('handles' => array(), 'versions' => array()));
1385 }
1386 }
1387
1388 if (isset($wp_scripts->registered[$handle]->extra['before'])) {
1389 if (!empty($header[count($header)-1]['handles'])) {
1390 array_push($header, array('handles' => array(), 'versions' => array()));
1391 }
1392 }
1393
1394 // push it to the array
1395 array_push($header[count($header)-1]['handles'], $handle);
1396 array_push($header[count($header)-1]['versions'], $scripts->registered[$handle]->ver);
1397 $header[count($header) - 1]['last_modified'][] = WP_Optimize_Minify_Functions::get_modification_time($wp_scripts->registered[$handle]->src);
1398
1399 // Register strategy if it is used
1400 if (isset($scripts->registered[$handle]->extra['strategy'])) $header[count($header)-1]['strategy'] = $scripts->registered[$handle]->extra['strategy'];
1401
1402 $previous_load_strategy = $scripts->registered[$handle]->extra['strategy'] ?? null;
1403
1404
1405 // external and ignored scripts
1406 } else {
1407 // add the ignored assets
1408 array_push($header, array('handle' => $handle));
1409 }
1410
1411 // make sure that the scripts skipped here, show up in the footer
1412 } else {
1413 wp_enqueue_script($handle, $href, array(), null, true); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Not needed as this is a timestamp based cache
1414 }
1415 }
1416
1417 // loop through header scripts and merge
1418 for ($i=0,$l=count($header); $i < $l; $i++) {
1419
1420 if (!isset($header[$i]['handle'])) {
1421
1422 $handles_str = is_array($header[$i]['handles']) ? $this->array_to_string_conversion($header[$i]['handles']) : strval($header[$i]['handles']);
1423 $last_modified_str = empty($header[$i]['last_modified']) ? '' : (is_array($header[$i]['last_modified']) ? $this->array_to_string_conversion($header[$i]['last_modified']) : strval($header[$i]['last_modified']));
1424
1425 if ($merge_js) {
1426 $hash = hash('adler32', $handles_str . $last_modified_str);
1427 } else {
1428 $hash = sanitize_file_name($handles_str . $last_modified_str);
1429 }
1430
1431 // static cache file info
1432 $file_name = 'wpo-minify-header-'.$hash.($minify_js ? '.min' : '');
1433 // create cache files and urls
1434 $file = $cache_dir.'/'.$file_name.'.js';
1435 $meta_json_file = $cache_base_dir.'/'.'meta.json';
1436 $file_url = WP_Optimize_Minify_Functions::get_protocol($cache_dir_url.'/'.$file_name.'.js');
1437
1438 // generate a new cache file
1439 clearstatcache();
1440 if (!file_exists($file)) {
1441
1442 // code and log initialization
1443 $log_header = $this->get_log_header();
1444 $log = array(
1445 'header' => $log_header,
1446 'files' => array()
1447 );
1448 $meta_log = $this->meta_log_initialization($log_header);
1449 $before_code = '';
1450 $code = '';
1451 $after_code = '';
1452
1453 // minify and write to file
1454 foreach ($header[$i]['handles'] as $handle) {
1455 if (!empty($wp_scripts->registered[$handle]->src)) {
1456 // only add minified scripts once to the bundle
1457 if (in_array($handle, $done)) continue;
1458
1459 // get href per handle
1460 $handle_src = $wp_scripts->registered[$handle]->src;
1461 $href = WP_Optimize_Minify_Functions::get_hurl($handle_src);
1462 $version = $wp_scripts->registered[$handle]->ver;
1463 $last_modified = WP_Optimize_Minify_Functions::get_modification_time($handle_src);
1464 if (empty($href)) continue;
1465 // download, minify, cache
1466 $tkey = 'js-'.hash('adler32', $handle . $href).'.js';
1467 $json = false;
1468 $json = WP_Optimize_Minify_Cache_Functions::get_transient($tkey);
1469 if (false === $json) {
1470 $enable_minification = $minify_js && !WP_Optimize_Minify_Functions::is_minified_css_js_filename($href);
1471 $json = WP_Optimize_Minify_Functions::download_and_minify($href, null, $enable_minification, 'js', $handle, $version);
1472 if ($this->options['debug']) {
1473 echo "<!-- wpo_min DEBUG: Uncached file processing now for " . esc_html($handle) . " / " . esc_html($href) . " / " . esc_html($version) . " -->" . "\n";
1474 }
1475 WP_Optimize_Minify_Cache_Functions::set_transient($tkey, $json);
1476 }
1477
1478 // decode
1479 $res = json_decode($json, true);
1480
1481 if (null === $res) continue;
1482
1483 if ($this->should_reset_minify_assets($res, $href, 'js', $handle, $version)) {
1484 WP_Optimize_Minify_Cache_Functions::reset();
1485 $this->minify_cache_incremented = true;
1486 $meta_log['invalidation_reason'] = 'Checksum Mismatch and version changed from ' . $res['request']['version'] . ' to ' . $version;
1487 $meta_log['files_that_changed'][$handle] = $res['log'];
1488 }
1489
1490 // response has failed
1491 if (true !== $res['status']) {
1492 $log['files'][$handle] = $res['log'];
1493 $log['files'][$handle]['uses_loading_strategy'] = isset($header[$i]['strategy']);
1494 $log['files'][$handle]['version'] = $version;
1495 $log['files'][$handle]['last_modified'] = $last_modified;
1496 continue;
1497 }
1498
1499 // Add extra data from wp_add_inline_script before
1500 if (!empty($wp_scripts->registered[$handle]->extra)) {
1501 if (!empty($wp_scripts->registered[$handle]->extra['before']) && is_array($wp_scripts->registered[$handle]->extra['before'])) {
1502 if ($merge_inline_extra_css_js) {
1503 $before_code .= "\n" . implode("\n", array_filter($wp_scripts->registered[$handle]->extra['before']));
1504 }
1505 }
1506 }
1507
1508 // Only add the $handle to $done if it was successfully downloaded
1509 $done[] = $handle;
1510
1511 // Add translations
1512 if (!empty($wp_scripts->registered[$handle]->textdomain)) {
1513 $code .= "\n" . $wp_scripts->print_translations($handle, false);
1514 }
1515
1516 // Add extra data from wp_add_inline_script after
1517 if (!empty($wp_scripts->registered[$handle]->extra)) {
1518 if (!empty($wp_scripts->registered[$handle]->extra['after']) && is_array($wp_scripts->registered[$handle]->extra['after'])) {
1519 if ($merge_inline_extra_css_js) {
1520 $after_code.= "\n" . implode("\n", array_filter($wp_scripts->registered[$handle]->extra['after']));
1521 }
1522 }
1523 }
1524
1525 // append code to merged file
1526 $code .= $before_code . "\n";
1527 $code .= isset($res['code']) ? $res['code'] : '';
1528 $code .= "\n" . $after_code . "\n";
1529 $code = WP_Optimize_Minify_Functions::prepare_merged_js($code, $href);
1530 $log['files'][$handle] = $res['log'];
1531 $log['files'][$handle]['uses_loading_strategy'] = isset($header[$i]['strategy']);
1532 $log['files'][$handle]['version'] = $version;
1533 $log['files'][$handle]['last_modified'] = $last_modified;
1534
1535 // consider dependencies on handles with an empty src
1536 } else {
1537 wp_dequeue_script($handle);
1538 wp_enqueue_script($handle);
1539 }
1540 }
1541
1542 // generate cache, write log
1543 if (!empty($code)) {
1544 // Only generate meta.json file if cache is reset
1545 if (!empty($meta_log['invalidation_reason'])) {
1546 file_put_contents($meta_json_file, wp_json_encode($meta_log));
1547 }
1548 WP_Optimize_Minify_Print::write_combined_asset($file, $code, $log);
1549 }
1550 } else {
1551 $log_file = $file.'.json';
1552 if (file_exists($log_file)) {
1553 $saved_log = json_decode(file_get_contents($log_file));
1554 if (is_object($saved_log) && property_exists($saved_log, 'files')) {
1555 $files = (array) $saved_log->files;
1556 foreach ($header[$i]['handles'] as $handle) {
1557 $handle = is_array($handle) ? '' : $handle;
1558 if (isset($files[$handle]) && $files[$handle]->success) {
1559 $done[] = $handle;
1560 }
1561 }
1562 } else {
1563 // The merged file already exists, so add all files to $done.
1564 $done = array_merge($done, $header[$i]['handles']);
1565 }
1566 } else {
1567 // The merged file already exists, so add all files to $done.
1568 $done = array_merge($done, $header[$i]['handles']);
1569 }
1570 }
1571
1572 // register minified file
1573 $dependencies = isset($excluded_dependencies[$i]) ? $excluded_dependencies[$i] : array();
1574 wp_register_script("wpo_min-header-$i", $file_url, $dependencies, null, false); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Not needed as this is a timestamp based cache
1575
1576 // add all extra data from wp_localize_script
1577 $before_code = '';
1578 $data = array();
1579 $after_code = '';
1580
1581 foreach ($header[$i]['handles'] as $handle) {
1582 if (isset($wp_scripts->registered[$handle]->extra['data'])) {
1583 $data[] = $wp_scripts->registered[$handle]->extra['data'];
1584 }
1585
1586 // Add extra data from wp_add_inline_script before
1587 if (!empty($wp_scripts->registered[$handle]->extra)) {
1588 if (!empty($wp_scripts->registered[$handle]->extra['before']) && is_array($wp_scripts->registered[$handle]->extra['before'])) {
1589 if (!$merge_inline_extra_css_js) {
1590 $before_code.= "\n" . implode("\n", array_filter($wp_scripts->registered[$handle]->extra['before'])) . "\n";
1591 }
1592 }
1593 if (!empty($wp_scripts->registered[$handle]->extra['after']) && is_array($wp_scripts->registered[$handle]->extra['after'])) {
1594 if (!$merge_inline_extra_css_js) {
1595 $after_code.= "\n" . implode("\n", array_filter($wp_scripts->registered[$handle]->extra['after'])) . "\n";
1596 }
1597 }
1598 }
1599 }
1600 if (count($data) > 0) {
1601 $wp_scripts->registered["wpo_min-header-$i"]->extra['data'] = implode("\n", $data);
1602 }
1603
1604 // enqueue file, if not empty
1605 if (file_exists($file) && (filesize($file) > 0 || count($data) > 0)) {
1606 if (!empty($before_code)) {
1607 wp_add_inline_script("wpo_min-header-$i", $before_code, 'before');
1608 }
1609 wp_enqueue_script("wpo_min-header-$i");
1610 if (!empty($after_code)) {
1611 wp_add_inline_script("wpo_min-header-$i", $after_code, 'after');
1612 }
1613
1614 if (isset($header[$i]['strategy'])) {
1615 // Backward compatible way of doing this
1616 wp_script_add_data( "wpo_min-header-$i", 'strategy', $header[$i]['strategy']);
1617 }
1618 } else {
1619 // file could not be generated, output something meaningful
1620 echo "<!-- ERROR: WP-Optimize minify was not allowed to save its cache on - ".esc_html(str_replace(ABSPATH, '', $file))." -->";
1621 echo "<!-- Please check if the path above is correct and ensure your server has write permission there! -->";
1622 echo "<!-- If you found a bug, please report this on https://wordpress.org/support/plugin/wp-optimize/ -->";
1623 }
1624
1625 // other scripts need to be requeued for the order of files to be kept
1626 } else {
1627 wp_dequeue_script($header[$i]['handle']);
1628 wp_enqueue_script($header[$i]['handle']);
1629 }
1630 }
1631
1632 // remove from queue
1633 $wp_scripts->done = $done;
1634 }
1635
1636 /**
1637 * Process CSS in the footer
1638 *
1639 * @return void
1640 */
1641 public function process_footer_css() {
1642 global $wp_styles;
1643 if (!is_object($wp_styles)) return;
1644 $cache_path = WP_Optimize_Minify_Cache_Functions::cache_path();
1645 $cache_base_dir = $cache_path['cachebasedir'];
1646 $cache_dir = $cache_path['cachedir'];
1647 $cache_dir_url = $cache_path['cachedirurl'];
1648 $exclude_css = $this->get_excluded_css_list();
1649 $ignore_list = WP_Optimize_Minify_Functions::compile_ignore_list($exclude_css);
1650 $async_css = $this->get_async_css_list();
1651 $minify_css = $this->options['enable_css_minification'];
1652 $merge_css = $this->options['enable_merging_of_css'];
1653 $process_css = $minify_css || $merge_css;
1654 $disable_google_fonts_processing = $this->options['disable_google_fonts_processing'];
1655 $log = "";
1656 $code = "";
1657 $styles = clone $wp_styles;
1658 $styles->all_deps($styles->queue);
1659 $done = $styles->done;
1660 $footer = array();
1661 $google_fonts = array();
1662 $inline_css = array();
1663
1664 // dequeue all styles
1665 if (isset($this->options['remove_css']) && $this->options['remove_css']) {
1666 foreach ($styles->to_do as $handle) :
1667 $done = array_merge($done, array($handle));
1668 endforeach;
1669
1670 // remove from queue
1671 $wp_styles->done = $done;
1672 return;
1673 }
1674
1675 // dequeue and get a list of Google fonts, or requeue external
1676 foreach ($styles->to_do as $handle) {
1677 $href = WP_Optimize_Minify_Functions::get_hurl($wp_styles->registered[$handle]->src);
1678 // inlined scripts without file
1679 if (empty($href)) continue;
1680
1681 if (!$disable_google_fonts_processing && WP_Optimize_Minify_Functions::is_google_font($href)) {
1682 wp_dequeue_style($handle);
1683 if ($this->options['remove_googlefonts']) {
1684 $done = array_merge($done, array($handle));
1685 continue;
1686 }
1687 // mark as done if to be removed
1688 if ($this->options['merge_google_fonts']
1689 || 'inline' === $this->options['gfonts_method']
1690 ) {
1691 if (WP_Optimize_Minify_Functions::is_flatsome_handle($handle)) {
1692 $href = WP_Optimize_Minify_Functions::fix_flatsome_google_fonts_url($href);
1693 $google_fonts[$handle] = $href;
1694 } else {
1695 $google_fonts[$handle] = $href;
1696 }
1697 } else {
1698 // skip google fonts optimization?
1699 wp_enqueue_style($handle);
1700 }
1701 } else {
1702 // failsafe
1703 wp_dequeue_style($handle);
1704 wp_enqueue_style($handle);
1705 }
1706
1707 }
1708
1709 // concat google fonts, if enabled
1710 if ($this->options['merge_google_fonts']
1711 && count($google_fonts) > 0
1712 || ('inline' === $this->options['gfonts_method'] && count($google_fonts) > 0)
1713 ) {
1714
1715 // merge google fonts if force inlining is enabled?
1716 $nfonts = array();
1717 if ($this->options['merge_google_fonts']) {
1718 $nfonts[] = WP_Optimize_Minify_Fonts::concatenate_google_fonts($google_fonts);
1719 } else {
1720 foreach ($google_fonts as $h => $a) {
1721 if (!empty($a)) {
1722 $nfonts[$h] = $a;
1723 }
1724 }
1725 }
1726
1727 // foreach google font (will be one if merged is not disabled)
1728 if (count($nfonts) > 0) {
1729 foreach ($nfonts as $handle => $href) {
1730 // hide from PageSpeedIndex, async, inline, or default
1731 if ('exclude' === $this->options['gfonts_method']) {
1732 WP_Optimize_Minify_Print::exclude_style($href);
1733 $done = array_merge($done, array($handle));
1734 } elseif ('async' === $this->options['gfonts_method']) {
1735 // async CSS
1736 WP_Optimize_Minify_Print::async_style($href);
1737 $done = array_merge($done, array($handle));
1738 } elseif ('inline' === $this->options['gfonts_method']) {
1739 // inline css
1740 WP_Optimize_Minify_Print::inline_style($handle, $href);
1741 $done = array_merge($done, array($handle));
1742 } else {
1743 // fallback, enqueue google fonts
1744 wp_enqueue_style($handle, $href, array(), null, 'all'); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Not needed as this google font and from cache
1745 }
1746 }
1747 }
1748 }
1749
1750 // get groups of handles
1751 $uniq = array();
1752 foreach ($styles->to_do as $handle) {
1753
1754 // skip already processed google fonts
1755 if (isset($google_fonts[$handle])) {
1756 continue;
1757 }
1758
1759 // conditionals
1760 $conditional = null; if (isset($wp_styles->registered[$handle]->extra["conditional"])) {
1761 $conditional = $wp_styles->registered[$handle]->extra["conditional"]; // such as ie7, ie8, ie9, etc
1762 }
1763
1764 // media type
1765 $mt = isset($wp_styles->registered[$handle]->args) ? $wp_styles->registered[$handle]->args : 'all';
1766 if ('screen' === $mt || 'screen, print' === $mt || empty($mt)) {
1767 $mt = 'all';
1768 }
1769 $mediatype = $mt;
1770
1771 // get full url
1772 $href = WP_Optimize_Minify_Functions::get_hurl($wp_styles->registered[$handle]->src);
1773 $version = $wp_styles->registered[$handle]->ver;
1774
1775 // inlined scripts without file
1776 if (empty($href)) {
1777 continue;
1778 }
1779
1780 // mark duplicates as done and remove from the queue
1781 if (!empty($href)) {
1782 $version = is_array($version) ? '' : $version;
1783 $key = hash('adler32', $href . $version);
1784 if (isset($uniq[$key])) {
1785 $done = array_merge($done, array($handle));
1786 continue;
1787 } else {
1788 $uniq[$key] = $handle;
1789 }
1790 }
1791
1792 // IE only files don't increment things
1793 $ieonly = WP_Optimize_Minify_Functions::is_url_in_ie_blacklist($href);
1794 if ($ieonly) {
1795 continue;
1796 }
1797
1798 $version = '?ver=' . (!empty($version) ? $version : $wp_styles->default_version);
1799 $href_versioned = $href . $version;
1800
1801 // Exclude specific CSS files from PageSpeedIndex?
1802 if (is_array($async_css) && WP_Optimize_Minify_Functions::in_arrayi($href, $async_css)) {
1803 WP_Optimize_Minify_Print::exclude_style($href_versioned);
1804 $done = array_merge($done, array($handle));
1805 continue;
1806 }
1807
1808 if (WP_Optimize_Minify_Functions::is_font_awesome($href)) {
1809 if ('inline' === $this->options['fawesome_method']) {
1810 WP_Optimize_Minify_Print::inline_style($handle, $href_versioned);
1811 $done = array_merge($done, array($handle));
1812 continue;
1813 } elseif ('async' === $this->options['fawesome_method']) {
1814 WP_Optimize_Minify_Print::async_style($href_versioned, $mediatype);
1815 $done = array_merge($done, array($handle));
1816 continue;
1817 } elseif ('exclude' === $this->options['fawesome_method']) {
1818 WP_Optimize_Minify_Print::exclude_style($href_versioned);
1819 $done = array_merge($done, array($handle));
1820 continue;
1821 }
1822 }
1823
1824 // Exclude Print mediatype
1825 if ($this->options['remove_print_mediatypes'] && 'print' === $mediatype) {
1826 $done = array_merge($done, array($handle));
1827 continue;
1828 }
1829
1830 // check if the current URL points to a minified file.
1831 $exclude_minified_css = WP_Optimize_Minify_Functions::is_minified_css_js_filename($href);
1832 $normal_processing = $exclude_minified_css && !$merge_css;
1833 // skip ignore list, conditional css, external css, font-awesome merge and already minified css
1834 if (($process_css && !$normal_processing && !WP_Optimize_Minify_Functions::in_arrayi($href, $ignore_list) && !isset($conditional) && WP_Optimize_Minify_Functions::internal_url($href, site_url()))
1835 || empty($href)
1836 ) {
1837
1838 // collect inline css for this handle
1839 if (isset($wp_styles->registered[$handle]->extra['after']) && is_array($wp_styles->registered[$handle]->extra['after'])) {
1840 $inline_css[$handle] = WP_Optimize_Minify_Functions::minify_css_string(implode('', $wp_styles->registered[$handle]->extra['after'])); // save
1841 $wp_styles->registered[$handle]->extra['after'] = null; // dequeue
1842 }
1843
1844 // process
1845 if (isset($footer[count($footer)-1]['handle']) || !count($footer) || $footer[count($footer)-1]['media'] !== $wp_styles->registered[$handle]->args || !$merge_css) {
1846 array_push($footer, array('handles' => array(), 'media' => $mediatype, 'versions' => array()));
1847 }
1848
1849 // push it to the array get latest modified time
1850 array_push($footer[count($footer)-1]['handles'], $handle);
1851 array_push($footer[count($footer)-1]['versions'], $version);
1852 $footer[count($footer) - 1]['last_modified'][] = WP_Optimize_Minify_Functions::get_modification_time($wp_styles->registered[$handle]->src);
1853
1854 // external and ignored css
1855 } else {
1856
1857 // normal enqueueing
1858 array_push($footer, array('handle' => $handle));
1859 }
1860 }
1861
1862 /**
1863 * Filters the array of stylesheets before processing them
1864 *
1865 * @param array $list - The list of items filtered
1866 * @param string $location - The location of the list (footer or header)
1867 * @return array
1868 */
1869 $footer = apply_filters('wpo_minify_stylesheets', $footer, 'footer');
1870
1871 // loop through footer css and merge
1872 for ($i=0,$l=count($footer); $i<$l; $i++) {
1873
1874 if (!isset($footer[$i]['handle'])) {
1875
1876 $handles_str = is_array($footer[$i]['handles']) ? $this->array_to_string_conversion($footer[$i]['handles']) : strval($footer[$i]['handles']);
1877 $last_modified_str = empty($footer[$i]['last_modified']) ? '' : (is_array($footer[$i]['last_modified']) ? $this->array_to_string_conversion($footer[$i]['last_modified']) : strval($footer[$i]['last_modified']));
1878
1879 if ($merge_css) {
1880 // get hash for the inline css in this group
1881 $inline_css_group = array();
1882 if (is_array($footer[$i]['handles'])) {
1883 foreach ($footer[$i]['handles'] as $h) {
1884 if (isset($inline_css[$h]) && !empty($inline_css[$h])) {
1885 $inline_css_group[] = $inline_css[$h];
1886 }
1887 }
1888 }
1889 $inline_css_hash = md5(implode('', $inline_css_group));
1890 $hash = hash('adler32', $handles_str . $inline_css_hash . $last_modified_str);
1891 } else {
1892 $hash = sanitize_file_name($handles_str . $last_modified_str);
1893 }
1894
1895 // static cache file info
1896 $file_name = 'wpo-minify-footer-'.$hash.($minify_css ? '.min' : '');
1897
1898 // create cache files and urls
1899 $file = $cache_dir.'/'.$file_name.'.css';
1900 $meta_json_file = $cache_base_dir.'/'.'meta.json';
1901 $file_url = WP_Optimize_Minify_Functions::get_protocol($cache_dir_url.'/'.$file_name.'.css');
1902
1903 // generate a new cache file
1904 clearstatcache();
1905 if (!file_exists($file)) {
1906
1907 // code and log initialization
1908 $log_header = $this->get_log_header();
1909 $log = array(
1910 'header' => $log_header,
1911 'files' => array()
1912 );
1913 $meta_log = $this->meta_log_initialization($log_header);
1914 $code = '';
1915
1916 // minify and write to file
1917 foreach ($footer[$i]['handles'] as $handle) {
1918 if (!empty($wp_styles->registered[$handle]->src)) {
1919
1920 // get href per handle
1921 $handle_src = $wp_styles->registered[$handle]->src;
1922 $href = WP_Optimize_Minify_Functions::get_hurl($handle_src);
1923 $version = $wp_styles->registered[$handle]->ver;
1924 $last_modified = WP_Optimize_Minify_Functions::get_modification_time($handle_src);
1925 // inlined scripts without href file
1926 if (empty($href)) continue;
1927
1928 // download, minify, cache
1929 $tkey = 'css-'.hash('adler32', $handle . $href).'.css';
1930 $json = false;
1931 $json = WP_Optimize_Minify_Cache_Functions::get_transient($tkey);
1932 if (false === $json) {
1933 $enable_minification = $minify_css && !WP_Optimize_Minify_Functions::is_minified_css_js_filename($href);
1934 $json = WP_Optimize_Minify_Functions::download_and_minify($href, null, $enable_minification, 'css', $handle, $version);
1935 if ($this->options['debug']) {
1936 echo "<!-- wpo_min DEBUG: Uncached file processing now for " . esc_html($handle) . " / " . esc_html($href) . " / " . esc_html($version) . " -->" . "\n";
1937 }
1938 WP_Optimize_Minify_Cache_Functions::set_transient($tkey, $json);
1939 }
1940
1941 // decode
1942 $res = json_decode($json, true);
1943
1944 if (null === $res) continue;
1945
1946 if ($this->should_reset_minify_assets($res, $href, 'css', $handle, $version)) {
1947 WP_Optimize_Minify_Cache_Functions::reset();
1948 $this->minify_cache_incremented = true;
1949 $meta_log['invalidation_reason'] = 'Checksum Mismatch and version changed from ' . $res['request']['version'] . ' to ' . $version;
1950 $meta_log['files_that_changed'][$handle] = $res['log'];
1951 }
1952
1953 // response has failed
1954 if (true !== $res['status']) {
1955 $log['files'][$handle] = $res['log'];
1956 $log['files'][$handle]['version'] = $version;
1957 $log['files'][$handle]['last_modified'] = $last_modified;
1958 continue;
1959 }
1960
1961 $done[] = $handle;
1962
1963 // append code to merged file
1964 $code .= $res['code'] ?? '';
1965 $log['files'][$handle] = $res['log'];
1966 $log['files'][$handle]['version'] = $version;
1967 $log['files'][$handle]['last_modified'] = $last_modified;
1968
1969 // append inlined styles
1970 if (isset($inline_css[$handle]) && !empty($inline_css[$handle])) {
1971 $code.= $inline_css[$handle];
1972 }
1973
1974 // consider dependencies on handles with an empty src
1975 } else {
1976 wp_dequeue_script($handle);
1977 wp_enqueue_script($handle);
1978 }
1979 }
1980
1981 // generate cache, add inline css, write log
1982 if (!empty($code)) {
1983 // Only generate meta.json file if cache is reset
1984 if (!empty($meta_log['invalidation_reason'])) {
1985 file_put_contents($meta_json_file, wp_json_encode($meta_log));
1986 }
1987 WP_Optimize_Minify_Print::write_combined_asset($file, $code, $log);
1988 }
1989 } else {
1990 $log_file = $file.'.json';
1991 if (file_exists($log_file)) {
1992 $saved_log = json_decode(file_get_contents($log_file));
1993 if (is_object($saved_log) && property_exists($saved_log, 'files')) {
1994 $files = (array) $saved_log->files;
1995 foreach ($footer[$i]['handles'] as $handle) {
1996 if (isset($files[$handle]) && $files[$handle]->success) {
1997 $done[] = $handle;
1998 }
1999 }
2000 } else {
2001 // The merged file already exists, so add all files to $done.
2002 $done = array_merge($done, $footer[$i]['handles']);
2003 }
2004 } else {
2005 // The merged file already exists, so add all files to $done.
2006 $done = array_merge($done, $footer[$i]['handles']);
2007 }
2008 }
2009
2010 // Async CSS with loadCSS ?
2011 if ($this->options['loadcss'] && !$this->options['remove_css']) {
2012 $mt = $footer[$i]['media'];
2013 WP_Optimize_Minify_Print::async_style($file_url, $mt);
2014 // enqueue file, if not empty
2015 } else {
2016 if (file_exists($file) && filesize($file) > 0) {
2017
2018 // inline if the file is smaller than 20KB or option has been enabled
2019 if (filesize($file) < 20000 && $this->options['inline_css']) {
2020 $this->inline_css(file_get_contents($file), $handle, $file_url, $footer[$i]['media']);
2021 } else {
2022 // enqueue it
2023 wp_enqueue_style("wpo_min-footer-$i", $file_url, array(), null, $footer[$i]['media']); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Not needed as this is a timestamp based cache
2024 }
2025 } else {
2026 // file could not be generated, output something meaningful
2027 echo "<!-- ERROR: WP-Optimize Minify was not allowed to save its cache on - ".esc_html(str_replace(ABSPATH, '', $file))." -->";
2028 echo "<!-- Please check if the path above is correct and ensure your server has write permission there! -->";
2029 }
2030 }
2031
2032 // other css need to be requeued for the order of files to be kept
2033 } else {
2034 wp_dequeue_style($footer[$i]['handle']);
2035 wp_enqueue_style($footer[$i]['handle']);
2036 }
2037 }
2038
2039 // remove from queue
2040 $wp_styles->done = $done;
2041 }
2042
2043 /**
2044 * Process JS Scripts of type module
2045 *
2046 * @param string $buffer Page HTML.
2047 *
2048 * @return string
2049 */
2050 public function process_script_modules($buffer) {
2051
2052 if ('' === $buffer) return '';
2053
2054 if (!WP_Optimize_Utils::is_valid_html($buffer)) return $buffer;
2055
2056 $minify_js = $this->options['enable_js_minification'];
2057 $cache_path = WP_Optimize_Minify_Cache_Functions::cache_path();
2058 $cache_dir = $cache_path['cachedir'];
2059 $cache_dir_url = $cache_path['cachedirurl'];
2060 $cache_base_dir = $cache_path['cachebasedir'];
2061
2062 $scripts = $this->get_scripts($buffer);
2063 if (empty($scripts)) return $buffer;
2064
2065 foreach ($scripts as $script) {
2066 if ("module" !== $script['type']) continue;
2067
2068 $href = WP_Optimize_Minify_Functions::get_hurl($script['url']);
2069 if (WP_Optimize_Minify_Functions::is_minified_css_js_filename($href)) continue;
2070
2071 $asset_content = WP_Optimize_Minify_Functions::get_asset_content($href);
2072 $code = $asset_content['content'];
2073
2074
2075 if (WP_Optimize_Minify_Functions::has_js_import_statements($code)) {
2076 // If the module script contains import statements, we cannot safely minify or cache it, so skip it.
2077 continue;
2078 }
2079
2080 $last_modified = WP_Optimize_Minify_Functions::get_modification_time($script['url']);
2081 $handle = $script['handle'];
2082 $version = $script['version'];
2083
2084 // Module script handles use slashes and @ symbols,
2085 // replace them with '-' and empty string, break if handle is still ''
2086 if (!empty($handle)) {
2087 $handle_no_slash = str_replace(array('/', '@'), array('-', ''), $handle);
2088 } else {
2089 continue;
2090 }
2091
2092 // code and log initialization
2093 $log_header = $this->get_log_header();
2094 $log = array(
2095 'header' => $log_header,
2096 'files' => array()
2097 );
2098 $meta_log = $this->meta_log_initialization($log_header);
2099 $code = '';
2100
2101 // create cache files and urls
2102 $file_name = 'wpo-minify-'.$handle_no_slash.($minify_js ? '.min' : '');
2103 $file = $cache_dir.'/'.$file_name.'.js';
2104 $meta_json_file = $cache_base_dir.'/'.'meta.json';
2105 $file_url = WP_Optimize_Minify_Functions::get_protocol($cache_dir_url.'/'.$file_name.'.js');
2106
2107 // download, minify, cache
2108 $tkey = 'js-'.hash('adler32', $handle_no_slash . $href).'.js';
2109 $json = WP_Optimize_Minify_Cache_Functions::get_transient($tkey);
2110 if (false === $json) {
2111 $enable_minification = $minify_js && !WP_Optimize_Minify_Functions::is_minified_css_js_filename($href);
2112 $json = WP_Optimize_Minify_Functions::download_and_minify($href, null, $enable_minification, 'js', $handle_no_slash, $version);
2113 if ($this->options['debug']) {
2114 $buffer .= "<!-- wpo_min DEBUG: Uncached file processing now for " . esc_html($handle) . " / " . esc_html($href) . " / " . esc_html($version) . " -->\n";
2115 }
2116 WP_Optimize_Minify_Cache_Functions::set_transient($tkey, $json);
2117 }
2118
2119 // decode
2120 $res = json_decode($json, true);
2121
2122 if (null === $res) continue;
2123
2124 // Reset minified script module
2125 if ($this->should_reset_minify_assets($res, $href, 'js', $handle, $version)) {
2126 WP_Optimize_Minify_Cache_Functions::reset();
2127 $this->minify_cache_incremented = true;
2128 $meta_log['invalidation_reason'] = 'Checksum Mismatch and version changed from ' . $res['request']['version'] . ' to ' . $version;
2129 $meta_log['files_that_changed'][$handle] = $res['log'];
2130 }
2131
2132 if (!$res['status']) continue;
2133
2134 $code .= $res['code'] ?? '';
2135 $code = WP_Optimize_Minify_Functions::prepare_merged_js($code, $href);
2136 $log['files'][$handle] = $res['log'];
2137 $log['files'][$handle]['version'] = $version;
2138 $log['files'][$handle]['last_modified'] = $last_modified;
2139
2140 // If code is empty (nothing to minify) break loop
2141 if (empty($code)) continue;
2142
2143 // Only generate meta.json file if cache is reset
2144 if (!empty($meta_log['invalidation_reason'])) {
2145 file_put_contents($meta_json_file, wp_json_encode($meta_log));
2146 }
2147 WP_Optimize_Minify_Print::write_combined_asset($file, $code, $log);
2148
2149 // Extract importmap script tag
2150 $json_map = $this->get_script_module_importmap($buffer);
2151 $json_map_update = $json_map;
2152
2153 // Update module source file and importmap with new file url (if necessary)
2154 $buffer = str_replace($script['url'], $file_url, $buffer);
2155 if (!empty($json_map) && isset($json_map['imports'][$handle])) {
2156 $json_map_update['imports'][$handle] = $file_url;
2157 $buffer = str_replace(wp_json_encode($json_map), wp_json_encode($json_map_update), $buffer);
2158 }
2159 }
2160
2161 return $buffer;
2162 }
2163
2164 /**
2165 * Orders the CSS per media type
2166 *
2167 * @param array $list - The list of assets
2168 * @return array
2169 */
2170 public function order_stylesheets_per_media_type($list) {
2171 // get unique mediatypes
2172 $allmedia = array();
2173 foreach ($list as $array) {
2174 if (isset($array['media'])) {
2175 $allmedia[$array['media']] = '';
2176 }
2177 }
2178
2179 // extract handles by mediatype
2180 $grouphandles = array();
2181 foreach ($allmedia as $md => $var) {
2182 foreach ($list as $array) {
2183 if (isset($array['media']) && $array['media'] === $md) {
2184 foreach ($array['handles'] as $h) {
2185 $grouphandles[$md][] = $h;
2186 }
2187 }
2188 }
2189 }
2190
2191 // reset and reorder list by mediatypes
2192 $newlist = array();
2193 foreach ($allmedia as $md => $var) {
2194 $newlist[] = array('handles' => $grouphandles[$md], 'media' => $md);
2195 }
2196 if (count($newlist) > 0) {
2197 $list = $newlist;
2198 }
2199
2200 return $list;
2201 }
2202
2203 /**
2204 * Merged google font
2205 *
2206 * @return void;
2207 */
2208 public function add_google_fonts_merged() {
2209 // must have something to do
2210 if (count($this->collect_google_fonts) === 0) return;
2211
2212 // merge google fonts
2213 $href = WP_Optimize_Minify_Fonts::concatenate_google_fonts($this->collect_google_fonts);
2214 if (empty($href)) {
2215 return;
2216 }
2217
2218 // hide google fonts from PageSpeedIndex
2219 if ('exclude' === $this->options['gfonts_method']) {
2220 // make a stylesheet, hide from PageSpeedIndex
2221 WP_Optimize_Minify_Print::exclude_style($href);
2222 } elseif ('async' === $this->options['gfonts_method']) {
2223 // load google fonts async
2224 WP_Optimize_Minify_Print::async_style($href);
2225 } else {
2226 // fallback to normal inline
2227 WP_Optimize_Minify_Print::style($href);
2228 }
2229
2230 // unset per hook
2231 foreach ($this->collect_google_fonts as $k => $v) {
2232 unset($this->collect_google_fonts[$k]);
2233 }
2234 }
2235
2236 /**
2237 * Check if the string given contains 'wpo-minify'
2238 *
2239 * @param string $var
2240 *
2241 * @return boolean
2242 */
2243 public function check_wpo($var) {
2244 return (false === strpos($var, 'assets/wpo-minify'));
2245 }
2246
2247 /**
2248 * Generate preload headers file
2249 *
2250 * @return boolean
2251 */
2252 public function generate_preload_headers() {
2253
2254 // always false for admin pages
2255 if (is_admin()) return false;
2256
2257 // get host with multisite support and query strings
2258 $host = isset($_SERVER['SERVER_NAME']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_NAME'])) : '';
2259 if (empty($host)) {
2260 $host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '';
2261 }
2262
2263 $request_query = '';
2264 $request_uri = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : '';
2265 if (!empty($request_uri)) {
2266 $request_query = wp_parse_url($request_uri, PHP_URL_QUERY);
2267 $request_uri = wp_parse_url($request_uri, PHP_URL_PATH);
2268 }
2269
2270 // initialize headers
2271 $headers = array();
2272
2273 // css headers
2274 if ($this->options['enabled_css_preload']
2275 && count($this->collect_preload_css) > 0
2276 ) {
2277 foreach ($this->collect_preload_css as $u) {
2278 // filter out footer files, because they are not in the critical path
2279 if (strpos($u, '/assets/wpo-minify-footer-') !== false) {
2280 continue;
2281 }
2282
2283 // add headers
2284 $headers[] = "Link: <$u>; rel=preload; as=style";
2285 }
2286 }
2287
2288 // js headers
2289 if ($this->options['enabled_js_preload']
2290 && count($this->collect_preload_js) > 0
2291 ) {
2292 foreach ($this->collect_preload_js as $u) {
2293
2294 // filter out footer files, because they are not in the critical path
2295 if (false !== strpos($u, '/assets/wpo-minify-footer-')) {
2296 continue;
2297 }
2298
2299 // add headers
2300 $headers[] = "Link: <$u>; rel=preload; as=script";
2301 }
2302 }
2303
2304 // must have something
2305 if (count($headers) === 0) {
2306 return false;
2307 } else {
2308 $headers = implode("\n", $headers);
2309 }
2310
2311 // get cache path
2312 $cache_path = WP_Optimize_Minify_Cache_Functions::cache_path();
2313 $header_dir = $cache_path['headerdir'];
2314
2315 // possible cache file locations
2316 $b = $header_dir . '/' . md5($host.'-'.$request_uri) . '.header';
2317 $a = $header_dir . '/' . md5($host.'-'.$request_uri . $request_query).'.header';
2318
2319 // reset file cache
2320 clearstatcache();
2321
2322 // if there are no query strings
2323 if ($b === $a) {
2324 if (!file_exists($a)) {
2325 WP_Optimize_Minify_Print::write_header($a, $headers);
2326 }
2327 return false;
2328 } elseif (!file_exists($b)) {
2329 WP_Optimize_Minify_Print::write_header($b, $headers);
2330 }
2331
2332 return false;
2333 }
2334
2335 /**
2336 * Collect all wpo_min JS files and save them to a headers file
2337 *
2338 * @param string $html
2339 * @param string $handle
2340 * @param string $src
2341 *
2342 * @return string
2343 */
2344 public function collect_js_preload_headers($html, $handle, $src) {
2345 if (false !== strpos($src, 'assets/wpo-minify')) {
2346 if (!in_array($src, $this->collect_preload_js)) {
2347 $this->collect_preload_js[] = $src;
2348 }
2349 }
2350 return $html . "\n";
2351 }
2352
2353 /**
2354 * Collect all wpo_min CSS files and save them to a headers file
2355 * this function intercepts the styles that are enqueued
2356 * take note that if the css is loaded ASYNC, it's not enqueued
2357 *
2358 * @param string $html - Html string
2359 * @param string $handle - Handle
2360 * @param string $src - The CSS file path
2361 *
2362 * @return string
2363 */
2364 public function collect_css_preload_headers($html, $handle, $src) {
2365 if (false !== strpos($src, 'assets/wpo-minify')) {
2366 if (!in_array($src, $this->collect_preload_css)) {
2367 $this->collect_preload_css[] = $src;
2368 }
2369 }
2370 return $html . "\n";
2371 }
2372
2373 /**
2374 * Get current headers file for the url
2375 *
2376 * @return bool|string
2377 */
2378 private function get_preload_headers() {
2379
2380 // always false for admin pages
2381 if (is_admin()) return false;
2382
2383 $enabled_css_preload = $this->options['enabled_css_preload'];
2384 $enabled_js_preload = $this->options['enabled_js_preload'];
2385
2386 if (!$enabled_css_preload && !$enabled_js_preload) {
2387 return false;
2388 }
2389
2390 // get host with multisite support and query strings
2391 $host = isset($_SERVER['SERVER_NAME']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_NAME'])) : '';
2392 if (empty($host)) {
2393 $host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'])) : '';
2394 }
2395 $request_query = '';
2396 $request_uri = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : '';
2397 if (!empty($request_uri)) {
2398 $request_query = wp_parse_url($request_uri, PHP_URL_QUERY);
2399 $request_uri = wp_parse_url($request_uri, PHP_URL_PATH);
2400 }
2401
2402 // get cache path
2403 $cache_path = WP_Optimize_Minify_Cache_Functions::cache_path();
2404 $header_dir = $cache_path['headerdir'];
2405 $cache_file_base = $header_dir . '/';
2406
2407 // possible cache file locations
2408 $b = $cache_file_base . md5("$host-$request_uri").'.header';
2409 $a = $cache_file_base . md5("$host-$request_uri$request_query").'.header';
2410
2411 // reset file cache
2412 clearstatcache();
2413
2414 // return header files or fallback
2415 if ($b === $a && file_exists($a)) {
2416 return file_get_contents($a);
2417 }
2418 if ($b !== $a && file_exists($b)) {
2419 return file_get_contents($b);
2420 }
2421
2422 return false;
2423 }
2424
2425 /**
2426 * Add pre-connect and preload headers
2427 *
2428 * @return void
2429 */
2430 public function extra_preload_headers() {
2431 if (!$this->run_on_page('extra_preload_headers')) return;
2432
2433 // fetch headers
2434 $pre_connect = $this->get_pre_connect_list();
2435
2436 // preload
2437 if (count($pre_connect) > 0) {
2438 foreach ($pre_connect as $url) {
2439 if (!empty($url) && filter_var($url, FILTER_VALIDATE_URL)) {
2440 header("Link: <$url>; rel=preconnect", false);
2441 }
2442 }
2443 }
2444
2445 $headers = $this->get_preload_headers();
2446 if ($headers) {
2447 $nh = array_map('trim', explode("\n", $headers));
2448 foreach ($nh as $h) {
2449 if (!empty($h)) {
2450 header($h, false);
2451 }
2452 }
2453 }
2454 }
2455
2456 /**
2457 * Add caching headers
2458 *
2459 * @param array $headers An array of headers
2460 *
2461 * @return array An array of modified headers
2462 */
2463 public function add_caching_headers($headers) {
2464 $cache = WP_Optimize()->get_page_cache();
2465 if ($cache->is_enabled()) return $headers;
2466
2467 if (!isset($headers['Cache-Control'])) {
2468 $headers['Cache-Control'] = 'must-revalidate';
2469 }
2470
2471 if (!isset($headers['Last-Modified'])) {
2472 $headers['Last-Modified'] = gmdate('D, d M Y H:i:s', time()) . ' GMT';
2473 }
2474
2475 return $headers;
2476 }
2477
2478 /**
2479 * Includes dependency files
2480 */
2481 private function include_dependencies() {
2482 if (!class_exists('WP_Optimize_Minify_Functions')) {
2483 include WP_OPTIMIZE_MINIFY_DIR.'/class-wp-optimize-minify-functions.php';
2484 }
2485
2486 if (!class_exists('WP_Optimize_Minify_Print')) {
2487 include WP_OPTIMIZE_MINIFY_DIR.'/class-wp-optimize-minify-print.php';
2488 }
2489
2490 if (!class_exists('WP_Optimize_Minify_Fonts')) {
2491 include WP_OPTIMIZE_MINIFY_DIR.'/class-wp-optimize-minify-fonts.php';
2492 }
2493 }
2494
2495 /**
2496 * Handles emoji removal
2497 */
2498 private function disable_emojis() {
2499 WP_Optimize_Minify_Functions::disable_wp_emojicons();
2500 add_filter('tiny_mce_plugins', array('WP_Optimize_Minify_Functions', 'disable_emojis_tinymce' ));
2501 }
2502
2503 /**
2504 * Handles header meta information removal
2505 */
2506 private function remove_header_meta_info() {
2507 // no resource hints, generator tag, shortlink, manifest link, etc
2508 remove_action('wp_head', 'wp_resource_hints', 2);
2509 remove_action('wp_head', 'wp_generator');
2510 remove_action('template_redirect', 'wp_shortlink_header', 11);
2511 remove_action('wp_head', 'wlwmanifest_link');
2512 remove_action('wp_head', 'rsd_link');
2513 remove_action('wp_head', 'adjacent_posts_rel_link_wp_head');
2514 remove_action('wp_head', 'feed_links', 2);
2515 remove_action('wp_head', 'feed_links_extra', 3);
2516 WP_Optimize_Minify_Functions::remove_redundant_shortlink();
2517 }
2518
2519 /**
2520 * Handles javascript processing
2521 */
2522 private function process_js() {
2523 add_action('wp_print_scripts', array($this, 'process_header_scripts'), PHP_INT_MAX);
2524 add_action('wp_print_footer_scripts', array($this, 'process_footer_scripts'), 9);
2525
2526 // Defer JS
2527 add_filter('script_loader_tag', array($this, 'defer_js'), 10, 3);
2528
2529 // Preloading
2530 if ($this->options['enabled_js_preload']) {
2531 add_filter('script_loader_tag', array($this, 'collect_js_preload_headers'), PHP_INT_MAX, 3);
2532 }
2533
2534 // add the LoadAsync JavaScript function
2535 $async_js = $this->get_async_js_list();
2536 if (count($async_js) > 0
2537 || ( 'all' === $this->options['enable_defer_js'] && 'async_using_js' === $this->options['defer_js_type'] )
2538 ) {
2539 add_action('wp_head', array('WP_Optimize_Minify_Print', 'add_load_async'), 0);
2540 }
2541
2542 // Process script module from generated html
2543 ob_start(array($this, "process_script_modules"));
2544 }
2545
2546 /**
2547 * Extract scripts tags from html buffer using regex
2548 *
2549 * @param string $html - HTML document as string
2550 * @return array
2551 */
2552 private function get_scripts($html) {
2553
2554 preg_match_all('/<script([^>]*)>/Umi', $html, $script_matches, PREG_SET_ORDER);
2555
2556 $modified_script_matches = array();
2557 foreach ($script_matches as $script) {
2558 $script['url'] = '';
2559 $script['handle'] = '';
2560 $script['version'] = '';
2561 $script['type'] = '';
2562 $attributes = WP_Optimize_Utils::parse_attributes($script[1]);
2563
2564 if (isset($attributes['src'])) {
2565 $script['url'] = $attributes['src'];
2566 $url_query = wp_parse_url($script['url'], PHP_URL_QUERY);
2567 $output = array();
2568 wp_parse_str($url_query, $output);
2569 if (isset($output['ver'])) $script['version'] = $output['ver'];
2570 }
2571
2572 // Extract the id/handle from attribute's array if present
2573 if (isset($attributes['id'])) $script['handle'] = $attributes['id'];
2574
2575 // Extract the type from attribute's array if present
2576 if (isset($attributes['type'])) $script['type'] = $attributes['type'];
2577
2578 $modified_script_matches[] = $script;
2579 }
2580
2581 return $modified_script_matches;
2582 }
2583
2584 /**
2585 * Extract `importmap` json object from html buffer using regex
2586 *
2587 * @param string $html - HTML document as string
2588 * @return array
2589 */
2590 private function get_script_module_importmap($html) {
2591 preg_match('/<script\s+type\s*=\s*[\'"]importmap.*>(?<importmap>.*)<\/script>/Umsi', $html, $match);
2592 if (!empty($match) && isset($match['importmap'])) return json_decode($match['importmap'], true);
2593
2594 return array();
2595 }
2596
2597 /**
2598 * Handles css processing
2599 */
2600 private function process_css() {
2601 add_action('wp_head', array($this, 'add_critical_path'), 2);
2602
2603 // merge, if inline is selected but prevent optimization for these locations
2604 if ($this->options['inline_css']) {
2605 // this prints the styles (not fonts) and checks the 'collect google fonts'
2606 add_filter('style_loader_tag', array($this, 'inline_css'), PHP_INT_MAX, 4);
2607 // this prints the Google fonts
2608 add_action('wp_print_styles', array($this, 'add_google_fonts_merged'), PHP_INT_MAX);
2609 add_action('wp_print_footer_scripts', array($this, 'add_google_fonts_merged'), PHP_INT_MAX);
2610 }
2611 // Preloading
2612 if ($this->options['enabled_css_preload']) {
2613 add_filter('style_loader_tag', array($this, 'collect_css_preload_headers'), PHP_INT_MAX, 3);
2614 }
2615 // Optimize the css and collect the Google fonts for merging
2616 add_action('wp_print_styles', array($this, 'process_header_css'), PHP_INT_MAX);
2617 add_action('wp_print_footer_scripts', array($this, 'process_footer_css'), 9);
2618
2619 /**
2620 * Filters whether to ignore the order of the CSS files, and group them by media type
2621 *
2622 * @param boolean $maintain_css_order
2623 * @return boolean
2624 * @default true
2625 */
2626 if (!apply_filters('wpo_minify_maintain_css_order', true)) {
2627 // Reorder stylesheets
2628 add_filter('wpo_minify_stylesheets', array($this, 'order_stylesheets_per_media_type'));
2629 }
2630 }
2631
2632 /**
2633 * Determines whether we should minify html or not
2634 *
2635 * @return bool
2636 */
2637 private function should_process_html() {
2638 return $this->options['html_minification'] && !is_admin() && $this->is_cache_preload();
2639 }
2640
2641 /**
2642 * Determines whether to use loadCSS polyfill or not
2643 *
2644 * @return bool
2645 */
2646 private function should_use_loadCSS() {
2647 return $this->options['loadcss']
2648 || 'async' === $this->options['fawesome_method']
2649 || 'async' === $this->options['gfonts_method'];
2650 }
2651
2652 /**
2653 * Removes query string from static assets
2654 */
2655 private function remove_query_string_from_static_assets() {
2656 add_filter('style_loader_src', array('WP_Optimize_Minify_Functions', 'remove_cssjs_ver'), 10, 2);
2657 add_filter('script_loader_src', array('WP_Optimize_Minify_Functions', 'remove_cssjs_ver'), 10, 2);
2658 }
2659
2660 /**
2661 * Converts the lines in a textarea option value into an array
2662 *
2663 * @param string $option
2664 * @return array
2665 */
2666 private function convert_option_to_array($option) {
2667 return trim($option) ? array_map('trim', explode("\n", trim($option))) : array();
2668 }
2669
2670 /**
2671 * Retrieves excluded css URLs as an array
2672 *
2673 * @return array
2674 */
2675 private function get_excluded_css_list() {
2676 $excluded_css = $this->options['exclude_css'];
2677 return $this->convert_option_to_array($excluded_css);
2678 }
2679
2680 /**
2681 * Retrieves async css URLs as an array
2682 *
2683 * @return array
2684 */
2685 private function get_async_css_list() {
2686 $async_css = $this->options['async_css'];
2687 return $this->convert_option_to_array($async_css);
2688 }
2689
2690 /**
2691 * Retrieves excluded javascript URLs as an array
2692 *
2693 * @return array
2694 */
2695 private function get_excluded_js_list() {
2696 $excluded_js = $this->options['exclude_js'];
2697 return $this->convert_option_to_array($excluded_js);
2698 }
2699
2700 /**
2701 * Retrieves async javascript URLs as an array
2702 *
2703 * @return array
2704 */
2705 private function get_async_js_list() {
2706 $async_js = $this->options['async_js'];
2707 return $this->convert_option_to_array($async_js);
2708 }
2709
2710 /**
2711 * Retrieves pre connect headers as an array
2712 *
2713 * @return array
2714 */
2715 private function get_pre_connect_list() {
2716 $pre_connect = $this->options['hpreconnect'];
2717 return $this->convert_option_to_array($pre_connect);
2718 }
2719
2720 /**
2721 * Array to string conversion
2722 *
2723 * @param array $arr
2724 * @return string
2725 */
2726 private function array_to_string_conversion($arr) {
2727 $str = '';
2728 // implode won't work, because element can be an array
2729 foreach ($arr as $value) {
2730 $str .= is_array($value) ? '' : $value;
2731 }
2732 return $str;
2733 }
2734
2735 /**
2736 * Decides whether minify cache should be incremented or not
2737 *
2738 * @param array $res
2739 * @param string $href
2740 * @param string $type
2741 * @param string $handle
2742 * @param mixed $version
2743 *
2744 * @return bool true when cache is invalid, false otherwise
2745 */
2746 private function should_reset_minify_assets($res, $href, $type, $handle, $version) {
2747 if (isset($res['request']['version']) && $res['request']['version'] != $version && !$this->minify_cache_incremented) {
2748 $enable_minification = $this->options['enable_'.$type.'_minification'] && !WP_Optimize_Minify_Functions::is_minified_css_js_filename($href);
2749 $new_json = WP_Optimize_Minify_Functions::download_and_minify($href, null, $enable_minification, $type, $handle, $version);
2750 $new_res = json_decode($new_json, true);
2751
2752 /**
2753 * In case `download_and_minify` fails because of
2754 * Network issues, Syntax errors in updated code,
2755 * Wrong use of `wpo_minify_get_js` and `wpo_minify_get_css` filters
2756 * We don't want to invalidate current assets, so bail out
2757 */
2758 if (null === $new_res) return false;
2759
2760 $hash = hash('sha256', $res['code']);
2761 $new_hash = hash('sha256', $new_res['code']);
2762 return strcmp($hash, $new_hash);
2763 }
2764 return false;
2765 }
2766
2767 /**
2768 * Returns the log header with the current date and url
2769 *
2770 * @return string
2771 */
2772 private function get_log_header() {
2773 $request_uri = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : '';
2774 $log_header = "PROCESSED on ".gmdate('r')." from ".home_url($request_uri);
2775 return $log_header;
2776 }
2777 }
2778