Completed
Push — master ( 10de85...76baec )
by Kacper
03:21
created

Xml/XmlElement.php (1 issue)

Labels
Severity

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
 * Nucleus - XMPP Library for PHP
4
 *
5
 * Copyright (C) 2016, Some rights reserved.
6
 *
7
 * @author Kacper "Kadet" Donat <[email protected]>
8
 *
9
 * Contact with author:
10
 * Xmpp: [email protected]
11
 * E-mail: [email protected]
12
 *
13
 * From Kadet with love.
14
 */
15
16
namespace Kadet\Xmpp\Xml;
17
18
use Interop\Container\ContainerInterface;
19
use Kadet\Xmpp\Exception\InvalidArgumentException;
20
use Kadet\Xmpp\Utils\Accessors;
21
use Kadet\Xmpp\Utils\filter;
22
use Kadet\Xmpp\Utils\helper;
23
24
/**
25
 * Class XmlElement
26
 * @package Kadet\Xmpp\Xml
27
 *
28
 * @property string          $localName  Tag name without prefix
29
 * @property string          $namespace  XML Namespace URI
30
 * @property string          $prefix     Tag prefix
31
 * @property string          $name       Full tag name prefix:local-name
32
 *
33
 * @property XmlElement|null $parent     Element's parent or null if root node.
34
 * @property XmlElement[]    parents     All element's parents in chronological order (from youngest to oldest)
35
 * @property XmlElement[]    $children   All element's child nodes
36
 *
37
 * @property array           $attributes Element's attributes, without xmlns definitions
38
 * @property array           $namespaces Element's namespaces
39
 *
40
 * @property string          $innerXml   Inner XML content
41
 */
42
class XmlElement implements ContainerInterface
43
{
44
    use Accessors;
45
46
    /**
47
     * Settings for tiding up XML output
48
     *
49
     * @var array
50
     */
51
    public static $tidy = [
52
        'indent'           => true,
53
        'input-xml'        => true,
54
        'output-xml'       => true,
55
        'drop-empty-paras' => false,
56
        'wrap'             => 0
57
    ];
58
59
    /** @var string */
60
    private $_localName;
61
    /** @var null|string|false */
62
    private $_prefix = null;
63
64
    /** @var array */
65
    private $_namespaces = [];
66
    /** @var array */
67
    private $_attributes = [];
68
69
    /**
70
     * @var XmlElement
71
     */
72
    private $_parent;
73
74
    /**
75
     * @var XmlElement[]
76
     */
77
    private $_children = [];
78
79
    /**
80
     * Initializes element with given name and URI
81
     *
82
     * @param string $name Element name, including prefix if needed
83
     * @param string $uri  Namespace URI of element
84
     */
85 23
    protected function init(string $name, string $uri = null)
86
    {
87 23
        list($name, $prefix) = self::resolve($name);
88
89 23
        $this->_localName = $name;
90 23
        $this->_prefix    = $prefix;
91
92 23
        if ($uri !== null) {
93 11
            $this->namespace = $uri;
94
        }
95 23
    }
96
97
    /**
98
     * XmlElement constructor
99
     *
100
     * @param string $name    Element name, including prefix if needed
101
     * @param string $uri     Namespace URI of element
102
     * @param array  $options {
103
     *     @var mixed  $content    Content of element
104
     *     @var array  $attributes Element attributes
105
     * }
106
     */
107 19
    public function __construct(string $name, string $uri = null, array $options = [])
108
    {
109 19
        $this->init($name, $uri);
110
111 19
        $this->applyOptions($options);
112 19
    }
113
114
    /**
115
     * Elements named constructor, same for every subclass.
116
     * It's used for factory creation.
117
     *
118
     * @param string $name Element name, including prefix if needed
119
     * @param string $uri  Namespace URI of element
120
     *
121
     * @return static
122
     */
123 4
    public static function plain(string $name, string $uri = null)
124
    {
125
        /** @var XmlElement $element */
126 4
        $element = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor();
127 4
        $element->init($name, $uri);
128
129 4
        return $element;
130
    }
131
132
    /**
133
     * @see $innerXml
134
     * @return string
135
     */
136 3
    public function getInnerXml()
137
    {
138
        return implode('', array_map(function ($element) {
139 3
            if (is_string($element)) {
140 2
                return htmlspecialchars($element);
141
            } elseif ($element instanceof XmlElement) {
142 1
                return $element->xml(false);
143
            }
144
145
            return (string)$element;
146 3
        }, $this->_children));
147
    }
148
149
    public function setInnerXml($value)
150
    {
151
        $this->_children = [];
152
153
        $this->append($value);
154
    }
155
156
    public function getContent()
157
    {
158
        return $this->children;
159
    }
160
161 1
    public function setContent($value)
162
    {
163 1
        $this->_children = [];
164 1
        $this->append($value);
165 1
    }
166
167
    /**
168
     * Returns XML representation of element
169
     *
170
     * @param bool $clean Result will be cleaned if set to true
171
     *
172
     * @return string
173
     */
174 3
    public function xml(bool $clean = true): string
175
    {
176 3
        if ($this->namespace && $this->_prefix === null) {
177 1
            $this->_prefix = $this->lookupPrefix($this->namespace);
178
        }
179
180 3
        $attributes = $this->attributes();
181
182 3
        $result = "<{$this->name}";
183
        $result .= ' ' . implode(' ', array_map(function ($key, $value) {
184 1
            return $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
185 3
        }, array_keys($attributes), array_values($attributes)));
186
187 3
        if (!empty($this->_children)) {
188 1
            $result .= ">{$this->innerXml}</{$this->name}>";
189
        } else {
190 3
            $result .= "/>";
191
        }
192
193 3
        return $clean && function_exists('tidy_repair_string') ? tidy_repair_string($result, self::$tidy) : $result;
194
    }
195
196
    /**
197
     * Looks up prefix associated with given URI
198
     *
199
     * @param string|null $uri
200
     * @return string|false
201
     */
202 12
    public function lookupPrefix(string $uri = null)
203
    {
204 12
        return $this->getNamespaces()[ $uri ] ?? false;
205
    }
206
207
    /**
208
     * Looks up URI associated with given prefix
209
     *
210
     * @param string|null $prefix
211
     * @return string|false
212
     */
213 18
    public function lookupUri(string $prefix = null)
214
    {
215 18
        return array_search($prefix, $this->getNamespaces()) ?: false;
216
    }
217
218
    /**
219
     * Returns element's namespaces
220
     *
221
     * @param bool $parent Include namespaces from parent?
222
     * @return array
223
     */
224 20
    public function getNamespaces($parent = true): array
225
    {
226 20
        if (!$this->_parent) {
227 20
            return $this->_namespaces;
228
        }
229
230 11
        if ($parent) {
231 11
            return array_merge($this->_namespaces, $this->_parent->getNamespaces());
232
        } else {
233 1
            return array_diff_assoc($this->_namespaces, $this->_parent->getNamespaces());
234
        }
235
    }
236
237
    /**
238
     * Sets XML attribute of element
239
     *
240
     * @param string      $attribute Attribute name, optionally with prefix
241
     * @param mixed       $value     Attribute value
242
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
243
     */
244 5
    public function setAttribute(string $attribute, $value, string $uri = null)
245
    {
246 5
        $attribute = $this->_prefix($attribute, $uri);
247 4
        if ($value === null) {
248 1
            unset($this->_attributes[ $attribute ]);
249
250 1
            return;
251
        }
252
253 4
        $this->_attributes[ $attribute ] = $value;
254 4
    }
255
256
    /**
257
     * Returns value of specified attribute.
258
     *
259
     * @param string      $attribute Attribute name, optionally with prefix
260
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
261
     * @return bool|mixed
262
     */
263 3
    public function getAttribute(string $attribute, string $uri = null)
264
    {
265 3
        return $this->_attributes[ $this->_prefix($attribute, $uri) ] ?? false;
266
    }
267
268
    /**
269
     * Checks if attribute exists
270
     *
271
     * @param string      $attribute Attribute name, optionally with prefix
272
     * @param string|null $uri       XML Namespace URI of attribute, prefix will be automatically looked up
273
     *
274
     * @return bool
275
     */
276 3
    public function hasAttribute(string $attribute, string $uri = null)
277
    {
278 3
        return isset($this->_attributes[ $this->_prefix($attribute, $uri) ]);
279
    }
280
281
    /**
282
     * Returns all element's parents in order, oldest ancestor is the last element in returned array.
283
     * @return XmlElement[]
284
     */
285
    public function getParents()
286
    {
287
        return $this->_parent ? array_merge([ $this->_parent ], $this->_parent->getParents()) : [];
288
    }
289
290
    /**
291
     * Returns element's parent
292
     * @return XmlElement|null
293
     */
294 2
    public function getParent()
295
    {
296 2
        return $this->_parent;
297
    }
298
299
    /**
300
     * Sets element's parent
301
     * @param XmlElement $parent
302
     */
303 11
    protected function setParent(XmlElement $parent)
304
    {
305 11
        if (!$this->_prefix && ($prefix = $parent->lookupPrefix($this->namespace)) !== false) {
306 1
            $this->_namespaces[ $this->namespace ] = $prefix;
307 1
            $this->_prefix                         = $prefix;
308
        }
309
310 11
        $this->_parent = $parent;
311 11
        if ($this->namespace === false) {
312 6
            $this->namespace = $parent->namespace;
313
        }
314 11
    }
315
316
    /**
317
     * Appends child to element
318
     *
319
     * @param XmlElement|string $element
320
     *
321
     * @return XmlElement|string Same as $element
322
     */
323 15
    public function append($element)
324
    {
325 15
        if (empty($element)) {
326 1
            return false;
327
        }
328
329 14
        if(is_array($element)) {
330 1
            array_walk($element, [$this, 'appendChild']);
331 1
            return $element;
332
        }
333
334 13
        return $this->appendChild($element);
335
    }
336
337 14
    protected function appendChild($element) {
338 14
        if (!is_string($element) && !$element instanceof XmlElement) {
339 1
            throw new InvalidArgumentException(helper\format(
340 1
                '$element should be either string or object of {class} class. or array of given types, {type} given', [
341 1
                    'class' => XmlElement::class,
342 1
                    'type'  => helper\typeof($element)
343
                ]
344
            ));
345
        }
346
347 13
        if ($element instanceof XmlElement) {
348 11
            $element->parent = $this;
349
        }
350
351 13
        return $this->_children[] = $element;
352
    }
353
354
    /**
355
     * Returns namespace URI associated with element or specified prefix
356
     *
357
     * @param string|bool|null $prefix
358
     * @return false|string
359
     */
360 18
    public function getNamespace($prefix = false)
361
    {
362 18
        if ($prefix === false) {
363 18
            $prefix = $this->prefix;
364
        }
365
366 18
        return $this->lookupUri($prefix);
0 ignored issues
show
It seems like $prefix defined by parameter $prefix on line 360 can also be of type boolean; however, Kadet\Xmpp\Xml\XmlElement::lookupUri() does only seem to accept null|string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
367
    }
368
369
    /**
370
     * Adds namespace to element, and associates it with prefix.
371
     *
372
     * @param string           $uri    Namespace URI
373
     * @param string|bool|null $prefix Prefix which will be used for namespace, false for using element's prefix
374
     *                                 and null for no prefix
375
     */
376 16
    public function setNamespace(string $uri, $prefix = false)
377
    {
378 16
        if ($prefix === false) {
379 14
            $prefix = $this->_prefix;
380
        }
381
382 16
        $this->_namespaces[ $uri ] = $prefix;
383 16
    }
384
385 7
    public function getName()
386
    {
387 7
        return ($this->_prefix ? $this->prefix . ':' : null) . $this->localName;
388
    }
389
390 7
    public function getChildren()
391
    {
392 7
        return $this->_children;
393
    }
394
395 18
    public function getPrefix()
396
    {
397 18
        return $this->_prefix;
398
    }
399
400 12
    public function getLocalName()
401
    {
402 12
        return $this->_localName;
403
    }
404
405 7
    public function getAttributes()
406
    {
407 7
        return $this->_attributes;
408
    }
409
410 1
    protected function setAttributes(array $attributes)
411
    {
412 1
        $this->_attributes = [];
413
414 1
        foreach ($attributes as $attribute => $value) {
415 1
            $this->setAttribute($attribute, $value);
416
        }
417 1
    }
418
419
    /**
420
     * Returns one element at specified index (for default the first one).
421
     *
422
     * @param string $name  Requested element tag name
423
     * @param string $uri   Requested element namespace
424
     * @param int    $index Index of element to retrieve
425
     *
426
     * @return XmlElement|false Retrieved element
427
     */
428 1
    public function element(string $name, string $uri = null, int $index = 0)
429
    {
430 1
        return array_values($this->elements($name, $uri))[ $index ] ?? false;
431
    }
432
433
    /**
434
     * Retrieves array of matching elements
435
     *
436
     * @param string      $name Requested element tag name
437
     * @param string|null $uri  Requested element namespace
438
     *
439
     * @return XmlElement[] Found Elements
440
     */
441 2
    public function elements($name, $uri = null) : array
442
    {
443 2
        $predicate = filter\element\name($name);
444 2
        if ($uri !== null) {
445 1
            $predicate = filter\all($predicate, filter\element\xmlns($uri));
446
        }
447
448 2
        return $this->all($predicate);
449
    }
450
451
    /**
452
     * Filters element with given predicate
453
     *
454
     * @param callable|string $predicate Predicate or class name
455
     *
456
     * @return XmlElement[]
457
     */
458 2
    public function all($predicate)
459
    {
460 2
        return array_values(array_filter($this->_children, filter\predicate($predicate)));
461
    }
462
463
    /**
464
     * Iterates over matching elements
465
     *
466
     * @param callable|string $predicate Predicate or class name
467
     *
468
     * @return XmlElement|false
469
     */
470 2
    public function get($predicate)
471
    {
472 2
        $predicate = filter\predicate($predicate);
473 2
        foreach ($this->_children as $index => $child) {
474 2
            if ($predicate($child)) {
475 2
                return $child;
476
            }
477
        }
478
479 2
        return false;
480
    }
481
482
    /**
483
     * Checks if any element matching predicate exists
484
     *
485
     * @param callable|string $predicate Predicate or class name
486
     *
487
     * @return bool
488
     */
489 1
    public function has($predicate)
490
    {
491 1
        return $this->get($predicate) !== false;
492
    }
493
494
    /**
495
     * @param string|null $query
496
     * @return XPathQuery
497
     */
498 1
    public function query(string $query = null)
499
    {
500 1
        return new XPathQuery($query, $this);
501
    }
502
503
    /**
504
     * Helper for retrieving all arguments (including namespaces)
505
     *
506
     * @return array
507
     */
508 3
    private function attributes(): array
509
    {
510 3
        $namespaces = $this->getNamespaces(false);
511 3
        $namespaces = array_map(function ($prefix, $uri) {
512 1
            return [$prefix ? "xmlns:{$prefix}" : 'xmlns', $uri];
513 3
        }, array_values($namespaces), array_keys($namespaces));
514
515 3
        return array_merge(
516 3
            $this->_attributes,
517 3
            array_combine(array_column($namespaces, 0), array_column($namespaces, 1))
518
        );
519
    }
520
521
    /**
522
     * Prefixes $name with attribute associated with $uri
523
     *
524
     * @param string $name Name to prefix
525
     * @param string $uri  Namespace URI
526
     *
527
     * @return string
528
     */
529 5
    protected function _prefix(string $name, string $uri = null): string
530
    {
531 5
        if ($uri === null) {
532 4
            return $name;
533
        }
534
535 3
        if (($prefix = $this->lookupPrefix($uri)) === false) {
536 1
            throw new InvalidArgumentException(helper\format('URI "{uri}" is not a registered namespace', ['uri' => $uri]));
537
        }
538
539 2
        return "{$prefix}:{$name}";
540
    }
541
542 2
    public function __toString()
543
    {
544 2
        return trim($this->xml(true));
545
    }
546
547
    /**
548
     * Splits name into local-name and prefix
549
     *
550
     * @param $name
551
     * @return array [$name, $prefix]
552
     */
553 23
    public static function resolve($name)
554
    {
555 23
        $prefix = null;
556 23
        if (($pos = strpos($name, ':')) !== false) {
557 2
            $prefix = substr($name, 0, $pos);
558 2
            $name   = substr($name, $pos + 1);
559
        }
560
561 23
        return [$name, $prefix];
562
    }
563
}
564