Completed
Push — master ( 45904d...9fbe45 )
by Kevin
03:18
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

Importance

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