MDL-13435 xmlize - now throwing exceptions on parsing error. Credit goes to Tim/Mahmoud
[moodle.git] / lib / xmlize.php
1 <?php
3 /**
4  * xmlize.php - xmlize() is by Hans Anderson, {@link http://www.hansanderson.com/contact/}
5  *
6  * Ye Ole "Feel Free To Use it However" License [PHP, BSD, GPL].
7  * some code in xml_depth is based on code written by other PHPers
8  * as well as one Perl script.  Poor programming practice and organization
9  * on my part is to blame for the credit these people aren't receiving.
10  * None of the code was copyrighted, though.
11  *
12  * @package core
13  * @subpackage lib
14  * @author Hans Anderson
15  * @version This is a stable release, 1.0.  I don't foresee any changes, but you
16  * might check {@link http://www.hansanderson.com/php/xml/} to see
17  * @copyright Hans Anderson
18  * @license Feel Free To Use it However
19  */
21 /**
22  * Exception thrown when there is an error parsing an XML file.
23  *
24  * @copyright 2010 The Open University
25  */
26 class xml_format_exception extends moodle_exception {
27     /** @var string */
28     public $errorstring;
29     public $line;
30     public $char;
31     function __construct($errorstring, $line, $char, $link = '') {
32         $this->errorstring = $errorstring;
33         $this->line = $line;
34         $this->char = $char;
36         $a = new stdClass();
37         $a->errorstring = $errorstring;
38         $a->errorline = $line;
39         $a->errorchar = $char;
40         parent::__construct('errorparsingxml', 'error', $link, $a);
41     }
42 }
44 /**
45  * Create an array structure from an XML string.
46  *
47  * Usage:<br>
48  * <code>
49  * $xml = xmlize($array);
50  * </code>
51  * See the function {@link traverse_xmlize()} for information about the
52  * structure of the array, it's much easier to explain by showing you.
53  * Be aware that the array is somewhat tricky.  I use xmlize all the time,
54  * but still need to use {@link traverse_xmlize()} quite often to show me the structure!
55  *
56  * THIS IS A PHP 5 VERSION:
57  *
58  * This modified version basically has a new optional parameter
59  * to specify an OUTPUT encoding. If not specified, it defaults to UTF-8.
60  * I recommend you to read this PHP bug. There you can see how PHP4, PHP5.0.0
61  * and PHP5.0.2 will handle this.
62  * {@link http://bugs.php.net/bug.php?id=29711}
63  * Ciao, Eloy :-)
64  *
65  * @param string $data The XML source to parse.
66  * @param int $whitespace  If set to 1 allows the parser to skip "space" characters in xml document. Default is 1
67  * @param string $encoding Specify an OUTPUT encoding. If not specified, it defaults to UTF-8.
68  * @param bool $reporterrors if set to true, then a {@link xml_format_exception}
69  *      exception will be thrown if the XML is not well-formed. Otherwise errors are ignored.
70  * @return array representation of the parsed XML.
71  */
72 function xmlize($data, $whitespace = 1, $encoding = 'UTF-8', $reporterrors = false) {
74     $data = trim($data);
75     $vals = array();
76     $parser = xml_parser_create($encoding);
77     xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
78     xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, $whitespace);
79     xml_parse_into_struct($parser, $data, $vals);
81     // Error handling when the xml file is not well-formed
82     if ($reporterrors) {
83         $errorcode = xml_get_error_code($parser);
84         if ($errorcode) {
85             $exception = new xml_format_exception(xml_error_string($errorcode),
86                     xml_get_current_line_number($parser),
87                     xml_get_current_column_number($parser));
88             xml_parser_free($parser);
89             throw $exception;
90         }
91     }
92     xml_parser_free($parser);
94     $i = 0;
95     if (empty($vals)) {
96         // XML file is invalid or empty, return false
97         return false;
98     }
100     $array = array();
101     $tagname = $vals[$i]['tag'];
102     if (isset($vals[$i]['attributes'])) {
103         $array[$tagname]['@'] = $vals[$i]['attributes'];
104     } else {
105         $array[$tagname]['@'] = array();
106     }
108     $array[$tagname]["#"] = xml_depth($vals, $i);
110     return $array;
113 /**
114  * @internal You don't need to do anything with this function, it's called by
115  * xmlize. It's a recursive function, calling itself as it goes deeper
116  * into the xml levels.  If you make any improvements, please let me know.
117  * @access private
118  */
119 function xml_depth($vals, &$i) {
120     $children = array();
122     if ( isset($vals[$i]['value']) )
123     {
124         array_push($children, $vals[$i]['value']);
125     }
127     while (++$i < count($vals)) {
129         switch ($vals[$i]['type']) {
131            case 'open':
133                 if ( isset ( $vals[$i]['tag'] ) )
134                 {
135                     $tagname = $vals[$i]['tag'];
136                 } else {
137                     $tagname = '';
138                 }
140                 if ( isset ( $children[$tagname] ) )
141                 {
142                     $size = sizeof($children[$tagname]);
143                 } else {
144                     $size = 0;
145                 }
147                 if ( isset ( $vals[$i]['attributes'] ) ) {
148                     $children[$tagname][$size]['@'] = $vals[$i]["attributes"];
150                 }
152                 $children[$tagname][$size]['#'] = xml_depth($vals, $i);
154             break;
157             case 'cdata':
158                 array_push($children, $vals[$i]['value']);
159             break;
161             case 'complete':
162                 $tagname = $vals[$i]['tag'];
164                 if( isset ($children[$tagname]) )
165                 {
166                     $size = sizeof($children[$tagname]);
167                 } else {
168                     $size = 0;
169                 }
171                 if( isset ( $vals[$i]['value'] ) )
172                 {
173                     $children[$tagname][$size]["#"] = $vals[$i]['value'];
174                 } else {
175                     $children[$tagname][$size]["#"] = '';
176                 }
178                 if ( isset ($vals[$i]['attributes']) ) {
179                     $children[$tagname][$size]['@']
180                                              = $vals[$i]['attributes'];
181                 }
183             break;
185             case 'close':
186                 return $children;
187             break;
188         }
190     }
192         return $children;
198 /**
199  * This helps you understand the structure of the array {@link xmlize()} outputs
200  *
201  * Function by acebone@f2s.com, a HUGE help!<br>
202  * Usage:<br>
203  * <code>
204  * traverse_xmlize($xml, 'xml_');
205  * print '<pre>' . implode("", $traverse_array . '</pre>';
206  * </code>
207  * @author acebone@f2s.com
208  * @param array $array ?
209  * @param string $arrName ?
210  * @param int $level ?
211  * @return int
212  * @todo Finish documenting this function
213  */
214 function traverse_xmlize($array, $arrName = 'array', $level = 0) {
216     foreach($array as $key=>$val)
217     {
218         if ( is_array($val) )
219         {
220             traverse_xmlize($val, $arrName . '[' . $key . ']', $level + 1);
221         } else {
222             $GLOBALS['traverse_array'][] = '$' . $arrName . '[' . $key . '] = "' . $val . "\"\n";
223         }
224     }
226     return 1;