Completed
Push — master ( 7ae040...f59f35 )
by Esteban De La Fuente
02:52
created

XML::xpath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 35 and the first side effect is on line 28.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
/**
4
 * LibreDTE
5
 * Copyright (C) SASCO SpA (https://sasco.cl)
6
 *
7
 * Este programa es software libre: usted puede redistribuirlo y/o
8
 * modificarlo bajo los términos de la Licencia Pública General Affero de GNU
9
 * publicada por la Fundación para el Software Libre, ya sea la versión
10
 * 3 de la Licencia, o (a su elección) cualquier versión posterior de la
11
 * misma.
12
 *
13
 * Este programa se distribuye con la esperanza de que sea útil, pero
14
 * SIN GARANTÍA ALGUNA; ni siquiera la garantía implícita
15
 * MERCANTIL o de APTITUD PARA UN PROPÓSITO DETERMINADO.
16
 * Consulte los detalles de la Licencia Pública General Affero de GNU para
17
 * 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
 * En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
22
 */
23
24
namespace sasco\LibreDTE;
25
26
// errores de XML se almacenarán internamente y no serán procesados por PHP
27
// se deberán recuperar con: libxml_get_errors()
28
libxml_use_internal_errors(true);
29
30
/**
31
 * Clase para trabajar con XMLs
32
 * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
33
 * @version 2017-01-20
34
 */
35
class XML extends \DomDocument
36
{
37
38
    /**
39
     * Constructor de la clase XML
40
     * @param version Versión del documento XML
41
     * @param encoding Codificación del documento XML
42
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
43
     * @version 2015-08-05
44
     */
45
    public function __construct($version = '1.0', $encoding = 'ISO-8859-1')
46
    {
47
        parent::__construct($version, $encoding);
48
        $this->formatOutput = true;
49
    }
50
51
    /**
52
     * Método que genera nodos XML a partir de un arreglo
53
     * @param array Arreglo con los datos que se usarán para generar XML
54
     * @param parent DOMElement padre para los elementos, o =null para que sea la raíz
55
     * @return Objeto \sasco\LibreDTE\XML
56
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
57
     * @version 2016-04-03
58
     */
59
    public function generate(array $array, \DOMElement &$parent = null)
60
    {
61
        if ($parent===null)
62
            $parent = &$this;
63
        foreach ($array as $key => $value) {
64
            if ($key=='@attributes') {
65
                foreach ($value as $attr => $val) {
66
                    if ($val!==false)
67
                        $parent->setAttribute($attr, $val);
0 ignored issues
show
Bug introduced by
The method setAttribute does only exist in DOMElement, but not in sasco\LibreDTE\XML.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
68
                }
69
            } else if ($key=='@value') {
70
                $parent->nodeValue = $this->sanitize($value);
71
            } else {
72
                if (is_array($value)) {
73
                    if (!empty($value)) {
74
                        $keys = array_keys($value);
75
                        if (!is_int($keys[0])) {
76
                            $value = [$value];
77
                        }
78
                        foreach ($value as $value2) {
79
                            $Node = new \DOMElement($key);
80
                            $parent->appendChild($Node);
81
                            $this->generate($value2, $Node);
82
                        }
83
                    }
84
                } else {
85
                    if (is_object($value) and $value instanceof \DOMElement) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
86
                        $Node = $this->importNode($value, true);
87
                        $parent->appendChild($Node);
88
                    } else {
89
                        if ($value!==false) {
90
                            $Node = new \DOMElement($key, $this->iso2utf($this->sanitize($value)));
91
                            $parent->appendChild($Node);
92
                        }
93
                    }
94
                }
95
            }
96
        }
97
        return $this;
98
    }
99
100
    /**
101
     * Método que sanitiza los valores que son asignados a los tags del XML
102
     * @param txt String que que se asignará como valor al nodo XML
103
     * @return String sanitizado
104
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
105
     * @version 2015-09-02
106
     */
107
    private function sanitize($txt)
108
    {
109
        // si no se paso un texto o bien es un número no se hace nada
110
        if (!$txt or is_numeric($txt))
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
111
            return $txt;
112
        // convertir "predefined entities" de XML
113
        $txt = str_replace(
114
            ['&amp;', '&#38;', '&lt;', '&#60;', '&gt;', '&#62', '&quot;', '&#34;', '&apos;', '&#39;'],
115
            ['&', '&', '<', '<', '>', '>', '"', '"', '\'', '\''],
116
            $txt
117
        );
118
        $txt = str_replace('&', '&amp;', $txt);
119
        /*$txt = str_replace(
120
            ['&', '"', '\''],
121
            ['&amp;', '&quot;', '&apos;'],
122
            $txt
123
        );*/
124
        // entregar texto sanitizado
125
        return $txt;
126
    }
127
128
    /**
129
     * Método que carga un string XML en el Objeto
130
     * @param source String con el documento XML a cargar
131
     * @param options Opciones para la carga del XML
132
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
133
     * @version 2016-11-21
134
     */
135
    public function loadXML($source, $options = null)
136
    {
137
        return $source ? parent::loadXML($this->iso2utf($source), $options) : false;
138
    }
139
140
    /**
141
     * Método para realizar consultas XPATH al documento XML
142
     * @param expression Expresión XPath a ejecutar
143
     * @return DOMNodeList
144
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
145
     * @version 2015-08-05
146
     */
147
    public function xpath($expression)
148
    {
149
        return (new \DOMXPath($this))->query($expression);
150
    }
151
152
    /**
153
     * Método que entrega el código XML aplanado y con la codificación que
154
     * corresponde
155
     * @param xpath XPath para consulta al XML y extraer sólo una parte
156
     * @return String con código XML aplanado
157
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
158
     * @version 2017-01-20
159
     */
160
    public function getFlattened($xpath = null)
161
    {
162
        if ($xpath) {
163
            $node = $this->xpath($xpath)->item(0);
164
            if (!$node)
165
                return false;
166
            $xml = $this->utf2iso($node->C14N());
167
            $xml = $this->fixEntities($xml);
168
        } else {
169
            $xml = $this->C14N();
170
        }
171
        $xml = preg_replace("/\>\n\s+\</", '><', $xml);
172
        $xml = preg_replace("/\>\n\t+\</", '><', $xml);
173
        $xml = preg_replace("/\>\n+\</", '><', $xml);
174
        return trim($xml);
175
    }
176
177
    /**
178
     * Método que codifica el string como ISO-8859-1 si es que fue pasado como
179
     * UTF-8
180
     * @param string String en UTF-8 o ISO-8859-1
181
     * @return String en ISO-8859-1
182
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
183
     * @version 2016-04-03
184
     */
185
    private function utf2iso($string)
186
    {
187
        return mb_detect_encoding($string, ['UTF-8', 'ISO-8859-1']) != 'ISO-8859-1' ? utf8_decode($string) : $string;
188
    }
189
190
    /**
191
     * Método que codifica el string como UTF-8 si es que fue pasado como
192
     * ISO-8859-1
193
     * @param string String en UTF-8 o ISO-8859-1
194
     * @return String en ISO-8859-1
195
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
196
     * @version 2016-04-03
197
     */
198
    private function iso2utf($string)
199
    {
200
        return $string;
201
        //return mb_detect_encoding($string, ['ISO-8859-1', 'UTF-8']) == 'ISO-8859-1' ? utf8_encode($string) : $string;
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
202
    }
203
204
    /**
205
     * Método que convierte el XML a un arreglo
206
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
207
     * @version 2016-06-11
208
     */
209
    public function toArray(\DOMElement $dom = null, array &$array = null, $arregloNodos = false)
210
    {
211
        // determinar valores de parámetros
212
        if (!$dom)
213
            $dom = $this->documentElement;
214
        if (!$dom)
215
            return false;
216
        if ($array===null)
217
            $array = [$dom->tagName => null];
218
        // agregar atributos del nodo
219
        if ($dom->hasAttributes()) {
220
            $array[$dom->tagName]['@attributes'] = [];
221
            foreach ($dom->attributes as $attribute) {
222
                $array[$dom->tagName]['@attributes'][$attribute->name] = $attribute->value;
223
            }
224
        }
225
        // agregar nodos hijos
226
        if ($dom->hasChildNodes()) {
227
            foreach($dom->childNodes as $child) {
228
                if ($child instanceof \DOMText) {
229
                    $textContent = trim($child->textContent);
230
                    if ($textContent!="") {
231
                        if ($dom->childNodes->length==1 and empty($array[$dom->tagName])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
232
                            $array[$dom->tagName] = $textContent;
233
                        } else
234
                            $array[$dom->tagName]['@value'] = $textContent;
235
                    }
236
                }
237
                else if ($child instanceof \DOMElement) {
238
                    $nodos_gemelos = $this->countTwins($dom, $child->tagName);
239
                    if ($nodos_gemelos==1) {
240
                        if ($arregloNodos)
241
                            $this->toArray($child, $array);
242
                        else
243
                            $this->toArray($child, $array[$dom->tagName]);
244
                    }
245
                    // crear arreglo con nodos hijos que tienen el mismo nombre de tag
246
                    else {
247
                        if (!isset($array[$dom->tagName][$child->tagName]))
248
                            $array[$dom->tagName][$child->tagName] = [];
249
                        $siguiente = count($array[$dom->tagName][$child->tagName]);
250
                        $array[$dom->tagName][$child->tagName][$siguiente] = [];
251
                        $this->toArray($child, $array[$dom->tagName][$child->tagName][$siguiente], true);
252
                    }
253
                }
254
            }
255
        }
256
        // entregar arreglo
257
        return $array;
258
    }
259
260
    /**
261
     * Método que cuenta los nodos con el mismo nombre hijos deun DOMElement
262
     * No sirve usar: $dom->getElementsByTagName($tagName)->length ya que esto
263
     * entrega todos los nodos con el nombre, sean hijos, nietos, etc.
264
     * @return Cantidad de nodos hijos con el mismo nombre en el DOMElement
265
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
266
     * @version 2015-09-07
267
     */
268
    private function countTwins(\DOMElement $dom, $tagName)
269
    {
270
        $twins = 0;
271
        foreach ($dom->childNodes as $child) {
272
            if ($child instanceof \DOMElement and $child->tagName==$tagName)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
273
                $twins++;
274
        }
275
        return $twins;
276
    }
277
278
    /**
279
     * Método que entrega los errores de libxml que pueden existir
280
     * @return Arreglo con los errores XML que han ocurrido
281
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
282
     * @version 2015-09-18
283
     */
284
    public function getErrors()
285
    {
286
        $errors = [];
287
        foreach (libxml_get_errors() as $e)
288
            $errors[] = $e->message;
289
        return $errors;
290
    }
291
292
    /**
293
     * Método que entrega el nombre del tag raíz del XML
294
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
295
     * @version 2015-12-14
296
     */
297
    public function getName()
298
    {
299
        return $this->documentElement->tagName;
300
    }
301
302
    /**
303
     * Método que entrega el nombre del archivo del schema del XML
304
     * @return Nombre del schema o bien =false si no se encontró
305
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
306
     * @version 2015-12-14
307
     */
308
    public function getSchema()
309
    {
310
        $schemaLocation = $this->documentElement->getAttribute('xsi:schemaLocation');
311
        if (!$schemaLocation or strpos($schemaLocation, ' ')===false)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
312
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by sasco\LibreDTE\XML::getSchema of type sasco\LibreDTE\Nombre.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
313
        list($uri, $xsd) = explode(' ', $schemaLocation);
0 ignored issues
show
Unused Code introduced by
The assignment to $uri is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
314
        return $xsd;
315
    }
316
317
    /**
318
     * Wrapper para saveXML() y corregir entities
319
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
320
     * @version 2017-01-20
321
     */
322
    public function saveXML(\DOMNode $node = null, $options = null)
323
    {
324
        $xml = parent::saveXML($node, $options);
325
        $xml = $this->fixEntities($xml);
326
        return $xml;
327
    }
328
329
    /**
330
     * Wrapper para C14N() y corregir entities
331
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
332
     * @version 2017-01-20
333
     */
334
    public function C14N($exclusive = null, $with_comments = null, array $xpath = null, array $ns_prefixes = null)
335
    {
336
        $xml = parent::C14N($exclusive, $with_comments, $xpath, $ns_prefixes);
337
        $xml = $this->fixEntities($xml);
338
        return $xml;
339
    }
340
341
    /**
342
     * Método que corrige las entities ' (&apos;) y " (&quot;) ya que el SII no
343
     * respeta el estándar y las requiere convertidas
344
     * @author Esteban De La Fuente Rubio, DeLaF (esteban[at]sasco.cl)
345
     * @version 2017-01-20
346
     */
347
    private function fixEntities($xml)
348
    {
349
        $newXML = '';
350
        $n_letras = strlen($xml);
351
        $convertir = false;
352
        for ($i=0; $i<$n_letras; ++$i) {
353
            if ($xml[$i]=='>')
354
                $convertir = true;
355
            if ($xml[$i]=='<')
356
                $convertir = false;
357
            if ($convertir) {
358
                $l = $xml[$i]=='\'' ? '&apos;' : ($xml[$i]=='"' ? '&quot;' : $xml[$i]);
359
            } else {
360
                $l = $xml[$i];
361
            }
362
            $newXML .= $l;
363
        }
364
        return $newXML;
365
    }
366
367
}
368