Completed
Push — master ( 62c01b...6e9edd )
by Kevin
02:26
created

Element::toHtml()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 9.4285
cc 2
eloc 6
nc 2
nop 2
crap 2
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
    private $attributes;
24
25
    /** @var array[Token] */
26
    private $children;
27
28
    /** @var string */
29
    private $name;
30
31
    /**
32
     * Constructor
33
     */
34 31
    public function __construct(Configuration $configuration, $name, array $attributes = array(), $parent = null)
35
    {
36 31
        parent::__construct(Token::ELEMENT, $configuration, $parent);
37
38 31
        $this->attributes = array();
39 31
        foreach ($attributes as $key => $value) {
40 17
            $this->addAttribute($key, $value);
41 31
        }
42
43 31
        $this->children = array();
44 31
        $this->setName($name);
45 31
    }
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 21
    public function addChild(Token $token)
111
    {
112 21
        $token->setParent($this);
113 21
        $this->children[] = $token;
114
115 21
        return $this;
116
    }
117
118
    /**
119
     * Required by the ContainsChildren interface.
120
     */
121 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...
122
    {
123 1
        $key = array_search($token, $this->children);
124 1
        if ($key !== false) {
125 1
            unset($this->children[$key]);
126
127 1
            return true;
128
        }
129
130 1
        return false;
131
    }
132
133
    /**
134
     * Getter for 'name'.
135
     */
136 11
    public function getName()
137
    {
138 11
        return $this->name;
139
    }
140
141
    /**
142
     * Chainable setter for 'name'.
143
     */
144 31
    public function setName($name)
145
    {
146 31
        if (!is_string($name)) {
147 1
            throw new \InvalidArgumentException('Element name must be string type.');
148
        }
149
150 31
        $this->name = trim(strtolower($name));
151
152 31
        return $this;
153
    }
154
155
    /**
156
     * Required by the Cleanable interface.
157
     */
158 18
    public function clean(LoggerInterface $logger = null)
159
    {
160 18
        if ($this->configuration->get('clean-strategy') == Configuration::CLEAN_STRATEGY_NONE) {
161 1
            return true;
162
        }
163
164
        // Remove non-standard attributes.
165 17
        foreach ($this->attributes as $name => $value) {
166 11
            $attributeParameters = $this->getAttributeParameters($name);
167 11
            if (empty($attributeParameters)) {
168 3
                if ($logger !== null) {
169 3
                    $logger->debug('Groundskeeper: Removed non-standard attribute "' . $name . '" from element "' . $this->name . '".');
170 3
                }
171
172 3
                unset($this->attributes[$name]);
173
174 3
                continue;
175
            }
176
177
            // Validate attribute value.
178
            list($caseSensitivity, $attributeType) =
179 11
                explode('_', $attributeParameters['valueType']);
180
181
            // Handle case-insensitivity.
182
            // Standard is case-insensitive attribute values should be lower case.
183
            // Not required, so don't throw if out of spec.
184 11
            if ($caseSensitivity == 'ci') {
185 1
                $newValue = strtolower($value);
186 1
                if ($newValue !== $value) {
187 1
                    if ($this->configuration->get('error-strategy') == Configuration::ERROR_STRATEGY_FIX) {
188 1
                        $this->attributes[$name] = $newValue;
189 1
                        if ($logger !== null) {
190 1
                            $logger->debug('Groundskeeper: The value for the attribute "' . $name . '" is case-insensitive.  The value has been converted to lower case.');
191 1
                        }
192 1
                    } elseif ($logger !== null) {
193
                        $logger->debug('Groundskeeper: The value for the attribute "' . $name . '" is case-insensitive.  Consider converting it to lower case.');
194
                    }
195 1
                }
196 1
            }
197
198 11
            switch (substr($attributeType, 0, 3)) {
199 11
            case 'enu': // enumeration
200
                /// @todo
201
                break;
202
203 11
            case 'uri': // URI
204
                /// @todo
205
                break;
206 11
            }
207 17
        }
208
209
        // Clean children.
210 17 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...
211 13
            if ($child instanceof Cleanable) {
212 12
                $isClean = $child->clean($logger);
213 12
                if (!$isClean) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

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