PluginProbe ʕ •ᴥ•ʔ
10Web Booster – Website speed optimization, Cache & Page Speed optimizer / trunk
10Web Booster – Website speed optimization, Cache & Page Speed optimizer vtrunk
2.33.0 2.30.5 2.30.7 2.30.9 2.31.10 2.31.8 2.32.11 2.32.21 2.32.3 2.32.4 2.32.7 2.6.31 2.6.40 2.6.42 2.6.7 2.7.37 2.7.44 2.7.47 2.8.18 2.8.19 2.8.32 2.8.34 2.8.35 2.9.23 2.9.24 2.9.25 2.9.27 v2.27.4 trunk 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.17 2.0.18 2.0.21 2.0.22 2.0.25 2.0.26 2.0.27 2.0.3 2.0.7 2.0.9 2.10.46 2.10.65 2.10.66 2.10.68 2.11.41 2.11.42 2.11.43 2.12.15 2.12.21 2.12.22 2.12.23 2.12.26 2.13.37 2.13.40 2.13.41 2.13.42 2.13.44 2.13.45 2.13.47 2.14.49 2.14.50 2.15.18 2.17.21 2.17.23 2.18.17 2.19.44 2.19.45 2.19.46 2.19.49 2.2.12 2.2.15 2.2.16 2.2.18 2.2.8 2.20.31 2.20.32 2.20.33 2.21.11 2.21.12 2.21.16 2.21.25 2.22.32 2.23.13 2.23.15 2.23.16 2.23.18 2.24.12 2.24.14 2.24.18 2.25.14 2.26.6 2.28.10 2.28.13 2.28.14 2.28.7 2.29.1 2.29.2 2.29.3 2.3.0 2.3.1 2.3.2 2.3.3 2.30.18
tenweb-speed-optimizer / includes / OptimizerStyles.php
tenweb-speed-optimizer / includes Last commit date
WebPageCache 7 months ago external 2 months ago ExcludeJsFromDelay.php 2 years ago OptimizerBanner.php 11 months ago OptimizerBase.php 2 years ago OptimizerCSSMin.php 2 years ago OptimizerCache.php 1 year ago OptimizerCacheStructure.php 2 years ago OptimizerCriticalCss.php 1 year ago OptimizerElementor.php 2 years ago OptimizerFonts.php 2 years ago OptimizerImages.php 1 year ago OptimizerLogger.php 1 year ago OptimizerMain.php 2 months ago OptimizerOnInit.php 11 months ago OptimizerScripts.php 2 years ago OptimizerSettings.php 2 months ago OptimizerStyles.php 2 years ago OptimizerUrl.php 1 year ago OptimizerUtils.php 2 months ago OptimizerWhiteLabel.php 2 years ago
OptimizerStyles.php
1194 lines
1 <?php
2
3 namespace TenWebOptimizer;
4
5 class OptimizerStyles extends OptimizerBase
6 {
7 const TWO_DELAYED_CSS_ATTRIBUTE = 'data-twodelayedcss';
8
9 const ASSETS_REGEX = '/url\s*\(\s*(?!["\']?data:)(?![\'|\"]?[\#|\%|])([^)]+)\s*\)([^;},\s]*)/i';
10
11 /**
12 * Font-face regex-fu from HamZa at: https://stackoverflow.com/a/21395083
13 * ~
14 *
15 * @font-face\s* # Match @font-face and some spaces
16 * ( # Start group 1
17 * \{ # Match {
18 * (?: # A non-capturing group
19 * [^{}]+ # Match anything except {} one or more times
20 * | # Or
21 * (?1) # Recurse/rerun the expression of group 1
22 * )* # Repeat 0 or more times
23 * \} # Match }
24 * ) # End group 1
25 * ~xs';
26 */
27 const FONT_FACE_REGEX = '~@font-face\s*(\{(?:[^{}]+|(?1))*\})~xsi'; // added `i` flag for case-insensitivity.
28
29 const IMPORT_URL_REGEX = '/@import.*url.*\(.*[\'|"](.*)[\'|"].*\)/Umsi';
30
31 private $css = [];
32
33 private $csscode = [];
34
35 private $url = [];
36
37 private $restofcontent = '';
38
39 private $datauris = false;
40
41 private $hashmap = [];
42
43 private $alreadyminified = false;
44
45 private $aggregate = false;
46
47 private $inline = false;
48
49 private $defer = false;
50
51 private $defer_inline = false;
52
53 private $whitelist = '';
54
55 private $cssinlinesize = '';
56
57 private $cssremovables = [];
58
59 private $cssdisables = [];
60
61 private $include_inline = false;
62
63 private $inject_min_late = '';
64
65 private $dontmove = [];
66
67 private $options = [];
68
69 private $minify_css = true;
70
71 private $current_url = null;
72
73 private $url_data = null;
74
75 private $async_type = 'stylesheet';
76
77 private $font_swap = false;
78
79 private $two_load_fonts_via_webfont = false;
80
81 public $webFont_list = [];
82
83 public $critical = null;
84
85 // public $cdn_url; // Used all over the place implicitly, so will have to be either public or protected :/ .
86 // Reads the page and collects style tags.
87 /**
88 * @var false|mixed|void
89 */
90 private $criticalCss;
91
92 /**
93 * @var array
94 */
95 private $hashes;
96
97 /**
98 * @var OptimizerCacheStructure
99 */
100 private $cacheStructure;
101
102 /**
103 * OptimizerStyles constructor.
104 *
105 * @param string $content
106 * @param OptimizerCacheStructure $cacheStructure
107 */
108 public $two_async_css_arr = [];
109
110 public $two_critical_connection_data = [
111 'critical_css' => false,
112 'critical_fonts' => false
113 ];
114
115 public $critical_fonts_arr = [];
116
117 public $use_uncritical = false;
118
119 private $TwoSettings;
120
121 public function __construct($content, $cacheStructure, $critical = null)
122 {
123 global $TwoSettings;
124 $this->TwoSettings = $TwoSettings;
125 $this->critical = $critical;
126 parent::__construct($content);
127 $this->cacheStructure = $cacheStructure;
128 }
129
130 public function read($options)
131 {
132 $excludeCSS = $options['css_exclude'];
133
134 if ('' !== $excludeCSS) {
135 $this->dontmove = array_filter(array_map('trim', explode(',', $excludeCSS)));
136 } else {
137 $this->dontmove = [];
138 }
139 // forcefully exclude CSS with data-noptimize attrib.
140 $this->dontmove[] = 'data-noptimize';
141 $this->dontmove[] = 'two_critical_bg';
142
143 if ($this->critical->critical_enabled && $this->critical->use_uncritical && $this->critical->status == 'success' && isset($this->critical->uncritical_css)) {
144 $this->use_uncritical = true;
145
146 return;
147 }
148 $this->replaceOptions($options);
149 $this->current_url = OptimizerUtils::get_page_url();
150 $this->url_data = OptimizerUtils::remove_domain_part($this->current_url);
151 $this->font_swap = empty($this->TwoSettings->get_settings('two_async_font')) ? false : true;
152 $this->two_load_fonts_via_webfont = empty($this->TwoSettings->get_settings('two_load_fonts_via_webfont')) ? false : true;
153 $two_disable_css = $this->TwoSettings->get_settings('two_disable_css');
154 $two_disable_css_page = $this->TwoSettings->get_settings('two_disable_page');
155
156 if (is_array($two_disable_css_page) && (isset($two_disable_css_page[$this->url_data]) || isset($two_disable_css_page[$this->current_url]))) {
157 if (isset($two_disable_css) && !empty($two_disable_css)) {
158 if (isset($two_disable_css_page[$this->url_data])) {
159 $two_disable_css .= ',' . $two_disable_css_page[$this->url_data];
160 }
161
162 if (isset($two_disable_css_page[$this->current_url])) {
163 $two_disable_css .= ',' . $two_disable_css_page[$this->current_url];
164 }
165 } else {
166 if (isset($two_disable_css_page[$this->url_data])) {
167 $two_disable_css = $two_disable_css_page[$this->url_data];
168 }
169
170 if (isset($two_disable_css_page[$this->current_url])) {
171 if (isset($two_disable_css) && !empty($two_disable_css)) {
172 $two_disable_css .= ',' . $two_disable_css_page[$this->current_url];
173 } else {
174 $two_disable_css .= $two_disable_css_page[$this->current_url];
175 }
176 }
177 }
178 }
179
180 $two_disable_css = explode(',', $two_disable_css);
181 $this->cssdisables = array_filter($two_disable_css);
182
183 $this->cssinlinesize = 256;
184 // filter to "late inject minified CSS", default to true for now (it is faster).
185 $this->inject_min_late = true;
186
187 // Determine whether we're doing CSS-files aggregation or not.
188 if (isset($options['aggregate'])) {
189 $this->aggregate = $options['aggregate'];
190 }
191
192 // Returning true for "dontaggregate" turns off aggregation.
193 // include inline?
194 if ($options['include_inline'] && $this->aggregate) {
195 $this->include_inline = true;
196 }
197
198 // Should we defer css?
199 // value: true / false.
200 $this->defer = $options['defer'];
201 // Should we inline while deferring?
202 // value: inlined CSS.
203 $this->defer_inline = $options['defer_inline'];
204 // Should we inline?
205 // value: true / false.
206 $this->inline = $options['inline'];
207
208 $this->minify_css = $options['minify_css'];
209
210 // noptimize me.
211 $this->content = $this->hide_noptimize($this->content);
212 // Exclude (no)script, as those may contain CSS which should be left as is.
213 $this->content = $this->replace_contents_with_marker_if_exists('SCRIPT', '<script', '#<(?:no)?script.*?<\/(?:no)?script>#is', $this->content);
214 // Save IE hacks.
215 $this->content = $this->hide_iehacks($this->content);
216 // Hide HTML comments.
217 $this->content = $this->hide_comments($this->content);
218 // Get <style> and <link>.
219 $current_media = 'all';
220
221 if (preg_match_all('#(<style[^>]*>.*</style>)|(<link[^>]*stylesheet[^>]*>)#Usmi', $this->content, $matches)) {
222 foreach ($matches[0] as $tag) {
223 if ($this->isremovable($tag, $this->cssremovables)) {
224 $this->content = str_replace($tag, '', $this->content);
225 $this->cacheStructure->addToTagsToReplace($tag, '');
226 }
227
228 if ($this->is_disable($tag, $this->cssdisables)) {
229 $this->content = str_replace($tag, '', $this->content);
230 $this->cacheStructure->addToTagsToReplace($tag, '');
231 } elseif ($this->ismovable($tag)) {
232 // Get the media.
233 $replace_tag = '';
234
235 if (false !== strpos($tag, 'media=')) {
236 preg_match('#media=(?:"|\')([^>]*)(?:"|\')#Ui', $tag, $medias);
237 $medias = explode(',', $medias[1]);
238 $media = [];
239
240 foreach ($medias as $elem) {
241 if (empty($elem)) {
242 $elem = 'all';
243
244 if ($this->options['async_all']) {
245 $elem = 'all_none';
246 $current_media = 'all';
247 }
248 }
249
250 if ($this->is_async($tag)) {
251 $current_media = $elem;
252 $elem = $elem . '_none';
253 }
254 $media[] = $elem;
255 }
256 } else {
257 // No media specified - applies to all.
258 $media = ['all'];
259
260 if ($this->options['async_all']) {
261 $media = ['all_none'];
262 }
263 }
264
265 if (preg_match('#<link.*href=("|\')(.*)("|\')#Usmi', $tag, $source)) {
266 // <link>.
267 $url = current(explode('?', $source[2], 2));
268 $current_url = $source[2];
269 $path = $this->getpath($url);
270
271 if (false !== $path && preg_match('#\.css$#', $path)) {
272 // Good link.
273 $this->css[md5($path)] = [$media, $path];
274 } else {
275 $new_tag = '';
276
277 if (strpos($source[2], 'fonts.googleapis')) {
278 $font_family = OptimizerUtils::get_url_query($source[2], 'family');
279
280 if (strpos($source[2], 'http') === false && substr($source[2], 0, 2) == '//') {
281 $source[2] = 'https:' . $source[2];
282 }
283
284 if (!$this->is_async($tag) && $this->two_load_fonts_via_webfont && $font_family) {
285 $this->content = str_replace($tag, '', $this->content);
286 $this->cacheStructure->addToTagsToReplace($tag, '');
287 $font_family = explode('|', $font_family);
288
289 foreach ($font_family as $font) {
290 $this->webFont_list[] = $font;
291 }
292 } elseif ($this->font_swap) {
293 $google_fonts_src = OptimizerUtils::replace_google_font_url($source[2]);
294 $current_url = $google_fonts_src;
295 $new_tag = str_replace($source[2], $google_fonts_src, $tag);
296 $this->content = str_replace($tag, $new_tag, $this->content);
297 $this->cacheStructure->addToTagsToReplace($tag, $new_tag);
298 $tag = $new_tag;
299 }
300 } else {
301 $new_tag = $tag;
302 }
303
304 if ($new_tag !== '' && $new_tag !== $tag && !strpos($source[2], 'fonts.googleapis')) {
305 if ($this->is_async($tag)) {
306 $this->two_async_css_arr[] = [
307 'url' => $current_url,
308 'media' => $current_media,
309 'uid' => ''
310 ];
311 $new_tag = '';
312 }
313 $this->content = str_replace($tag, $new_tag, $this->content);
314 $this->cacheStructure->addToTagsToReplace($tag, $new_tag);
315 }
316
317 // Link is dynamic (.php etc).
318 if ($this->is_async($tag)) {
319 $this->two_async_css_arr[] = [
320 'url' => $current_url,
321 'media' => $current_media,
322 'uid' => ''
323 ];
324 $replace_tag = '';
325 } else {
326 $tag = '';
327 }
328 }
329 } else {
330 //optimize inline styles
331 list($originalCode, $code) = $this->optimizeInlineStyle($tag);
332
333 if ($this->include_inline) {
334 $this->css[md5($code)] = [$media, 'INLINE;' . $code];
335 } else {
336 //here we change inline styles code inside <style> tag to optimized one
337 $id_empty_tag = preg_replace('/\s+/', '', $originalCode);
338
339 if (!empty($id_empty_tag)) {
340 $tag = $originalCode;
341 $replace_tag = $code;
342 }
343 }
344 }
345 // Remove the original style tag.
346 $this->content = str_replace($tag, $replace_tag, $this->content, $changesMade);
347 $this->cacheStructure->addToTagsToReplace($tag, $replace_tag);
348 } else {
349 if (preg_match('#<link.*href=("|\')(.*)("|\')#Usmi', $tag, $source)) {
350 $exploded_url = explode('?', $source[2], 2);
351 $url = $exploded_url[0];
352 $path = $this->getpath($url);
353 $new_tag = $tag;
354 // Excluded CSS, minify that file:
355 // -> if aggregate is on and exclude minify is on
356 // -> if aggregate is off and the file is not in dontmove.
357
358 if ($this->is_async($tag)) {
359 $this->two_async_css_arr[] = [
360 'url' => $source[2],
361 'media' => 'all',
362 'uid' => ''
363 ];
364 $new_tag = '';
365 }
366
367 if ($path && $this->minify_css) {
368 $consider_minified_array = false;
369
370 if ((false === $this->aggregate && str_replace($this->dontmove, '', $path) === $path) || (true === $this->aggregate && (false === $consider_minified_array || str_replace($consider_minified_array, '', $path) === $path))) {
371 $minified_url = $this->minify_single($path);
372
373 if (!empty($minified_url)) {
374 // Replace orig URL with cached minified URL.
375 $new_tag = str_replace($url, $minified_url, $tag);
376 }
377 }
378 }
379
380 // And replace
381 if ($new_tag !== '' && $new_tag !== $tag) {
382 $this->content = str_replace($tag, $new_tag, $this->content);
383 $this->cacheStructure->addToTagsToReplace($tag, $new_tag);
384 }
385 } else {
386 //optimize inline styles
387 list($originalCode, $code) = $this->optimizeInlineStyle($tag);
388
389 if ($code !== '' && $code !== $originalCode) {
390 $this->content = str_replace($originalCode, $code, $this->content);
391 $this->cacheStructure->addToTagsToReplace($originalCode, $code);
392 }
393 }
394 }
395 }
396
397 return $this->content;
398 }
399
400 // Really, no styles?
401 return false;
402 }
403
404 /**
405 * Run CSS optimization for code inside style tag and returns array of original and optimized code
406 *
407 * @return array [$originalCode, $optimizedCode]
408 */
409 private function optimizeInlineStyle($tag)
410 {
411 $cssMinifier = new OptimizerCSSMin();
412 // Inline css in style tags can be wrapped in comment tags, so restore comments.
413 $tag = $this->restore_comments($tag);
414 preg_match('#<style.*>(.*)</style>#Usmi', $tag, $code);
415
416 if (empty($code)) {
417 return ['', ''];
418 }
419 $originalCode = $code[1];
420 // And re-hide them to be able to to the removal based on tag.
421 $tag = $this->hide_comments($tag);
422 $code = preg_replace('#^.*<!\[CDATA\[(?:\s*\*/)?(.*)(?://|/\*)\s*?\]\]>.*$#sm', '$1', $code[1]);
423 //run optimizations without minifying
424 $code = $cssMinifier->run($code, false);
425
426 return [$originalCode, $code];
427 }
428
429 private function is_async($tag)
430 {
431 if ($this->options['disable_async']) {
432 return false;
433 }
434
435 if (!$this->ismovable($tag)) {
436 return false;
437 }
438
439 if (is_array($this->options) && isset($this->options['async_all']) && $this->options['async_all']) {
440 return true;
441 }
442 $two_async_css_list = $this->TwoSettings->get_settings('two_async_css');
443 $two_async_page = $this->TwoSettings->get_settings('two_async_page');
444
445 if (is_array($two_async_page) && (isset($two_async_page[$this->url_data]) || isset($two_async_page[$this->current_url]))) {
446 if (isset($two_async_css_list) && !empty($two_async_css_list)) {
447 if (isset($two_async_page[$this->url_data])) {
448 $two_async_css_list .= ',' . $two_async_page[$this->url_data];
449 }
450
451 if (isset($two_async_page[$this->current_url])) {
452 $two_async_css_list .= ',' . $two_async_page[$this->current_url];
453 }
454 } else {
455 if (isset($two_async_page[$this->url_data])) {
456 $two_async_css_list = $two_async_page[$this->url_data];
457 }
458
459 if (isset($two_async_page[$this->current_url])) {
460 if (!isset($two_async_css_list) && !empty($two_async_css_list)) {
461 $two_async_css_list .= ',' . $two_async_page[$this->current_url];
462 } else {
463 $two_async_css_list = $two_async_page[$this->current_url];
464 }
465 }
466 }
467 }
468 $two_async_css = [];
469
470 if (isset($two_async_css_list) && $two_async_css_list != false) {
471 $two_async_css = explode(',', str_replace(' ', '', $two_async_css_list));
472 }
473 $flag = false;
474
475 foreach ($two_async_css as $val) {
476 if ($flag) {
477 break;
478 }
479
480 if (!empty($val)) {
481 $pos = strpos($tag, $val);
482
483 if ($pos !== false) {
484 $flag = true;
485 }
486 }
487 }
488
489 if ($flag) {
490 return true;
491 }
492
493 return false;
494 }
495
496 /**
497 * Given an array of key/value pairs to replace in $string,
498 * it does so by replacing the longest-matching strings first.
499 *
500 * @param string $string
501 * @param array $replacements
502 *
503 * @return string
504 */
505 protected static function replace_longest_matches_first($string, $replacements = [])
506 {
507 if (!empty($replacements)) {
508 // Sort the replacements array by key length in desc order (so that the longest strings are replaced first).
509 $keys = array_map('strlen', array_keys($replacements));
510 array_multisort($keys, SORT_DESC, $replacements);
511 $string = str_replace(array_keys($replacements), array_values($replacements), $string);
512 }
513
514 return $string;
515 }
516
517 public function replace_urls($code = '')
518 {
519 $replacements = [];
520 $code = self::replace_longest_matches_first($code, $replacements);
521
522 return $code;
523 }
524
525 public function hide_fontface_and_maybe_cdn($code)
526 {
527 // Proceed only if @font-face declarations exist within $code.
528 preg_match_all(self::FONT_FACE_REGEX, $code, $fontfaces);
529
530 if (isset($fontfaces[0])) {
531 // Check if we need to cdn fonts or not.
532 $do_font_cdn = false;
533
534 foreach ($fontfaces[0] as $full_match) {
535 // Keep original match so we can search/replace it.
536 $match_search = $full_match;
537
538 // Do font cdn if needed.
539 if ($do_font_cdn) {
540 $full_match = $this->replace_urls($full_match);
541 }
542 // Replace declaration with its base64 encoded string.
543 $replacement = self::build_marker('FONTFACE', $full_match);
544 $code = str_replace($match_search, $replacement, $code);
545 }
546 }
547
548 return $code;
549 }
550
551 /**
552 * Restores original @font-face declarations that have been "hidden"
553 * using `hide_fontface_and_maybe_cdn()`.
554 *
555 * @param string $code
556 *
557 * @return string
558 */
559 public function restore_fontface($code)
560 {
561 return $this->restore_marked_content('FONTFACE', $code);
562 }
563
564 // Re-write (and/or inline) referenced assets.
565 public function rewrite_assets($code, $hashes)
566 {
567 // Handle @font-face rules by hiding and processing them separately.
568 $code = $this->hide_fontface_and_maybe_cdn($code);
569 // Re-write (and/or inline) URLs to point them to the CDN host.
570 $url_src_matches = [];
571 $imgreplace = [];
572 // Matches and captures anything specified within the literal `url()` and excludes those containing data: URIs.
573 preg_match_all(self::ASSETS_REGEX, $code, $url_src_matches);
574
575 $code = self::replace_longest_matches_first($code, $imgreplace);
576 // Replace back font-face markers with actual font-face declarations.
577 $code = $this->restore_fontface($code);
578
579 return $code;
580 }
581
582 // Joins and optimizes CSS.
583 public function optimize()
584 {
585 foreach ($this->css as $styleHash => $group) {
586 list($media, $css) = $group;
587 $cssPath = '';
588
589 if (preg_match('#^INLINE;#', $css)) {
590 // <style>.
591 $css = preg_replace('#^INLINE;#', '', $css);
592 $css = self::fixurls(ABSPATH . 'index.php', $css); // ABSPATH already contains a trailing slash.
593 $this->hashes[] = $styleHash;
594 } else {
595 // <link>
596 if (false !== $css && file_exists($css) && is_readable($css)) {
597 $cssPath = $css;
598 $css = self::fixurls($cssPath, file_get_contents($cssPath)); // phpcs:ignore
599 $css = preg_replace('/\x{EF}\x{BB}\x{BF}/', '', $css);
600
601 if ($this->can_inject_late($cssPath, $css)) {
602 $css = self::build_injectlater_marker($cssPath, md5($css));
603 }
604 $this->hashes[] = $styleHash;
605 } else {
606 // Couldn't read CSS. Maybe getpath isn't working?
607 $css = '';
608 }
609 }
610
611 foreach ($media as $elem) {
612 if (!empty($css)) {
613 if (!empty($elem)) {
614 $css_media = $elem;
615 $pos = strpos($css_media, '_none');
616
617 if ($pos) {
618 $css_media = str_replace('_none', '', $css_media);
619 } else {
620 $elem = 'all';
621 }
622
623 if ($css_media != 'all') {
624 $css = '@media ' . $css_media . '{ ' . $css . ' }';
625 }
626 }
627
628 if (!isset($this->csscode[$elem])) {
629 $this->csscode[$elem] = '';
630 }
631 $this->csscode[$elem] .= "\n\n/*FILESTART " . ($cssPath ? $cssPath : '') . " */\n" . $css;
632 }
633 }
634 }
635 // Check for duplicate code.
636 $md5list = [];
637 $tmpcss = $this->csscode;
638
639 foreach ($tmpcss as $media => $code) {
640 $md5sum = md5($code);
641 $medianame = $media;
642
643 foreach ($md5list as $med => $sum) {
644 // If same code.
645 if ($sum === $md5sum) {
646 // Add the merged code.
647 $medianame = $med . ', ' . $media;
648 $this->csscode[$medianame] = $code;
649 $md5list[$medianame] = $md5list[$med];
650 unset($this->csscode[$med], $this->csscode[$media], $md5list[$med]);
651 }
652 }
653 $md5list[$medianame] = $md5sum;
654 }
655 unset($tmpcss);
656
657 // Manage @imports, while is for recursive import management.
658 foreach ($this->csscode as &$thiscss) {
659 // Flag to trigger import reconstitution and var to hold external imports.
660 $fiximports = false;
661 $external_imports = '';
662 // remove comments to avoid importing commented-out imports.
663 $thiscss_nocomments = preg_replace('#/\*.*\*/#Us', '', $thiscss);
664
665 while (preg_match_all('#@import +(?:url)?(?:(?:\((["\']?)(?:[^"\')]+)\1\)|(["\'])(?:[^"\']+)\2)(?:[^,;"\']+(?:,[^,;"\']+)*)?)(?:;)#mi', $thiscss_nocomments, $matches)) {
666 foreach ($matches[0] as $import) {
667 if ($this->isremovable($import, $this->cssremovables)) {
668 $thiscss = str_replace($import, '', $thiscss);
669 $import_ok = true;
670 } else {
671 $url = trim(preg_replace('#^.*((?:https?:|ftp:)?//.*\.css).*$#', '$1', trim($import)), " \t\n\r\0\x0B\"'");
672 $path = $this->getpath($url);
673 $import_ok = false;
674
675 if (file_exists($path) && is_readable($path)) {
676 $code = addcslashes(self::fixurls($path, file_get_contents($path)), '\\');
677 $code = preg_replace('/\x{EF}\x{BB}\x{BF}/', '', $code);
678 $tmpstyle = $code;
679
680 if (!empty($tmpstyle)) {
681 $code = $tmpstyle;
682 $this->alreadyminified = true;
683 } elseif ($this->can_inject_late($path, $code)) {
684 $code = self::build_injectlater_marker($path, md5($code));
685 }
686
687 if (!empty($code)) {
688 $tmp_thiscss = preg_replace('#(/\*FILESTART\*/.*)' . preg_quote($import, '#') . '#Us', '/*FILESTART2*/' . $code . '$1', $thiscss, -1, $replaceCount);
689
690 if (!empty($tmp_thiscss) && !empty($replaceCount)) {
691 $thiscss = $tmp_thiscss;
692 $import_ok = true;
693 unset($tmp_thiscss);
694 }
695 }
696 unset($code);
697 }
698 }
699
700 if (!$import_ok) {
701 // External imports and general fall-back.
702 $external_imports .= $import;
703 $thiscss = str_replace($import, '', $thiscss);
704 $fiximports = true;
705 }
706 }
707 $thiscss = preg_replace('#/\*FILESTART\*/#', '', $thiscss);
708 $thiscss = preg_replace('#/\*FILESTART2\*/#', '/*FILESTART*/', $thiscss);
709 // and update $thiscss_nocomments before going into next iteration in while loop.
710 $thiscss_nocomments = preg_replace('#/\*.*\*/#Us', '', $thiscss);
711 }
712
713 unset($thiscss_nocomments);
714
715 // Add external imports to top of aggregated CSS.
716 if ($fiximports) {
717 $thiscss = $external_imports . $thiscss;
718 }
719 }
720 unset($thiscss);
721
722 // $this->csscode has all the uncompressed code now.
723 foreach ($this->csscode as &$code) {
724 $hash = md5($code);
725 // Rewrite and/or inline referenced assets.
726 $code = $this->rewrite_assets($code, $this->hashes);
727 // Load Google fonts via webfont
728 $code = $this->get_replace_GoogleFonts($code);
729 // Minify.
730 $code = $this->run_minifier_on($code);
731 // Bring back INJECTLATER stuff.
732 $code = $this->inject_minified($code);
733
734 // Filter results.
735 $tmp_code = $code;
736
737 if (!empty($tmp_code)) {
738 $code = $tmp_code;
739 unset($tmp_code);
740 }
741 $this->hashmap[md5($code)] = $hash;
742 }
743 unset($code);
744
745 return true;
746 }
747
748 /*replace google fonts to empty string for WebFont*/
749 public function get_replace_GoogleFonts($code)
750 {
751 preg_match_all(self::IMPORT_URL_REGEX, $code, $matches, PREG_SET_ORDER, 0);
752
753 if (is_array($matches)) {
754 foreach ($matches as $font_el) {
755 if (isset($font_el[0]) && isset($font_el[1])) {
756 if (filter_var($font_el[1], FILTER_VALIDATE_URL) != false) {
757 $url = $font_el[1];
758 $font_family = OptimizerUtils::get_url_query($url, 'family');
759
760 if ($font_family) {
761 $code = str_replace($font_el[0], '', $code);
762 $font_family = explode('|', $font_family);
763
764 foreach ($font_family as $font) {
765 $this->webFont_list[] = $font;
766 }
767 }
768 }
769 }
770 }
771 }
772
773 return $code;
774 }
775
776 public function run_minifier_on($code)
777 {
778 if (!$this->alreadyminified) {
779 $do_minify = true;
780
781 if ($do_minify) {
782 $cssmin = new OptimizerCSSMin();
783 $tmp_code = trim($cssmin->run($code, $this->minify_css));
784
785 if (!empty($tmp_code)) {
786 $code = $tmp_code;
787 unset($tmp_code);
788 }
789 }
790 }
791
792 return $code;
793 }
794
795 // Caches the CSS in uncompressed, deflated and gzipped form.
796 public function cache()
797 {
798 // CSS cache.
799 foreach ($this->csscode as $media => $code) {
800 $cache = new OptimizerCache(null, 'css', $media);
801 $cache->cache($code, 'text/css');
802 $this->url[$media] = TWO_CACHE_URL . $cache->getname();
803 }
804 }
805
806 // Returns the content.
807 public function getcontent()
808 {
809 if (!empty($this->restofcontent)) {
810 $this->content .= $this->restofcontent;
811 $this->restofcontent = '';
812 }
813 // Inject the new stylesheets.
814 $replaceTag = ['<head', 'after_tag'];
815
816 if ($this->inline) {
817 foreach ($this->csscode as $media => $code) {
818 $this->content = OptimizerUtils::inject_in_html($this->content, '<style type="text/css" media="' . $media . '">' . $code . '</style>', $replaceTag);
819 $this->cacheStructure->addToTagsToAdd('<style type="text/css" media="' . $media . '">' . $code . '</style>', $replaceTag);
820 }
821 } else {
822 foreach ($this->url as $media => $url) {
823 $url = $this->url_replace_cdn($url);
824 $load_none = '';
825 $rel = 'stylesheet';
826
827 $css_href = 'href';
828
829 if ($this->critical->uncritical_load_type === 'on_interaction' && $this->critical->critical_enabled) {
830 $css_href = self::TWO_DELAYED_CSS_ATTRIBUTE;
831 }
832 $two_new_tag = '<link type="text/css" media="' . $media . '" ' . $css_href . '="' . $url . '" rel="' . $rel . '" ' . $load_none . ' />';
833
834 $pos = strpos($media, '_none');
835
836 if ($pos) {
837 $data_media = str_replace('_none', '', $media);
838 $load_none = 'data-two_media="' . $data_media . '" onload="if(media!=\'all\')media=this.getAttribute(\'data-two_media\');"';
839 $media = 'none';
840 $rel = 'stylesheet';
841 $two_new_tag = '';
842 $this->two_async_css_arr[] = [
843 'url' => $url,
844 'media' => $data_media,
845 'uid' => ''
846 ];
847 }
848 $this->content = OptimizerUtils::inject_in_html(
849 $this->content,
850 $two_new_tag,
851 $replaceTag
852 );
853 $this->cacheStructure->addToTagsToAdd(
854 $two_new_tag,
855 $replaceTag
856 );
857 }
858 }
859 $this->content = OptimizerUtils::injectCriticalBg($this->content, $this->critical, $this->cacheStructure);
860
861 if ($this->critical->critical_enabled || $this->critical->critical_font_enabled) {
862 $this->content = $this->injectCriticalCss();
863 }
864 // restore comments.
865 $this->content = $this->restore_comments($this->content);
866 // restore IE hacks.
867 $this->content = $this->restore_iehacks($this->content);
868 // restore (no)script.
869 $this->content = $this->restore_marked_content('SCRIPT', $this->content);
870 // Restore noptimize.
871 $this->content = $this->restore_noptimize($this->content);
872
873 // Return the modified stylesheet.
874 return $this->content;
875 }
876
877 public static function fixurls($file, $code, $asyncAllIsEnabled = false)
878 {
879 // Switch all imports to the url() syntax.
880 $code = preg_replace('#@import ("|\')(.+?)\.css.*?("|\')#', '@import url("${2}.css")', $code);
881
882 if (preg_match_all(self::ASSETS_REGEX, $code, $matches)) {
883 $file = str_replace(WP_ROOT_DIR, '/', $file);
884 $dir = dirname($file); // Like /themes/expound/css.
885 /**
886 * $dir should not contain backslashes, since it's used to replace
887 * urls, but it can contain them when running on Windows because
888 * fixurls() is sometimes called with `ABSPATH . 'index.php'`
889 */
890 $dir = str_replace('\\', '/', $dir);
891 unset($file); // not used below at all.
892 $replace = [];
893
894 foreach ($matches[1] as $k => $url) {
895 // Remove quotes.
896 $old_url = $url;
897 $url = trim($url, " \t\n\r\0\x0B\"'");
898 $noQurl = trim($url, "\"'");
899
900 if ($old_url !== $noQurl) {
901 $removedQuotes = true;
902 } else {
903 $removedQuotes = false;
904 }
905
906 if ('' === $noQurl) {
907 continue;
908 }
909 $url = $noQurl;
910
911 if (preg_match('#^(https?://|ftp://|data:)#i', $url)) {
912 // URL is protocol-relative, host-relative or something we don't touch.
913 continue;
914 } else {
915 if (strpos($url, '//') === 0) {
916 $url_data = wp_parse_url($url);
917
918 if (is_array($url_data) && isset($url_data['host'])) {
919 if (strpos($url, '//' . $url_data['host']) == 0) {
920 $newurl = str_replace('//' . $url_data['host'], TWO_WP_ROOT_URL, $url);
921 } else {
922 $newurl = str_replace('//', TWO_WP_ROOT_URL, $url);
923 }
924 } else {
925 continue;
926 }
927 } elseif (strpos($url, '/') === 0) {
928 $newurl = TWO_WP_ROOT_URL . $url;
929 } else {
930 $newurl = str_replace(' ', '%20', TWO_WP_ROOT_URL . str_replace('//', '/', $dir . '/' . $url));
931 }
932 /**
933 * Hash the url + whatever was behind potentially for replacement
934 * We must do this, or different css classes referencing the same bg image (but
935 * different parts of it, say, in sprites and such) loose their stuff...
936 */
937 $hash = md5($url . $matches[2][$k]);
938 $code = str_replace($matches[0][$k], $hash, $code);
939
940 if ($removedQuotes) {
941 $replace[$hash] = "url('" . $newurl . "')" . $matches[2][$k];
942 } else {
943 $replace[$hash] = 'url(' . $newurl . ')' . $matches[2][$k];
944 }
945 }
946 }
947 $code = self::replace_longest_matches_first($code, $replace);
948 }
949
950 return $code;
951 }
952
953 private function ismovable($tag)
954 {
955 if (!$this->aggregate) {
956 return false;
957 }
958
959 if (!empty($this->whitelist)) {
960 foreach ($this->whitelist as $match) {
961 if (false !== strpos($tag, $match)) {
962 return true;
963 }
964 }
965
966 // no match with whitelist.
967 return false;
968 } else {
969 if (is_array($this->dontmove) && !empty($this->dontmove)) {
970 foreach ($this->dontmove as $match) {
971 if (false !== strpos($tag, $match)) {
972 // Matched something.
973 return false;
974 }
975 }
976 }
977
978 // If we're here it's safe to move.
979 return true;
980 }
981 }
982
983 private function can_inject_late($cssPath, $css)
984 {
985 $consider_minified_array = false;
986
987 if (true !== $this->inject_min_late) {
988 // late-inject turned off.
989 return false;
990 } elseif ((false === strpos($cssPath, 'min.css')) && (str_replace($consider_minified_array, '', $cssPath) === $cssPath)) {
991 // file not minified based on filename & filter.
992 return false;
993 } elseif (false !== strpos($css, '@import')) {
994 // can't late-inject files with imports as those need to be aggregated.
995 return false;
996 } elseif (preg_match('#background[^;}]*url\(#Ui', $css)) {
997 // don't late-inject CSS with images if CDN is set OR if image inlining is on.
998 return false;
999 } else {
1000 // phew, all is safe, we can late-inject.
1001 return true;
1002 }
1003 }
1004
1005 /**
1006 * Minifies (and cdn-replaces) a single local css file
1007 * and returns its (cached) url.
1008 *
1009 * @param string $filepath filepath
1010 * @param bool $cache_miss Optional. Force a cache miss. Default false.
1011 *
1012 * @return bool|string url pointing to the minified css file or false
1013 */
1014 public function minify_single($filepath, $cache_miss = false)
1015 {
1016 $contents = $this->prepare_minify_single($filepath);
1017
1018 if (empty($contents)) {
1019 return false;
1020 }
1021 // Check cache.
1022 $name_prefix = 'minified_' . str_replace('.css', '', basename($filepath));
1023 $cache = new OptimizerCache(null, 'css', 'all', $name_prefix);
1024 // Fixurls...
1025 $contents = self::fixurls($filepath, $contents);
1026 // CDN-replace any referenced assets if needed...
1027 $contents = $this->replace_urls($contents);
1028 // Now minify...
1029 $cssmin = new OptimizerCSSMin();
1030 $contents = trim($cssmin->run($contents, $this->minify_css));
1031 // Store in cache.
1032 $contents = $this->get_replace_GoogleFonts($contents);
1033 $cache->cache($contents, 'text/css');
1034 $url = $this->build_minify_single_url($cache);
1035
1036 return $url;
1037 }
1038
1039 public function replaceOptions($options)
1040 {
1041 $this->options = $options;
1042 }
1043
1044 private function is_disable($tag, $disables_css)
1045 {
1046 foreach ($disables_css as $match) {
1047 if (false !== strpos($tag, $match)) {
1048 return true;
1049 }
1050 }
1051
1052 return false;
1053 }
1054
1055 /**
1056 * Inject criticalCss that we placed in admin
1057 *
1058 * @return string
1059 */
1060 private function injectCriticalCss()
1061 {
1062 if (isset($_GET['no_critical_css']) && $_GET['no_critical_css'] == 1) { // phpcs:ignore
1063 return $this->content;
1064 }
1065
1066 if (isset($this->critical->critical_css) && isset($this->critical->status) && $this->critical->status == 'success') {
1067 $file_url = TWO_CACHE_URL . 'critical/' . $this->critical->critical_css;
1068 $file_dir = TWO_CACHE_DIR . 'critical/' . $this->critical->critical_css;
1069
1070 if (file_exists($file_dir)) {
1071 $critical_content = file_get_contents($file_dir); // phpcs:ignore
1072 $critical_content = OptimizerUtils::replace_bg($critical_content);
1073
1074 if (!empty($critical_content)) {
1075 $critical_styles = '<style class="two_critical_css"' . ('true' == $this->TwoSettings->get_settings('two_critical_remove') ? ' id="two_critical_css"' : '') . ' type="text/css">' . $critical_content . '</style>';
1076 }
1077 }
1078
1079 if (isset($critical_styles)) {
1080 $this->two_critical_connection_data['critical_css'] = true;
1081
1082 $init_uncritical = false;
1083
1084 if ($this->use_uncritical && isset($this->critical->uncritical_css) && isset($this->critical->critical_fonts)) {
1085 $this->two_async_css_arr = [];
1086 $this->two_async_css_arr[] = [
1087 'url' => TWO_CACHE_URL . 'critical/' . $this->critical->uncritical_css,
1088 'media' => 'all',
1089 'uid' => ''
1090 ];
1091 $this->critical_fonts_arr = $this->critical->critical_fonts;
1092 $init_uncritical = true;
1093 }
1094
1095 $critical_font_css = '';
1096
1097 if (isset($this->critical->critical_fonts) && is_array($this->critical->critical_fonts) && !$init_uncritical) {
1098 $this->two_critical_connection_data['critical_fonts'] = true;
1099
1100 foreach ($this->critical->critical_fonts as $critical_font) {
1101 if (isset($critical_font->font_face)) {
1102 $critical_font_css .= ' ' . $critical_font->font_face;
1103 }
1104 }
1105 }
1106
1107 if (empty($critical_font_css)) {
1108 $this->two_critical_connection_data['critical_fonts'] = false;
1109 }
1110
1111 if (!empty($this->TwoSettings->get_settings('two_async_font'))) {
1112 $critical_font_css = $this->addAttrDisplaySwap($critical_font_css);
1113 }
1114 $critical_font_css = '<style class="two_critical_font_css" type="text/css">' . $critical_font_css . '</style>';
1115
1116 if ($this->critical->uncritical_load_type === 'not_load' || $init_uncritical) {
1117 if (preg_match_all('#(<style[^>]*>.*</style>)|(<link[^>]*stylesheet[^>]*>)#Usmi', $this->content, $matches)) {
1118 foreach ($matches[0] as $tag) {
1119 if (is_array($this->dontmove) && !empty($this->dontmove)) {
1120 foreach ($this->dontmove as $ex_el) {
1121 if (false !== strpos($tag, $ex_el)) {
1122 continue 2;
1123 }
1124 }
1125 }
1126 $this->content = str_replace($tag, '', $this->content);
1127 $this->cacheStructure->addToTagsToReplace($tag, '');
1128 }
1129 }
1130 }
1131
1132 if ($this->critical->critical_enabled) {
1133 $this->content = OptimizerUtils::inject_in_html($this->content, $critical_styles, ['</head>', 'before']);
1134 }
1135
1136 if ($this->critical->critical_font_enabled) {
1137 $this->content = OptimizerUtils::inject_in_html($this->content, $critical_font_css, ['</head>', 'before']);
1138 }
1139
1140 if ($this->critical->critical_enabled) {
1141 $this->cacheStructure->addToTagsToAdd($critical_styles, ['</head>', 'before']);
1142 }
1143
1144 if ($this->critical->critical_font_enabled) {
1145 $this->cacheStructure->addToTagsToAdd($critical_font_css, ['</head>', 'before']);
1146 }
1147 }
1148 }
1149
1150 return $this->content;
1151 }
1152
1153 private function addAttrDisplaySwap($critical_font_css)
1154 {
1155 $critical_font_css = trim($critical_font_css);
1156 $allFontFaces = explode('@font-face', $critical_font_css);
1157 $sepFontFaces = [];
1158
1159 foreach ($allFontFaces as $fontFace) {
1160 $fontFace = trim($fontFace);
1161
1162 if (empty($fontFace)) {
1163 continue;
1164 }
1165 $positionStart = strpos($fontFace, 'font-display:');
1166
1167 if (!$positionStart) {
1168 $mid = strpos($fontFace, '}');
1169 $fontFace = substr_replace($fontFace, ';font-display: swap}', $mid);
1170 } else {
1171 $mid = substr($fontFace, $positionStart + 13);
1172 $positionEnd = strpos($mid, ';');
1173
1174 if (!$positionEnd) {
1175 $positionEnd = strpos($mid, '}');
1176 }
1177 $fontDisplay = substr($mid, 0, $positionEnd);
1178
1179 if ($fontDisplay !== 'swap') {
1180 $fontFace = str_replace($fontDisplay, 'swap', $fontFace);
1181 }
1182 }
1183 $sepFontFaces[] = $fontFace;
1184 }
1185 $allFontFaces = implode('@font-face', $sepFontFaces);
1186
1187 if (!empty($allFontFaces)) {
1188 $allFontFaces = '@font-face' . $allFontFaces;
1189 }
1190
1191 return $allFontFaces;
1192 }
1193 }
1194