Completed
Branch development (176841)
by Elk
06:59
created

XmlArray::_array()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
nc 8
nop 1
dl 0
loc 20
ccs 0
cts 18
cp 0
crap 42
rs 8.9777
c 0
b 0
f 0
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
	/**
28
	 * Holds xml parsed results
29
	 * @var array
30
	 */
31
	public $array;
32
33
	/**
34
	 * Holds debugging level
35
	 * @var int|null
36
	 */
37
	public $debug_level;
38
39
	/**
40
	 * Holds trim level textual data
41
	 * @var bool
42
	 */
43
	public $trim;
44
45
	/**
46
	 * Constructor for the xml parser.
47
	 *
48
	 * Example use:
49
	 *   $xml = new \ElkArte\XmlArray(file('data.xml'));
50
	 *
51
	 * @param string $data the xml data or an array of, unless is_clone is true.
52
	 * @param bool $auto_trim default false, used to automatically trim textual data.
53
	 * @param int|null $level default null, the debug level, specifies whether notices should be generated for missing elements and attributes.
54
	 * @param bool $is_clone default false. If is_clone is true, the  \ElkArte\XmlArray is cloned from another - used internally only.
55
	 */
56
	public function __construct($data, $auto_trim = false, $level = null, $is_clone = false)
57
	{
58
		// If we're using this try to get some more memory.
59
		detectServer()->setMemoryLimit('128M');
60
61
		// Set the debug level.
62
		$this->debug_level = $level !== null ? $level : error_reporting();
63
		$this->trim = $auto_trim;
64
65
		// Is the data already parsed?
66
		if ($is_clone)
67
		{
68
			$this->array = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type string is incompatible with the declared type array of property $array.

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..

Loading history...
69
			return;
70
		}
71
72
		// Is the input an array? (ie. passed from file()?)
73
		if (is_array($data))
74
			$data = implode('', $data);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
75
76
		// Remove any xml declaration or doctype, and parse out comments and CDATA.
77
		$data = preg_replace('/<!--.*?-->/s', '', $this->_to_cdata(preg_replace(array('/^<\?xml.+?\?' . '>/is', '/<!DOCTYPE[^>]+?' . '>/s'), '', $data)));
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
78
79
		// Now parse the xml!
80
		$this->array = $this->_parse($data);
81
	}
82
83
	/**
84
	 * Get the root element's name.
85
	 *
86
	 * Example use:
87
	 *   echo $element->name();
88
	 */
89
	public function name()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
90
	{
91
		return isset($this->array['name']) ? $this->array['name'] : '';
92
	}
93
94
	/**
95
	 * Get a specified element's value or attribute by path.
96
	 *
97
	 * - Children are parsed for text, but only textual data is returned
98
	 * unless get_elements is true.
99
	 *
100
	 * Example use:
101
	 *    $data = $xml->fetch('html/head/title');
102
	 *
103
	 * @param string $path - the path to the element to fetch
104
	 * @param bool $get_elements - whether to include elements
105
	 *
106
	 * @return bool|string
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|string.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
107
	 */
108
	public function fetch($path, $get_elements = false)
109
	{
110
		// Get the element, in array form.
111
		$array = $this->path($path);
112
113
		if ($array === false)
114
			return false;
115
116
		// Getting elements into this is a bit complicated...
117
		if ($get_elements && !is_string($array))
118
		{
119
			$temp = '';
120
121
			// Use the _xml() function to get the xml data.
122
			foreach ($array->array as $val)
123
			{
124
				// Skip the name and any attributes.
125
				if (is_array($val))
126
					$temp .= $this->_xml($val, null);
127
			}
128
129
			// Just get the XML data and then take out the CDATAs.
130
			return $this->_to_cdata($temp);
131
		}
132
133
		// Return the value - taking care to pick out all the text values.
134
		return is_string($array) ? $array : $this->_fetch($array->array);
135
	}
136
137
	/**
138
	 * Get an element, returns a new \ElkArte\XmlArray.
139
	 *
140
	 * - It finds any elements that match the path specified.
141
	 * - It will always return a set if there is more than one of the element
142
	 * or return_set is true.
143
	 *
144
	 * Example use:
145
	 *   $element = $xml->path('html/body');
146
	 *
147
	 * @param string $path  - the path to the element to get
148
	 * @param bool $return_full  - always return full result set
149
	 * @return \ElkArte\XmlArray|bool a new \ElkArte\XmlArray.
150
	 */
151
	public function path($path, $return_full = false)
152
	{
153
		// Split up the path.
154
		$path = explode('/', $path);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $path. This often makes code more readable.
Loading history...
155
156
		// Start with a base array.
157
		$array = $this->array;
158
159
		// For each element in the path.
160
		foreach ($path as $el)
161
		{
162
			// Deal with sets....
163
			if (strpos($el, '[') !== false)
164
			{
165
				$lvl = (int) substr($el, strpos($el, '[') + 1);
166
				$el = substr($el, 0, strpos($el, '['));
167
			}
168
			// Find an attribute.
169
			elseif (substr($el, 0, 1) === '@')
170
			{
171
				// It simplifies things if the attribute is already there ;).
172
				if (isset($array[$el]))
173
					return $array[$el];
174
				else
175
				{
176
					$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
177
					$i = 0;
178 View Code Duplication
					while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this))
179
						$i++;
180
					$debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line'];
181
182
					// Cause an error.
183
					if ($this->debug_level & E_NOTICE)
184
						trigger_error('Undefined XML attribute: ' . substr($el, 1) . $debug, E_USER_NOTICE);
185
186
					return false;
187
				}
188
			}
189
			else
190
				$lvl = null;
191
192
			// Find this element.
193
			$array = $this->_path($array, $el, $lvl);
194
		}
195
196
		// Clean up after $lvl, for $return_full.
197
		if ($return_full && (!isset($array['name']) || substr($array['name'], -1) !== ']'))
198
			$array = array('name' => $el . '[]', $array);
0 ignored issues
show
Bug introduced by
The variable $el does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
199
200
		// Create the right type of class...
201
		$newClass = get_class($this);
202
203
		// Return a new \ElkArte\XmlArray for the result.
204
		return $array === false ? false : new $newClass($array, $this->trim, $this->debug_level, true);
205
	}
206
207
	/**
208
	 * Check if an element exists.
209
	 *
210
	 * Example use,
211
	 *   echo $xml->exists('html/body') ? 'y' : 'n';
212
	 *
213
	 * @param string $path - the path to the element to get.
214
	 * @return boolean
215
	 */
216
	public function exists($path)
217
	{
218
		// Split up the path.
219
		$path = explode('/', $path);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $path. This often makes code more readable.
Loading history...
220
221
		// Start with a base array.
222
		$array = $this->array;
223
224
		// For each element in the path.
225
		foreach ($path as $el)
226
		{
227
			// Deal with sets....
228
			$el_strpos = strpos($el, '[');
229
230
			if ($el_strpos !== false)
231
			{
232
				$lvl = (int) substr($el, $el_strpos + 1);
233
				$el = substr($el, 0, $el_strpos);
234
			}
235
			// Find an attribute.
236
			elseif (substr($el, 0, 1) === '@')
237
				return isset($array[$el]);
238
			else
239
				$lvl = null;
240
241
			// Find this element.
242
			$array = $this->_path($array, $el, $lvl, true);
243
		}
244
245
		return $array !== false;
246
	}
247
248
	/**
249
	 * Count the number of occurrences of a path.
250
	 *
251
	 * Example use:
252
	 *   echo $xml->count('html/head/meta');
253
	 *
254
	 * @param string $path - the path to search for.
255
	 * @return int the number of elements the path matches.
256
	 */
257
	public function count($path)
258
	{
259
		// Get the element, always returning a full set.
260
		$temp = $this->path($path, true);
261
262
		// Start at zero, then count up all the numeric keys.
263
		$i = 0;
264
		foreach ($temp->array as $item)
265
		{
266
			if (is_array($item))
267
				$i++;
268
		}
269
270
		return $i;
271
	}
272
273
	/**
274
	 * Get an array of \ElkArte\XmlArray's matching the specified path.
275
	 *
276
	 * - This differs from ->path(path, true) in that instead of an \ElkArte\XmlArray
277
	 * of elements, an array of \ElkArte\XmlArray's is returned for use with foreach.
278
	 *
279
	 * Example use:
280
	 *   foreach ($xml->set('html/body/p') as $p)
281
	 *
282
	 * @param string $path  - the path to search for.
283
	 * @return array an array of \ElkArte\XmlArray objects
284
	 */
285
	public function set($path)
286
	{
287
		// None as yet, just get the path.
288
		$array = array();
289
		$xml = $this->path($path, true);
290
291
		foreach ($xml->array as $val)
292
		{
293
			// Skip these, they aren't elements.
294
			if (!is_array($val) || $val['name'] === '!')
295
				continue;
296
297
			// Create the right type of class...
298
			$newClass = get_class($this);
299
300
			// Create a new \ElkArte\XmlArray and stick it in the array.
301
			$array[] = new $newClass($val, $this->trim, $this->debug_level, true);
302
		}
303
304
		return $array;
305
	}
306
307
	/**
308
	 * Create an xml file from an \ElkArte\XmlArray, the specified path if any.
309
	 *
310
	 * Example use:
311
	 *   echo $this->create_xml();
312
	 *
313
	 * @param string|null $path - the path to the element. (optional)
314
	 * @return string xml-formatted string.
0 ignored issues
show
Documentation introduced by
Should the return type not be false|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
315
	 */
316
	public function create_xml($path = null)
317
	{
318
		// Was a path specified?  If so, use that array.
319 View Code Duplication
		if ($path !== null)
320
		{
321
			$path = $this->path($path);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $path. This often makes code more readable.
Loading history...
322
323
			// The path was not found
324
			if ($path === false)
325
				return false;
326
327
			$path = $path->array;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $path. This often makes code more readable.
Loading history...
328
		}
329
		// Just use the current array.
330
		else
331
			$path = $this->array;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $path. This often makes code more readable.
Loading history...
332
333
		// Add the xml declaration to the front.
334
		return '<?xml version="1.0"?' . '>' . $this->_xml($path, 0);
335
	}
336
337
	/**
338
	 * Output the xml in an array form.
339
	 *
340
	 * Example use:
341
	 *   print_r($xml->to_array());
342
	 *
343
	 * @param string|null $path the path to output.
344
	 *
345
	 * @return array|bool|string
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|string|array.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
346
	 */
347
	public function to_array($path = null)
348
	{
349
		// Are we doing a specific path?
350 View Code Duplication
		if ($path !== null)
351
		{
352
			$path = $this->path($path);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $path. This often makes code more readable.
Loading history...
353
354
			// The path was not found
355
			if ($path === false)
356
				return false;
357
358
			$path = $path->array;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $path. This often makes code more readable.
Loading history...
359
		}
360
		// No, so just use the current array.
361
		else
362
			$path = $this->array;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $path. This often makes code more readable.
Loading history...
363
364
		return $this->_array($path);
365
	}
366
367
	/**
368
	 * Parse data into an array. (privately used...)
369
	 *
370
	 * @param string $data to parse
371
	 *
372
	 * @return array
373
	 */
374
	protected function _parse($data)
375
	{
376
		// Start with an 'empty' array with no data.
377
		$current = array(
378
		);
379
380
		// Loop until we're out of data.
381
		while ($data !== '')
382
		{
383
			// Find and remove the next tag.
384
			preg_match('/\A<([\w\-:]+)((?:\s+.+?)?)([\s]?\/)?' . '>/', $data, $match);
385
			if (isset($match[0]))
386
				$data = preg_replace('/' . preg_quote($match[0], '/') . '/s', '', $data, 1);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
387
388
			// Didn't find a tag?  Keep looping....
389
			if (!isset($match[1]) || $match[1] === '')
390
			{
391
				// If there's no <, the rest is data.
392
				$data_strpos = strpos($data, '<');
393
				if ($data_strpos === false)
394
				{
395
					$text_value = $this->_from_cdata($data);
396
					$data = '';
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
397
398
					if ($text_value !== '')
399
						$current[] = array(
400
							'name' => '!',
401
							'value' => $text_value
402
						);
403
				}
404
				// If the < isn't immediately next to the current position... more data.
405
				elseif ($data_strpos > 0)
406
				{
407
					$text_value = $this->_from_cdata(substr($data, 0, $data_strpos));
408
					$data = substr($data, $data_strpos);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
409
410
					if ($text_value != '')
411
						$current[] = array(
412
							'name' => '!',
413
							'value' => $text_value
414
						);
415
				}
416
				// If we're looking at a </something> with no start, kill it.
417
				elseif ($data_strpos !== false && $data_strpos === 0)
418
				{
419
					$data_strpos = strpos($data, '<', 1);
420
					if ($data_strpos !== false)
421
					{
422
						$text_value = $this->_from_cdata(substr($data, 0, $data_strpos));
423
						$data = substr($data, $data_strpos);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
424
425
						if ($text_value != '')
426
							$current[] = array(
427
								'name' => '!',
428
								'value' => $text_value
429
							);
430
					}
431 View Code Duplication
					else
432
					{
433
						$text_value = $this->_from_cdata($data);
434
						$data = '';
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
435
436
						if ($text_value != '')
437
							$current[] = array(
438
								'name' => '!',
439
								'value' => $text_value
440
							);
441
					}
442
				}
443
444
				// Wait for an actual occurrence of an element.
445
				continue;
446
			}
447
448
			// Create a new element in the array.
449
			$el = &$current[];
450
			$el['name'] = $match[1];
451
452
			// If this ISN'T empty, remove the close tag and parse the inner data.
453
			if ((!isset($match[3]) || trim($match[3]) != '/') && (!isset($match[2]) || trim($match[2]) != '/'))
454
			{
455
				// Because PHP 5.2.0+ seems to croak using regex, we'll have to do this the less fun way.
456
				$last_tag_end = strpos($data, '</' . $match[1] . '>');
457
				if ($last_tag_end === false)
458
					continue;
459
460
				$offset = 0;
461
				while (1 == 1)
462
				{
463
					// Where is the next start tag?
464
					$next_tag_start = strpos($data, '<' . $match[1], $offset);
465
466
					// If the next start tag is after the last end tag then we've found the right close.
467
					if ($next_tag_start === false || $next_tag_start > $last_tag_end)
468
						break;
469
470
					// If not then find the next ending tag.
471
					$next_tag_end = strpos($data, '</' . $match[1] . '>', $offset);
472
473
					// Didn't find one? Then just use the last and sod it.
474
					if ($next_tag_end === false)
475
						break;
476
					else
477
					{
478
						$last_tag_end = $next_tag_end;
479
						$offset = $next_tag_start + 1;
480
					}
481
				}
482
483
				// Parse the insides.
484
				$inner_match = substr($data, 0, $last_tag_end);
485
486
				// Data now starts from where this section ends.
487
				$data = substr($data, $last_tag_end + strlen('</' . $match[1] . '>'));
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
488
489
				if (!empty($inner_match))
490
				{
491
					// Parse the inner data.
492
					if (strpos($inner_match, '<') !== false)
493
						$el += $this->_parse($inner_match);
494 View Code Duplication
					elseif (trim($inner_match) != '')
495
					{
496
						$text_value = $this->_from_cdata($inner_match);
497
						if ($text_value != '')
498
							$el[] = array(
499
								'name' => '!',
500
								'value' => $text_value
501
							);
502
					}
503
				}
504
			}
505
506
			// If we're dealing with attributes as well, parse them out.
507
			if (isset($match[2]) && $match[2] !== '')
508
			{
509
				// Find all the attribute pairs in the string.
510
				preg_match_all('/([\w:]+)="(.+?)"/', $match[2], $attr, PREG_SET_ORDER);
511
512
				// Set them as @attribute-name.
513
				foreach ($attr as $match_attr)
0 ignored issues
show
Bug introduced by
The expression $attr of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
514
					$el['@' . $match_attr[1]] = $match_attr[2];
515
			}
516
		}
517
518
		// Return the parsed array.
519
		return $current;
520
	}
521
522
	/**
523
	 * Get a specific element's xml. (privately used...)
524
	 *
525
	 * @param mixed[] $array
526
	 * @param null|integer $indent
527
	 *
528
	 * @return string
529
	 */
530
	protected function _xml($array, $indent)
531
	{
532
		$indentation = $indent !== null ? '
533
' . str_repeat('	', $indent) : '';
534
535
		// This is a set of elements, with no name...
536
		if (is_array($array) && !isset($array['name']))
537
		{
538
			$temp = '';
539
			foreach ($array as $val)
540
				$temp .= $this->_xml($val, $indent);
0 ignored issues
show
Documentation introduced by
$val is of type null, but the function expects a array<integer,*>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
541
			return $temp;
542
		}
543
544
		// This is just text!
545
		if ($array['name'] === '!')
546
			return $indentation . '<![CDATA[' . $array['value'] . ']]>';
547
		elseif (substr($array['name'], -2) === '[]')
548
			$array['name'] = substr($array['name'], 0, -2);
549
550
		// Start the element.
551
		$output = $indentation . '<' . $array['name'];
552
553
		$inside_elements = false;
554
		$output_el = '';
555
556
		// Run through and recursively output all the elements or attributes inside this.
557
		foreach ($array as $k => $v)
558
		{
559
			if (substr($k, 0, 1) === '@')
560
				$output .= ' ' . substr($k, 1) . '="' . $v . '"';
561
			elseif (is_array($v))
562
			{
563
				$output_el .= $this->_xml($v, $indent === null ? null : $indent + 1);
564
				$inside_elements = true;
565
			}
566
		}
567
568
		// Indent, if necessary.... then close the tag.
569
		if ($inside_elements)
570
			$output .= '>' . $output_el . $indentation . '</' . $array['name'] . '>';
571
		else
572
			$output .= ' />';
573
574
		return $output;
575
	}
576
577
	/**
578
	 * Return an element as an array
579
	 *
580
	 * @param mixed[] $array An array of data
581
	 *
582
	 * @return array|string
583
	 */
584
	protected function _array($array)
585
	{
586
		$return = array();
587
		$text = '';
588
		foreach ($array as $value)
589
		{
590
			if (!is_array($value) || !isset($value['name']))
591
				continue;
592
593
			if ($value['name'] === '!')
594
				$text .= $value['value'];
595
			else
596
				$return[$value['name']] = $this->_array($value);
597
		}
598
599
		if (empty($return))
600
			return $text;
601
		else
602
			return $return;
603
	}
604
605
	/**
606
	 * Parse out CDATA tags. (htmlspecialchars them...)
607
	 *
608
	 * @param string $data The data with CDATA tags
609
	 *
610
	 * @return string
611
	 */
612
	protected function _to_cdata($data)
613
	{
614
		$inCdata = $inComment = false;
615
		$output = '';
616
617
		$parts = preg_split('~(<!\[CDATA\[|\]\]>|<!--|-->)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
618
		foreach ($parts as $part)
619
		{
620
			// Handle XML comments.
621
			if (!$inCdata && $part === '<!--')
622
				$inComment = true;
623
			if ($inComment && $part === '-->')
624
				$inComment = false;
625
			elseif ($inComment)
626
				continue;
627
628
			// Handle Cdata blocks.
629
			elseif (!$inComment && $part === '<![CDATA[')
630
				$inCdata = true;
631
			elseif ($inCdata && $part === ']]>')
632
				$inCdata = false;
633
			elseif ($inCdata)
634
				$output .= htmlentities($part, ENT_QUOTES);
635
636
			// Everything else is kept as is.
637
			else
638
				$output .= $part;
639
		}
640
641
		return $output;
642
	}
643
644
	/**
645
	 * Turn the CDATAs back to normal text.
646
	 *
647
	 * @param string $data The data with CDATA tags
648
	 *
649
	 * @return string
650
	 */
651
	protected function _from_cdata($data)
652
	{
653
		// Get the HTML translation table and reverse it
654
		$trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES));
655
656
		// Translate all the entities out.
657
		$data = preg_replace_callback('~&#(\d{1,4});~', array($this, '_from_cdata_callback'), $data);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
658
		$data = strtr($data, $trans_tbl);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $data. This often makes code more readable.
Loading history...
659
660
		return $this->trim ? trim($data) : $data;
661
	}
662
663
	/**
664
	 * Callback for the preg_replace in _from_cdata
665
	 *
666
	 * @param mixed[] $match An array of data
667
	 *
668
	 * @return string
669
	 */
670
	protected function _from_cdata_callback($match)
671
	{
672
		return chr($match[1]);
673
	}
674
675
	/**
676
	 * Given an array, return the text from that array. (recursive and privately used.)
677
	 *
678
	 * @param string[]|string $array An array of data
679
	 *
680
	 * @return string
681
	 */
682
	protected function _fetch($array)
683
	{
684
		// Don't return anything if this is just a string.
685
		if (is_string($array))
686
			return '';
687
688
		$temp = '';
689
		foreach ($array as $text)
690
		{
691
			// This means it's most likely an attribute or the name itself.
692
			if (!isset($text['name']))
693
				continue;
694
695
			// This is text!
696
			if ($text['name'] === '!')
697
				$temp .= $text['value'];
698
			// Another element - dive in ;).
699
			else
700
				$temp .= $this->_fetch($text);
701
		}
702
703
		// Return all the bits and pieces we've put together.
704
		return $temp;
705
	}
706
707
	/**
708
	 * Get a specific array by path, one level down. (privately used...)
709
	 *
710
	 * @param mixed[] $array An array of data
711
	 * @param string $path The path
712
	 * @param int $level How far deep into the array we should go
713
	 * @param bool $no_error Whether or not to ignore errors
714
	 *
715
	 * @return array|bool|mixed|mixed[]
716
	 */
717
	protected function _path($array, $path, $level, $no_error = false)
718
	{
719
		// Is $array even an array?  It might be false!
720
		if (!is_array($array))
721
			return false;
722
723
		// Asking for *no* path?
724
		if ($path === '' || $path === '.')
725
			return $array;
726
		$paths = explode('|', $path);
727
728
		// A * means all elements of any name.
729
		$show_all = in_array('*', $paths);
730
731
		$results = array();
732
733
		// Check each element.
734
		foreach ($array as $value)
735
		{
736
			if (!is_array($value) || $value['name'] === '!')
737
				continue;
738
739
			if ($show_all || in_array($value['name'], $paths))
740
			{
741
				// Skip elements before "the one".
742
				if ($level !== null && $level > 0)
743
					$level--;
744
				else
745
					$results[] = $value;
746
			}
747
		}
748
749
		// No results found...
750
		if (empty($results))
751
		{
752
			$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
753
			$i = 0;
754 View Code Duplication
			while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this))
755
				$i++;
756
757
			$debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line'];
758
759
			// Cause an error.
760
			if ($this->debug_level & E_NOTICE && !$no_error)
761
				trigger_error('Undefined XML element: ' . $path . $debug, E_USER_NOTICE);
762
763
			return false;
764
		}
765
		// Only one result.
766
		elseif (count($results) === 1 || $level !== null)
767
			return $results[0];
768
		// Return the result set.
769
		else
770
			return $results + array('name' => $path . '[]');
771
	}
772
}
773