Issues (40)

lib/Http/XmlResponse.php (1 issue)

Severity
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 - 2025
11
 */
12
13
namespace OCA\Music\Http;
14
15
use OCP\AppFramework\Http;
16
use OCP\AppFramework\Http\Response;
17
18
/**
19
 * This class creates an XML response out of a passed in associative array,
20
 * similarly how the class JSONResponse works. The content is described with
21
 * a recursive array structure, where arrays may have string or integer keys.
22
 * One array should not mix string and integer keys, that will lead to undefined
23
 * outcome. Furthermore, array with integer keys is supported only as payload of
24
 * an array with string keys.
25
 *
26
 * Note that this response type has been created to fulfill the needs of the
27
 * SubsonicController and AmpacheController and may not be suitable for all other
28
 * purposes.
29
 */
30
class XmlResponse extends Response {
31
	private array $content;
32
	private \DOMDocument $doc;
33
	/** @var bool|string[] $attributeKeys */
34
	private $attributeKeys;
35
	private bool $boolAsInt;
36
	private bool $nullAsEmpty;
37
	private ?string $textNodeKey;
38
39
	/**
40
	 * @param array $content
41
	 * @param bool|string[] $attributes If true, then key-value pair is made into attribute if possible.
42
	 *                                  If false, then key-value pairs are never made into attributes.
43
	 *                                  If an array, then keys found from the array are made into attributes if possible.
44
	 * @param bool $boolAsInt If true, any boolean values are yielded as int 0/1.
45
	 *                        If false, any boolean values are yielded as string "false"/"true".
46
	 * @param bool $nullAsEmpty If true, any null values are converted to empty strings, and the result has an empty element or attribute.
47
	 *                          If false, any null-valued keys are are left out from the result.
48
	 * @param ?string $textNodeKey When a key within @a $content matches this, the corresponding value is converted to a text node,
49
	 *                             instead of creating an element or attribute named by the key.
50
	 */
51
	public function __construct(array $content, /*mixed*/ $attributes=true,
52
								bool $boolAsInt=false, bool $nullAsEmpty=false,
53
								?string $textNodeKey='value') {
54
		$this->setStatus(Http::STATUS_OK);
55
		$this->addHeader('Content-Type', 'application/xml');
56
57
		// The content must have exactly one root element, add one if necessary
58
		if (\count($content) != 1) {
59
			$content = ['root' => $content];
60
		}
61
		$this->content = $content;
62
		$this->doc = new \DOMDocument('1.0', 'UTF-8');
63
		$this->doc->formatOutput = true;
64
		$this->attributeKeys = $attributes;
65
		$this->boolAsInt = $boolAsInt;
66
		$this->nullAsEmpty = $nullAsEmpty;
67
		$this->textNodeKey = $textNodeKey;
68
	}
69
70
	/**
71
	 * @return string
72
	 */
73
	public function render() {
74
		$rootName = (string)\array_keys($this->content)[0];
75
		$rootElem = $this->doc->createElement($rootName);
76
		$this->doc->appendChild($rootElem);
77
78
		foreach ($this->content[$rootName] as $childKey => $childValue) {
79
			$this->addChildElement($rootElem, $childKey, $childValue);
80
		}
81
82
		return $this->doc->saveXML();
83
	}
84
85
	/**
86
	 * Add child element or attribute to a given element. In case the value of the child is an array,
87
	 * all the nested children will be added recursively.
88
	 * @param string|int|float|bool|array|\stdClass|null $value
89
	 */
90
	private function addChildElement(\DOMElement $parentElem, string $key, /*mixed*/ $value, bool $allowAttribute=true) : void {
91
		if (\is_bool($value)) {
92
			if ($this->boolAsInt) {
93
				$value = $value ? '1' : '0';
94
			} else {
95
				$value = $value ? 'true' : 'false';
96
			}
97
		} elseif (\is_numeric($value)) {
98
			$value = (string)$value;
99
		} elseif ($value === null && $this->nullAsEmpty) {
100
			$value = '';
101
		}
102
103
		if (\is_string($value)) {
104
			if ($key == $this->textNodeKey) {
105
				$parentElem->appendChild($this->doc->createTextNode($value));
106
			} elseif ($allowAttribute && $this->keyMayDefineAttribute($key)) {
107
				$parentElem->setAttribute($key, $value);
108
			} else {
109
				$child = $this->doc->createElement($key);
110
				$child->appendChild($this->doc->createTextNode($value));
111
				$parentElem->appendChild($child);
112
			}
113
		} elseif (\is_array($value)) {
114
			if (self::arrayIsIndexed($value)) {
115
				foreach ($value as $child) {
116
					$this->addChildElement($parentElem, $key, $child, /*allowAttribute=*/false);
117
				}
118
			} else { // associative array
119
				$element = $this->doc->createElement($key);
120
				$parentElem->appendChild($element);
121
				foreach ($value as $childKey => $childValue) {
122
					$this->addChildElement($element, (string)$childKey, $childValue);
123
				}
124
			}
125
		} elseif ($value instanceof \stdClass) {
126
			// empty element
127
			$element = $this->doc->createElement($key);
128
			$parentElem->appendChild($element);
129
		} elseif ($value === null) {
0 ignored issues
show
The condition $value === null is always true.
Loading history...
130
			// skip
131
		} else {
132
			throw new \Exception("Unexpected value type for key $key");
133
		}
134
	}
135
136
	private function keyMayDefineAttribute(string $key) : bool {
137
		if (\is_array($this->attributeKeys)) {
138
			return \in_array($key, $this->attributeKeys);
139
		} else {
140
			return \boolval($this->attributeKeys);
141
		}
142
	}
143
144
	/**
145
	 * Array is considered to be "indexed" if its first element has numerical key.
146
	 * Empty array is considered to be "indexed".
147
	 */
148
	private static function arrayIsIndexed(array $array) : bool {
149
		\reset($array);
150
		return empty($array) || \is_int(\key($array));
151
	}
152
}
153