Completed
Push — master ( fa912e...9a88a5 )
by Lars
02:35
created

sortCssClassNames()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 12
cts 12
cp 1
rs 9.2568
c 0
b 0
f 0
cc 5
nc 4
nop 2
crap 5
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
     * @param HtmlMinInterface       $htmlMin
36
     *
37
     * @return void
38
     */
39 53
    public function domElementBeforeMinification(SimpleHtmlDomInterface $element, HtmlMinInterface $htmlMin)
40
    {
41 53
    }
42
43
    /**
44
     * Receive dom elements after the minification.
45
     *
46
     * @param SimpleHtmlDomInterface $element
47
     * @param HtmlMinInterface       $htmlMin
48
     *
49
     * @return void
50
     */
51 53
    public function domElementAfterMinification(SimpleHtmlDomInterface $element, HtmlMinInterface $htmlMin)
52
    {
53 53
        $attributes = $element->getAllAttributes();
54 53
        if ($attributes === null) {
55 51
            return;
56
        }
57
58 36
        $tagName = $element->getNode()->nodeName;
59 36
        $attrs = [];
60 36
        foreach ((array) $attributes as $attrName => $attrValue) {
61
62
            // -------------------------------------------------------------------------
63
            // Remove local domains from attributes.
64
            // -------------------------------------------------------------------------
65
66 36
            if ($htmlMin->isDoMakeSameDomainsLinksRelative()) {
67 1
                $localDomains = $htmlMin->getLocalDomains();
68 1
                foreach ($localDomains as $localDomain) {
69
                    /** @noinspection InArrayCanBeUsedInspection */
70
                    if (
71
                        (
72 1
                            $attrName === 'href'
73
                            ||
74 1
                            $attrName === 'src'
75
                            ||
76 1
                            $attrName === 'srcset'
77
                            ||
78 1
                            $attrName === 'action'
79
                        )
80
                        &&
81 1
                        !(isset($attributes['rel']) && $attributes['rel'] === 'external')
82
                        &&
83 1
                        !(isset($attributes['target']) && $attributes['target'] === '_blank')
84
                        &&
85 1
                        \stripos($attrValue, $localDomain) !== false
86
                    ) {
87 1
                        $localDomainEscaped = \preg_quote($localDomain, '/');
88
89 1
                        $attrValue = (string) \preg_replace("/^(?:(?:https?:)?\/\/)?{$localDomainEscaped}(?!\w)(?:\/?)/i", '/', $attrValue);
90
                    }
91
                }
92
            }
93
94
            // -------------------------------------------------------------------------
95
            // Remove optional "http:"-prefix from attributes.
96
            // -------------------------------------------------------------------------
97
98 36
            if ($htmlMin->isDoRemoveHttpPrefixFromAttributes()) {
99 5
                $attrValue = $this->removeUrlSchemeHelper(
100 5
                    $attrValue,
101 5
                    $attrName,
102 5
                    'http',
103 5
                    $attributes,
104 5
                    $tagName,
105 5
                    $htmlMin
106
                );
107
            }
108
109 36
            if ($htmlMin->isDoRemoveHttpsPrefixFromAttributes()) {
110 1
                $attrValue = $this->removeUrlSchemeHelper(
111 1
                    $attrValue,
112 1
                    $attrName,
113 1
                    'https',
114 1
                    $attributes,
115 1
                    $tagName,
116 1
                    $htmlMin
117
                );
118
            }
119
120
            // -------------------------------------------------------------------------
121
            // Remove some special attributes.
122
            // -------------------------------------------------------------------------
123
124 36
            if ($this->removeAttributeHelper(
125 36
                $element->tag,
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...
126 36
                $attrName,
127 36
                $attrValue,
128 36
                $attributes,
129 36
                $htmlMin
130
            )) {
131 8
                $element->{$attrName} = null;
132
133 8
                continue;
134
            }
135
136
            // -------------------------------------------------------------------------
137
            // Sort css-class-names, for better gzip results.
138
            // -------------------------------------------------------------------------
139
140 35
            if ($htmlMin->isDoSortCssClassNames()) {
141 34
                $attrValue = $this->sortCssClassNames($attrName, $attrValue);
142
            }
143
144 35
            if ($htmlMin->isDoSortHtmlAttributes()) {
145 34
                $attrs[$attrName] = $attrValue;
146 34
                $element->{$attrName} = null;
147
            }
148
        }
149
150
        // -------------------------------------------------------------------------
151
        // Sort html-attributes, for better gzip results.
152
        // -------------------------------------------------------------------------
153
154 36
        if ($htmlMin->isDoSortHtmlAttributes()) {
155 35
            \ksort($attrs);
156 35
            foreach ($attrs as $attrName => $attrValue) {
157 34
                $attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue);
158 34
                $element->setAttribute((string) $attrName, $attrValue, true);
159
            }
160
        }
161 36
    }
162
163
    /**
164
     * Check if the attribute can be removed.
165
     *
166
     * @param string           $tag
167
     * @param string           $attrName
168
     * @param string           $attrValue
169
     * @param array            $allAttr
170
     * @param HtmlMinInterface $htmlMin
171
     *
172
     * @return bool
173
     */
174 36
    private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr, HtmlMinInterface $htmlMin): bool
175
    {
176
        // remove defaults
177 36
        if ($htmlMin->isDoRemoveDefaultAttributes()) {
178 1
            if ($tag === 'script' && $attrName === 'language' && $attrValue === 'javascript') {
179
                return true;
180
            }
181
182 1
            if ($tag === 'form' && $attrName === 'method' && $attrValue === 'get') {
183
                return true;
184
            }
185
186 1
            if ($tag === 'form' && $attrName === 'autocomplete' && $attrValue === 'on') {
187
                return true;
188
            }
189
190 1
            if ($tag === 'form' && $attrName === 'enctype' && $attrValue === 'application/x-www-form-urlencoded') {
191
                return true;
192
            }
193
194 1
            if ($tag === 'input' && $attrName === 'type' && $attrValue === 'text') {
195
                return true;
196
            }
197
198 1
            if ($tag === 'textarea' && $attrName === 'wrap' && $attrValue === 'soft') {
199
                return true;
200
            }
201
202 1
            if ($tag === 'area' && $attrName === 'shape' && $attrValue === 'rect') {
203
                return true;
204
            }
205
206 1
            if ($tag === 'th' && $attrName === 'scope' && $attrValue === 'auto') {
207
                return true;
208
            }
209
210 1
            if ($tag === 'ol' && $attrName === 'type' && $attrValue === 'decimal') {
211
                return true;
212
            }
213
214 1
            if ($tag === 'ol' && $attrName === 'start' && $attrValue === '1') {
215
                return true;
216
            }
217
218 1
            if ($tag === 'track' && $attrName === 'kind' && $attrValue === 'subtitles') {
219
                return true;
220
            }
221
222 1
            if ($attrName === 'spellcheck' && $attrValue === 'default') {
223
                return true;
224
            }
225
226 1
            if ($attrName === 'draggable' && $attrValue === 'auto') {
227
                return true;
228
            }
229
        }
230
231
        // remove deprecated charset-attribute (the browser will use the charset from the HTTP-Header, anyway)
232 36
        if ($htmlMin->isDoRemoveDeprecatedScriptCharsetAttribute()) {
233
            /** @noinspection NestedPositiveIfStatementsInspection */
234 35
            if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) {
235
                return true;
236
            }
237
        }
238
239
        // remove deprecated anchor-jump
240 36 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...
241
            /** @noinspection NestedPositiveIfStatementsInspection */
242 35
            if ($tag === 'a' && $attrName === 'name' && isset($allAttr['id']) && $allAttr['id'] === $attrValue) {
243
                return true;
244
            }
245
        }
246
247 36
        if ($htmlMin->isDoRemoveDefaultMediaTypeFromStyleAndLinkTag()) {
248
            /** @noinspection NestedPositiveIfStatementsInspection */
249 35
            if (($tag === 'link' || $tag === 'style') && $attrName === 'media' && $attrValue === 'all') {
250 1
                return true;
251
            }
252
        }
253
254
        // remove "type=text/css" for css "stylesheet"-links
255 36
        if ($htmlMin->isDoRemoveDeprecatedTypeFromStylesheetLink()) {
256
            /** @noinspection NestedPositiveIfStatementsInspection */
257 35
            if ($tag === 'link' && $attrName === 'type' && $attrValue === 'text/css' && isset($allAttr['rel']) && $allAttr['rel'] === 'stylesheet' && $htmlMin->isXHTML() === false && $htmlMin->isHTML4() === false) {
258 1
                return true;
259
            }
260
        }
261
        // remove deprecated css-mime-types
262 36
        if ($htmlMin->isDoRemoveDeprecatedTypeFromStyleAndLinkTag()) {
263
            /** @noinspection NestedPositiveIfStatementsInspection */
264 35
            if (($tag === 'link' || $tag === 'style') && $attrName === 'type' && $attrValue === 'text/css' && $htmlMin->isXHTML() === false && $htmlMin->isHTML4() === false) {
265 1
                return true;
266
            }
267
        }
268
269
        // remove deprecated script-mime-types
270 36
        if ($htmlMin->isDoRemoveDeprecatedTypeFromScriptTag()) {
271
            /** @noinspection NestedPositiveIfStatementsInspection */
272 35
            if ($tag === 'script' && $attrName === 'type' && isset(self::$executableScriptsMimeTypes[$attrValue]) && $htmlMin->isXHTML() === false && $htmlMin->isHTML4() === false) {
273 4
                return true;
274
            }
275
        }
276
277
        // remove 'type=submit' from <button type="submit">
278 35
        if ($htmlMin->isDoRemoveDefaultTypeFromButton()) {
279
            /** @noinspection NestedPositiveIfStatementsInspection */
280
            if ($tag === 'button' && $attrName === 'type' && $attrValue === 'submit') {
281
                return true;
282
            }
283
        }
284
285
        // remove 'value=""' from <input type="text">
286 35 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...
287
            /** @noinspection NestedPositiveIfStatementsInspection */
288 34
            if ($tag === 'input' && $attrName === 'value' && $attrValue === '' && isset($allAttr['type']) && $allAttr['type'] === 'text') {
289 1
                return true;
290
            }
291
        }
292
293
        // remove some empty attributes
294 35
        if ($htmlMin->isDoRemoveEmptyAttributes()) {
295
            /** @noinspection NestedPositiveIfStatementsInspection */
296 34
            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)) {
297 5
                return true;
298
            }
299
        }
300
301 35
        return false;
302
    }
303
304
    /**
305
     * @param string           $attrValue
306
     * @param string           $attrName
307
     * @param string           $scheme
308
     * @param string[]         $attributes
309
     * @param string           $tagName
310
     * @param HtmlMinInterface $htmlMin
311
     *
312
     * @return string
313
     *
314
     * @noinspection PhpTooManyParametersInspection
315
     */
316 5
    private function removeUrlSchemeHelper(
317
        string $attrValue,
318
        string $attrName,
319
        string $scheme,
320
        array $attributes,
321
        string $tagName,
322
        HtmlMinInterface $htmlMin
323
    ): string {
324
        /** @noinspection InArrayCanBeUsedInspection */
325
        if (
326 5
            !(isset($attributes['rel']) && $attributes['rel'] === 'external')
327
            &&
328 5
            !(isset($attributes['target']) && $attributes['target'] === '_blank')
329
            &&
330
            (
331
                (
332 5
                    $attrName === 'href'
333
                    &&
334
                    (
335 4
                        !$htmlMin->isdoKeepHttpAndHttpsPrefixOnExternalAttributes()
336
                        ||
337 4
                        $tagName === 'link'
338
                    )
339
                )
340
                ||
341 5
                $attrName === 'src'
342
                ||
343 5
                $attrName === 'srcset'
344
                ||
345 5
                $attrName === 'action'
346
            )
347
        ) {
348 4
            $attrValue = \str_replace($scheme . '://', '//', $attrValue);
349
        }
350
351 5
        return $attrValue;
352
    }
353
354
    /**
355
     * @param string $attrName
356
     * @param string $attrValue
357
     *
358
     * @return string
359
     */
360 34
    private function sortCssClassNames($attrName, $attrValue): string
361
    {
362 34
        if ($attrName !== 'class' || !$attrValue) {
363 29
            return $attrValue;
364
        }
365
366 18
        $classes = \array_unique(
367 18
            \explode(' ', $attrValue)
368
        );
369 18
        \sort($classes);
370
371 18
        $attrValue = '';
372 18
        foreach ($classes as $class) {
373 18
            if (!$class) {
374 3
                continue;
375
            }
376
377 18
            $attrValue .= \trim($class) . ' ';
378
        }
379
380 18
        return \trim($attrValue);
381
    }
382
}
383