PluginProbe
Autoptimize / 3.0.3
Autoptimize v3.0.3
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
← All changes | classes/autoptimizeBase.php +613 -327 2.2.23.0.3 View file →
@@ -1,83 +1,141 @@
1 1 <?php
2 -if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
2 +/**
3 + * Base class other (more-specific) classes inherit from.
4 + */
3 5
4 -abstract class autoptimizeBase {
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 + */
5 17 protected $content = '';
6 - protected $tagWarning = false;
7 -
8 - public function __construct($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 + {
9 35 $this->content = $content;
10 36 }
11 -
12 - //Reads the page and collects tags
13 - abstract public function read($justhead);
14 -
15 - //Joins and optimizes collected things
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 + */
16 52 abstract public function minify();
17 -
18 - //Caches the things
53 +
54 + /**
55 + * Caches the things.
56 + *
57 + * @return void
58 + */
19 59 abstract public function cache();
20 -
21 - //Returns the content
60 +
61 + /**
62 + * Returns the content
63 + *
64 + * @return string
65 + */
22 66 abstract public function getcontent();
23 -
24 - //Converts an URL to a full path
25 - protected function getpath($url) {
26 - $url=apply_filters( 'autoptimize_filter_cssjs_alter_url', $url);
27 -
28 - if (strpos($url,'%')!==false) {
29 - $url=urldecode($url);
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;
30 82 }
31 83
32 - $siteHost=parse_url(AUTOPTIMIZE_WP_SITE_URL,PHP_URL_HOST);
33 - $contentHost=parse_url(AUTOPTIMIZE_WP_ROOT_URL,PHP_URL_HOST);
34 -
35 - // normalize
36 - if (strpos($url,'//')===0) {
37 - if (is_ssl()) {
38 - $url = "https:".$url;
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;
39 96 } else {
40 - $url = "http:".$url;
97 + $url = 'http:' . $url;
41 98 }
42 - } else if ((strpos($url,'//')===false) && (strpos($url,$siteHost)===false)) {
43 - if (AUTOPTIMIZE_WP_SITE_URL === $siteHost) {
44 - $url = AUTOPTIMIZE_WP_SITE_URL.$url;
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 );
45 104 } else {
46 - $subdir_levels=substr_count(preg_replace("/https?:\/\//","",AUTOPTIMIZE_WP_SITE_URL),"/");
47 - $url = AUTOPTIMIZE_WP_SITE_URL.str_repeat("/..",$subdir_levels).$url;
105 + $url = AUTOPTIMIZE_WP_SITE_URL . autoptimizeUtils::path_canonicalize( $url );
48 106 }
49 107 }
50 -
51 - if ($siteHost !== $contentHost) {
52 - $url=str_replace(AUTOPTIMIZE_WP_CONTENT_URL,AUTOPTIMIZE_WP_SITE_URL.AUTOPTIMIZE_WP_CONTENT_NAME,$url);
108 +
109 + if ( $site_host !== $content_host ) {
110 + $url = str_replace( AUTOPTIMIZE_WP_CONTENT_URL, AUTOPTIMIZE_WP_SITE_URL . AUTOPTIMIZE_WP_CONTENT_NAME, $url );
53 111 }
54 112
55 - // first check; hostname wp site should be hostname of url
56 - $thisHost=@parse_url($url,PHP_URL_HOST);
57 - if ($thisHost !== $siteHost) {
58 - /*
59 - * first try to get all domains from WPML (if available)
60 - * then explicitely declare $this->cdn_url as OK as well
61 - * then apply own filter autoptimize_filter_cssjs_multidomain takes an array of hostnames
62 - * each item in that array will be considered part of the same WP multisite installation
63 - */
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 + */
64 122 $multidomains = array();
65 -
66 - $multidomainsWPML = apply_filters('wpml_setting', array(), 'language_domains');
67 - if (!empty($multidomainsWPML)) {
68 - $multidomains = array_map(array($this,"ao_getDomain"),$multidomainsWPML);
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 );
69 127 }
70 -
71 - if (!empty($this->cdn_url)) {
72 - $multidomains[]=parse_url($this->cdn_url,PHP_URL_HOST);
128 +
129 + if ( ! empty( $this->cdn_url ) ) {
130 + $multidomains[] = parse_url( $this->cdn_url, PHP_URL_HOST );
73 131 }
74 -
75 - $multidomains = apply_filters('autoptimize_filter_cssjs_multidomain', $multidomains);
76 -
77 - if (!empty($multidomains)) {
78 - if (in_array($thisHost,$multidomains)) {
79 - $url=str_replace($thisHost, parse_url(AUTOPTIMIZE_WP_SITE_URL,PHP_URL_HOST), $url);
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 );
80 138 } else {
81 139 return false;
82 140 }
83 141 } else {
@@ -83,29 +141,44 @@
83 141 } else {
84 142 return false;
85 143 }
86 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 + }
87 153
88 - // try to remove "wp root url" from url while not minding http<>https
89 - $tmp_ao_root = preg_replace('/https?:/','',AUTOPTIMIZE_WP_ROOT_URL);
90 - if ($siteHost !== $contentHost) {
91 - // as we replaced the content-domain with the site-domain, we should match against that
92 - $tmp_ao_root = preg_replace('/https?:/','',AUTOPTIMIZE_WP_SITE_URL);
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 + }
93 160 }
94 - $tmp_url = preg_replace('/https?:/','',$url);
95 - $path = str_replace($tmp_ao_root,'',$tmp_url);
96 -
97 - // if path starts with :// or //, this is not a URL in the WP context and we have to assume we can't aggregate
98 - if (preg_match('#^:?//#',$path)) {
99 - /** External script/css (adsense, etc) */
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).
100 169 return false;
101 170 }
102 171
103 - // prepend with WP_ROOT_DIR to have full path to file
104 - $path = str_replace('//','/',WP_ROOT_DIR.$path);
172 + // Prepend with WP_ROOT_DIR to have full path to file.
173 + $path = str_replace( '//', '/', trailingslashit( WP_ROOT_DIR ) . $path );
105 174
106 - // final check: does file exist and is it readable
107 - if (file_exists($path) && is_file($path) && is_readable($path)) {
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 ) ) {
108 181 return $path;
109 182 } else {
110 183 return false;
111 184 }
@@ -110,309 +183,522 @@
110 183 return false;
111 184 }
112 185 }
113 186
114 - // needed for WPML-filter
115 - protected function ao_getDomain($in) {
116 - // make sure the url starts with something vaguely resembling a protocol
117 - if ((strpos($in,"http")!==0) && (strpos($in,"//")!==0)) {
118 - $in="http://".$in;
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;
119 202 }
120 -
121 - // do the actual parse_url
122 - $out = parse_url($in,PHP_URL_HOST);
123 -
124 - // fallback if parse_url does not understand the url is in fact a url
125 - if (empty($out)) $out=$in;
126 -
127 - return $out;
128 - }
129 203
204 + // Grab the hostname.
205 + $hostname = parse_url( $url, PHP_URL_HOST );
130 206
131 - // logger
132 - protected function ao_logger($logmsg,$appendHTML=true) {
133 - if ($appendHTML) {
134 - $logmsg="<!--noptimize--><!-- ".$logmsg." --><!--/noptimize-->";
135 - $this->content.=$logmsg;
136 - } else {
137 - error_log("Autoptimize: ".$logmsg);
207 + // Fallback when parse_url() fails.
208 + if ( empty( $hostname ) ) {
209 + $hostname = $url;
138 210 }
211 +
212 + return $hostname;
139 213 }
140 214
141 - // hide everything between noptimize-comment tags
142 - protected function hide_noptimize($noptimize_in) {
143 - if ( preg_match( '/<!--\s?noptimize\s?-->/', $noptimize_in ) ) {
144 - $noptimize_out = preg_replace_callback(
145 - '#<!--\s?noptimize\s?-->.*?<!--\s?/\s?noptimize\s?-->#is',
146 - create_function(
147 - '$matches',
148 - 'return "%%NOPTIMIZE".AUTOPTIMIZE_HASH."%%".base64_encode($matches[0])."%%NOPTIMIZE%%";'
149 - ),
150 - $noptimize_in
151 - );
152 - } else {
153 - $noptimize_out = $noptimize_in;
154 - }
155 - return $noptimize_out;
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 + );
156 230 }
157 -
158 - // unhide noptimize-tags
159 - protected function restore_noptimize($noptimize_in) {
160 - if ( strpos( $noptimize_in, '%%NOPTIMIZE%%' ) !== false ) {
161 - $noptimize_out = preg_replace_callback(
162 - '#%%NOPTIMIZE'.AUTOPTIMIZE_HASH.'%%(.*?)%%NOPTIMIZE%%#is',
163 - create_function(
164 - '$matches',
165 - 'return base64_decode($matches[1]);'
166 - ),
167 - $noptimize_in
168 - );
169 - } else {
170 - $noptimize_out = $noptimize_in;
171 - }
172 - return $noptimize_out;
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 );
173 242 }
174 243
175 - protected function hide_iehacks($iehacks_in) {
176 - if ( strpos( $iehacks_in, '<!--[if' ) !== false ) {
177 - $iehacks_out = preg_replace_callback(
178 - '#<!--\[if.*?\[endif\]-->#is',
179 - create_function(
180 - '$matches',
181 - 'return "%%IEHACK".AUTOPTIMIZE_HASH."%%".base64_encode($matches[0])."%%IEHACK%%";'
182 - ),
183 - $iehacks_in
184 - );
185 - } else {
186 - $iehacks_out = $iehacks_in;
187 - }
188 - return $iehacks_out;
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 + );
189 259 }
190 260
191 - protected function restore_iehacks($iehacks_in) {
192 - if ( strpos( $iehacks_in, '%%IEHACK%%' ) !== false ) {
193 - $iehacks_out = preg_replace_callback(
194 - '#%%IEHACK'.AUTOPTIMIZE_HASH.'%%(.*?)%%IEHACK%%#is',
195 - create_function(
196 - '$matches',
197 - 'return base64_decode($matches[1]);'
198 - ),
199 - $iehacks_in
200 - );
201 - } else {
202 - $iehacks_out=$iehacks_in;
203 - }
204 - return $iehacks_out;
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 );
205 271 }
206 272
207 - protected function hide_comments($comments_in) {
208 - if ( strpos( $comments_in, '<!--' ) !== false ) {
209 - $comments_out = preg_replace_callback(
210 - '#<!--.*?-->#is',
211 - create_function(
212 - '$matches',
213 - 'return "%%COMMENTS".AUTOPTIMIZE_HASH."%%".base64_encode($matches[0])."%%COMMENTS%%";'
214 - ),
215 - $comments_in
216 - );
217 - } else {
218 - $comments_out = $comments_in;
219 - }
220 - return $comments_out;
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 + );
221 290 }
222 291
223 - protected function restore_comments($comments_in) {
224 - if ( strpos( $comments_in, '%%COMMENTS%%' ) !== false ) {
225 - $comments_out = preg_replace_callback(
226 - '#%%COMMENTS'.AUTOPTIMIZE_HASH.'%%(.*?)%%COMMENTS%%#is',
227 - create_function(
228 - '$matches',
229 - 'return base64_decode($matches[1]);'
230 - ),
231 - $comments_in
232 - );
233 - } else {
234 - $comments_out=$comments_in;
235 - }
236 - return $comments_out;
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 );
237 303 }
238 304
239 - protected function url_replace_cdn( $url ) {
240 - // API filter to change base CDN URL
241 - $cdn_url = apply_filters( 'autoptimize_filter_base_cdnurl', $this->cdn_url );
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 );
242 319
243 - if ( !empty($cdn_url) ) {
244 - // prepend domain-less absolute URL's
245 - if ( ( substr( $url, 0, 1 ) === '/' ) && ( substr( $url, 1, 1 ) !== '/' ) ) {
246 - $url = rtrim( $cdn_url, '/' ) . $url;
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;
247 332 } else {
248 - // get wordpress base URL
249 - $WPSiteBreakdown = parse_url( AUTOPTIMIZE_WP_SITE_URL );
250 - $WPBaseUrl = $WPSiteBreakdown['scheme'] . '://' . $WPSiteBreakdown['host'];
251 - if ( ! empty( $WPSiteBreakdown['port'] ) ) {
252 - $WPBaseUrl .= ":" . $WPSiteBreakdown['port'];
253 - }
254 - // replace full url's with scheme
255 - $tmp_url = str_replace( $WPBaseUrl, rtrim( $cdn_url, '/' ), $url );
256 - if ( $tmp_url === $url ) {
257 - // last attempt; replace scheme-less URL's
258 - $url = str_replace( preg_replace( '/https?:/', '', $WPBaseUrl ), rtrim( $cdn_url, '/' ), $url );
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 );
259 338 } else {
260 - $url = $tmp_url;
339 + $site_url = AUTOPTIMIZE_WP_SITE_URL;
261 340 }
341 + $url = str_replace( $site_url, $cdn_url, $url );
262 342 }
263 343 }
264 344
265 - // allow API filter to alter URL after CDN replacement
345 + // Allow API filter to take further care of CDN replacement.
266 346 $url = apply_filters( 'autoptimize_filter_base_replace_cdn', $url );
347 +
267 348 return $url;
268 349 }
269 350
270 - protected function inject_in_html($payload,$replaceTag) {
271 - if (strpos($this->content,$replaceTag[0])!== false) {
272 - if ($replaceTag[1]==="after") {
273 - $replaceBlock=$replaceTag[0].$payload;
274 - } else if ($replaceTag[1]==="replace"){
275 - $replaceBlock=$payload;
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;
276 374 } else {
277 - $replaceBlock=$payload.$replaceTag[0];
375 + $content = $payload . $where[0];
278 376 }
279 - $this->content = substr_replace($this->content,$replaceBlock,strpos($this->content,$replaceTag[0]),strlen($replaceTag[0]));
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 + );
280 386 } else {
387 + // Couldn't find what was specified, just append and add a warning.
281 388 $this->content .= $payload;
282 - if (!$this->tagWarning) {
283 - $this->content .= "<!--noptimize--><!-- Autoptimize found a problem with the HTML in your Theme, tag \"".str_replace(array("<",">"),"",$replaceTag[0])."\" missing --><!--/noptimize-->";
284 - $this->tagWarning=true;
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;
285 393 }
286 394 }
287 395 }
288 -
289 - protected function isremovable($tag, $removables) {
290 - foreach ($removables as $match) {
291 - if (strpos($tag,$match)!==false) {
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 ) ) {
292 409 return true;
293 410 }
294 411 }
412 +
295 413 return false;
296 414 }
297 -
298 - // inject already minified code in optimized JS/CSS
299 - protected function inject_minified($in) {
300 - if ( strpos( $in, '%%INJECTLATER%%' ) !== false ) {
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%%' ) ) {
301 510 $out = preg_replace_callback(
302 - '#\/\*\!%%INJECTLATER'.AUTOPTIMIZE_HASH.'%%(.*?)%%INJECTLATER%%\*\/#is',
303 - create_function(
304 - '$matches',
305 - '$filepath=base64_decode(strtok($matches[1],"|"));
306 - $filecontent=file_get_contents($filepath);
307 -
308 - // remove BOM
309 - $filecontent = preg_replace("#\x{EF}\x{BB}\x{BF}#","",$filecontent);
511 + '#\/\*\!%%INJECTLATER' . AUTOPTIMIZE_HASH . '%%(.*?)%%INJECTLATER%%\*\/#is',
512 + array( $this, 'inject_minified_callback' ),
513 + $in
514 + );
515 + }
310 516
311 - // remove comments and blank lines
312 - if (substr($filepath,-3,3)===".js") {
313 - $filecontent=preg_replace("#^\s*\/\/.*$#Um","",$filecontent);
314 - }
517 + return $out;
518 + }
315 519
316 - $filecontent=preg_replace("#^\s*\/\*[^!].*\*\/\s?#Um","",$filecontent);
317 - $filecontent=preg_replace("#(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+#", "\n", $filecontent);
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 ) . '*/';
318 535
319 - // differentiate between JS, CSS and other files
320 - if (substr($filepath,-3,3)===".js") {
321 - if ((substr($filecontent,-1,1)!==";")&&(substr($filecontent,-1,1)!=="}")) {
322 - $filecontent.=";";
323 - }
536 + return $contents;
537 + }
324 538
325 - if (get_option("autoptimize_js_trycatch")==="on") {
326 - $filecontent="try{".$filecontent."}catch(e){}";
327 - }
328 - } else if ((substr($filepath,-4,4)===".css")) {
329 - $filecontent=autoptimizeStyles::fixurls($filepath,$filecontent);
330 - } else {
331 - $filecontent="";
332 - }
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 );
333 556
334 - // return
335 - return "\n".$filecontent;'
336 - ),
337 - $in
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
338 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';
339 664 } else {
340 - $out = $in;
665 + return false;
341 666 }
342 - return $out;
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;
343 685 }
344 -
345 - protected function minify_single($pathIn) {
346 - // determine JS or CSS and set var (also mimetype), return false if neither
347 - if ( $this->str_ends_in($pathIn,".js") === true ) {
348 - $codeType="js";
349 - $codeMime="text/javascript";
350 - } else if ( $this->str_ends_in($pathIn,".css") === true ) {
351 - $codeType="css";
352 - $codeMime="text/css";
353 - } else {
354 - return false;
355 - }
356 -
357 - // if min.js or min.css return false
358 - if (( $this->str_ends_in($pathIn,"-min.".$codeType) === true ) || ( $this->str_ends_in($pathIn,".min.".$codeType) === true ) || ( $this->str_ends_in($pathIn,"js/jquery/jquery.js") === true ) ) {
359 - return false;
360 - }
361 -
362 - // read file, return false if empty
363 - $_toMinify = file_get_contents($pathIn);
364 - if ( empty($_toMinify) ) return false;
365 -
366 - // check cache
367 - $_md5hash = "single_".md5($_toMinify);
368 - $_cache = new autoptimizeCache($_md5hash,$codeType);
369 - if ($_cache->check() ) {
370 - $_CachedMinifiedUrl = AUTOPTIMIZE_CACHE_URL.$_cache->getname();
371 - } else {
372 - // if not in cache first minify
373 - $_Minified = $_toMinify;
374 - if ($codeType === "js") {
375 - if (class_exists('JSMin') && apply_filters( 'autoptimize_js_do_minify' , true)) {
376 - if (@is_callable(array("JSMin","minify"))) {
377 - $tmp_code = trim(JSMin::minify($_toMinify));
378 - }
379 - }
380 - } else if ($codeType === "css") {
381 - if (class_exists('Minify_CSS_Compressor')) {
382 - $tmp_code = trim(Minify_CSS_Compressor::process($_toMinify));
383 - } else if(class_exists('CSSmin')) {
384 - $cssmin = new CSSmin();
385 - if (method_exists($cssmin,"run")) {
386 - $tmp_code = trim($cssmin->run($_toMinify));
387 - } elseif (@is_callable(array($cssmin,"minify"))) {
388 - $tmp_code = trim(CssMin::minify($_toMinify));
389 - }
390 - }
391 - }
392 - if (!empty($tmp_code)) {
393 - $_Minified = $tmp_code;
394 - unset($tmp_code);
395 - }
396 - // and then cache
397 - $_cache->cache($_Minified,$codeMime);
398 - $_CachedMinifiedUrl = AUTOPTIMIZE_CACHE_URL.$_cache->getname();
399 - }
400 - unset($_cache);
401 -
402 - // if CDN, then CDN
403 - $_CachedMinifiedUrl = $this->url_replace_cdn($_CachedMinifiedUrl);
404 686
405 - return $_CachedMinifiedUrl;
406 - }
407 -
408 - protected function str_ends_in($haystack,$needle) {
409 - $needleLength = strlen($needle);
410 - $haystackLength = strlen($haystack);
411 - $lastPos=strrpos($haystack,$needle);
412 - if ($lastPos === $haystackLength - $needleLength) {
413 - return true;
414 - } else {
415 - return false;
416 - }
417 - }
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 + }
418 704 }