Completed
Push — master ( 226547...612b7b )
by Nathan
02:27
created

HtmlElement.php (8 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace NatePage\EasyHtmlElement;
4
5
use NatePage\EasyHtmlElement\Exception\InvalidElementException;
6
use NatePage\EasyHtmlElement\Exception\InvalidArgumentsNumberException;
7
use NatePage\EasyHtmlElement\Exception\UndefinedElementException;
8
9
class HtmlElement implements HtmlElementInterface
10
{
11
    /** @var array  */
12
    private $map;
13
14
    /** @var EscaperInterface */
15
    private $escaper;
16
17
    /** @var array The already resolved elements */
18
    private $resolved = array();
19
20
    /** @var array The default values of element options */
21
    private $defaults = array(
22
        'parent' => null,
23
        'children' => array(),
24
        'extends' => array(),
25
        'attr' => array(),
26
        'text' => null,
27
        'type' => null,
28
        'class' => Element::class
29
    );
30
31
    /** @var array The options to check to valid a branch */
32
    private $checks = array('parent', 'extends', 'children');
33
34
    /** @var array The mergeable attributes */
35
    private $mergeableAttributes = array('class', 'style');
36
37
    /** @var array The special escaping types */
38
    private $specialEscapingTypes = array('script', 'style');
39
40
    /** @var bool Determine if html is escaped or not */
41
    private $escapeHtml = true;
42
43
    /** @var bool Determine if html attributes are escaped or not */
44
    private $escapeHtmlAttr = true;
45
46
    /** @var bool Determine if javascript is escaped or not */
47
    private $escapeJs = true;
48
49
    /** @var bool Determine if css is escaped or not */
50
    private $escapeCss = true;
51
52
    /** @var bool Determine if urls are escaped or not */
53
    private $escapeUrl = true;
54
55
    /**
56
     * HtmlElement constructor.
57
     *
58
     * @param array $map                     The elements map
59
     * @param EscaperInterface|null $escaper The escaper, by default ZendFramework/Escaper is used
60
     * @param string $encoding               The encoding used for escaping, by default utf-8 is used
61
     */
62
    public function __construct(array $map = array(), EscaperInterface $escaper = null, $encoding = 'utf-8')
63
    {
64
        $this->map = $map;
65
        $this->escaper = null !== $escaper ? $escaper : new Escaper($encoding);
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function setMap(array $map)
72
    {
73
        $this->map = $map;
74
75
        return $this;
76
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81
    public function getMap()
82
    {
83
        return $this->map;
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function addOneToMap($name, array $element)
90
    {
91
        $this->map[$name] = $element;
92
93
        return $this;
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99
    public function addManyToMap(array $elements)
100
    {
101
        foreach($elements as $name => $element){
102
            $this->addOneToMap($name, $element);
103
        }
104
105
        return $this;
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function setEscaper(EscaperInterface $escaper)
112
    {
113
        $this->escaper = $escaper;
114
115
        return $this;
116
    }
117
118
    /**
119
     * {@inheritdoc}
120
     */
121
    public function getEscaper()
122
    {
123
        return $this->escaper;
124
    }
125
126
    /**
127
     * {@inheritdoc}
128
     */
129
    public function setEscapeHtml($escapeHtml = true)
130
    {
131
        $this->escapeHtml = $escapeHtml;
132
133
        return $this;
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139
    public function isEscapeHtml()
140
    {
141
        return $this->escapeHtml;
142
    }
143
144
    /**
145
     * {@inheritdoc}
146
     */
147
    public function setEscapeHtmlAttr($escapeHtmlAttr = true)
148
    {
149
        $this->escapeHtmlAttr = $escapeHtmlAttr;
150
151
        return $this;
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157
    public function isEscapeHtmlAttr()
158
    {
159
        return $this->escapeHtmlAttr;
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function setEscapeJs($escapeJs = true)
166
    {
167
        $this->escapeJs = $escapeJs;
168
169
        return $this;
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175
    public function isEscapeJs()
176
    {
177
        return $this->escapeJs;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183
    public function setEscapeCss($escapeCss = true)
184
    {
185
        $this->escapeCss = $escapeCss;
186
187
        return $this;
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193
    public function isEscapeCss()
194
    {
195
        return $this->escapeCss;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function setEscapeUrl($escapeUrl = true)
202
    {
203
        $this->escapeUrl = $escapeUrl;
204
205
        return $this;
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211
    public function isEscapeUrl()
212
    {
213
        return $this->escapeUrl;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219
    public function exists($name)
220
    {
221
        return array_key_exists(lcfirst($name), $this->map);
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227
    public function load($name, array $parameters = array(), array $attributes = array(), array $children = array())
228
    {
229
        $element = $this->getInstance($name, $parameters, true);
230
231
        $element->addAttributes($this->escapeAttributes($attributes));
232
233
        foreach($children as $child){
234
            $element->addChild($this->escape($child));
235
        }
236
237
        return $element;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243
    static public function create($type = null, $text = null, array $attributes = array(), array $children = array())
0 ignored issues
show
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
244
    {
245
        $htmlElement = new HtmlElement();
246
247
        $attributes = $htmlElement->escapeAttributes($attributes);
248
249
        foreach($children as $key => $child){
250
            $children[$key] = $htmlElement->escape($child);
251
        }
252
253
        return $htmlElement->escape(new Element($type, $text, $attributes, $children));
254
    }
255
256
    /**
257
     * Create element on static calls.
258
     *
259
     * @param string $type     The element type
260
     * @param array $arguments The arguments array to set:
261
     *                         [0] = text (string)
262
     *                         [1] = attributes (array)
263
     *                         [2] = children (array)
264
     *
265
     * @return ElementInterface
266
     *
267
     * @throws InvalidArgumentsNumberException If the arguments length is more than 3
268
     */
269
    static public function __callStatic($type, $arguments)
0 ignored issues
show
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
270
    {
271
        switch(count($arguments)){
272
            case 0:
273
                return self::create($type);
274
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
275
            case 1:
276
                return self::create($type, $arguments[0]);
277
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
278
            case 2:
279
                return self::create($type, $arguments[0], $arguments[1]);
280
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
281
            case 3:
282
                return self::create($type, $arguments[0], $arguments[1], $arguments[2]);
283
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
284
            default:
285
                throw new InvalidArgumentsNumberException(sprintf(
286
                    'Maximum numbers of arguments is %d, [%d] given.',
287
                    3,
288
                    count($arguments)
289
                ));
290
                break;
0 ignored issues
show
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
291
        }
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     */
297
    public function escape(ElementInterface $element)
298
    {
299
        if($this->escapeHtml && !in_array($element->getType(), $this->specialEscapingTypes)){
300
            $element->setText($this->escaper->escapeHtml($element->getText()));
301
        }
302
303
        $element->setAttributes($this->escapeAttributes($element->getAttributes()));
304
305
        if($this->escapeJs && 'script' == $element->getType()){
306
            $element->setText($this->escaper->escapeJs($element->getText()));
307
        }
308
309
        if($this->escapeCss && 'style' == $element->getType()){
310
            $element->setText($this->escaper->escapeCss($element->getText()));
311
        }
312
313
        return $element;
314
    }
315
316
    /**
317
     * {@inheritdoc}
318
     */
319
    public function escapeAttributes(array $attributes)
320
    {
321
        if($this->escapeHtmlAttr || $this->escapeUrl){
322
            foreach($attributes as $attr => $value){
323
                if('href' == $attr){
324
                    if($this->escapeUrl){
325
                        $value = $this->escaper->escapeUrl($value);
326
                    }
327
                } else {
328
                    if($this->escapeHtmlAttr){
329
                        $value = $this->escaper->escapeHtmlAttr($value);
330
                    }
331
                }
332
333
                $attributes[$attr] = $value;
334
            }
335
        }
336
337
        return $attributes;
338
    }
339
340
    /**
341
     * Get the element instance.
342
     *
343
     * @param string $name      The element name
344
     * @param array $parameters The parameters to replace in element
345
     * @param bool $mainCall    Determine if it's the main(first) call of the method
346
     *
347
     * @return ElementInterface
348
     */
349
    private function getInstance($name, array $parameters, $mainCall = false)
350
    {
351
        $element = $this->resolveElement($name, $parameters, $mainCall);
352
353
        $class = $element['class'];
354
        $type = $element['type'];
355
        $text = $element['text'];
356
        $attributes = $element['attr'];
357
358
        $instance = new $class($type, $text, $attributes);
359
360
        if(!$instance instanceof ElementInterface){
361
            throw new InvalidElementException(sprintf(
362
                'The element "%s" does not implement the %s',
363
                get_class($instance),
364
                ElementInterface::class
365
            ));
366
        }
367
368
        $children = array();
369
        foreach((array) $element['children'] as $child){
370
            $children[] = $this->getInstance($child, $parameters);
371
        }
372
373
        $instance->setChildren($children);
374
375
        if(null !== $element['parent']){
376
            $parent = $this->getInstance($element['parent'], $parameters);
377
378
            $parent->addChild($instance);
379
        }
380
381
        return $this->escape($instance);
382
    }
383
384
    /**
385
     * Get the resolved element representation.
386
     *
387
     * @param string $name      The current element name
388
     * @param array $parameters The parameters to replace in element
389
     * @param bool $mainCall    Determine if it's the main(first) call of the method
390
     *
391
     * @return array
392
     */
393
    private function resolveElement($name, array $parameters, $mainCall = false)
394
    {
395
        $getCurrent = true;
396
397
        if(is_array($name)){
398
            $current = $name;
399
            $name = $current['name'];
400
401
            $getCurrent = false;
402
        }
403
404
        if($this->alreadyResolved($name)){
405
            return $this->resolved[$name];
406
        }
407
408
        if($getCurrent && !$this->exists($name)){
409
            throw new UndefinedElementException(sprintf('The element with name "%s" does not exist.', $name));
410
        }
411
412
        if($mainCall){
413
            $this->validBranch($name);
414
        }
415
416
        if($getCurrent){
417
            $current = $this->getCurrentElement($name);
418
        }
419
420
        foreach($this->defaults as $default => $value){
421
            if(!isset($current[$default])){
422
                $current[$default] = $value;
0 ignored issues
show
The variable $current does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
423
            }
424
        }
425
426
        $current = $this->replaceParameters($current, $parameters);
427
428
        foreach((array) $current['extends'] as $extend){
429
            $extend = $this->resolveElement($extend, $parameters);
430
            $current = $this->extendElement($extend, $current);
431
        }
432
433
        $this->resolved[$name] = $current;
434
435
        return $current;
436
    }
437
438
    /**
439
     * Check if an element has been already resolved.
440
     *
441
     * @param string $name
442
     *
443
     * @return bool
444
     */
445
    private function alreadyResolved($name)
446
    {
447
        return array_key_exists($name, $this->resolved);
448
    }
449
450
    /**
451
     * Valid the current map branch.
452
     *
453
     * @param string $name               The current element name
454
     * @param array $circular            The array of elements names called in the current branch of map
455
     *
456
     * @throws InvalidElementException   If the current element is defined dynamically and doesn't define a name
457
     *                                   If the current element define a parent, child or extends which creates circular
458
     *                                   declaration
459
     * @throws UndefinedElementException If the current element define a parent, child or extends which doesn't exist
460
     */
461
    private function validBranch($name, array $circular = array())
462
    {
463
        $getCurrent = true;
464
465
        if(is_array($name)){
466
            if(!isset($name['name'])){
467
                throw new InvalidElementException(sprintf(
468
                    'Elements defined dynamically in parent or children must define a name.'
469
                ));
470
            }
471
472
            $current = $name;
473
474
            $name = $current['name'];
475
            unset($current['name']);
476
477
            $getCurrent = false;
478
        }
479
480
        $circular[] = $name;
481
482
        if($getCurrent){
483
            $current = $this->getCurrentElement($name);
484
        }
485
486
        if(isset($current['class']) && !class_exists($current['class'])){
487
            throw new InvalidElementException(sprintf(
488
                'The element "%s" define a class which doesn\'t exist.',
489
                $name
490
            ));
491
        }
492
493
        foreach($this->checks as $check){
494
            if(isset($current[$check])){
495
                $currentCheck = (array) $current[$check];
496
497
                if(in_array($name, $currentCheck)){
498
                    throw new InvalidElementException(sprintf(
499
                        'Element "%s" cannot define himself as %s.',
500
                        $name,
501
                        $check
502
                    ));
503
                }
504
505
                foreach($currentCheck as $cc){
506
                    if(!is_array($cc) && !$this->exists($cc)){
507
                        throw new UndefinedElementException(sprintf(
508
                            'The element "%s" defines a %s "%s" wich doesn\'t exist.',
509
                            $name,
510
                            $check,
511
                            $cc
512
                        ));
513
                    }
514
515
                    if(!is_array($cc) && in_array($cc, $circular)){
516
                        $circular[] = $cc;
517
518
                        throw new InvalidElementException(sprintf(
519
                            'Element "%s" cannot define "%s" as %s. It\'s a circular reference. [%s]',
520
                            $name,
521
                            $cc,
522
                            $check,
523
                            implode('->', $circular)
524
                        ));
525
                    }
526
527
                    $this->validBranch($cc, $circular);
528
                }
529
            }
530
        }
531
    }
532
533
    /**
534
     * Get the current element representation.
535
     *
536
     * @param string $name The element name
537
     *
538
     * @return array
539
     */
540
    private function getCurrentElement($name)
541
    {
542
        return $this->map[lcfirst($name)];
543
    }
544
545
    /**
546
     * Extend element from another one.
547
     *
548
     * @param array $extend  The array of the element to extend
549
     * @param array $current The current element which extends
550
     *
551
     * @return array
552
     */
553
    private function extendElement($extend, $current)
554
    {
555
        $current['class'] = $extend['class'];
556
557
        $current['attr'] = $this->extendAttributes($extend['attr'], $current['attr']);
558
559
        return $current;
560
    }
561
562
    /**
563
     * Extend attributes from another element.
564
     *
565
     * @param array $from The array of attributes to extend
566
     * @param array $to   The array of attributes which extends
567
     *
568
     * @return array
569
     */
570
    private function extendAttributes(array $from, array $to)
571
    {
572
        foreach($from as $key => $value){
573
            if(in_array($key, $this->mergeableAttributes) && isset($to[$key])){
574
                $to[$key] = array_merge((array) $to[$key], (array) $value);
575
            } elseif(!isset($to[$key])){
576
                $to[$key] = $value;
577
            } elseif(is_array($value)){
578
                $to[$key] = $this->extendAttributes($value, $to[$key]);
579
            }
580
        }
581
582
        return $to;
583
    }
584
585
    /**
586
     * Replace the parameters of the element.
587
     *
588
     * @param array $element    The element with the parameters to replace
589
     * @param array $parameters The array of parameters values
590
     *
591
     * @return array
592
     */
593
    private function replaceParameters(array $element, array $parameters)
594
    {
595
        foreach($element as $key => $value){
596
            if(is_array($value)){
597
                $element[$key] = $this->replaceParameters($value, $parameters);
598
            }
599
600
            if(is_string($value)){
601
                foreach($parameters as $parameter => $replace){
602
                    $value = str_replace('%'.$parameter.'%', $replace, $value);
603
                }
604
605
                $element[$key] = $value;
606
            }
607
        }
608
609
        return $element;
610
    }
611
}
612