PluginProbe
Autoptimize / 2.5.1
Autoptimize v2.5.1
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.1, at classes/autoptimizeBase.php

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