Passed
Push — master ( 009f03...e6fcd5 )
by Thomas
02:32
created

DeferBackend   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 187
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 34
eloc 83
c 2
b 0
f 0
dl 0
loc 187
rs 9.68

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getDeferBackend() 0 7 2
A getCSS() 0 14 4
A javascript() 0 7 2
A replaceBackend() 0 20 6
F includeInHTML() 0 85 18
A customScript() 0 13 2
1
<?php
2
3
namespace LeKoala\DeferBackend;
4
5
use Exception;
6
use SilverStripe\View\HTML;
7
use SilverStripe\View\SSViewer;
8
use SilverStripe\View\Requirements;
9
use SilverStripe\View\Requirements_Backend;
10
11
/**
12
 * A backend that defers everything by default
13
 *
14
 * Also insert custom head tags first because order may matter
15
 *
16
 * @link https://flaviocopes.com/javascript-async-defer/
17
 */
18
class DeferBackend extends Requirements_Backend
19
{
20
    // It's better to write to the head with defer
21
    public $writeJavascriptToBody = false;
22
23
    /**
24
     * @return $this
25
     */
26
    public static function getDeferBackend()
27
    {
28
        $backend = Requirements::backend();
29
        if (!$backend instanceof self) {
30
            throw new Exception("Requirements backend is currently of class " . get_class($backend));
31
        }
32
        return $backend;
33
    }
34
35
    /**
36
     * @param Requirements_Backend $oldBackend defaults to current backend
37
     * @return $this
38
     */
39
    public static function replaceBackend(Requirements_Backend $oldBackend = null)
40
    {
41
        if ($oldBackend === null) {
42
            $oldBackend = Requirements::backend();
43
        }
44
        $deferBackend = new static;
45
        foreach ($oldBackend->getCSS() as $file => $opts) {
46
            $deferBackend->css($file, null, $opts);
47
        }
48
        foreach ($oldBackend->getJavascript() as $file => $opts) {
49
            $deferBackend->javascript($file, null, $opts);
0 ignored issues
show
Unused Code introduced by
The call to LeKoala\DeferBackend\DeferBackend::javascript() has too many arguments starting with $opts. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

49
            $deferBackend->/** @scrutinizer ignore-call */ 
50
                           javascript($file, null, $opts);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
50
        }
51
        foreach ($oldBackend->getCustomCSS() as $id => $script) {
52
            $deferBackend->customCSS($script, $id);
53
        }
54
        foreach ($oldBackend->getCustomScripts() as $id => $script) {
55
            $deferBackend->customScript($script, $id);
56
        }
57
        Requirements::set_backend($deferBackend);
58
        return $deferBackend;
59
    }
60
61
    /**
62
     * @inheritDoc
63
     */
64
    public function javascript($file, $options = array())
65
    {
66
        // We want to defer by default, but we can disable it if needed
67
        if (!isset($options['defer'])) {
68
            $options['defer'] = true;
69
        }
70
        return parent::javascript($file, $options);
0 ignored issues
show
Bug introduced by
Are you sure the usage of parent::javascript($file, $options) targeting SilverStripe\View\Requir...s_Backend::javascript() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
71
    }
72
73
    /**
74
     * @inheritDoc
75
     */
76
    public function customScript($script, $uniquenessID = null)
77
    {
78
        // Wrap script in a DOMContentLoaded
79
        // Make sure we don't add the eventListener twice
80
        // @link https://stackoverflow.com/questions/41394983/how-to-defer-inline-javascript
81
        if (strpos($script, 'window.addEventListener') === false) {
82
            $script = "window.addEventListener('DOMContentLoaded', function() { $script });";
83
        }
84
85
        // Remove comments if any
86
        $script = preg_replace('/(?:(?:\/\*(?:[^*]|(?:\*+[^*\/]))*\*+\/)|(?:(?<!\:|\\\|\'|\")\/\/.*))/', '', $script);
87
88
        return parent::customScript($script, $uniquenessID);
0 ignored issues
show
Bug introduced by
Are you sure the usage of parent::customScript($script, $uniquenessID) targeting SilverStripe\View\Requir...Backend::customScript() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
89
    }
90
91
    /**
92
     * Get all css files
93
     *
94
     * @return array
95
     */
96
    public function getCSS()
97
    {
98
        $css = array_diff_key($this->css, $this->blocked);
99
        // Theme and assets files should always come last to have a proper cascade
100
        $allCss = [];
101
        $themeCss = [];
102
        foreach ($css as $file => $arr) {
103
            if (strpos($file, 'themes') === 0 || strpos($file, '/assets') === 0) {
104
                $themeCss[$file] = $arr;
105
            } else {
106
                $allCss[$file] = $arr;
107
            }
108
        }
109
        return array_merge($allCss, $themeCss);
110
    }
111
112
    /**
113
     * Update the given HTML content with the appropriate include tags for the registered
114
     * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter,
115
     * including a head and body tag.
116
     *
117
     * @param string $content HTML content that has already been parsed from the $templateFile through {@link SSViewer}
118
     * @return string HTML content augmented with the requirements tags
119
     */
120
    public function includeInHTML($content)
121
    {
122
        // Get our CSP nonce, it's always good to have even if we don't use it :-)
123
        $nonce = CspProvider::getCspNonce();
124
125
        // Skip if content isn't injectable, or there is nothing to inject
126
        $tagsAvailable = preg_match('#</head\b#', $content);
127
        $hasFiles = $this->css || $this->javascript || $this->customCSS || $this->customScript || $this->customHeadTags;
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customHeadTags of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->customCSS of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->css of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->customScript of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $this->javascript of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
128
        if (!$tagsAvailable || !$hasFiles) {
129
            return $content;
130
        }
131
        $requirements = '';
132
        $jsRequirements = '';
133
134
        // Combine files - updates $this->javascript and $this->css
135
        $this->processCombinedFiles();
136
137
        // Script tags for js links
138
        foreach ($this->getJavascript() as $file => $attributes) {
139
            // Build html attributes
140
            $htmlAttributes = [
141
                'type' => isset($attributes['type']) ? $attributes['type'] : "application/javascript",
142
                'src' => $this->pathForFile($file),
143
                'nonce' => $nonce,
144
            ];
145
            if (!empty($attributes['async'])) {
146
                $htmlAttributes['async'] = 'async';
147
            }
148
            if (!empty($attributes['defer'])) {
149
                $htmlAttributes['defer'] = 'defer';
150
            }
151
            $jsRequirements .= HTML::createTag('script', $htmlAttributes);
152
            $jsRequirements .= "\n";
153
        }
154
155
        // Add all inline JavaScript *after* including external files they might rely on
156
        foreach ($this->getCustomScripts() as $script) {
157
            $jsRequirements .= HTML::createTag(
158
                'script',
159
                [
160
                    'type' => 'application/javascript',
161
                    'nonce' => $nonce,
162
                ],
163
                "//<![CDATA[\n{$script}\n//]]>"
164
            );
165
            $jsRequirements .= "\n";
166
        }
167
168
        // Custom head tags (comes first)
169
        foreach ($this->getCustomHeadTags() as $customHeadTag) {
170
            $requirements .= "{$customHeadTag}\n";
171
        }
172
173
        // CSS file links
174
        foreach ($this->getCSS() as $file => $params) {
175
            $htmlAttributes = [
176
                'rel' => 'stylesheet',
177
                'type' => 'text/css',
178
                'href' => $this->pathForFile($file),
179
            ];
180
            if (!empty($params['media'])) {
181
                $htmlAttributes['media'] = $params['media'];
182
            }
183
            $requirements .= HTML::createTag('link', $htmlAttributes);
184
            $requirements .= "\n";
185
        }
186
187
        // Literal custom CSS content
188
        foreach ($this->getCustomCSS() as $css) {
189
            $requirements .= HTML::createTag('style', ['type' => 'text/css'], "\n{$css}\n");
190
            $requirements .= "\n";
191
        }
192
193
        // Inject CSS  into body
194
        $content = $this->insertTagsIntoHead($requirements, $content);
195
196
        // Inject scripts
197
        if ($this->getForceJSToBottom()) {
198
            $content = $this->insertScriptsAtBottom($jsRequirements, $content);
199
        } elseif ($this->getWriteJavascriptToBody()) {
200
            $content = $this->insertScriptsIntoBody($jsRequirements, $content);
201
        } else {
202
            $content = $this->insertTagsIntoHead($jsRequirements, $content);
203
        }
204
        return $content;
205
    }
206
}
207