PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.7.0
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.7.0
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-functions.php

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

1,287 lines 39.0 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 // handle better utf-8 and Unicode encoding
6 if (function_exists('mb_internal_encoding')) {
7 mb_internal_encoding('UTF-8');
8 }
9
10 // must have
11 // phpcs:disable
12 // Squiz.PHP.DiscouragedFunctions.Discouraged -- Not applicable here
13 ini_set('pcre.backtrack_limit', 5000000);
14 ini_set('pcre.recursion_limit', 5000000);
15 // phpcs:enable
16
17 use MatthiasMullie\Minify;
18
19 class WP_Optimize_Minify_Functions {
20
21 /**
22 * Applies `strip_tags` function for given array of messages
23 *
24 * @param array $messages Array of messages
25 * @param string $allowed_tags Tags to retain in message (optional)
26 *
27 * @return array
28 */
29 public static function apply_strip_tags_for_messages_array($messages, $allowed_tags = '<strong>') {
30 return array_map(function($message) use ($allowed_tags) {
31 return strip_tags($message, $allowed_tags);
32 }, $messages);
33 }
34
35 /**
36 * Detect external or internal scripts
37 *
38 * @param string $src
39 * @return boolean
40 */
41 public static function is_local_domain($src) {
42 $wpo_minify_options = wp_optimize_minify_config()->get();
43
44 $locations = array(home_url(), site_url(), network_home_url(), network_site_url());
45 $async_using_js = 'all' === $wpo_minify_options['enable_defer_js'] && 'async_using_js' === $wpo_minify_options['defer_js_type'];
46
47 // excluded from cdn because of https://www.chromestatus.com/feature/5718547946799104 (we use document.write to preserve render blocking)
48 if (!empty($wpo_minify_options['cdn_url'])
49 && (!$async_using_js || $wpo_minify_options['cdn_force'])
50 ) {
51 array_push($locations, $wpo_minify_options['cdn_url']);
52 }
53
54 // cleanup locations
55 $locations = array_filter(array_unique($locations));
56
57 // external or not?
58 $ret = false;
59 foreach ($locations as $l) {
60 $l = preg_replace('/^https?:\/\//i', '', trim($l));
61 $l = trim(trim(preg_replace('/^www./', '', $l), '/'));
62 if (false !== stripos($src, $l) && false === $ret) {
63 $ret = true;
64 }
65 }
66
67 // response
68 return $ret;
69 }
70
71 /**
72 * Functions, get hurl info
73 *
74 * @param mixed $src
75 *
76 * @return string
77 */
78 public static function get_hurl($src) {
79 $wp_home = site_url();
80 $wp_domain = trim(str_ireplace(array('http://', 'https://'), '', trim($wp_home, '/')));
81 // preserve empty source handles
82 $hurl = null === $src ? '' : trim($src);
83 if (empty($hurl)) {
84 return $hurl;
85 }
86
87 // some fixes
88 $hurl = str_ireplace(array('&#038;', '&amp;'), '&', $hurl);
89
90 if (is_ssl()) {
91 $protocol = 'https://';
92 } else {
93 $protocol = 'http://';
94 }
95
96 // make sure wp_home doesn't have a forward slash
97 $wp_home = rtrim($wp_home, '/');
98
99 // apply some filters
100 if ("//" === substr($hurl, 0, 2)) {
101 $hurl = $protocol.ltrim($hurl, "/");
102 }//end if
103 if ("http" === substr($hurl, 0, 4) && false === stripos($hurl, $wp_domain)) {
104 return $hurl;
105 }//end if
106 if ("http" !== substr($hurl, 0, 4) && false !== stripos($hurl, $wp_domain)) {
107 $hurl = $wp_home.'/'.ltrim($hurl, "/");
108 }//end if
109
110 // prevent double forward slashes in the middle
111 $hurl = str_ireplace('###', '://', str_ireplace('//', '/', str_ireplace('://', '###', $hurl)));
112
113 // consider different wp-content directory
114 $proceed = 0;
115 if (!empty($wp_home)) {
116 $alt_wp_content = basename($wp_home);
117 if (substr($hurl, 0, strlen($alt_wp_content)) === $alt_wp_content) {
118 $proceed = 1;
119 }
120 }
121
122 // Get the name of the WP-CONTENT folder. Default is wp-content, but can be changed by the user.
123 $wp_content_folder = str_replace(ABSPATH, '', WP_CONTENT_DIR);
124
125 // protocol + home for relative paths
126 if ("/".WPINC === substr($hurl, 0, 12)
127 || "/wp-admin" === substr($hurl, 0, 9)
128 || "/$wp_content_folder" === substr($hurl, 0, 11)
129 || 1 === $proceed
130 ) {
131 $hurl = $wp_home.'/'.ltrim($hurl, "/");
132 }
133
134 // make sure there is a protocol prefix as required
135 $hurl = $protocol.preg_replace('/^https?:\/\//i', '', $hurl); // enforce protocol
136
137 // no query strings
138 if (false !== stripos($hurl, '.js?v')) {
139 $hurl = stristr($hurl, '.js?v', true).'.js';
140 }//end if
141 if (false !== stripos($hurl, '.css?v')) {
142 $hurl = stristr($hurl, '.css?v', true).'.css';
143 }//end if
144
145 return $hurl;
146 }
147
148 /**
149 * Check if it's an internal url or not
150 *
151 * @param string $hurl
152 * @param string $wp_home
153 * @param mixed $noxtra
154 * @return boolean
155 */
156 public static function internal_url($hurl, $wp_home, $noxtra = null) {
157 $wpo_minify_options = wp_optimize_minify_config()->get();
158
159 if (substr($hurl, 0, strlen($wp_home)) === $wp_home) {
160 return true;
161 }
162 if (false !== stripos($hurl, $wp_home)) {
163 return true;
164 }
165 if (isset($_SERVER['HTTP_HOST']) && false !== stripos($hurl, preg_replace('/:\d+$/', '', sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST']))))) {
166 return true;
167 }
168 if (isset($_SERVER['SERVER_NAME']) && false !== stripos($hurl, preg_replace('/:\d+$/', '', sanitize_text_field(wp_unslash($_SERVER['SERVER_NAME']))))) {
169 return true;
170 }
171 if (isset($_SERVER['SERVER_ADDR']) && '::1' !== sanitize_text_field(wp_unslash($_SERVER['SERVER_ADDR'])) && false !== stripos($hurl, preg_replace('/:\d+$/', '', sanitize_text_field(wp_unslash($_SERVER['SERVER_ADDR']))))) {
172 return true;
173 }
174
175 // allow specific external urls to be merged
176 if (null === $noxtra) {
177 $merge_allowed_urls = array_map('trim', explode("\n", $wpo_minify_options['merge_allowed_urls']));
178 if (is_array($merge_allowed_urls) && strlen(implode($merge_allowed_urls)) > 0) {
179 foreach ($merge_allowed_urls as $e) {
180 if (false !== stripos($hurl, $e) && !empty($e)) {
181 return true;
182 }
183 }
184 }
185 }
186
187 return false;
188 }
189
190 /**
191 * Case-insensitive in_array() wrapper
192 *
193 * @param string $hurl
194 * @param array $ignore
195 * @return boolean
196 */
197 public static function in_arrayi($hurl, $ignore) {
198 $hurl = preg_replace('/^https?:\/\//i', '//', $hurl); // better compatibility
199 $hurl = strtok(urldecode(rawurldecode($hurl)), '?'); // no query string, decode entities
200
201 if (!empty($hurl) && is_array($ignore)) {
202 foreach ($ignore as $i) {
203 $i = preg_replace('/^https?:\/\//i', '//', $i); // better compatibility
204 $i = strtok(urldecode(rawurldecode($i)), '?'); // no query string, decode entities
205 $i = trim(trim(trim(rtrim($i, '/')), '*')); // wildcard char removal
206 if (!empty($i) && false !== stripos($hurl, $i)) {
207 return true;
208 }
209 }
210 }
211 return false;
212 }
213
214 /**
215 * Check if selected url is point to already minified css/js file
216 *
217 * @param string $url
218 * @return bool
219 */
220 public static function is_minified_css_js_filename($url) {
221 $parts = wp_parse_url($url);
222 if (empty($parts['path']) || !is_string(basename($parts['path']))) return false;
223 return 1 === preg_match('/\.min\.(js|css)$/i', basename($parts['path']));
224 }
225
226 /**
227 * Better compatibility urls + fix w3.org NamespaceAndDTDIdentifiers
228 *
229 * @param string $code
230 * @return string
231 * */
232 private static function compat_urls($code) {
233 $wpo_minify_options = wp_optimize_minify_config()->get();
234 $default_protocol = $wpo_minify_options['default_protocol'];
235 if ('dynamic' === $default_protocol) {
236 if ((isset($_SERVER['HTTPS']) && ('on' === $_SERVER['HTTPS'] || 1 === (int) $_SERVER['HTTPS']))
237 || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'])
238 ) {
239 $default_protocol = 'https://';
240 } else {
241 $default_protocol = 'http://';
242 }
243 } else {
244 $default_protocol = $default_protocol.'://';
245 }
246 $code = preg_replace('/^https?:\/\//i', $default_protocol, $code);
247 $code = str_ireplace($default_protocol.'www.w3.org', 'http://www.w3.org', $code);
248 return $code;
249 }
250
251 /**
252 * Minify css string with PHP Minify
253 *
254 * @param string $css
255 * @return string
256 */
257 public static function minify_css_string($css) {
258 $css = apply_filters('wpo_minify_css_string', $css);
259 $minifier = new Minify\CSS($css);
260 $minifier->setMaxImportSize(15); // [css only] embed assets up to 15 Kb (default 5Kb) - processes gif, png, jpg, jpeg, svg & woff
261 $min = $minifier->minify();
262 if (false !== $min) {
263 return self::compat_urls($min);
264 }
265 return self::compat_urls($css);
266 }
267
268 /**
269 * Minify js on demand (one file at one time, for compatibility)
270 *
271 * @param string $url
272 * @param string $js
273 * @param boolean $enable_js_minification
274 * @return string
275 */
276 public static function get_js($url, $js, $enable_js_minification) {
277 $wpo_minify_options = wp_optimize_minify_config()->get();
278
279 // exclude minification on already minified files + jquery (because minification might break those)
280 $excl = array('jquery.js', '.min.js', '-min.js', '/uploads/fusion-scripts/', '/min/', '.packed.js', '/includes/builder/scripts/');
281 foreach ($excl as $e) {
282 if (false !== stripos(basename($url), $e)) {
283 $enable_js_minification = false;
284 break;
285 }
286 }
287
288 $encoding = mb_detect_encoding($js);
289
290 // remove BOM
291 $js = self::remove_utf8_bom($js);
292
293 self::maybe_log_error_message($url, $encoding, $js);
294
295 // minify JS
296 if ($enable_js_minification) {
297 $js = self::minify_js_string($js);
298 } else {
299 $js = self::compat_urls($js);
300 }
301
302 // Remove source mapping files
303 $js = preg_replace('/(\/\/\s*[#]\s*sourceMappingURL\s*[=]\s*)(.+)\s*/ui', '', $js);
304
305 // needed when merging js files
306 $js = trim($js);
307 if (';' !== substr($js, -1)) {
308 $js = $js.';';
309 }
310 if ($wpo_minify_options['debug']) {
311 $js = '/* info: ' . $url . ' */' . "\n" . $js;
312 }
313
314 /**
315 * Filters the imported JavaScript
316 *
317 * @param string $js - The imported JS
318 * @param string $url - The imported url
319 * @param boolean $enable_js_minification - Whether to minify or not
320 */
321 $filtered_js = apply_filters('wpo_minify_get_js', $js . "\n", $url, $enable_js_minification);
322 return is_string($filtered_js) ? $filtered_js : $js;
323 }
324
325 /**
326 * Minify JS string with PHP Minify or YUI Compressors
327 *
328 * @param string $js
329 * @return string
330 */
331 public static function minify_js_string($js) {
332 $js = apply_filters('wpo_minify_js_string', $js);
333 // PHP Minify from https://github.com/matthiasmullie/minify
334 $minifier = new Minify\JS($js);
335 $min = $minifier->minify();
336 if (false !== $min && (strlen(trim($js)) === strlen(trim($min)) || strlen(trim($min)) > 0)) {
337 return self::compat_urls($min);
338 }
339
340 // if we are here, something went wrong and minification didn't work
341 $js = "\n/*! wpo_min: Minification of the following section failed, so it has been merged instead. */\n".$js;
342 return self::compat_urls($js);
343 }
344
345 /**
346 * Wrapper to minify the inlined JS string - Adds an extra check for JSON
347 *
348 * @param string $js
349 * @return string
350 */
351 public static function minify_inline_js($js) {
352 $js = apply_filters('wpo_minify_inline_js_string', $js);
353 // Do not minify JSON (JS minification will minify `true` to `!0`, which will break the JSON)
354 if (json_decode($js)) return $js;
355 return self::minify_js_string($js, true);
356 }
357
358 /**
359 * Check if the JS code contains import statements
360 *
361 * @param string $js
362 * @return boolean
363 */
364 public static function has_js_import_statements($js) {
365 $js = preg_replace('/\/\/.*|\/\*[\s\S]*?\*\//', '', $js); // Remove comments to avoid false positives
366 return 1 === preg_match('/\bimport\b/', $js);
367 }
368
369 /**
370 * Functions, minify html
371 *
372 * @param string $html
373 * @return string
374 */
375 public static function minify_html($html) {
376 $minify_css = wp_optimize_minify_config()->get('enable_css_minification');
377 $minify_js = wp_optimize_minify_config()->get('enable_js_minification');
378 $options = array();
379 if ($minify_css && apply_filters('wpo_minify_inline_css', true)) {
380 $options['cssMinifier'] = array('WP_Optimize_Minify_Functions', 'minify_css_string');
381 }
382 if ($minify_js && apply_filters('wpo_minify_inline_js', true)) {
383 $options['jsMinifier'] = array('WP_Optimize_Minify_Functions', 'minify_inline_js');
384 }
385 return Minify_HTML::minify($html, $options);
386 }
387
388 /**
389 * Functions to minify HTML
390 *
391 * @param string $html
392 * @return string
393 */
394 public static function html_compression_finish($html) {
395 return self::minify_html($html);
396 }
397
398 /**
399 * Start the compression
400 *
401 * @return void
402 */
403 public static function html_compression_start() {
404 if (self::exclude_contents()) {
405 return;
406 }
407 ob_start(array(__CLASS__, 'html_compression_finish'));
408 }
409
410 /**
411 * Remove default HTTP headers
412 *
413 * @return void
414 */
415 public static function remove_redundant_shortlink() {
416 remove_action('wp_head', 'wp_shortlink_wp_head', 10);
417 remove_action('template_redirect', 'wp_shortlink_header', 11);
418 }
419
420 /**
421 * Minify css on demand (one file at one time, for compatibility)
422 *
423 * @param string $url
424 * @param string $css
425 * @param boolean $enable_css_minification
426 * @return string
427 */
428 public static function get_css($url, $css, $enable_css_minification) {
429 $wpo_minify_options = wp_optimize_minify_config()->get();
430
431 $encoding = mb_detect_encoding($css);
432
433 // remove BOM
434 $css = self::remove_utf8_bom($css);
435
436 self::maybe_log_error_message($url, $encoding, $css);
437
438 // fix url paths
439 if (!empty($url)) {
440 $css = self::make_css_urls_absolute($css, $url);
441 }
442
443 $css = str_ireplace('@charset "UTF-8";', '', $css);
444
445 // remove query strings from fonts (for better seo, but add a small cache buster based on most recent updates)
446 // last update or zero
447 $cache_time = $wpo_minify_options['last-cache-update'];
448 // fonts cache buster
449 $css = preg_replace('/(.eot|.woff2|.woff|.ttf)+[?+](.+?)(\)|\'|\")/ui', "$1"."#".$cache_time."$3", $css);
450 // Remove Sourcemappingurls
451 $css = preg_replace('/(\/\*\s*[#]\s*sourceMappingURL\s*[=]\s*)(.[^*]+)\s*\*\//ui', '', $css);
452 // If @import is found, process it/them
453 if (false !== strpos($css, '@import')) $css = self::replace_css_import($css, $url);
454
455 // minify CSS
456 if ($enable_css_minification) {
457 $css = self::minify_css_string($css);
458 } else {
459 $css = self::compat_urls($css);
460 }
461
462 // cdn urls
463 $cdn_url = $wpo_minify_options['cdn_url'];
464 if (!empty($cdn_url)) {
465 $wp_domain = trim(preg_replace('/^https?:\/\//i', '', trim(site_url(), '/')));
466 $cdn_url = trim(trim(preg_replace('/^https?:\/\//i', '', trim($cdn_url, '/'))), '/');
467 $css = str_ireplace($wp_domain, $cdn_url, $css);
468 }
469
470 // add css comment
471 $css = trim($css);
472 if ($wpo_minify_options['debug']) {
473 $css = '/* info: ' . $url . ' */' . "\n" . trim($css);
474 }
475
476 /**
477 * Filters the imported CSS
478 *
479 * @param string $css - The imported CSS
480 * @param string $url - The imported url
481 * @param boolean $enable_css_minification - Whether to minify or not
482 */
483 $filtered_css = apply_filters('wpo_minify_get_css', $css, $url, $enable_css_minification);
484 return is_string($filtered_css) ? $filtered_css : $css;
485 }
486
487 /**
488 * Adds full path to relative url() rules
489 *
490 * @param string $css - The CSS to process
491 * @param string $url - The URL or the CSS being processed
492 * @return string
493 */
494 public static function make_css_urls_absolute($css, $url) {
495 $matches = array();
496 preg_match_all("/url\(\s*['\"]?(?!data:)(?!http)(?![\/'\"])(.+?)['\"]?\s*\)/ui", $css, $matches);
497 foreach ($matches[1] as $a) {
498 $b = trim($a);
499 if ($b !== $a) {
500 $css = str_replace($a, $b, $css);
501 }
502 }
503 return (string) preg_replace("/url\(\s*['\"]?(?!data:)(?!http)(?![\/'\"])(.+?)['\"]?\s*\)/ui", "url(".dirname($url)."/$1)", $css);
504 }
505
506 /**
507 * Include @import[ed] files - The @import statement can only be used at the top of a file, which breaks when merging everything.
508 *
509 * @param string $css - The original CSS containing the @import statement
510 * @param string $file_url - The original CSS' URL
511 * @return string
512 */
513 public static function replace_css_import($css, $file_url) {
514 $remove_print_mediatypes = wp_optimize_minify_config()->get('remove_print_mediatypes');
515 $debug = wp_optimize_minify_config()->get('debug');
516 return preg_replace_callback('/(?:@import)\s(?:url\()?\s?["\'](.*?)["\']\s?\)?(?:[^;]*);?/im', function($matches) use ($file_url, $remove_print_mediatypes, $debug) {
517 // @import contains url()
518 if (preg_match('/url\s*\((.[^\)]*)[\)*?](.*);/', $matches[0], $url_matches)) {
519 $url = trim(str_replace(array('"', "'"), '', $url_matches[1]));
520 $media_query = trim($url_matches[2]);
521 // @import uses quotes only
522 } elseif (preg_match('/["\'](.*)["\'](.*);?/', $matches[0], $no_url_matches)) {
523 $url = trim($no_url_matches[1]);
524 $media_query = trim($no_url_matches[2], ") ;");
525 }
526
527 // If $media_query contains print, and $remove_print_mediatypes is true, return empty string
528 if ($remove_print_mediatypes && false !== strpos($media_query, 'print') && apply_filters('wpo_minfy_remove_print_mediatypes_import', true, $url, $media_query, $matches[0], $file_url)) return ($debug ? '/*! Info: the import of "'.$url.'" was removed because the setting remove_print_mediatypes is enabled. */' : '');
529
530 $purl = wp_parse_url($url);
531 // If there's no host, the url is relative to $file_url, so prepend with the base url.
532 if (!isset($purl['host'])) {
533 $url = dirname($file_url).'/'.$url;
534 }
535
536 // Download @import
537 $asset_content = WP_Optimize_Minify_Functions::get_asset_content($url);
538 $content = $asset_content['content'];
539
540 if (!$content) return '';
541
542 // Fix the URLs
543 $content = WP_Optimize_Minify_Functions::make_css_urls_absolute($content, $url);
544
545 if ($media_query) {
546 // Wrap the code with the media query
547 $content = "@media $media_query {\n$content\n}";
548 }
549
550 if ($debug) {
551 $content = "/*! CSS import Information: code imported from $url */\n$content\n/*! END CSS import Information */";
552 }
553
554 // If the code contains its own @import, recursively include it.
555 if (false !== strpos($content, '@import')) {
556 return WP_Optimize_Minify_Functions::replace_css_import($content, $url);
557 }
558
559 return $content;
560 }, $css);
561 }
562
563 /**
564 * Download and cache css and js files
565 *
566 * @param ?string $hurl
567 * @param ?string $inline
568 * @param boolean $enable_minification
569 * @param string $type
570 * @param ?string $handle
571 * @param mixed $version
572 *
573 * @return boolean|string
574 */
575 public static function download_and_minify($hurl, $inline, $enable_minification, $type, $handle, $version = '') {
576 // must have
577 if (empty($hurl)) {
578 return false;
579 }
580 if (!in_array($type, array('js', 'css'))) {
581 return false;
582 }
583
584 $wpo_minify_options = wp_optimize_minify_config()->get();
585
586 // filters and defaults
587 $print_url = str_ireplace(array(site_url(), home_url(), 'http:', 'https:'), '', $hurl);
588
589 $log = array(
590 'url' => $print_url,
591 );
592
593 // defaults
594 if (false !== $enable_minification) {
595 $enable_minification = true;
596 }
597 if (empty($inline)) {
598 $inline = '';
599 }
600 $print_handle = '';
601 if (empty($handle)) {
602 $handle = '';
603 } else {
604 $print_handle = "[$handle]";
605 }
606
607 // debug request
608 $dreq = array(
609 'hurl' => $hurl,
610 'inline' => $inline,
611 'enable_minification' => $enable_minification,
612 'type' => $type,
613 'handle' => $handle,
614 'version' => $version
615 );
616
617 $asset_content = self::get_asset_content($hurl);
618 $code = $asset_content['content'];
619
620 // If $code is empty:
621 if (!$code) {
622 $log['success'] = false;
623 if ($wpo_minify_options['debug']) {
624 $log['debug'] = "$print_handle failed. Tried wp_remote_get and local file_get_contents.";
625 }
626 $return = array('request' => $dreq, 'log' => $log, 'code' => '', 'status' => false);
627 return wp_json_encode($return);
628 }
629
630 if ('js' === $type) {
631 $code = self::get_js($hurl, $code, $enable_minification);
632 } else {
633 $code = self::get_css($hurl, $code.$inline, $enable_minification);
634 }
635
636 // log, save and return
637 if ($wpo_minify_options['debug']) {
638 $version_msg = ('' !== $version) ? "[Version: $version]" : "";
639 $log['debug'] = $print_handle . $version_msg . ' was '.('local' === $asset_content['method'] ? 'opened' : 'fetched').' from '.$hurl;
640 }
641 $log['success'] = true;
642 $return = array('request' => $dreq, 'log' => $log, 'code' => $code, 'status' => true);
643 return wp_json_encode($return);
644 }
645
646 /**
647 * Get the content of an asset, whether local or remote
648 *
649 * @param string $url
650 * @return array
651 */
652 public static function get_asset_content($url) {
653
654 $wp_home = site_url();
655 $wp_domain = wp_parse_url($wp_home, PHP_URL_HOST);
656 // If the file is local.
657 if (false !== stripos($url, $wp_domain)) {
658 // default
659 $f = str_ireplace(rtrim($wp_home, '/'), rtrim(ABSPATH, '/'), $url);
660 // fail over when home_url != site_url
661 if (!file_exists($f)) {
662 $nhurl = str_ireplace(site_url(), home_url(), $url);
663 $f = str_ireplace(rtrim($wp_home, '/'), rtrim(ABSPATH, '/'), $nhurl);
664 }
665 clearstatcache();
666 if (file_exists($f)) {
667 $content = file_get_contents($f);
668 // check for php code, skip if found
669 if ("<?php" !== strtolower(substr($content, 0, 5)) && false === stripos($content, "<?php")) {
670 return array('content' => $content, 'method' => 'local');
671 }
672 }
673 }
674
675
676 // else, fallback to remote urls (or windows)
677 $content = self::download_remote($url);
678 if (false !== $content
679 && !empty($content)
680 && "<!doctype" !== strtolower(substr($content, 0, 9))
681 ) {
682 // check if we got HTML instead of js or css code
683 return array('content' => $content, 'method' => 'remote');
684 }
685
686
687 // fallback when home_url != site_url
688 if (false !== stripos($url, $wp_domain) && home_url() !== site_url()) {
689 $nhurl = str_ireplace(site_url(), home_url(), $url);
690 $content = self::download_remote($nhurl);
691 if (false !== $content && !empty($content) && '<!doctype' !== strtolower(substr($content, 0, 9))) {
692 return array('content' => $content, 'method' => 'remote');
693 }
694 }
695
696 return array('content' => '', 'method' => 'none');
697 }
698
699 /**
700 * Remove emoji support
701 *
702 * @return void
703 */
704 public static function disable_wp_emojicons() {
705 remove_action('wp_head', 'print_emoji_detection_script', 7);
706 remove_action('admin_print_scripts', 'print_emoji_detection_script');
707 remove_action('wp_print_styles', 'print_emoji_styles');
708 remove_action('wp_enqueue_scripts', 'wp_enqueue_emoji_styles');
709 remove_action('admin_print_styles', 'print_emoji_styles');
710 remove_action('admin_enqueue_scripts', 'wp_enqueue_emoji_styles');
711 remove_filter('the_content_feed', 'wp_staticize_emoji');
712 remove_filter('comment_text_rss', 'wp_staticize_emoji');
713 remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
714 }
715
716 /**
717 * Remove from tinymce
718 *
719 * @return array
720 */
721 public static function disable_emojis_tinymce($plugins) {
722 if (is_array($plugins)) {
723 return array_diff($plugins, array('wpemoji'));
724 } else {
725 return array();
726 }
727 }
728
729 /**
730 * Remove UTF8 BOM.
731 * Returns BOM removed string or null when `$string` does not have a recognised encoding
732 *
733 * @param string $string
734 * @return string|null
735 */
736 public static function remove_utf8_bom($string) {
737 $bom = pack('H*', 'EFBBBF');
738 return preg_replace("/^$bom/ui", '', $string);
739 }
740
741 /**
742 * Remove query string from static css files
743 *
744 * @param string $src
745 * @return string
746 */
747 public static function remove_cssjs_ver($src) {
748 if (stripos($src, '?ver=') && self::is_already_minified($src)) {
749 $src = remove_query_arg('ver', $src);
750 }
751 return $src;
752 }
753
754 /**
755 * Determine if the source is already minified (served from minify cache)
756 *
757 * @param string $src
758 * @return boolean
759 */
760 public static function is_already_minified($src) {
761 return false !== strpos($src, 'cache/wpo-minify');
762 }
763
764 /**
765 * Rewrite cache files to http, https or dynamic
766 *
767 * @param string $url
768 * @return string
769 */
770 public static function get_protocol($url) {
771 $wp_domain = trim(preg_replace('/^https?:\/\//i', '', trim(site_url(), '/')));
772 $wpo_minify_options = wp_optimize_minify_config()->get();
773 $default_protocol = $wpo_minify_options['default_protocol'];
774
775 $url = ltrim(preg_replace('/^https?:\/\//i', '', $url), '/'); // better compatibility
776
777 // cdn support
778 $cdn_url = $wpo_minify_options['cdn_url'];
779 $cdn_url = trim(trim(preg_replace('/^https?:\/\//i', '', trim($cdn_url, '/'))), '/');
780
781 // process cdn rewrite
782 if (!empty($cdn_url) && false !== self::is_local_domain($url)) {
783
784 // for js files, we need to consider thew defer for insights option
785 if ('.js' === substr($url, -3)) {
786 $async_using_js = 'all' === $wpo_minify_options['enable_defer_js'] && 'async_using_js' === $wpo_minify_options['defer_js_type'];
787 if (!$async_using_js
788 || $wpo_minify_options['cdn_force']
789 ) {
790 $url = str_ireplace($wp_domain, $cdn_url, $url);
791 }
792 } else {
793 $url = str_ireplace($wp_domain, $cdn_url, $url);
794 }
795 }
796
797 // enforce protocol if needed
798 if ('dynamic' === $default_protocol) {
799 if ((isset($_SERVER['HTTPS']) && ('on' === $_SERVER['HTTPS'] || 1 === (int) $_SERVER['HTTPS']))
800 || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'])
801 ) {
802 $default_protocol = 'https://';
803 } else {
804 $default_protocol = 'http://';
805 }
806 } else {
807 $default_protocol = $default_protocol.'://';
808 }
809
810 // return
811 return $default_protocol . $url;
812 }
813
814 /**
815 * Exclude processing from some pages / posts / contents
816 *
817 * @return boolean
818 */
819 public static function exclude_contents() {
820 // prevent execution for specific urls
821 if (isset($_SERVER['REQUEST_URI']) && !empty($_SERVER['REQUEST_URI'])) {
822 $disable_on_url = array_filter(array_map('trim', explode("\n", get_option('wpo_min_disable_on_url', ''))));
823 foreach ($disable_on_url as $url) {
824 if (wp_parse_url(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])), PHP_URL_PATH) === $url) {
825 return true;
826 }
827 }
828 }
829
830 // for compatibility, let's always skip the checkout page
831 if (function_exists('is_checkout') && true === is_checkout()) {
832 return true;
833 }
834
835 if (isset($_SERVER['REQUEST_URI'])) {
836 $is_txt_extension = '.txt' === strtolower(substr(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])), -4));
837 $is_xml_extension = '.xml' === strtolower(substr(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])), -4));
838 }
839
840 // exclude processing here
841 if (is_feed()
842 || is_admin()
843 || is_preview()
844 || (function_exists('is_customize_preview') && is_customize_preview())
845 || (defined('DOING_AJAX') && DOING_AJAX)
846 || (function_exists('wp_doing_ajax') && wp_doing_ajax())
847 || (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
848 || (defined('WP_BLOG_ADMIN') && WP_BLOG_ADMIN)
849 || (defined('WP_NETWORK_ADMIN') && WP_NETWORK_ADMIN)
850 || (defined('WP_INSTALLING') && WP_INSTALLING)
851 || (defined('WP_IMPORTING') && WP_IMPORTING)
852 || (defined('WP_REPAIRING') && WP_REPAIRING)
853 || (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST)
854 || (defined('SHORTINIT') && SHORTINIT)
855 || (defined('REST_REQUEST') && REST_REQUEST)
856 || (isset($_SERVER['REQUEST_METHOD']) && 'POST' === $_SERVER['REQUEST_METHOD'])
857 || (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && 'xmlhttprequest' === strtolower(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_REQUESTED_WITH']))))
858 || (isset($_SERVER['REQUEST_URI']) && ($is_txt_extension || $is_xml_extension))
859 ) {
860 return true;
861 }
862
863 // phpcs:disable
864 // WordPress.Security.NonceVerification.Recommended -- Using $_GET element only to compare, returns boolean
865 // Thrive plugins and other post_types
866 $arr = array('tve_form_type', 'tve_lead_shortcode', 'tqb_splash');
867 foreach ($arr as $a) {
868 if (isset($_GET['post_type']) && $a === $_GET['post_type']) {
869 return true;
870 }
871 }
872
873 // Thrive architect
874 if (isset($_GET['tve']) && 'true' === $_GET['tve']) return true;
875
876
877 if (is_array($_GET)) {
878 foreach ($_GET as $k => $v) {
879 if (is_string($v) && is_string($k)) {
880 if (false !== stripos($k, 'elementor') || false !== stripos($v, 'elementor')) {
881 return true;
882 }
883 }
884 }
885 }
886
887 // Other _GET parameters
888 if (is_array($_GET)) {
889 $get_params = array_keys($_GET);
890 $excluded_params = array(
891 // customizer preview, visual composer
892 'customize_theme',
893 'preview_id',
894 'preview',
895 // Elementor
896 'elementor-preview',
897 // Divi builder
898 'et_fb',
899 'PageSpeed',
900 // Brizy Editor
901 'brizy-edit',
902 'brizy-edit-iframe',
903 // Beaver builder
904 'fl_builder',
905 'trp-edit-translation',
906 );
907 return (bool) count(array_intersect($excluded_params, $get_params));
908 }
909 // phpcs:enable
910
911 /**
912 * Whether to exclude the content or not from the minifying process.
913 */
914 return (bool) apply_filters('wpo_minify_exclude_contents', false);
915 }
916
917 /**
918 * Get the default files which are ignored / excluded from processing
919 *
920 * @return array
921 */
922 public static function get_default_ignore() {
923 $default_exclusions = array(
924 '/genericons.css',
925 '/Avada/assets/js/main.min.js',
926 '/woocommerce-product-search/js/product-search.js',
927 '/includes/builder/scripts/frontend-builder-scripts.js',
928 '/assets/js/jquery.themepunch.tools.min.js',
929 '/js/TweenMax.min.js',
930 '/jupiter/assets/js/min/full-scripts',
931 '/wp-content/themes/Divi/core/admin/js/react-dom.production.min.js',
932 '/LayerSlider/static/layerslider/js/greensock.js',
933 '/themes/kalium/assets/js/main.min.js',
934 '/wp-includes/js/mediaelement/wp-mediaelement.min.js',
935 'elementor-admin-bar',
936 'pdfjs-dist',
937 'wordpress-popular-posts',
938 'uploads/bb-plugin/cache', // Beaver builder page specific pages
939 );
940 /**
941 * Filters the default exclusions
942 *
943 * @param array The exclusions
944 * @return array
945 */
946 $filtered_exclusions = apply_filters('wp-optimize-minify-default-exclusions', $default_exclusions);
947 return is_array($filtered_exclusions) ? $filtered_exclusions : $default_exclusions;
948 }
949
950 /**
951 * Know files that should always be ignored
952 *
953 * @param array $ignore
954 * @return array
955 */
956 public static function compile_ignore_list($ignore) {
957 $ignore_list = self::get_default_ignore();
958 $user_excluded_ignore_items = wp_optimize_minify_config()->get('ignore_list');
959 if (is_array($user_excluded_ignore_items)) $ignore_list = array_diff($ignore_list, $user_excluded_ignore_items);
960 if (is_array($ignore)) {
961 $master_ignore = array_merge(array_map('strtolower', $ignore), array_map('strtolower', $ignore_list));
962 return array_unique($master_ignore);
963 } else {
964 return $ignore_list;
965 }
966 }
967
968 /**
969 * Get the files excluded for IE compatibility
970 *
971 * @return array
972 */
973 public static function get_default_ie_blacklist() {
974 $default_ie_blacklist = array(
975 '/html5shiv.js',
976 '/html5shiv-printshiv.min.js',
977 '/excanvas.js',
978 '/avada-ie9.js',
979 '/respond.js',
980 '/respond.min.js',
981 '/selectivizr.js',
982 '/Avada/assets/css/ie.css',
983 '/html5.js',
984 '/IE9.js',
985 '/fusion-ie9.js',
986 '/vc_lte_ie9.min.css',
987 '/old-ie.css',
988 '/ie.css',
989 '/vc-ie8.min.css',
990 '/mailchimp-for-wp/assets/js/third-party/placeholders.min.js',
991 '/assets/js/plugins/wp-enqueue/min/webfontloader.js',
992 '/a.optnmstr.com/app/js/api.min.js',
993 '/pixelyoursite/js/public.js',
994 '/assets/js/wcdrip-drip.js',
995 '/instantpage.js',
996 );
997 /**
998 * Filters the default IE specific / blacklisted items
999 *
1000 * @param array The blacklist
1001 * @return array
1002 */
1003 $filtered_ie_blacklist = apply_filters('wp-optimize-minify-blacklist', $default_ie_blacklist);
1004 return is_array($filtered_ie_blacklist) ? $filtered_ie_blacklist : $default_ie_blacklist;
1005 }
1006
1007 /**
1008 * Get the files excluded for IE compatibility
1009 *
1010 * @return array
1011 */
1012 public static function get_ie_blacklist() {
1013 $user_excluded_blacklist_items = wp_optimize_minify_config()->get('blacklist');
1014 $default_blacklist = self::get_default_ie_blacklist();
1015 if (!is_array($user_excluded_blacklist_items)) return $default_blacklist;
1016 return array_diff($default_blacklist, $user_excluded_blacklist_items);
1017 }
1018
1019 /**
1020 * IE only files that should always be ignored, without incrementing our groups
1021 *
1022 * @param string $url
1023 * @return boolean
1024 */
1025 public static function is_url_in_ie_blacklist($url) {
1026 // from the database
1027 $blacklist = self::get_ie_blacklist();
1028 // must have
1029 $blacklist[] = '/wpo_min/cache/';
1030
1031 // is the url on our list and return
1032 $res = self::in_arrayi($url, $blacklist);
1033 if ($res) {
1034 return true;
1035 } else {
1036 return false;
1037 }
1038 }
1039
1040 /**
1041 * Download function with fallback
1042 *
1043 * @param string $url
1044 * @return boolean|string
1045 */
1046 public static function download_remote($url) {
1047
1048 $args = array(
1049 // info (needed for Google fonts woff files + hinted fonts) as well as to bypass some security filters
1050 'user-agent' => WP_Optimize_Utils::get_user_agent('gfont'),
1051 'timeout' => 7
1052 );
1053
1054 // fetch via WordPress functions
1055 $response = wp_remote_get(
1056 $url,
1057 /**
1058 * Filters the arguments passed to wp_remote_get when downloading the scripts.
1059 *
1060 * @param array $args - The arguments filtered
1061 * @param string $url - The URL of the downloaded asset
1062 * @return array
1063 */
1064 apply_filters('wpo_minify_download_request_args', $args, $url)
1065 );
1066
1067 $res_code = wp_remote_retrieve_response_code($response);
1068 if (200 === $res_code) {
1069 $data = wp_remote_retrieve_body($response);
1070 if (strlen($data) > 1) {
1071 return $data;
1072 }
1073 }
1074
1075 return false;
1076 }
1077
1078 /**
1079 * Prepares the merged JavaScript
1080 *
1081 * @param string $script
1082 * @param string $merged_url
1083 * @return string
1084 */
1085 public static function prepare_merged_js($script, $merged_url) {
1086 $enable_js_trycatch = wp_optimize_minify_config()->get('enable_js_trycatch');
1087 if ($enable_js_trycatch) {
1088 return 'try{'."\n".$script."\n".'}'."\n".'catch(e){console.error("WP-Optimize Minify: An error has occurred in the minified code. \n\n- Original script: '.esc_attr($merged_url).'\n- Error message: "+ e.message);}'."\n";
1089 }
1090 return $script;
1091 }
1092
1093 /**
1094 * Checks if URL is a font-awesome resource (checks if it contains font-awesome or fontawesome)
1095 *
1096 * @param string $href
1097 * @return boolean
1098 */
1099 public static function is_font_awesome($href) {
1100 return (bool) preg_match('/font[-_]?awesome/i', $href);
1101 }
1102
1103 /**
1104 * Checks if URL is a Google font resource
1105 *
1106 * @param string $href
1107 * @return boolean
1108 */
1109 public static function is_google_font($href) {
1110 return 'fonts.googleapis.com' === strtolower(wp_parse_url($href, PHP_URL_HOST));
1111 }
1112
1113 /**
1114 * Get the content of an asset, whether local or remote
1115 *
1116 * @param string $url
1117 * @return int|false
1118 */
1119 public static function get_file_size($url) {
1120 $original_url = $url;
1121 if (is_multisite()) {
1122 if (function_exists('get_main_site_id')) {
1123 $site_id = get_main_site_id();
1124 } else {
1125 $network = get_network();
1126 $site_id = $network->site_id;
1127 }
1128 switch_to_blog($site_id);
1129 }
1130 $upload_dir = wp_upload_dir();
1131 $uploads_url = trailingslashit($upload_dir['baseurl']);
1132 $uploads_dir = trailingslashit($upload_dir['basedir']);
1133 if (is_multisite()) {
1134 restore_current_blog();
1135 }
1136 $possible_urls = array(
1137 WP_CONTENT_URL => WP_CONTENT_DIR,
1138 WP_PLUGIN_URL => WP_PLUGIN_DIR,
1139 $uploads_url => $uploads_dir,
1140 get_template_directory_uri() => get_template_directory(),
1141 untrailingslashit(includes_url()) => ABSPATH . WPINC,
1142 );
1143
1144 $file = false;
1145 foreach ($possible_urls as $possible_url => $path) {
1146 $pos = strpos($url, $possible_url);
1147 if (false !== $pos) {
1148 $file = substr_replace($url, $path, $pos, strlen($possible_url));
1149 break;
1150 }
1151 }
1152 if (is_string($file) && file_exists($file)) {
1153 return filesize($file);
1154 }
1155
1156 return self::get_remote_file_size($original_url);
1157 }
1158
1159 /**
1160 * Get the file size of a file hosting in other servers
1161 *
1162 * @param string $url
1163 * @return int|false
1164 */
1165 public static function get_remote_file_size($url) {
1166 $args = array(
1167 // info (needed for Google fonts woff files + hinted fonts) as well as to bypass some security filters
1168 'user-agent' => WP_Optimize_Utils::get_user_agent('gfont'),
1169 'timeout' => 7
1170 );
1171
1172 // fetch via WordPress functions
1173 $response = wp_remote_get($url, $args);
1174
1175 if (is_wp_error($response)) return false;
1176
1177 if (!empty($response) && is_array($response) && isset($response['headers']['Content-Length'])) {
1178 return $response['headers']['Content-Length'];
1179 }
1180
1181 return false;
1182 }
1183
1184 /**
1185 * Check if it is 'flatsome' handle
1186 *
1187 * @param string $handle Handle of enqueued css file
1188 *
1189 * @return bool
1190 */
1191 public static function is_flatsome_handle($handle) {
1192 return 0 === strcmp('flatsome-googlefonts', $handle);
1193 }
1194
1195 /**
1196 * Fix google fonts url for 'flatsome' theme
1197 *
1198 * @param string $href Enqueued google fonts url
1199 *
1200 * @return string Fixed google fonts url
1201 */
1202 public static function fix_flatsome_google_fonts_url($href) {
1203 // Get query from $href
1204 $query = wp_parse_url($href, PHP_URL_QUERY);
1205 $query_arr = explode('&', $query);
1206
1207 // Separate 'family and display' arguments in query
1208 $family = str_replace('family=', '', $query_arr[0]);
1209 $display_type = $query_arr[1];
1210
1211 // Remove empty fonts
1212 $font_families = explode('|', $family);
1213 $font_families = array_diff($font_families, array(':regular', 'initial', 'inherit', ''));
1214
1215 $system_fonts = array('+apple+system', '-apple-system', 'BlinkMacSystemFont', 'Segoe+UI', 'Helvetica+Neue', 'sans+serif', 'sans-serif', 'Georgia', 'Times', 'Times+New+Roman', 'serif');
1216 $google_fonts = array();
1217
1218 // URL may look like following, fix it
1219 // https://fonts.googleapis.com/css?family=-apple-system,+BlinkMacSystemFont,+%22Segoe+UI%22,+Roboto,+Oxygen-Sans,+Ubuntu,+Cantarell,+%22Helvetica+Neue%22,+sans-serif:regular,700,regular,700|Bebas+Neue:regular,regular&display=block
1220 // http://fonts.googleapis.com/css?family=Bellota:regular|:regular|Lato:regular|Creepster:regular&display=block
1221 // http://fonts.googleapis.com/css?family=Lora:regular,regular|initial|Lato:regular,regular|&display=swap
1222
1223 foreach ($font_families as $font) {
1224 $font_variant = explode(':', $font);
1225
1226 // Remove beginning '+'
1227 $font = str_replace(',+', ',', $font_variant[0]);
1228
1229 // Replace '-' with '+'
1230 $font = str_replace(array('-', ' '), '+', $font);
1231
1232 // Remove `"` or '%22'
1233 $font = str_replace(array('%22', '"'), '', $font);
1234 $font_arr = explode(',', $font);
1235 $font_arr = array_diff($font_arr, $system_fonts);
1236 $variant = '';
1237 if (!empty($font_variant[1])) {
1238 $variant = implode(',', array_unique(explode(',', $font_variant[1])));
1239 }
1240 if (empty($variant)) {
1241 $variant = 'regular';
1242 }
1243 foreach ($font_arr as $font) {
1244 $google_fonts[] = $font . ':' . $variant;
1245 }
1246 }
1247 $protocol = is_ssl() ? 'https:' : 'http:';
1248 return $protocol . '//fonts.googleapis.com/css?family=' . implode('|', $google_fonts) . '&' . $display_type;
1249 }
1250
1251 /**
1252 * Get the file modification time
1253 *
1254 * @param string $asset_src
1255 * @return string
1256 */
1257 public static function get_modification_time($asset_src) {
1258 $hurl = self::get_hurl($asset_src);
1259 $abs_file_path = WP_Optimize_Utils::get_file_path($hurl);
1260 if (empty($abs_file_path)) return '';
1261
1262 $modification_time = strval(@filemtime($abs_file_path)); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Suppress E-Warning on failure
1263 $filtered_modification_time = apply_filters('wpo_minify_file_modification_time', $modification_time, $abs_file_path);
1264
1265 return is_string($filtered_modification_time) ? $filtered_modification_time : $modification_time;
1266 }
1267
1268 /**
1269 * When BOM removed code is null (due to unrecognised character encoding), logs error message
1270 *
1271 * @param string $url URL of the script/stylesheet
1272 * @param string|false $encoding Character encoding
1273 * @param string|null $code Script/Stylesheet code
1274 *
1275 * @return void
1276 */
1277 private static function maybe_log_error_message($url, $encoding, $code) {
1278 if (null === $code) {
1279 $message = "Minify: Could not process {$url}, it contains invalid characters. ";
1280 if (false === $encoding) {
1281 $message .= "Could not determine its character encoding.";
1282 }
1283 error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Using for debugging purpose
1284 }
1285 }
1286 }
1287