PluginProbe
Autoptimize / 2.5.0
Autoptimize v2.5.0
2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 All 107 releases
autoptimize / classes / autoptimizeBase.php

autoptimizeBase.php in Autoptimize 2.5.0, at classes/autoptimizeBase.php

704 lines 22.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Base class other (more-specific) classes inherit from.
4 */
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit;
8 }
9
10 abstract class autoptimizeBase
11 {
12 /**
13 * Holds content being processed (html, scripts, styles)
14 *
15 * @var string
16 */
17 protected $content = '';
18
19 /**
20 * Controls debug logging.
21 *
22 * @var bool
23 */
24 public $debug_log = false;
25
26 public function __construct( $content )
27 {
28 $this->content = $content;
29 }
30
31 /**
32 * Reads the page and collects tags.
33 *
34 * @param array $options Options.
35 *
36 * @return bool
37 */
38 abstract public function read( $options );
39
40 /**
41 * Joins and optimizes collected things.
42 *
43 * @return bool
44 */
45 abstract public function minify();
46
47 /**
48 * Caches the things.
49 *
50 * @return void
51 */
52 abstract public function cache();
53
54 /**
55 * Returns the content
56 *
57 * @return string
58 */
59 abstract public function getcontent();
60
61 /**
62 * Tranfsorms a given URL to a full local filepath if possible.
63 * Returns local filepath or false.
64 *
65 * @param string $url URL to transform.
66 *
67 * @return bool|string
68 */
69 public function getpath( $url )
70 {
71 $url = apply_filters( 'autoptimize_filter_cssjs_alter_url', $url );
72
73 if ( false !== strpos( $url, '%' ) ) {
74 $url = urldecode( $url );
75 }
76
77 $site_host = parse_url( AUTOPTIMIZE_WP_SITE_URL, PHP_URL_HOST );
78 $content_host = parse_url( AUTOPTIMIZE_WP_ROOT_URL, PHP_URL_HOST );
79
80 // Normalizing attempts...
81 $double_slash_position = strpos( $url, '//' );
82 if ( 0 === $double_slash_position ) {
83 if ( is_ssl() ) {
84 $url = 'https:' . $url;
85 } else {
86 $url = 'http:' . $url;
87 }
88 } elseif ( ( false === $double_slash_position ) && ( false === strpos( $url, $site_host ) ) ) {
89 if ( AUTOPTIMIZE_WP_SITE_URL === $site_host ) {
90 $url = AUTOPTIMIZE_WP_SITE_URL . $url;
91 } else {
92 $url = AUTOPTIMIZE_WP_SITE_URL . autoptimizeUtils::path_canonicalize( $url );
93 }
94 }
95
96 if ( $site_host !== $content_host ) {
97 $url = str_replace( AUTOPTIMIZE_WP_CONTENT_URL, AUTOPTIMIZE_WP_SITE_URL . AUTOPTIMIZE_WP_CONTENT_NAME, $url );
98 }
99
100 // First check; hostname wp site should be hostname of url!
101 $url_host = @parse_url( $url, PHP_URL_HOST ); // @codingStandardsIgnoreLine
102 if ( $url_host !== $site_host ) {
103 /**
104 * First try to get all domains from WPML (if available)
105 * then explicitely declare $this->cdn_url as OK as well
106 * then apply own filter autoptimize_filter_cssjs_multidomain takes an array of hostnames
107 * each item in that array will be considered part of the same WP multisite installation
108 */
109 $multidomains = array();
110
111 $multidomains_wpml = apply_filters( 'wpml_setting', array(), 'language_domains' );
112 if ( ! empty( $multidomains_wpml ) ) {
113 $multidomains = array_map( array( $this, 'get_url_hostname' ), $multidomains_wpml );
114 }
115
116 if ( ! empty( $this->cdn_url ) ) {
117 $multidomains[] = parse_url( $this->cdn_url, PHP_URL_HOST );
118 }
119
120 $multidomains = apply_filters( 'autoptimize_filter_cssjs_multidomain', $multidomains );
121
122 if ( ! empty( $multidomains ) ) {
123 if ( in_array( $url_host, $multidomains ) ) {
124 $url = str_replace( $url_host, $site_host, $url );
125 } else {
126 return false;
127 }
128 } else {
129 return false;
130 }
131 }
132
133 // Try to remove "wp root url" from url while not minding http<>https.
134 $tmp_ao_root = preg_replace( '/https?:/', '', AUTOPTIMIZE_WP_ROOT_URL );
135
136 if ( $site_host !== $content_host ) {
137 // As we replaced the content-domain with the site-domain, we should match against that.
138 $tmp_ao_root = preg_replace( '/https?:/', '', AUTOPTIMIZE_WP_SITE_URL );
139 }
140
141 $tmp_url = preg_replace( '/https?:/', '', $url );
142 $path = str_replace( $tmp_ao_root, '', $tmp_url );
143
144 // If path starts with :// or //, this is not a URL in the WP context and
145 // we have to assume we can't aggregate.
146 if ( preg_match( '#^:?//#', $path ) ) {
147 // External script/css (adsense, etc).
148 return false;
149 }
150
151 // Prepend with WP_ROOT_DIR to have full path to file.
152 $path = str_replace( '//', '/', WP_ROOT_DIR . $path );
153
154 // Final check: does file exist and is it readable?
155 if ( file_exists( $path ) && is_file( $path ) && is_readable( $path ) ) {
156 return $path;
157 } else {
158 return false;
159 }
160 }
161
162 /**
163 * Returns the hostname part of a given $url if we're able to parse it.
164 * If not, it returns the original url (prefixed with http:// scheme in case
165 * it was missing).
166 * Used as callback for WPML multidomains filter.
167 *
168 * @param string $url URL.
169 *
170 * @return string
171 */
172 protected function get_url_hostname( $url )
173 {
174 // Checking that the url starts with something vaguely resembling a protocol.
175 if ( ( 0 !== strpos( $url, 'http' ) ) && ( 0 !== strpos( $url, '//' ) ) ) {
176 $url = 'http://' . $url;
177 }
178
179 // Grab the hostname.
180 $hostname = parse_url( $url, PHP_URL_HOST );
181
182 // Fallback when parse_url() fails.
183 if ( empty( $hostname ) ) {
184 $hostname = $url;
185 }
186
187 return $hostname;
188 }
189
190 /**
191 * Hides everything between noptimize-comment tags.
192 *
193 * @param string $markup Markup to process.
194 *
195 * @return string
196 */
197 protected function hide_noptimize( $markup )
198 {
199 return $this->replace_contents_with_marker_if_exists(
200 'NOPTIMIZE',
201 '/<!--\s?noptimize\s?-->/',
202 '#<!--\s?noptimize\s?-->.*?<!--\s?/\s?noptimize\s?-->#is',
203 $markup
204 );
205 }
206
207 /**
208 * Unhide noptimize-tags.
209 *
210 * @param string $markup Markup to process.
211 *
212 * @return string
213 */
214 protected function restore_noptimize( $markup )
215 {
216 return $this->restore_marked_content( 'NOPTIMIZE', $markup );
217 }
218
219 /**
220 * Hides "iehacks" content.
221 *
222 * @param string $markup Markup to process.
223 *
224 * @return string
225 */
226 protected function hide_iehacks( $markup )
227 {
228 return $this->replace_contents_with_marker_if_exists(
229 'IEHACK', // Marker name...
230 '<!--[if', // Invalid regex, will fallback to search using strpos()...
231 '#<!--\[if.*?\[endif\]-->#is', // Replacement regex...
232 $markup
233 );
234 }
235
236 /**
237 * Restores "hidden" iehacks content.
238 *
239 * @param string $markup Markup to process.
240 *
241 * @return string
242 */
243 protected function restore_iehacks( $markup )
244 {
245 return $this->restore_marked_content( 'IEHACK', $markup );
246 }
247
248 /**
249 * "Hides" content within HTML comments using a regex-based replacement
250 * if HTML comment markers are found.
251 * `<!--example-->` becomes `%%COMMENTS%%ZXhhbXBsZQ==%%COMMENTS%%`
252 *
253 * @param string $markup Markup to process.
254 *
255 * @return string
256 */
257 protected function hide_comments( $markup )
258 {
259 return $this->replace_contents_with_marker_if_exists(
260 'COMMENTS',
261 '<!--',
262 '#<!--.*?-->#is',
263 $markup
264 );
265 }
266
267 /**
268 * Restores original HTML comment markers inside a string whose HTML
269 * comments have been "hidden" by using `hide_comments()`.
270 *
271 * @param string $markup Markup to process.
272 *
273 * @return string
274 */
275 protected function restore_comments( $markup )
276 {
277 return $this->restore_marked_content( 'COMMENTS', $markup );
278 }
279
280 /**
281 * Replaces the given URL with the CDN-version of it when CDN replacement
282 * is supposed to be done.
283 *
284 * @param string $url URL to process.
285 *
286 * @return string
287 */
288 public function url_replace_cdn( $url )
289 {
290 // For 2.3 back-compat in which cdn-ing appeared to be automatically
291 // including WP subfolder/subdirectory into account as part of cdn-ing,
292 // even though it might've caused serious troubles in certain edge-cases.
293 $cdn_url = autoptimizeUtils::tweak_cdn_url_if_needed( $this->cdn_url );
294
295 // Allows API/filter to further tweak the cdn url...
296 $cdn_url = apply_filters( 'autoptimize_filter_base_cdnurl', $cdn_url );
297 if ( ! empty( $cdn_url ) ) {
298 $this->debug_log( 'before=' . $url );
299
300 // Simple str_replace-based approach fails when $url is protocol-or-host-relative.
301 $is_protocol_relative = autoptimizeUtils::is_protocol_relative( $url );
302 $is_host_relative = ( ! $is_protocol_relative && ( '/' === $url{0} ) );
303 $cdn_url = rtrim( $cdn_url, '/' );
304
305 if ( $is_host_relative ) {
306 // Prepending host-relative urls with the cdn url.
307 $url = $cdn_url . $url;
308 } else {
309 // Either a protocol-relative or "regular" url, replacing it either way.
310 if ( $is_protocol_relative ) {
311 // Massage $site_url so that simple str_replace() still "works" by
312 // searching for the protocol-relative version of AUTOPTIMIZE_WP_SITE_URL.
313 $site_url = str_replace( array( 'http:', 'https:' ), '', AUTOPTIMIZE_WP_SITE_URL );
314 } else {
315 $site_url = AUTOPTIMIZE_WP_SITE_URL;
316 }
317 $this->debug_log( '`' . $site_url . '` -> `' . $cdn_url . '` in `' . $url . '`' );
318 $url = str_replace( $site_url, $cdn_url, $url );
319 }
320
321 $this->debug_log( 'after=' . $url );
322 }
323
324 // Allow API filter to take further care of CDN replacement.
325 $url = apply_filters( 'autoptimize_filter_base_replace_cdn', $url );
326
327 return $url;
328 }
329
330 /**
331 * Injects/replaces the given payload markup into `$this->content`
332 * at the specified location.
333 * If the specified tag cannot be found, the payload is appended into
334 * $this->content along with a warning wrapped inside <!--noptimize--> tags.
335 *
336 * @param string $payload Markup to inject.
337 * @param array $where Array specifying the tag name and method of injection.
338 * Index 0 is the tag name (i.e., `</body>`).
339 * Index 1 specifies Ë›'before', 'after' or 'replace'. Defaults to 'before'.
340 *
341 * @return void
342 */
343 protected function inject_in_html( $payload, $where )
344 {
345 $warned = false;
346 $position = autoptimizeUtils::strpos( $this->content, $where[0] );
347 if ( false !== $position ) {
348 // Found the tag, setup content/injection as specified.
349 if ( 'after' === $where[1] ) {
350 $content = $where[0] . $payload;
351 } elseif ( 'replace' === $where[1] ) {
352 $content = $payload;
353 } else {
354 $content = $payload . $where[0];
355 }
356 // Place where specified.
357 $this->content = autoptimizeUtils::substr_replace(
358 $this->content,
359 $content,
360 $position,
361 // Using plain strlen() should be safe here for now, since
362 // we're not searching for multibyte chars here still...
363 strlen( $where[0] )
364 );
365 } else {
366 // Couldn't find what was specified, just append and add a warning.
367 $this->content .= $payload;
368 if ( ! $warned ) {
369 $tag_display = str_replace( array( '<', '>' ), '', $where[0] );
370 $this->content .= '<!--noptimize--><!-- Autoptimize found a problem with the HTML in your Theme, tag `' . $tag_display . '` missing --><!--/noptimize-->';
371 $warned = true;
372 }
373 }
374 }
375
376 /**
377 * Returns true if given `$tag` is found in the list of `$removables`.
378 *
379 * @param string $tag Tag to search for.
380 * @param array $removables List of things considered completely removable.
381 *
382 * @return bool
383 */
384 protected function isremovable( $tag, $removables )
385 {
386 foreach ( $removables as $match ) {
387 if ( false !== strpos( $tag, $match ) ) {
388 return true;
389 }
390 }
391
392 return false;
393 }
394
395 /**
396 * Callback used in `self::inject_minified()`.
397 *
398 * @param array $matches Regex matches.
399 *
400 * @return string
401 */
402 public function inject_minified_callback( $matches )
403 {
404 static $conf = null;
405 if ( null === $conf ) {
406 $conf = autoptimizeConfig::instance();
407 }
408
409 /**
410 * $matches[1] holds the whole match caught by regex in self::inject_minified(),
411 * so we take that and split the string on `|`.
412 * First element is the filepath, second is the md5 hash of contents
413 * the filepath had when it was being processed.
414 * If we don't have those, we'll bail out early.
415 */
416 $filepath = null;
417 $filehash = null;
418
419 // Grab the parts we need.
420 $parts = explode( '|', $matches[1] );
421 if ( ! empty( $parts ) ) {
422 $filepath = isset( $parts[0] ) ? base64_decode( $parts[0] ) : null;
423 $filehash = isset( $parts[1] ) ? $parts[1] : null;
424 }
425
426 // Bail early if something's not right...
427 if ( ! $filepath || ! $filehash ) {
428 return "\n";
429 }
430
431 $filecontent = file_get_contents( $filepath );
432
433 // Some things are differently handled for css/js...
434 $is_js_file = ( '.js' === substr( $filepath, -3, 3 ) );
435
436 $is_css_file = false;
437 if ( ! $is_js_file ) {
438 $is_css_file = ( '.css' === substr( $filepath, -4, 4 ) );
439 }
440
441 // BOMs being nuked here unconditionally (regardless of where they are)!
442 $filecontent = preg_replace( "#\x{EF}\x{BB}\x{BF}#", '', $filecontent );
443
444 // Remove comments and blank lines.
445 if ( $is_js_file ) {
446 $filecontent = preg_replace( '#^\s*\/\/.*$#Um', '', $filecontent );
447 }
448
449 // Nuke un-important comments.
450 $filecontent = preg_replace( '#^\s*\/\*[^!].*\*\/\s?#Um', '', $filecontent );
451
452 // Normalize newlines.
453 $filecontent = preg_replace( '#(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+#', "\n", $filecontent );
454
455 // JS specifics.
456 if ( $is_js_file ) {
457 // Append a semicolon at the end of js files if it's missing.
458 $last_char = substr( $filecontent, -1, 1 );
459 if ( ';' !== $last_char && '}' !== $last_char ) {
460 $filecontent .= ';';
461 }
462 // Check if try/catch should be used.
463 $opt_js_try_catch = $conf->get( 'autoptimize_js_trycatch' );
464 if ( 'on' === $opt_js_try_catch ) {
465 // It should, wrap in try/catch.
466 $filecontent = 'try{' . $filecontent . '}catch(e){}';
467 }
468 } elseif ( $is_css_file ) {
469 $filecontent = autoptimizeStyles::fixurls( $filepath, $filecontent );
470 } else {
471 $filecontent = '';
472 }
473
474 // Return modified (or empty!) code/content.
475 return "\n" . $filecontent;
476 }
477
478 /**
479 * Inject already minified code in optimized JS/CSS.
480 *
481 * @param string $in Markup.
482 *
483 * @return string
484 */
485 protected function inject_minified( $in )
486 {
487 $out = $in;
488 if ( false !== strpos( $in, '%%INJECTLATER%%' ) ) {
489 $out = preg_replace_callback(
490 '#\/\*\!%%INJECTLATER' . AUTOPTIMIZE_HASH . '%%(.*?)%%INJECTLATER%%\*\/#is',
491 array( $this, 'inject_minified_callback' ),
492 $in
493 );
494 }
495
496 return $out;
497 }
498
499 /**
500 * Specialized method to create the INJECTLATER marker.
501 * These are somewhat "special", in the sense that they're additionally wrapped
502 * within an "exclamation mark style" comment, so that they're not stripped
503 * out by minifiers.
504 * They also currently contain the hash of the file's contents too (unlike other markers).
505 *
506 * @param string $filepath Filepath.
507 * @param string $hash Hash.
508 *
509 * @return string
510 */
511 public static function build_injectlater_marker( $filepath, $hash )
512 {
513 $contents = '/*!' . self::build_marker( 'INJECTLATER', $filepath, $hash ) . '*/';
514
515 return $contents;
516 }
517
518 /**
519 * Creates and returns a `%%`-style named marker which holds
520 * the base64 encoded `$data`.
521 * If `$hash` is provided, it's appended to the base64 encoded string
522 * using `|` as the separator (in order to support building the
523 * somewhat special/different INJECTLATER marker).
524 *
525 * @param string $name Marker name.
526 * @param string $data Marker data which will be base64-encoded.
527 * @param string|null $hash Optional.
528 *
529 * @return string
530 */
531 public static function build_marker( $name, $data, $hash = null )
532 {
533 // Start the marker, add the data.
534 $marker = '%%' . $name . AUTOPTIMIZE_HASH . '%%' . base64_encode( $data );
535
536 // Add the hash if provided.
537 if ( null !== $hash ) {
538 $marker .= '|' . $hash;
539 }
540
541 // Close the marker.
542 $marker .= '%%' . $name . '%%';
543
544 return $marker;
545 }
546
547 /**
548 * Searches for `$search` in `$content` (using either `preg_match()`
549 * or `strpos()`, depending on whether `$search` is a valid regex pattern or not).
550 * If something is found, it replaces `$content` using `$re_replace_pattern`,
551 * effectively creating our named markers (`%%{$marker}%%`.
552 * These are then at some point replaced back to their actual/original/modified
553 * contents using `autoptimizeBase::restore_marked_content()`.
554 *
555 * @param string $marker Marker name (without percent characters).
556 * @param string $search A string or full blown regex pattern to search for in $content. Uses `strpos()` or `preg_match()`.
557 * @param string $re_replace_pattern Regex pattern to use when replacing contents.
558 * @param string $content Content to work on.
559 *
560 * @return string
561 */
562 public static function replace_contents_with_marker_if_exists( $marker, $search, $re_replace_pattern, $content )
563 {
564 $found = false;
565
566 $is_regex = autoptimizeUtils::str_is_valid_regex( $search );
567 if ( $is_regex ) {
568 $found = preg_match( $search, $content );
569 } else {
570 $found = ( false !== strpos( $content, $search ) );
571 }
572
573 if ( $found ) {
574 $content = preg_replace_callback(
575 $re_replace_pattern,
576 function( $matches ) use ( $marker ) {
577 return autoptimizeBase::build_marker( $marker, $matches[0] );
578 },
579 $content
580 );
581 }
582
583 return $content;
584 }
585
586 /**
587 * Complements `autoptimizeBase::replace_contents_with_marker_if_exists()`.
588 *
589 * @param string $marker Marker.
590 * @param string $content Markup.
591 *
592 * @return string
593 */
594 public static function restore_marked_content( $marker, $content )
595 {
596 if ( false !== strpos( $content, $marker ) ) {
597 $content = preg_replace_callback(
598 '#%%' . $marker . AUTOPTIMIZE_HASH . '%%(.*?)%%' . $marker . '%%#is',
599 function ( $matches ) {
600 return base64_decode( $matches[1] );
601 },
602 $content
603 );
604 }
605
606 return $content;
607 }
608
609 /**
610 * Logs given `$data` for debugging purposes (when debug logging is on).
611 *
612 * @param mixed $data Data to log.
613 *
614 * @return void
615 */
616 protected function debug_log( $data )
617 {
618 if ( ! isset( $this->debug_log ) || ! $this->debug_log ) {
619 return;
620 }
621
622 if ( ! is_string( $data ) && ! is_resource( $data ) ) {
623 $data = var_export( $data, true );
624 }
625
626 error_log( $data );
627 }
628
629 /**
630 * Checks if a single local css/js file can be minified and returns source if so.
631 *
632 * @param string $filepath Filepath.
633 *
634 * @return bool|string to be minified code or false.
635 */
636 protected function prepare_minify_single( $filepath )
637 {
638 // Decide what we're dealing with, return false if we don't know.
639 if ( $this->str_ends_in( $filepath, '.js' ) ) {
640 $type = 'js';
641 } elseif ( $this->str_ends_in( $filepath, '.css' ) ) {
642 $type = 'css';
643 } else {
644 return false;
645 }
646
647 // Bail if it looks like its already minifed (by having -min or .min
648 // in filename) or if it looks like WP jquery.js (which is minified).
649 $minified_variants = array(
650 '-min.' . $type,
651 '.min.' . $type,
652 'js/jquery/jquery.js',
653 );
654 foreach ( $minified_variants as $ending ) {
655 if ( $this->str_ends_in( $filepath, $ending ) ) {
656 return false;
657 }
658 }
659
660 // Get file contents, bail if empty.
661 $contents = file_get_contents( $filepath );
662
663 return $contents;
664 }
665
666 /**
667 * Given an autoptimizeCache instance returns the (maybe cdn-ed) url of
668 * the cached file.
669 *
670 * @param autoptimizeCache $cache autoptimizeCache instance.
671 *
672 * @return string
673 */
674 protected function build_minify_single_url( autoptimizeCache $cache )
675 {
676 $url = AUTOPTIMIZE_CACHE_URL . $cache->getname();
677
678 // CDN-replace the resulting URL if needed...
679 $url = $this->url_replace_cdn( $url );
680
681 return $url;
682 }
683
684 /**
685 * Returns true if given $str ends with given $test.
686 *
687 * @param string $str String to check.
688 * @param string $test Ending to match.
689 *
690 * @return bool
691 */
692 protected function str_ends_in( $str, $test )
693 {
694 // @codingStandardsIgnoreStart
695 // substr_compare() is bugged on 5.5.11: https://3v4l.org/qGYBH
696 // return ( 0 === substr_compare( $str, $test, -strlen( $test ) ) );
697 // @codingStandardsIgnoreEnd
698
699 $length = strlen( $test );
700
701 return ( substr( $str, -$length, $length ) === $test );
702 }
703 }
704