]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - tests/PHPUnit/Util/XML.php
Added unit tests.
[Github/sugarcrm.git] / tests / PHPUnit / Util / XML.php
1 <?php
2 /**
3  * PHPUnit
4  *
5  * Copyright (c) 2002-2009, Sebastian Bergmann <sb@sebastian-bergmann.de>.
6  * All rights reserved.
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions
10  * are met:
11  *
12  *   * Redistributions of source code must retain the above copyright
13  *     notice, this list of conditions and the following disclaimer.
14  *
15  *   * Redistributions in binary form must reproduce the above copyright
16  *     notice, this list of conditions and the following disclaimer in
17  *     the documentation and/or other materials provided with the
18  *     distribution.
19  *
20  *   * Neither the name of Sebastian Bergmann nor the names of his
21  *     contributors may be used to endorse or promote products derived
22  *     from this software without specific prior written permission.
23  *
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
27  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
28  * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
30  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
34  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35  * POSSIBILITY OF SUCH DAMAGE.
36  *
37  * @category   Testing
38  * @package    PHPUnit
39  * @author     Sebastian Bergmann <sb@sebastian-bergmann.de>
40  * @copyright  2002-2009 Sebastian Bergmann <sb@sebastian-bergmann.de>
41  * @license    http://www.opensource.org/licenses/bsd-license.php  BSD License
42
43  * @link       http://www.phpunit.de/
44  * @since      File available since Release 3.2.0
45  */
46
47 require_once 'PHPUnit/Util/Filter.php';
48
49 PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');
50
51 /**
52  * XML helpers.
53  *
54  * @category   Testing
55  * @package    PHPUnit
56  * @author     Sebastian Bergmann <sb@sebastian-bergmann.de>
57  * @copyright  2002-2009 Sebastian Bergmann <sb@sebastian-bergmann.de>
58  * @license    http://www.opensource.org/licenses/bsd-license.php  BSD License
59  * @version    Release: 3.3.17
60  * @link       http://www.phpunit.de/
61  * @since      Class available since Release 3.2.0
62  */
63 class PHPUnit_Util_XML
64 {
65     /**
66      * Converts a string to UTF-8 encoding.
67      *
68      * @param  string $string
69      * @return string
70      * @since  Method available since Release 3.2.19
71      */
72     public static function convertToUtf8($string)
73     {
74         if (!self::isUtf8($string)) {
75             if (function_exists('mb_convert_encoding')) {
76                 $string = mb_convert_encoding($string, 'UTF-8');
77             } else {
78                 $string = utf8_encode($string);
79             }
80         }
81
82         return $string;
83     }
84
85     /**
86      * Checks a string for UTF-8 encoding.
87      *
88      * @param  string $string
89      * @return boolean
90      * @since  Method available since Release 3.3.0
91      */
92     public static function isUtf8($string)
93     {
94         $length = strlen($string);
95
96         for ($i = 0; $i < $length; $i++) {
97             if      (ord($string[$i]) < 0x80)          $n = 0;
98             elseif ((ord($string[$i]) & 0xE0) == 0xC0) $n = 1;
99             elseif ((ord($string[$i]) & 0xF0) == 0xE0) $n = 2;
100             elseif ((ord($string[$i]) & 0xF0) == 0xF0) $n = 3;
101             else   return FALSE;
102
103             for ($j = 0; $j < $n; $j++) {
104                 if ((++$i == $length) || ((ord($string[$i]) & 0xC0) != 0x80)) return FALSE;
105             }
106         }
107
108         return TRUE;
109     }
110
111     /**
112      * Loads an XML (or HTML) file into a DOMDocument object.
113      *
114      * @param  string  $filename
115      * @param  boolean $isHtml
116      * @return DOMDocument
117      * @since  Method available since Release 3.3.0
118      */
119     public static function loadFile($filename, $isHtml = FALSE)
120     {
121         $reporting = error_reporting(0);
122         $contents  = file_get_contents($filename);
123         error_reporting($reporting);
124
125         if ($contents === FALSE) {
126             throw new RuntimeException(
127               sprintf(
128                 'Could not read "%s".',
129                 $filename
130               )
131             );
132         }
133
134         return self::load($contents, $isHtml, $filename);
135     }
136
137     /**
138      * Load an $actual document into a DOMDocument.  This is called
139      * from the selector assertions.
140      *
141      * If $actual is already a DOMDocument, it is returned with
142      * no changes.  Otherwise, $actual is loaded into a new DOMDocument
143      * as either HTML or XML, depending on the value of $isHtml.
144      *
145      * Note: prior to PHPUnit 3.3.0, this method loaded a file and
146      * not a string as it currently does.  To load a file into a
147      * DOMDocument, use loadFile() instead.
148      *
149      * @param  string|DOMDocument  $actual
150      * @param  boolean             $isHtml
151      * @param  string              $filename
152      * @return DOMDocument
153      * @since  Method available since Release 3.3.0
154      * @author Mike Naberezny <mike@maintainable.com>
155      * @author Derek DeVries <derek@maintainable.com>
156      */
157     public static function load($actual, $isHtml = FALSE, $filename = '')
158     {
159         if ($actual instanceof DOMDocument) {
160             return $actual;
161         }
162
163         $internal  = libxml_use_internal_errors(TRUE);
164         $reporting = error_reporting(0);
165         $dom       = new DOMDocument;
166
167         if ($isHtml) {
168             $loaded = $dom->loadHTML($actual);
169         } else {
170             $loaded = $dom->loadXML($actual);
171         }
172
173         libxml_use_internal_errors($internal);
174         error_reporting($reporting);
175
176         if ($loaded === FALSE) {
177             $message = '';
178
179             foreach (libxml_get_errors() as $error) {
180                 $message .= $error->message;
181             }
182
183             if ($filename != '') {
184                 throw new RuntimeException(
185                   sprintf(
186                     'Could not load "%s".%s',
187
188                     $filename,
189                     $message != '' ? "\n" . $message : ''
190                   )
191                 );
192             } else {
193                 throw new RuntimeException($message);
194             }
195         }
196
197         return $dom;
198     }
199
200     /**
201      *
202      *
203      * @param  DOMNode $node
204      * @since  Method available since Release 3.3.0
205      * @author Mattis Stordalen Flister <mattis@xait.no>
206      */
207     public static function removeCharacterDataNodes(DOMNode $node)
208     {
209         if ($node->hasChildNodes()) {
210             for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
211                 if (($child = $node->childNodes->item($i)) instanceof DOMCharacterData) {
212                     $node->removeChild($child);
213                 }
214             }
215         }
216     }
217
218     /**
219      * Validate list of keys in the associative array.
220      *
221      * @param  array $hash
222      * @param  array $validKeys
223      * @return array
224      * @throws InvalidArgumentException
225      * @since  Method available since Release 3.3.0
226      * @author Mike Naberezny <mike@maintainable.com>
227      * @author Derek DeVries <derek@maintainable.com>
228      */
229     public static function assertValidKeys(array $hash, array $validKeys)
230     {
231         $valids = array();
232
233         // Normalize validation keys so that we can use both indexed and
234         // associative arrays.
235         foreach ($validKeys as $key => $val) {
236             is_int($key) ? $valids[$val] = NULL : $valids[$key] = $val;
237         }
238
239         $validKeys = array_keys($valids);
240
241         // Check for invalid keys.
242         foreach ($hash as $key => $value) {
243             if (!in_array($key, $validKeys)) {
244                 $unknown[] = $key;
245             }
246         }
247
248         if (!empty($unknown)) {
249             throw new InvalidArgumentException(
250               'Unknown key(s): ' . implode(', ', $unknown)
251             );
252         }
253
254         // Add default values for any valid keys that are empty.
255         foreach ($valids as $key => $value) {
256             if (!isset($hash[$key])) {
257               $hash[$key] = $value;
258             }
259         }
260
261         return $hash;
262     }
263
264     /**
265      * Parse a CSS selector into an associative array suitable for
266      * use with findNodes().
267      *
268      * @param  string $selector
269      * @param  mixed  $content
270      * @return array
271      * @since  Method available since Release 3.3.0
272      * @author Mike Naberezny <mike@maintainable.com>
273      * @author Derek DeVries <derek@maintainable.com>
274      */
275     public static function convertSelectToTag($selector, $content = TRUE)
276     {
277         $selector = trim(preg_replace("/\s+/", " ", $selector));
278
279         // substitute spaces within attribute value
280         while (preg_match('/\[[^\]]+"[^"]+\s[^"]+"\]/', $selector)) {
281             $selector = preg_replace('/(\[[^\]]+"[^"]+)\s([^"]+"\])/', "$1__SPACE__$2", $selector);
282         }
283
284         $elements    = strstr($selector, ' ') ? explode(' ', $selector) : array($selector);
285         $previousTag = array();
286
287         foreach (array_reverse($elements) as $element) {
288             $element = str_replace('__SPACE__', ' ', $element);
289
290             // child selector
291             if ($element == '>') {
292                 $previousTag = array('child' => $previousTag['descendant']);
293                 continue;
294             }
295
296             $tag = array();
297
298             // match element tag
299             preg_match("/^([^\.#\[]*)/", $element, $eltMatches);
300
301             if (!empty($eltMatches[1])) {
302                 $tag['tag'] = $eltMatches[1];
303             }
304
305             // match attributes (\[[^\]]*\]*), ids (#[^\.#\[]*), and classes (\.[^\.#\[]*))
306             preg_match_all("/(\[[^\]]*\]*|#[^\.#\[]*|\.[^\.#\[]*)/", $element, $matches);
307
308             if (!empty($matches[1])) {
309                 $classes = array();
310                 $attrs   = array();
311
312                 foreach ($matches[1] as $match) {
313                     // id matched
314                     if (substr($match, 0, 1) == '#') {
315                         $tag['id'] = substr($match, 1);
316                     }
317
318                     // class matched
319                     else if (substr($match, 0, 1) == '.') {
320                         $classes[] = substr($match, 1);
321                     }
322
323                     // attribute matched
324                     else if (substr($match, 0, 1) == '[' && substr($match, -1, 1) == ']') {
325                         $attribute = substr($match, 1, strlen($match) - 2);
326                         $attribute = str_replace('"', '', $attribute);
327
328                         // match single word
329                         if (strstr($attribute, '~=')) {
330                             list($key, $value) = explode('~=', $attribute);
331                             $value = "regexp:/.*\b$value\b.*/";
332                         }
333
334                         // match substring
335                         else if (strstr($attribute, '*=')) {
336                             list($key, $value) = explode('*=', $attribute);
337                             $value = "regexp:/.*$value.*/";
338                         }
339
340                         // exact match
341                         else {
342                             list($key, $value) = explode('=', $attribute);
343                         }
344
345                         $attrs[$key] = $value;
346                     }
347                 }
348
349                 if ($classes) {
350                     $tag['class'] = join(' ', $classes);
351                 }
352
353                 if ($attrs) {
354                     $tag['attributes'] = $attrs;
355                 }
356             }
357
358             // tag content
359             if (is_string($content)) {
360                 $tag['content'] = $content;
361             }
362
363             // determine previous child/descendants
364             if (!empty($previousTag['descendant'])) {
365                 $tag['descendant'] = $previousTag['descendant'];
366             }
367
368             else if (!empty($previousTag['child'])) {
369                 $tag['child'] = $previousTag['child'];
370             }
371
372             $previousTag = array('descendant' => $tag);
373         }
374
375         return $tag;
376     }
377
378     /**
379      * Parse an $actual document and return an array of DOMNodes
380      * matching the CSS $selector.  If an error occurs, it will
381      * return FALSE.
382      *
383      * To only return nodes containing a certain content, give
384      * the $content to match as a string.  Otherwise, setting
385      * $content to TRUE will return all nodes matching $selector.
386      *
387      * The $actual document may be a DOMDocument or a string
388      * containing XML or HTML, identified by $isHtml.
389      *
390      * @param  array   $selector
391      * @param  string  $content
392      * @param  mixed   $actual
393      * @param  boolean $isHtml
394      * @return false|array
395      * @since  Method available since Release 3.3.0
396      * @author Mike Naberezny <mike@maintainable.com>
397      * @author Derek DeVries <derek@maintainable.com>
398      */
399     public static function cssSelect($selector, $content, $actual, $isHtml = TRUE)
400     {
401         $matcher = self::convertSelectToTag($selector, $content);
402         $dom     = self::load($actual, $isHtml);
403         $tags    = self::findNodes($dom, $matcher);
404
405         return $tags;
406     }
407
408     /**
409      * Parse out the options from the tag using DOM object tree.
410      *
411      * @param  DOMDocument $dom
412      * @param  array       $options
413      * @return array
414      * @since  Method available since Release 3.3.0
415      * @author Mike Naberezny <mike@maintainable.com>
416      * @author Derek DeVries <derek@maintainable.com>
417      */
418     public static function findNodes(DOMDocument $dom, array $options)
419     {
420         $valid = array(
421           'id', 'class', 'tag', 'content', 'attributes', 'parent',
422           'child', 'ancestor', 'descendant', 'children'
423         );
424
425         $filtered = array();
426         $options  = self::assertValidKeys($options, $valid);
427
428         // find the element by id
429         if ($options['id']) {
430             $options['attributes']['id'] = $options['id'];
431         }
432
433         if ($options['class']) {
434             $options['attributes']['class'] = $options['class'];
435         }
436
437         // find the element by a tag type
438         if ($options['tag']) {
439             $elements = $dom->getElementsByTagName($options['tag']);
440
441             foreach ($elements as $element) {
442                 $nodes[] = $element;
443             }
444
445             if (empty($nodes)) {
446                 return FALSE;
447             }
448
449         // no tag selected, get them all
450         } else {
451             $tags = array(
452               'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
453               'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
454               'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
455               'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
456               'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
457               'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
458               'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
459               'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
460               'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
461               'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
462               'tr', 'tt', 'ul', 'var'
463             );
464
465             foreach ($tags as $tag) {
466                 $elements = $dom->getElementsByTagName($tag);
467
468                 foreach ($elements as $element) {
469                     $nodes[] = $element;
470                 }
471             }
472
473             if (empty($nodes)) {
474                 return FALSE;
475             }
476         }
477
478         // filter by attributes
479         if ($options['attributes']) {
480             foreach ($nodes as $node) {
481                 $invalid = FALSE;
482
483                 foreach ($options['attributes'] as $name => $value) {
484                     // match by regexp if like "regexp:/foo/i"
485                     if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
486                         if (!preg_match($matches[1], $node->getAttribute($name))) {
487                             $invalid = TRUE;
488                         }
489                     }
490
491                     // class can match only a part
492                     else if ($name == 'class') {
493                         // split to individual classes
494                         $findClasses = explode(' ', preg_replace("/\s+/", " ", $value));
495                         $allClasses  = explode(' ', preg_replace("/\s+/", " ", $node->getAttribute($name)));
496
497                         // make sure each class given is in the actual node
498                         foreach ($findClasses as $findClass) {
499                             if (!in_array($findClass, $allClasses)) {
500                                 $invalid = TRUE;
501                             }
502                         }
503                     }
504
505                     // match by exact string
506                     else {
507                         if ($node->getAttribute($name) != $value) {
508                             $invalid = TRUE;
509                         }
510                     }
511                 }
512
513                 // if every attribute given matched
514                 if (!$invalid) {
515                     $filtered[] = $node;
516                 }
517             }
518
519             $nodes    = $filtered;
520             $filtered = array();
521
522             if (empty($nodes)) {
523                 return FALSE;
524             }
525         }
526
527         // filter by content
528         if ($options['content'] !== NULL) {
529             foreach ($nodes as $node) {
530                 $invalid = FALSE;
531
532                 // match by regexp if like "regexp:/foo/i"
533                 if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
534                     if (!preg_match($matches[1], self::getNodeText($node))) {
535                         $invalid = TRUE;
536                     }
537                 }
538
539                 // match by exact string
540                 else if (strstr(self::getNodeText($node), $options['content']) === FALSE) {
541                     $invalid = TRUE;
542                 }
543
544                 if (!$invalid) {
545                     $filtered[] = $node;
546                 }
547             }
548
549             $nodes    = $filtered;
550             $filtered = array();
551
552             if (empty($nodes)) {
553                 return FALSE;
554             }
555         }
556
557         // filter by parent node
558         if ($options['parent']) {
559             $parentNodes = self::findNodes($dom, $options['parent']);
560             $parentNode  = isset($parentNodes[0]) ? $parentNodes[0] : NULL;
561
562             foreach ($nodes as $node) {
563                 if ($parentNode !== $node->parentNode) {
564                     break;
565                 }
566
567                 $filtered[] = $node;
568             }
569
570             $nodes    = $filtered;
571             $filtered = array();
572
573             if (empty($nodes)) {
574                 return FALSE;
575             }
576         }
577
578         // filter by child node
579         if ($options['child']) {
580             $childNodes = self::findNodes($dom, $options['child']);
581             $childNodes = !empty($childNodes) ? $childNodes : array();
582
583             foreach ($nodes as $node) {
584                 foreach ($node->childNodes as $child) {
585                     foreach ($childNodes as $childNode) {
586                         if ($childNode === $child) {
587                             $filtered[] = $node;
588                         }
589                     }
590                 }
591             }
592
593             $nodes    = $filtered;
594             $filtered = array();
595
596             if (empty($nodes)) {
597                 return FALSE;
598             }
599         }
600
601         // filter by ancestor
602         if ($options['ancestor']) {
603             $ancestorNodes = self::findNodes($dom, $options['ancestor']);
604             $ancestorNode  = isset($ancestorNodes[0]) ? $ancestorNodes[0] : NULL;
605
606             foreach ($nodes as $node) {
607                 $parent = $node->parentNode;
608
609                 while ($parent->nodeType != XML_HTML_DOCUMENT_NODE) {
610                     if ($parent === $ancestorNode) {
611                         $filtered[] = $node;
612                     }
613
614                     $parent = $parent->parentNode;
615                 }
616             }
617
618             $nodes    = $filtered;
619             $filtered = array();
620
621             if (empty($nodes)) {
622                 return FALSE;
623             }
624         }
625
626         // filter by descendant
627         if ($options['descendant']) {
628             $descendantNodes = self::findNodes($dom, $options['descendant']);
629             $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array();
630
631             foreach ($nodes as $node) {
632                 foreach (self::getDescendants($node) as $descendant) {
633                     foreach ($descendantNodes as $descendantNode) {
634                         if ($descendantNode === $descendant) {
635                             $filtered[] = $node;
636                         }
637                     }
638                 }
639             }
640
641             $nodes    = $filtered;
642             $filtered = array();
643
644             if (empty($nodes)) {
645                 return FALSE;
646             }
647         }
648
649         // filter by children
650         if ($options['children']) {
651             $validChild   = array('count', 'greater_than', 'less_than', 'only');
652             $childOptions = self::assertValidKeys($options['children'], $validChild);
653
654             foreach ($nodes as $node) {
655                 $childNodes = $node->childNodes;
656
657                 foreach ($childNodes as $childNode) {
658                     if ($childNode->nodeType !== XML_CDATA_SECTION_NODE &&
659                         $childNode->nodeType !== XML_TEXT_NODE) {
660                         $children[] = $childNode;
661                     }
662                 }
663
664                 // we must have children to pass this filter
665                 if (!empty($children)) {
666                    // exact count of children
667                     if ($childOptions['count'] !== NULL) {
668                         if (count($children) !== $childOptions['count']) {
669                             break;
670                         }
671                     }
672
673                     // range count of children
674                     else if ($childOptions['less_than']    !== NULL &&
675                             $childOptions['greater_than'] !== NULL) {
676                         if (count($children) >= $childOptions['less_than'] ||
677                             count($children) <= $childOptions['greater_than']) {
678                             break;
679                         }
680                     }
681
682                     // less than a given count
683                     else if ($childOptions['less_than'] !== NULL) {
684                         if (count($children) >= $childOptions['less_than']) {
685                             break;
686                         }
687                     }
688
689                     // more than a given count
690                     else if ($childOptions['greater_than'] !== NULL) {
691                         if (count($children) <= $childOptions['greater_than']) {
692                             break;
693                         }
694                     }
695
696                     // match each child against a specific tag
697                     if ($childOptions['only']) {
698                         $onlyNodes = self::findNodes($dom, $childOptions['only']);
699
700                         // try to match each child to one of the 'only' nodes
701                         foreach ($children as $child) {
702                             $matched = FALSE;
703
704                             foreach ($onlyNodes as $onlyNode) {
705                                 if ($onlyNode === $child) {
706                                     $matched = TRUE;
707                                 }
708                             }
709
710                             if (!$matched) {
711                                 break(2);
712                             }
713                         }
714                     }
715
716                     $filtered[] = $node;
717                 }
718             }
719
720             $nodes    = $filtered;
721             $filtered = array();
722
723             if (empty($nodes)) {
724                 return;
725             }
726         }
727
728         // return the first node that matches all criteria
729         return !empty($nodes) ? $nodes : array();
730     }
731
732     /**
733      * Recursively get flat array of all descendants of this node.
734      *
735      * @param  DOMNode $node
736      * @return array
737      * @since  Method available since Release 3.3.0
738      * @author Mike Naberezny <mike@maintainable.com>
739      * @author Derek DeVries <derek@maintainable.com>
740      */
741     protected static function getDescendants(DOMNode $node)
742     {
743         $allChildren = array();
744         $childNodes  = $node->childNodes ? $node->childNodes : array();
745
746         foreach ($childNodes as $child) {
747             if ($child->nodeType === XML_CDATA_SECTION_NODE ||
748                 $child->nodeType === XML_TEXT_NODE) {
749                 continue;
750             }
751
752             $children    = self::getDescendants($child);
753             $allChildren = array_merge($allChildren, $children, array($child));
754         }
755
756         return isset($allChildren) ? $allChildren : array();
757     }
758
759     /**
760      * Get the text value of this node's child text node.
761      *
762      * @param  DOMNode $node
763      * @return string
764      * @since  Method available since Release 3.3.0
765      * @author Mike Naberezny <mike@maintainable.com>
766      * @author Derek DeVries <derek@maintainable.com>
767      */
768     protected static function getNodeText(DOMNode $node)
769     {
770         $childNodes = $node->childNodes instanceof DOMNodeList ? $node->childNodes : array();
771         $text       = '';
772
773         foreach ($childNodes as $child) {
774             if ($child->nodeType === XML_TEXT_NODE) {
775                 $text .= trim($child->data).' ';
776             } else {
777                 $text .= self::getNodeText($child);
778             }
779         }
780
781         return str_replace('  ', ' ', $text);
782     }
783 }
784 ?>