<?php
/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/


class CXmlImportReader extends CImportReader {

	/**
	 * Convert string with xml data to php array.
	 *
	 * @throws Exception
	 *
	 * @param string $string
	 *
	 * @return array
	 */
	public function read($string) {
		if ($string === '') {
			throw new Exception(_s('Cannot read XML: %1$s.', _('XML is empty')));
		}

		libxml_use_internal_errors(true);

		if (PHP_VERSION_ID < 80000) {
			// Deprecated and disabled by default since PHP 8.0.
			libxml_disable_entity_loader();
		}

		$result = simplexml_load_string($string, null, LIBXML_IMPORT_FLAGS);

		if (!$result) {
			$errors = libxml_get_errors();
			libxml_clear_errors();

			foreach ($errors as $error) {
				throw new Exception(_s('Cannot read XML: %1$s.', _s('%1$s [Line: %2$s | Column: %3$s]',
					'('.$error->code.') '.trim($error->message), $error->line, $error->column
				)));
			}
		}

		$xml = new XMLReader();
		$xml->xml($string);
		$data = $this->xml_to_array($xml);
		$xml->close();

		return $data;
	}

	/**
	 * Method for recursive processing of xml dom nodes.
	 *
	 * @param XMLReader $xml
	 * @param string    $path
	 *
	 * @return array|string
	 */
	protected function xml_to_array(XMLReader $xml, $path = '') {
		$data = null;

		while ($xml->read()) {
			switch ($xml->nodeType) {
				case XMLReader::ELEMENT:
					if ($data === null) {
						$data = [];
					}
					elseif (!is_array($data)) {
						throw new Exception(_s('Invalid tag "%1$s": %2$s.', $path,
							_s('unexpected text "%1$s"', trim($data))
						));
					}

					$node_name = $xml->name;
					$sub_path = $path.'/'.$node_name;
					if (array_key_exists($node_name, $data)) {
						$node_name .= count($data);
						$sub_path .= '('.count($data).')';
					}

					/*
					 * A special case for 1.8 import where attributes are still used attributes must be added to the
					 * array as if they where child elements.
					 */
					if ($xml->hasAttributes) {
						while ($xml->moveToNextAttribute()) {
							$data[$node_name][$xml->name] = $xml->value;
						}

						// it makes $this->isEmptyElement valid after processing attributes
						$xml->moveToElement();

						/*
						 * We assume that an element with attributes always contains child elements, not a text node
						 * works for 1.8 XML.
						 */
						if (!$xml->isEmptyElement) {
							$child_data = $this->xml_to_array($xml, $sub_path);
							if (is_array($child_data)) {
								foreach ($child_data as $child_node_name => $child_node_value) {
									if (array_key_exists($child_node_name, $data[$node_name])) {
										$child_node_name .= count($data[$node_name]);
									}
									$data[$node_name][$child_node_name] = $child_node_value;
								}
							}
							elseif ($child_data !== '') {
								throw new Exception(_s('Invalid tag "%1$s": %2$s.', $sub_path,
									_s('unexpected text "%1$s"', trim($child_data))
								));
							}
						}
					}
					else {
						$data[$node_name] = $xml->isEmptyElement ? '' : $this->xml_to_array($xml, $sub_path);
					}
					break;

				case XMLReader::CDATA:
					// falls through
				case XMLReader::TEXT:
					if ($data === null) {
						$data = $xml->value;
					}
					elseif (is_array($data)) {
						throw new Exception(_s('Invalid tag "%1$s": %2$s.', $path,
							_s('unexpected text "%1$s"', trim($xml->value))
						));
					}

					break;

				case XMLReader::END_ELEMENT:
					/*
					 * For tags with empty value: <dns></dns>.
					 */
					if ($data === null) {
						$data = '';
					}

					return $data;
			}
		}

		return $data;
	}
}
