Completed
Push — master ( 9fbe45...817727 )
by Kevin
03:24
created

Element::getAttribute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 8
ccs 5
cts 6
cp 0.8333
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 2.0185
1
<?php
2
3
namespace Groundskeeper\Tokens;
4
5
use Groundskeeper\Configuration;
6
use Psr\Log\LoggerInterface;
7
8
class Element extends AbstractToken implements Cleanable, ContainsChildren, Removable
9
{
10
    const ATTR_BOOL      = 'ci_boo'; // boolean
11
    const ATTR_CI_ENUM   = 'ci_enu'; // case-insensitive enumeration
12
    const ATTR_CI_SSENUM = 'ci_sse'; // case-insensitive space-separated enumeration
13
    const ATTR_INT       = 'ci_int'; // integer
14
    const ATTR_JS        = 'cs_jsc'; // javascript
15
    const ATTR_CI_STRING = 'ci_str'; // case-insensitive string
16
    const ATTR_CS_STRING = 'cs_str'; // case-sensitive string
17
    const ATTR_URI       = 'cs_uri'; // uri
18
19
    /** @var array */
20
    protected $attributes;
21
22
    /** @var array[Token] */
23
    protected $children;
24
25
    /** @var string */
26
    private $name;
27
28
    /**
29
     * Constructor
30
     */
31 91
    public function __construct(Configuration $configuration, $name, array $attributes = array())
32
    {
33 91
        parent::__construct(Token::ELEMENT, $configuration);
34
35 91
        $this->attributes = array();
36 91
        foreach ($attributes as $key => $value) {
37 49
            $this->addAttribute($key, $value);
38 91
        }
39
40 91
        $this->children = array();
41 91
        $this->setName($name);
42 91
    }
43
44
    /**
45
     * Getter for 'attributes'.
46
     */
47 2
    public function getAttributes()
48 1
    {
49 2
        return $this->attributes;
50 1
    }
51
52 2
    public function getAttribute($key)
53 2
    {
54 1
        if (!$this->hasAttribute($key)) {
55
            throw new \InvalidArgumentException('Invalid attribute key: ' . $key);
56
        }
57
58 1
        return $this->attributes[$key];
59 1
    }
60
61
    /**
62
     * Hasser for 'attributes'.
63
     *
64
     * @param string $key
65
     *
66
     * @return bool True if the attribute is present.
67
     */
68 22
    public function hasAttribute($key)
69
    {
70 22
        return array_key_exists($key, $this->attributes);
71
    }
72
73 51
    public function addAttribute($key, $value)
74
    {
75 51
        $key = trim(strtolower($key));
76 51
        if ($key == '') {
77 1
            throw new \InvalidArgumentException('Invalid empty attribute key.');
78
        }
79
80 50
        $this->attributes[$key] = $value;
81
82 50
        return $this;
83 1
    }
84
85 2
    public function removeAttribute($key)
86 1
    {
87 2
        $key = trim(strtolower($key));
88 2
        if (isset($this->attributes[$key])) {
89 2
            unset($this->attributes[$key]);
90
91 2
            return true;
92
        }
93
94 2
        return false;
95
    }
96
97
    /**
98
     * Required by ContainsChildren interface.
99
     */
100 2
    public function getChildren()
101
    {
102 2
        return $this->children;
103
    }
104
105
    /**
106
     * Required by ContainsChildren interface.
107
     */
108 1
    public function hasChild(Token $token)
109
    {
110 1
        return array_search($token, $this->children, true) !== false;
111
    }
112
113
    /**
114
     * Required by ContainsChildren interface.
115
     */
116 82
    public function appendChild(Token $token)
117
    {
118 82
        $token->setParent($this);
119 82
        $this->children[] = $token;
120
121 82
        return $this;
122
    }
123
124
    /**
125
     * Required by ContainsChildren interface.
126
     */
127 4
    public function prependChild(Token $token)
128
    {
129 4
        $token->setParent($this);
130 4
        array_unshift($this->children, $token);
131
132 4
        return $this;
133
    }
134
135
    /**
136
     * Required by the ContainsChildren interface.
137
     */
138 27 View Code Duplication
    public function removeChild(Token $token)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
139
    {
140 27
        $key = array_search($token, $this->children, true);
141 27
        if ($key !== false) {
142 27
            unset($this->children[$key]);
143
144 27
            return true;
145
        }
146
147 1
        return false;
148
    }
149
150
    /**
151
     * Getter for 'name'.
152
     */
153 77
    public function getName()
154
    {
155 77
        return $this->name;
156
    }
157
158
    /**
159
     * Chainable setter for 'name'.
160
     */
161 91
    public function setName($name)
162
    {
163 91
        if (!is_string($name)) {
164 1
            throw new \InvalidArgumentException('Element name must be string type.');
165
        }
166
167 91
        $this->name = trim(strtolower($name));
168
169 91
        return $this;
170
    }
171
172
    /**
173
     * Required by the Cleanable interface.
174
     */
175 78
    public function clean(LoggerInterface $logger)
176
    {
177 78
        if ($this->configuration->get('clean-strategy') == Configuration::CLEAN_STRATEGY_NONE) {
178 1
            return true;
179
        }
180
181
        // Remove non-standard attributes.
182 77
        if ($this->configuration->get('clean-strategy') != Configuration::CLEAN_STRATEGY_LENIENT) {
183 77
            foreach ($this->attributes as $name => $value) {
184 41
                $attributeParameters = $this->getAttributeParameters($name);
185 41
                if (empty($attributeParameters)) {
186 7
                    $logger->debug('Removing non-standard attribute "' . $name . '" from ' . $this);
187 7
                    unset($this->attributes[$name]);
188 7
                }
189 77
            }
190 77
        }
191
192 77
        foreach ($this->attributes as $name => $value) {
193
            // Validate attribute value.
194 42
            $attributeParameters = $this->getAttributeParameters($name);
195 42
            if (empty($attributeParameters)) {
196
                $attributeParameters = array(
197 5
                    'name' => $name,
198 5
                    'regex' => '/\S*/i',
199
                    'valueType' => self::ATTR_CS_STRING
200 5
                );
201 5
            }
202
203
            list($caseSensitivity, $attributeType) =
204 42
                explode('_', $attributeParameters['valueType']);
205
206
            // Handle case-insensitivity.
207
            // Standard is case-insensitive attribute values should be lower case.
208 42
            if ($caseSensitivity == 'ci') {
209 10
                $newValue = strtolower($value);
210 10
                if ($newValue !== $value) {
211 2
                    $logger->debug('Within ' . $this . ' the value for the attribute "' . $name . '" is case-insensitive.  The value has been converted to lower case.');
212 2
                    $this->attributes[$name] = $newValue;
213 2
                }
214 10
            }
215
216 42
            switch (substr($attributeType, 0, 3)) {
217 42
            case 'enu': // enumeration
218
                /// @todo
219 1
                break;
220
221 41
            case 'uri': // URI
222
                /// @todo
223 13
                break;
224 42
            }
225 77
        }
226
227
        // Fix self (if possible)
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
228 77
        $this->fixSelf($logger);
229
230
        // Remove self or children?
231 77
        if ($this->configuration->get('clean-strategy') !== Configuration::CLEAN_STRATEGY_LENIENT) {
232
            // Remove self?
233 77
            if ($this->removeInvalidSelf($logger)) {
234 16
                return false;
235
            }
236
237
            // Remove children?
238 77
            $this->removeInvalidChildren($logger);
239 77
        }
240
241
        // Clean children.
242 77
        return AbstractToken::cleanChildTokens(
243 77
            $this->configuration,
244 77
            $this->children,
245
            $logger
246 77
        );
247
    }
248
249 72
    protected function fixSelf(LoggerInterface $logger)
250
    {
251 72
    }
252
253 68
    protected function removeInvalidChildren(LoggerInterface $logger)
254
    {
255 68
    }
256
257 72
    protected function removeInvalidSelf(LoggerInterface $logger)
258
    {
259 72
        return false;
260
    }
261
262 42
    protected function getAllowedAttributes()
263
    {
264
        return array(
265
            // Global Attributes
266 42
            '/^accesskey$/i' => self::ATTR_CS_STRING,
267 42
            '/^class$/i' => self::ATTR_CS_STRING,
268 42
            '/^contenteditable$/i' => self::ATTR_CS_STRING,
269 42
            '/^contextmenu$/i' => self::ATTR_CS_STRING,
270 42
            '/^data-\S/i' => self::ATTR_CS_STRING,
271 42
            '/^dir$/i' => self::ATTR_CI_ENUM . '("ltr","rtl"|"ltr")',
272 42
            '/^draggable$/i' => self::ATTR_CS_STRING,
273 42
            '/^dropzone$/i' => self::ATTR_CS_STRING,
274 42
            '/^hidden$/i' => self::ATTR_CS_STRING,
275 42
            '/^id$/i' => self::ATTR_CS_STRING,
276 42
            '/^is$/i' => self::ATTR_CS_STRING,
277 42
            '/^itemid$/i' => self::ATTR_CS_STRING,
278 42
            '/^itemprop$/i' => self::ATTR_CS_STRING,
279 42
            '/^itemref$/i' => self::ATTR_CS_STRING,
280 42
            '/^itemscope$/i' => self::ATTR_CS_STRING,
281 42
            '/^itemtype$/i' => self::ATTR_CS_STRING,
282 42
            '/^lang$/i' => self::ATTR_CI_STRING,
283 42
            '/^slot$/i' => self::ATTR_CS_STRING,
284 42
            '/^spellcheck$/i' => self::ATTR_CS_STRING,
285 42
            '/^style$/i' => self::ATTR_CS_STRING,
286 42
            '/^tabindex$/i' => self::ATTR_CS_STRING,
287 42
            '/^title$/i' => self::ATTR_CS_STRING,
288 42
            '/^translate$/i' => self::ATTR_CI_ENUM . '("yes","no",""|"yes")',
289
290
            // Event Handler Content Attributes
291
            // https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-content-attributes
292 42
            '/^onabort$/i' => self::ATTR_JS,
293 42
            '/^onautocomplete$/i' => self::ATTR_JS,
294 42
            '/^onautocompleteerror$/i' => self::ATTR_JS,
295 42
            '/^onblur$/i' => self::ATTR_JS,
296 42
            '/^oncancel$/i' => self::ATTR_JS,
297 42
            '/^oncanplay$/i' => self::ATTR_JS,
298 42
            '/^oncanplaythrough$/i' => self::ATTR_JS,
299 42
            '/^onchange$/i' => self::ATTR_JS,
300 42
            '/^onclick$/i' => self::ATTR_JS,
301 42
            '/^onclose$/i' => self::ATTR_JS,
302 42
            '/^oncontextmenu$/i' => self::ATTR_JS,
303 42
            '/^oncuechange$/i' => self::ATTR_JS,
304 42
            '/^ondblclick$/i' => self::ATTR_JS,
305 42
            '/^ondrag$/i' => self::ATTR_JS,
306 42
            '/^ondragend$/i' => self::ATTR_JS,
307 42
            '/^ondragenter$/i' => self::ATTR_JS,
308 42
            '/^ondragexit$/i' => self::ATTR_JS,
309 42
            '/^ondragleave$/i' => self::ATTR_JS,
310 42
            '/^ondragover$/i' => self::ATTR_JS,
311 42
            '/^ondragstart$/i' => self::ATTR_JS,
312 42
            '/^ondrop$/i' => self::ATTR_JS,
313 42
            '/^ondurationchange$/i' => self::ATTR_JS,
314 42
            '/^onemptied$/i' => self::ATTR_JS,
315 42
            '/^onended$/i' => self::ATTR_JS,
316 42
            '/^onerror$/i' => self::ATTR_JS,
317 42
            '/^onfocus$/i' => self::ATTR_JS,
318 42
            '/^oninput$/i' => self::ATTR_JS,
319 42
            '/^oninvalid$/i' => self::ATTR_JS,
320 42
            '/^onkeydown$/i' => self::ATTR_JS,
321 42
            '/^onkeypress$/i' => self::ATTR_JS,
322 42
            '/^onkeyup$/i' => self::ATTR_JS,
323 42
            '/^onload$/i' => self::ATTR_JS,
324 42
            '/^onloadeddata$/i' => self::ATTR_JS,
325 42
            '/^onloadedmetadata$/i' => self::ATTR_JS,
326 42
            '/^onloadstart$/i' => self::ATTR_JS,
327 42
            '/^onmousedown$/i' => self::ATTR_JS,
328 42
            '/^onmouseenter$/i' => self::ATTR_JS,
329 42
            '/^onmouseleave$/i' => self::ATTR_JS,
330 42
            '/^onmousemove$/i' => self::ATTR_JS,
331 42
            '/^onmouseout$/i' => self::ATTR_JS,
332 42
            '/^onmouseover$/i' => self::ATTR_JS,
333 42
            '/^onmouseup$/i' => self::ATTR_JS,
334 42
            '/^onwheel$/i' => self::ATTR_JS,
335 42
            '/^onpause$/i' => self::ATTR_JS,
336 42
            '/^onplay$/i' => self::ATTR_JS,
337 42
            '/^onplaying$/i' => self::ATTR_JS,
338 42
            '/^onprogress$/i' => self::ATTR_JS,
339 42
            '/^onratechange$/i' => self::ATTR_JS,
340 42
            '/^onreset$/i' => self::ATTR_JS,
341 42
            '/^onresize$/i' => self::ATTR_JS,
342 42
            '/^onscroll$/i' => self::ATTR_JS,
343 42
            '/^onseeked$/i' => self::ATTR_JS,
344 42
            '/^onseeking$/i' => self::ATTR_JS,
345 42
            '/^onselect$/i' => self::ATTR_JS,
346 42
            '/^onshow$/i' => self::ATTR_JS,
347 42
            '/^onstalled$/i' => self::ATTR_JS,
348 42
            '/^onsubmit$/i' => self::ATTR_JS,
349 42
            '/^onsuspend$/i' => self::ATTR_JS,
350 42
            '/^ontimeupdate$/i' => self::ATTR_JS,
351 42
            '/^ontoggle$/i' => self::ATTR_JS,
352 42
            '/^onvolumechange$/i' => self::ATTR_JS,
353 42
            '/^onwaiting$/i' => self::ATTR_JS,
354
355
            // WAI-ARIA
356
            // https://w3c.github.io/aria/aria/aria.html
357 42
            '/^role$/i' => self::ATTR_CI_STRING,
358
359
            // ARIA global states and properties
360 42
            '/^aria-atomic$/i' => self::ATTR_CS_STRING,
361 42
            '/^aria-busy$/i' => self::ATTR_CS_STRING,
362 42
            '/^aria-controls$/i' => self::ATTR_CS_STRING,
363 42
            '/^aria-current$/i' => self::ATTR_CS_STRING,
364 42
            '/^aria-describedby$/i' => self::ATTR_CS_STRING,
365 42
            '/^aria-details$/i' => self::ATTR_CS_STRING,
366 42
            '/^aria-disabled$/i' => self::ATTR_CS_STRING,
367 42
            '/^aria-dropeffect$/i' => self::ATTR_CS_STRING,
368 42
            '/^aria-errormessage$/i' => self::ATTR_CS_STRING,
369 42
            '/^aria-flowto$/i' => self::ATTR_CS_STRING,
370 42
            '/^aria-grabbed$/i' => self::ATTR_CS_STRING,
371 42
            '/^aria-haspopup$/i' => self::ATTR_CS_STRING,
372 42
            '/^aria-hidden$/i' => self::ATTR_CS_STRING,
373 42
            '/^aria-invalid$/i' => self::ATTR_CS_STRING,
374 42
            '/^aria-label$/i' => self::ATTR_CS_STRING,
375 42
            '/^aria-labelledby$/i' => self::ATTR_CS_STRING,
376 42
            '/^aria-live$/i' => self::ATTR_CS_STRING,
377 42
            '/^aria-owns$/i' => self::ATTR_CS_STRING,
378 42
            '/^aria-relevant$/i' => self::ATTR_CS_STRING,
379 42
            '/^aria-roledescription$/i' => self::ATTR_CS_STRING,
380
381
            // ARIA widget attributes
382 42
            '/^aria-autocomplete$/i' => self::ATTR_CS_STRING,
383 42
            '/^aria-checked$/i' => self::ATTR_CS_STRING,
384 42
            '/^aria-expanded$/i' => self::ATTR_CS_STRING,
385 42
            '/^aria-level$/i' => self::ATTR_CS_STRING,
386 42
            '/^aria-modal$/i' => self::ATTR_CS_STRING,
387 42
            '/^aria-multiline$/i' => self::ATTR_CS_STRING,
388 42
            '/^aria-multiselectable$/i' => self::ATTR_CS_STRING,
389 42
            '/^aria-orientation$/i' => self::ATTR_CS_STRING,
390 42
            '/^aria-placeholder$/i' => self::ATTR_CS_STRING,
391 42
            '/^aria-pressed$/i' => self::ATTR_CS_STRING,
392 42
            '/^aria-readonly$/i' => self::ATTR_CS_STRING,
393 42
            '/^aria-required$/i' => self::ATTR_CS_STRING,
394 42
            '/^aria-selected$/i' => self::ATTR_CS_STRING,
395 42
            '/^aria-sort$/i' => self::ATTR_CS_STRING,
396 42
            '/^aria-valuemax$/i' => self::ATTR_CS_STRING,
397 42
            '/^aria-valuemin$/i' => self::ATTR_CS_STRING,
398 42
            '/^aria-valuenow$/i' => self::ATTR_CS_STRING,
399 42
            '/^aria-valuetext$/i' => self::ATTR_CS_STRING,
400
401
            // ARIA relationship attributes
402 42
            '/^aria-activedescendant$/i' => self::ATTR_CS_STRING,
403 42
            '/^aria-colcount$/i' => self::ATTR_CS_STRING,
404 42
            '/^aria-colindex$/i' => self::ATTR_CS_STRING,
405 42
            '/^aria-colspan$/i' => self::ATTR_CS_STRING,
406 42
            '/^aria-posinset$/i' => self::ATTR_CS_STRING,
407 42
            '/^aria-rowcount$/i' => self::ATTR_CS_STRING,
408 42
            '/^aria-rowindex$/i' => self::ATTR_CS_STRING,
409 42
            '/^aria-rowspan$/i' => self::ATTR_CS_STRING,
410
            '/^aria-setsize$/i' => self::ATTR_CS_STRING
411 42
        );
412
    }
413
414 42
    protected function getAttributeParameters($name)
415
    {
416 42
        $allowedAttributes = $this->getAllowedAttributes();
417 42
        foreach ($allowedAttributes as $attrRegex => $valueType) {
418 42
            if (preg_match($attrRegex, $name) === 1) {
419
                return array(
420 42
                    'name' => $name,
421 42
                    'regex' => $attrRegex,
422
                    'valueType' => $valueType
423 42
                );
424
            }
425 33
        }
426
427 7
        return array();
428
    }
429
430
    /**
431
     * Required by the Removable interface.
432
     */
433 75 View Code Duplication
    public function remove(LoggerInterface $logger)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
434
    {
435 75
        $hasRemovableElements = $this->configuration->get('element-blacklist') != '';
436 75
        $hasRemovableTypes = $this->configuration->get('type-blacklist') != '';
437 75
        foreach ($this->children as $child) {
438
            // Check types.
439 73
            if ($hasRemovableTypes &&
440 73
                !$this->configuration->isAllowedType($child->getType())) {
441 2
                $logger->debug('Removing ' . $child);
442 2
                $this->removeChild($child);
443
444 2
                continue;
445
            }
446
447
            // Check elements.
448 71
            if ($hasRemovableElements &&
449 71
                $child->getType() == Token::ELEMENT &&
450 71
                !$this->configuration->isAllowedElement($child->getName())) {
451 3
                $logger->debug('Removing ' . $child);
452 3
                $this->removeChild($child);
453
454 3
                continue;
455
            }
456
457
            // Check children.
458 71
            if ($child instanceof Removable) {
459 62
                $child->remove($logger);
460 62
            }
461 75
        }
462 75
    }
463
464
    /**
465
     * Required by the Token interface.
466
     */
467 7
    public function toHtml($prefix, $suffix)
468
    {
469 7
        $output = $this->buildStartTag($prefix, $suffix);
470 7
        if (empty($this->children)) {
471 6
            return $output;
472
        }
473
474 5
        $output .= $this->buildChildrenHtml($prefix, $suffix);
475
476 5
        return $output . $prefix . '</' . $this->name . '>' . $suffix;
477
    }
478
479 81
    protected function buildStartTag($prefix, $suffix, $forceOpen = false)
480
    {
481 81
        $output = $prefix . '<' . $this->name;
482 81
        foreach ($this->attributes as $key => $value) {
483 44
            $output .= ' ' . strtolower($key);
484 44
            if (is_string($value)) {
485
                /// @todo Escape double quotes in value.
486 44
                $output .= '="' . $value . '"';
487 44
            }
488 81
        }
489
490 81
        if (!$forceOpen && empty($this->children)) {
491 27
            return $output . '/>' . $suffix;
492
        }
493
494 79
        return $output . '>' . $suffix;
495
    }
496
497 79
    protected function buildChildrenHtml($prefix, $suffix)
498
    {
499 79
        $output = '';
500 79
        foreach ($this->children as $child) {
501
            $newPrefix = $prefix .
502 77
                str_repeat(
503 77
                    ' ',
504 77
                    $this->configuration->get('indent-spaces')
505 77
                );
506 77
            $output .= $child->toHtml($newPrefix, $suffix);
507 79
        }
508
509 79
        return $output;
510
    }
511
512 49
    public function __toString()
513
    {
514 49
        return '"' . $this->name . '" element';
515
    }
516
}
517