1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* XMPP Library |
4
|
|
|
* |
5
|
|
|
* Copyright (C) 2016, Some right 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 Kadet\Xmpp\Exception\InvalidArgumentException; |
19
|
|
|
use Kadet\Xmpp\Utils\Accessors; |
20
|
|
|
use Kadet\Xmpp\Utils\filter; |
21
|
|
|
use Kadet\Xmpp\Utils\helper; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Class XmlElement |
25
|
|
|
* @package Kadet\Xmpp\Xml |
26
|
|
|
* |
27
|
|
|
* @property string $localName Tag name without prefix |
28
|
|
|
* @property string $namespace XML Namespace URI |
29
|
|
|
* @property string $prefix Tag prefix |
30
|
|
|
* @property string $name Full tag name prefix:local-name |
31
|
|
|
* |
32
|
|
|
* @property XmlElement|null $parent Element's parent or null if root node. |
33
|
|
|
* @property XmlElement[] $children All element's child nodes |
34
|
|
|
* |
35
|
|
|
* @property array $attributes Element's attributes, without xmlns definitions |
36
|
|
|
* @property array $namespaces Element's namespaces |
37
|
|
|
* |
38
|
|
|
* @property string $innerXml Inner XML content |
39
|
|
|
*/ |
40
|
|
|
class XmlElement |
41
|
|
|
{ |
42
|
|
|
use Accessors; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Settings for tiding up XML output |
46
|
|
|
* |
47
|
|
|
* @var array |
48
|
|
|
*/ |
49
|
|
|
public static $tidy = [ |
50
|
|
|
'indent' => true, |
51
|
|
|
'input-xml' => true, |
52
|
|
|
'output-xml' => true, |
53
|
|
|
'drop-empty-paras' => false, |
54
|
|
|
'wrap' => 0 |
55
|
|
|
]; |
56
|
|
|
|
57
|
|
|
/** @var string */ |
58
|
|
|
private $_localName; |
59
|
|
|
/** @var null|string */ |
60
|
|
|
private $_prefix = null; |
61
|
|
|
|
62
|
|
|
/** @var array */ |
63
|
|
|
private $_namespaces = []; |
64
|
|
|
/** @var array */ |
65
|
|
|
private $_attributes = []; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* @var XmlElement |
69
|
|
|
*/ |
70
|
|
|
private $_parent; |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* @var XmlElement[] |
74
|
|
|
*/ |
75
|
|
|
private $_children = []; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Initializes element with given name and URI |
79
|
|
|
* |
80
|
|
|
* @param string $name Element name, including prefix if needed |
81
|
|
|
* @param string $uri Namespace URI of element |
82
|
|
|
*/ |
83
|
19 |
|
protected function init(string $name, string $uri = null) |
84
|
|
|
{ |
85
|
19 |
|
list($name, $prefix) = self::resolve($name); |
86
|
|
|
|
87
|
19 |
|
$this->_localName = $name; |
88
|
19 |
|
$this->_prefix = $prefix; |
89
|
|
|
|
90
|
19 |
|
if ($uri !== null) { |
91
|
8 |
|
$this->namespace = $uri; |
92
|
|
|
} |
93
|
19 |
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* XmlElement constructor |
97
|
|
|
* |
98
|
|
|
* @param string $name Element name, including prefix if needed |
99
|
|
|
* @param string $uri Namespace URI of element |
100
|
|
|
*/ |
101
|
15 |
|
public function __construct(string $name, string $uri = null) |
102
|
|
|
{ |
103
|
15 |
|
$this->init($name, $uri); |
104
|
15 |
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Elements named constructor, same for every subclass. |
108
|
|
|
* It's used for factory creation. |
109
|
|
|
* |
110
|
|
|
* @param string $name Element name, including prefix if needed |
111
|
|
|
* @param string $uri Namespace URI of element |
112
|
|
|
* |
113
|
|
|
* @return XmlElement |
114
|
|
|
*/ |
115
|
4 |
|
public static function plain(string $name, string $uri = null) |
116
|
|
|
{ |
117
|
|
|
/** @var XmlElement $element */ |
118
|
4 |
|
$element = (new \ReflectionClass(static::class))->newInstanceWithoutConstructor(); |
119
|
4 |
|
$element->init($name, $uri); |
120
|
|
|
|
121
|
4 |
|
return $element; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* @see $innerXml |
126
|
|
|
* @return string |
127
|
|
|
*/ |
128
|
2 |
|
public function getInnerXml() |
129
|
|
|
{ |
130
|
|
|
return implode('', array_map(function ($element) { |
131
|
2 |
|
if (is_string($element)) { |
132
|
1 |
|
return htmlspecialchars($element); |
133
|
|
|
} elseif ($element instanceof XmlElement) { |
134
|
1 |
|
return $element->xml(false); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
return (string)$element; |
138
|
2 |
|
}, $this->_children)); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Returns XML representation of element |
143
|
|
|
* |
144
|
|
|
* @param bool $clean Result will be cleaned if set to true |
145
|
|
|
* |
146
|
|
|
* @return string |
147
|
|
|
*/ |
148
|
2 |
|
public function xml(bool $clean = true): string |
149
|
|
|
{ |
150
|
2 |
|
if ($this->namespace && $this->_prefix === null) { |
151
|
1 |
|
$this->_prefix = $this->lookupPrefix($this->namespace); |
|
|
|
|
152
|
|
|
} |
153
|
|
|
|
154
|
2 |
|
$attributes = $this->attributes(); |
155
|
|
|
|
156
|
2 |
|
$result = "<{$this->name}"; |
157
|
|
|
$result .= ' ' . implode(' ', array_map(function ($key, $value) { |
158
|
1 |
|
return $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"'; |
159
|
2 |
|
}, array_keys($attributes), array_values($attributes))); |
160
|
|
|
|
161
|
2 |
|
if (!empty($this->_children)) { |
162
|
1 |
|
$result .= ">{$this->innerXml}</{$this->name}>"; |
163
|
|
|
} else { |
164
|
2 |
|
$result .= "/>"; |
165
|
|
|
} |
166
|
|
|
|
167
|
2 |
|
return $clean && function_exists('tidy_repair_string') ? tidy_repair_string($result, self::$tidy) : $result; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Looks up prefix associated with given URI |
172
|
|
|
* |
173
|
|
|
* @param string|null $uri |
174
|
|
|
* @return string|false |
175
|
|
|
*/ |
176
|
9 |
|
public function lookupPrefix(string $uri = null) |
177
|
|
|
{ |
178
|
9 |
|
return $this->getNamespaces()[$uri] ?? false; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Looks up URI associated with given prefix |
183
|
|
|
* |
184
|
|
|
* @param string|null $prefix |
185
|
|
|
* @return string|false |
186
|
|
|
*/ |
187
|
13 |
|
public function lookupUri(string $prefix = null) |
188
|
|
|
{ |
189
|
13 |
|
return array_search($prefix, $this->getNamespaces()) ?: false; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Returns element's namespaces |
194
|
|
|
* |
195
|
|
|
* @param bool $parent Include namespaces from parent? |
196
|
|
|
* @return array |
197
|
|
|
*/ |
198
|
16 |
|
public function getNamespaces($parent = true): array |
199
|
|
|
{ |
200
|
16 |
|
if (!$this->_parent) { |
201
|
16 |
|
return $this->_namespaces; |
202
|
|
|
} |
203
|
|
|
|
204
|
8 |
|
if ($parent) { |
205
|
8 |
|
return array_merge($this->_namespaces, $this->_parent->getNamespaces()); |
206
|
|
|
} else { |
207
|
1 |
|
return array_diff_assoc($this->_namespaces, $this->_parent->getNamespaces()); |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* Sets XML attribute of element |
213
|
|
|
* |
214
|
|
|
* For `http://www.w3.org/2000/xmlns/` URI it acts like `setNamespace($value, $attribute)` |
215
|
|
|
* |
216
|
|
|
* @param string $attribute Attribute name, optionally with prefix |
217
|
|
|
* @param mixed $value Attribute value |
218
|
|
|
* @param string|null $uri XML Namespace URI of attribute, prefix will be automatically looked up |
219
|
|
|
*/ |
220
|
5 |
View Code Duplication |
public function setAttribute(string $attribute, $value, string $uri = null) |
|
|
|
|
221
|
|
|
{ |
222
|
5 |
|
if ($uri === 'http://www.w3.org/2000/xmlns/') { |
223
|
1 |
|
$this->setNamespace($value, $attribute); |
224
|
1 |
|
return; |
225
|
|
|
} |
226
|
|
|
|
227
|
4 |
|
if ($uri !== null) { |
228
|
3 |
|
$attribute = $this->_prefix($attribute, $uri); |
229
|
|
|
} |
230
|
|
|
|
231
|
3 |
|
$this->_attributes[$attribute] = $value; |
232
|
3 |
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Returns value of specified attribute. |
236
|
|
|
* |
237
|
|
|
* For `http://www.w3.org/2000/xmlns/` URI it acts like `lookupUri($attribute)` |
238
|
|
|
* |
239
|
|
|
* @param string $attribute |
240
|
|
|
* @param string|null $uri |
241
|
|
|
* @return bool|mixed |
242
|
|
|
*/ |
243
|
2 |
View Code Duplication |
public function getAttribute(string $attribute, string $uri = null) |
|
|
|
|
244
|
|
|
{ |
245
|
2 |
|
if ($uri === 'http://www.w3.org/2000/xmlns/') { |
246
|
|
|
return $this->lookupUri($attribute); |
247
|
|
|
} |
248
|
|
|
|
249
|
2 |
|
if ($uri !== null) { |
250
|
1 |
|
$attribute = $this->_prefix($attribute, $uri); |
251
|
|
|
} |
252
|
|
|
|
253
|
2 |
|
return $this->_attributes[$attribute] ?? false; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* Returns element's parent |
258
|
|
|
* @return XmlElement|null |
259
|
|
|
*/ |
260
|
1 |
|
public function getParent() |
261
|
|
|
{ |
262
|
1 |
|
return $this->_parent; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Sets element's parent |
267
|
|
|
* @param XmlElement $parent |
268
|
|
|
*/ |
269
|
8 |
|
protected function setParent(XmlElement $parent) |
270
|
|
|
{ |
271
|
8 |
|
if (!$this->_prefix && ($prefix = $parent->lookupPrefix($this->namespace)) !== false) { |
|
|
|
|
272
|
1 |
|
$this->_namespaces[$this->namespace] = $prefix; |
273
|
1 |
|
$this->_prefix = $prefix; |
274
|
|
|
} |
275
|
|
|
|
276
|
8 |
|
$this->_parent = $parent; |
277
|
8 |
|
if ($this->namespace === false) { |
278
|
3 |
|
$this->namespace = $parent->namespace; |
279
|
|
|
} |
280
|
8 |
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* Appends child to element |
284
|
|
|
* |
285
|
|
|
* @param XmlElement|string $element |
286
|
|
|
* |
287
|
|
|
* @return XmlElement|string Same as $element |
288
|
|
|
*/ |
289
|
10 |
|
public function append($element) |
290
|
|
|
{ |
291
|
10 |
|
if (!is_string($element) && !$element instanceof XmlElement) { |
292
|
1 |
|
throw new InvalidArgumentException(helper\format('$element should be either string or object of {class} class, {type} given', [ |
293
|
1 |
|
'class' => XmlElement::class, |
294
|
1 |
|
'type' => helper\typeof($element) |
295
|
|
|
])); |
296
|
|
|
} |
297
|
|
|
|
298
|
9 |
|
if ($element instanceof XmlElement) { |
299
|
8 |
|
$element->parent = $this; |
300
|
|
|
} |
301
|
|
|
|
302
|
9 |
|
return $this->_children[] = $element; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* Returns namespace URI associated with element |
307
|
|
|
* |
308
|
|
|
* @return false|string |
309
|
|
|
*/ |
310
|
13 |
|
public function getNamespace() |
311
|
|
|
{ |
312
|
13 |
|
return $this->lookupUri($this->prefix); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Adds namespace to element, and associates it with prefix. |
317
|
|
|
* |
318
|
|
|
* @param string $uri Namespace URI |
319
|
|
|
* @param string|bool|null $prefix Prefix which will be used for namespace, false for using element's prefix |
320
|
|
|
* and null for no prefix |
321
|
|
|
*/ |
322
|
13 |
|
public function setNamespace(string $uri, $prefix = false) |
323
|
|
|
{ |
324
|
13 |
|
if ($prefix === false) { |
325
|
10 |
|
$prefix = $this->_prefix; |
326
|
|
|
} |
327
|
|
|
|
328
|
13 |
|
$this->_namespaces[$uri] = $prefix; |
329
|
13 |
|
} |
330
|
|
|
|
331
|
6 |
|
public function getName() |
332
|
|
|
{ |
333
|
6 |
|
return ($this->_prefix ? $this->prefix . ':' : null) . $this->localName; |
334
|
|
|
} |
335
|
|
|
|
336
|
5 |
|
public function getChildren() |
337
|
|
|
{ |
338
|
5 |
|
return $this->_children; |
339
|
|
|
} |
340
|
|
|
|
341
|
13 |
|
public function getPrefix() |
342
|
|
|
{ |
343
|
13 |
|
return $this->_prefix; |
344
|
|
|
} |
345
|
|
|
|
346
|
8 |
|
public function getLocalName() |
347
|
|
|
{ |
348
|
8 |
|
return $this->_localName; |
349
|
|
|
} |
350
|
|
|
|
351
|
6 |
|
public function getAttributes() |
352
|
|
|
{ |
353
|
6 |
|
return $this->_attributes; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Returns one element at specified index (for default the first one). |
358
|
|
|
* |
359
|
|
|
* @param string $name Requested element tag name |
360
|
|
|
* @param string $uri Requested element namespace |
361
|
|
|
* @param int $index Index of element to retrieve |
362
|
|
|
* |
363
|
|
|
* @return XmlElement|false Retrieved element |
364
|
|
|
*/ |
365
|
1 |
|
public function element(string $name, string $uri = null, int $index = 0) |
366
|
|
|
{ |
367
|
1 |
|
return array_values($this->elements($name, $uri))[$index] ?? false; |
|
|
|
|
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* Retrieves array of matching elements |
372
|
|
|
* |
373
|
|
|
* @param string $name Requested element tag name |
374
|
|
|
* @param null $uri Requested element namespace |
375
|
|
|
* |
376
|
|
|
* @return XmlElement[] Found Elements |
377
|
|
|
*/ |
378
|
2 |
|
public function elements($name, $uri = null) : array |
379
|
|
|
{ |
380
|
2 |
|
$predicate = filter\tag($name); |
381
|
2 |
|
if ($uri !== null) { |
382
|
1 |
|
$predicate = filter\all($predicate, filter\xmlns($uri)); |
383
|
|
|
} |
384
|
|
|
|
385
|
2 |
|
return $this->all($predicate); |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
/** |
389
|
|
|
* Filters element with given predicate |
390
|
|
|
* |
391
|
|
|
* @param callable|string $predicate Predicate or class name |
392
|
|
|
* |
393
|
|
|
* @return XmlElement[] |
394
|
|
|
*/ |
395
|
2 |
|
public function all($predicate) |
396
|
|
|
{ |
397
|
2 |
|
return array_filter($this->_children, filter\predicate($predicate)); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* @param string|null $query |
402
|
|
|
* @return XPathQuery |
403
|
|
|
*/ |
404
|
1 |
|
public function query(string $query = null) |
405
|
|
|
{ |
406
|
1 |
|
return new XPathQuery($query, $this); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* Helper for retrieving all arguments (including namespaces) |
411
|
|
|
* |
412
|
|
|
* @return array |
413
|
|
|
*/ |
414
|
2 |
|
private function attributes(): array |
415
|
|
|
{ |
416
|
2 |
|
$namespaces = $this->getNamespaces(false); |
417
|
2 |
|
$namespaces = array_map(function ($prefix, $uri) { |
418
|
1 |
|
return [$prefix ? "xmlns:{$prefix}" : 'xmlns', $uri]; |
419
|
2 |
|
}, array_values($namespaces), array_keys($namespaces)); |
420
|
|
|
|
421
|
2 |
|
return array_merge( |
422
|
2 |
|
$this->_attributes, |
423
|
2 |
|
array_combine(array_column($namespaces, 0), array_column($namespaces, 1)) |
424
|
|
|
); |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* Prefixes $name with attribute associated with $uri |
429
|
|
|
* |
430
|
|
|
* @param string $name Name to prefix |
431
|
|
|
* @param string $uri Namespace URI |
432
|
|
|
* |
433
|
|
|
* @return string |
434
|
|
|
*/ |
435
|
3 |
|
protected function _prefix(string $name, string $uri): string |
436
|
|
|
{ |
437
|
3 |
|
if (($prefix = $this->lookupPrefix($uri)) === false) { |
438
|
1 |
|
throw new InvalidArgumentException(helper\format('URI "{uri}" is not a registered namespace', ['uri' => $uri])); |
439
|
|
|
} |
440
|
|
|
|
441
|
2 |
|
return "{$prefix}:{$name}"; |
442
|
|
|
} |
443
|
|
|
|
444
|
1 |
|
public function __toString() |
445
|
|
|
{ |
446
|
1 |
|
return trim($this->xml(true)); |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* Splits name into local-name and prefix |
451
|
|
|
* |
452
|
|
|
* @param $name |
453
|
|
|
* @return array [$name, $prefix] |
454
|
|
|
*/ |
455
|
19 |
|
public static function resolve($name) |
456
|
|
|
{ |
457
|
19 |
|
$prefix = null; |
458
|
19 |
|
if (($pos = strpos($name, ':')) !== false) { |
459
|
2 |
|
$prefix = substr($name, 0, $pos); |
460
|
2 |
|
$name = substr($name, $pos + 1); |
461
|
|
|
} |
462
|
|
|
|
463
|
19 |
|
return [$name, $prefix]; |
464
|
|
|
} |
465
|
|
|
} |
466
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.