1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Class for CSS optimization. |
4
|
|
|
*/ |
5
|
|
|
|
6
|
|
|
if ( ! defined( 'ABSPATH' ) ) { |
7
|
|
|
exit; |
8
|
|
|
} |
9
|
|
|
|
10
|
|
|
class autoptimizeStyles extends autoptimizeBase |
11
|
|
|
{ |
12
|
|
|
const ASSETS_REGEX = '/url\s*\(\s*(?!["\']?data:)(?![\'|\"]?[\#|\%|])([^)]+)\s*\)([^;},\s]*)/i'; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Font-face regex-fu from HamZa at: https://stackoverflow.com/a/21395083 |
16
|
|
|
* ~ |
17
|
|
|
* @font-face\s* # Match @font-face and some spaces |
18
|
|
|
* ( # Start group 1 |
19
|
|
|
* \{ # Match { |
20
|
|
|
* (?: # A non-capturing group |
21
|
|
|
* [^{}]+ # Match anything except {} one or more times |
22
|
|
|
* | # Or |
23
|
|
|
* (?1) # Recurse/rerun the expression of group 1 |
24
|
|
|
* )* # Repeat 0 or more times |
25
|
|
|
* \} # Match } |
26
|
|
|
* ) # End group 1 |
27
|
|
|
* ~xs'; |
28
|
|
|
*/ |
29
|
|
|
const FONT_FACE_REGEX = '~@font-face\s*(\{(?:[^{}]+|(?1))*\})~xsi'; // added `i` flag for case-insensitivity. |
30
|
|
|
|
31
|
|
|
private $css = array(); |
32
|
|
|
private $csscode = array(); |
33
|
|
|
private $url = array(); |
34
|
|
|
private $restofcontent = ''; |
35
|
|
|
private $datauris = false; |
36
|
|
|
private $hashmap = array(); |
37
|
|
|
private $alreadyminified = false; |
38
|
|
|
private $aggregate = true; |
39
|
|
|
private $inline = false; |
40
|
|
|
private $defer = false; |
41
|
|
|
private $defer_inline = false; |
42
|
|
|
private $whitelist = ''; |
43
|
|
|
private $cssinlinesize = ''; |
44
|
|
|
private $cssremovables = array(); |
45
|
|
|
private $include_inline = false; |
46
|
|
|
private $inject_min_late = ''; |
47
|
|
|
private $dontmove = array(); |
48
|
|
|
private $options = array(); |
49
|
|
|
private $minify_excluded = true; |
50
|
|
|
|
51
|
|
|
// public $cdn_url; // Used all over the place implicitly, so will have to be either public or protected :/ . |
52
|
|
|
|
53
|
|
|
// Reads the page and collects style tags. |
54
|
|
|
public function read( $options ) |
55
|
|
|
{ |
56
|
|
|
$noptimizeCSS = apply_filters( 'autoptimize_filter_css_noptimize', false, $this->content ); |
57
|
|
|
if ( $noptimizeCSS ) { |
58
|
|
|
return false; |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
$whitelistCSS = apply_filters( 'autoptimize_filter_css_whitelist', '', $this->content ); |
62
|
|
View Code Duplication |
if ( ! empty( $whitelistCSS ) ) { |
|
|
|
|
63
|
|
|
$this->whitelist = array_filter( array_map( 'trim', explode( ',', $whitelistCSS ) ) ); |
|
|
|
|
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
$removableCSS = apply_filters( 'autoptimize_filter_css_removables', '' ); |
67
|
|
View Code Duplication |
if ( ! empty( $removableCSS ) ) { |
|
|
|
|
68
|
|
|
$this->cssremovables = array_filter( array_map( 'trim', explode( ',', $removableCSS ) ) ); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
$this->cssinlinesize = apply_filters( 'autoptimize_filter_css_inlinesize', 256 ); |
72
|
|
|
|
73
|
|
|
// filter to "late inject minified CSS", default to true for now (it is faster). |
74
|
|
|
$this->inject_min_late = apply_filters( 'autoptimize_filter_css_inject_min_late', true ); |
75
|
|
|
|
76
|
|
|
// Remove everything that's not the header. |
77
|
|
View Code Duplication |
if ( apply_filters( 'autoptimize_filter_css_justhead', $options['justhead'] ) ) { |
|
|
|
|
78
|
|
|
$content = explode( '</head>', $this->content, 2 ); |
79
|
|
|
$this->content = $content[0] . '</head>'; |
80
|
|
|
$this->restofcontent = $content[1]; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
// Determine whether we're doing CSS-files aggregation or not. |
84
|
|
|
if ( isset( $options['aggregate'] ) && ! $options['aggregate'] ) { |
85
|
|
|
$this->aggregate = false; |
86
|
|
|
} |
87
|
|
|
// Returning true for "dontaggregate" turns off aggregation. |
88
|
|
|
if ( $this->aggregate && apply_filters( 'autoptimize_filter_css_dontaggregate', false ) ) { |
89
|
|
|
$this->aggregate = false; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
// include inline? |
93
|
|
|
if ( apply_filters( 'autoptimize_css_include_inline', $options['include_inline'] ) ) { |
94
|
|
|
$this->include_inline = true; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
// List of CSS strings which are excluded from autoptimization. |
98
|
|
|
$excludeCSS = apply_filters( 'autoptimize_filter_css_exclude', $options['css_exclude'], $this->content ); |
99
|
|
View Code Duplication |
if ( '' !== $excludeCSS ) { |
|
|
|
|
100
|
|
|
$this->dontmove = array_filter( array_map( 'trim', explode( ',', $excludeCSS ) ) ); |
101
|
|
|
} else { |
102
|
|
|
$this->dontmove = array(); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
// forcefully exclude CSS with data-noptimize attrib. |
106
|
|
|
$this->dontmove[] = 'data-noptimize'; |
107
|
|
|
|
108
|
|
|
// Should we defer css? |
109
|
|
|
// value: true / false. |
110
|
|
|
$this->defer = $options['defer']; |
111
|
|
|
$this->defer = apply_filters( 'autoptimize_filter_css_defer', $this->defer, $this->content ); |
112
|
|
|
|
113
|
|
|
// Should we inline while deferring? |
114
|
|
|
// value: inlined CSS. |
115
|
|
|
$this->defer_inline = apply_filters( 'autoptimize_filter_css_defer_inline', $options['defer_inline'], $this->content ); |
116
|
|
|
|
117
|
|
|
// Should we inline? |
118
|
|
|
// value: true / false. |
119
|
|
|
$this->inline = $options['inline']; |
120
|
|
|
$this->inline = apply_filters( 'autoptimize_filter_css_inline', $this->inline, $this->content ); |
121
|
|
|
|
122
|
|
|
// Store cdn url. |
123
|
|
|
$this->cdn_url = $options['cdn_url']; |
124
|
|
|
|
125
|
|
|
// Store data: URIs setting for later use. |
126
|
|
|
$this->datauris = $options['datauris']; |
127
|
|
|
|
128
|
|
|
// Determine whether excluded files should be minified if not yet so. |
129
|
|
|
if ( ! $options['minify_excluded'] && $options['aggregate'] ) { |
130
|
|
|
$this->minify_excluded = false; |
131
|
|
|
} |
132
|
|
|
$this->minify_excluded = apply_filters( 'autoptimize_filter_css_minify_excluded', $this->minify_excluded ); |
133
|
|
|
|
134
|
|
|
// noptimize me. |
135
|
|
|
$this->content = $this->hide_noptimize( $this->content ); |
136
|
|
|
|
137
|
|
|
// Exclude (no)script, as those may contain CSS which should be left as is. |
138
|
|
|
$this->content = $this->replace_contents_with_marker_if_exists( |
139
|
|
|
'SCRIPT', |
140
|
|
|
'<script', |
141
|
|
|
'#<(?:no)?script.*?<\/(?:no)?script>#is', |
142
|
|
|
$this->content |
143
|
|
|
); |
144
|
|
|
|
145
|
|
|
// Save IE hacks. |
146
|
|
|
$this->content = $this->hide_iehacks( $this->content ); |
147
|
|
|
|
148
|
|
|
// Hide HTML comments. |
149
|
|
|
$this->content = $this->hide_comments( $this->content ); |
150
|
|
|
|
151
|
|
|
// Get <style> and <link>. |
152
|
|
|
if ( preg_match_all( '#(<style[^>]*>.*</style>)|(<link[^>]*stylesheet[^>]*>)#Usmi', $this->content, $matches ) ) { |
153
|
|
|
|
154
|
|
|
foreach ( $matches[0] as $tag ) { |
155
|
|
|
if ( $this->isremovable( $tag, $this->cssremovables ) ) { |
156
|
|
|
$this->content = str_replace( $tag, '', $this->content ); |
157
|
|
|
} elseif ( $this->ismovable( $tag ) ) { |
158
|
|
|
// Get the media. |
159
|
|
|
if ( false !== strpos( $tag, 'media=' ) ) { |
160
|
|
|
preg_match( '#media=(?:"|\')([^>]*)(?:"|\')#Ui', $tag, $medias ); |
161
|
|
|
$medias = explode( ',', $medias[1] ); |
162
|
|
|
$media = array(); |
163
|
|
|
foreach ( $medias as $elem ) { |
164
|
|
|
/* $media[] = current(explode(' ',trim($elem),2)); */ |
165
|
|
|
if ( empty( $elem ) ) { |
166
|
|
|
$elem = 'all'; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
$media[] = $elem; |
170
|
|
|
} |
171
|
|
|
} else { |
172
|
|
|
// No media specified - applies to all. |
173
|
|
|
$media = array( 'all' ); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
$media = apply_filters( 'autoptimize_filter_css_tagmedia', $media, $tag ); |
177
|
|
|
|
178
|
|
|
if ( preg_match( '#<link.*href=("|\')(.*)("|\')#Usmi', $tag, $source ) ) { |
179
|
|
|
// <link>. |
180
|
|
|
$url = current( explode( '?', $source[2], 2 ) ); |
181
|
|
|
$path = $this->getpath( $url ); |
182
|
|
|
|
183
|
|
|
if ( false !== $path && preg_match( '#\.css$#', $path ) ) { |
184
|
|
|
// Good link. |
185
|
|
|
$this->css[] = array( $media, $path ); |
186
|
|
|
} else { |
187
|
|
|
// Link is dynamic (.php etc). |
188
|
|
|
$new_tag = $this->optionally_defer_excluded( $tag, 'none' ); |
189
|
|
View Code Duplication |
if ( $new_tag !== '' && $new_tag !== $tag ) { |
|
|
|
|
190
|
|
|
$this->content = str_replace( $tag, $new_tag, $this->content ); |
191
|
|
|
} |
192
|
|
|
$tag = ''; |
193
|
|
|
} |
194
|
|
|
} else { |
195
|
|
|
// Inline css in style tags can be wrapped in comment tags, so restore comments. |
196
|
|
|
$tag = $this->restore_comments( $tag ); |
197
|
|
|
preg_match( '#<style.*>(.*)</style>#Usmi', $tag, $code ); |
198
|
|
|
|
199
|
|
|
// And re-hide them to be able to to the removal based on tag. |
200
|
|
|
$tag = $this->hide_comments( $tag ); |
201
|
|
|
|
202
|
|
|
if ( $this->include_inline ) { |
203
|
|
|
$code = preg_replace( '#^.*<!\[CDATA\[(?:\s*\*/)?(.*)(?://|/\*)\s*?\]\]>.*$#sm', '$1', $code[1] ); |
204
|
|
|
$this->css[] = array( $media, 'INLINE;' . $code ); |
205
|
|
|
} else { |
206
|
|
|
$tag = ''; |
207
|
|
|
} |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
// Remove the original style tag. |
211
|
|
|
$this->content = str_replace( $tag, '', $this->content ); |
212
|
|
|
} else { |
213
|
|
|
if ( preg_match( '#<link.*href=("|\')(.*)("|\')#Usmi', $tag, $source ) ) { |
214
|
|
|
$exploded_url = explode( '?', $source[2], 2 ); |
215
|
|
|
$url = $exploded_url[0]; |
216
|
|
|
$path = $this->getpath( $url ); |
217
|
|
|
$new_tag = $tag; |
218
|
|
|
|
219
|
|
|
// Excluded CSS, minify that file: |
220
|
|
|
// -> if aggregate is on and exclude minify is on |
221
|
|
|
// -> if aggregate is off and the file is not in dontmove. |
222
|
|
View Code Duplication |
if ( $path && $this->minify_excluded ) { |
|
|
|
|
223
|
|
|
$consider_minified_array = apply_filters( 'autoptimize_filter_css_consider_minified', false ); |
224
|
|
|
if ( ( false === $this->aggregate && str_replace( $this->dontmove, '', $path ) === $path ) || ( true === $this->aggregate && ( false === $consider_minified_array || str_replace( $consider_minified_array, '', $path ) === $path ) ) ) { |
225
|
|
|
$minified_url = $this->minify_single( $path ); |
226
|
|
|
if ( ! empty( $minified_url ) ) { |
227
|
|
|
// Replace orig URL with cached minified URL. |
228
|
|
|
$new_tag = str_replace( $url, $minified_url, $tag ); |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
// Optionally defer (preload) non-aggregated CSS. |
234
|
|
|
$new_tag = $this->optionally_defer_excluded( $new_tag, $url ); |
235
|
|
|
|
236
|
|
|
// And replace! |
237
|
|
View Code Duplication |
if ( $new_tag !== '' && $new_tag !== $tag ) { |
|
|
|
|
238
|
|
|
$this->content = str_replace( $tag, $new_tag, $this->content ); |
239
|
|
|
} |
240
|
|
|
} |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
return true; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
// Really, no styles? |
247
|
|
|
return false; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Checks if non-optimized CSS is to be preloaded and if so return |
252
|
|
|
* the tag with preload code. |
253
|
|
|
* |
254
|
|
|
* @param string $tag (required). |
255
|
|
|
* @param string $url (optional). |
256
|
|
|
* |
257
|
|
|
* @return string $new_tag |
258
|
|
|
*/ |
259
|
|
|
private function optionally_defer_excluded( $tag, $url = '' ) |
260
|
|
|
{ |
261
|
|
|
// Defer single CSS if "inline & defer" is ON and there is inline CSS. |
262
|
|
|
if ( $this->defer && ! empty( $this->defer_inline ) ) { |
263
|
|
|
// Get/ set (via filter) the JS to be triggers onload of the preloaded CSS. |
264
|
|
|
$_preload_onload = apply_filters( |
265
|
|
|
'autoptimize_filter_css_preload_onload', |
266
|
|
|
"this.onload=null;this.rel='stylesheet'", |
267
|
|
|
$url |
268
|
|
|
); |
269
|
|
|
// Adapt original <link> element for CSS to be preloaded and add <noscript>-version for fallback. |
270
|
|
|
$new_tag = '<noscript>' . autoptimizeUtils::remove_id_from_node( $tag ) . '</noscript>' . str_replace( |
271
|
|
|
array( |
272
|
|
|
"rel='stylesheet'", |
273
|
|
|
'rel="stylesheet"', |
274
|
|
|
), |
275
|
|
|
"rel='preload' as='style' onload=\"" . $_preload_onload . "\"", |
276
|
|
|
$tag |
277
|
|
|
); |
278
|
|
|
} else { |
279
|
|
|
$new_tag = $tag; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
return $new_tag; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Checks if the local file referenced by $path is a valid |
287
|
|
|
* candidate for being inlined into a data: URI |
288
|
|
|
* |
289
|
|
|
* @param string $path |
290
|
|
|
* @return boolean |
291
|
|
|
*/ |
292
|
|
|
private function is_datauri_candidate( $path ) |
293
|
|
|
{ |
294
|
|
|
// Call only once since it's called from a loop. |
295
|
|
|
static $max_size = null; |
296
|
|
|
if ( null === $max_size ) { |
297
|
|
|
$max_size = $this->get_datauri_maxsize(); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
if ( $path && preg_match( '#\.(jpe?g|png|gif|webp|bmp)$#i', $path ) && |
301
|
|
|
file_exists( $path ) && is_readable( $path ) && filesize( $path ) <= $max_size ) { |
302
|
|
|
|
303
|
|
|
// Seems we have a candidate. |
304
|
|
|
$is_candidate = true; |
305
|
|
|
} else { |
306
|
|
|
// Filter allows overriding default decision (which checks for local file existence). |
307
|
|
|
$is_candidate = apply_filters( 'autoptimize_filter_css_is_datauri_candidate', false, $path ); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
return $is_candidate; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* Returns the amount of bytes that shouldn't be exceeded if a file is to |
315
|
|
|
* be inlined into a data: URI. Defaults to 4096, passed through |
316
|
|
|
* `autoptimize_filter_css_datauri_maxsize` filter. |
317
|
|
|
* |
318
|
|
|
* @return mixed |
319
|
|
|
*/ |
320
|
|
|
private function get_datauri_maxsize() |
321
|
|
|
{ |
322
|
|
|
static $max_size = null; |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* No need to apply the filter multiple times in case the |
326
|
|
|
* method itself is invoked multiple times during a single request. |
327
|
|
|
* This prevents some wild stuff like having different maxsizes |
328
|
|
|
* for different files/site-sections etc. But if you're into that sort |
329
|
|
|
* of thing you're probably better of building assets completely |
330
|
|
|
* outside of WordPress anyway. |
331
|
|
|
*/ |
332
|
|
|
if ( null === $max_size ) { |
333
|
|
|
$max_size = (int) apply_filters( 'autoptimize_filter_css_datauri_maxsize', 4096 ); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
return $max_size; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
private function check_datauri_exclude_list( $url ) |
340
|
|
|
{ |
341
|
|
|
static $exclude_list = null; |
342
|
|
|
$no_datauris = array(); |
343
|
|
|
|
344
|
|
|
// Again, skip doing certain stuff repeatedly when loop-called. |
345
|
|
|
if ( null === $exclude_list ) { |
346
|
|
|
$exclude_list = apply_filters( 'autoptimize_filter_css_datauri_exclude', '' ); |
347
|
|
|
$no_datauris = array_filter( array_map( 'trim', explode( ',', $exclude_list ) ) ); |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
$matched = false; |
351
|
|
|
|
352
|
|
|
if ( ! empty( $exclude_list ) ) { |
353
|
|
|
foreach ( $no_datauris as $no_datauri ) { |
354
|
|
|
if ( false !== strpos( $url, $no_datauri ) ) { |
355
|
|
|
$matched = true; |
356
|
|
|
break; |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
return $matched; |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
private function build_or_get_datauri_image( $path ) |
365
|
|
|
{ |
366
|
|
|
/** |
367
|
|
|
* TODO/FIXME: document the required return array format, or better yet, |
368
|
|
|
* use a string, since we don't really need an array for this. That would, however, |
369
|
|
|
* require changing even more code, which is not happening right now... |
370
|
|
|
*/ |
371
|
|
|
|
372
|
|
|
// Allows short-circuiting datauri generation for an image. |
373
|
|
|
$result = apply_filters( 'autoptimize_filter_css_datauri_image', array(), $path ); |
374
|
|
|
if ( ! empty( $result ) ) { |
375
|
|
|
if ( is_array( $result ) && isset( $result['full'] ) && isset( $result['base64data'] ) ) { |
376
|
|
|
return $result; |
377
|
|
|
} |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
$hash = md5( $path ); |
381
|
|
|
$check = new autoptimizeCache( $hash, 'img' ); |
382
|
|
|
if ( $check->check() ) { |
383
|
|
|
// we have the base64 image in cache. |
384
|
|
|
$headAndData = $check->retrieve(); |
385
|
|
|
$_base64data = explode( ';base64,', $headAndData ); |
386
|
|
|
$base64data = $_base64data[1]; |
387
|
|
|
unset( $_base64data ); |
388
|
|
|
} else { |
389
|
|
|
// It's an image and we don't have it in cache, get the type by extension. |
390
|
|
|
$exploded_path = explode( '.', $path ); |
391
|
|
|
$type = end( $exploded_path ); |
392
|
|
|
|
393
|
|
|
switch ( $type ) { |
394
|
|
|
case 'jpg': |
395
|
|
|
case 'jpeg': |
396
|
|
|
$dataurihead = 'data:image/jpeg;base64,'; |
397
|
|
|
break; |
398
|
|
|
case 'gif': |
399
|
|
|
$dataurihead = 'data:image/gif;base64,'; |
400
|
|
|
break; |
401
|
|
|
case 'png': |
402
|
|
|
$dataurihead = 'data:image/png;base64,'; |
403
|
|
|
break; |
404
|
|
|
case 'bmp': |
405
|
|
|
$dataurihead = 'data:image/bmp;base64,'; |
406
|
|
|
break; |
407
|
|
|
case 'webp': |
408
|
|
|
$dataurihead = 'data:image/webp;base64,'; |
409
|
|
|
break; |
410
|
|
|
default: |
411
|
|
|
$dataurihead = 'data:application/octet-stream;base64,'; |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
// Encode the data. |
415
|
|
|
$base64data = base64_encode( file_get_contents( $path ) ); |
416
|
|
|
$headAndData = $dataurihead . $base64data; |
417
|
|
|
|
418
|
|
|
// Save in cache. |
419
|
|
|
$check->cache( $headAndData, 'text/plain' ); |
420
|
|
|
} |
421
|
|
|
unset( $check ); |
422
|
|
|
|
423
|
|
|
return array( 'full' => $headAndData, 'base64data' => $base64data ); |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
/** |
427
|
|
|
* Given an array of key/value pairs to replace in $string, |
428
|
|
|
* it does so by replacing the longest-matching strings first. |
429
|
|
|
* |
430
|
|
|
* @param string $string |
431
|
|
|
* @param array $replacements |
432
|
|
|
* |
433
|
|
|
* @return string |
434
|
|
|
*/ |
435
|
|
|
protected static function replace_longest_matches_first( $string, $replacements = array() ) |
436
|
|
|
{ |
437
|
|
|
if ( ! empty( $replacements ) ) { |
438
|
|
|
// Sort the replacements array by key length in desc order (so that the longest strings are replaced first). |
439
|
|
|
$keys = array_map( 'strlen', array_keys( $replacements ) ); |
440
|
|
|
array_multisort( $keys, SORT_DESC, $replacements ); |
441
|
|
|
$string = str_replace( array_keys( $replacements ), array_values( $replacements ), $string ); |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
return $string; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* Rewrites/Replaces any ASSETS_REGEX-matching urls in a string. |
449
|
|
|
* Removes quotes/cruft around each one and passes it through to |
450
|
|
|
* `autoptimizeBase::url_replace_cdn()`. |
451
|
|
|
* Replacements are performed in a `longest-match-replaced-first` way. |
452
|
|
|
* |
453
|
|
|
* @param string $code CSS code. |
454
|
|
|
* |
455
|
|
|
* @return string |
456
|
|
|
*/ |
457
|
|
|
public function replace_urls( $code = '' ) |
458
|
|
|
{ |
459
|
|
|
$replacements = array(); |
460
|
|
|
|
461
|
|
|
preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches ); |
462
|
|
|
if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) { |
463
|
|
|
foreach ( $url_src_matches[1] as $count => $original_url ) { |
464
|
|
|
// Removes quotes and other cruft. |
465
|
|
|
$url = trim( $original_url, " \t\n\r\0\x0B\"'" ); |
466
|
|
|
|
467
|
|
|
/** |
468
|
|
|
* TODO/FIXME: Add a way for other code / callable to be called here |
469
|
|
|
* and provide it's own results for the $replacements array |
470
|
|
|
* for the "current" key. |
471
|
|
|
* If such a result is returned/provided, we sholud then avoid |
472
|
|
|
* calling url_replace_cdn() here for the current iteration. |
473
|
|
|
* |
474
|
|
|
* This would maybe allow the inlining logic currently present |
475
|
|
|
* in `autoptimizeStyles::rewrite_assets()` to be "pulled out" |
476
|
|
|
* and given as a callable to this method or something... and |
477
|
|
|
* then we could "just" call `replace_urls()` from within |
478
|
|
|
* `autoptimizeStyles::rewrite_assets()` and avoid some |
479
|
|
|
* (currently present) code/logic duplication. |
480
|
|
|
*/ |
481
|
|
|
|
482
|
|
|
// Do CDN replacement if needed. |
483
|
|
View Code Duplication |
if ( ! empty( $this->cdn_url ) ) { |
|
|
|
|
484
|
|
|
$replacement_url = $this->url_replace_cdn( $url ); |
485
|
|
|
// Prepare replacements array. |
486
|
|
|
$replacements[ $url_src_matches[1][ $count ] ] = str_replace( |
487
|
|
|
$original_url, $replacement_url, $url_src_matches[1][$count] |
488
|
|
|
); |
489
|
|
|
} |
490
|
|
|
} |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
$code = self::replace_longest_matches_first( $code, $replacements ); |
494
|
|
|
|
495
|
|
|
return $code; |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
/** |
499
|
|
|
* "Hides" @font-face declarations by replacing them with `%%FONTFACE%%` markers. |
500
|
|
|
* Also does CDN replacement of any font-urls within those declarations if the `autoptimize_filter_css_fonts_cdn` |
501
|
|
|
* filter is used. |
502
|
|
|
* |
503
|
|
|
* @param string $code |
504
|
|
|
* @return string |
505
|
|
|
*/ |
506
|
|
|
public function hide_fontface_and_maybe_cdn( $code ) |
507
|
|
|
{ |
508
|
|
|
// Proceed only if @font-face declarations exist within $code. |
509
|
|
|
preg_match_all( self::FONT_FACE_REGEX, $code, $fontfaces ); |
510
|
|
|
if ( isset( $fontfaces[0] ) ) { |
511
|
|
|
// Check if we need to cdn fonts or not. |
512
|
|
|
$do_font_cdn = apply_filters( 'autoptimize_filter_css_fonts_cdn', false ); |
513
|
|
|
|
514
|
|
|
foreach ( $fontfaces[0] as $full_match ) { |
515
|
|
|
// Keep original match so we can search/replace it. |
516
|
|
|
$match_search = $full_match; |
517
|
|
|
|
518
|
|
|
// Do font cdn if needed. |
519
|
|
|
if ( $do_font_cdn ) { |
520
|
|
|
$full_match = $this->replace_urls( $full_match ); |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
// Replace declaration with its base64 encoded string. |
524
|
|
|
$replacement = self::build_marker( 'FONTFACE', $full_match ); |
525
|
|
|
$code = str_replace( $match_search, $replacement, $code ); |
526
|
|
|
} |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
return $code; |
530
|
|
|
} |
531
|
|
|
|
532
|
|
|
/** |
533
|
|
|
* Restores original @font-face declarations that have been "hidden" |
534
|
|
|
* using `hide_fontface_and_maybe_cdn()`. |
535
|
|
|
* |
536
|
|
|
* @param string $code |
537
|
|
|
* @return string |
538
|
|
|
*/ |
539
|
|
|
public function restore_fontface( $code ) |
540
|
|
|
{ |
541
|
|
|
return $this->restore_marked_content( 'FONTFACE', $code ); |
542
|
|
|
} |
543
|
|
|
|
544
|
|
|
// Re-write (and/or inline) referenced assets. |
545
|
|
|
public function rewrite_assets( $code ) |
546
|
|
|
{ |
547
|
|
|
// Handle @font-face rules by hiding and processing them separately. |
548
|
|
|
$code = $this->hide_fontface_and_maybe_cdn( $code ); |
549
|
|
|
|
550
|
|
|
/** |
551
|
|
|
* TODO/FIXME: |
552
|
|
|
* Certain code parts below are kind-of repeated now in `replace_urls()`, which is not ideal. |
553
|
|
|
* There is maybe a way to separate/refactor things and then be able to keep |
554
|
|
|
* the ASSETS_REGEX rewriting/handling logic in a single place (along with removing quotes/cruft from matched urls). |
555
|
|
|
* See comments in `replace_urls()` regarding this. The idea is to extract the inlining |
556
|
|
|
* logic out (which is the only real difference between replace_urls() and the code below), but still |
557
|
|
|
* achieve identical results as before. |
558
|
|
|
*/ |
559
|
|
|
|
560
|
|
|
// Re-write (and/or inline) URLs to point them to the CDN host. |
561
|
|
|
$url_src_matches = array(); |
562
|
|
|
$imgreplace = array(); |
563
|
|
|
// Matches and captures anything specified within the literal `url()` and excludes those containing data: URIs. |
564
|
|
|
preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches ); |
565
|
|
|
if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) { |
566
|
|
|
foreach ( $url_src_matches[1] as $count => $original_url ) { |
567
|
|
|
// Removes quotes and other cruft. |
568
|
|
|
$url = trim( $original_url, " \t\n\r\0\x0B\"'" ); |
569
|
|
|
|
570
|
|
|
// If datauri inlining is turned on, do it. |
571
|
|
|
$inlined = false; |
572
|
|
|
if ( $this->datauris ) { |
573
|
|
|
$iurl = $url; |
574
|
|
|
if ( false !== strpos( $iurl, '?' ) ) { |
575
|
|
|
$iurl = strtok( $iurl, '?' ); |
576
|
|
|
} |
577
|
|
|
|
578
|
|
|
$ipath = $this->getpath( $iurl ); |
579
|
|
|
|
580
|
|
|
$excluded = $this->check_datauri_exclude_list( $ipath ); |
581
|
|
|
if ( ! $excluded ) { |
582
|
|
|
$is_datauri_candidate = $this->is_datauri_candidate( $ipath ); |
|
|
|
|
583
|
|
|
if ( $is_datauri_candidate ) { |
584
|
|
|
$datauri = $this->build_or_get_datauri_image( $ipath ); |
585
|
|
|
$base64data = $datauri['base64data']; |
|
|
|
|
586
|
|
|
// Add it to the list for replacement. |
587
|
|
|
$imgreplace[ $url_src_matches[1][ $count ] ] = str_replace( |
588
|
|
|
$original_url, |
589
|
|
|
$datauri['full'], |
590
|
|
|
$url_src_matches[1][$count] |
591
|
|
|
); |
592
|
|
|
$inlined = true; |
593
|
|
|
} |
594
|
|
|
} |
595
|
|
|
} |
596
|
|
|
|
597
|
|
|
/** |
598
|
|
|
* Doing CDN URL replacement for every found match (if CDN is |
599
|
|
|
* specified). This way we make sure to do it even if |
600
|
|
|
* inlining isn't turned on, or if a resource is skipped from |
601
|
|
|
* being inlined for whatever reason above. |
602
|
|
|
*/ |
603
|
|
View Code Duplication |
if ( ! $inlined && ( ! empty( $this->cdn_url ) || has_filter( 'autoptimize_filter_base_replace_cdn' ) ) ) { |
|
|
|
|
604
|
|
|
// Just do the "simple" CDN replacement. |
605
|
|
|
$replacement_url = $this->url_replace_cdn( $url ); |
606
|
|
|
$imgreplace[ $url_src_matches[1][ $count ] ] = str_replace( |
607
|
|
|
$original_url, $replacement_url, $url_src_matches[1][$count] |
608
|
|
|
); |
609
|
|
|
} |
610
|
|
|
} |
611
|
|
|
} |
612
|
|
|
|
613
|
|
|
$code = self::replace_longest_matches_first( $code, $imgreplace ); |
614
|
|
|
|
615
|
|
|
// Replace back font-face markers with actual font-face declarations. |
616
|
|
|
$code = $this->restore_fontface( $code ); |
617
|
|
|
|
618
|
|
|
return $code; |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
// Joins and optimizes CSS. |
622
|
|
|
public function minify() |
623
|
|
|
{ |
624
|
|
|
foreach ( $this->css as $group ) { |
625
|
|
|
list( $media, $css ) = $group; |
626
|
|
|
if ( preg_match( '#^INLINE;#', $css ) ) { |
627
|
|
|
// <style>. |
628
|
|
|
$css = preg_replace( '#^INLINE;#', '', $css ); |
629
|
|
|
$css = self::fixurls( ABSPATH . 'index.php', $css ); // ABSPATH already contains a trailing slash. |
630
|
|
|
$tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, '' ); |
631
|
|
|
if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) { |
632
|
|
|
$css = $tmpstyle; |
633
|
|
|
$this->alreadyminified = true; |
634
|
|
|
} |
635
|
|
|
} else { |
636
|
|
|
// <link> |
637
|
|
|
if ( false !== $css && file_exists( $css ) && is_readable( $css ) ) { |
638
|
|
|
$cssPath = $css; |
639
|
|
|
$css = self::fixurls( $cssPath, file_get_contents( $cssPath ) ); |
640
|
|
|
$css = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $css ); |
641
|
|
|
$tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, $cssPath ); |
642
|
|
View Code Duplication |
if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) { |
|
|
|
|
643
|
|
|
$css = $tmpstyle; |
644
|
|
|
$this->alreadyminified = true; |
645
|
|
|
} elseif ( $this->can_inject_late( $cssPath, $css ) ) { |
646
|
|
|
$css = self::build_injectlater_marker( $cssPath, md5( $css ) ); |
647
|
|
|
} |
648
|
|
|
} else { |
649
|
|
|
// Couldn't read CSS. Maybe getpath isn't working? |
650
|
|
|
$css = ''; |
651
|
|
|
} |
652
|
|
|
} |
653
|
|
|
|
654
|
|
|
foreach ( $media as $elem ) { |
655
|
|
|
if ( ! empty( $css ) ) { |
656
|
|
|
if ( ! isset( $this->csscode[$elem] ) ) { |
657
|
|
|
$this->csscode[$elem] = ''; |
658
|
|
|
} |
659
|
|
|
$this->csscode[$elem] .= "\n/*FILESTART*/" . $css; |
660
|
|
|
} |
661
|
|
|
} |
662
|
|
|
} |
663
|
|
|
|
664
|
|
|
// Check for duplicate code. |
665
|
|
|
$md5list = array(); |
666
|
|
|
$tmpcss = $this->csscode; |
667
|
|
|
foreach ( $tmpcss as $media => $code ) { |
668
|
|
|
$md5sum = md5( $code ); |
669
|
|
|
$medianame = $media; |
670
|
|
|
foreach ( $md5list as $med => $sum ) { |
671
|
|
|
// If same code. |
672
|
|
|
if ( $sum === $md5sum ) { |
673
|
|
|
// Add the merged code. |
674
|
|
|
$medianame = $med . ', ' . $media; |
675
|
|
|
$this->csscode[$medianame] = $code; |
676
|
|
|
$md5list[$medianame] = $md5list[$med]; |
677
|
|
|
unset( $this->csscode[$med], $this->csscode[$media], $md5list[$med] ); |
678
|
|
|
} |
679
|
|
|
} |
680
|
|
|
$md5list[$medianame] = $md5sum; |
681
|
|
|
} |
682
|
|
|
unset( $tmpcss ); |
683
|
|
|
|
684
|
|
|
// Manage @imports, while is for recursive import management. |
685
|
|
|
foreach ( $this->csscode as &$thiscss ) { |
686
|
|
|
// Flag to trigger import reconstitution and var to hold external imports. |
687
|
|
|
$fiximports = false; |
688
|
|
|
$external_imports = ''; |
689
|
|
|
|
690
|
|
|
// remove comments to avoid importing commented-out imports. |
691
|
|
|
$thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss ); |
692
|
|
|
while ( preg_match_all( '#@import +(?:url)?(?:(?:\((["\']?)(?:[^"\')]+)\1\)|(["\'])(?:[^"\']+)\2)(?:[^,;"\']+(?:,[^,;"\']+)*)?)(?:;)#mi', $thiscss_nocomments, $matches ) ) { |
693
|
|
|
foreach ( $matches[0] as $import ) { |
694
|
|
|
if ( $this->isremovable( $import, $this->cssremovables ) ) { |
695
|
|
|
$thiscss = str_replace( $import, '', $thiscss ); |
696
|
|
|
$import_ok = true; |
697
|
|
|
} else { |
698
|
|
|
$url = trim( preg_replace( '#^.*((?:https?:|ftp:)?//.*\.css).*$#', '$1', trim( $import ) ), " \t\n\r\0\x0B\"'" ); |
699
|
|
|
$path = $this->getpath( $url ); |
700
|
|
|
$import_ok = false; |
701
|
|
|
if ( file_exists( $path ) && is_readable( $path ) ) { |
702
|
|
|
$code = addcslashes( self::fixurls( $path, file_get_contents( $path ) ), "\\" ); |
703
|
|
|
$code = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $code ); |
704
|
|
|
$tmpstyle = apply_filters( 'autoptimize_css_individual_style', $code, '' ); |
705
|
|
View Code Duplication |
if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) { |
|
|
|
|
706
|
|
|
$code = $tmpstyle; |
707
|
|
|
$this->alreadyminified = true; |
708
|
|
|
} elseif ( $this->can_inject_late( $path, $code ) ) { |
709
|
|
|
$code = self::build_injectlater_marker( $path, md5( $code ) ); |
|
|
|
|
710
|
|
|
} |
711
|
|
|
|
712
|
|
|
if ( ! empty( $code ) ) { |
713
|
|
|
$tmp_thiscss = preg_replace( '#(/\*FILESTART\*/.*)' . preg_quote( $import, '#' ) . '#Us', '/*FILESTART2*/' . $code . '$1', $thiscss ); |
714
|
|
|
if ( ! empty( $tmp_thiscss ) ) { |
715
|
|
|
$thiscss = $tmp_thiscss; |
716
|
|
|
$import_ok = true; |
717
|
|
|
unset( $tmp_thiscss ); |
718
|
|
|
} |
719
|
|
|
} |
720
|
|
|
unset( $code ); |
721
|
|
|
} |
722
|
|
|
} |
723
|
|
|
if ( ! $import_ok ) { |
724
|
|
|
// External imports and general fall-back. |
725
|
|
|
$external_imports .= $import; |
726
|
|
|
|
727
|
|
|
$thiscss = str_replace( $import, '', $thiscss ); |
728
|
|
|
$fiximports = true; |
729
|
|
|
} |
730
|
|
|
} |
731
|
|
|
$thiscss = preg_replace( '#/\*FILESTART\*/#', '', $thiscss ); |
732
|
|
|
$thiscss = preg_replace( '#/\*FILESTART2\*/#', '/*FILESTART*/', $thiscss ); |
733
|
|
|
|
734
|
|
|
// and update $thiscss_nocomments before going into next iteration in while loop. |
735
|
|
|
$thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss ); |
736
|
|
|
} |
737
|
|
|
unset( $thiscss_nocomments ); |
738
|
|
|
|
739
|
|
|
// Add external imports to top of aggregated CSS. |
740
|
|
|
if ( $fiximports ) { |
741
|
|
|
$thiscss = $external_imports . $thiscss; |
742
|
|
|
} |
743
|
|
|
} |
744
|
|
|
unset( $thiscss ); |
745
|
|
|
|
746
|
|
|
// $this->csscode has all the uncompressed code now. |
747
|
|
|
foreach ( $this->csscode as &$code ) { |
748
|
|
|
// Check for already-minified code. |
749
|
|
|
$hash = md5( $code ); |
750
|
|
|
do_action( 'autoptimize_action_css_hash', $hash ); |
751
|
|
|
$ccheck = new autoptimizeCache( $hash, 'css' ); |
752
|
|
|
if ( $ccheck->check() ) { |
753
|
|
|
$code = $ccheck->retrieve(); |
754
|
|
|
$this->hashmap[md5( $code )] = $hash; |
755
|
|
|
continue; |
756
|
|
|
} |
757
|
|
|
unset( $ccheck ); |
758
|
|
|
|
759
|
|
|
// Rewrite and/or inline referenced assets. |
760
|
|
|
$code = $this->rewrite_assets( $code ); |
761
|
|
|
|
762
|
|
|
// Minify. |
763
|
|
|
$code = $this->run_minifier_on( $code ); |
764
|
|
|
|
765
|
|
|
// Bring back INJECTLATER stuff. |
766
|
|
|
$code = $this->inject_minified( $code ); |
767
|
|
|
|
768
|
|
|
// Filter results. |
769
|
|
|
$tmp_code = apply_filters( 'autoptimize_css_after_minify', $code ); |
770
|
|
|
if ( ! empty( $tmp_code ) ) { |
771
|
|
|
$code = $tmp_code; |
772
|
|
|
unset( $tmp_code ); |
773
|
|
|
} |
774
|
|
|
|
775
|
|
|
$this->hashmap[md5( $code )] = $hash; |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
unset( $code ); |
779
|
|
|
return true; |
780
|
|
|
} |
781
|
|
|
|
782
|
|
|
public function run_minifier_on( $code ) |
783
|
|
|
{ |
784
|
|
|
if ( ! $this->alreadyminified ) { |
785
|
|
|
$do_minify = apply_filters( 'autoptimize_css_do_minify', true ); |
786
|
|
|
|
787
|
|
View Code Duplication |
if ( $do_minify ) { |
|
|
|
|
788
|
|
|
$cssmin = new autoptimizeCSSmin(); |
789
|
|
|
$tmp_code = trim( $cssmin->run( $code ) ); |
790
|
|
|
|
791
|
|
|
if ( ! empty( $tmp_code ) ) { |
792
|
|
|
$code = $tmp_code; |
793
|
|
|
unset( $tmp_code ); |
794
|
|
|
} |
795
|
|
|
} |
796
|
|
|
} |
797
|
|
|
|
798
|
|
|
return $code; |
799
|
|
|
} |
800
|
|
|
|
801
|
|
|
// Caches the CSS in uncompressed, deflated and gzipped form. |
802
|
|
|
public function cache() |
803
|
|
|
{ |
804
|
|
|
// CSS cache. |
805
|
|
|
foreach ( $this->csscode as $media => $code ) { |
806
|
|
|
$md5 = $this->hashmap[md5( $code )]; |
807
|
|
|
$cache = new autoptimizeCache( $md5, 'css' ); |
808
|
|
|
if ( ! $cache->check() ) { |
809
|
|
|
// Cache our code. |
810
|
|
|
$cache->cache( $code, 'text/css' ); |
811
|
|
|
} |
812
|
|
|
$this->url[$media] = AUTOPTIMIZE_CACHE_URL . $cache->getname(); |
813
|
|
|
} |
814
|
|
|
} |
815
|
|
|
|
816
|
|
|
// Returns the content. |
817
|
|
|
public function getcontent() |
818
|
|
|
{ |
819
|
|
|
// Restore the full content (only applies when "autoptimize_filter_css_justhead" filter is true). |
820
|
|
|
if ( ! empty( $this->restofcontent ) ) { |
821
|
|
|
$this->content .= $this->restofcontent; |
822
|
|
|
$this->restofcontent = ''; |
823
|
|
|
} |
824
|
|
|
|
825
|
|
|
// type is not added by default. |
826
|
|
|
$type_css = ''; |
827
|
|
|
if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) { |
828
|
|
|
$type_css = 'type="text/css" '; |
829
|
|
|
} |
830
|
|
|
|
831
|
|
|
// Inject the new stylesheets. |
832
|
|
|
$replaceTag = array( '<title', 'before' ); |
833
|
|
|
$replaceTag = apply_filters( 'autoptimize_filter_css_replacetag', $replaceTag, $this->content ); |
834
|
|
|
|
835
|
|
|
if ( $this->inline ) { |
836
|
|
|
foreach ( $this->csscode as $media => $code ) { |
837
|
|
|
$this->inject_in_html( '<style ' . $type_css . 'media="' . $media . '">' . $code . '</style>', $replaceTag ); |
838
|
|
|
} |
839
|
|
|
} else { |
840
|
|
|
if ( $this->defer ) { |
841
|
|
|
$preloadCssBlock = ''; |
842
|
|
|
$inlined_ccss_block = ''; |
843
|
|
|
$noScriptCssBlock = "<noscript id=\"aonoscrcss\">"; |
844
|
|
|
|
845
|
|
|
$defer_inline_code = $this->defer_inline; |
846
|
|
|
if ( ! empty( $defer_inline_code ) ) { |
847
|
|
|
if ( apply_filters( 'autoptimize_filter_css_critcss_minify', true ) ) { |
848
|
|
|
$iCssHash = md5( $defer_inline_code ); |
849
|
|
|
$iCssCache = new autoptimizeCache( $iCssHash, 'css' ); |
850
|
|
|
if ( $iCssCache->check() ) { |
851
|
|
|
// we have the optimized inline CSS in cache. |
852
|
|
|
$defer_inline_code = $iCssCache->retrieve(); |
853
|
|
View Code Duplication |
} else { |
|
|
|
|
854
|
|
|
$cssmin = new autoptimizeCSSmin(); |
855
|
|
|
$tmp_code = trim( $cssmin->run( $defer_inline_code ) ); |
|
|
|
|
856
|
|
|
|
857
|
|
|
if ( ! empty( $tmp_code ) ) { |
858
|
|
|
$defer_inline_code = $tmp_code; |
859
|
|
|
$iCssCache->cache( $defer_inline_code, 'text/css' ); |
860
|
|
|
unset( $tmp_code ); |
861
|
|
|
} |
862
|
|
|
} |
863
|
|
|
} |
864
|
|
|
// inlined critical css set here, but injected when full CSS is injected |
865
|
|
|
// to avoid CSS containing SVG with <title tag receiving the full CSS link. |
866
|
|
|
$inlined_ccss_block = '<style ' . $type_css . 'id="aoatfcss" media="all">' . $defer_inline_code . '</style>'; |
867
|
|
|
} |
868
|
|
|
} |
869
|
|
|
|
870
|
|
|
foreach ( $this->url as $media => $url ) { |
871
|
|
|
$url = $this->url_replace_cdn( $url ); |
872
|
|
|
|
873
|
|
|
// Add the stylesheet either deferred (import at bottom) or normal links in head. |
874
|
|
|
if ( $this->defer ) { |
875
|
|
|
$preloadOnLoad = autoptimizeConfig::get_ao_css_preload_onload(); |
876
|
|
|
|
877
|
|
|
$preloadCssBlock .= '<link rel="preload" as="style" media="' . $media . '" href="' . $url . '" onload="' . $preloadOnLoad . '" />'; |
|
|
|
|
878
|
|
|
$noScriptCssBlock .= '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />'; |
|
|
|
|
879
|
|
|
} else { |
880
|
|
|
if ( strlen( $this->csscode[$media] ) > $this->cssinlinesize ) { |
881
|
|
|
$this->inject_in_html( '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />', $replaceTag ); |
882
|
|
|
} elseif ( strlen( $this->csscode[$media] ) > 0 ) { |
883
|
|
|
$this->inject_in_html( '<style ' . $type_css . 'media="' . $media . '">' . $this->csscode[$media] . '</style>', $replaceTag ); |
884
|
|
|
} |
885
|
|
|
} |
886
|
|
|
} |
887
|
|
|
|
888
|
|
|
if ( $this->defer ) { |
889
|
|
|
$preload_polyfill = autoptimizeConfig::get_ao_css_preload_polyfill(); |
890
|
|
|
$noScriptCssBlock .= '</noscript>'; |
891
|
|
|
// Inject inline critical CSS, the preloaded full CSS and the noscript-CSS. |
892
|
|
|
$this->inject_in_html( $inlined_ccss_block . $preloadCssBlock . $noScriptCssBlock, $replaceTag ); |
|
|
|
|
893
|
|
|
|
894
|
|
|
// Adds preload polyfill at end of body tag. |
895
|
|
|
$this->inject_in_html( |
896
|
|
|
apply_filters( 'autoptimize_css_preload_polyfill', $preload_polyfill ), |
897
|
|
|
apply_filters( 'autoptimize_css_preload_polyfill_injectat', array( '</body>', 'before' ) ) |
898
|
|
|
); |
899
|
|
|
} |
900
|
|
|
} |
901
|
|
|
|
902
|
|
|
// restore comments. |
903
|
|
|
$this->content = $this->restore_comments( $this->content ); |
904
|
|
|
|
905
|
|
|
// restore IE hacks. |
906
|
|
|
$this->content = $this->restore_iehacks( $this->content ); |
907
|
|
|
|
908
|
|
|
// restore (no)script. |
909
|
|
|
$this->content = $this->restore_marked_content( 'SCRIPT', $this->content ); |
910
|
|
|
|
911
|
|
|
// Restore noptimize. |
912
|
|
|
$this->content = $this->restore_noptimize( $this->content ); |
913
|
|
|
|
914
|
|
|
// Return the modified stylesheet. |
915
|
|
|
return $this->content; |
916
|
|
|
} |
917
|
|
|
|
918
|
|
|
static function fixurls( $file, $code ) |
|
|
|
|
919
|
|
|
{ |
920
|
|
|
// Switch all imports to the url() syntax. |
921
|
|
|
$code = preg_replace( '#@import ("|\')(.+?)\.css.*?("|\')#', '@import url("${2}.css")', $code ); |
922
|
|
|
|
923
|
|
|
if ( preg_match_all( self::ASSETS_REGEX, $code, $matches ) ) { |
924
|
|
|
$file = str_replace( WP_ROOT_DIR, '/', $file ); |
925
|
|
|
/** |
926
|
|
|
* rollback as per https://github.com/futtta/autoptimize/issues/94 |
927
|
|
|
* $file = str_replace( AUTOPTIMIZE_WP_CONTENT_NAME, '', $file ); |
928
|
|
|
*/ |
929
|
|
|
$dir = dirname( $file ); // Like /themes/expound/css. |
930
|
|
|
|
931
|
|
|
/** |
932
|
|
|
* $dir should not contain backslashes, since it's used to replace |
933
|
|
|
* urls, but it can contain them when running on Windows because |
934
|
|
|
* fixurls() is sometimes called with `ABSPATH . 'index.php'` |
935
|
|
|
*/ |
936
|
|
|
$dir = str_replace( '\\', '/', $dir ); |
937
|
|
|
unset( $file ); // not used below at all. |
938
|
|
|
|
939
|
|
|
$replace = array(); |
940
|
|
|
foreach ( $matches[1] as $k => $url ) { |
941
|
|
|
// Remove quotes. |
942
|
|
|
$url = trim( $url, " \t\n\r\0\x0B\"'" ); |
943
|
|
|
$noQurl = trim( $url, "\"'" ); |
944
|
|
|
if ( $url !== $noQurl ) { |
945
|
|
|
$removedQuotes = true; |
946
|
|
|
} else { |
947
|
|
|
$removedQuotes = false; |
948
|
|
|
} |
949
|
|
|
|
950
|
|
|
if ( '' === $noQurl ) { |
951
|
|
|
continue; |
952
|
|
|
} |
953
|
|
|
|
954
|
|
|
$url = $noQurl; |
955
|
|
|
if ( '/' === $url[0] || preg_match( '#^(https?://|ftp://|data:)#i', $url ) ) { |
956
|
|
|
// URL is protocol-relative, host-relative or something we don't touch. |
957
|
|
|
continue; |
958
|
|
|
} else { |
959
|
|
|
// Relative URL. |
960
|
|
|
/** |
961
|
|
|
* rollback as per https://github.com/futtta/autoptimize/issues/94 |
962
|
|
|
* $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_CONTENT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) ); |
963
|
|
|
*/ |
964
|
|
|
$newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_ROOT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) ); |
965
|
|
|
$newurl = apply_filters( 'autoptimize_filter_css_fixurl_newurl', $newurl ); |
966
|
|
|
|
967
|
|
|
/** |
968
|
|
|
* Hash the url + whatever was behind potentially for replacement |
969
|
|
|
* We must do this, or different css classes referencing the same bg image (but |
970
|
|
|
* different parts of it, say, in sprites and such) loose their stuff... |
971
|
|
|
*/ |
972
|
|
|
$hash = md5( $url . $matches[2][$k] ); |
973
|
|
|
$code = str_replace( $matches[0][$k], $hash, $code ); |
974
|
|
|
|
975
|
|
|
if ( $removedQuotes ) { |
976
|
|
|
$replace[$hash] = "url('" . $newurl . "')" . $matches[2][$k]; |
977
|
|
|
} else { |
978
|
|
|
$replace[$hash] = 'url(' . $newurl . ')' . $matches[2][$k]; |
979
|
|
|
} |
980
|
|
|
} |
981
|
|
|
} |
982
|
|
|
|
983
|
|
|
$code = self::replace_longest_matches_first( $code, $replace ); |
984
|
|
|
} |
985
|
|
|
|
986
|
|
|
return $code; |
987
|
|
|
} |
988
|
|
|
|
989
|
|
|
private function ismovable( $tag ) |
990
|
|
|
{ |
991
|
|
|
if ( ! $this->aggregate ) { |
992
|
|
|
return false; |
993
|
|
|
} |
994
|
|
|
|
995
|
|
|
if ( ! empty( $this->whitelist ) ) { |
996
|
|
|
foreach ( $this->whitelist as $match ) { |
|
|
|
|
997
|
|
|
if ( false !== strpos( $tag, $match ) ) { |
998
|
|
|
return true; |
999
|
|
|
} |
1000
|
|
|
} |
1001
|
|
|
// no match with whitelist. |
1002
|
|
|
return false; |
1003
|
|
|
} else { |
1004
|
|
|
if ( is_array( $this->dontmove ) && ! empty( $this->dontmove ) ) { |
1005
|
|
|
foreach ( $this->dontmove as $match ) { |
1006
|
|
|
if ( false !== strpos( $tag, $match ) ) { |
1007
|
|
|
// Matched something. |
1008
|
|
|
return false; |
1009
|
|
|
} |
1010
|
|
|
} |
1011
|
|
|
} |
1012
|
|
|
|
1013
|
|
|
// If we're here it's safe to move. |
1014
|
|
|
return true; |
1015
|
|
|
} |
1016
|
|
|
} |
1017
|
|
|
|
1018
|
|
|
private function can_inject_late( $cssPath, $css ) |
1019
|
|
|
{ |
1020
|
|
|
$consider_minified_array = apply_filters( 'autoptimize_filter_css_consider_minified', false, $cssPath ); |
1021
|
|
|
if ( true !== $this->inject_min_late ) { |
1022
|
|
|
// late-inject turned off. |
1023
|
|
|
return false; |
1024
|
|
|
} elseif ( ( false === strpos( $cssPath, 'min.css' ) ) && ( str_replace( $consider_minified_array, '', $cssPath ) === $cssPath ) ) { |
1025
|
|
|
// file not minified based on filename & filter. |
1026
|
|
|
return false; |
1027
|
|
|
} elseif ( false !== strpos( $css, '@import' ) ) { |
1028
|
|
|
// can't late-inject files with imports as those need to be aggregated. |
1029
|
|
|
return false; |
1030
|
|
|
} elseif ( ( false !== strpos( $css, '@font-face' ) ) && ( apply_filters( 'autoptimize_filter_css_fonts_cdn', false ) === true ) && ( ! empty( $this->cdn_url ) ) ) { |
1031
|
|
|
// don't late-inject CSS with font-src's if fonts are set to be CDN'ed. |
1032
|
|
|
return false; |
1033
|
|
|
} elseif ( ( ( $this->datauris == true ) || ( ! empty( $this->cdn_url ) ) ) && preg_match( '#background[^;}]*url\(#Ui', $css ) ) { |
|
|
|
|
1034
|
|
|
// don't late-inject CSS with images if CDN is set OR if image inlining is on. |
1035
|
|
|
return false; |
1036
|
|
|
} else { |
1037
|
|
|
// phew, all is safe, we can late-inject. |
1038
|
|
|
return true; |
1039
|
|
|
} |
1040
|
|
|
} |
1041
|
|
|
|
1042
|
|
|
/** |
1043
|
|
|
* Minifies (and cdn-replaces) a single local css file |
1044
|
|
|
* and returns its (cached) url. |
1045
|
|
|
* |
1046
|
|
|
* @param string $filepath Filepath. |
1047
|
|
|
* @param bool $cache_miss Optional. Force a cache miss. Default false. |
1048
|
|
|
* |
1049
|
|
|
* @return bool|string Url pointing to the minified css file or false. |
1050
|
|
|
*/ |
1051
|
|
|
public function minify_single( $filepath, $cache_miss = false ) |
1052
|
|
|
{ |
1053
|
|
|
$contents = $this->prepare_minify_single( $filepath ); |
1054
|
|
|
|
1055
|
|
|
if ( empty( $contents ) ) { |
1056
|
|
|
return false; |
1057
|
|
|
} |
1058
|
|
|
|
1059
|
|
|
// Check cache. |
1060
|
|
|
$hash = 'single_' . md5( $contents ); |
1061
|
|
|
$cache = new autoptimizeCache( $hash, 'css' ); |
1062
|
|
|
do_action( 'autoptimize_action_css_hash', $hash ); |
1063
|
|
|
|
1064
|
|
|
// If not in cache already, minify... |
1065
|
|
|
if ( ! $cache->check() || $cache_miss ) { |
1066
|
|
|
// Fixurls... |
1067
|
|
|
$contents = self::fixurls( $filepath, $contents ); |
1068
|
|
|
// CDN-replace any referenced assets if needed... |
1069
|
|
|
$contents = $this->hide_fontface_and_maybe_cdn( $contents ); |
1070
|
|
|
$contents = $this->replace_urls( $contents ); |
1071
|
|
|
$contents = $this->restore_fontface( $contents ); |
1072
|
|
|
// Now minify... |
1073
|
|
|
$cssmin = new autoptimizeCSSmin(); |
1074
|
|
|
$contents = trim( $cssmin->run( $contents ) ); |
1075
|
|
|
// Store in cache. |
1076
|
|
|
$cache->cache( $contents, 'text/css' ); |
1077
|
|
|
} |
1078
|
|
|
|
1079
|
|
|
$url = $this->build_minify_single_url( $cache ); |
1080
|
|
|
|
1081
|
|
|
return $url; |
1082
|
|
|
} |
1083
|
|
|
|
1084
|
|
|
/** |
1085
|
|
|
* Returns whether we're doing aggregation or not. |
1086
|
|
|
* |
1087
|
|
|
* @return bool |
1088
|
|
|
*/ |
1089
|
|
|
public function aggregating() |
1090
|
|
|
{ |
1091
|
|
|
return $this->aggregate; |
1092
|
|
|
} |
1093
|
|
|
|
1094
|
|
|
public function getOptions() |
1095
|
|
|
{ |
1096
|
|
|
return $this->options; |
1097
|
|
|
} |
1098
|
|
|
|
1099
|
|
|
public function replaceOptions( $options ) |
1100
|
|
|
{ |
1101
|
|
|
$this->options = $options; |
1102
|
|
|
} |
1103
|
|
|
|
1104
|
|
|
public function setOption( $name, $value ) |
1105
|
|
|
{ |
1106
|
|
|
$this->options[$name] = $value; |
1107
|
|
|
$this->$name = $value; |
1108
|
|
|
} |
1109
|
|
|
|
1110
|
|
|
public function getOption( $name ) |
1111
|
|
|
{ |
1112
|
|
|
return $this->options[$name]; |
1113
|
|
|
} |
1114
|
|
|
} |
1115
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.