Completed
Push — master ( 471c26...e80620 )
by frank
01:49
created

autoptimizeStyles::replaceOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
    const FONT_FACE_REGEX = '~@font-face\s*(\{(?:[^{}]+|(?1))*\})~xsi'; // added `i` flag for case-insensitivity.
18
19
    /**
20
     * Store CSS.
21
     *
22
     * @var array
23
     */
24
    private $css = array();
25
26
    /**
27
     * To store CSS code
28
     *
29
     * @var array
30
     */
31
    private $csscode = array();
32
33
    /**
34
     * To store urls
35
     *
36
     * @var array
37
     */
38
    private $url = array();
39
40
    /**
41
     * String to store rest of content (when old setting "only in head" is used)
42
     *
43
     * @var string
44
     */
45
    private $restofcontent = '';
46
47
    /**
48
     * Setting to change small images to inline CSS
49
     *
50
     * @var bool
51
     */
52
    private $datauris = false;
53
54
    /**
55
     * Array to store hashmap
56
     *
57
     * @var array
58
     */
59
    private $hashmap = array();
60
61
    /**
62
     * Flag to indicate if CSS is already minified
63
     *
64
     * @var bool
65
     */
66
    private $alreadyminified = false;
67
68
    /**
69
     * Setting if CSS should be aggregated
70
     *
71
     * @var bool
72
     */
73
    private $aggregate = true;
74
75
    /**
76
     * Setting if all CSS should be inlined
77
     *
78
     * @var bool
79
     */
80
    private $inline = false;
81
82
    /**
83
     * Setting if CSS should be deferred
84
     *
85
     * @var bool
86
     */
87
    private $defer = false;
88
89
    /**
90
     * Setting for to be inlined CSS.
91
     *
92
     * @var string
93
     */
94
    private $defer_inline = '';
95
96
    /**
97
     * Setting for allowlist of what should be aggregated.
98
     *
99
     * @var string
100
     */
101
    private $allowlist = '';
102
103
    /**
104
     * Setting (only filter) for size under which CSS should be inlined instead of linked.
105
     *
106
     * @var string
107
     */
108
    private $cssinlinesize = '';
109
110
    /**
111
     * Setting (only filter) of CSS that can be removed.
112
     *
113
     * @var array
114
     */
115
    private $cssremovables = array();
116
117
    /**
118
     * Setting: should inline CSS be aggregated.
119
     *
120
     * @var bool
121
     */
122
    private $include_inline = false;
123
124
    /**
125
     * Setting (only filter) if minified CSS can be injected after minificatoin of aggregated CSS.
126
     *
127
     * @var bool
128
     */
129
    private $inject_min_late = true;
130
131
    /**
132
     * Holds all exclusions.
133
     *
134
     * @var array
135
     */
136
    private $dontmove = array();
137
138
    /**
139
     * Holds all options.
140
     *
141
     * @var array
142
     */
143
    private $options = array();
144
145
    /**
146
     * Setting; should excluded CSS-files be minified.
147
     *
148
     * @var bool
149
     */
150
    private $minify_excluded = true;
151
152
    /**
153
     * Setting (filter only); should all media-attributes be forced to "all".
154
     *
155
     * @var bool
156
     */
157
    private $media_force_all = false;
158
159
    /**
160
     * Reads the page and collects style tags.
161
     *
162
     * @param array $options all options.
163
     */
164
    public function read( $options )
165
    {
166
        $noptimize_css = apply_filters( 'autoptimize_filter_css_noptimize', false, $this->content );
167
        if ( $noptimize_css ) {
168
            return false;
169
        }
170
171
        $allowlist_css = apply_filters( 'autoptimize_filter_css_allowlist', '', $this->content );
172
        $allowlist_css = apply_filters( 'autoptimize_filter_css_whitelist', $allowlist_css, $this->content ); // fixme: to be removed in next version.
173 View Code Duplication
        if ( ! empty( $allowlist_css ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
174
            $this->allowlist = array_filter( array_map( 'trim', explode( ',', $allowlist_css ) ) );
0 ignored issues
show
Documentation Bug introduced by
It seems like array_filter(array_map('...(',', $allowlist_css))) of type array is incompatible with the declared type string of property $allowlist.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
175
        }
176
177
        $removable_css = apply_filters( 'autoptimize_filter_css_removables', '' );
178 View Code Duplication
        if ( ! empty( $removable_css ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
179
            $this->cssremovables = array_filter( array_map( 'trim', explode( ',', $removable_css ) ) );
180
        }
181
182
        $this->cssinlinesize = apply_filters( 'autoptimize_filter_css_inlinesize', 256 );
183
184
        // filter to "late inject minified CSS", default to true for now (it is faster).
185
        $this->inject_min_late = apply_filters( 'autoptimize_filter_css_inject_min_late', true );
186
187
        // Remove everything that's not the header.
188 View Code Duplication
        if ( apply_filters( 'autoptimize_filter_css_justhead', $options['justhead'] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
189
            $content             = explode( '</head>', $this->content, 2 );
190
            $this->content       = $content[0] . '</head>';
191
            $this->restofcontent = $content[1];
192
        }
193
194
        // Determine whether we're doing CSS-files aggregation or not.
195
        if ( isset( $options['aggregate'] ) && ! $options['aggregate'] ) {
196
            $this->aggregate = false;
197
        }
198
        // Returning true for "dontaggregate" turns off aggregation.
199
        if ( $this->aggregate && apply_filters( 'autoptimize_filter_css_dontaggregate', false ) ) {
200
            $this->aggregate = false;
201
        }
202
203
        // include inline?
204
        if ( apply_filters( 'autoptimize_css_include_inline', $options['include_inline'] ) ) {
205
            $this->include_inline = true;
206
        }
207
208
        // List of CSS strings which are excluded from autoptimization.
209
        $exclude_css = apply_filters( 'autoptimize_filter_css_exclude', $options['css_exclude'], $this->content );
210 View Code Duplication
        if ( '' !== $exclude_css ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
211
            $this->dontmove = array_filter( array_map( 'trim', explode( ',', $exclude_css ) ) );
212
        } else {
213
            $this->dontmove = array();
214
        }
215
216
        // forcefully exclude CSS with data-noptimize attrib.
217
        $this->dontmove[] = 'data-noptimize';
218
219
        // Should we defer css?
220
        // value: true / false.
221
        $this->defer = $options['defer'];
222
        $this->defer = apply_filters( 'autoptimize_filter_css_defer', $this->defer, $this->content );
223
224
        // Should we inline while deferring?
225
        // value: inlined CSS.
226
        $this->defer_inline = apply_filters( 'autoptimize_filter_css_defer_inline', $this->sanitize_css( $options['defer_inline'] ), $this->content );
227
228
        // Should we inline?
229
        // value: true / false.
230
        $this->inline = $options['inline'];
231
        $this->inline = apply_filters( 'autoptimize_filter_css_inline', $this->inline, $this->content );
232
233
        // Store cdn url.
234
        $this->cdn_url = $options['cdn_url'];
235
236
        // Store data: URIs setting for later use.
237
        $this->datauris = $options['datauris'];
238
239
        // Determine whether excluded files should be minified if not yet so.
240
        if ( ! $options['minify_excluded'] && $options['aggregate'] ) {
241
            $this->minify_excluded = false;
242
        }
243
        $this->minify_excluded = apply_filters( 'autoptimize_filter_css_minify_excluded', $this->minify_excluded, '' );
244
245
        // should we force all media-attributes to all?
246
        $this->media_force_all = apply_filters( 'autoptimize_filter_css_tagmedia_forceall', false );
247
248
        // noptimize me.
249
        $this->content = $this->hide_noptimize( $this->content );
250
251
        // Exclude (no)script, as those may contain CSS which should be left as is.
252
        $this->content = $this->replace_contents_with_marker_if_exists(
253
            'SCRIPT',
254
            '<script',
255
            '#<(?:no)?script.*?<\/(?:no)?script>#is',
256
            $this->content
257
        );
258
259
        // Save IE hacks.
260
        $this->content = $this->hide_iehacks( $this->content );
261
262
        // Hide HTML comments.
263
        $this->content = $this->hide_comments( $this->content );
264
265
        // Get <style> and <link>.
266
        if ( preg_match_all( '#(<style[^>]*>.*</style>)|(<link[^>]*stylesheet[^>]*>)#Usmi', $this->content, $matches ) ) {
267
268
            foreach ( $matches[0] as $tag ) {
269
                if ( $this->isremovable( $tag, $this->cssremovables ) ) {
270
                    $this->content = str_replace( $tag, '', $this->content );
271
                } elseif ( $this->ismovable( $tag ) ) {
272
                    // Get the media.
273
                    if ( false !== strpos( $tag, 'media=' ) ) {
274
                        preg_match( '#media=(?:"|\')([^>]*)(?:"|\')#Ui', $tag, $medias );
275
                        $medias = explode( ',', $medias[1] );
276
                        $media  = array();
277
                        foreach ( $medias as $elem ) {
278
                            if ( empty( $elem ) ) {
279
                                $elem = 'all';
280
                            }
281
282
                            $media[] = $elem;
283
                        }
284
                    } else {
285
                        // No media specified - applies to all.
286
                        $media = array( 'all' );
287
                    }
288
289
                    // forcing media attribute to all to merge all in one file.
290
                    if ( $this->media_force_all ) {
291
                        $media = array( 'all' );
292
                    }
293
294
                    $media = apply_filters( 'autoptimize_filter_css_tagmedia', $media, $tag );
295
296
                    if ( preg_match( '#<link.*href=("|\')(.*)("|\')#Usmi', $tag, $source ) ) {
297
                        // <link>.
298
                        $url  = current( explode( '?', $source[2], 2 ) );
299
                        $path = $this->getpath( $url );
300
301
                        if ( false !== $path && preg_match( '#\.css$#', $path ) ) {
302
                            // Good link.
303
                            $this->css[] = array( $media, $path );
304
                        } else {
305
                            // Link is dynamic (.php etc).
306
                            $new_tag = $this->optionally_defer_excluded( $tag, 'none' );
307
                            if ( '' !== $new_tag && $new_tag !== $tag ) {
308
                                $this->content = str_replace( $tag, $new_tag, $this->content );
309
                            }
310
                            $tag = '';
311
                        }
312
                    } else {
313
                        // Inline css in style tags can be wrapped in comment tags, so restore comments.
314
                        $tag = $this->restore_comments( $tag );
315
                        preg_match( '#<style.*>(.*)</style>#Usmi', $tag, $code );
316
317
                        // And re-hide them to be able to to the removal based on tag.
318
                        $tag = $this->hide_comments( $tag );
319
320
                        if ( $this->include_inline ) {
321
                            $code        = preg_replace( '#^.*<!\[CDATA\[(?:\s*\*/)?(.*)(?://|/\*)\s*?\]\]>.*$#sm', '$1', $code[1] );
322
                            $this->css[] = array( $media, 'INLINE;' . $code );
323
                        } else {
324
                            $tag = '';
325
                        }
326
                    }
327
328
                    // Remove the original style tag.
329
                    $this->content = str_replace( $tag, '', $this->content );
330
                } else {
331
                    if ( preg_match( '#<link.*href=("|\')(.*)("|\')#Usmi', $tag, $source ) ) {
332
                        $exploded_url = explode( '?', $source[2], 2 );
333
                        $url          = $exploded_url[0];
334
                        $path         = $this->getpath( $url );
335
                        $new_tag      = $tag;
336
337
                        // Excluded CSS, minify that file:
338
                        // -> if aggregate is on and exclude minify is on
339
                        // -> if aggregate is off and the file is not in dontmove.
340 View Code Duplication
                        if ( $path && $this->minify_excluded ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
341
                            $consider_minified_array = apply_filters( 'autoptimize_filter_css_consider_minified', false );
342
                            if ( ( false === $this->aggregate && str_replace( $this->dontmove, '', $path ) === $path ) || ( true === $this->aggregate && ( false === $consider_minified_array || str_replace( $consider_minified_array, '', $path ) === $path ) ) ) {
343
                                $minified_url = $this->minify_single( $path );
344
                                if ( ! empty( $minified_url ) ) {
345
                                    // Replace orig URL with cached minified URL.
346
                                    $new_tag = str_replace( $url, $minified_url, $tag );
347
                                } elseif ( apply_filters( 'autoptimize_filter_ccsjs_remove_empty_minified_url', false ) ) {
348
                                    // Remove the original style tag, because cache content is empty but only if
349
                                    // filter is true-ed because $minified_url is also false if file is minified already.
350
                                    $new_tag = '';
351
                                }
352
                            }
353
                        }
354
355
                        if ( '' !== $new_tag ) {
356
                            // Optionally defer (preload) non-aggregated CSS.
357
                            $new_tag = $this->optionally_defer_excluded( $new_tag, $url );
358
                        }
359
360
                        // And replace!
361 View Code Duplication
                        if ( ( '' !== $new_tag && $new_tag !== $tag ) || ( '' === $new_tag && apply_filters( 'autoptimize_filter_css_remove_empty_files', false ) ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
362
                            $this->content = str_replace( $tag, $new_tag, $this->content );
363
                        }
364
                    }
365
                }
366
            }
367
            return true;
368
        }
369
370
        // Really, no styles?
371
        return false;
372
    }
373
374
    /**
375
     * Checks if non-optimized CSS is to be preloaded and if so return
376
     * the tag with preload code.
377
     *
378
     * @param string $tag (required).
379
     * @param string $url (optional).
380
     *
381
     * @return string $new_tag
382
     */
383
    private function optionally_defer_excluded( $tag, $url = '' )
384
    {
385
        // Defer single CSS if "inline & defer" is ON and there is inline CSS.
386
        if ( ! empty( $tag ) && false === strpos( $tag, ' onload=' ) && $this->defer && ! empty( $this->defer_inline ) && apply_filters( 'autoptimize_filter_css_defer_excluded', true, $tag ) ) {
387
            // get media attribute and based on that create onload JS attribute value.
388
            if ( false === strpos( $tag, 'media=' ) ) {
389
                $tag = str_replace( '<link', "<link media='all'", $tag );
390
            }
391
392
            preg_match( '#media=(?:"|\')([^>]*)(?:"|\')#Ui', $tag, $_medias );
393
            $_media          = $_medias[1];
394
            $_preload_onload = autoptimizeConfig::get_ao_css_preload_onload( $_media );
395
396
            if ( 'print' !== $_media ) {
397
                // If not media=print, adapt original <link> element for CSS to be preloaded and add <noscript>-version for fallback.
398
                $new_tag = '<noscript>' . autoptimizeUtils::remove_id_from_node( $tag ) . '</noscript>' . str_replace(
399
                    $_medias[0],
400
                    "media='print' onload=\"" . $_preload_onload . '"',
401
                    $tag
402
                );
403
404
                // Optionally (but default false) preload the (excluded) CSS-file.
405
                if ( apply_filters( 'autoptimize_fitler_css_preload_and_print', false ) && 'none' !== $url ) {
406
                    $new_tag = '<link rel="preload" as="stylesheet" href="' . $url . '"/>' . $new_tag;
407
                }
408
            } else {
409
                $new_tag = $tag;
410
            }
411
412
            return $new_tag;
413
        }
414
415
        // Return unchanged $tag.
416
        return $tag;
417
    }
418
419
    /**
420
     * Checks if the local file referenced by $path is a valid
421
     * candidate for being inlined into a data: URI
422
     *
423
     * @param string $path image path.
424
     * @return boolean
425
     */
426
    private function is_datauri_candidate( $path )
427
    {
428
        // Call only once since it's called from a loop.
429
        static $max_size = null;
430
        if ( null === $max_size ) {
431
            $max_size = $this->get_datauri_maxsize();
432
        }
433
434
        if ( $path && preg_match( '#\.(jpe?g|png|gif|webp|bmp)$#i', $path ) &&
435
            file_exists( $path ) && is_readable( $path ) && filesize( $path ) <= $max_size ) {
436
437
            // Seems we have a candidate.
438
            $is_candidate = true;
439
        } else {
440
            // Filter allows overriding default decision (which checks for local file existence).
441
            $is_candidate = apply_filters( 'autoptimize_filter_css_is_datauri_candidate', false, $path );
442
        }
443
444
        return $is_candidate;
445
    }
446
447
    /**
448
     * Returns the amount of bytes that shouldn't be exceeded if a file is to
449
     * be inlined into a data: URI. Defaults to 4096, passed through
450
     * `autoptimize_filter_css_datauri_maxsize` filter.
451
     *
452
     * @return mixed
453
     */
454
    private function get_datauri_maxsize()
455
    {
456
        static $max_size = null;
457
458
        /**
459
         * No need to apply the filter multiple times in case the
460
         * method itself is invoked multiple times during a single request.
461
         * This prevents some wild stuff like having different maxsizes
462
         * for different files/site-sections etc. But if you're into that sort
463
         * of thing you're probably better of building assets completely
464
         * outside of WordPress anyway.
465
         */
466
        if ( null === $max_size ) {
467
            $max_size = (int) apply_filters( 'autoptimize_filter_css_datauri_maxsize', 4096 );
468
        }
469
470
        return $max_size;
471
    }
472
473
    private function check_datauri_exclude_list( $url )
474
    {
475
        static $exclude_list = null;
476
        $no_datauris         = array();
477
478
        // Again, skip doing certain stuff repeatedly when loop-called.
479
        if ( null === $exclude_list ) {
480
            $exclude_list = apply_filters( 'autoptimize_filter_css_datauri_exclude', '' );
481
            $no_datauris  = array_filter( array_map( 'trim', explode( ',', $exclude_list ) ) );
482
        }
483
484
        $matched = false;
485
486
        if ( ! empty( $exclude_list ) ) {
487
            foreach ( $no_datauris as $no_datauri ) {
488
                if ( false !== strpos( $url, $no_datauri ) ) {
489
                    $matched = true;
490
                    break;
491
                }
492
            }
493
        }
494
495
        return $matched;
496
    }
497
498
    private function build_or_get_datauri_image( $path )
499
    {
500
        /**
501
         * TODO/FIXME: document the required return array format, or better yet,
502
         * use a string, since we don't really need an array for this. That would, however,
503
         * require changing even more code, which is not happening right now...
504
         */
505
506
        // Allows short-circuiting datauri generation for an image.
507
        $result = apply_filters( 'autoptimize_filter_css_datauri_image', array(), $path );
508
        if ( ! empty( $result ) ) {
509
            if ( is_array( $result ) && isset( $result['full'] ) && isset( $result['base64data'] ) ) {
510
                return $result;
511
            }
512
        }
513
514
        $hash  = md5( $path );
515
        $check = new autoptimizeCache( $hash, 'img' );
516
        if ( $check->check() ) {
517
            // we have the base64 image in cache.
518
            $head_and_data = $check->retrieve();
519
            $_base64data   = explode( ';base64,', $head_and_data );
520
            $base64data    = $_base64data[1];
521
            unset( $_base64data );
522
        } else {
523
            // It's an image and we don't have it in cache, get the type by extension.
524
            $exploded_path = explode( '.', $path );
525
            $type          = end( $exploded_path );
526
527
            switch ( $type ) {
528
                case 'jpg':
529
                case 'jpeg':
530
                    $dataurihead = 'data:image/jpeg;base64,';
531
                    break;
532
                case 'gif':
533
                    $dataurihead = 'data:image/gif;base64,';
534
                    break;
535
                case 'png':
536
                    $dataurihead = 'data:image/png;base64,';
537
                    break;
538
                case 'bmp':
539
                    $dataurihead = 'data:image/bmp;base64,';
540
                    break;
541
                case 'webp':
542
                    $dataurihead = 'data:image/webp;base64,';
543
                    break;
544
                default:
545
                    $dataurihead = 'data:application/octet-stream;base64,';
546
            }
547
548
            // Encode the data.
549
            $base64data    = base64_encode( file_get_contents( $path ) );
550
            $head_and_data = $dataurihead . $base64data;
551
552
            // Save in cache.
553
            $check->cache( $head_and_data, 'text/plain' );
554
        }
555
        unset( $check );
556
557
        return array(
558
            'full'       => $head_and_data,
559
            'base64data' => $base64data,
560
        );
561
    }
562
563
    /**
564
     * Given an array of key/value pairs to replace in $string,
565
     * it does so by replacing the longest-matching strings first.
566
     *
567
     * @param string $string string in which to replace.
568
     * @param array  $replacements to be replaced strings and replacement.
569
     *
570
     * @return string
571
     */
572
    protected static function replace_longest_matches_first( $string, $replacements = array() )
573
    {
574
        if ( ! empty( $replacements ) ) {
575
            // Sort the replacements array by key length in desc order (so that the longest strings are replaced first).
576
            $keys = array_map( 'strlen', array_keys( $replacements ) );
577
            array_multisort( $keys, SORT_DESC, $replacements );
578
            $string = str_replace( array_keys( $replacements ), array_values( $replacements ), $string );
579
        }
580
581
        return $string;
582
    }
583
584
    /**
585
     * Rewrites/Replaces any ASSETS_REGEX-matching urls in a string.
586
     * Removes quotes/cruft around each one and passes it through to
587
     * `autoptimizeBase::url_replace_cdn()`.
588
     * Replacements are performed in a `longest-match-replaced-first` way.
589
     *
590
     * @param string $code CSS code.
591
     *
592
     * @return string
593
     */
594
    public function replace_urls( $code = '' )
595
    {
596
        $replacements = array();
597
598
        preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches );
599
        if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) {
600
            foreach ( $url_src_matches[1] as $count => $original_url ) {
601
                // Removes quotes and other cruft.
602
                $url = trim( $original_url, " \t\n\r\0\x0B\"'" );
603
604
                /**
605
                 * TODO/FIXME: Add a way for other code / callable to be called here
606
                 * and provide it's own results for the $replacements array
607
                 * for the "current" key.
608
                 * If such a result is returned/provided, we sholud then avoid
609
                 * calling url_replace_cdn() here for the current iteration.
610
                 *
611
                 * This would maybe allow the inlining logic currently present
612
                 * in `autoptimizeStyles::rewrite_assets()` to be "pulled out"
613
                 * and given as a callable to this method or something... and
614
                 * then we could "just" call `replace_urls()` from within
615
                 * `autoptimizeStyles::rewrite_assets()` and avoid some
616
                 * (currently present) code/logic duplication.
617
                 */
618
619
                // Do CDN replacement if needed.
620 View Code Duplication
                if ( ! empty( $this->cdn_url ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
621
                    $replacement_url = $this->url_replace_cdn( $url );
622
                    // Prepare replacements array.
623
                    $replacements[ $url_src_matches[1][ $count ] ] = str_replace(
624
                        $original_url, $replacement_url, $url_src_matches[1][ $count ]
625
                    );
626
                }
627
            }
628
        }
629
630
        $code = self::replace_longest_matches_first( $code, $replacements );
631
632
        return $code;
633
    }
634
635
    /**
636
     * "Hides" @font-face declarations by replacing them with `%%FONTFACE%%` markers.
637
     * Also does CDN replacement of any font-urls within those declarations if the `autoptimize_filter_css_fonts_cdn`
638
     * filter is used.
639
     *
640
     * @param string $code HTML being processed to hide fonts.
641
     * @return string
642
     */
643
    public function hide_fontface_and_maybe_cdn( $code )
644
    {
645
        // Proceed only if @font-face declarations exist within $code.
646
        preg_match_all( self::FONT_FACE_REGEX, $code, $fontfaces );
647
        if ( isset( $fontfaces[0] ) ) {
648
            // Check if we need to cdn fonts or not.
649
            $do_font_cdn = apply_filters( 'autoptimize_filter_css_fonts_cdn', false );
650
651
            foreach ( $fontfaces[0] as $full_match ) {
652
                // Keep original match so we can search/replace it.
653
                $match_search = $full_match;
654
655
                // Do font cdn if needed.
656
                if ( $do_font_cdn ) {
657
                    $full_match = $this->replace_urls( $full_match );
658
                }
659
660
                // Replace declaration with its base64 encoded string.
661
                $replacement = self::build_marker( 'FONTFACE', $full_match );
662
                $code        = str_replace( $match_search, $replacement, $code );
663
            }
664
        }
665
666
        return $code;
667
    }
668
669
    /**
670
     * Restores original @font-face declarations that have been "hidden"
671
     * using `hide_fontface_and_maybe_cdn()`.
672
     *
673
     * @param string $code HTML being processed to unhide fonts.
674
     * @return string
675
     */
676
    public function restore_fontface( $code )
677
    {
678
        return $this->restore_marked_content( 'FONTFACE', $code );
679
    }
680
681
    /**
682
     * Re-write (and/or inline) referenced assets.
683
     *
684
     * @param string $code HTML being processed rewrite assets.
685
     * @return string
686
     */
687
    public function rewrite_assets( $code )
688
    {
689
        // Handle @font-face rules by hiding and processing them separately.
690
        $code = $this->hide_fontface_and_maybe_cdn( $code );
691
692
        /**
693
         * TODO/FIXME:
694
         * Certain code parts below are kind-of repeated now in `replace_urls()`, which is not ideal.
695
         * There is maybe a way to separate/refactor things and then be able to keep
696
         * the ASSETS_REGEX rewriting/handling logic in a single place (along with removing quotes/cruft from matched urls).
697
         * See comments in `replace_urls()` regarding this. The idea is to extract the inlining
698
         * logic out (which is the only real difference between replace_urls() and the code below), but still
699
         * achieve identical results as before.
700
         */
701
702
        // Re-write (and/or inline) URLs to point them to the CDN host.
703
        $url_src_matches = array();
704
        $imgreplace      = array();
705
706
        // Matches and captures anything specified within the literal `url()` and excludes those containing data: URIs.
707
        preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches );
708
        if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) {
709
            foreach ( $url_src_matches[1] as $count => $original_url ) {
710
                // Removes quotes and other cruft.
711
                $url = trim( $original_url, " \t\n\r\0\x0B\"'" );
712
713
                // If datauri inlining is turned on, do it.
714
                $inlined = false;
715
                if ( $this->datauris ) {
716
                    $iurl = $url;
717
                    if ( false !== strpos( $iurl, '?' ) ) {
718
                        $iurl = strtok( $iurl, '?' );
719
                    }
720
721
                    $ipath = $this->getpath( $iurl );
722
723
                    $excluded = $this->check_datauri_exclude_list( $ipath );
724
                    if ( ! $excluded ) {
725
                        $is_datauri_candidate = $this->is_datauri_candidate( $ipath );
0 ignored issues
show
Security Bug introduced by
It seems like $ipath defined by $this->getpath($iurl) on line 721 can also be of type false; however, autoptimizeStyles::is_datauri_candidate() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
726
                        if ( $is_datauri_candidate ) {
727
                            $datauri    = $this->build_or_get_datauri_image( $ipath );
728
                            $base64data = $datauri['base64data'];
0 ignored issues
show
Unused Code introduced by
$base64data is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
729
                            // Add it to the list for replacement.
730
                            $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace(
731
                                $original_url,
732
                                $datauri['full'],
733
                                $url_src_matches[1][ $count ]
734
                            );
735
                            $inlined                                     = true;
736
                        }
737
                    }
738
                }
739
740
                /**
741
                 * Doing CDN URL replacement for every found match (if CDN is
742
                 * specified). This way we make sure to do it even if
743
                 * inlining isn't turned on, or if a resource is skipped from
744
                 * being inlined for whatever reason above.
745
                 */
746 View Code Duplication
                if ( ! $inlined && ( ! empty( $this->cdn_url ) || has_filter( 'autoptimize_filter_base_replace_cdn' ) ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
747
                    // Just do the "simple" CDN replacement.
748
                    $replacement_url                             = $this->url_replace_cdn( $url );
749
                    $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace(
750
                        $original_url, $replacement_url, $url_src_matches[1][ $count ]
751
                    );
752
                }
753
            }
754
        }
755
756
        $code = self::replace_longest_matches_first( $code, $imgreplace );
757
758
        // Replace back font-face markers with actual font-face declarations.
759
        $code = $this->restore_fontface( $code );
760
761
        return $code;
762
    }
763
764
    /**
765
     * Joins and optimizes CSS.
766
     */
767
    public function minify()
768
    {
769
        foreach ( $this->css as $group ) {
770
            list( $media, $css ) = $group;
771
            if ( preg_match( '#^INLINE;#', $css ) ) {
772
                // <style>.
773
                $css      = preg_replace( '#^INLINE;#', '', $css );
774
                $css      = self::fixurls( ABSPATH . 'index.php', $css ); // ABSPATH already contains a trailing slash.
775
                $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, '' );
776
                if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
777
                    $css                   = $tmpstyle;
778
                    $this->alreadyminified = true;
779
                }
780
            } else {
781
                // <link>
782
                if ( false !== $css && file_exists( $css ) && is_readable( $css ) ) {
783
                    $css_path = $css;
784
                    $css      = self::fixurls( $css_path, file_get_contents( $css_path ) );
785
                    $css      = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $css );
786
                    $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, $css_path );
787 View Code Duplication
                    if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
788
                        $css                   = $tmpstyle;
789
                        $this->alreadyminified = true;
790
                    } elseif ( $this->can_inject_late( $css_path, $css ) ) {
791
                        $css = self::build_injectlater_marker( $css_path, md5( $css ) );
792
                    }
793
                } else {
794
                    // Couldn't read CSS. Maybe getpath isn't working?
795
                    $css = '';
796
                }
797
            }
798
799
            foreach ( $media as $elem ) {
800
                if ( ! empty( $css ) ) {
801
                    if ( ! isset( $this->csscode[ $elem ] ) ) {
802
                        $this->csscode[ $elem ] = '';
803
                    }
804
                    $this->csscode[ $elem ] .= "\n/*FILESTART*/" . $css;
805
                }
806
            }
807
        }
808
809
        // Check for duplicate code.
810
        $md5list = array();
811
        $tmpcss  = $this->csscode;
812
        foreach ( $tmpcss as $media => $code ) {
813
            $md5sum    = md5( $code );
814
            $medianame = $media;
815
            foreach ( $md5list as $med => $sum ) {
816
                // If same code.
817
                if ( $sum === $md5sum ) {
818
                    // Add the merged code.
819
                    $medianame                   = $med . ', ' . $media;
820
                    $this->csscode[ $medianame ] = $code;
821
                    $md5list[ $medianame ]       = $md5list[ $med ];
822
                    unset( $this->csscode[ $med ], $this->csscode[ $media ], $md5list[ $med ] );
823
                }
824
            }
825
            $md5list[ $medianame ] = $md5sum;
826
        }
827
        unset( $tmpcss );
828
829
        // Manage @imports, while is for recursive import management.
830
        foreach ( $this->csscode as &$thiscss ) {
831
            // Flag to trigger import reconstitution and var to hold external imports.
832
            $fiximports       = false;
833
            $external_imports = '';
834
835
            // remove comments to avoid importing commented-out imports.
836
            $thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss );
837
            while ( preg_match_all( '#@import +(?:url)?(?:(?:\((["\']?)(?:[^"\')]+)\1\)|(["\'])(?:[^"\']+)\2)(?:[^,;"\']+(?:,[^,;"\']+)*)?)(?:;)#mi', $thiscss_nocomments, $matches ) ) {
838
                foreach ( $matches[0] as $import ) {
839
                    if ( $this->isremovable( $import, $this->cssremovables ) ) {
840
                        $thiscss   = str_replace( $import, '', $thiscss );
841
                        $import_ok = true;
842
                    } else {
843
                        $url       = trim( preg_replace( '#^.*((?:https?:|ftp:)?//.*\.css).*$#', '$1', trim( $import ) ), " \t\n\r\0\x0B\"'" );
844
                        $path      = $this->getpath( $url );
845
                        $import_ok = false;
846
                        if ( file_exists( $path ) && is_readable( $path ) ) {
847
                            $code     = addcslashes( self::fixurls( $path, file_get_contents( $path ) ), '\\' );
0 ignored issues
show
Security Bug introduced by
It seems like $path defined by $this->getpath($url) on line 844 can also be of type false; however, autoptimizeStyles::fixurls() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
848
                            $code     = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $code );
849
                            $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $code, '' );
850 View Code Duplication
                            if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
851
                                $code                  = $tmpstyle;
852
                                $this->alreadyminified = true;
853
                            } elseif ( $this->can_inject_late( $path, $code ) ) {
854
                                $code = self::build_injectlater_marker( $path, md5( $code ) );
0 ignored issues
show
Security Bug introduced by
It seems like $path defined by $this->getpath($url) on line 844 can also be of type false; however, autoptimizeBase::build_injectlater_marker() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
855
                            }
856
857
                            if ( ! empty( $code ) ) {
858
                                $tmp_thiscss = preg_replace( '#(/\*FILESTART\*/.*)' . preg_quote( $import, '#' ) . '#Us', '/*FILESTART2*/' . $code . '$1', $thiscss );
859
                                if ( ! empty( $tmp_thiscss ) ) {
860
                                    $thiscss   = $tmp_thiscss;
861
                                    $import_ok = true;
862
                                    unset( $tmp_thiscss );
863
                                }
864
                            }
865
                            unset( $code );
866
                        }
867
                    }
868
                    if ( ! $import_ok ) {
869
                        // External imports and general fall-back.
870
                        $external_imports .= $import;
871
872
                        $thiscss    = str_replace( $import, '', $thiscss );
873
                        $fiximports = true;
874
                    }
875
                }
876
                $thiscss = preg_replace( '#/\*FILESTART\*/#', '', $thiscss );
877
                $thiscss = preg_replace( '#/\*FILESTART2\*/#', '/*FILESTART*/', $thiscss );
878
879
                // and update $thiscss_nocomments before going into next iteration in while loop.
880
                $thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss );
881
            }
882
            unset( $thiscss_nocomments );
883
884
            // Add external imports to top of aggregated CSS.
885
            if ( $fiximports ) {
886
                $thiscss = $external_imports . $thiscss;
887
            }
888
        }
889
        unset( $thiscss );
890
891
        // $this->csscode has all the uncompressed code now.
892
        foreach ( $this->csscode as &$code ) {
893
            // Check for already-minified code.
894
            $hash = md5( $code );
895
            do_action( 'autoptimize_action_css_hash', $hash );
896
            $ccheck = new autoptimizeCache( $hash, 'css' );
897
            if ( $ccheck->check() ) {
898
                $code                          = $ccheck->retrieve();
899
                $this->hashmap[ md5( $code ) ] = $hash;
900
                continue;
901
            }
902
            unset( $ccheck );
903
904
            // Rewrite and/or inline referenced assets.
905
            $code = $this->rewrite_assets( $code );
906
907
            // Minify.
908
            $code = $this->run_minifier_on( $code );
909
910
            // Bring back INJECTLATER stuff.
911
            $code = $this->inject_minified( $code );
912
913
            // Filter results.
914
            $tmp_code = apply_filters( 'autoptimize_css_after_minify', $code );
915
            if ( ! empty( $tmp_code ) ) {
916
                $code = $tmp_code;
917
                unset( $tmp_code );
918
            }
919
920
            $this->hashmap[ md5( $code ) ] = $hash;
921
        }
922
923
        unset( $code );
924
        return true;
925
    }
926
927
    public function run_minifier_on( $code )
928
    {
929
        if ( ! $this->alreadyminified ) {
930
            $do_minify = apply_filters( 'autoptimize_css_do_minify', true );
931
932 View Code Duplication
            if ( $do_minify ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
933
                $cssmin   = new autoptimizeCSSmin();
934
                $tmp_code = trim( $cssmin->run( $code ) );
935
936
                if ( ! empty( $tmp_code ) ) {
937
                    $code = $tmp_code;
938
                    unset( $tmp_code );
939
                }
940
            }
941
        }
942
943
        return $code;
944
    }
945
946
    /**
947
     * Caches the CSS in uncompressed, deflated and gzipped form.
948
     */
949
    public function cache()
950
    {
951
        // CSS cache.
952
        foreach ( $this->csscode as $media => $code ) {
953
            if ( empty( $code ) ) {
954
                continue;
955
            }
956
957
            $md5   = $this->hashmap[ md5( $code ) ];
958
            $cache = new autoptimizeCache( $md5, 'css' );
959
            if ( ! $cache->check() ) {
960
                // Cache our code.
961
                $cache->cache( $code, 'text/css' );
962
            }
963
            $this->url[ $media ] = AUTOPTIMIZE_CACHE_URL . $cache->getname();
964
        }
965
    }
966
967
    /**
968
     * Returns the content.
969
     */
970
    public function getcontent()
971
    {
972
        // Restore the full content (only applies when "autoptimize_filter_css_justhead" filter is true).
973
        if ( ! empty( $this->restofcontent ) ) {
974
            $this->content      .= $this->restofcontent;
975
            $this->restofcontent = '';
976
        }
977
978
        // type is not added by default.
979
        $type_css = '';
980
        if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) {
981
            $type_css = 'type="text/css" ';
982
        }
983
984
        // Inject the new stylesheets.
985
        $replace_tag = array( '<title', 'before' );
986
        $replace_tag = apply_filters( 'autoptimize_filter_css_replacetag', $replace_tag, $this->content );
987
988
        if ( $this->inline ) {
989
            foreach ( $this->csscode as $media => $code ) {
990
                $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<style ' . $type_css . 'media="' . $media . '">' . $code . '</style>' ), $replace_tag );
991
            }
992
        } else {
993
            if ( $this->defer ) {
994
                $preload_css_block  = '';
995
                $inlined_ccss_block = '';
996
                $noscript_css_block = '<noscript id="aonoscrcss">';
997
998
                $defer_inline_code = $this->defer_inline;
999
                if ( ! empty( $defer_inline_code ) ) {
1000
                    if ( apply_filters( 'autoptimize_filter_css_critcss_minify', true ) ) {
1001
                        $icss_hash  = md5( $defer_inline_code );
1002
                        $icss_cache = new autoptimizeCache( $icss_hash, 'css' );
1003
                        if ( $icss_cache->check() ) {
1004
                            // we have the optimized inline CSS in cache.
1005
                            $defer_inline_code = $icss_cache->retrieve();
1006 View Code Duplication
                        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
1007
                            $cssmin   = new autoptimizeCSSmin();
1008
                            $tmp_code = trim( $cssmin->run( $defer_inline_code ) );
1009
1010
                            if ( ! empty( $tmp_code ) ) {
1011
                                $defer_inline_code = $tmp_code;
1012
                                $icss_cache->cache( $defer_inline_code, 'text/css' );
1013
                                unset( $tmp_code );
1014
                            }
1015
                        }
1016
                    }
1017
                    // inlined critical css set here, but injected when full CSS is injected
1018
                    // to avoid CSS containing SVG with <title tag receiving the full CSS link.
1019
                    $inlined_ccss_block = '<style ' . $type_css . 'id="aoatfcss" media="all">' . $defer_inline_code . '</style>';
1020
                }
1021
            }
1022
1023
            foreach ( $this->url as $media => $url ) {
1024
                $url = $this->url_replace_cdn( $url );
1025
1026
                // Add the stylesheet either deferred (import at bottom) or normal links in head.
1027
                if ( $this->defer && 'print' !== $media ) {
1028
                    $preload_onload = autoptimizeConfig::get_ao_css_preload_onload( $media );
1029
1030
                    $preload_css_block .= '<link rel="stylesheet" media="print" href="' . $url . '" onload="' . $preload_onload . '" />';
0 ignored issues
show
Bug introduced by
The variable $preload_css_block does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1031
                    if ( apply_filters( 'autoptimize_fitler_css_preload_and_print', false ) ) {
1032
                        $preload_css_block = '<link rel="preload" as="stylesheet" href="' . $url . '"/>' . $preload_css_block;
1033
                    }
1034
                    $noscript_css_block .= '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />';
0 ignored issues
show
Bug introduced by
The variable $noscript_css_block does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1035
                } else {
1036
                    if ( strlen( $this->csscode[ $media ] ) > $this->cssinlinesize ) {
1037
                        $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />' ), $replace_tag );
1038
                    } elseif ( strlen( $this->csscode[ $media ] ) > 0 ) {
1039
                        $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<style ' . $type_css . 'media="' . $media . '">' . $this->csscode[ $media ] . '</style>' ), $replace_tag );
1040
                    }
1041
                }
1042
            }
1043
1044
            if ( $this->defer ) {
1045
                $noscript_css_block .= '</noscript>';
1046
                // Inject inline critical CSS, the preloaded full CSS and the noscript-CSS.
1047
                $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', $inlined_ccss_block . $preload_css_block . $noscript_css_block ), $replace_tag );
0 ignored issues
show
Bug introduced by
The variable $inlined_ccss_block does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1048
            }
1049
        }
1050
1051
        // restore comments.
1052
        $this->content = $this->restore_comments( $this->content );
1053
1054
        // restore IE hacks.
1055
        $this->content = $this->restore_iehacks( $this->content );
1056
1057
        // restore (no)script.
1058
        $this->content = $this->restore_marked_content( 'SCRIPT', $this->content );
1059
1060
        // Restore noptimize.
1061
        $this->content = $this->restore_noptimize( $this->content );
1062
1063
        // Return the modified stylesheet.
1064
        return $this->content;
1065
    }
1066
1067
    /**
1068
     * Make sure URL's are absolute iso relative to original CSS location.
1069
     *
1070
     * @param string $file filename of optimized CSS-file.
1071
     * @param string $code CSS-code in which to fix URL's.
1072
     */
1073
    static function fixurls( $file, $code )
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1074
    {
1075
        // Switch all imports to the url() syntax.
1076
        $code = preg_replace( '#@import ("|\')(.+?)\.css.*?("|\')#', '@import url("${2}.css")', $code );
1077
1078
        if ( preg_match_all( self::ASSETS_REGEX, $code, $matches ) ) {
1079
            $file = str_replace( WP_ROOT_DIR, '/', $file );
1080
            /**
1081
             * Rollback as per https://github.com/futtta/autoptimize/issues/94
1082
             * $file = str_replace( AUTOPTIMIZE_WP_CONTENT_NAME, '', $file );
1083
             */
1084
            $dir = dirname( $file ); // Like /themes/expound/css.
1085
1086
            /**
1087
             * $dir should not contain backslashes, since it's used to replace
1088
             * urls, but it can contain them when running on Windows because
1089
             * fixurls() is sometimes called with `ABSPATH . 'index.php'`
1090
             */
1091
            $dir = str_replace( '\\', '/', $dir );
1092
            unset( $file ); // not used below at all.
1093
1094
            $replace = array();
1095
            foreach ( $matches[1] as $k => $url ) {
1096
                // Remove quotes.
1097
                $url      = trim( $url, " \t\n\r\0\x0B\"'" );
1098
                $no_q_url = trim( $url, "\"'" );
1099
                if ( $url !== $no_q_url ) {
1100
                    $removed_quotes = true;
1101
                } else {
1102
                    $removed_quotes = false;
1103
                }
1104
1105
                if ( '' === $no_q_url ) {
1106
                    continue;
1107
                }
1108
1109
                $url = $no_q_url;
1110
                if ( '/' === $url[0] || preg_match( '#^(https?://|ftp://|data:)#i', $url ) ) {
1111
                    // URL is protocol-relative, host-relative or something we don't touch.
1112
                    continue;
1113
                } else { // Relative URL.
1114
1115
                    /*
1116
                     * rollback as per https://github.com/futtta/autoptimize/issues/94
1117
                     * $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_CONTENT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) );
1118
                     */
1119
                    $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_ROOT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) );
1120
                    $newurl = apply_filters( 'autoptimize_filter_css_fixurl_newurl', $newurl );
1121
1122
                    /**
1123
                     * Hash the url + whatever was behind potentially for replacement
1124
                     * We must do this, or different css classes referencing the same bg image (but
1125
                     * different parts of it, say, in sprites and such) loose their stuff...
1126
                     */
1127
                    $hash = md5( $url . $matches[2][ $k ] );
1128
                    $code = str_replace( $matches[0][ $k ], $hash, $code );
1129
1130
                    if ( $removed_quotes ) {
1131
                        $replace[ $hash ] = "url('" . $newurl . "')" . $matches[2][ $k ];
1132
                    } else {
1133
                        $replace[ $hash ] = 'url(' . $newurl . ')' . $matches[2][ $k ];
1134
                    }
1135
                }
1136
            }
1137
1138
            $code = self::replace_longest_matches_first( $code, $replace );
1139
        }
1140
1141
        return $code;
1142
    }
1143
1144
    private function ismovable( $tag )
1145
    {
1146
        if ( ! $this->aggregate ) {
1147
            return false;
1148
        }
1149
1150
        if ( ! empty( $this->allowlist ) ) {
1151
            foreach ( $this->allowlist as $match ) {
0 ignored issues
show
Bug introduced by
The expression $this->allowlist of type string is not traversable.
Loading history...
1152
                if ( false !== strpos( $tag, $match ) ) {
1153
                    return true;
1154
                }
1155
            }
1156
            // no match with allowlist.
1157
            return false;
1158
        } else {
1159
            if ( is_array( $this->dontmove ) && ! empty( $this->dontmove ) ) {
1160
                foreach ( $this->dontmove as $match ) {
1161
                    if ( false !== strpos( $tag, $match ) ) {
1162
                        // Matched something.
1163
                        return false;
1164
                    }
1165
                }
1166
            }
1167
1168
            // If we're here it's safe to move.
1169
            return true;
1170
        }
1171
    }
1172
1173
    private function can_inject_late( $css_path, $css )
1174
    {
1175
        $consider_minified_array = apply_filters( 'autoptimize_filter_css_consider_minified', false, $css_path );
1176
        if ( true !== $this->inject_min_late ) {
1177
            // late-inject turned off.
1178
            return false;
1179
        } elseif ( ( false === strpos( $css_path, 'min.css' ) ) && ( str_replace( $consider_minified_array, '', $css_path ) === $css_path ) ) {
1180
            // file not minified based on filename & filter.
1181
            return false;
1182
        } elseif ( false !== strpos( $css, '@import' ) ) {
1183
            // can't late-inject files with imports as those need to be aggregated.
1184
            return false;
1185
        } elseif ( ( false !== strpos( $css, '@font-face' ) ) && ( apply_filters( 'autoptimize_filter_css_fonts_cdn', false ) === true ) && ( ! empty( $this->cdn_url ) ) ) {
1186
            // don't late-inject CSS with font-src's if fonts are set to be CDN'ed.
1187
            return false;
1188
        } elseif ( ( ( true == $this->datauris ) || ( ! empty( $this->cdn_url ) ) ) && preg_match( '#background[^;}]*url\(#Ui', $css ) ) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1189
            // don't late-inject CSS with images if CDN is set OR if image inlining is on.
1190
            return false;
1191
        } else {
1192
            // phew, all is safe, we can late-inject.
1193
            return true;
1194
        }
1195
    }
1196
1197
    /**
1198
     * Minifies (and cdn-replaces) a single local css file
1199
     * and returns its (cached) url.
1200
     *
1201
     * @param string $filepath Filepath.
1202
     * @param bool   $cache_miss Optional. Force a cache miss. Default false.
1203
     *
1204
     * @return bool|string Url pointing to the minified css file or false.
1205
     */
1206
    public function minify_single( $filepath, $cache_miss = false )
1207
    {
1208
        $contents = $this->prepare_minify_single( $filepath );
1209
1210
        if ( empty( $contents ) ) {
1211
            return false;
1212
        }
1213
1214
        // Check cache.
1215
        $hash  = 'single_' . md5( $contents );
1216
        $cache = new autoptimizeCache( $hash, 'css' );
1217
        do_action( 'autoptimize_action_css_hash', $hash );
1218
1219
        // If not in cache already, minify...
1220
        if ( ! $cache->check() || $cache_miss ) {
1221
            // Fixurls...
1222
            $contents = self::fixurls( $filepath, $contents );
1223
            // CDN-replace any referenced assets if needed...
1224
            $contents = $this->hide_fontface_and_maybe_cdn( $contents );
1225
            $contents = $this->replace_urls( $contents );
1226
            $contents = $this->restore_fontface( $contents );
1227
            // Now minify...
1228
            $cssmin   = new autoptimizeCSSmin();
1229
            $contents = trim( $cssmin->run( $contents ) );
1230
1231
            // Check if minified cache content is empty.
1232
            if ( empty( $contents ) ) {
1233
                return false;
1234
            }
1235
1236
            // Filter contents of excluded minified CSS.
1237
            $contents = apply_filters( 'autoptimize_filter_css_single_after_minify', $contents );
1238
1239
            // Store in cache.
1240
            $cache->cache( $contents, 'text/css' );
1241
        }
1242
1243
        $url = $this->build_minify_single_url( $cache );
1244
1245
        return $url;
1246
    }
1247
1248
    /**
1249
     * Returns whether we're doing aggregation or not.
1250
     *
1251
     * @return bool
1252
     */
1253
    public function aggregating()
1254
    {
1255
        return $this->aggregate;
1256
    }
1257
1258
    public function getOptions()
1259
    {
1260
        return $this->options;
1261
    }
1262
1263
    public function replaceOptions( $options )
1264
    {
1265
        $this->options = $options;
1266
    }
1267
1268
    public function setOption( $name, $value )
1269
    {
1270
        $this->options[ $name ] = $value;
1271
        $this->$name            = $value;
1272
    }
1273
1274
    public function getOption( $name )
1275
    {
1276
        return $this->options[ $name ];
1277
    }
1278
1279
    /**
1280
     * Sanitize user-provided CSS.
1281
     *
1282
     * For now just strip_slashes and preg_replace to escape < in certain cases but might do full CSS escaping in the future, see:
1283
     * https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-4-css-encode-and-strictly-validate-before-inserting-untrusted-data-into-html-style-property-values
1284
     * https://github.com/twigphp/Twig/blob/3.x/src/Extension/EscaperExtension.php#L300-L319
1285
     * https://github.com/laminas/laminas-escaper/blob/2.8.x/src/Escaper.php#L205-L221
1286
     *
1287
     * @param string $css the to be sanitized CSS
1288
     * @return string sanitized CSS.
1289
     */
1290
    public static function sanitize_css( $css )
1291
    {
1292
        $css = strip_tags( $css );
1293
        if ( strpos( $css, '<' ) !== false ) {
1294
            $css = preg_replace( '#<(\/?\w+)#', '\00003C$1', $css );
1295
        }
1296
        return $css;
1297
    }
1298
}
1299