elkarte /
Elkarte
| 1 | <?php |
||||
| 2 | |||||
| 3 | /** |
||||
| 4 | * The XmlArray class is an xml parser. |
||||
| 5 | * |
||||
| 6 | * @package ElkArte Forum |
||||
| 7 | * @copyright ElkArte Forum contributors |
||||
| 8 | * @license BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file) |
||||
| 9 | * |
||||
| 10 | * This file contains code covered by: |
||||
| 11 | * copyright: 2011 Simple Machines (http://www.simplemachines.org) |
||||
| 12 | * |
||||
| 13 | * @version 2.0 dev |
||||
| 14 | * |
||||
| 15 | */ |
||||
| 16 | |||||
| 17 | namespace ElkArte; |
||||
| 18 | |||||
| 19 | /** |
||||
| 20 | * Class representing an xml array. |
||||
| 21 | * |
||||
| 22 | * Reads in xml, allows you to access it simply. |
||||
| 23 | * Version 2.0 dev. |
||||
| 24 | */ |
||||
| 25 | class XmlArray |
||||
| 26 | { |
||||
| 27 | /** @var array Holds xml parsed results */ |
||||
| 28 | public $array; |
||||
| 29 | |||||
| 30 | /** @var int|null Holds debugging level */ |
||||
| 31 | public $debug_level; |
||||
| 32 | |||||
| 33 | /** @var bool Holds trim level textual data */ |
||||
| 34 | public $trim; |
||||
| 35 | |||||
| 36 | /** |
||||
| 37 | * Constructor for the xml parser. |
||||
| 38 | * |
||||
| 39 | * Example use: |
||||
| 40 | * $xml = new \ElkArte\XmlArray(file('data.xml')); |
||||
| 41 | * |
||||
| 42 | * @param string $data the xml data or an array of, unless is_clone is true. |
||||
| 43 | * @param bool $auto_trim default false, used to automatically trim textual data. |
||||
| 44 | * @param int|null $level default null, the debug level, specifies whether notices should be generated for missing elements and attributes. |
||||
| 45 | * @param bool $is_clone default false. If is_clone is true, the \ElkArte\XmlArray is cloned from another - used internally only. |
||||
| 46 | */ |
||||
| 47 | public function __construct($data, $auto_trim = false, $level = null, $is_clone = false) |
||||
| 48 | { |
||||
| 49 | // If we're using this try to get some more memory. |
||||
| 50 | detectServer()->setMemoryLimit('128M'); |
||||
| 51 | |||||
| 52 | // Set the debug level. |
||||
| 53 | $this->debug_level = $level ?? error_reporting(); |
||||
| 54 | $this->trim = $auto_trim; |
||||
| 55 | |||||
| 56 | // Is the data already parsed? |
||||
| 57 | if ($is_clone) |
||||
| 58 | { |
||||
| 59 | 4 | $this->array = $data; |
|||
|
0 ignored issues
–
show
|
|||||
| 60 | |||||
| 61 | return; |
||||
| 62 | 4 | } |
|||
| 63 | |||||
| 64 | // Is the input an array? (ie. passed from file()?) |
||||
| 65 | 4 | if (is_array($data)) |
|||
|
0 ignored issues
–
show
|
|||||
| 66 | 4 | { |
|||
| 67 | $data = implode('', $data); |
||||
| 68 | } |
||||
| 69 | 4 | ||||
| 70 | // Remove any xml declaration or doctype, and parse out comments and CDATA. |
||||
| 71 | 4 | $data = preg_replace('/<!--.*?-->/s', '', $this->_to_cdata(preg_replace(['/^<\?xml.+?\?' . '>/is', '/<!DOCTYPE[^>]+?' . '>/'], '', $data))); |
|||
| 72 | |||||
| 73 | 4 | // Now parse the xml! |
|||
| 74 | $this->array = $this->_parse($data); |
||||
| 75 | } |
||||
| 76 | |||||
| 77 | 4 | /** |
|||
| 78 | * Parse out CDATA tags. (htmlspecialchars them...) |
||||
| 79 | * |
||||
| 80 | * @param string $data The data with CDATA tags |
||||
| 81 | * |
||||
| 82 | * @return string |
||||
| 83 | 4 | */ |
|||
| 84 | protected function _to_cdata($data): string |
||||
| 85 | { |
||||
| 86 | 4 | $inCdata = false; |
|||
| 87 | 4 | $inComment = false; |
|||
| 88 | $output = ''; |
||||
| 89 | |||||
| 90 | $parts = preg_split('~(<!\[CDATA\[|\]\]>|<!--|-->)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE); |
||||
| 91 | foreach ($parts as $part) |
||||
| 92 | { |
||||
| 93 | // Handle XML comments. |
||||
| 94 | if (!$inCdata && $part === '<!--') |
||||
| 95 | { |
||||
| 96 | 4 | $inComment = true; |
|||
| 97 | } |
||||
| 98 | 4 | if ($inComment && $part === '-->') |
|||
| 99 | 4 | { |
|||
| 100 | $inComment = false; |
||||
| 101 | 4 | } |
|||
| 102 | 4 | elseif ($inComment) |
|||
| 103 | { |
||||
| 104 | continue; |
||||
| 105 | 4 | } |
|||
| 106 | |||||
| 107 | // Handle Cdata blocks. |
||||
| 108 | elseif (!$inComment && $part === '<![CDATA[') |
||||
| 109 | 4 | { |
|||
| 110 | $inCdata = true; |
||||
| 111 | } |
||||
| 112 | elseif ($inCdata && $part === ']]>') |
||||
| 113 | 4 | { |
|||
| 114 | $inCdata = false; |
||||
| 115 | } |
||||
| 116 | elseif ($inCdata) |
||||
| 117 | { |
||||
| 118 | $output .= htmlentities($part, ENT_QUOTES); |
||||
| 119 | 4 | } |
|||
| 120 | |||||
| 121 | 4 | // Everything else is kept as is. |
|||
| 122 | else |
||||
| 123 | 4 | { |
|||
| 124 | $output .= $part; |
||||
| 125 | 4 | } |
|||
| 126 | } |
||||
| 127 | 4 | ||||
| 128 | return $output; |
||||
| 129 | 4 | } |
|||
| 130 | |||||
| 131 | /** |
||||
| 132 | * Parse data into an array. (privately used...) |
||||
| 133 | * |
||||
| 134 | * @param string $data to parse |
||||
| 135 | 4 | * |
|||
| 136 | * @return array |
||||
| 137 | */ |
||||
| 138 | protected function _parse($data): array |
||||
| 139 | 4 | { |
|||
| 140 | // Start with an 'empty' array with no data. |
||||
| 141 | $current = []; |
||||
| 142 | |||||
| 143 | // Loop until we're out of data. |
||||
| 144 | while ($data !== '') |
||||
| 145 | { |
||||
| 146 | // Find and remove the next tag. |
||||
| 147 | preg_match('/\A<([\w\-:]+)((?:\s+.+?)?)([\s]?\/)?' . '>/', $data, $match); |
||||
| 148 | if (isset($match[0])) |
||||
| 149 | 4 | { |
|||
| 150 | $data = preg_replace('/' . preg_quote($match[0], '/') . '/s', '', $data, 1); |
||||
| 151 | } |
||||
| 152 | 4 | ||||
| 153 | // Didn't find a tag? Keep looping.... |
||||
| 154 | if (!isset($match[1]) || $match[1] === '') |
||||
| 155 | 4 | { |
|||
| 156 | // If there's no <, the rest is data. |
||||
| 157 | $data_strpos = strpos($data, '<'); |
||||
| 158 | 4 | if ($data_strpos === false) |
|||
| 159 | 4 | { |
|||
| 160 | $text_value = $this->_from_cdata($data); |
||||
| 161 | 4 | $data = ''; |
|||
| 162 | |||||
| 163 | if ($text_value !== '') |
||||
| 164 | { |
||||
| 165 | 4 | $current[] = [ |
|||
| 166 | 'name' => '!', |
||||
| 167 | 'value' => $text_value |
||||
| 168 | 4 | ]; |
|||
| 169 | 4 | } |
|||
| 170 | } |
||||
| 171 | 4 | // If the < isn't immediately next to the current position... more data. |
|||
| 172 | 4 | elseif ($data_strpos > 0) |
|||
| 173 | { |
||||
| 174 | 4 | $text_value = $this->_from_cdata(substr($data, 0, $data_strpos)); |
|||
| 175 | $data = substr($data, $data_strpos); |
||||
| 176 | 4 | ||||
| 177 | 4 | if ($text_value !== '') |
|||
| 178 | 4 | { |
|||
| 179 | $current[] = [ |
||||
| 180 | 'name' => '!', |
||||
| 181 | 'value' => $text_value |
||||
| 182 | ]; |
||||
| 183 | 4 | } |
|||
| 184 | } |
||||
| 185 | 4 | // If we're looking at a </something> with no start, kill it. |
|||
| 186 | 4 | elseif ($data_strpos !== false && $data_strpos === 0) |
|||
| 187 | { |
||||
| 188 | 4 | $data_strpos = strpos($data, '<', 1); |
|||
| 189 | if ($data_strpos !== false) |
||||
| 190 | 4 | { |
|||
| 191 | 4 | $text_value = $this->_from_cdata(substr($data, 0, $data_strpos)); |
|||
| 192 | 4 | $data = substr($data, $data_strpos); |
|||
| 193 | |||||
| 194 | if ($text_value !== '') |
||||
| 195 | { |
||||
| 196 | $current[] = [ |
||||
| 197 | 'name' => '!', |
||||
| 198 | 'value' => $text_value |
||||
| 199 | ]; |
||||
| 200 | } |
||||
| 201 | } |
||||
| 202 | else |
||||
| 203 | { |
||||
| 204 | $text_value = $this->_from_cdata($data); |
||||
| 205 | $data = ''; |
||||
| 206 | |||||
| 207 | if ($text_value !== '') |
||||
| 208 | { |
||||
| 209 | $current[] = [ |
||||
| 210 | 'name' => '!', |
||||
| 211 | 'value' => $text_value |
||||
| 212 | ]; |
||||
| 213 | } |
||||
| 214 | } |
||||
| 215 | } |
||||
| 216 | |||||
| 217 | // Wait for an actual occurrence of an element. |
||||
| 218 | continue; |
||||
| 219 | } |
||||
| 220 | |||||
| 221 | // Create a new element in the array. |
||||
| 222 | $el = &$current[]; |
||||
| 223 | $el['name'] = $match[1]; |
||||
| 224 | |||||
| 225 | // If this ISN'T empty, remove the close tag and parse the inner data. |
||||
| 226 | if ((!isset($match[3]) || trim($match[3]) !== '/') && (!isset($match[2]) || trim($match[2]) !== '/')) |
||||
| 227 | { |
||||
| 228 | // Because PHP 5.2.0+ seems to croak using regex, we'll have to do this the less fun way. |
||||
| 229 | 4 | $last_tag_end = strpos($data, '</' . $match[1] . '>'); |
|||
| 230 | if ($last_tag_end === false) |
||||
| 231 | { |
||||
| 232 | continue; |
||||
| 233 | 4 | } |
|||
| 234 | 4 | ||||
| 235 | $offset = 0; |
||||
| 236 | while (true) |
||||
| 237 | 4 | { |
|||
| 238 | // Where is the next start tag? |
||||
| 239 | $next_tag_start = strpos($data, '<' . $match[1], $offset); |
||||
| 240 | 4 | ||||
| 241 | 4 | // If the next start tag is after the last end tag then we've found the right close. |
|||
| 242 | if ($next_tag_start === false || $next_tag_start > $last_tag_end) |
||||
| 243 | { |
||||
| 244 | break; |
||||
| 245 | } |
||||
| 246 | 4 | ||||
| 247 | 4 | // If not then find the next ending tag. |
|||
| 248 | $next_tag_end = strpos($data, '</' . $match[1] . '>', $offset); |
||||
| 249 | |||||
| 250 | 4 | // Didn't find one? Then just use the last and sod it. |
|||
| 251 | if ($next_tag_end === false) |
||||
| 252 | { |
||||
| 253 | 4 | break; |
|||
| 254 | } |
||||
| 255 | 4 | else |
|||
| 256 | { |
||||
| 257 | $last_tag_end = $next_tag_end; |
||||
| 258 | $offset = $next_tag_start + 1; |
||||
| 259 | } |
||||
| 260 | } |
||||
| 261 | |||||
| 262 | // Parse the insides. |
||||
| 263 | $inner_match = substr($data, 0, $last_tag_end); |
||||
| 264 | |||||
| 265 | // Data now starts from where this section ends. |
||||
| 266 | $data = substr($data, $last_tag_end + strlen('</' . $match[1] . '>')); |
||||
| 267 | |||||
| 268 | if (!empty($inner_match)) |
||||
| 269 | { |
||||
| 270 | // Parse the inner data. |
||||
| 271 | if (strpos($inner_match, '<') !== false) |
||||
| 272 | { |
||||
| 273 | $el += $this->_parse($inner_match); |
||||
| 274 | 4 | } |
|||
| 275 | elseif (trim($inner_match) !== '') |
||||
| 276 | { |
||||
| 277 | 4 | $text_value = $this->_from_cdata($inner_match); |
|||
| 278 | if (trim($text_value) !== '') |
||||
| 279 | 4 | { |
|||
| 280 | $el[] = [ |
||||
| 281 | 'name' => '!', |
||||
| 282 | 4 | 'value' => $text_value |
|||
| 283 | ]; |
||||
| 284 | 4 | } |
|||
| 285 | } |
||||
| 286 | 4 | } |
|||
| 287 | } |
||||
| 288 | 4 | ||||
| 289 | 4 | // If we're dealing with attributes as well, parse them out. |
|||
| 290 | if (isset($match[2]) && $match[2] !== '') |
||||
| 291 | 4 | { |
|||
| 292 | 4 | // Find all the attribute pairs in the string. |
|||
| 293 | 4 | preg_match_all('/([\w:]+)="(.+?)"/', $match[2], $attr, PREG_SET_ORDER); |
|||
| 294 | |||||
| 295 | // Set them as @attribute-name. |
||||
| 296 | foreach ($attr as $match_attr) |
||||
| 297 | { |
||||
| 298 | $el['@' . $match_attr[1]] = $match_attr[2]; |
||||
| 299 | } |
||||
| 300 | } |
||||
| 301 | 4 | } |
|||
| 302 | |||||
| 303 | // Return the parsed array. |
||||
| 304 | 4 | return $current; |
|||
| 305 | } |
||||
| 306 | |||||
| 307 | 4 | /** |
|||
| 308 | * Turn the CDATAs back to normal text. |
||||
| 309 | 4 | * |
|||
| 310 | * @param string $data The data with CDATA tags |
||||
| 311 | * |
||||
| 312 | * @return string |
||||
| 313 | */ |
||||
| 314 | protected function _from_cdata($data): string |
||||
| 315 | 4 | { |
|||
| 316 | // Get the HTML translation table and reverse it |
||||
| 317 | $trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES)); |
||||
| 318 | |||||
| 319 | // Translate all the entities out. |
||||
| 320 | $data = preg_replace_callback('~&#(\d{1,4});~', fn($match) => $this->_from_cdata_callback($match), $data); |
||||
| 321 | $data = strtr($data, $trans_tbl); |
||||
| 322 | |||||
| 323 | return $this->trim ? trim($data) : $data; |
||||
| 324 | } |
||||
| 325 | 4 | ||||
| 326 | /** |
||||
| 327 | * Callback for the preg_replace in _from_cdata |
||||
| 328 | 4 | * |
|||
| 329 | * @param array $match An array of data |
||||
| 330 | * |
||||
| 331 | * @return string |
||||
| 332 | */ |
||||
| 333 | 4 | protected function _from_cdata_callback($match): string |
|||
| 334 | 4 | { |
|||
| 335 | return chr($match[1]); |
||||
| 336 | 4 | } |
|||
| 337 | |||||
| 338 | /** |
||||
| 339 | * Get the root element's name. |
||||
| 340 | * |
||||
| 341 | * Example use: |
||||
| 342 | * echo $element->name(); |
||||
| 343 | */ |
||||
| 344 | public function name() |
||||
| 345 | { |
||||
| 346 | return $this->array['name'] ?? ''; |
||||
| 347 | } |
||||
| 348 | |||||
| 349 | /** |
||||
| 350 | * Get a specified element's value or attribute by path. |
||||
| 351 | * |
||||
| 352 | * - Children are parsed for text, but only textual data is returned |
||||
| 353 | * unless get_elements is true. |
||||
| 354 | * |
||||
| 355 | * Example use: |
||||
| 356 | * $data = $xml->fetch('html/head/title'); |
||||
| 357 | 2 | * |
|||
| 358 | * @param string $path - the path to the element to fetch |
||||
| 359 | 2 | * @param bool $get_elements - whether to include elements |
|||
| 360 | * |
||||
| 361 | * @return bool|string |
||||
| 362 | */ |
||||
| 363 | public function fetch($path, $get_elements = false) |
||||
| 364 | { |
||||
| 365 | // Get the element, in array form. |
||||
| 366 | $array = $this->path($path); |
||||
| 367 | |||||
| 368 | if ($array === false) |
||||
| 369 | { |
||||
| 370 | return false; |
||||
| 371 | } |
||||
| 372 | |||||
| 373 | // Getting elements into this is a bit complicated... |
||||
| 374 | if ($get_elements && !is_string($array)) |
||||
| 375 | { |
||||
| 376 | 2 | $temp = ''; |
|||
| 377 | |||||
| 378 | // Use the _xml() function to get the xml data. |
||||
| 379 | 2 | foreach ($array->array as $val) |
|||
| 380 | { |
||||
| 381 | 2 | // Skip the name and any attributes. |
|||
| 382 | if (is_array($val)) |
||||
| 383 | { |
||||
| 384 | $temp .= $this->_xml($val, null); |
||||
| 385 | } |
||||
| 386 | } |
||||
| 387 | 2 | ||||
| 388 | // Just get the XML data and then take out the CDATAs. |
||||
| 389 | return $this->_to_cdata($temp); |
||||
| 390 | } |
||||
| 391 | |||||
| 392 | // Return the value - taking care to pick out all the text values. |
||||
| 393 | return is_string($array) ? $array : $this->_fetch($array->array); |
||||
|
0 ignored issues
–
show
|
|||||
| 394 | } |
||||
| 395 | |||||
| 396 | /** |
||||
| 397 | * Get an element, returns a new \ElkArte\XmlArray. |
||||
| 398 | * |
||||
| 399 | * - It finds any elements that match the path specified. |
||||
| 400 | * - It will always return a set if there is more than one of the element |
||||
| 401 | * or return_set is true. |
||||
| 402 | * |
||||
| 403 | * Example use: |
||||
| 404 | * $element = $xml->path('html/body'); |
||||
| 405 | * |
||||
| 406 | 2 | * @param string $path - the path to the element to get |
|||
| 407 | * @param bool $return_full - always return full result set |
||||
| 408 | * @return \ElkArte\XmlArray|bool a new \ElkArte\XmlArray. |
||||
| 409 | */ |
||||
| 410 | public function path($path, $return_full = false) |
||||
| 411 | { |
||||
| 412 | // Split up the path. |
||||
| 413 | $path = explode('/', $path); |
||||
| 414 | |||||
| 415 | // Start with a base array. |
||||
| 416 | $array = $this->array; |
||||
| 417 | |||||
| 418 | // For each element in the path. |
||||
| 419 | foreach ($path as $el) |
||||
| 420 | { |
||||
| 421 | // Deal with sets.... |
||||
| 422 | if (strpos($el, '[') !== false) |
||||
| 423 | 4 | { |
|||
| 424 | $lvl = (int) substr($el, strpos($el, '[') + 1); |
||||
| 425 | $el = substr($el, 0, strpos($el, '[')); |
||||
| 426 | 4 | } |
|||
| 427 | // Find an attribute. |
||||
| 428 | elseif (substr($el, 0, 1) === '@') |
||||
| 429 | 4 | { |
|||
| 430 | // It simplifies things if the attribute is already there ;). |
||||
| 431 | if (isset($array[$el])) |
||||
| 432 | 4 | { |
|||
| 433 | return $array[$el]; |
||||
| 434 | } |
||||
| 435 | 4 | ||||
| 436 | $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); |
||||
| 437 | 4 | $i = 0; |
|||
| 438 | 4 | while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] === get_class($this)) |
|||
| 439 | { |
||||
| 440 | $i++; |
||||
| 441 | 2 | } |
|||
| 442 | $debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line']; |
||||
| 443 | |||||
| 444 | 2 | // Cause an error. |
|||
| 445 | if (($this->debug_level & E_NOTICE) !== 0) |
||||
| 446 | 2 | { |
|||
| 447 | trigger_error('Undefined XML attribute: ' . substr($el, 1) . $debug, E_USER_NOTICE); |
||||
| 448 | } |
||||
| 449 | |||||
| 450 | return false; |
||||
| 451 | } |
||||
| 452 | else |
||||
| 453 | { |
||||
| 454 | $lvl = null; |
||||
| 455 | } |
||||
| 456 | |||||
| 457 | // Find this element. |
||||
| 458 | $array = $this->_path($array, $el, $lvl); |
||||
| 459 | } |
||||
| 460 | |||||
| 461 | // Clean up after $lvl, for $return_full. |
||||
| 462 | if ($return_full && (!isset($array['name']) || substr($array['name'], -1) !== ']')) |
||||
| 463 | { |
||||
| 464 | $array = ['name' => $el . '[]', $array]; |
||||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
| 465 | } |
||||
| 466 | |||||
| 467 | // Create the right type of class... |
||||
| 468 | $newClass = get_class($this); |
||||
| 469 | 2 | ||||
| 470 | // Return a new \ElkArte\XmlArray for the result. |
||||
| 471 | return $array === false ? false : new $newClass($array, $this->trim, $this->debug_level, true); |
||||
| 472 | } |
||||
| 473 | 4 | ||||
| 474 | /** |
||||
| 475 | * Get a specific array by path, one level down. (privately used...) |
||||
| 476 | * |
||||
| 477 | 4 | * @param array $array An array of data |
|||
| 478 | * @param string $path The path |
||||
| 479 | * @param int $level How far deep into the array we should go |
||||
| 480 | * @param bool $no_error Whether or not to ignore errors |
||||
| 481 | * |
||||
| 482 | * @return array|bool |
||||
| 483 | 4 | */ |
|||
| 484 | protected function _path($array, $path, $level, $no_error = false) |
||||
| 485 | { |
||||
| 486 | 4 | // Is $array even an array? It might be false! |
|||
| 487 | if (!is_array($array)) |
||||
|
0 ignored issues
–
show
|
|||||
| 488 | { |
||||
| 489 | return false; |
||||
| 490 | } |
||||
| 491 | |||||
| 492 | // Asking for *no* path? |
||||
| 493 | if ($path === '' || $path === '.') |
||||
| 494 | { |
||||
| 495 | return $array; |
||||
| 496 | } |
||||
| 497 | $paths = explode('|', $path); |
||||
| 498 | |||||
| 499 | 4 | // A * means all elements of any name. |
|||
| 500 | $show_all = in_array('*', $paths); |
||||
| 501 | |||||
| 502 | 4 | $results = []; |
|||
| 503 | |||||
| 504 | // Check each element. |
||||
| 505 | foreach ($array as $value) |
||||
| 506 | { |
||||
| 507 | if (!is_array($value) || $value['name'] === '!') |
||||
| 508 | 4 | { |
|||
| 509 | continue; |
||||
| 510 | 2 | } |
|||
| 511 | |||||
| 512 | 4 | if ($show_all || in_array($value['name'], $paths, true)) |
|||
| 513 | { |
||||
| 514 | // Skip elements before "the one". |
||||
| 515 | 4 | if ($level !== null && $level > 0) |
|||
| 516 | { |
||||
| 517 | 4 | $level--; |
|||
| 518 | } |
||||
| 519 | else |
||||
| 520 | 4 | { |
|||
| 521 | $results[] = $value; |
||||
| 522 | 4 | } |
|||
| 523 | } |
||||
| 524 | 4 | } |
|||
| 525 | |||||
| 526 | // No results found... |
||||
| 527 | 4 | if (empty($results)) |
|||
| 528 | { |
||||
| 529 | $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); |
||||
| 530 | 4 | $i = 0; |
|||
| 531 | while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] === get_class($this)) |
||||
| 532 | { |
||||
| 533 | $i++; |
||||
| 534 | } |
||||
| 535 | |||||
| 536 | 4 | $debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line']; |
|||
| 537 | |||||
| 538 | // Cause an error. |
||||
| 539 | if ($this->debug_level & E_NOTICE && !$no_error) |
||||
| 540 | { |
||||
| 541 | trigger_error('Undefined XML element: ' . $path . $debug, E_USER_NOTICE); |
||||
| 542 | 4 | } |
|||
| 543 | |||||
| 544 | return false; |
||||
| 545 | } |
||||
| 546 | |||||
| 547 | // Only one result. |
||||
| 548 | if ($level !== null || count($results) === 1) |
||||
|
0 ignored issues
–
show
|
|||||
| 549 | { |
||||
| 550 | return $results[0]; |
||||
| 551 | } |
||||
| 552 | |||||
| 553 | // Return the result set. |
||||
| 554 | return $results + ['name' => $path . '[]']; |
||||
| 555 | } |
||||
| 556 | |||||
| 557 | /** |
||||
| 558 | * Get a specific element's xml. (privately used...) |
||||
| 559 | * |
||||
| 560 | * @param array $array |
||||
| 561 | * @param null|int $indent |
||||
| 562 | 4 | * |
|||
| 563 | * @return string |
||||
| 564 | 4 | */ |
|||
| 565 | protected function _xml($array, $indent): string |
||||
| 566 | { |
||||
| 567 | $indentation = $indent !== null ? ' |
||||
| 568 | ' . str_repeat(' ', $indent) : ''; |
||||
| 569 | 2 | ||||
| 570 | // This is a set of elements, with no name... |
||||
| 571 | if (is_array($array) && !isset($array['name'])) |
||||
| 572 | { |
||||
| 573 | $temp = ''; |
||||
| 574 | foreach ($array as $val) |
||||
| 575 | { |
||||
| 576 | $temp .= $this->_xml($val, $indent); |
||||
| 577 | } |
||||
| 578 | |||||
| 579 | return $temp; |
||||
| 580 | } |
||||
| 581 | |||||
| 582 | // This is just text! |
||||
| 583 | if ($array['name'] === '!') |
||||
| 584 | { |
||||
| 585 | return $indentation . '<![CDATA[' . $array['value'] . ']]>'; |
||||
| 586 | } |
||||
| 587 | |||||
| 588 | if (substr($array['name'], -2) === '[]') |
||||
| 589 | { |
||||
| 590 | $array['name'] = substr($array['name'], 0, -2); |
||||
| 591 | } |
||||
| 592 | |||||
| 593 | // Start the element. |
||||
| 594 | $output = $indentation . '<' . $array['name']; |
||||
| 595 | |||||
| 596 | $inside_elements = false; |
||||
| 597 | $output_el = ''; |
||||
| 598 | |||||
| 599 | // Run through and recursively output all the elements or attributes inside this. |
||||
| 600 | foreach ($array as $k => $v) |
||||
| 601 | { |
||||
| 602 | if (substr($k, 0, 1) === '@') |
||||
| 603 | { |
||||
| 604 | $output .= ' ' . substr($k, 1) . '="' . $v . '"'; |
||||
| 605 | } |
||||
| 606 | elseif (is_array($v)) |
||||
| 607 | { |
||||
| 608 | $output_el .= $this->_xml($v, $indent === null ? null : $indent + 1); |
||||
| 609 | $inside_elements = true; |
||||
| 610 | } |
||||
| 611 | } |
||||
| 612 | |||||
| 613 | // Indent, if necessary.... then close the tag. |
||||
| 614 | if ($inside_elements) |
||||
| 615 | { |
||||
| 616 | $output .= '>' . $output_el . $indentation . '</' . $array['name'] . '>'; |
||||
| 617 | } |
||||
| 618 | else |
||||
| 619 | { |
||||
| 620 | $output .= ' />'; |
||||
| 621 | } |
||||
| 622 | |||||
| 623 | return $output; |
||||
| 624 | } |
||||
| 625 | |||||
| 626 | /** |
||||
| 627 | * Given an array, return the text from that array. (recursive and privately used.) |
||||
| 628 | * |
||||
| 629 | * @param string[]|string $array An array of data |
||||
| 630 | * |
||||
| 631 | * @return string |
||||
| 632 | */ |
||||
| 633 | protected function _fetch($array): string |
||||
| 634 | { |
||||
| 635 | // Don't return anything if this is just a string. |
||||
| 636 | if (is_string($array)) |
||||
| 637 | { |
||||
| 638 | return ''; |
||||
| 639 | } |
||||
| 640 | |||||
| 641 | $temp = ''; |
||||
| 642 | foreach ($array as $text) |
||||
| 643 | { |
||||
| 644 | // This means it's most likely an attribute or the name itself. |
||||
| 645 | if (!isset($text['name'])) |
||||
| 646 | { |
||||
| 647 | continue; |
||||
| 648 | 2 | } |
|||
| 649 | |||||
| 650 | // This is text! |
||||
| 651 | 2 | if ($text['name'] === '!') |
|||
| 652 | { |
||||
| 653 | $temp .= $text['value']; |
||||
| 654 | } |
||||
| 655 | // Another element - dive in ;). |
||||
| 656 | 2 | else |
|||
| 657 | 2 | { |
|||
| 658 | $temp .= $this->_fetch($text); |
||||
| 659 | } |
||||
| 660 | 2 | } |
|||
| 661 | |||||
| 662 | 2 | // Return all the bits and pieces we've put together. |
|||
| 663 | return $temp; |
||||
| 664 | } |
||||
| 665 | |||||
| 666 | 2 | /** |
|||
| 667 | * Check if an element exists. |
||||
| 668 | 2 | * |
|||
| 669 | * Example use, |
||||
| 670 | * echo $xml->exists('html/body') ? 'y' : 'n'; |
||||
| 671 | * |
||||
| 672 | * @param string $path - the path to the element to get. |
||||
| 673 | 1 | * @return bool |
|||
| 674 | */ |
||||
| 675 | public function exists($path): bool |
||||
| 676 | { |
||||
| 677 | // Split up the path. |
||||
| 678 | 2 | $path = explode('/', $path); |
|||
| 679 | |||||
| 680 | // Start with a base array. |
||||
| 681 | $array = $this->array; |
||||
| 682 | |||||
| 683 | // For each element in the path. |
||||
| 684 | foreach ($path as $el) |
||||
| 685 | { |
||||
| 686 | // Deal with sets.... |
||||
| 687 | $el_strpos = strpos($el, '['); |
||||
| 688 | |||||
| 689 | if ($el_strpos !== false) |
||||
| 690 | 4 | { |
|||
| 691 | $lvl = (int) substr($el, $el_strpos + 1); |
||||
| 692 | $el = substr($el, 0, $el_strpos); |
||||
| 693 | 4 | } |
|||
| 694 | // Find an attribute. |
||||
| 695 | elseif (substr($el, 0, 1) === '@') |
||||
| 696 | 4 | { |
|||
| 697 | return isset($array[$el]); |
||||
| 698 | } |
||||
| 699 | 4 | else |
|||
| 700 | { |
||||
| 701 | $lvl = null; |
||||
| 702 | 4 | } |
|||
| 703 | |||||
| 704 | 4 | // Find this element. |
|||
| 705 | $array = $this->_path($array, $el, $lvl, true); |
||||
|
0 ignored issues
–
show
It seems like
$array can also be of type boolean; however, parameter $array of ElkArte\XmlArray::_path() does only seem to accept array, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 706 | 4 | } |
|||
| 707 | 4 | ||||
| 708 | return $array !== false; |
||||
| 709 | } |
||||
| 710 | 2 | ||||
| 711 | /** |
||||
| 712 | 2 | * Count the number of occurrences of a path. |
|||
| 713 | * |
||||
| 714 | * Example use: |
||||
| 715 | * echo $xml->count('html/head/meta'); |
||||
| 716 | 2 | * |
|||
| 717 | * @param string $path - the path to search for. |
||||
| 718 | * @return int the number of elements the path matches. |
||||
| 719 | */ |
||||
| 720 | 4 | public function count($path): int |
|||
| 721 | { |
||||
| 722 | // Get the element, always returning a full set. |
||||
| 723 | 4 | $temp = $this->path($path, true); |
|||
| 724 | |||||
| 725 | // Start at zero, then count up all the numeric keys. |
||||
| 726 | $i = 0; |
||||
| 727 | foreach ($temp->array as $item) |
||||
| 728 | { |
||||
| 729 | if (is_array($item)) |
||||
| 730 | { |
||||
| 731 | $i++; |
||||
| 732 | } |
||||
| 733 | } |
||||
| 734 | |||||
| 735 | return $i; |
||||
| 736 | } |
||||
| 737 | |||||
| 738 | /** |
||||
| 739 | * Get an array of \ElkArte\XmlArray's matching the specified path. |
||||
| 740 | * |
||||
| 741 | * - This differs from ->path(path, true) in that instead of an \ElkArte\XmlArray |
||||
| 742 | * of elements, an array of \ElkArte\XmlArray's is returned for use with foreach. |
||||
| 743 | * |
||||
| 744 | * Example use: |
||||
| 745 | * foreach ($xml->set('html/body/p') as $p) |
||||
| 746 | * |
||||
| 747 | * @param string $path - the path to search for. |
||||
| 748 | * @return array an array of \ElkArte\XmlArray objects |
||||
| 749 | */ |
||||
| 750 | public function set($path): array |
||||
| 751 | { |
||||
| 752 | // None as yet, just get the path. |
||||
| 753 | $array = []; |
||||
| 754 | $xml = $this->path($path, true); |
||||
| 755 | |||||
| 756 | foreach ($xml->array as $val) |
||||
| 757 | { |
||||
| 758 | // Skip these, they aren't elements. |
||||
| 759 | if (!is_array($val) || $val['name'] === '!') |
||||
| 760 | { |
||||
| 761 | continue; |
||||
| 762 | } |
||||
| 763 | |||||
| 764 | // Create the right type of class... |
||||
| 765 | 2 | $newClass = get_class($this); |
|||
| 766 | |||||
| 767 | // Create a new \ElkArte\XmlArray and stick it in the array. |
||||
| 768 | 2 | $array[] = new $newClass($val, $this->trim, $this->debug_level, true); |
|||
| 769 | 2 | } |
|||
| 770 | |||||
| 771 | 2 | return $array; |
|||
| 772 | } |
||||
| 773 | |||||
| 774 | 2 | /** |
|||
| 775 | * Create an xml file from an \ElkArte\XmlArray, the specified path if any. |
||||
| 776 | 2 | * |
|||
| 777 | * Example use: |
||||
| 778 | * echo $this->create_xml(); |
||||
| 779 | * |
||||
| 780 | 2 | * @param string|null $path - the path to the element. (optional) |
|||
| 781 | * @return string xml-formatted string. |
||||
| 782 | */ |
||||
| 783 | 2 | public function create_xml($path = null) |
|||
| 784 | { |
||||
| 785 | // Was a path specified? If so, use that array. |
||||
| 786 | 2 | if ($path !== null) |
|||
| 787 | { |
||||
| 788 | $path = $this->path($path); |
||||
| 789 | |||||
| 790 | // The path was not found |
||||
| 791 | if ($path === false) |
||||
| 792 | { |
||||
| 793 | return false; |
||||
|
0 ignored issues
–
show
|
|||||
| 794 | } |
||||
| 795 | |||||
| 796 | $path = $path->array; |
||||
| 797 | } |
||||
| 798 | // Just use the current array. |
||||
| 799 | else |
||||
| 800 | { |
||||
| 801 | $path = $this->array; |
||||
| 802 | } |
||||
| 803 | |||||
| 804 | // Add the xml declaration to the front. |
||||
| 805 | return '<?xml version="1.0"?' . '>' . $this->_xml($path, 0); |
||||
| 806 | } |
||||
| 807 | |||||
| 808 | /** |
||||
| 809 | * Output the xml in an array form. |
||||
| 810 | * |
||||
| 811 | * Example use: |
||||
| 812 | * print_r($xml->to_array()); |
||||
| 813 | * |
||||
| 814 | * @param string|null $path the path to output. |
||||
| 815 | * |
||||
| 816 | * @return array|bool|string |
||||
| 817 | */ |
||||
| 818 | public function to_array($path = null) |
||||
| 819 | { |
||||
| 820 | // Are we doing a specific path? |
||||
| 821 | if ($path !== null) |
||||
| 822 | { |
||||
| 823 | $path = $this->path($path); |
||||
| 824 | |||||
| 825 | // The path was not found |
||||
| 826 | if ($path === false) |
||||
| 827 | { |
||||
| 828 | return false; |
||||
| 829 | } |
||||
| 830 | |||||
| 831 | $path = $path->array; |
||||
| 832 | } |
||||
| 833 | 4 | // No, so just use the current array. |
|||
| 834 | else |
||||
| 835 | { |
||||
| 836 | 4 | $path = $this->array; |
|||
| 837 | } |
||||
| 838 | |||||
| 839 | return $this->_array($path); |
||||
| 840 | } |
||||
| 841 | |||||
| 842 | /** |
||||
| 843 | * Return an element as an array |
||||
| 844 | * |
||||
| 845 | * @param array $array An array of data |
||||
| 846 | * |
||||
| 847 | * @return array|string |
||||
| 848 | */ |
||||
| 849 | protected function _array($array) |
||||
| 850 | { |
||||
| 851 | 4 | $return = []; |
|||
| 852 | $text = ''; |
||||
| 853 | foreach ($array as $value) |
||||
| 854 | 4 | { |
|||
| 855 | if (!is_array($value) || !isset($value['name'])) |
||||
| 856 | { |
||||
| 857 | continue; |
||||
| 858 | } |
||||
| 859 | |||||
| 860 | if ($value['name'] === '!') |
||||
| 861 | { |
||||
| 862 | $text .= $value['value']; |
||||
| 863 | } |
||||
| 864 | 4 | else |
|||
| 865 | { |
||||
| 866 | 4 | $return[$value['name']] = $this->_array($value); |
|||
| 867 | 4 | } |
|||
| 868 | 4 | } |
|||
| 869 | |||||
| 870 | 4 | if (empty($return)) |
|||
| 871 | { |
||||
| 872 | 4 | return $text; |
|||
| 873 | } |
||||
| 874 | |||||
| 875 | 4 | return $return; |
|||
| 876 | } |
||||
| 877 | } |
||||
| 878 |
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..