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
|
|
|
|
21
|
|
|
use \Kadet\Xmpp\Utils\helper; |
22
|
|
|
use \Kadet\Xmpp\Utils\filter; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* Class XmlElement |
26
|
|
|
* @package Kadet\Xmpp\Xml |
27
|
|
|
* |
28
|
|
|
* @property string $localName Tag name without prefix. |
29
|
|
|
* @property string $namespace |
30
|
|
|
* @property string $prefix |
31
|
|
|
* @property string $name |
32
|
|
|
* @property string $innerXml |
33
|
|
|
* @property XmlElement $parent |
34
|
|
|
* @property XmlElement[] $children |
35
|
|
|
* @property array $attributes |
36
|
|
|
* @property array $namespaces |
37
|
|
|
*/ |
38
|
|
|
class XmlElement |
39
|
|
|
{ |
40
|
|
|
use Accessors; |
41
|
|
|
|
42
|
|
|
public static $tidy = [ |
43
|
|
|
'indent' => true, |
44
|
|
|
'input-xml' => true, |
45
|
|
|
'output-xml' => true, |
46
|
|
|
'drop-empty-paras' => false, |
47
|
|
|
'wrap' => 0 |
48
|
|
|
]; |
49
|
|
|
|
50
|
|
|
private $_localName; |
51
|
|
|
private $_prefix = null; |
52
|
|
|
|
53
|
|
|
private $_namespaces = [ |
54
|
|
|
]; |
55
|
|
|
private $_attributes = []; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* @var XmlElement |
59
|
|
|
*/ |
60
|
|
|
private $_parent; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* @var XmlElement[] |
64
|
|
|
*/ |
65
|
|
|
private $_children = []; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* XmlElement constructor. |
69
|
|
|
* @param string $name |
70
|
|
|
* @param string $uri |
71
|
|
|
*/ |
72
|
15 |
|
public function __construct(string $name, string $uri = null) |
73
|
|
|
{ |
74
|
15 |
|
list($name, $prefix) = self::resolve($name); |
75
|
|
|
|
76
|
15 |
|
$this->_localName = $name; |
77
|
15 |
|
$this->_prefix = $prefix; |
78
|
|
|
|
79
|
15 |
|
if ($uri !== null) { |
80
|
5 |
|
$this->namespace = $uri; |
81
|
|
|
} |
82
|
15 |
|
} |
83
|
|
|
|
84
|
1 |
|
public function __toString() |
85
|
|
|
{ |
86
|
1 |
|
return trim($this->xml(true)); |
87
|
|
|
} |
88
|
|
|
|
89
|
2 |
|
public function xml($clean = true): string |
90
|
|
|
{ |
91
|
2 |
|
if($this->namespace && $this->_prefix === null) { |
92
|
1 |
|
$this->_prefix = $this->lookupPrefix($this->namespace); |
93
|
|
|
} |
94
|
|
|
|
95
|
2 |
|
$attributes = $this->attributes(); |
96
|
|
|
|
97
|
2 |
|
$result = "<{$this->name}"; |
98
|
|
|
$result .= ' '.implode(' ', array_map(function($key, $value) { |
99
|
1 |
|
return $key.'="'.htmlspecialchars($value, ENT_QUOTES).'"'; |
100
|
2 |
|
}, array_keys($attributes), array_values($attributes))); |
101
|
|
|
|
102
|
2 |
|
if(!empty($this->_children)) { |
103
|
1 |
|
$result .= ">{$this->innerXml}</{$this->name}>"; |
104
|
|
|
} else { |
105
|
2 |
|
$result .= "/>"; |
106
|
|
|
} |
107
|
|
|
|
108
|
2 |
|
return $clean && function_exists('tidy_repair_string') ? tidy_repair_string($result, self::$tidy) : $result; |
109
|
|
|
} |
110
|
|
|
|
111
|
2 |
|
public function getInnerXml() |
112
|
|
|
{ |
113
|
|
|
return implode('', array_map(function($element) { |
114
|
2 |
|
if(is_string($element)) { |
115
|
1 |
|
return htmlspecialchars($element); |
116
|
|
|
} elseif ($element instanceof XmlElement) { |
117
|
1 |
|
return $element->xml(false); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
return (string)$element; |
121
|
2 |
|
}, $this->_children)); |
122
|
|
|
} |
123
|
|
|
|
124
|
5 |
|
public function setAttribute(string $attribute, $value, string $uri = null) |
125
|
|
|
{ |
126
|
5 |
|
if($uri === 'http://www.w3.org/2000/xmlns/') { |
127
|
1 |
|
$this->setNamespace($value, $attribute); |
128
|
1 |
|
return; |
129
|
|
|
} |
130
|
|
|
|
131
|
4 |
|
if($uri !== null) { |
132
|
3 |
|
$attribute = $this->_prefix($attribute, $uri); |
133
|
|
|
} |
134
|
|
|
|
135
|
3 |
|
$this->_attributes[$attribute] = $value; |
136
|
3 |
|
} |
137
|
|
|
|
138
|
2 |
|
public function getAttribute(string $attribute, string $uri = null) |
139
|
|
|
{ |
140
|
2 |
|
if($uri !== null) { |
141
|
1 |
|
$attribute = $this->_prefix($attribute, $uri); |
142
|
|
|
} |
143
|
|
|
|
144
|
2 |
|
return $this->_attributes[$attribute] ?? false; |
145
|
|
|
} |
146
|
|
|
|
147
|
1 |
|
public function getParent() |
148
|
|
|
{ |
149
|
1 |
|
return $this->_parent; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* @return array |
154
|
|
|
*/ |
155
|
16 |
|
public function getNamespaces($parent = true): array |
156
|
|
|
{ |
157
|
16 |
|
if(!$this->_parent) { |
158
|
16 |
|
return $this->_namespaces; |
159
|
|
|
} |
160
|
|
|
|
161
|
8 |
|
if($parent) { |
162
|
8 |
|
return array_merge($this->_namespaces, $this->_parent->getNamespaces()); |
163
|
|
|
} else { |
164
|
1 |
|
return array_diff_assoc($this->_namespaces, $this->_parent->getNamespaces()); |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
2 |
|
private function attributes(): array |
169
|
|
|
{ |
170
|
2 |
|
$namespaces = $this->getNamespaces(false); |
171
|
2 |
|
$namespaces = array_map(function($prefix, $uri) { |
172
|
1 |
|
return [ $prefix ? "xmlns:{$prefix}" : 'xmlns', $uri ]; |
173
|
2 |
|
}, array_values($namespaces), array_keys($namespaces)); |
174
|
|
|
|
175
|
2 |
|
return array_merge( |
176
|
2 |
|
$this->_attributes, |
177
|
2 |
|
array_combine(array_column($namespaces, 0), array_column($namespaces, 1)) |
178
|
|
|
); |
179
|
|
|
} |
180
|
|
|
|
181
|
13 |
|
public function lookupUri(string $prefix = null) |
182
|
|
|
{ |
183
|
13 |
|
return array_search($prefix, $this->getNamespaces()) ?: false; |
184
|
|
|
} |
185
|
|
|
|
186
|
9 |
|
public function lookupPrefix(string $uri = null) |
187
|
|
|
{ |
188
|
9 |
|
return $this->getNamespaces()[$uri] ?? false; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* @param string $uri |
193
|
|
|
* @param string|false $prefix |
194
|
|
|
*/ |
195
|
13 |
|
public function setNamespace(string $uri, $prefix = false) |
196
|
|
|
{ |
197
|
13 |
|
if($prefix === false) { |
198
|
10 |
|
$prefix = $this->_prefix; |
199
|
|
|
} |
200
|
|
|
|
201
|
13 |
|
$this->_namespaces[$uri] = $prefix; |
202
|
13 |
|
} |
203
|
|
|
|
204
|
13 |
|
public function getNamespace() |
205
|
|
|
{ |
206
|
13 |
|
return $this->lookupUri($this->prefix); |
207
|
|
|
} |
208
|
|
|
|
209
|
5 |
|
public function getChildren() |
210
|
|
|
{ |
211
|
5 |
|
return $this->_children; |
212
|
|
|
} |
213
|
|
|
|
214
|
10 |
|
public function append($element) |
215
|
|
|
{ |
216
|
10 |
|
if(!is_string($element) && !$element instanceof XmlElement) { |
217
|
1 |
|
throw new InvalidArgumentException(helper\format('$element should be either string or object of {class} class, {type} given', [ |
218
|
1 |
|
'class' => XmlElement::class, |
219
|
1 |
|
'type' => helper\typeof($element) |
220
|
|
|
])); |
221
|
|
|
} |
222
|
|
|
|
223
|
9 |
|
if($element instanceof XmlElement) { |
224
|
8 |
|
$element->parent = $this; |
225
|
|
|
} |
226
|
|
|
|
227
|
9 |
|
return $this->_children[] = $element; |
228
|
|
|
} |
229
|
|
|
|
230
|
6 |
|
public function getName() |
231
|
|
|
{ |
232
|
6 |
|
return ($this->_prefix ? $this->prefix.':' : null).$this->localName; |
233
|
|
|
} |
234
|
|
|
|
235
|
13 |
|
public function getPrefix() |
236
|
|
|
{ |
237
|
13 |
|
return $this->_prefix; |
238
|
|
|
} |
239
|
|
|
|
240
|
8 |
|
public function getLocalName() |
241
|
|
|
{ |
242
|
8 |
|
return $this->_localName; |
243
|
|
|
} |
244
|
|
|
|
245
|
8 |
|
protected function setParent(XmlElement $parent) |
246
|
|
|
{ |
247
|
8 |
|
if(!$this->_prefix && ($prefix = $parent->lookupPrefix($this->namespace)) !== false) { |
248
|
1 |
|
$this->_namespaces[$this->namespace] = $prefix; |
249
|
1 |
|
$this->_prefix = $prefix; |
250
|
|
|
} |
251
|
|
|
|
252
|
8 |
|
$this->_parent = $parent; |
253
|
8 |
|
if($this->namespace === false) { |
254
|
3 |
|
$this->namespace = $parent->namespace; |
255
|
|
|
} |
256
|
|
|
|
257
|
8 |
|
if(!$this->_prefix) { |
258
|
6 |
|
$this->_prefix = $this->lookupPrefix($this->namespace); |
259
|
|
|
} |
260
|
8 |
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Retrieves array of matching elements |
264
|
|
|
* |
265
|
|
|
* @param string $name Requested element tag name |
266
|
|
|
* @param null $uri Requested element namespace |
267
|
|
|
* |
268
|
|
|
* @return XmlElement[] Found Elements |
269
|
|
|
*/ |
270
|
2 |
|
public function elements($name, $uri = null) : array |
271
|
|
|
{ |
272
|
2 |
|
$predicate = filter\tag($name); |
273
|
2 |
|
if($uri !== null) { |
274
|
1 |
|
$predicate = filter\all($predicate, filter\xmlns($uri)); |
275
|
|
|
} |
276
|
|
|
|
277
|
2 |
|
return $this->all($predicate); |
278
|
|
|
} |
279
|
|
|
|
280
|
6 |
|
public function getAttributes() |
281
|
|
|
{ |
282
|
6 |
|
return $this->_attributes; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* Returns one element at specified index (for default the first one). |
287
|
|
|
* |
288
|
|
|
* @param string $name Requested element tag name |
289
|
|
|
* @param string $uri Requested element namespace |
290
|
|
|
* @param int $index Index of element to retrieve |
291
|
|
|
* |
292
|
|
|
* @return XmlElement|false Retrieved element |
293
|
|
|
*/ |
294
|
1 |
|
public function element(string $name, string $uri = null, int $index = 0) |
295
|
|
|
{ |
296
|
1 |
|
return array_values($this->elements($name, $uri))[$index] ?? false; |
|
|
|
|
297
|
|
|
} |
298
|
|
|
|
299
|
2 |
|
public function all($predicate) { |
300
|
2 |
|
$predicate = filter\predicate($predicate); |
301
|
2 |
|
return array_filter($this->_children, $predicate); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* @param string|null $query |
306
|
|
|
* @return XPathQuery |
307
|
|
|
*/ |
308
|
1 |
|
public function query(string $query = null) |
309
|
|
|
{ |
310
|
1 |
|
return new XPathQuery($query, $this); |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
protected function init() { } |
314
|
|
|
|
315
|
4 |
|
public static function plain(string $name, string $uri = null) |
316
|
|
|
{ |
317
|
4 |
|
$reflection = new \ReflectionClass(static::class); |
318
|
|
|
|
319
|
4 |
|
list($name, $prefix) = static::resolve($name); |
320
|
|
|
|
321
|
|
|
/** @var XmlElement $element */ |
322
|
4 |
|
$element = $reflection->newInstanceWithoutConstructor(); |
323
|
4 |
|
$element->_localName = $name; |
324
|
4 |
|
$element->_prefix = $prefix; |
325
|
|
|
|
326
|
4 |
|
if ($uri !== null) { |
327
|
3 |
|
$element->namespace = $uri; |
328
|
|
|
} |
329
|
|
|
|
330
|
4 |
|
$element->init(); |
331
|
4 |
|
return $element; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* @param string $name |
336
|
|
|
* @param string $uri |
337
|
|
|
* @return string |
338
|
|
|
*/ |
339
|
3 |
|
protected function _prefix(string $name, string $uri): string |
340
|
|
|
{ |
341
|
3 |
|
if (($prefix = $this->lookupPrefix($uri)) === false) { |
342
|
1 |
|
throw new InvalidArgumentException(helper\format('URI "{uri}" is not a registered namespace', ['uri' => $uri])); |
343
|
|
|
} |
344
|
|
|
|
345
|
2 |
|
return "{$prefix}:{$name}"; |
346
|
|
|
} |
347
|
|
|
|
348
|
19 |
|
public static function resolve($name) |
349
|
|
|
{ |
350
|
19 |
|
$prefix = null; |
351
|
19 |
|
if (($pos = strpos($name, ':')) !== false) { |
352
|
2 |
|
$prefix = substr($name, 0, $pos); |
353
|
2 |
|
$name = substr($name, $pos + 1); |
354
|
|
|
} |
355
|
|
|
|
356
|
19 |
|
return [$name, $prefix]; |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
|
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.