1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* Derafu: Biblioteca PHP (Núcleo). |
7
|
|
|
* Copyright (C) Derafu <https://www.derafu.org> |
8
|
|
|
* |
9
|
|
|
* Este programa es software libre: usted puede redistribuirlo y/o modificarlo |
10
|
|
|
* bajo los términos de la Licencia Pública General Affero de GNU publicada por |
11
|
|
|
* la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o |
12
|
|
|
* (a su elección) cualquier versión posterior de la misma. |
13
|
|
|
* |
14
|
|
|
* Este programa se distribuye con la esperanza de que sea útil, pero SIN |
15
|
|
|
* GARANTÍA ALGUNA; ni siquiera la garantía implícita MERCANTIL o de APTITUD |
16
|
|
|
* PARA UN PROPÓSITO DETERMINADO. Consulte los detalles de la Licencia Pública |
17
|
|
|
* General Affero de GNU para obtener una información más detallada. |
18
|
|
|
* |
19
|
|
|
* Debería haber recibido una copia de la Licencia Pública General Affero de GNU |
20
|
|
|
* junto a este programa. |
21
|
|
|
* |
22
|
|
|
* En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>. |
23
|
|
|
*/ |
24
|
|
|
|
25
|
|
|
namespace Derafu\Lib\Core\Package\Prime\Component\Xml\Entity; |
26
|
|
|
|
27
|
|
|
use Derafu\Lib\Core\Helper\Selector; |
28
|
|
|
use Derafu\Lib\Core\Helper\Str; |
29
|
|
|
use Derafu\Lib\Core\Helper\Xml as XmlUtil; |
30
|
|
|
use Derafu\Lib\Core\Package\Prime\Component\Xml\Contract\XmlInterface; |
31
|
|
|
use Derafu\Lib\Core\Package\Prime\Component\Xml\Exception\XmlException; |
32
|
|
|
use Derafu\Lib\Core\Support\Xml\XPathQuery; |
33
|
|
|
use DOMDocument; |
34
|
|
|
use DOMElement; |
35
|
|
|
use DOMNode; |
36
|
|
|
use DOMNodeList; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Clase que representa un documento XML. |
40
|
|
|
*/ |
41
|
|
|
class Xml extends DOMDocument implements XmlInterface |
42
|
|
|
{ |
43
|
|
|
/** |
44
|
|
|
* Instancia para facilitar el manejo de XML usando XPath. |
45
|
|
|
* |
46
|
|
|
* @var XPathQuery |
47
|
|
|
*/ |
48
|
|
|
private XPathQuery $xPathQuery; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Representación del XML como arreglo. |
52
|
|
|
* |
53
|
|
|
* @var array |
54
|
|
|
*/ |
55
|
|
|
private array $array; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Constructor del documento XML. |
59
|
|
|
* |
60
|
|
|
* @param string $version Versión del documento XML. |
61
|
|
|
* @param string $encoding Codificación del documento XML. |
62
|
|
|
*/ |
63
|
78 |
|
public function __construct( |
64
|
|
|
string $version = '1.0', |
65
|
|
|
string $encoding = 'ISO-8859-1' |
66
|
|
|
) { |
67
|
78 |
|
parent::__construct($version, $encoding); |
68
|
|
|
|
69
|
78 |
|
$this->formatOutput = true; |
70
|
78 |
|
$this->preserveWhiteSpace = true; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* {@inheritDoc} |
75
|
|
|
*/ |
76
|
1 |
|
public function getName(): string |
77
|
|
|
{ |
78
|
1 |
|
return $this->documentElement->tagName; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* {@inheritDoc} |
83
|
|
|
*/ |
84
|
3 |
|
public function getNamespace(): ?string |
85
|
|
|
{ |
86
|
3 |
|
$namespace = $this->documentElement->getAttribute('xmlns'); |
87
|
|
|
|
88
|
3 |
|
return $namespace !== '' ? $namespace : null; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* {@inheritDoc} |
93
|
|
|
*/ |
94
|
2 |
|
public function getSchema(): ?string |
95
|
|
|
{ |
96
|
2 |
|
$schemaLocation = $this->documentElement->getAttribute( |
97
|
2 |
|
'xsi:schemaLocation' |
98
|
2 |
|
); |
99
|
|
|
|
100
|
2 |
|
if (!$schemaLocation || !str_contains($schemaLocation, ' ')) { |
101
|
1 |
|
return null; |
102
|
|
|
} |
103
|
|
|
|
104
|
1 |
|
return explode(' ', $schemaLocation)[1]; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* {@inheritDoc} |
109
|
|
|
*/ |
110
|
46 |
|
public function loadXml(string $source, int $options = 0): bool |
111
|
|
|
{ |
112
|
|
|
// Si no hay un string XML en el origen entonces se lanza excepción. |
113
|
46 |
|
if (empty($source)) { |
114
|
|
|
throw new XmlException( |
115
|
|
|
'El contenido del XML que se desea cargar está vacío.' |
116
|
|
|
); |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
// Convertir el XML si es necesario. |
120
|
46 |
|
preg_match( |
121
|
46 |
|
'/<\?xml\s+version="([^"]+)"\s+encoding="([^"]+)"\?>/', |
122
|
46 |
|
$source, |
123
|
46 |
|
$matches |
124
|
46 |
|
); |
125
|
|
|
//$version = $matches[1] ?? $this->xmlVersion; |
126
|
46 |
|
$encoding = strtoupper($matches[2] ?? $this->encoding); |
127
|
46 |
|
if ($encoding === 'UTF-8') { |
128
|
4 |
|
$source = Str::utf8decode($source); |
129
|
4 |
|
$source = str_replace( |
130
|
4 |
|
' encoding="UTF-8"?>', |
131
|
4 |
|
' encoding="ISO-8859-1"?>', |
132
|
4 |
|
$source |
133
|
4 |
|
); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
// Obtener estado actual de libxml y cambiarlo antes de cargar el XML |
137
|
|
|
// para obtener los errores en una variable si falla algo. |
138
|
46 |
|
$useInternalErrors = libxml_use_internal_errors(true); |
139
|
|
|
|
140
|
|
|
// Cargar el XML. |
141
|
46 |
|
$status = parent::loadXml($source, $options); |
142
|
|
|
|
143
|
|
|
// Obtener errores, limpiarlos y restaurar estado de errores de libxml. |
144
|
46 |
|
$errors = libxml_get_errors(); |
145
|
46 |
|
libxml_clear_errors(); |
146
|
46 |
|
libxml_use_internal_errors($useInternalErrors); |
147
|
|
|
|
148
|
46 |
|
if (!$status) { |
149
|
4 |
|
throw new XmlException('Error al cargar el XML.', $errors); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
// Retornar estado de la carga del XML. |
153
|
|
|
// Sólo retornará `true`, pues si falla lanza excepción. |
154
|
42 |
|
return true; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* {@inheritDoc} |
159
|
|
|
*/ |
160
|
23 |
|
public function saveXml(?DOMNode $node = null, int $options = 0): string |
161
|
|
|
{ |
162
|
23 |
|
$xml = parent::saveXml($node, $options); |
163
|
|
|
|
164
|
23 |
|
return XmlUtil::fixEntities($xml); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* {@inheritDoc} |
169
|
|
|
*/ |
170
|
3 |
|
public function getXml(): string |
171
|
|
|
{ |
172
|
3 |
|
$xml = $this->saveXml(); |
173
|
3 |
|
$xml = preg_replace( |
174
|
3 |
|
'/<\?xml\s+version="1\.0"\s+encoding="[^"]+"\s*\?>/i', |
175
|
3 |
|
'', |
176
|
3 |
|
$xml |
177
|
3 |
|
); |
178
|
|
|
|
179
|
3 |
|
return trim($xml); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* {@inheritDoc} |
184
|
|
|
*/ |
185
|
19 |
|
public function C14NWithIsoEncoding(?string $xpath = null): string |
186
|
|
|
{ |
187
|
|
|
// Si se proporciona XPath, filtrar los nodos. |
188
|
19 |
|
if ($xpath) { |
189
|
8 |
|
$node = $this->getNodes($xpath)->item(0); |
190
|
8 |
|
if (!$node) { |
191
|
2 |
|
throw new XmlException(sprintf( |
192
|
2 |
|
'No fue posible obtener el nodo con el XPath %s.', |
193
|
2 |
|
$xpath |
194
|
2 |
|
)); |
195
|
|
|
} |
196
|
6 |
|
$xml = $node->C14N(); |
197
|
|
|
} |
198
|
|
|
// Usar C14N() para todo el documento si no se especifica XPath. |
199
|
|
|
else { |
200
|
13 |
|
$xml = $this->C14N(); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
// Corregir XML entities. |
204
|
17 |
|
$xml = XmlUtil::fixEntities($xml); |
205
|
|
|
|
206
|
|
|
// Convertir el XML aplanado de UTF-8 a ISO-8859-1. |
207
|
|
|
// Requerido porque C14N() siempre entrega los datos en UTF-8. |
208
|
17 |
|
$xml = Str::utf8decode($xml); |
209
|
|
|
|
210
|
|
|
// Entregar el XML canonicalizado. |
211
|
17 |
|
return $xml; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* {@inheritDoc} |
216
|
|
|
*/ |
217
|
1 |
|
public function C14NWithIsoEncodingFlattened(?string $xpath = null): string |
218
|
|
|
{ |
219
|
|
|
// Obtener XML canonicalizado y codificado en ISO8859-1. |
220
|
1 |
|
$xml = $this->C14NWithIsoEncoding($xpath); |
221
|
|
|
|
222
|
|
|
// Eliminar los espacios entre tags. |
223
|
1 |
|
$xml = preg_replace("/>\s+</", '><', $xml); |
224
|
|
|
|
225
|
|
|
// Entregar el XML canonicalizado y aplanado. |
226
|
1 |
|
return $xml; |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* {@inheritDoc} |
231
|
|
|
*/ |
232
|
|
|
public function getSignatureNodeXml(): ?string |
233
|
|
|
{ |
234
|
|
|
$tag = $this->documentElement->tagName; |
235
|
|
|
$xpath = '/' . $tag . '/Signature'; |
236
|
|
|
$signatureElement = $this->getNodes($xpath)->item(0); |
237
|
|
|
|
238
|
|
|
return $signatureElement?->C14N(); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* {@inheritDoc} |
243
|
|
|
*/ |
244
|
|
|
public function query(string $query, array $params = []): string|array|null |
245
|
|
|
{ |
246
|
|
|
if (!isset($this->xPathQuery)) { |
247
|
|
|
$this->xPathQuery = new XPathQuery($this); |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
return $this->xPathQuery->get($query, $params); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* {@inheritDoc} |
255
|
|
|
*/ |
256
|
8 |
|
public function getNodes(string $query, array $params = []): DOMNodeList |
257
|
|
|
{ |
258
|
8 |
|
if (!isset($this->xPathQuery)) { |
259
|
8 |
|
$this->xPathQuery = new XPathQuery($this); |
260
|
|
|
} |
261
|
|
|
|
262
|
8 |
|
return $this->xPathQuery->getNodes($query, $params); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* {@inheritDoc} |
267
|
|
|
*/ |
268
|
|
|
public function get(string $selector): mixed |
269
|
|
|
{ |
270
|
|
|
return Selector::get($this->toArray(), $selector); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* {@inheritDoc} |
275
|
|
|
*/ |
276
|
|
|
public function toArray(): array |
277
|
|
|
{ |
278
|
|
|
if (!isset($this->array)) { |
279
|
|
|
$this->array = $this->query('/'); |
|
|
|
|
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
return $this->array; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* {@inheritDoc} |
287
|
|
|
*/ |
288
|
21 |
|
public function getDocumentElement(): ?DOMElement |
289
|
|
|
{ |
290
|
21 |
|
return $this->documentElement; |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
|
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.