Completed
Push — master ( 6e9edd...3f79e5 )
by Kevin
02:25
created

Element::appendChild()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1
Metric Value
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
crap 1
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\Cleanable;
9
use Groundskeeper\Tokens\ContainsChildren;
10
use Groundskeeper\Tokens\Removable;
11
use Groundskeeper\Tokens\Token;
12
use Psr\Log\LoggerInterface;
13
14
class Element extends AbstractToken implements Cleanable, ContainsChildren, Removable
15
{
16
    const ATTR_CI_ENUM   = 'ci_enu';// case-insensitive enumeration
17
    const ATTR_JS        = 'cs_jsc';
18
    const ATTR_CI_STRING = 'ci_str'; // case-insensitive string
19
    const ATTR_CS_STRING = 'cs_str'; // case-sensitive string
20
    const ATTR_URI       = 'cs_uri';
21
22
    /** @var array */
23
    protected $attributes;
24
25
    /** @var array[Token] */
26
    protected $children;
27
28
    /** @var string */
29
    private $name;
30
31
    /**
32
     * Constructor
33
     */
34 47
    public function __construct(Configuration $configuration, $name, array $attributes = array(), $parent = null)
35
    {
36 47
        parent::__construct(Token::ELEMENT, $configuration, $parent);
37
38 47
        $this->attributes = array();
39 47
        foreach ($attributes as $key => $value) {
40 17
            $this->addAttribute($key, $value);
41 47
        }
42
43 47
        $this->children = array();
44 47
        $this->setName($name);
45 47
    }
46
47
    /**
48
     * Getter for 'attributes'.
49
     */
50 2
    public function getAttributes()
51
    {
52 2
        return $this->attributes;
53
    }
54
55
    /**
56
     * Hasser for 'attributes'.
57
     *
58
     * @param string $key
59
     *
60
     * @return boolean True if the attribute is present.
61
     */
62 4
    public function hasAttribute($key)
63
    {
64 4
        return array_key_exists($key, $this->attributes);
65
    }
66
67 19
    public function addAttribute($key, $value)
68
    {
69 19
        $key = trim(strtolower($key));
70 19
        if ($key == '') {
71 1
            throw new \InvalidArgumentException('Invalid emtpy attribute key.');
72
        }
73
74 18
        $this->attributes[$key] = $value;
75
76 18
        return $this;
77
    }
78
79 1
    public function removeAttribute($key)
80
    {
81 1
        $key = strtolower($key);
82 1
        if (isset($this->attributes[$key])) {
83 1
            unset($this->attributes[$key]);
84
85 1
            return true;
86
        }
87
88 1
        return false;
89
    }
90
91
    /**
92
     * Required by ContainsChildren interface.
93
     */
94 2
    public function getChildren()
95
    {
96 2
        return $this->children;
97
    }
98
99
    /**
100
     * Required by ContainsChildren interface.
101
     */
102 1
    public function hasChild(Token $token)
103
    {
104 1
        return array_search($token, $this->children) !== false;
105
    }
106
107
    /**
108
     * Required by ContainsChildren interface.
109
     */
110 37
    public function appendChild(Token $token)
111
    {
112 37
        $token->setParent($this);
113 37
        $this->children[] = $token;
114
115 37
        return $this;
116
    }
117
118
    /**
119
     * Required by ContainsChildren interface.
120
     */
121 2
    public function prependChild(Token $token)
122
    {
123 2
        $token->setParent($this);
124 2
        array_unshift($this->children, $token);
125
126 2
        return $this;
127
    }
128
129
    /**
130
     * Required by the ContainsChildren interface.
131
     */
132 1 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...
133
    {
134 1
        $key = array_search($token, $this->children);
135 1
        if ($key !== false) {
136 1
            unset($this->children[$key]);
137
138 1
            return true;
139
        }
140
141 1
        return false;
142
    }
143
144
    /**
145
     * Getter for 'name'.
146
     */
147 24
    public function getName()
148
    {
149 24
        return $this->name;
150
    }
151
152
    /**
153
     * Chainable setter for 'name'.
154
     */
155 47
    public function setName($name)
156
    {
157 47
        if (!is_string($name)) {
158 1
            throw new \InvalidArgumentException('Element name must be string type.');
159
        }
160
161 47
        $this->name = trim(strtolower($name));
162
163 47
        return $this;
164
    }
165
166
    /**
167
     * Required by the Cleanable interface.
168
     */
169 32
    public function clean(LoggerInterface $logger = null)
170
    {
171 32
        if ($this->configuration->get('clean-strategy') == Configuration::CLEAN_STRATEGY_NONE) {
172 1
            return true;
173
        }
174
175
        // Remove non-standard attributes.
176 31
        foreach ($this->attributes as $name => $value) {
177 11
            $attributeParameters = $this->getAttributeParameters($name);
178 11
            if (empty($attributeParameters)) {
179 3
                if ($logger !== null) {
180 3
                    $logger->debug('Groundskeeper: Removed non-standard attribute "' . $name . '" from element "' . $this->name . '".');
181 3
                }
182
183 3
                unset($this->attributes[$name]);
184
185 3
                continue;
186
            }
187
188
            // Validate attribute value.
189
            list($caseSensitivity, $attributeType) =
190 11
                explode('_', $attributeParameters['valueType']);
191
192
            // Handle case-insensitivity.
193
            // Standard is case-insensitive attribute values should be lower case.
194
            // Not required, so don't throw if out of spec.
195 11
            if ($caseSensitivity == 'ci') {
196 1
                $newValue = strtolower($value);
197 1
                if ($newValue !== $value) {
198 1
                    if ($this->configuration->get('error-strategy') == Configuration::ERROR_STRATEGY_FIX) {
199 1
                        $this->attributes[$name] = $newValue;
200 1
                        if ($logger !== null) {
201 1
                            $logger->debug('Groundskeeper: The value for the attribute "' . $name . '" is case-insensitive.  The value has been converted to lower case.');
202 1
                        }
203 1
                    } elseif ($logger !== null) {
204
                        $logger->debug('Groundskeeper: The value for the attribute "' . $name . '" is case-insensitive.  Consider converting it to lower case.');
205
                    }
206 1
                }
207 1
            }
208
209 11
            switch (substr($attributeType, 0, 3)) {
210 11
            case 'enu': // enumeration
211
                /// @todo
212
                break;
213
214 11
            case 'uri': // URI
215
                /// @todo
216
                break;
217 11
            }
218 31
        }
219
220
        // Clean children.
221 31
        return AbstractToken::cleanChildTokens(
222 31
            $this->configuration,
223 31
            $this->children,
224
            $logger
225 31
        );
226
    }
227
228 11
    protected function getAllowedAttributes()
229
    {
230
        return array(
231
            // Global Attributes
232 11
            '/^accesskey$/i' => self::ATTR_CS_STRING,
233 11
            '/^class$/i' => self::ATTR_CS_STRING,
234 11
            '/^contenteditable$/i' => self::ATTR_CS_STRING,
235 11
            '/^contextmenu$/i' => self::ATTR_CS_STRING,
236 11
            '/^data-\S/i' => self::ATTR_CS_STRING,
237 11
            '/^dir$/i' => self::ATTR_CI_ENUM . '("ltr","rtl"|"ltr")',
238 11
            '/^draggable$/i' => self::ATTR_CS_STRING,
239 11
            '/^dropzone$/i' => self::ATTR_CS_STRING,
240 11
            '/^hidden$/i' => self::ATTR_CS_STRING,
241 11
            '/^id$/i' => self::ATTR_CS_STRING,
242 11
            '/^is$/i' => self::ATTR_CS_STRING,
243 11
            '/^itemid$/i' => self::ATTR_CS_STRING,
244 11
            '/^itemprop$/i' => self::ATTR_CS_STRING,
245 11
            '/^itemref$/i' => self::ATTR_CS_STRING,
246 11
            '/^itemscope$/i' => self::ATTR_CS_STRING,
247 11
            '/^itemtype$/i' => self::ATTR_CS_STRING,
248 11
            '/^lang$/i' => self::ATTR_CI_STRING,
249 11
            '/^slot$/i' => self::ATTR_CS_STRING,
250 11
            '/^spellcheck$/i' => self::ATTR_CS_STRING,
251 11
            '/^style$/i' => self::ATTR_CS_STRING,
252 11
            '/^tabindex$/i' => self::ATTR_CS_STRING,
253 11
            '/^title$/i' => self::ATTR_CS_STRING,
254 11
            '/^translate$/i' => self::ATTR_CI_ENUM . '("yes","no",""|"yes")',
255
256
            // Event Handler Content Attributes
257
            // https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-content-attributes
258 11
            '/^onabort$/i' => self::ATTR_JS,
259 11
            '/^onautocomplete$/i' => self::ATTR_JS,
260 11
            '/^onautocompleteerror$/i' => self::ATTR_JS,
261 11
            '/^onblur$/i' => self::ATTR_JS,
262 11
            '/^oncancel$/i' => self::ATTR_JS,
263 11
            '/^oncanplay$/i' => self::ATTR_JS,
264 11
            '/^oncanplaythrough$/i' => self::ATTR_JS,
265 11
            '/^onchange$/i' => self::ATTR_JS,
266 11
            '/^onclick$/i' => self::ATTR_JS,
267 11
            '/^onclose$/i' => self::ATTR_JS,
268 11
            '/^oncontextmenu$/i' => self::ATTR_JS,
269 11
            '/^oncuechange$/i' => self::ATTR_JS,
270 11
            '/^ondblclick$/i' => self::ATTR_JS,
271 11
            '/^ondrag$/i' => self::ATTR_JS,
272 11
            '/^ondragend$/i' => self::ATTR_JS,
273 11
            '/^ondragenter$/i' => self::ATTR_JS,
274 11
            '/^ondragexit$/i' => self::ATTR_JS,
275 11
            '/^ondragleave$/i' => self::ATTR_JS,
276 11
            '/^ondragover$/i' => self::ATTR_JS,
277 11
            '/^ondragstart$/i' => self::ATTR_JS,
278 11
            '/^ondrop$/i' => self::ATTR_JS,
279 11
            '/^ondurationchange$/i' => self::ATTR_JS,
280 11
            '/^onemptied$/i' => self::ATTR_JS,
281 11
            '/^onended$/i' => self::ATTR_JS,
282 11
            '/^onerror$/i' => self::ATTR_JS,
283 11
            '/^onfocus$/i' => self::ATTR_JS,
284 11
            '/^oninput$/i' => self::ATTR_JS,
285 11
            '/^oninvalid$/i' => self::ATTR_JS,
286 11
            '/^onkeydown$/i' => self::ATTR_JS,
287 11
            '/^onkeypress$/i' => self::ATTR_JS,
288 11
            '/^onkeyup$/i' => self::ATTR_JS,
289 11
            '/^onload$/i' => self::ATTR_JS,
290 11
            '/^onloadeddata$/i' => self::ATTR_JS,
291 11
            '/^onloadedmetadata$/i' => self::ATTR_JS,
292 11
            '/^onloadstart$/i' => self::ATTR_JS,
293 11
            '/^onmousedown$/i' => self::ATTR_JS,
294 11
            '/^onmouseenter$/i' => self::ATTR_JS,
295 11
            '/^onmouseleave$/i' => self::ATTR_JS,
296 11
            '/^onmousemove$/i' => self::ATTR_JS,
297 11
            '/^onmouseout$/i' => self::ATTR_JS,
298 11
            '/^onmouseover$/i' => self::ATTR_JS,
299 11
            '/^onmouseup$/i' => self::ATTR_JS,
300 11
            '/^onwheel$/i' => self::ATTR_JS,
301 11
            '/^onpause$/i' => self::ATTR_JS,
302 11
            '/^onplay$/i' => self::ATTR_JS,
303 11
            '/^onplaying$/i' => self::ATTR_JS,
304 11
            '/^onprogress$/i' => self::ATTR_JS,
305 11
            '/^onratechange$/i' => self::ATTR_JS,
306 11
            '/^onreset$/i' => self::ATTR_JS,
307 11
            '/^onresize$/i' => self::ATTR_JS,
308 11
            '/^onscroll$/i' => self::ATTR_JS,
309 11
            '/^onseeked$/i' => self::ATTR_JS,
310 11
            '/^onseeking$/i' => self::ATTR_JS,
311 11
            '/^onselect$/i' => self::ATTR_JS,
312 11
            '/^onshow$/i' => self::ATTR_JS,
313 11
            '/^onstalled$/i' => self::ATTR_JS,
314 11
            '/^onsubmit$/i' => self::ATTR_JS,
315 11
            '/^onsuspend$/i' => self::ATTR_JS,
316 11
            '/^ontimeupdate$/i' => self::ATTR_JS,
317 11
            '/^ontoggle$/i' => self::ATTR_JS,
318 11
            '/^onvolumechange$/i' => self::ATTR_JS,
319 11
            '/^onwaiting$/i' => self::ATTR_JS,
320
321
            // WAI-ARIA
322
            // https://w3c.github.io/aria/aria/aria.html
323 11
            '/^role$/i' => self::ATTR_CI_STRING,
324
325
            // ARIA global states and properties
326 11
            '/^aria-atomic$/i' => self::ATTR_CS_STRING,
327 11
            '/^aria-busy$/i' => self::ATTR_CS_STRING,
328 11
            '/^aria-controls$/i' => self::ATTR_CS_STRING,
329 11
            '/^aria-current$/i' => self::ATTR_CS_STRING,
330 11
            '/^aria-describedby$/i' => self::ATTR_CS_STRING,
331 11
            '/^aria-details$/i' => self::ATTR_CS_STRING,
332 11
            '/^aria-disabled$/i' => self::ATTR_CS_STRING,
333 11
            '/^aria-dropeffect$/i' => self::ATTR_CS_STRING,
334 11
            '/^aria-errormessage$/i' => self::ATTR_CS_STRING,
335 11
            '/^aria-flowto$/i' => self::ATTR_CS_STRING,
336 11
            '/^aria-grabbed$/i' => self::ATTR_CS_STRING,
337 11
            '/^aria-haspopup$/i' => self::ATTR_CS_STRING,
338 11
            '/^aria-hidden$/i' => self::ATTR_CS_STRING,
339 11
            '/^aria-invalid$/i' => self::ATTR_CS_STRING,
340 11
            '/^aria-label$/i' => self::ATTR_CS_STRING,
341 11
            '/^aria-labelledby$/i' => self::ATTR_CS_STRING,
342 11
            '/^aria-live$/i' => self::ATTR_CS_STRING,
343 11
            '/^aria-owns$/i' => self::ATTR_CS_STRING,
344 11
            '/^aria-relevant$/i' => self::ATTR_CS_STRING,
345 11
            '/^aria-roledescription$/i' => self::ATTR_CS_STRING,
346
347
            // ARIA widget attributes
348 11
            '/^aria-autocomplete$/i' => self::ATTR_CS_STRING,
349 11
            '/^aria-checked$/i' => self::ATTR_CS_STRING,
350 11
            '/^aria-expanded$/i' => self::ATTR_CS_STRING,
351 11
            '/^aria-level$/i' => self::ATTR_CS_STRING,
352 11
            '/^aria-modal$/i' => self::ATTR_CS_STRING,
353 11
            '/^aria-multiline$/i' => self::ATTR_CS_STRING,
354 11
            '/^aria-multiselectable$/i' => self::ATTR_CS_STRING,
355 11
            '/^aria-orientation$/i' => self::ATTR_CS_STRING,
356 11
            '/^aria-placeholder$/i' => self::ATTR_CS_STRING,
357 11
            '/^aria-pressed$/i' => self::ATTR_CS_STRING,
358 11
            '/^aria-readonly$/i' => self::ATTR_CS_STRING,
359 11
            '/^aria-required$/i' => self::ATTR_CS_STRING,
360 11
            '/^aria-selected$/i' => self::ATTR_CS_STRING,
361 11
            '/^aria-sort$/i' => self::ATTR_CS_STRING,
362 11
            '/^aria-valuemax$/i' => self::ATTR_CS_STRING,
363 11
            '/^aria-valuemin$/i' => self::ATTR_CS_STRING,
364 11
            '/^aria-valuenow$/i' => self::ATTR_CS_STRING,
365 11
            '/^aria-valuetext$/i' => self::ATTR_CS_STRING,
366
367
            // ARIA relationship attributes
368 11
            '/^aria-activedescendant$/i' => self::ATTR_CS_STRING,
369 11
            '/^aria-colcount$/i' => self::ATTR_CS_STRING,
370 11
            '/^aria-colindex$/i' => self::ATTR_CS_STRING,
371 11
            '/^aria-colspan$/i' => self::ATTR_CS_STRING,
372 11
            '/^aria-posinset$/i' => self::ATTR_CS_STRING,
373 11
            '/^aria-rowcount$/i' => self::ATTR_CS_STRING,
374 11
            '/^aria-rowindex$/i' => self::ATTR_CS_STRING,
375 11
            '/^aria-rowspan$/i' => self::ATTR_CS_STRING,
376
            '/^aria-setsize$/i' => self::ATTR_CS_STRING
377 11
        );
378
    }
379
380 11
    protected function getAttributeParameters($name)
381
    {
382 11
        $allowedAttributes = $this->getAllowedAttributes();
383 11
        foreach ($allowedAttributes as $attrRegex => $valueType) {
384 11
            if (preg_match($attrRegex, $name) === 1) {
385
                return array(
386 11
                    'name' => $name,
387 11
                    'regex' => $attrRegex,
388
                    'valueType' => $valueType
389 11
                );
390
            }
391 11
        }
392
393 3
        return array();
394
    }
395
396
    /**
397
     * Required by the Removable interface.
398
     */
399 29 View Code Duplication
    public function remove(LoggerInterface $logger = null)
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...
400
    {
401 29
        $hasRemovableTypes = $this->configuration->get('type-blacklist') !==
402 29
            Configuration::TYPE_BLACKLIST_NONE;
403 29
        $hasRemovableElements = $this->configuration->get('element-blacklist') !==
404 29
            Configuration::ELEMENT_BLACKLIST_NONE;
405 29
        foreach ($this->children as $key => $child) {
406
            // Check types.
407 27
            if ($hasRemovableTypes &&
408 27
                !$this->configuration->isAllowedType($child->getType())) {
409 4
                unset($this->children[$key]);
410 4
                if ($logger !== null) {
411 4
                    $logger->debug('Removing token of type: ' . $child->getType());
412 4
                }
413
414 4
                continue;
415
            }
416
417
            // Check elements.
418 27
            if ($hasRemovableElements &&
419 27
                $child instanceof Element &&
420 27
                !$this->configuration->isAllowedElement($child->getName())) {
421 3
                unset($this->children[$key]);
422 3
                if ($logger !== null) {
423 3
                    $logger->debug('Removing element of type: ' . $child->getName());
424 3
                }
425
426 3
                continue;
427
            }
428
429
            // Check children.
430 27
            if ($child instanceof Removable) {
431 26
                $child->remove($logger);
432 26
            }
433 29
        }
434 29
    }
435
436
    /**
437
     * Required by the Token interface.
438
     */
439 22
    public function toHtml($prefix, $suffix)
440
    {
441 22
        $output = $this->buildStartTag($prefix, $suffix);
442 22
        if (empty($this->children)) {
443 9
            return $output;
444
        }
445
446 20
        $output .= $this->buildChildrenHtml($prefix, $suffix);
447
448 20
        return $output . $prefix . '</' . $this->name . '>' . $suffix;
449
    }
450
451 29
    protected function buildStartTag($prefix, $suffix, $forceOpen = false)
452
    {
453 29
        $output = $prefix . '<' . $this->name;
454 29
        foreach ($this->attributes as $key => $value) {
455 13
            $output .= ' ' . strtolower($key);
456 13
            if (is_string($value)) {
457
                /// @todo Escape double quotes in value.
458 11
                $output .= '="' . $value . '"';
459 11
            }
460 29
        }
461
462 29
        if (!$forceOpen && empty($this->children)) {
463 15
            return $output . '/>' . $suffix;
464
        }
465
466 27
        return $output . '>' . $suffix;
467
    }
468
469 27
    protected function buildChildrenHtml($prefix, $suffix)
470
    {
471 27
        $output = '';
472 27
        foreach ($this->children as $child) {
473
            $newPrefix = $prefix .
474 27
                str_repeat(
475 27
                    ' ',
476 27
                    $this->configuration->get('indent-spaces')
477 27
                );
478 27
            $output .= $child->toHtml($newPrefix, $suffix);
479 27
        }
480
481 27
        return $output;
482
    }
483
}
484