Test Failed
Push — develop ( 425fc0...4f2be8 )
by Paul
13:11
created

Attributes::normalizeStringAttributes()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
dl 0
loc 10
ccs 7
cts 7
cp 1
rs 10
c 1
b 0
f 0
cc 4
nc 4
nop 0
crap 4
1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules\Html;
4
5
use GeminiLabs\SiteReviews\Helpers\Arr;
6
7
class Attributes
8
{
9
    public const ATTRIBUTES_A = [
10
        'download', 'href', 'hreflang', 'ping', 'referrerpolicy', 'rel', 'target', 'type',
11
    ];
12
13
    public const ATTRIBUTES_BUTTON = [
14
        'autofocus', 'disabled', 'form', 'formaction', 'formenctype', 'formmethod',
15
        'formnovalidate', 'formtarget', 'name', 'type', 'value',
16
    ];
17
18
    public const ATTRIBUTES_FORM = [
19
        'accept', 'accept-charset', 'action', 'autocapitalize', 'autocomplete', 'enctype', 'method',
20
        'name', 'novalidate', 'target',
21
    ];
22
23
    public const ATTRIBUTES_IMG = [
24
        'alt', 'crossorigin', 'decoding', 'height', 'ismap', 'loading', 'referrerpolicy', 'sizes',
25
        'src', 'srcset', 'width', 'usemap',
26
    ];
27
28
    public const ATTRIBUTES_INPUT = [
29
        'accept', 'autocomplete', 'autocorrect', 'autofocus', 'capture', 'checked', 'disabled',
30
        'form', 'formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget', 'height',
31
        'incremental', 'inputmode', 'list', 'max', 'maxlength', 'min', 'minlength', 'multiple',
32
        'name', 'pattern', 'placeholder', 'readonly', 'results', 'required', 'selectionDirection',
33
        'selectionEnd', 'selectionStart', 'size', 'spellcheck', 'src', 'step', 'tabindex', 'type',
34
        'value', 'webkitdirectory', 'width',
35
    ];
36
37
    public const ATTRIBUTES_INPUT_EXCLUDED = [
38
        'autocapitalize' => ['url', 'email', 'password'],
39
        'autocomplete' => ['checkbox', 'radio', 'button'],
40
        'hidden' => ['hidden'],
41
        'list' => ['hidden', 'password', 'checkbox', 'radio', 'button'],
42
        'readonly' => ['hidden', 'range', 'color', 'checkbox', 'radio', 'button'],
43
        'required' => ['hidden', 'range', 'color', 'button'],
44
        'value' => ['image'],
45
    ];
46
47
    public const ATTRIBUTES_INPUT_INCLUDED = [
48
        'accept' => ['file'],
49
        'alt' => ['image'],
50
        'capture' => ['file'],
51
        'checked' => ['checkbox', 'radio'],
52
        'dirname' => ['hidden', 'text', 'search', 'url', 'tel', 'email'],
53
        'formaction' => ['image', 'submit'],
54
        'formenctype' => ['image', 'submit'],
55
        'formmethod' => ['image', 'submit'],
56
        'formnovalidate' => ['image', 'submit'],
57
        'formtarget' => ['image', 'submit'],
58
        'height' => ['image'],
59
        'max' => ['date', 'month', 'week', 'time', 'datetime-local', 'number', 'range'],
60
        'maxlength' => ['text', 'search', 'url', 'tel', 'email', 'password'],
61
        'min' => ['date', 'month', 'week', 'time', 'datetime-local', 'number', 'range'],
62
        'minlength' => ['text', 'search', 'url', 'tel', 'email', 'password'],
63
        'multiple' => ['email', 'file'],
64
        'pattern' => ['text', 'search', 'url', 'tel', 'email', 'password'],
65
        'placeholder' => ['text', 'search', 'url', 'tel', 'email', 'password', 'number'],
66
        'popovertarget' => ['button'],
67
        'popovertargetaction' => ['button'],
68
        'size' => ['text', 'search', 'url', 'tel', 'email', 'password'],
69
        'src' => ['image'],
70
        'step' => ['date', 'month', 'week', 'time', 'datetime-local', 'number', 'range'],
71
        'width' => ['image'],
72
    ];
73
74
    public const ATTRIBUTES_LABEL = [
75
        'for',
76
    ];
77
78
    public const ATTRIBUTES_OPTGROUP = [
79
        'disabled', 'label',
80
    ];
81
82
    public const ATTRIBUTES_OPTION = [
83
        'disabled', 'label', 'selected', 'value',
84
    ];
85
86
    public const ATTRIBUTES_SELECT = [
87
        'autofocus', 'disabled', 'form', 'multiple', 'name', 'required', 'size',
88
    ];
89
90
    public const ATTRIBUTES_TEXTAREA = [
91
        'autocapitalize', 'autocomplete', 'autofocus', 'cols', 'disabled', 'form', 'maxlength',
92
        'minlength', 'name', 'placeholder', 'readonly', 'required', 'rows', 'spellcheck', 'wrap',
93
    ];
94
95
    public const BOOLEAN_ATTRIBUTES = [
96
        'autofocus', 'capture', 'checked', 'disabled', 'draggable', 'formnovalidate', 'hidden',
97
        'multiple', 'novalidate', 'readonly', 'required', 'selected', 'spellcheck',
98
        'webkitdirectory',
99
    ];
100
101
    public const GLOBAL_ATTRIBUTES = [
102
        'accesskey', 'class', 'contenteditable', 'contextmenu', 'dir', 'draggable', 'dropzone',
103
        'hidden', 'id', 'lang', 'role', 'spellcheck', 'style', 'tabindex', 'title',
104
    ];
105
106
    public const GLOBAL_WILDCARD_ATTRIBUTES = [
107
        'aria-', 'data-', 'item', 'on',
108
    ];
109
110
    public const INPUT_TYPES = [
111
        'button', 'checkbox', 'color', 'date', 'datetime-local', 'email', 'file', 'hidden', 'image',
112
        'month', 'number', 'password', 'radio', 'range', 'reset', 'search', 'submit', 'tel', 'text',
113
        'time', 'url', 'week',
114
    ];
115
116
    public const URL_ATTRIBUTES = [
117
        'href', 'src',
118
    ];
119
120
    protected array $attributes = [];
121
122
    /**
123
     * @return static
124
     */
125 130
    public function __call(string $method, array $args = [])
126
    {
127 130
        $constant = 'static::ATTRIBUTES_'.strtoupper($method);
128 130
        $allowedAttributes = defined($constant)
129 129
            ? constant($constant)
130 43
            : [];
131 130
        $this->normalize(Arr::consolidate(Arr::get($args, 0)), $allowedAttributes);
132 130
        if ('input' === $method) {
133 89
            $this->normalizeInput();
134
        }
135 130
        return $this;
136
    }
137
138 10
    public function isInputType(string $type): bool
139
    {
140 10
        return in_array($type, Attributes::INPUT_TYPES);
141
    }
142
143
    /**
144
     * @return static
145
     */
146
    public function set(array $attributes)
147
    {
148
        $this->normalize($attributes);
149
        return $this;
150
    }
151
152
    public function toArray(): array
153
    {
154
        return $this->attributes;
155
    }
156
157 130
    public function toString(): string
158
    {
159 130
        $attributes = [];
160 130
        foreach ($this->attributes as $attribute => $value) {
161 130
            if (in_array($attribute, static::BOOLEAN_ATTRIBUTES)) {
162 4
                $attributes[] = $attribute;
163 4
                continue;
164
            }
165 130
            if (!is_scalar($value)) {
166
                continue;
167
            }
168 130
            $attributes[] = "{$attribute}=\"{$value}\"";
169
        }
170 130
        return implode(' ', $attributes);
171
    }
172
173 130
    protected function filterAttributes(array $allowedAttributes): array
174
    {
175 130
        return array_intersect_key($this->attributes, array_flip($allowedAttributes));
176
    }
177
178 130
    protected function filterGlobalAttributes(): array
179
    {
180 130
        $globalAttributes = $this->filterAttributes(static::GLOBAL_ATTRIBUTES);
181 130
        $wildcards = [];
182 130
        foreach (static::GLOBAL_WILDCARD_ATTRIBUTES as $wildcard) {
183 130
            $newWildcards = array_filter($this->attributes,
184 130
                fn ($key) => str_starts_with($key, $wildcard),
185 130
                ARRAY_FILTER_USE_KEY
186 130
            );
187 130
            $wildcards = array_merge($wildcards, $newWildcards);
188
        }
189 130
        return array_merge($globalAttributes, $wildcards);
190
    }
191
192 130
    protected function getPermanentAttributes(): array
193
    {
194 130
        $permanentAttributes = [];
195 130
        if (array_key_exists('value', $this->attributes)) {
196 128
            $permanentAttributes['value'] = $this->attributes['value'];
197
        }
198 130
        return $permanentAttributes;
199
    }
200
201
    /**
202
     * @param mixed $value
203
     */
204 130
    protected function isAttributeKeyNumeric(string $key, $value): bool
205
    {
206 130
        return is_string($value)
207 130
            && is_numeric($key)
208 130
            && !array_key_exists($value, $this->attributes);
209
    }
210
211 130
    protected function normalize(array $args, array $allowedAttributes = []): void
212
    {
213 130
        $this->attributes = array_change_key_case($args, CASE_LOWER);
214 130
        $this->normalizeBooleanAttributes();
215 130
        $this->normalizeDataAttributes();
216 130
        $this->normalizeStringAttributes();
217 130
        $this->removeEmptyAttributes();
218 130
        $this->removeIndexedAttributes();
219 130
        $this->attributes = array_merge(
220 130
            $this->filterGlobalAttributes(),
221 130
            $this->filterAttributes($allowedAttributes)
222 130
        );
223
    }
224
225 130
    protected function normalizeBooleanAttributes(): void
226
    {
227 130
        foreach ($this->attributes as $key => $value) {
228 130
            if ($this->isAttributeKeyNumeric($key, $value)) {
229
                $key = $value;
230
                $value = true;
231
            }
232 130
            if (!in_array($key, static::BOOLEAN_ATTRIBUTES)) {
233 130
                continue;
234
            }
235 128
            $this->attributes[$key] = wp_validate_boolean($value);
236
        }
237
    }
238
239 130
    protected function normalizeDataAttributes(): void
240
    {
241 130
        foreach ($this->attributes as $key => $value) {
242 130
            if ($this->isAttributeKeyNumeric($key, $value)) {
243
                $key = $value;
244
                $value = '';
245
            }
246 130
            if (!str_starts_with($key, 'data-')) {
247 130
                continue;
248
            }
249 6
            if (is_array($value)) {
250
                $value = wp_json_encode($value, JSON_HEX_APOS | JSON_NUMERIC_CHECK | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
251
            }
252 6
            $this->attributes[$key] = esc_attr($value);
253
        }
254
    }
255
256 89
    protected function normalizeInput(): void
257
    {
258 89
        $attributes = wp_parse_args($this->attributes, ['type' => '']);
259 89
        if (!in_array($attributes['type'], static::INPUT_TYPES)) {
260 2
            $attributes['type'] = 'text';
261
        }
262 89
        $included = array_intersect_key(static::ATTRIBUTES_INPUT_INCLUDED, $attributes);
263 89
        foreach ($included as $attribute => $types) {
264 19
            if (!in_array($attributes['type'], $types)) {
265 1
                unset($attributes[$attribute]);
266
            }
267
        }
268 89
        $excluded = array_intersect_key(static::ATTRIBUTES_INPUT_EXCLUDED, $attributes);
269 89
        foreach ($excluded as $attribute => $types) {
270 89
            if (in_array($attributes['type'], $types)) {
271
                unset($attributes[$attribute]);
272
            }
273
        }
274 89
        $this->attributes = $attributes;
275
    }
276
277 130
    protected function normalizeStringAttributes(): void
278
    {
279 130
        foreach ($this->attributes as $key => $value) {
280 130
            if (!is_string($value)) {
281 129
                continue;
282
            }
283 130
            if (in_array($key, static::URL_ATTRIBUTES)) {
284 1
                $this->attributes[$key] = esc_url(trim($value));
285
            } else {
286 130
                $this->attributes[$key] = esc_attr(trim($value));
287
            }
288
        }
289
    }
290
291 130
    protected function removeEmptyAttributes(): void
292
    {
293 130
        $attributes = $this->attributes;
294 130
        $permanentAttributes = $this->getPermanentAttributes();
295 130
        foreach ($this->attributes as $key => $value) {
296 130
            if (in_array($key, static::BOOLEAN_ATTRIBUTES) && !$value) {
297 128
                unset($attributes[$key]);
298
            }
299 130
            if (str_starts_with($key, 'data-')) {
300 6
                $permanentAttributes[$key] = $value;
301 6
                unset($attributes[$key]);
302
            }
303
        }
304 130
        $this->attributes = array_merge(Arr::removeEmptyValues($attributes), $permanentAttributes);
305
    }
306
307 130
    protected function removeIndexedAttributes(): void
308
    {
309 130
        $this->attributes = array_diff_key(
310 130
            $this->attributes,
311 130
            array_filter($this->attributes, 'is_numeric', ARRAY_FILTER_USE_KEY)
312 130
        );
313
    }
314
}
315