PluginProbe
Autoptimize / 2.7.0
Autoptimize v2.7.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.7.0, at classes/autoptimizeBase.php

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