Completed
Push — master ( 9d450b...0b908e )
by Kevin
02:19
created

Element::handleValidationError()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 7
ccs 0
cts 0
cp 0
rs 9.4285
cc 2
eloc 4
nc 2
nop 2
crap 6
1
<?php
2
3
namespace Groundskeeper\Tokens\Elements;
4
5
use Groundskeeper\Configuration;
6
use Groundskeeper\Exceptions\ValidationException;
7
use Groundskeeper\Tokens\AbstractToken;
8
use Groundskeeper\Tokens\Token;
9
10
class Element extends AbstractToken
11
{
12
    const ATTR_CI_ENUM   = 'attr_ci_enum';// case-insensitive enumeration
13
    const ATTR_JS        = 'attr_js';
14
    const ATTR_CI_STRING = 'attr_ci_str'; // case-insensitive string
15
    const ATTR_CS_STRING = 'attr_cs_str'; // case-sensitive string
16
    const ATTR_URI       = 'attr_uri';
17
18
    /** @var array */
19
    private $attributes;
20
21
    /** @var array[Token] */
22
    private $children;
23
24
    /** @var string */
25
    private $name;
26
27
    /**
28
     * Constructor
29 3
     */
30
    public function __construct($name, array $attributes = array(), $parent = null)
31 3
    {
32
        parent::__construct(Token::ELEMENT, $parent);
33 3
34 3
        $this->attributes = array();
35 3
        foreach ($attributes as $key => $value) {
36 3
            $this->addAttribute($key, $value);
37
        }
38
39
        $this->children = array();
40
        $this->setName($name);
41 1
    }
42
43 1
    /**
44
     * Getter for 'attributes'.
45
     */
46
    public function getAttributes()
47
    {
48
        return $this->attributes;
49
    }
50
51
    /**
52
     * Hasser for 'attributes'.
53
     *
54
     * @param string $key
55
     *
56
     * @return boolean True if the attribute is present.
57
     */
58
    public function hasAttribute($key)
59
    {
60
        return array_key_exists($key, $this->attributes);
61
    }
62
63
    public function addAttribute($key, $value)
64
    {
65
        $key = trim(strtolower($key));
66
        if ($key == '') {
67
            throw new \InvalidArgumentException('Invalid emtpy attribute key.');
68
        }
69 1
70
        $this->attributes[$key] = $value;
71 1
72
        return $this;
73
    }
74
75
    public function removeAttribute($key)
76
    {
77
        $key = strtolower($key);
78
        if (isset($this->attributes[$key])) {
79
            unset($this->attributes[$key]);
80
81
            return true;
82
        }
83
84
        return false;
85
    }
86
87
    /**
88
     * Getter for 'children'.
89
     */
90
    public function getChildren()
91
    {
92
        return $this->children;
93
    }
94
95
    public function addChild(Token $token)
96 1
    {
97
        $token->setParent($this);
98 1
        $this->children[] = $token;
99
100
        return $this;
101
    }
102
103
    public function removeChild(Token $token)
104 3
    {
105
        $key = array_search($token, $this->children);
106 3
        if ($key !== false) {
107
            unset($this->children[$key]);
108
109
            return true;
110 3
        }
111
112 3
        return false;
113
    }
114
115
    /**
116
     * Getter for 'name'.
117
     */
118
    public function getName()
119
    {
120
        return $this->name;
121
    }
122
123
    /**
124
     * Chainable setter for 'name'.
125
     */
126
    public function setName($name)
127
    {
128
        if (!is_string($name)) {
129
            throw new \InvalidArgumentException('Element name must be string type.');
130
        }
131
132
        $this->name = trim(strtolower($name));
133
134
        return $this;
135
    }
136
137
    protected function getAllowedAttrbutes()
138
    {
139
        return array(
140
            // Global Attributes
141
            '/^accesskey$/i' => self::ATTR_CS_STRING,
142
            '/^class$/i' => self::ATTR_CS_STRING,
143
            '/^contenteditable$/i' => self::ATTR_CS_STRING,
144
            '/^contextmenu$/i' => self::ATTR_CS_STRING,
145
            '/^data-\S/i' => self::ATTR_CS_STRING,
146
            '/^dir$/i' => self::ATTR_CI_ENUM . '("ltr","rtl")',
147
            '/^draggable$/i' => self::ATTR_CS_STRING,
148
            '/^dropzone$/i' => self::ATTR_CS_STRING,
149
            '/^hidden$/i' => self::ATTR_CS_STRING,
150
            '/^id$/i' => self::ATTR_CS_STRING,
151
            '/^is$/i' => self::ATTR_CS_STRING,
152
            '/^itemid$/i' => self::ATTR_CS_STRING,
153
            '/^itemprop$/i' => self::ATTR_CS_STRING,
154
            '/^itemref$/i' => self::ATTR_CS_STRING,
155
            '/^itemscope$/i' => self::ATTR_CS_STRING,
156
            '/^itemtype$/i' => self::ATTR_CS_STRING,
157
            '/^lang$/i' => self::ATTR_CI_STRING,
158
            '/^slot$/i' => self::ATTR_CS_STRING,
159
            '/^spellcheck$/i' => self::ATTR_CS_STRING,
160
            '/^style$/i' => self::ATTR_CS_STRING,
161
            '/^tabindex$/i' => self::ATTR_CS_STRING,
162
            '/^title$/i' => self::ATTR_CS_STRING,
163
            '/^translate$/i' => self::ATTR_CI_ENUM . '("yes","no","")',
164
165
            // Event Handler Content Attributes
166
            // https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-content-attributes
167
            '/^onabort$/i' => self::ATTR_JS,
168
            '/^onautocomplete$/i' => self::ATTR_JS,
169
            '/^onautocompleteerror$/i' => self::ATTR_JS,
170
            '/^onblur$/i' => self::ATTR_JS,
171
            '/^oncancel$/i' => self::ATTR_JS,
172
            '/^oncanplay$/i' => self::ATTR_JS,
173
            '/^oncanplaythrough$/i' => self::ATTR_JS,
174
            '/^onchange$/i' => self::ATTR_JS,
175
            '/^onclick$/i' => self::ATTR_JS,
176
            '/^onclose$/i' => self::ATTR_JS,
177
            '/^oncontextmenu$/i' => self::ATTR_JS,
178
            '/^oncuechange$/i' => self::ATTR_JS,
179
            '/^ondblclick$/i' => self::ATTR_JS,
180
            '/^ondrag$/i' => self::ATTR_JS,
181
            '/^ondragend$/i' => self::ATTR_JS,
182
            '/^ondragenter$/i' => self::ATTR_JS,
183
            '/^ondragexit$/i' => self::ATTR_JS,
184
            '/^ondragleave$/i' => self::ATTR_JS,
185
            '/^ondragover$/i' => self::ATTR_JS,
186
            '/^ondragstart$/i' => self::ATTR_JS,
187
            '/^ondrop$/i' => self::ATTR_JS,
188
            '/^ondurationchange$/i' => self::ATTR_JS,
189
            '/^onemptied$/i' => self::ATTR_JS,
190
            '/^onended$/i' => self::ATTR_JS,
191
            '/^onerror$/i' => self::ATTR_JS,
192
            '/^onfocus$/i' => self::ATTR_JS,
193
            '/^oninput$/i' => self::ATTR_JS,
194
            '/^oninvalid$/i' => self::ATTR_JS,
195
            '/^onkeydown$/i' => self::ATTR_JS,
196
            '/^onkeypress$/i' => self::ATTR_JS,
197
            '/^onkeyup$/i' => self::ATTR_JS,
198
            '/^onload$/i' => self::ATTR_JS,
199
            '/^onloadeddata$/i' => self::ATTR_JS,
200
            '/^onloadedmetadata$/i' => self::ATTR_JS,
201
            '/^onloadstart$/i' => self::ATTR_JS,
202
            '/^onmousedown$/i' => self::ATTR_JS,
203
            '/^onmouseenter$/i' => self::ATTR_JS,
204
            '/^onmouseleave$/i' => self::ATTR_JS,
205
            '/^onmousemove$/i' => self::ATTR_JS,
206
            '/^onmouseout$/i' => self::ATTR_JS,
207
            '/^onmouseover$/i' => self::ATTR_JS,
208
            '/^onmouseup$/i' => self::ATTR_JS,
209
            '/^onwheel$/i' => self::ATTR_JS,
210
            '/^onpause$/i' => self::ATTR_JS,
211
            '/^onplay$/i' => self::ATTR_JS,
212
            '/^onplaying$/i' => self::ATTR_JS,
213
            '/^onprogress$/i' => self::ATTR_JS,
214
            '/^onratechange$/i' => self::ATTR_JS,
215
            '/^onreset$/i' => self::ATTR_JS,
216
            '/^onresize$/i' => self::ATTR_JS,
217
            '/^onscroll$/i' => self::ATTR_JS,
218
            '/^onseeked$/i' => self::ATTR_JS,
219
            '/^onseeking$/i' => self::ATTR_JS,
220
            '/^onselect$/i' => self::ATTR_JS,
221
            '/^onshow$/i' => self::ATTR_JS,
222
            '/^onstalled$/i' => self::ATTR_JS,
223
            '/^onsubmit$/i' => self::ATTR_JS,
224
            '/^onsuspend$/i' => self::ATTR_JS,
225
            '/^ontimeupdate$/i' => self::ATTR_JS,
226
            '/^ontoggle$/i' => self::ATTR_JS,
227
            '/^onvolumechange$/i' => self::ATTR_JS,
228
            '/^onwaiting$/i' => self::ATTR_JS,
229
230
            // WAI-ARIA
231
            // https://w3c.github.io/aria/aria/aria.html
232
            '/^role$/i' => self::ATTR_CI_STRING,
233
234
            // ARIA global states and properties
235
            '/^aria-atomic$/i' => self::ATTR_CS_STRING,
236
            '/^aria-busy$/i' => self::ATTR_CS_STRING,
237
            '/^aria-controls$/i' => self::ATTR_CS_STRING,
238
            '/^aria-current$/i' => self::ATTR_CS_STRING,
239
            '/^aria-describedby$/i' => self::ATTR_CS_STRING,
240
            '/^aria-details$/i' => self::ATTR_CS_STRING,
241
            '/^aria-disabled$/i' => self::ATTR_CS_STRING,
242
            '/^aria-dropeffect$/i' => self::ATTR_CS_STRING,
243
            '/^aria-errormessage$/i' => self::ATTR_CS_STRING,
244
            '/^aria-flowto$/i' => self::ATTR_CS_STRING,
245
            '/^aria-grabbed$/i' => self::ATTR_CS_STRING,
246
            '/^aria-haspopup$/i' => self::ATTR_CS_STRING,
247
            '/^aria-hidden$/i' => self::ATTR_CS_STRING,
248
            '/^aria-invalid$/i' => self::ATTR_CS_STRING,
249
            '/^aria-label$/i' => self::ATTR_CS_STRING,
250
            '/^aria-labelledby$/i' => self::ATTR_CS_STRING,
251
            '/^aria-live$/i' => self::ATTR_CS_STRING,
252
            '/^aria-owns$/i' => self::ATTR_CS_STRING,
253
            '/^aria-relevant$/i' => self::ATTR_CS_STRING,
254
            '/^aria-roledescription$/i' => self::ATTR_CS_STRING,
255
256
            // ARIA widget attributes
257
            '/^aria-autocomplete$/i' => self::ATTR_CS_STRING,
258
            '/^aria-checked$/i' => self::ATTR_CS_STRING,
259
            '/^aria-expanded$/i' => self::ATTR_CS_STRING,
260
            '/^aria-level$/i' => self::ATTR_CS_STRING,
261
            '/^aria-modal$/i' => self::ATTR_CS_STRING,
262
            '/^aria-multiline$/i' => self::ATTR_CS_STRING,
263
            '/^aria-multiselectable$/i' => self::ATTR_CS_STRING,
264
            '/^aria-orientation$/i' => self::ATTR_CS_STRING,
265
            '/^aria-placeholder$/i' => self::ATTR_CS_STRING,
266
            '/^aria-pressed$/i' => self::ATTR_CS_STRING,
267
            '/^aria-readonly$/i' => self::ATTR_CS_STRING,
268
            '/^aria-required$/i' => self::ATTR_CS_STRING,
269
            '/^aria-selected$/i' => self::ATTR_CS_STRING,
270
            '/^aria-sort$/i' => self::ATTR_CS_STRING,
271
            '/^aria-valuemax$/i' => self::ATTR_CS_STRING,
272
            '/^aria-valuemin$/i' => self::ATTR_CS_STRING,
273
            '/^aria-valuenow$/i' => self::ATTR_CS_STRING,
274
            '/^aria-valuetext$/i' => self::ATTR_CS_STRING,
275 2
276
            // ARIA relationship attributes
277 2
            '/^aria-activedescendant$/i' => self::ATTR_CS_STRING,
278
            '/^aria-colcount$/i' => self::ATTR_CS_STRING,
279 2
            '/^aria-colindex$/i' => self::ATTR_CS_STRING,
280
            '/^aria-colspan$/i' => self::ATTR_CS_STRING,
281
            '/^aria-posinset$/i' => self::ATTR_CS_STRING,
282
            '/^aria-rowcount$/i' => self::ATTR_CS_STRING,
283 2
            '/^aria-rowindex$/i' => self::ATTR_CS_STRING,
284
            '/^aria-rowspan$/i' => self::ATTR_CS_STRING,
285 2
            '/^aria-setsize$/i' => self::ATTR_CS_STRING
286
        );
287
    }
288
289
    protected function isAttributeNameValid($name)
290
    {
291
        $allowedAttributes = $this->getAllowedAttrbutes();
292
        foreach ($allowedAttributes as $attrRegex => $valueType) {
293
            if (preg_match($attrRegex, $name) === 1) {
294 2
                return true;
295 2
            }
296
        }
297 2
298
        return false;
299 2
    }
300 2
301
    public function validate(Configuration $configuration)
302 2
    {
303
        if ($configuration->get('clean-strategy') == 'none') {
304 2
            $this->isValid = true;
305
            foreach ($this->children as $child) {
306
                $child->validate($configuration);
307
            }
308 2
309 2
            return;
310 2
        }
311
312
        parent::validate($configuration);
313
314
        // If not valid, then we are done.
315
        if (!$this->isValid) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isValid of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
316
            return;
317
        }
318
319
        if ($configuration->get('clean-strategy') != 'none') {
320
            // Remove non-standard attributes.
321 2
            foreach ($this->attributes as $name => $value) {
322
                // Validate attribute name
323 2
                if (!$this->isAttributeNameValid($name)) {
324 2
                    unset($this->attributes[$name]);
325
                    continue;
326
                }
327
328
                // Validate attributes value
329 2
                /// @todo
330
            }
331 2
        }
332 2
333
        foreach ($this->children as $child) {
334
            $child->validate($configuration);
335
        }
336
    }
337
338
    protected function handleValidationError(Configuration $configuration, $message)
339
    {
340
        $this->isValid = false;
341
        if ($configuration->get('error-strategy') == 'throw') {
342
            throw new ValidationException($message);
343
        }
344
    }
345
346
    public function toString(Configuration $configuration, $prefix = '', $suffix = '')
347
    {
348
        if (!$this->isValid && $configuration->get('clean-strategy') != 'none') {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isValid of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
349
            return '';
350
        }
351
352
        $output = $this->toStringTag($configuration, $prefix, $suffix);
353
        if (empty($this->children)) {
354
            return $output;
355
        }
356
357 View Code Duplication
        foreach ($this->children as $child) {
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...
358
            $newPrefix = $prefix . str_repeat(' ', $configuration->get('indent-spaces'));
359
            $output .= $child->toString($configuration, $newPrefix, $suffix);
360
        }
361
362
        return $output . $prefix . '</' . $this->name . '>' . $suffix;
363
    }
364
365
    protected function toStringTag(Configuration $configuration, $prefix = '', $suffix = '', $forceOpen = false)
0 ignored issues
show
Unused Code introduced by
The parameter $configuration is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
366
    {
367
        $output = $prefix . '<' . $this->name;
368
        foreach ($this->attributes as $key => $value) {
369
            $output .= ' ' . strtolower($key);
370
            if (is_string($value)) {
371
                $output .= '="' . $value . '"';
372
            }
373
        }
374
375
        if (!$forceOpen && empty($this->children)) {
376
            return $output . '/>' . $suffix;
377
        }
378
379
        return $output . '>' . $suffix;
380
    }
381
}
382