PluginProbe
Autoptimize / trunk
Autoptimize vtrunk
3.1.16 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 All 108 releases
← All changes | classes/autoptimizeBase.php +621 -328 2.3.1 → trunk 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 }
87 -
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);
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 );
93 152 }
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) */
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).
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);
105 -
106 - // final check: does file exist and is it readable
107 - if (file_exists($path) && is_file($path) && is_readable($path)) {
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 ) ) {
108 181 return $path;
109 182 } else {
110 183 return false;
111 184 }
@@ -110,309 +183,529 @@
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 ) && false !== apply_filters( 'autoptimize_filter_base_apply_cdn', true, $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 ) {
301 - $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);
310 415
311 - // remove comments and blank lines
312 - if (substr($filepath,-3,3)===".js") {
313 - $filecontent=preg_replace("#^\s*\/\/.*$#Um","",$filecontent);
314 - }
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 + }
315 429
316 - $filecontent=preg_replace("#^\s*\/\*[^!].*\*\/\s?#Um","",$filecontent);
317 - $filecontent=preg_replace("#(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+#", "\n", $filecontent);
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;
318 439
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 - }
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 + }
324 446
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 - }
447 + // Bail early if something's not right...
448 + if ( ! $filepath || ! $filehash ) {
449 + return "\n";
450 + }
333 451
334 - // return
335 - return "\n".$filecontent;'
336 - ),
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' ),
337 513 $in
338 514 );
339 - } else {
340 - $out = $in;
341 515 }
516 +
342 517 return $out;
343 518 }
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 - }
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 + // Check for error (for example, an error can occur if $content is very large).
604 + if ( null === $content ) {
605 + $error_message = 'Autoptimize: preg_replace_callback() failed';
606 + if ( function_exists( 'preg_last_error_msg' ) ) {
607 + $error_message .= ': ' . preg_last_error_msg();
390 608 }
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);
609 + error_log( $error_message );
610 + }
611 + }
404 612
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 - }
613 + return $content;
614 + }
615 +
616 + /**
617 + * Complements `autoptimizeBase::replace_contents_with_marker_if_exists()`.
618 + *
619 + * @param string $marker Marker.
620 + * @param string $content Markup.
621 + *
622 + * @return string
623 + */
624 + public static function restore_marked_content( $marker, $content )
625 + {
626 + if ( false !== strpos( $content, $marker ) ) {
627 + $content = preg_replace_callback(
628 + '#%%' . $marker . AUTOPTIMIZE_HASH . '%%(.*?)%%' . $marker . '%%#is',
629 + function ( $matches ) {
630 + return base64_decode( $matches[1] );
631 + },
632 + $content
633 + );
634 + }
635 +
636 + return $content;
637 + }
638 +
639 + /**
640 + * Logs given `$data` for debugging purposes (when debug logging is on).
641 + *
642 + * @param mixed $data Data to log.
643 + *
644 + * @return void
645 + */
646 + protected function debug_log( $data )
647 + {
648 + if ( ! isset( $this->debug_log ) || ! $this->debug_log ) {
649 + return;
650 + }
651 +
652 + if ( ! is_string( $data ) && ! is_resource( $data ) ) {
653 + $data = var_export( $data, true );
654 + }
655 +
656 + error_log( $data );
657 + }
658 +
659 + /**
660 + * Checks if a single local css/js file can be minified and returns source if so.
661 + *
662 + * @param string $filepath Filepath.
663 + *
664 + * @return bool|string to be minified code or false.
665 + */
666 + protected function prepare_minify_single( $filepath )
667 + {
668 + // Decide what we're dealing with, return false if we don't know.
669 + if ( autoptimizeUtils::str_ends_in( $filepath, '.js' ) ) {
670 + $type = 'js';
671 + } elseif ( autoptimizeUtils::str_ends_in( $filepath, '.css' ) ) {
672 + $type = 'css';
673 + } else {
674 + return false;
675 + }
676 +
677 + // Bail if it looks like its already minifed (by having -min or .min in filename).
678 + $minified_variants = array(
679 + '-min.' . $type,
680 + '.min.' . $type,
681 + );
682 + foreach ( $minified_variants as $ending ) {
683 + if ( autoptimizeUtils::str_ends_in( $filepath, $ending ) && true === apply_filters( 'autoptimize_filter_base_prepare_exclude_minified', true ) ) {
684 + return false;
685 + }
686 + }
687 +
688 + // Get file contents, bail if empty.
689 + $contents = file_get_contents( $filepath );
690 +
691 + return $contents;
692 + }
693 +
694 + /**
695 + * Given an autoptimizeCache instance returns the (maybe cdn-ed) url of
696 + * the cached file.
697 + *
698 + * @param autoptimizeCache $cache autoptimizeCache instance.
699 + *
700 + * @return string
701 + */
702 + protected function build_minify_single_url( autoptimizeCache $cache )
703 + {
704 + $url = AUTOPTIMIZE_CACHE_URL . $cache->getname();
705 +
706 + // CDN-replace the resulting URL if needed...
707 + $url = $this->url_replace_cdn( $url );
708 +
709 + return $url;
710 + }
418 711 }