Completed
Push — master ( 0777f1...91ab9a )
by Kevin
03:18
created

Element::addAttribute()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 18

Duplication

Lines 8
Ratio 29.63 %

Code Coverage

Tests 19
CRAP Score 3

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 8
loc 27
ccs 19
cts 19
cp 1
rs 8.8571
cc 3
eloc 18
nc 3
nop 2
crap 3
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
    /** @var array */
11
    protected $attributes;
12
13
    /** @var array[Token] */
14
    protected $children;
15
16
    /** @var string */
17
    private $name;
18
19
    /**
20
     * Constructor
21
     */
22 105
    public function __construct(Configuration $configuration, $name, array $attributes = array())
23
    {
24 105
        parent::__construct($configuration);
25
26 105
        $this->attributes = array();
27 105
        foreach ($attributes as $key => $value) {
28 57
            $this->addAttribute($key, $value);
29 105
        }
30
31 105
        $this->children = array();
32
33 105
        if (!is_string($name)) {
34 1
            throw new \InvalidArgumentException('Element name must be string type.');
35 3
        }
36
37 104
        $this->name = trim(strtolower($name));
38 104
    }
39
40
    /**
41
     * Getter for 'attributes'.
42
     */
43 4
    public function getAttributes()
44
    {
45 2
        $attributeArray = array();
46 4
        foreach ($this->attributes as $attribute) {
47 1
            $attributeArray[$attribute->getName()] = $attribute->getValue();
48 2
        }
49
50 2
        return $attributeArray;
51 2
    }
52
53 4
    public function getAttribute($key)
54
    {
55 4
        if (!$this->hasAttribute($key)) {
56 1
            throw new \InvalidArgumentException('Invalid attribute key: ' . $key);
57
        }
58
59 3
        $attributeObject = $this->attributes[$key];
60
61 3
        return $attributeObject->getValue();
62
    }
63
64
    /**
65
     * Hasser for 'attributes'.
66
     *
67
     * @param string $key
68
     *
69
     * @return bool True if the attribute is present.
70
     */
71 24
    public function hasAttribute($key)
72
    {
73 24
        return array_key_exists($key, $this->attributes);
74
    }
75
76 59
    public function addAttribute($key, $value)
77
    {
78 59
        $key = trim(strtolower($key));
79 58
        if ($key == '') {
80 1
            throw new \InvalidArgumentException('Invalid empty attribute key.');
81 1
        }
82
83 57
        $attributeParameters = $this->getAttributeParameters($key);
84 57
        $isStandard = true;
85 57 View Code Duplication
        if (empty($attributeParameters)) {
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...
86
            $attributeParameters = array(
87 10
                'name' => $key,
88 10
                'regex' => '/\S*/i',
89
                'valueType' => Attribute::CS_STRING
90 11
            );
91 10
            $isStandard = false;
92 10
        }
93
94 57
        $this->attributes[$key] = new Attribute(
95 57
            $key,
96 57
            $value,
97 57
            $attributeParameters['valueType'],
0 ignored issues
show
Unused Code introduced by
The call to Attribute::__construct() has too many arguments starting with $attributeParameters['valueType'].

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
98
            $isStandard
99 57
        );
100
101 57
        return $this;
102
    }
103
104 8
    public function removeAttribute($key)
105
    {
106 5
        $key = trim(strtolower($key));
107 2
        if (isset($this->attributes[$key])) {
108 2
            unset($this->attributes[$key]);
109
110 2
            return true;
111
        }
112
113 2
        return false;
114 8
    }
115
116
    /**
117
     * Required by ContainsChildren interface.
118
     */
119 3
    public function getChildren()
120
    {
121 3
        return $this->children;
122
    }
123
124
    /**
125
     * Required by ContainsChildren interface.
126
     */
127 1
    public function hasChild(Token $token)
128
    {
129 1
        return array_search($token, $this->children, true) !== false;
130
    }
131
132
    /**
133
     * Required by ContainsChildren interface.
134
     */
135 89
    public function appendChild(Token $token)
136
    {
137 89
        $token->setParent($this);
138 89
        $this->children[] = $token;
139
140 89
        return $this;
141
    }
142
143
    /**
144
     * Required by ContainsChildren interface.
145
     */
146 5
    public function prependChild(Token $token)
147
    {
148 5
        $token->setParent($this);
149 5
        array_unshift($this->children, $token);
150
151 5
        return $this;
152
    }
153
154
    /**
155
     * Required by the ContainsChildren interface.
156
     */
157 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...
158
    {
159 27
        $key = array_search($token, $this->children, true);
160 27
        if ($key !== false) {
161 27
            unset($this->children[$key]);
162
163 27
            return true;
164
        }
165
166 1
        return false;
167
    }
168
169
    /**
170
     * Getter for 'name'.
171
     */
172 85
    public function getName()
173
    {
174 85
        return $this->name;
175
    }
176
177
    /**
178
     * Required by the Cleanable interface.
179
     */
180 88
    public function clean(LoggerInterface $logger)
181
    {
182 88
        if ($this->configuration->get('clean-strategy') == Configuration::CLEAN_STRATEGY_NONE) {
183 1
            return true;
184
        }
185
186
        // Assign attributes to the attributes. (Soooo meta ....)
187 87
        foreach ($this->attributes as $attribute) {
188 49
            $attributeParameters = $this->getAttributeParameters(
189 49
                $attribute->getName()
190 49
            );
191 49
            $isStandard = true;
192 49 View Code Duplication
            if (empty($attributeParameters)) {
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...
193
                $attributeParameters = array(
194 8
                    'name' => $attribute->getName(),
195 8
                    'regex' => '/\S*/i',
196
                    'valueType' => Attribute::CS_STRING
197 8
                );
198 8
                $isStandard = false;
199 8
            }
200
201 49
            $attribute->setType($attributeParameters['valueType']);
202 49
            $attribute->setIsStandard($isStandard);
203 87
        }
204
205
        // Clean attributes.
206 87
        foreach ($this->attributes as $attribute) {
207 49
            $attributeCleanResult = $attribute->clean(
208 49
                $this->configuration,
209 49
                $this,
210
                $logger
211 49
            );
212 49
            if (!$attributeCleanResult && $this->configuration->get('clean-strategy') !== Configuration::CLEAN_STRATEGY_LENIENT) {
213 9
                unset($this->attributes[$attribute->getName()]);
214 9
            }
215 87
        }
216
217
        // 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...
218 87
        $this->fixSelf($logger);
219
220
        // Remove self or children?
221 87
        if ($this->configuration->get('clean-strategy') !== Configuration::CLEAN_STRATEGY_LENIENT) {
222
            // Remove self?
223 87
            if ($this->removeInvalidSelf($logger)) {
224 19
                return false;
225
            }
226
227
            // Remove children?
228 87
            $this->removeInvalidChildren($logger);
229 87
        }
230
231
        // Clean children.
232 87
        return AbstractToken::cleanChildTokens(
233 87
            $this->configuration,
234 87
            $this->children,
235
            $logger
236 87
        );
237
    }
238
239 81
    protected function fixSelf(LoggerInterface $logger)
240
    {
241 81
    }
242
243 78
    protected function removeInvalidChildren(LoggerInterface $logger)
244
    {
245 78
    }
246
247 80
    protected function removeInvalidSelf(LoggerInterface $logger)
248
    {
249 80
        return false;
250
    }
251
252 57
    protected function getAllowedAttributes()
253
    {
254
        return array(
255
            // Global Attributes
256 57
            '/^accesskey$/i' => Attribute::CS_STRING,
257 57
            '/^class$/i' => Attribute::CS_STRING,
258 57
            '/^contenteditable$/i' => Attribute::CS_STRING,
259 57
            '/^contextmenu$/i' => Attribute::CS_STRING,
260 57
            '/^data-\S/i' => Attribute::CS_STRING,
261 57
            '/^dir$/i' => Attribute::CI_ENUM . '("ltr","rtl"|"ltr")',
262 57
            '/^draggable$/i' => Attribute::CS_STRING,
263 57
            '/^dropzone$/i' => Attribute::CS_STRING,
264 57
            '/^hidden$/i' => Attribute::CS_STRING,
265 57
            '/^id$/i' => Attribute::CS_STRING,
266 57
            '/^is$/i' => Attribute::CS_STRING,
267 57
            '/^itemid$/i' => Attribute::CS_STRING,
268 57
            '/^itemprop$/i' => Attribute::CS_STRING,
269 57
            '/^itemref$/i' => Attribute::CS_STRING,
270 57
            '/^itemscope$/i' => Attribute::CS_STRING,
271 57
            '/^itemtype$/i' => Attribute::CS_STRING,
272 57
            '/^lang$/i' => Attribute::CI_STRING,
273 57
            '/^slot$/i' => Attribute::CS_STRING,
274 57
            '/^spellcheck$/i' => Attribute::CS_STRING,
275 57
            '/^style$/i' => Attribute::CS_STRING,
276 57
            '/^tabindex$/i' => Attribute::CS_STRING,
277 57
            '/^title$/i' => Attribute::CS_STRING,
278 57
            '/^translate$/i' => Attribute::CI_ENUM . '("yes","no",""|"yes")',
279
280
            // Event Handler Content Attributes
281
            // https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-content-attributes
282 57
            '/^onabort$/i' => Attribute::JS,
283 57
            '/^onautocomplete$/i' => Attribute::JS,
284 57
            '/^onautocompleteerror$/i' => Attribute::JS,
285 57
            '/^onblur$/i' => Attribute::JS,
286 57
            '/^oncancel$/i' => Attribute::JS,
287 57
            '/^oncanplay$/i' => Attribute::JS,
288 57
            '/^oncanplaythrough$/i' => Attribute::JS,
289 57
            '/^onchange$/i' => Attribute::JS,
290 57
            '/^onclick$/i' => Attribute::JS,
291 57
            '/^onclose$/i' => Attribute::JS,
292 57
            '/^oncontextmenu$/i' => Attribute::JS,
293 57
            '/^oncuechange$/i' => Attribute::JS,
294 57
            '/^ondblclick$/i' => Attribute::JS,
295 57
            '/^ondrag$/i' => Attribute::JS,
296 57
            '/^ondragend$/i' => Attribute::JS,
297 57
            '/^ondragenter$/i' => Attribute::JS,
298 57
            '/^ondragexit$/i' => Attribute::JS,
299 57
            '/^ondragleave$/i' => Attribute::JS,
300 57
            '/^ondragover$/i' => Attribute::JS,
301 57
            '/^ondragstart$/i' => Attribute::JS,
302 57
            '/^ondrop$/i' => Attribute::JS,
303 57
            '/^ondurationchange$/i' => Attribute::JS,
304 57
            '/^onemptied$/i' => Attribute::JS,
305 57
            '/^onended$/i' => Attribute::JS,
306 57
            '/^onerror$/i' => Attribute::JS,
307 57
            '/^onfocus$/i' => Attribute::JS,
308 57
            '/^oninput$/i' => Attribute::JS,
309 57
            '/^oninvalid$/i' => Attribute::JS,
310 57
            '/^onkeydown$/i' => Attribute::JS,
311 57
            '/^onkeypress$/i' => Attribute::JS,
312 57
            '/^onkeyup$/i' => Attribute::JS,
313 57
            '/^onload$/i' => Attribute::JS,
314 57
            '/^onloadeddata$/i' => Attribute::JS,
315 57
            '/^onloadedmetadata$/i' => Attribute::JS,
316 57
            '/^onloadstart$/i' => Attribute::JS,
317 57
            '/^onmousedown$/i' => Attribute::JS,
318 57
            '/^onmouseenter$/i' => Attribute::JS,
319 57
            '/^onmouseleave$/i' => Attribute::JS,
320 57
            '/^onmousemove$/i' => Attribute::JS,
321 57
            '/^onmouseout$/i' => Attribute::JS,
322 57
            '/^onmouseover$/i' => Attribute::JS,
323 57
            '/^onmouseup$/i' => Attribute::JS,
324 57
            '/^onwheel$/i' => Attribute::JS,
325 57
            '/^onpause$/i' => Attribute::JS,
326 57
            '/^onplay$/i' => Attribute::JS,
327 57
            '/^onplaying$/i' => Attribute::JS,
328 57
            '/^onprogress$/i' => Attribute::JS,
329 57
            '/^onratechange$/i' => Attribute::JS,
330 57
            '/^onreset$/i' => Attribute::JS,
331 57
            '/^onresize$/i' => Attribute::JS,
332 57
            '/^onscroll$/i' => Attribute::JS,
333 57
            '/^onseeked$/i' => Attribute::JS,
334 57
            '/^onseeking$/i' => Attribute::JS,
335 57
            '/^onselect$/i' => Attribute::JS,
336 57
            '/^onshow$/i' => Attribute::JS,
337 57
            '/^onstalled$/i' => Attribute::JS,
338 57
            '/^onsubmit$/i' => Attribute::JS,
339 57
            '/^onsuspend$/i' => Attribute::JS,
340 57
            '/^ontimeupdate$/i' => Attribute::JS,
341 57
            '/^ontoggle$/i' => Attribute::JS,
342 57
            '/^onvolumechange$/i' => Attribute::JS,
343 57
            '/^onwaiting$/i' => Attribute::JS,
344
345
            // WAI-ARIA
346
            // https://w3c.github.io/aria/aria/aria.html
347 57
            '/^role$/i' => Attribute::CI_STRING,
348
349
            // ARIA global states and properties
350 57
            '/^aria-atomic$/i' => Attribute::CS_STRING,
351 57
            '/^aria-busy$/i' => Attribute::CS_STRING,
352 57
            '/^aria-controls$/i' => Attribute::CS_STRING,
353 57
            '/^aria-current$/i' => Attribute::CS_STRING,
354 57
            '/^aria-describedby$/i' => Attribute::CS_STRING,
355 57
            '/^aria-details$/i' => Attribute::CS_STRING,
356 57
            '/^aria-disabled$/i' => Attribute::CS_STRING,
357 57
            '/^aria-dropeffect$/i' => Attribute::CS_STRING,
358 57
            '/^aria-errormessage$/i' => Attribute::CS_STRING,
359 57
            '/^aria-flowto$/i' => Attribute::CS_STRING,
360 57
            '/^aria-grabbed$/i' => Attribute::CS_STRING,
361 57
            '/^aria-haspopup$/i' => Attribute::CS_STRING,
362 57
            '/^aria-hidden$/i' => Attribute::CS_STRING,
363 57
            '/^aria-invalid$/i' => Attribute::CS_STRING,
364 57
            '/^aria-label$/i' => Attribute::CS_STRING,
365 57
            '/^aria-labelledby$/i' => Attribute::CS_STRING,
366 57
            '/^aria-live$/i' => Attribute::CS_STRING,
367 57
            '/^aria-owns$/i' => Attribute::CS_STRING,
368 57
            '/^aria-relevant$/i' => Attribute::CS_STRING,
369 57
            '/^aria-roledescription$/i' => Attribute::CS_STRING,
370
371
            // ARIA widget attributes
372 57
            '/^aria-autocomplete$/i' => Attribute::CS_STRING,
373 57
            '/^aria-checked$/i' => Attribute::CS_STRING,
374 57
            '/^aria-expanded$/i' => Attribute::CS_STRING,
375 57
            '/^aria-level$/i' => Attribute::CS_STRING,
376 57
            '/^aria-modal$/i' => Attribute::CS_STRING,
377 57
            '/^aria-multiline$/i' => Attribute::CS_STRING,
378 57
            '/^aria-multiselectable$/i' => Attribute::CS_STRING,
379 57
            '/^aria-orientation$/i' => Attribute::CS_STRING,
380 57
            '/^aria-placeholder$/i' => Attribute::CS_STRING,
381 57
            '/^aria-pressed$/i' => Attribute::CS_STRING,
382 57
            '/^aria-readonly$/i' => Attribute::CS_STRING,
383 57
            '/^aria-required$/i' => Attribute::CS_STRING,
384 57
            '/^aria-selected$/i' => Attribute::CS_STRING,
385 57
            '/^aria-sort$/i' => Attribute::CS_STRING,
386 57
            '/^aria-valuemax$/i' => Attribute::CS_STRING,
387 57
            '/^aria-valuemin$/i' => Attribute::CS_STRING,
388 57
            '/^aria-valuenow$/i' => Attribute::CS_STRING,
389 57
            '/^aria-valuetext$/i' => Attribute::CS_STRING,
390
391
            // ARIA relationship attributes
392 57
            '/^aria-activedescendant$/i' => Attribute::CS_STRING,
393 57
            '/^aria-colcount$/i' => Attribute::CS_STRING,
394 57
            '/^aria-colindex$/i' => Attribute::CS_STRING,
395 57
            '/^aria-colspan$/i' => Attribute::CS_STRING,
396 57
            '/^aria-posinset$/i' => Attribute::CS_STRING,
397 57
            '/^aria-rowcount$/i' => Attribute::CS_STRING,
398 57
            '/^aria-rowindex$/i' => Attribute::CS_STRING,
399 57
            '/^aria-rowspan$/i' => Attribute::CS_STRING,
400
            '/^aria-setsize$/i' => Attribute::CS_STRING
401 57
        );
402
    }
403
404 57
    private function getAttributeParameters($key)
405
    {
406 57
        $allowedAttributes = $this->getAllowedAttributes();
407 57
        foreach ($allowedAttributes as $attrRegex => $valueType) {
408 57
            if (preg_match($attrRegex, $key) === 1) {
409
                return array(
410 56
                    'name' => $key,
411 56
                    'regex' => $attrRegex,
412
                    'valueType' => $valueType
413 56
                );
414
            }
415 47
        }
416
417 10
        return array();
418
    }
419
420
    /**
421
     * Required by the Removable interface.
422
     */
423 85 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...
424
    {
425 85
        $hasRemovableElements = $this->configuration->get('element-blacklist') != '';
426 85
        $hasRemovableTypes = $this->configuration->get('type-blacklist') != '';
427 85
        foreach ($this->children as $child) {
428
            // Check types.
429 80
            if ($hasRemovableTypes &&
430 80
                !$this->configuration->isAllowedType($child->getType())) {
431 2
                $logger->debug('Removing ' . $child);
432 2
                $this->removeChild($child);
433
434 2
                continue;
435
            }
436
437
            // Check elements.
438 78
            if ($hasRemovableElements &&
439 78
                $child instanceof self &&
440 78
                !$this->configuration->isAllowedElement($child->getName())) {
441 3
                $logger->debug('Removing ' . $child);
442 3
                $this->removeChild($child);
443
444 3
                continue;
445
            }
446
447
            // Check children.
448 78
            if ($child instanceof Removable) {
449 67
                $child->remove($logger);
450 67
            }
451 85
        }
452 85
    }
453
454
    /**
455
     * Required by the Token interface.
456
     */
457 8
    public function toHtml($prefix, $suffix)
458
    {
459 8
        $output = $this->buildStartTag($prefix, $suffix);
460 8
        if (empty($this->children)) {
461 6
            return $output;
462
        }
463
464 6
        $output .= $this->buildChildrenHtml($prefix, $suffix);
465
466 6
        return $output . $prefix . '</' . $this->name . '>' . $suffix;
467
    }
468
469 91
    protected function buildStartTag($prefix, $suffix, $forceOpen = false)
470
    {
471 91
        $output = $prefix . '<' . $this->name;
472 91
        foreach ($this->attributes as $attribute) {
473 51
            $output .= ' ' . (string) $attribute;
474 91
        }
475
476 91
        if (!$forceOpen && empty($this->children)) {
477 30
            return $output . '/>' . $suffix;
478
        }
479
480 87
        return $output . '>' . $suffix;
481
    }
482
483 87
    protected function buildChildrenHtml($prefix, $suffix)
484
    {
485 87
        $output = '';
486 87
        foreach ($this->children as $child) {
487
            $newPrefix = $prefix .
488 84
                str_repeat(
489 84
                    ' ',
490 84
                    $this->configuration->get('indent-spaces')
491 84
                );
492 84
            $output .= $child->toHtml($newPrefix, $suffix);
493 87
        }
494
495 87
        return $output;
496
    }
497
498 17
    public function getType()
499
    {
500 17
        return Token::ELEMENT;
501
    }
502
503 56
    public function __toString()
504
    {
505 56
        return '"' . $this->name . '" element';
506
    }
507
}
508