Passed
Branch master (0b4ab1)
by Esteban De La Fuente
74:02 queued 50:02
created

Xml   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 231
Duplicated Lines 0 %

Test Coverage

Coverage 89.33%

Importance

Changes 0
Metric Value
eloc 61
c 0
b 0
f 0
dl 0
loc 231
ccs 67
cts 75
cp 0.8933
rs 10
wmc 18

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A C14NWithIsoEncoding() 0 27 3
A getXml() 0 10 1
A C14NWithIsoEncodingFlattened() 0 10 1
A getName() 0 3 1
A getSignatureNodeXml() 0 7 1
A saveXml() 0 5 1
A loadXml() 0 45 4
A getNamespace() 0 5 2
A getSchema() 0 11 3
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\Str;
28
use Derafu\Lib\Core\Helper\Xml as XmlUtil;
29
use Derafu\Lib\Core\Package\Prime\Component\Xml\Exception\XmlException;
30
use DomDocument;
31
use DOMNode;
32
33
/**
34
 * Clase que representa un documento XML.
35
 */
36
class Xml extends DomDocument
37
{
38
    /**
39
     * Constructor del documento XML.
40
     *
41
     * @param string $version Versión del documento XML.
42
     * @param string $encoding Codificación del documento XML.
43
     */
44 78
    public function __construct(
45
        string $version = '1.0',
46
        string $encoding = 'ISO-8859-1'
47
    ) {
48 78
        parent::__construct($version, $encoding);
49
50 78
        $this->formatOutput = true;
51 78
        $this->preserveWhiteSpace = true;
52
    }
53
54
    /**
55
     * Entrega el nombre del tag raíz del XML.
56
     *
57
     * @return string Nombre del tag raíz.
58
     */
59 1
    public function getName(): string
60
    {
61 1
        return $this->documentElement->tagName;
62
    }
63
64
    /**
65
     * Obtiene el espacio de nombres (namespace) del elemento raíz del
66
     * documento XML.
67
     *
68
     * @return string|null Espacio de nombres del documento XML o `null` si no
69
     * está presente.
70
     */
71 3
    public function getNamespace(): ?string
72
    {
73 3
        $namespace = $this->documentElement->getAttribute('xmlns');
74
75 3
        return $namespace !== '' ? $namespace : null;
76
    }
77
78
    /**
79
     * Entrega el nombre del archivo del schema del XML.
80
     *
81
     * @return string|null Nombre del schema o `null` si no se encontró.
82
     */
83 2
    public function getSchema(): ?string
84
    {
85 2
        $schemaLocation = $this->documentElement->getAttribute(
86 2
            'xsi:schemaLocation'
87 2
        );
88
89 2
        if (!$schemaLocation || !str_contains($schemaLocation, ' ')) {
90 1
            return null;
91
        }
92
93 1
        return explode(' ', $schemaLocation)[1];
94
    }
95
96
    /**
97
     * Carga un string XML en la instancia del documento XML.
98
     *
99
     * @param string $source String con el documento XML a cargar.
100
     * @param int $options Opciones para la carga del XML.
101
     * @return bool `true` si el XML se cargó correctamente.
102
     * @throws XmlException Si no es posible cargar el XML.
103
     */
104 46
    public function loadXml(string $source, int $options = 0): bool
105
    {
106
        // Si no hay un string XML en el origen entonces se lanza excepción.
107 46
        if (empty($source)) {
108
            throw new XmlException(
109
                'El contenido del XML que se desea cargar está vacío.'
110
            );
111
        }
112
113
        // Convertir el XML si es necesario.
114 46
        preg_match(
115 46
            '/<\?xml\s+version="([^"]+)"\s+encoding="([^"]+)"\?>/',
116 46
            $source,
117 46
            $matches
118 46
        );
119
        //$version = $matches[1] ?? $this->xmlVersion;
120 46
        $encoding = strtoupper($matches[2] ?? $this->encoding);
121 46
        if ($encoding === 'UTF-8') {
122 4
            $source = Str::utf8decode($source);
123 4
            $source = str_replace(
124 4
                ' encoding="UTF-8"?>',
125 4
                ' encoding="ISO-8859-1"?>',
126 4
                $source
127 4
            );
128
        }
129
130
        // Obtener estado actual de libxml y cambiarlo antes de cargar el XML
131
        // para obtener los errores en una variable si falla algo.
132 46
        $useInternalErrors = libxml_use_internal_errors(true);
133
134
        // Cargar el XML.
135 46
        $status = parent::loadXml($source, $options);
136
137
        // Obtener errores, limpiarlos y restaurar estado de errores de libxml.
138 46
        $errors = libxml_get_errors();
139 46
        libxml_clear_errors();
140 46
        libxml_use_internal_errors($useInternalErrors);
141
142 46
        if (!$status) {
143 4
            throw new XmlException('Error al cargar el XML.', $errors);
144
        }
145
146
        // Retornar estado de la carga del XML.
147
        // Sólo retornará `true`, pues si falla lanza excepción.
148 42
        return true;
149
    }
150
151
    /**
152
     * Genera el documento XML como string.
153
     *
154
     * Wrapper de parent::saveXml() para poder corregir XML entities.
155
     *
156
     * Incluye encabezado del XML con versión y codificación.
157
     *
158
     * @param DOMNode|null $node Nodo a serializar.
159
     * @param int $options Opciones de serialización.
160
     * @return string XML serializado y corregido.
161
     */
162 23
    public function saveXml(?DOMNode $node = null, int $options = 0): string
163
    {
164 23
        $xml = parent::saveXml($node, $options);
165
166 23
        return XmlUtil::fixEntities($xml);
167
    }
168
169
    /**
170
     * Genera el documento XML como string.
171
     *
172
     * Wrapper de saveXml() para generar un string sin el encabezado del XML y
173
     * sin salto de línea inicial o final.
174
     *
175
     * @return string XML serializado y corregido.
176
     */
177 3
    public function getXml(): string
178
    {
179 3
        $xml = $this->saveXml();
180 3
        $xml = preg_replace(
181 3
            '/<\?xml\s+version="1\.0"\s+encoding="[^"]+"\s*\?>/i',
182 3
            '',
183 3
            $xml
184 3
        );
185
186 3
        return trim($xml);
187
    }
188
189
    /**
190
     * Entrega el string XML canonicalizado y con la codificación que
191
     * corresponde (ISO-8859-1).
192
     *
193
     * Esto básicamente usa C14N(), sin embargo, C14N() siempre entrega el XML
194
     * en codificación UTF-8. Por lo que este método permite obtenerlo con C14N
195
     * pero con la codificación correcta de ISO-8859-1. Además se corrigen las
196
     * XML entities.
197
     *
198
     * @param string|null $xpath XPath para consulta al XML y extraer solo una
199
     * parte, desde un tag/nodo específico.
200
     * @return string String XML canonicalizado.
201
     * @throws XmlException En caso de ser pasado un XPath y no encontrarlo.
202
     */
203 19
    public function C14NWithIsoEncoding(?string $xpath = null): string
204
    {
205
        // Si se proporciona XPath, filtrar los nodos.
206 19
        if ($xpath) {
207 8
            $node = XmlUtil::xpath($this, $xpath)->item(0);
208 8
            if (!$node) {
209 2
                throw new XmlException(sprintf(
210 2
                    'No fue posible obtener el nodo con el XPath %s.',
211 2
                    $xpath
212 2
                ));
213
            }
214 6
            $xml = $node->C14N();
215
        }
216
        // Usar C14N() para todo el documento si no se especifica XPath.
217
        else {
218 13
            $xml = $this->C14N();
219
        }
220
221
        // Corregir XML entities.
222 17
        $xml = XmlUtil::fixEntities($xml);
223
224
        // Convertir el XML aplanado de UTF-8 a ISO-8859-1.
225
        // Requerido porque C14N() siempre entrega los datos en UTF-8.
226 17
        $xml = Str::utf8decode($xml);
227
228
        // Entregar el XML canonicalizado.
229 17
        return $xml;
230
    }
231
232
    /**
233
     * Entrega el string XML canonicalizado, con la codificación que
234
     * corresponde (ISO-8859-1) y aplanado.
235
     *
236
     * Es un wrapper de C14NWithIsoEncoding() que aplana el XML resultante.
237
     *
238
     * @param string|null $xpath XPath para consulta al XML y extraer solo una
239
     * parte, desde un tag/nodo específico.
240
     * @return string String XML canonicalizado y aplanado.
241
     * @throws XmlException En caso de ser pasado un XPath y no encontrarlo.
242
     */
243 1
    public function C14NWithIsoEncodingFlattened(?string $xpath = null): string
244
    {
245
        // Obtener XML canonicalizado y codificado en ISO8859-1.
246 1
        $xml = $this->C14NWithIsoEncoding($xpath);
247
248
        // Eliminar los espacios entre tags.
249 1
        $xml = preg_replace("/>\s+</", '><', $xml);
250
251
        // Entregar el XML canonicalizado y aplanado.
252 1
        return $xml;
253
    }
254
255
    /**
256
     * Obtiene el string del nodo de la firma electrónica del XML.
257
     *
258
     * @return string|null String XML de la firma si existe.
259
     */
260
    public function getSignatureNodeXml(): ?string
261
    {
262
        $tag = $this->documentElement->tagName;
263
        $xpath = '/*[local-name()="' . $tag . '"]/*[local-name()="Signature"]';
264
        $signatureElement = XmlUtil::xpath($this, $xpath)->item(0);
265
266
        return $signatureElement?->C14N();
267
    }
268
}
269