Passed
Push — master ( e584ed...a08a48 )
by Esteban De La Fuente
05:48
created

XmlDocument::getXML()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

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