Completed
Push — master ( ba0a70...3c4315 )
by Nathan
02:21
created

HtmlElement::isEscapeCss()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace NatePage\EasyHtmlElement;
4
5
use NatePage\EasyHtmlElement\Exception\InvalidArgumentsNumberException;
6
use NatePage\EasyHtmlElement\Exception\InvalidElementException;
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
     * Create element on static calls.
70
     *
71
     * @param string $type      The element type
72
     * @param array  $arguments The arguments array to set:
73
     *                          [0] = text (string)
74
     *                          [1] = attributes (array)
75
     *                          [2] = children (array)
76
     *
77
     * @return ElementInterface
78
     *
79
     * @throws InvalidArgumentsNumberException If the arguments length is more than 3
80
     */
81
    public static function __callStatic($type, $arguments)
82
    {
83
        switch (count($arguments)) {
84
            case 0:
85
                return self::create($type);
86
            case 1:
87
                return self::create($type, $arguments[0]);
88
            case 2:
89
                return self::create($type, $arguments[0], $arguments[1]);
90
            case 3:
91
                return self::create($type, $arguments[0], $arguments[1], $arguments[2]);
92
            default:
93
                throw new InvalidArgumentsNumberException(sprintf(
94
                    'Maximum numbers of arguments is %d, [%d] given.',
95
                    3,
96
                    count($arguments)
97
                ));
98
        }
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public static function create($type = null, $text = null, array $attributes = array(), array $children = array())
105
    {
106
        $htmlElement = new HtmlElement();
107
108
        $attributes = $htmlElement->escapeAttributes($attributes);
109
110
        foreach ($children as $key => $child) {
111
            $children[$key] = $htmlElement->escape($child);
112
        }
113
114
        return $htmlElement->escape(new Element($type, $text, $attributes, $children));
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120
    public function escapeAttributes(array $attributes)
121
    {
122
        if ($this->escapeHtmlAttr || $this->escapeUrl) {
123
            foreach ($attributes as $attr => $value) {
124
                if ('href' == $attr) {
125
                    if ($this->escapeUrl) {
126
                        $value = $this->escaper->escapeUrl($value);
127
                    }
128
                } else {
129
                    if ($this->escapeHtmlAttr) {
130
                        $value = $this->escaper->escapeHtmlAttr($value);
131
                    }
132
                }
133
134
                $attributes[$attr] = $value;
135
            }
136
        }
137
138
        return $attributes;
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function escape(ElementInterface $element)
145
    {
146
        if ($this->escapeHtml && !in_array($element->getType(), $this->specialEscapingTypes)) {
147
            $element->setText($this->escaper->escapeHtml($element->getText()));
148
        }
149
150
        $element->setAttributes($this->escapeAttributes($element->getAttributes()));
151
152
        if ($this->escapeJs && 'script' == $element->getType()) {
153
            $element->setText($this->escaper->escapeJs($element->getText()));
154
        }
155
156
        if ($this->escapeCss && 'style' == $element->getType()) {
157
            $element->setText($this->escaper->escapeCss($element->getText()));
158
        }
159
160
        return $element;
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166
    public function getMap()
167
    {
168
        return $this->map;
169
    }
170
171
    /**
172
     * {@inheritdoc}
173
     */
174
    public function setMap(array $map)
175
    {
176
        $this->map = $map;
177
178
        return $this;
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184
    public function addManyToMap(array $elements)
185
    {
186
        foreach ($elements as $name => $element) {
187
            $this->addOneToMap($name, $element);
188
        }
189
190
        return $this;
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    public function addOneToMap($name, array $element)
197
    {
198
        $this->map[$name] = $element;
199
200
        return $this;
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    public function getEscaper()
207
    {
208
        return $this->escaper;
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214
    public function setEscaper(EscaperInterface $escaper)
215
    {
216
        $this->escaper = $escaper;
217
218
        return $this;
219
    }
220
221
    /**
222
     * {@inheritdoc}
223
     */
224
    public function isEscapeHtml()
225
    {
226
        return $this->escapeHtml;
227
    }
228
229
    /**
230
     * {@inheritdoc}
231
     */
232
    public function setEscapeHtml($escapeHtml = true)
233
    {
234
        $this->escapeHtml = $escapeHtml;
235
236
        return $this;
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242
    public function isEscapeHtmlAttr()
243
    {
244
        return $this->escapeHtmlAttr;
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250
    public function setEscapeHtmlAttr($escapeHtmlAttr = true)
251
    {
252
        $this->escapeHtmlAttr = $escapeHtmlAttr;
253
254
        return $this;
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260
    public function isEscapeJs()
261
    {
262
        return $this->escapeJs;
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268
    public function setEscapeJs($escapeJs = true)
269
    {
270
        $this->escapeJs = $escapeJs;
271
272
        return $this;
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278
    public function isEscapeCss()
279
    {
280
        return $this->escapeCss;
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286
    public function setEscapeCss($escapeCss = true)
287
    {
288
        $this->escapeCss = $escapeCss;
289
290
        return $this;
291
    }
292
293
    /**
294
     * {@inheritdoc}
295
     */
296
    public function isEscapeUrl()
297
    {
298
        return $this->escapeUrl;
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    public function setEscapeUrl($escapeUrl = true)
305
    {
306
        $this->escapeUrl = $escapeUrl;
307
308
        return $this;
309
    }
310
311
    /**
312
     * {@inheritdoc}
313
     */
314
    public function load($name, array $parameters = array(), array $attributes = array(), array $children = array())
315
    {
316
        $element = $this->getInstance($name, $parameters, true);
317
318
        $element->addAttributes($this->escapeAttributes($attributes));
319
320
        foreach ($children as $child) {
321
            $element->addChild($this->escape($child));
322
        }
323
324
        return $element;
325
    }
326
327
    /**
328
     * Get the element instance.
329
     *
330
     * @param string $name       The element name
331
     * @param array  $parameters The parameters to replace in element
332
     * @param bool   $mainCall   Determine if it's the main(first) call of the method
333
     *
334
     * @return ElementInterface
335
     *
336
     * @throws InvalidElementException If the current instance doesn't implement ElementInterface
337
     */
338
    private function getInstance($name, array $parameters, $mainCall = false)
339
    {
340
        $element = $this->resolveElement($name, $parameters, $mainCall);
341
342
        $class = $element['class'];
343
        $type = $element['type'];
344
        $text = $element['text'];
345
        $attributes = $element['attr'];
346
347
        $instance = new $class($type, $text, $attributes);
348
349
        if (!$instance instanceof ElementInterface) {
350
            throw new InvalidElementException(sprintf(
351
                'The element "%s" does not implement the %s',
352
                get_class($instance),
353
                ElementInterface::class
354
            ));
355
        }
356
357
        $children = array();
358
        foreach ((array) $element['children'] as $child) {
359
            $children[] = $this->getInstance($child, $parameters);
360
        }
361
362
        $instance->setChildren($children);
363
364
        if (null !== $element['parent']) {
365
            $parent = $this->getInstance($element['parent'], $parameters);
366
367
            $parent->addChild($instance);
368
        }
369
370
        return $this->escape($instance);
371
    }
372
373
    /**
374
     * Get the resolved element representation.
375
     *
376
     * @param string $name       The current element name
377
     * @param array  $parameters The parameters to replace in element
378
     * @param bool   $mainCall   Determine if it's the main(first) call of the method
379
     *
380
     * @return array
381
     */
382
    private function resolveElement($name, array $parameters, $mainCall = false)
383
    {
384
        $current = $this->getCurrentElement($name);
385
386
        $name = $current['name'];
387
388
        if ($this->alreadyResolved($name)) {
389
            return $this->resolved[$name];
390
        }
391
392
        if ($mainCall) {
393
            $this->validBranch($name);
394
        }
395
396
        foreach ($this->defaults as $default => $value) {
397
            if (!isset($current[$default])) {
398
                $current[$default] = $value;
399
            }
400
        }
401
402
        $current = $this->replaceParameters($current, $parameters);
403
404
        foreach ((array) $current['extends'] as $extend) {
405
            $extend = $this->resolveElement($extend, $parameters);
406
            $current = $this->extendElement($extend, $current);
407
        }
408
409
        $this->resolved[$name] = $current;
410
411
        return $current;
412
    }
413
414
    /**
415
     * Check if an element has been already resolved.
416
     *
417
     * @param string $name
418
     *
419
     * @return bool
420
     */
421
    private function alreadyResolved($name)
422
    {
423
        return array_key_exists($name, $this->resolved);
424
    }
425
426
    /**
427
     * {@inheritdoc}
428
     */
429
    public function exists($name)
430
    {
431
        return array_key_exists(lcfirst($name), $this->map);
432
    }
433
434
    /**
435
     * Valid the current map branch.
436
     *
437
     * @param string $name     The current element name
438
     * @param array  $circular The array of elements names called in the current branch of map
439
     *
440
     * @throws InvalidElementException   If the current element define a parent, child or extends which creates circular
441
     *                                   declaration
442
     * @throws UndefinedElementException If the current element define a parent, child or extends which doesn't exist
443
     */
444
    private function validBranch($name, array $circular = array())
445
    {
446
        $current = $this->getCurrentElement($name);
447
448
        $name = $current['name'];
449
        $circular[] = $name;
450
451
        if (isset($current['class']) && !class_exists($current['class'])) {
452
            throw new InvalidElementException(sprintf('The element "%s" define a class which doesn\'t exist.', $name));
453
        }
454
455
        foreach ($this->checks as $check) {
456
            if (isset($current[$check])) {
457
                $currentCheck = (array) $current[$check];
458
459
                if (in_array($name, $currentCheck)) {
460
                    throw new InvalidElementException(sprintf(
461
                        'Element "%s" cannot define himself as %s.',
462
                        $name,
463
                        $check
464
                    ));
465
                }
466
467
                foreach ($currentCheck as $cc) {
468
                    if (!is_array($cc) && in_array($cc, $circular)) {
469
                        $circular[] = $cc;
470
471
                        throw new InvalidElementException(sprintf(
472
                            'Element "%s" cannot define "%s" as %s. It\'s a circular reference. [%s]',
473
                            $name,
474
                            $cc,
475
                            $check,
476
                            implode(' -> ', $circular)
477
                        ));
478
                    }
479
480
                    $this->validBranch($cc, $circular);
481
                }
482
            }
483
        }
484
    }
485
486
    /**
487
     * Get the current element representation.
488
     *
489
     * @param string $name The element name
490
     *
491
     * @return array
492
     *
493
     * @throws InvalidElementException   If the current element is defined dynamically and doesn't define a name
494
     * @throws UndefinedElementException If the current element doesn't exist
495
     */
496
    private function getCurrentElement($name)
497
    {
498
        if (is_array($name)) {
499
            if (!isset($name['name'])) {
500
                throw new InvalidElementException(sprintf(
501
                    'Elements defined dynamically in parent or children must define a name.'
502
                ));
503
            }
504
505
            return $name;
506
        }
507
508
        if (!$this->exists($name)) {
509
            throw new UndefinedElementException(sprintf('The element with name "%s" does not exist.', $name));
510
        }
511
512
        $current = $this->map[lcfirst($name)];
513
        $current['name'] = $name;
514
515
        return $current;
516
    }
517
518
    /**
519
     * Replace the parameters of the element.
520
     *
521
     * @param array $element    The element with the parameters to replace
522
     * @param array $parameters The array of parameters values
523
     *
524
     * @return array
525
     */
526
    private function replaceParameters(array $element, array $parameters)
527
    {
528
        foreach ($element as $key => $value) {
529
            if (is_array($value)) {
530
                $element[$key] = $this->replaceParameters($value, $parameters);
531
            }
532
533
            if (is_string($value)) {
534
                foreach ($parameters as $parameter => $replace) {
535
                    $value = str_replace('%'.$parameter.'%', $replace, $value);
536
                }
537
538
                $element[$key] = $value;
539
            }
540
        }
541
542
        return $element;
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