Passed
Pull Request — master (#1078)
by Pauli
02:53
created

XmlResponse   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 106
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 59
dl 0
loc 106
rs 10
c 1
b 0
f 0
wmc 25

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 2
A keyMayDefineAttribute() 0 5 2
D addChildElement() 0 43 18
A arrayIsIndexed() 0 3 2
A render() 0 4 1
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2019 - 2023
11
 */
12
13
namespace OCA\Music\Http;
14
15
use OCP\AppFramework\Http\Response;
16
17
/**
18
 * This class creates an XML response out of a passed in associative array,
19
 * similarly how the class JSONResponse works. The content is described with
20
 * a recursive array structure, where arrays may have string or integer keys.
21
 * One array should not mix string and integer keys, that will lead to undefined
22
 * outcome. Furthermore, array with integer keys is supported only as payload of
23
 * an array with string keys.
24
 *
25
 * Note that this response type has been created to fulfill the needs of the
26
 * SubsonicController and AmpacheController and may not be suitable for all other
27
 * purposes.
28
 */
29
class XmlResponse extends Response {
30
	private $content;
31
	private $doc;
32
	private $attributeKeys;
33
	private $boolAsInt;
34
	private $nullAsEmpty;
35
	private $textNodeKey;
36
37
	/**
38
	 * @param array $content
39
	 * @param bool|string[] $attributes If true, then key-value pair is made into attribute if possible.
40
	 *                                  If false, then key-value pairs are never made into attributes.
41
	 *                                  If an array, then keys found from the array are made into attributes if possible.
42
	 * @param bool $boolAsInt If true, any boolean values are yielded as int 0/1.
43
	 *                        If false, any boolean values are yielded as string "false"/"true".
44
	 * @param bool $nullAsEmpty If true, any null values are converted to empty strings, and the result has an empty element or attribute.
45
	 *                          If false, any null-valued keys are are left out from the result.
46
	 * @param ?string $textNodeKey When a key within @a $content mathes this, the corresponding value is converted to a text node,
47
	 *                             instead of creating an element or attribute named by the key.
48
	 */
49
	public function __construct(array $content, /*mixed*/ $attributes=true,
50
								bool $boolAsInt=false, bool $nullAsEmpty=false,
51
								?string $textNodeKey='value') {
52
		$this->addHeader('Content-Type', 'application/xml');
53
54
		// The content must have exactly one root element, add one if necessary
55
		if (\count($content) != 1) {
56
			$content = ['root' => $content];
57
		}
58
		$this->content = $content;
59
		$this->doc = new \DOMDocument('1.0', 'UTF-8');
60
		$this->doc->formatOutput = true;
61
		$this->attributeKeys = $attributes;
62
		$this->boolAsInt = $boolAsInt;
63
		$this->nullAsEmpty = $nullAsEmpty;
64
		$this->textNodeKey = $textNodeKey;
65
	}
66
67
	public function render() {
68
		$rootName = \array_keys($this->content)[0];
69
		$this->addChildElement($this->doc, $rootName, $this->content[$rootName]);
70
		return $this->doc->saveXML();
71
	}
72
73
	private function addChildElement($parentNode, $key, $value, $allowAttribute=true) {
74
		if (\is_bool($value)) {
75
			if ($this->boolAsInt) {
76
				$value = $value ? '1' : '0';
77
			} else {
78
				$value = $value ? 'true' : 'false';
79
			}
80
		} elseif (\is_numeric($value)) {
81
			$value = (string)$value;
82
		} elseif ($value === null && $this->nullAsEmpty) {
83
			$value = '';
84
		}
85
86
		if (\is_string($value)) {
87
			if ($key == $this->textNodeKey) {
88
				$parentNode->appendChild($this->doc->createTextNode($value));
89
			} elseif ($allowAttribute && $this->keyMayDefineAttribute($key)) {
90
				$parentNode->setAttribute($key, $value);
91
			} else {
92
				$child = $this->doc->createElement($key);
93
				$child->appendChild($this->doc->createTextNode($value));
94
				$parentNode->appendChild($child);
95
			}
96
		} elseif (\is_array($value)) {
97
			if (self::arrayIsIndexed($value)) {
98
				foreach ($value as $child) {
99
					$this->addChildElement($parentNode, $key, $child, /*allowAttribute=*/false);
100
				}
101
			} else { // associative array
102
				$element = $this->doc->createElement($key);
103
				$parentNode->appendChild($element);
104
				foreach ($value as $childKey => $childValue) {
105
					$this->addChildElement($element, $childKey, $childValue);
106
				}
107
			}
108
		} elseif ($value instanceof \stdClass) {
109
			// empty element
110
			$element = $this->doc->createElement($key);
111
			$parentNode->appendChild($element);
112
		} elseif ($value === null) {
113
			// skip
114
		} else {
115
			throw new \Exception("Unexpected value type for key $key");
116
		}
117
	}
118
119
	private function keyMayDefineAttribute($key) {
120
		if (\is_array($this->attributeKeys)) {
121
			return \in_array($key, $this->attributeKeys);
122
		} else {
123
			return \boolval($this->attributeKeys);
124
		}
125
	}
126
127
	/**
128
	 * Array is considered to be "indexed" if its first element has numerical key.
129
	 * Empty array is considered to be "indexed".
130
	 * @param array $array
131
	 */
132
	private static function arrayIsIndexed(array $array) {
133
		\reset($array);
134
		return empty($array) || \is_int(\key($array));
135
	}
136
}
137