PluginProbe
Autoptimize / 3.1.1
Autoptimize v3.1.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 3.1.1, at classes/autoptimizeBase.php

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