PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.5.2
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.5.2
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.5.2, at minify/class-wp-optimize-minify-functions.php

1,276 lines 38.7 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 * Functions, minify html
360 *
361 * @param string $html
362 * @return string
363 */
364 public static function minify_html($html) {
365 $minify_css = wp_optimize_minify_config()->get('enable_css_minification');
366 $minify_js = wp_optimize_minify_config()->get('enable_js_minification');
367 $options = array();
368 if ($minify_css && apply_filters('wpo_minify_inline_css', true)) {
369 $options['cssMinifier'] = array('WP_Optimize_Minify_Functions', 'minify_css_string');
370 }
371 if ($minify_js && apply_filters('wpo_minify_inline_js', true)) {
372 $options['jsMinifier'] = array('WP_Optimize_Minify_Functions', 'minify_inline_js');
373 }
374 return Minify_HTML::minify($html, $options);
375 }
376
377 /**
378 * Functions to minify HTML
379 *
380 * @param string $html
381 * @return string
382 */
383 public static function html_compression_finish($html) {
384 return self::minify_html($html);
385 }
386
387 /**
388 * Start the compression
389 *
390 * @return void
391 */
392 public static function html_compression_start() {
393 if (self::exclude_contents()) {
394 return;
395 }
396 ob_start(array(__CLASS__, 'html_compression_finish'));
397 }
398
399 /**
400 * Remove default HTTP headers
401 *
402 * @return void
403 */
404 public static function remove_redundant_shortlink() {
405 remove_action('wp_head', 'wp_shortlink_wp_head', 10);
406 remove_action('template_redirect', 'wp_shortlink_header', 11);
407 }
408
409 /**
410 * Minify css on demand (one file at one time, for compatibility)
411 *
412 * @param string $url
413 * @param string $css
414 * @param boolean $enable_css_minification
415 * @return string
416 */
417 public static function get_css($url, $css, $enable_css_minification) {
418 $wpo_minify_options = wp_optimize_minify_config()->get();
419
420 $encoding = mb_detect_encoding($css);
421
422 // remove BOM
423 $css = self::remove_utf8_bom($css);
424
425 self::maybe_log_error_message($url, $encoding, $css);
426
427 // fix url paths
428 if (!empty($url)) {
429 $css = self::make_css_urls_absolute($css, $url);
430 }
431
432 $css = str_ireplace('@charset "UTF-8";', '', $css);
433
434 // remove query strings from fonts (for better seo, but add a small cache buster based on most recent updates)
435 // last update or zero
436 $cache_time = $wpo_minify_options['last-cache-update'];
437 // fonts cache buster
438 $css = preg_replace('/(.eot|.woff2|.woff|.ttf)+[?+](.+?)(\)|\'|\")/ui', "$1"."#".$cache_time."$3", $css);
439 // Remove Sourcemappingurls
440 $css = preg_replace('/(\/\*\s*[#]\s*sourceMappingURL\s*[=]\s*)(.[^*]+)\s*\*\//ui', '', $css);
441 // If @import is found, process it/them
442 if (false !== strpos($css, '@import')) $css = self::replace_css_import($css, $url);
443
444 // minify CSS
445 if ($enable_css_minification) {
446 $css = self::minify_css_string($css);
447 } else {
448 $css = self::compat_urls($css);
449 }
450
451 // cdn urls
452 $cdn_url = $wpo_minify_options['cdn_url'];
453 if (!empty($cdn_url)) {
454 $wp_domain = trim(preg_replace('/^https?:\/\//i', '', trim(site_url(), '/')));
455 $cdn_url = trim(trim(preg_replace('/^https?:\/\//i', '', trim($cdn_url, '/'))), '/');
456 $css = str_ireplace($wp_domain, $cdn_url, $css);
457 }
458
459 // add css comment
460 $css = trim($css);
461 if ($wpo_minify_options['debug']) {
462 $css = '/* info: ' . $url . ' */' . "\n" . trim($css);
463 }
464
465 /**
466 * Filters the imported CSS
467 *
468 * @param string $css - The imported CSS
469 * @param string $url - The imported url
470 * @param boolean $enable_css_minification - Whether to minify or not
471 */
472 $filtered_css = apply_filters('wpo_minify_get_css', $css, $url, $enable_css_minification);
473 return is_string($filtered_css) ? $filtered_css : $css;
474 }
475
476 /**
477 * Adds full path to relative url() rules
478 *
479 * @param string $css - The CSS to process
480 * @param string $url - The URL or the CSS being processed
481 * @return string
482 */
483 public static function make_css_urls_absolute($css, $url) {
484 $matches = array();
485 preg_match_all("/url\(\s*['\"]?(?!data:)(?!http)(?![\/'\"])(.+?)['\"]?\s*\)/ui", $css, $matches);
486 foreach ($matches[1] as $a) {
487 $b = trim($a);
488 if ($b !== $a) {
489 $css = str_replace($a, $b, $css);
490 }
491 }
492 return (string) preg_replace("/url\(\s*['\"]?(?!data:)(?!http)(?![\/'\"])(.+?)['\"]?\s*\)/ui", "url(".dirname($url)."/$1)", $css);
493 }
494
495 /**
496 * Include @import[ed] files - The @import statement can only be used at the top of a file, which breaks when merging everything.
497 *
498 * @param string $css - The original CSS containing the @import statement
499 * @param string $file_url - The original CSS' URL
500 * @return string
501 */
502 public static function replace_css_import($css, $file_url) {
503 $remove_print_mediatypes = wp_optimize_minify_config()->get('remove_print_mediatypes');
504 $debug = wp_optimize_minify_config()->get('debug');
505 return preg_replace_callback('/(?:@import)\s(?:url\()?\s?["\'](.*?)["\']\s?\)?(?:[^;]*);?/im', function($matches) use ($file_url, $remove_print_mediatypes, $debug) {
506 // @import contains url()
507 if (preg_match('/url\s*\((.[^\)]*)[\)*?](.*);/', $matches[0], $url_matches)) {
508 $url = trim(str_replace(array('"', "'"), '', $url_matches[1]));
509 $media_query = trim($url_matches[2]);
510 // @import uses quotes only
511 } elseif (preg_match('/["\'](.*)["\'](.*);?/', $matches[0], $no_url_matches)) {
512 $url = trim($no_url_matches[1]);
513 $media_query = trim($no_url_matches[2], ") ;");
514 }
515
516 // If $media_query contains print, and $remove_print_mediatypes is true, return empty string
517 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. */' : '');
518
519 $purl = wp_parse_url($url);
520 // If there's no host, the url is relative to $file_url, so prepend with the base url.
521 if (!isset($purl['host'])) {
522 $url = dirname($file_url).'/'.$url;
523 }
524
525 // Download @import
526 $asset_content = WP_Optimize_Minify_Functions::get_asset_content($url);
527 $content = $asset_content['content'];
528
529 if (!$content) return '';
530
531 // Fix the URLs
532 $content = WP_Optimize_Minify_Functions::make_css_urls_absolute($content, $url);
533
534 if ($media_query) {
535 // Wrap the code with the media query
536 $content = "@media $media_query {\n$content\n}";
537 }
538
539 if ($debug) {
540 $content = "/*! CSS import Information: code imported from $url */\n$content\n/*! END CSS import Information */";
541 }
542
543 // If the code contains its own @import, recursively include it.
544 if (false !== strpos($content, '@import')) {
545 return WP_Optimize_Minify_Functions::replace_css_import($content, $url);
546 }
547
548 return $content;
549 }, $css);
550 }
551
552 /**
553 * Download and cache css and js files
554 *
555 * @param ?string $hurl
556 * @param ?string $inline
557 * @param boolean $enable_minification
558 * @param string $type
559 * @param ?string $handle
560 * @param mixed $version
561 *
562 * @return boolean|string
563 */
564 public static function download_and_minify($hurl, $inline, $enable_minification, $type, $handle, $version = '') {
565 // must have
566 if (empty($hurl)) {
567 return false;
568 }
569 if (!in_array($type, array('js', 'css'))) {
570 return false;
571 }
572
573 $wpo_minify_options = wp_optimize_minify_config()->get();
574
575 // filters and defaults
576 $print_url = str_ireplace(array(site_url(), home_url(), 'http:', 'https:'), '', $hurl);
577
578 $log = array(
579 'url' => $print_url,
580 );
581
582 // defaults
583 if (false !== $enable_minification) {
584 $enable_minification = true;
585 }
586 if (empty($inline)) {
587 $inline = '';
588 }
589 $print_handle = '';
590 if (empty($handle)) {
591 $handle = '';
592 } else {
593 $print_handle = "[$handle]";
594 }
595
596 // debug request
597 $dreq = array(
598 'hurl' => $hurl,
599 'inline' => $inline,
600 'enable_minification' => $enable_minification,
601 'type' => $type,
602 'handle' => $handle,
603 'version' => $version
604 );
605
606 $asset_content = self::get_asset_content($hurl);
607 $code = $asset_content['content'];
608
609 // If $code is empty:
610 if (!$code) {
611 $log['success'] = false;
612 if ($wpo_minify_options['debug']) {
613 $log['debug'] = "$print_handle failed. Tried wp_remote_get and local file_get_contents.";
614 }
615 $return = array('request' => $dreq, 'log' => $log, 'code' => '', 'status' => false);
616 return wp_json_encode($return);
617 }
618
619 if ('js' === $type) {
620 $code = self::get_js($hurl, $code, $enable_minification);
621 } else {
622 $code = self::get_css($hurl, $code.$inline, $enable_minification);
623 }
624
625 // log, save and return
626 if ($wpo_minify_options['debug']) {
627 $version_msg = ('' !== $version) ? "[Version: $version]" : "";
628 $log['debug'] = $print_handle . $version_msg . ' was '.('local' === $asset_content['method'] ? 'opened' : 'fetched').' from '.$hurl;
629 }
630 $log['success'] = true;
631 $return = array('request' => $dreq, 'log' => $log, 'code' => $code, 'status' => true);
632 return wp_json_encode($return);
633 }
634
635 /**
636 * Get the content of an asset, whether local or remote
637 *
638 * @param string $url
639 * @return array
640 */
641 public static function get_asset_content($url) {
642
643 $wp_home = site_url();
644 $wp_domain = wp_parse_url($wp_home, PHP_URL_HOST);
645 // If the file is local.
646 if (false !== stripos($url, $wp_domain)) {
647 // default
648 $f = str_ireplace(rtrim($wp_home, '/'), rtrim(ABSPATH, '/'), $url);
649 // fail over when home_url != site_url
650 if (!file_exists($f)) {
651 $nhurl = str_ireplace(site_url(), home_url(), $url);
652 $f = str_ireplace(rtrim($wp_home, '/'), rtrim(ABSPATH, '/'), $nhurl);
653 }
654 clearstatcache();
655 if (file_exists($f)) {
656 $content = file_get_contents($f);
657 // check for php code, skip if found
658 if ("<?php" !== strtolower(substr($content, 0, 5)) && false === stripos($content, "<?php")) {
659 return array('content' => $content, 'method' => 'local');
660 }
661 }
662 }
663
664
665 // else, fallback to remote urls (or windows)
666 $content = self::download_remote($url);
667 if (false !== $content
668 && !empty($content)
669 && "<!doctype" !== strtolower(substr($content, 0, 9))
670 ) {
671 // check if we got HTML instead of js or css code
672 return array('content' => $content, 'method' => 'remote');
673 }
674
675
676 // fallback when home_url != site_url
677 if (false !== stripos($url, $wp_domain) && home_url() !== site_url()) {
678 $nhurl = str_ireplace(site_url(), home_url(), $url);
679 $content = self::download_remote($nhurl);
680 if (false !== $content && !empty($content) && '<!doctype' !== strtolower(substr($content, 0, 9))) {
681 return array('content' => $content, 'method' => 'remote');
682 }
683 }
684
685 return array('content' => '', 'method' => 'none');
686 }
687
688 /**
689 * Remove emoji support
690 *
691 * @return void
692 */
693 public static function disable_wp_emojicons() {
694 remove_action('wp_head', 'print_emoji_detection_script', 7);
695 remove_action('admin_print_scripts', 'print_emoji_detection_script');
696 remove_action('wp_print_styles', 'print_emoji_styles');
697 remove_action('wp_enqueue_scripts', 'wp_enqueue_emoji_styles');
698 remove_action('admin_print_styles', 'print_emoji_styles');
699 remove_action('admin_enqueue_scripts', 'wp_enqueue_emoji_styles');
700 remove_filter('the_content_feed', 'wp_staticize_emoji');
701 remove_filter('comment_text_rss', 'wp_staticize_emoji');
702 remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
703 }
704
705 /**
706 * Remove from tinymce
707 *
708 * @return array
709 */
710 public static function disable_emojis_tinymce($plugins) {
711 if (is_array($plugins)) {
712 return array_diff($plugins, array('wpemoji'));
713 } else {
714 return array();
715 }
716 }
717
718 /**
719 * Remove UTF8 BOM.
720 * Returns BOM removed string or null when `$string` does not have a recognised encoding
721 *
722 * @param string $string
723 * @return string|null
724 */
725 public static function remove_utf8_bom($string) {
726 $bom = pack('H*', 'EFBBBF');
727 return preg_replace("/^$bom/ui", '', $string);
728 }
729
730 /**
731 * Remove query string from static css files
732 *
733 * @param string $src
734 * @return string
735 */
736 public static function remove_cssjs_ver($src) {
737 if (stripos($src, '?ver=') && self::is_already_minified($src)) {
738 $src = remove_query_arg('ver', $src);
739 }
740 return $src;
741 }
742
743 /**
744 * Determine if the source is already minified (served from minify cache)
745 *
746 * @param string $src
747 * @return boolean
748 */
749 public static function is_already_minified($src) {
750 return false !== strpos($src, 'cache/wpo-minify');
751 }
752
753 /**
754 * Rewrite cache files to http, https or dynamic
755 *
756 * @param string $url
757 * @return string
758 */
759 public static function get_protocol($url) {
760 $wp_domain = trim(preg_replace('/^https?:\/\//i', '', trim(site_url(), '/')));
761 $wpo_minify_options = wp_optimize_minify_config()->get();
762 $default_protocol = $wpo_minify_options['default_protocol'];
763
764 $url = ltrim(preg_replace('/^https?:\/\//i', '', $url), '/'); // better compatibility
765
766 // cdn support
767 $cdn_url = $wpo_minify_options['cdn_url'];
768 $cdn_url = trim(trim(preg_replace('/^https?:\/\//i', '', trim($cdn_url, '/'))), '/');
769
770 // process cdn rewrite
771 if (!empty($cdn_url) && false !== self::is_local_domain($url)) {
772
773 // for js files, we need to consider thew defer for insights option
774 if ('.js' === substr($url, -3)) {
775 $async_using_js = 'all' === $wpo_minify_options['enable_defer_js'] && 'async_using_js' === $wpo_minify_options['defer_js_type'];
776 if (!$async_using_js
777 || $wpo_minify_options['cdn_force']
778 ) {
779 $url = str_ireplace($wp_domain, $cdn_url, $url);
780 }
781 } else {
782 $url = str_ireplace($wp_domain, $cdn_url, $url);
783 }
784 }
785
786 // enforce protocol if needed
787 if ('dynamic' === $default_protocol) {
788 if ((isset($_SERVER['HTTPS']) && ('on' === $_SERVER['HTTPS'] || 1 === (int) $_SERVER['HTTPS']))
789 || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'])
790 ) {
791 $default_protocol = 'https://';
792 } else {
793 $default_protocol = 'http://';
794 }
795 } else {
796 $default_protocol = $default_protocol.'://';
797 }
798
799 // return
800 return $default_protocol . $url;
801 }
802
803 /**
804 * Exclude processing from some pages / posts / contents
805 *
806 * @return boolean
807 */
808 public static function exclude_contents() {
809 // prevent execution for specific urls
810 if (isset($_SERVER['REQUEST_URI']) && !empty($_SERVER['REQUEST_URI'])) {
811 $disable_on_url = array_filter(array_map('trim', explode("\n", get_option('wpo_min_disable_on_url', ''))));
812 foreach ($disable_on_url as $url) {
813 if (wp_parse_url(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])), PHP_URL_PATH) === $url) {
814 return true;
815 }
816 }
817 }
818
819 // for compatibility, let's always skip the checkout page
820 if (function_exists('is_checkout') && true === is_checkout()) {
821 return true;
822 }
823
824 if (isset($_SERVER['REQUEST_URI'])) {
825 $is_txt_extension = '.txt' === strtolower(substr(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])), -4));
826 $is_xml_extension = '.xml' === strtolower(substr(esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])), -4));
827 }
828
829 // exclude processing here
830 if (is_feed()
831 || is_admin()
832 || is_preview()
833 || (function_exists('is_customize_preview') && is_customize_preview())
834 || (defined('DOING_AJAX') && DOING_AJAX)
835 || (function_exists('wp_doing_ajax') && wp_doing_ajax())
836 || (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)
837 || (defined('WP_BLOG_ADMIN') && WP_BLOG_ADMIN)
838 || (defined('WP_NETWORK_ADMIN') && WP_NETWORK_ADMIN)
839 || (defined('WP_INSTALLING') && WP_INSTALLING)
840 || (defined('WP_IMPORTING') && WP_IMPORTING)
841 || (defined('WP_REPAIRING') && WP_REPAIRING)
842 || (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST)
843 || (defined('SHORTINIT') && SHORTINIT)
844 || (defined('REST_REQUEST') && REST_REQUEST)
845 || (isset($_SERVER['REQUEST_METHOD']) && 'POST' === $_SERVER['REQUEST_METHOD'])
846 || (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && 'xmlhttprequest' === strtolower(sanitize_text_field(wp_unslash($_SERVER['HTTP_X_REQUESTED_WITH']))))
847 || (isset($_SERVER['REQUEST_URI']) && ($is_txt_extension || $is_xml_extension))
848 ) {
849 return true;
850 }
851
852 // phpcs:disable
853 // WordPress.Security.NonceVerification.Recommended -- Using $_GET element only to compare, returns boolean
854 // Thrive plugins and other post_types
855 $arr = array('tve_form_type', 'tve_lead_shortcode', 'tqb_splash');
856 foreach ($arr as $a) {
857 if (isset($_GET['post_type']) && $a === $_GET['post_type']) {
858 return true;
859 }
860 }
861
862 // Thrive architect
863 if (isset($_GET['tve']) && 'true' === $_GET['tve']) return true;
864
865
866 if (is_array($_GET)) {
867 foreach ($_GET as $k => $v) {
868 if (is_string($v) && is_string($k)) {
869 if (false !== stripos($k, 'elementor') || false !== stripos($v, 'elementor')) {
870 return true;
871 }
872 }
873 }
874 }
875
876 // Other _GET parameters
877 if (is_array($_GET)) {
878 $get_params = array_keys($_GET);
879 $excluded_params = array(
880 // customizer preview, visual composer
881 'customize_theme',
882 'preview_id',
883 'preview',
884 // Elementor
885 'elementor-preview',
886 // Divi builder
887 'et_fb',
888 'PageSpeed',
889 // Brizy Editor
890 'brizy-edit',
891 'brizy-edit-iframe',
892 // Beaver builder
893 'fl_builder',
894 'trp-edit-translation',
895 );
896 return (bool) count(array_intersect($excluded_params, $get_params));
897 }
898 // phpcs:enable
899
900 /**
901 * Whether to exclude the content or not from the minifying process.
902 */
903 return (bool) apply_filters('wpo_minify_exclude_contents', false);
904 }
905
906 /**
907 * Get the default files which are ignored / excluded from processing
908 *
909 * @return array
910 */
911 public static function get_default_ignore() {
912 $default_exclusions = array(
913 '/genericons.css',
914 '/Avada/assets/js/main.min.js',
915 '/woocommerce-product-search/js/product-search.js',
916 '/includes/builder/scripts/frontend-builder-scripts.js',
917 '/assets/js/jquery.themepunch.tools.min.js',
918 '/js/TweenMax.min.js',
919 '/jupiter/assets/js/min/full-scripts',
920 '/wp-content/themes/Divi/core/admin/js/react-dom.production.min.js',
921 '/LayerSlider/static/layerslider/js/greensock.js',
922 '/themes/kalium/assets/js/main.min.js',
923 '/wp-includes/js/mediaelement/wp-mediaelement.min.js',
924 'elementor-admin-bar',
925 'pdfjs-dist',
926 'wordpress-popular-posts',
927 'uploads/bb-plugin/cache', // Beaver builder page specific pages
928 );
929 /**
930 * Filters the default exclusions
931 *
932 * @param array The exclusions
933 * @return array
934 */
935 $filtered_exclusions = apply_filters('wp-optimize-minify-default-exclusions', $default_exclusions);
936 return is_array($filtered_exclusions) ? $filtered_exclusions : $default_exclusions;
937 }
938
939 /**
940 * Know files that should always be ignored
941 *
942 * @param array $ignore
943 * @return array
944 */
945 public static function compile_ignore_list($ignore) {
946 $ignore_list = self::get_default_ignore();
947 $user_excluded_ignore_items = wp_optimize_minify_config()->get('ignore_list');
948 if (is_array($user_excluded_ignore_items)) $ignore_list = array_diff($ignore_list, $user_excluded_ignore_items);
949 if (is_array($ignore)) {
950 $master_ignore = array_merge(array_map('strtolower', $ignore), array_map('strtolower', $ignore_list));
951 return array_unique($master_ignore);
952 } else {
953 return $ignore_list;
954 }
955 }
956
957 /**
958 * Get the files excluded for IE compatibility
959 *
960 * @return array
961 */
962 public static function get_default_ie_blacklist() {
963 $default_ie_blacklist = array(
964 '/html5shiv.js',
965 '/html5shiv-printshiv.min.js',
966 '/excanvas.js',
967 '/avada-ie9.js',
968 '/respond.js',
969 '/respond.min.js',
970 '/selectivizr.js',
971 '/Avada/assets/css/ie.css',
972 '/html5.js',
973 '/IE9.js',
974 '/fusion-ie9.js',
975 '/vc_lte_ie9.min.css',
976 '/old-ie.css',
977 '/ie.css',
978 '/vc-ie8.min.css',
979 '/mailchimp-for-wp/assets/js/third-party/placeholders.min.js',
980 '/assets/js/plugins/wp-enqueue/min/webfontloader.js',
981 '/a.optnmstr.com/app/js/api.min.js',
982 '/pixelyoursite/js/public.js',
983 '/assets/js/wcdrip-drip.js',
984 '/instantpage.js',
985 );
986 /**
987 * Filters the default IE specific / blacklisted items
988 *
989 * @param array The blacklist
990 * @return array
991 */
992 $filtered_ie_blacklist = apply_filters('wp-optimize-minify-blacklist', $default_ie_blacklist);
993 return is_array($filtered_ie_blacklist) ? $filtered_ie_blacklist : $default_ie_blacklist;
994 }
995
996 /**
997 * Get the files excluded for IE compatibility
998 *
999 * @return array
1000 */
1001 public static function get_ie_blacklist() {
1002 $user_excluded_blacklist_items = wp_optimize_minify_config()->get('blacklist');
1003 $default_blacklist = self::get_default_ie_blacklist();
1004 if (!is_array($user_excluded_blacklist_items)) return $default_blacklist;
1005 return array_diff($default_blacklist, $user_excluded_blacklist_items);
1006 }
1007
1008 /**
1009 * IE only files that should always be ignored, without incrementing our groups
1010 *
1011 * @param string $url
1012 * @return boolean
1013 */
1014 public static function is_url_in_ie_blacklist($url) {
1015 // from the database
1016 $blacklist = self::get_ie_blacklist();
1017 // must have
1018 $blacklist[] = '/wpo_min/cache/';
1019
1020 // is the url on our list and return
1021 $res = self::in_arrayi($url, $blacklist);
1022 if ($res) {
1023 return true;
1024 } else {
1025 return false;
1026 }
1027 }
1028
1029 /**
1030 * Download function with fallback
1031 *
1032 * @param string $url
1033 * @return boolean|string
1034 */
1035 public static function download_remote($url) {
1036
1037 $args = array(
1038 // info (needed for Google fonts woff files + hinted fonts) as well as to bypass some security filters
1039 'user-agent' => WP_Optimize_Utils::get_user_agent('gfont'),
1040 'timeout' => 7
1041 );
1042
1043 // fetch via WordPress functions
1044 $response = wp_remote_get(
1045 $url,
1046 /**
1047 * Filters the arguments passed to wp_remote_get when downloading the scripts.
1048 *
1049 * @param array $args - The arguments filtered
1050 * @param string $url - The URL of the downloaded asset
1051 * @return array
1052 */
1053 apply_filters('wpo_minify_download_request_args', $args, $url)
1054 );
1055
1056 $res_code = wp_remote_retrieve_response_code($response);
1057 if (200 === $res_code) {
1058 $data = wp_remote_retrieve_body($response);
1059 if (strlen($data) > 1) {
1060 return $data;
1061 }
1062 }
1063
1064 return false;
1065 }
1066
1067 /**
1068 * Prepares the merged JavaScript
1069 *
1070 * @param string $script
1071 * @param string $merged_url
1072 * @return string
1073 */
1074 public static function prepare_merged_js($script, $merged_url) {
1075 $enable_js_trycatch = wp_optimize_minify_config()->get('enable_js_trycatch');
1076 if ($enable_js_trycatch) {
1077 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";
1078 }
1079 return $script;
1080 }
1081
1082 /**
1083 * Checks if URL is a font-awesome resource (checks if it contains font-awesome or fontawesome)
1084 *
1085 * @param string $href
1086 * @return boolean
1087 */
1088 public static function is_font_awesome($href) {
1089 return (bool) preg_match('/font[-_]?awesome/i', $href);
1090 }
1091
1092 /**
1093 * Checks if URL is a Google font resource
1094 *
1095 * @param string $href
1096 * @return boolean
1097 */
1098 public static function is_google_font($href) {
1099 return 'fonts.googleapis.com' === strtolower(wp_parse_url($href, PHP_URL_HOST));
1100 }
1101
1102 /**
1103 * Get the content of an asset, whether local or remote
1104 *
1105 * @param string $url
1106 * @return int|false
1107 */
1108 public static function get_file_size($url) {
1109 $original_url = $url;
1110 if (is_multisite()) {
1111 if (function_exists('get_main_site_id')) {
1112 $site_id = get_main_site_id();
1113 } else {
1114 $network = get_network();
1115 $site_id = $network->site_id;
1116 }
1117 switch_to_blog($site_id);
1118 }
1119 $upload_dir = wp_upload_dir();
1120 $uploads_url = trailingslashit($upload_dir['baseurl']);
1121 $uploads_dir = trailingslashit($upload_dir['basedir']);
1122 if (is_multisite()) {
1123 restore_current_blog();
1124 }
1125 $possible_urls = array(
1126 WP_CONTENT_URL => WP_CONTENT_DIR,
1127 WP_PLUGIN_URL => WP_PLUGIN_DIR,
1128 $uploads_url => $uploads_dir,
1129 get_template_directory_uri() => get_template_directory(),
1130 untrailingslashit(includes_url()) => ABSPATH . WPINC,
1131 );
1132
1133 $file = false;
1134 foreach ($possible_urls as $possible_url => $path) {
1135 $pos = strpos($url, $possible_url);
1136 if (false !== $pos) {
1137 $file = substr_replace($url, $path, $pos, strlen($possible_url));
1138 break;
1139 }
1140 }
1141 if (is_string($file) && file_exists($file)) {
1142 return filesize($file);
1143 }
1144
1145 return self::get_remote_file_size($original_url);
1146 }
1147
1148 /**
1149 * Get the file size of a file hosting in other servers
1150 *
1151 * @param string $url
1152 * @return int|false
1153 */
1154 public static function get_remote_file_size($url) {
1155 $args = array(
1156 // info (needed for Google fonts woff files + hinted fonts) as well as to bypass some security filters
1157 'user-agent' => WP_Optimize_Utils::get_user_agent('gfont'),
1158 'timeout' => 7
1159 );
1160
1161 // fetch via WordPress functions
1162 $response = wp_remote_get($url, $args);
1163
1164 if (is_wp_error($response)) return false;
1165
1166 if (!empty($response) && is_array($response) && isset($response['headers']['Content-Length'])) {
1167 return $response['headers']['Content-Length'];
1168 }
1169
1170 return false;
1171 }
1172
1173 /**
1174 * Check if it is 'flatsome' handle
1175 *
1176 * @param string $handle Handle of enqueued css file
1177 *
1178 * @return bool
1179 */
1180 public static function is_flatsome_handle($handle) {
1181 return 0 === strcmp('flatsome-googlefonts', $handle);
1182 }
1183
1184 /**
1185 * Fix google fonts url for 'flatsome' theme
1186 *
1187 * @param string $href Enqueued google fonts url
1188 *
1189 * @return string Fixed google fonts url
1190 */
1191 public static function fix_flatsome_google_fonts_url($href) {
1192 // Get query from $href
1193 $query = wp_parse_url($href, PHP_URL_QUERY);
1194 $query_arr = explode('&', $query);
1195
1196 // Separate 'family and display' arguments in query
1197 $family = str_replace('family=', '', $query_arr[0]);
1198 $display_type = $query_arr[1];
1199
1200 // Remove empty fonts
1201 $font_families = explode('|', $family);
1202 $font_families = array_diff($font_families, array(':regular', 'initial', 'inherit', ''));
1203
1204 $system_fonts = array('+apple+system', '-apple-system', 'BlinkMacSystemFont', 'Segoe+UI', 'Helvetica+Neue', 'sans+serif', 'sans-serif', 'Georgia', 'Times', 'Times+New+Roman', 'serif');
1205 $google_fonts = array();
1206
1207 // URL may look like following, fix it
1208 // 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
1209 // http://fonts.googleapis.com/css?family=Bellota:regular|:regular|Lato:regular|Creepster:regular&display=block
1210 // http://fonts.googleapis.com/css?family=Lora:regular,regular|initial|Lato:regular,regular|&display=swap
1211
1212 foreach ($font_families as $font) {
1213 $font_variant = explode(':', $font);
1214
1215 // Remove beginning '+'
1216 $font = str_replace(',+', ',', $font_variant[0]);
1217
1218 // Replace '-' with '+'
1219 $font = str_replace(array('-', ' '), '+', $font);
1220
1221 // Remove `"` or '%22'
1222 $font = str_replace(array('%22', '"'), '', $font);
1223 $font_arr = explode(',', $font);
1224 $font_arr = array_diff($font_arr, $system_fonts);
1225 $variant = '';
1226 if (!empty($font_variant[1])) {
1227 $variant = implode(',', array_unique(explode(',', $font_variant[1])));
1228 }
1229 if (empty($variant)) {
1230 $variant = 'regular';
1231 }
1232 foreach ($font_arr as $font) {
1233 $google_fonts[] = $font . ':' . $variant;
1234 }
1235 }
1236 $protocol = is_ssl() ? 'https:' : 'http:';
1237 return $protocol . '//fonts.googleapis.com/css?family=' . implode('|', $google_fonts) . '&' . $display_type;
1238 }
1239
1240 /**
1241 * Get the file modification time
1242 *
1243 * @param string $asset_src
1244 * @return string
1245 */
1246 public static function get_modification_time($asset_src) {
1247 $hurl = self::get_hurl($asset_src);
1248 $abs_file_path = WP_Optimize_Utils::get_file_path($hurl);
1249 if (empty($abs_file_path)) return '';
1250
1251 $modification_time = strval(@filemtime($abs_file_path)); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Suppress E-Warning on failure
1252 $filtered_modification_time = apply_filters('wpo_minify_file_modification_time', $modification_time, $abs_file_path);
1253
1254 return is_string($filtered_modification_time) ? $filtered_modification_time : $modification_time;
1255 }
1256
1257 /**
1258 * When BOM removed code is null (due to unrecognised character encoding), logs error message
1259 *
1260 * @param string $url URL of the script/stylesheet
1261 * @param string|false $encoding Character encoding
1262 * @param string|null $code Script/Stylesheet code
1263 *
1264 * @return void
1265 */
1266 private static function maybe_log_error_message($url, $encoding, $code) {
1267 if (null === $code) {
1268 $message = "Minify: Could not process {$url}, it contains invalid characters. ";
1269 if (false === $encoding) {
1270 $message .= "Could not determine its character encoding.";
1271 }
1272 error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Using for debugging purpose
1273 }
1274 }
1275 }
1276