Completed
Push — master ( 91add2...61ae49 )
by Lars
13:45
created

removeHttpPrefixHelper()   B

Complexity

Conditions 9
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 5
cts 5
cp 1
rs 8.0555
c 0
b 0
f 0
cc 9
nc 2
nop 4
crap 9
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * HtmlMinDomObserverOptimizeAttributes: Optimize html attributes. [protected html is still protected]
9
 *
10
 * Sort HTML-Attributes, so that gzip can do better work and remove some default attributes...
11
 */
12
final class HtmlMinDomObserverOptimizeAttributes implements HtmlMinDomObserverInterface
13
{
14
    /**
15
     * // https://mathiasbynens.be/demo/javascript-mime-type
16
     * // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
17
     *
18
     * @var string[]
19
     *
20
     * @psalm-var array<string, string>
21
     */
22
    private static $executableScriptsMimeTypes = [
23
        'text/javascript'          => '',
24
        'text/ecmascript'          => '',
25
        'text/jscript'             => '',
26
        'application/javascript'   => '',
27
        'application/x-javascript' => '',
28
        'application/ecmascript'   => '',
29
    ];
30
31
    /**
32
     * Receive dom elements before the minification.
33
     *
34
     * @param SimpleHtmlDomInterface $element
35 48
     * @param HtmlMinInterface       $htmlMin
36
     *
37 48
     * @return void
38
     */
39
    public function domElementBeforeMinification(SimpleHtmlDomInterface $element, HtmlMinInterface $htmlMin)
40
    {
41
    }
42
43
    /**
44
     * Receive dom elements after the minification.
45 48
     *
46
     * @param SimpleHtmlDomInterface $element
47 48
     * @param HtmlMinInterface       $htmlMin
48 48
     *
49 47
     * @return void
50
     */
51
    public function domElementAfterMinification(SimpleHtmlDomInterface $element, HtmlMinInterface $htmlMin)
52 31
    {
53 31
        $attributes = $element->getAllAttributes();
54
        if ($attributes === null) {
55
            return;
56
        }
57
58
        $attrs = [];
59 31
        foreach ((array) $attributes as $attrName => $attrValue) {
60
61
            // -------------------------------------------------------------------------
62
            // Remove optional "http:"-prefix from attributes.
63 3
            // -------------------------------------------------------------------------
64
65 3
            if ($htmlMin->isDoRemoveHttpPrefixFromAttributes()) {
66
                $attrValue = $this->removeHttpPrefixHelper(
67 3
                    $attrValue,
68
                    $attrName,
69 2
                    'http',
70
                    $attributes
71
                );
72
            }
73 31
74 6
            if ($htmlMin->isDoRemoveHttpsPrefixFromAttributes()) {
75
                $attrValue = $this->removeHttpPrefixHelper(
76 6
                    $attrValue,
77
                    $attrName,
78
                    'https',
79
                    $attributes
80
                );
81
            }
82
83 31
            if ($this->removeAttributeHelper($element->tag, $attrName, $attrValue, $attributes, $htmlMin)) {
0 ignored issues
show
Bug introduced by
Accessing tag on the interface voku\helper\SimpleHtmlDomInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
84 30
                $element->{$attrName} = null;
85
86
                continue;
87 31
            }
88 30
89 30
            // -------------------------------------------------------------------------
90
            // Sort css-class-names, for better gzip results.
91
            // -------------------------------------------------------------------------
92
93
            if ($htmlMin->isDoSortCssClassNames()) {
94
                $attrValue = $this->sortCssClassNames($attrName, $attrValue);
95
            }
96
97 31
            if ($htmlMin->isDoSortHtmlAttributes()) {
98 30
                $attrs[$attrName] = $attrValue;
99 30
                $element->{$attrName} = null;
100 30
            }
101 30
        }
102
103
        // -------------------------------------------------------------------------
104 31
        // Sort html-attributes, for better gzip results.
105
        // -------------------------------------------------------------------------
106
107
        if ($htmlMin->isDoSortHtmlAttributes()) {
108
            \ksort($attrs);
109
            foreach ($attrs as $attrName => $attrValue) {
110
                $attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue);
111
                $element->setAttribute((string) $attrName, $attrValue, true);
112
            }
113
        }
114
    }
115
116
    /**
117 31
     * Check if the attribute can be removed.
118
     *
119
     * @param string  $tag
120 31
     * @param string  $attrName
121 1
     * @param string  $attrValue
122
     * @param array   $allAttr
123
     * @param HtmlMinInterface $htmlMin
124
     *
125 1
     * @return bool
126
     */
127
    private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr, HtmlMinInterface $htmlMin): bool
128
    {
129 1
        // remove defaults
130
        if ($htmlMin->isDoRemoveDefaultAttributes()) {
131
            if ($tag === 'script' && $attrName === 'language' && $attrValue === 'javascript') {
132
                return true;
133 1
            }
134
135
            if ($tag === 'form' && $attrName === 'method' && $attrValue === 'get') {
136
                return true;
137
            }
138
139 31
            if ($tag === 'input' && $attrName === 'type' && $attrValue === 'text') {
140
                return true;
141 30
            }
142
143
            if ($tag === 'area' && $attrName === 'shape' && $attrValue === 'rect') {
144
                return true;
145
            }
146
        }
147 31
148
        // remove deprecated charset-attribute (the browser will use the charset from the HTTP-Header, anyway)
149 30
        if ($htmlMin->isDoRemoveDeprecatedScriptCharsetAttribute()) {
150
            /** @noinspection NestedPositiveIfStatementsInspection */
151
            if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) {
152
                return true;
153
            }
154
        }
155 31
156
        // remove deprecated anchor-jump
157 30 View Code Duplication
        if ($htmlMin->isDoRemoveDeprecatedAnchorName()) {
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...
158 1
            /** @noinspection NestedPositiveIfStatementsInspection */
159
            if ($tag === 'a' && $attrName === 'name' && isset($allAttr['id']) && $allAttr['id'] === $attrValue) {
160
                return true;
161
            }
162
        }
163 31
164
        // remove "type=text/css" for css links
165 30 View Code Duplication
        if ($htmlMin->isDoRemoveDeprecatedTypeFromStylesheetLink()) {
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...
166 1
            /** @noinspection NestedPositiveIfStatementsInspection */
167
            if ($tag === 'link' && $attrName === 'type' && $attrValue === 'text/css' && isset($allAttr['rel']) && $allAttr['rel'] === 'stylesheet') {
168
                return true;
169
            }
170
        }
171 31
172
        // remove deprecated script-mime-types
173 30 View Code Duplication
        if ($htmlMin->isDoRemoveDeprecatedTypeFromScriptTag()) {
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 1
            /** @noinspection NestedPositiveIfStatementsInspection */
175
            if ($tag === 'script' && $attrName === 'type' && isset($allAttr['src'], self::$executableScriptsMimeTypes[$attrValue])) {
176
                return true;
177
            }
178
        }
179 31
180
        // remove 'value=""' from <input type="text">
181 30 View Code Duplication
        if ($htmlMin->isDoRemoveValueFromEmptyInput()) {
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...
182 5
            /** @noinspection NestedPositiveIfStatementsInspection */
183
            if ($tag === 'input' && $attrName === 'value' && $attrValue === '' && isset($allAttr['type']) && $allAttr['type'] === 'text') {
184
                return true;
185
            }
186 31
        }
187
188
        // remove some empty attributes
189
        if ($htmlMin->isDoRemoveEmptyAttributes()) {
190
            /** @noinspection NestedPositiveIfStatementsInspection */
191
            if (\trim($attrValue) === '' && \preg_match('/^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(?:down|up|over|move|out)|key(?:press|down|up)))$/', $attrName)) {
192
                return true;
193
            }
194
        }
195 30
196
        return false;
197 30
    }
198 25
199
    /**
200
     * @param string $attrValue
201 18
     * @param string $attrName
202 18
     * @param string $scheme
203
     * @param array  $attributes
204 18
     *
205
     * @return string
206 18
     */
207 18
    private function removeHttpPrefixHelper(
208 18
        string $attrValue,
209 3
        string $attrName,
210
        string $scheme,
211
        array $attributes
212 18
    ): string {
213
        /** @noinspection InArrayCanBeUsedInspection */
214
        if (
215 18
            ($attrName === 'href' || $attrName === 'src' || $attrName === 'srcset' || $attrName === 'action')
216
            &&
217
            !(isset($attributes['rel']) && $attributes['rel'] === 'external')
218
            &&
219
            !(isset($attributes['target']) && $attributes['target'] === '_blank')
220
        ) {
221
            $attrValue = \str_replace($scheme . '://', '//', $attrValue);
222
        }
223
224
        return $attrValue;
225
    }
226
227
    /**
228
     * @param string $attrName
229
     * @param string $attrValue
230
     *
231
     * @return string
232
     */
233
    private function sortCssClassNames($attrName, $attrValue): string
234
    {
235
        if ($attrName !== 'class' || !$attrValue) {
236
            return $attrValue;
237
        }
238
239
        $classes = \array_unique(
240
            \explode(' ', $attrValue)
241
        );
242
        \sort($classes);
243
244
        $attrValue = '';
245
        foreach ($classes as $class) {
246
            if (!$class) {
247
                continue;
248
            }
249
250
            $attrValue .= \trim($class) . ' ';
251
        }
252
253
        return \trim($attrValue);
254
    }
255
}
256