<?php
/*
 * e107 website system
 *
 * Copyright (C) 2008-2011 e107 Inc (e107.org)
 * Released under the terms and conditions of the
 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
 *
 * Simple XML Parser
 *
 * $URL $
 * $Id$
*/

if (!defined('e107_INIT')) { exit; }


/**
 *
 */
class parseXml extends xmlClass // BC with v1.x
{
	private $xmlData = array();
	private $counterArray = array();

	
	function __construct()
	{
		$data = debug_backtrace(true);
		$log = e107::getLog();
		$log->addDebug('Deprecated XML Parser Used');
		
		$log->addArray($data);
		$log->save('DEPRECATED',E_LOG_NOTICE,'',false, LOG_TO_ROLLING);
		
	}

	/**
	 * @param $feed
	 * @return void
	 */
	function setUrl($feed)
	{
		$this->setFeedUrl($feed);
	}

	/**
	 * @param $address
	 * @param $timeout
	 * @return false|string
	 */
	function getRemoteXmlFile($address, $timeout = 10)
	{	
	//	$data = $this->getRemoteFile($address, $timeout);	
		$fl = e107::getFile();
		$data = $fl->getRemoteContent($address, ['timeout' => $timeout]);

		$this->xmlLegacyContents = $data;

		return $data;	
	}

	/**
	 * @return array|false
	 */
	function parseXmlContents ()
	{
		$log = e107::getLog();
		
		foreach($this -> xmlData as $key => $value)
		{
			unset($this -> xmlData[$key]);
		}
		foreach($this -> counterArray as $key => $value)
		{
			unset($this -> counterArray[$key]);
		}

		if(!function_exists('xml_parser_create'))
		{
			$log->addDebug("No XML source specified")->save('XML',E_LOG_WARNING);
			return FALSE;
		}

		if(!$this -> xmlLegacyContents)
        {
            
			$log->addDebug("No XML source specified")->save('XML');
            return FALSE;
        }

		$this->parser = xml_parser_create('');

		xml_set_object($this->parser, $this);
		xml_set_element_handler($this->parser, 'startElement', 'endElement');
		xml_set_character_data_handler( $this->parser, 'characterData' );

		$array = explode("\n", $this -> xmlLegacyContents);


		foreach($array as $data)
		{

			if(strlen($data) == 4096)
			{
				$log->addDebug("The XML cannot be parsed as it is badly formed.")->save('XML');
				return FALSE;
			}

            if (!xml_parse($this->parser, $data))
            {
				$error = sprintf('XML error: %s at line %d, column %d', xml_error_string(xml_get_error_code($this->parser)), xml_get_current_line_number($this->parser),xml_get_current_column_number($this->parser));
				$log->addDebug($error)->save('XML');
				if(e_DEBUG === true)
				{
					$error .= "\n".$data;
					$error .= "\n--------------------------------------------\n\n";
					$log->addDebug($error)->toFile('xmlErrors',"XML Error Log",true);
				}
				return FALSE;
            }
        }
		xml_parser_free( $this->parser );
		return $this -> xmlData;
	}


	/**
	 * @param $p
	 * @param $element
	 * @param $attrs
	 * @return void
	 */
	function startElement ($p, $element, &$attrs)
	{
		$this -> start_tag = $element;
		$this -> current_tag = strtolower($element);
		if(!array_key_exists($this -> current_tag, $this -> counterArray))
		{
			$this -> counterArray[$this -> current_tag] = 0;
			$this -> xmlData[$this -> current_tag][$this -> counterArray[$this -> current_tag]] = "";
		}
	}

	/**
	 * @param $p
	 * @param $element
	 * @return void
	 */
	function endElement ($p, $element)
	{
		if($this -> start_tag == $element)
		{
			$this -> counterArray[$this -> current_tag] ++;
		}
	}

	/**
	 * @param $p
	 * @param $data
	 * @return void
	 */
	function characterData ($p, $data)
	{
		$data = trim ( rtrim ( $data ));
		$data = preg_replace('/&(?!amp;)/', '&amp;', $data);
		if(!array_key_exists($this -> current_tag, $this -> xmlData))
		{
			$this -> xmlData [$this -> current_tag] = array();
		}
		if(array_key_exists($this -> counterArray[$this -> current_tag], $this -> xmlData [$this -> current_tag]))
		{
			$this -> xmlData [$this -> current_tag] [$this -> counterArray[$this -> current_tag]] .= $data;
		}
		else
		{
			$this -> xmlData [$this -> current_tag] [$this -> counterArray[$this -> current_tag]] = $data;
		}
	}
	
	
}







/**
 * Simple XML Parser
 *
 * @package e107
 * @category e107_handlers
 * @version 1.1
 * @author McFly
 * @copyright Copyright (c) 2009, e107 Inc.
 */
class xmlClass
{

	/**
	 * Loaded XML string
	 *
	 * @var string
	 */
	public $xmlFileContents = '';


	/**
	 * Set to FALSE if not enabled (default on initialisation)
	 * Otherwise mirrors the required subset of the loaded XML - set a field FALSE to accept all
	 * ...elements lower down the tree. e.g.:
	 * <code>
	 * $filter = array(
	 * 	'name' => FALSE,
	 * 	'administration' => FALSE,
	 * 	'management' => array('install' => FALSE)
	 * 	);
	 * </code>
	 *
	 * @see setOptFilter()
	 * @see parseXml()
	 * @see xml2array()
	 * @var mixed
	 */
	public $filter = false; // Optional filter for loaded XML

	/**
	 * Set true to strip all mention of comments from the returned array (default);
	 * FALSE to return comment markers (object SimpleXMLElement)
	 *
	 * @see setOptStripComments()
	 * @see parseXml()
	 * @see xml2array()
	 * @var boolean
	 */
	public $stripComments = true;

	/**
	 * Log of all paths replaced.
	 *
	 * @var array
	 */
	public $fileConvertLog = array();

	public $convertFilePaths = FALSE;

	public $modifiedPrefsOnly = false;

	public $filePathDestination = FALSE;

	public $convertFileTypes = array("jpg", "gif", "png", "jpeg");

	public $filePathPrepend = array();

	public $filePathConvKeys = array();

	public $errors;

	private $arrayTags;

	private $stringTags;

	private $urlPrefix = false;

	/**
	 * Add root element to the result array
	 * Exmple:
	 * <code>
	 * <root>
	 * <tag>some value</tag>
	 * </root>
	 * </code>
	 *
	 * if
	 * <code>$_optAddRoot = true;</code>
	 * xml2array() result is array('root' => array('tag' => 'some value'));
	 *
	 * if
	 * <code>$_optAddRoot = false;</code>
	 * xml2array() result is array('tag' => 'some value');
	 *
	 * @see xml2array()
	 * @see setOptAddRoot()
	 * @var boolean
	 */
	protected $_optAddRoot = false;

	/**
	 * Always return array, even for single first level tag => value pair
	 * Exmple:
	 * <code>
	 * <root>
	 * <tag>some value</tag>
	 * </root>
	 * </code>
	 *
	 * if
	 * <code>$_optForceArray = true;</code>
	 * xml2array() result is array('tag' => array('value' => 'some value'));
	 * where 'value' is the value of $_optValueKey
	 *
	 * If
	 * <code>$_optForceArray = false;</code>
	 * xml2array() result is array('tag' => 'some value');
	 *
	 * @see xml2array()
	 * @see setOptForceArray()
	 * @var boolean
	 */
	protected $_optForceArray = false;

	/**
	 * Key name for simple tag => value pairs
	 *
	 * @see xml2array()
	 * @see setOptValueKey()
	 * @var string
	 */
	protected $_optValueKey = '@value';


	protected $_feedUrl = FALSE;



	/**
	 * Constructor - set defaults
	 *
	 *//*
	function __construct()
	{
		$this->reset();

		if(count($this->filePathConversions))
		{
			$this->filePathConvKeys = array_keys($this->filePathConversions);
		}
	}*/

	/**
	 * Reset object
	 *
	 * @param boolean $xml_contents [optional]
	 * @return xmlClass
	 */
	function reset($xml_contents = true)
	{
		if($xml_contents)
		{
			$this->xmlFileContents = '';
		}
		$this->filter = false;
		$this->stripComments = true;
		$this->_optAddRoot = false;
		$this->_optValueKey = '@value';
		$this->_optForceArray = false;
		return $this;
	}

	/**
	 * Set addRoot option
	 *
	 * @param boolean $flag
	 * @return xmlClass
	 */
	public function setOptAddRoot($flag)
	{
		$this->_optAddRoot = (boolean) $flag;
		return $this;
	}

	/**
	 * Set Xml tags that should always return arrays.
	 *
	 * @param string $string (comma separated)
	 * @return xmlClass
	 */
	public function setOptArrayTags($string)
	{
		$this->arrayTags = (array) explode(",", $string);
		return $this;
	}

	/**
	 * @param $string
	 * @return $this
	 */
	public function setOptStringTags($string)
	{
		$this->stringTags = (array) explode(",", $string); 
		return $this;
	}

	/**
	 * Set forceArray option
	 *
	 * @param boolean $flag
	 * @return xmlClass
	 */
	public function setOptForceArray($flag)
	{
		$this->_optForceArray = (boolean) $flag;
		return $this;
	}

	/**
	 * Set valueKey option
	 *
	 * @param string $str
	 * @return xmlClass
	 */
	public function setOptValueKey($str)
	{
		$this->_optValueKey = trim((string) $str);
		return $this;
	}

	/**
	 * Set strpComments option
	 *
	 * @param boolean $flag
	 * @return xmlClass
	 */
	public function setOptStripComments($flag)
	{
		$this->stripComments = (boolean) $flag;
		return $this;
	}

	/**
	 * Set strpComments option
	 *
	 * @param array $filter
	 * @return xmlClass
	 */
	public function setOptFilter($filter)
	{
		$this->filter = (array) $filter;
		return $this;
	}
	

	/**
	 * Set urlPrefix
	 *
	 * @param string $url
	 * @return xmlClass
	 */
	public function setUrlPrefix($url)
	{
		$this->urlPrefix = $url;
		return $this;
	}


	/**
	 * @param $feed
	 * @return $this
	 */
	public function setFeedUrl($feed)
	{
		if($feed)
		{
			$this->_feedUrl = $feed;
		}
		return $this;	
	}

	/**
	 * Get Remote XML file contents
	 * use setOptArrayTags above if you require a consistent array result by in 1 item or many. 
	 * @param string $address
	 * @param integer $timeout [optional] seconds
	 * @return string
	 */
	function getRemoteFile($address, $timeout = 10, $postData=null)
	{		
		$_file = e107::getFile();
		$this->xmlFileContents = $_file->getRemoteContent($address, array('timeout' => $timeout, 'post' => $postData));
		$this->errors = $_file->getErrorMessage();
		
		return $this->xmlFileContents;

	}

	/**
	 * Parse $xmlFileContents XML string to array
	 *
	 * @param string $xmlData [optional]
	 * @param boolean $simple [optional] false - use xml2array(), true - use xml_convert_to_array()
	 * @return array|string
	 */
	function parseXml($xmlData = '', $simple = true)
	{
		if ($xmlData)
		{
			$this->xmlFileContents = $xmlData;
		}
		elseif ($this->xmlFileContents)
		{
			$xmlData = $this->xmlFileContents;
		}
		if (!$xmlData)
		{
			return FALSE;
		}

		$extendedTypes = array(
			'content:encoded'   => 'content_encoded',
			'<media:'     => '<media_',
			'</media:'    => '</media_',
			'<opensearch:'  => '<opensearch_',
			'</opensearch:' => '</opensearch_'
		);

		$xmlData = str_replace(array_keys($extendedTypes), array_values($extendedTypes), $xmlData);

		if(strpos($xmlData,'<html lang=')!==false)
		{
			$this->errors = "HTML cannot be parsed as XML";
			return false;
		}


		if(!$xml = simplexml_load_string($xmlData, 'SimpleXMLElement', LIBXML_NOCDATA))
		{
			$this->errors = $this->getErrors($xmlData);
			return FALSE;
		}

		$xml = $simple ? $this->xml_convert_to_array($xml, $this->filter, $this->stripComments) : $this->xml2array($xml);
		return $xml;
	}

	/**
	 * Advanced XML parser - handles tags with attributes and values
	 * properly.
	 * TODO - filter (see xml_convert_to_array)
	 * FIXME can't handle multi-dimensional associative arrays (e.g. <screnshots><image>...</image><image>...</image></screenshots> to screenshots[image] = array(...))
	 * XXX New parser in testing phase - see e_marketplace::parse()
	 * @param SimpleXMLElement $xml
	 * @param string $rec_parent used for recursive calls only
	 * @return array|string
	 */
	function xml2array($xml, $rec_parent = '')
	{
		
		$ret = array();
	
		$tags = (array) $xml;
	
		
		//remove comments
		if($this->stripComments && isset($tags['comment']))
		{
			unset($tags['comment']);
		}

		//first call
		if(!$rec_parent)
		{
			//$ret = $this->xml2array($xml, true);
			//repeating code because of the _optForceArray functionality

			if(!is_object($xml))
			{
				return array();
			}

			$tags = array_keys($tags);
			foreach ($tags as $tag)
			{
				if($tag === '@attributes')
				{
					$tmp = (array) $xml->attributes();
					$ret['@attributes'] = $tmp['@attributes'];
					continue;
				}

				$count = count($xml->{$tag});
				if($count > 1)
				{
					for ($i = 0; $i < $count; $i++)
					{
						$ret[$tag][$i] = $this->xml2array($xml->{$tag}[$i], $tag);
					}
					continue;
				}
				$ret[$tag] = $this->xml2array($xml->{$tag}, $tag);
			}

			$ret = $this->parseArrayTags($ret);
			$ret = $this->parseStringTags($ret);

			return ($this->_optAddRoot ? array($xml->getName() => $ret) : $ret);
		}

		//Recursive calls start here
		if(self::is_assoc($tags))
		{
			$tags = array_keys($tags);
			$count_tags = count($tags);

			//loop through tags
			foreach ($tags as $tag)
			{
				if(is_int($tag)) continue;
				switch($tag)
				{
					case '@attributes':
						$tmp = (array) $xml->attributes();
						$ret['@attributes'] = $tmp['@attributes'];

						if($count_tags == 1 || ['@attributes', 0] === $tags) //only attributes & possible value
						{
							$ret[$this->_optValueKey] = trim((string) $xml);
							//return $ret;
						}
					break;

					case 'comment':
						$ret[$this->_optValueKey] = trim((string) $xml);
						$ret['comment'] = $xml->comment;
					break;

					//more cases?
					default:
						
//FIXME - commented code breaks parsing of plugin.xml extended and userclass tags and possibly other xml files. 
						
	/*
						// fix - empty SimpleXMLElement
						if(empty($xml->{$tag}))
						{
							if($this->arrayTags && in_array($tag, $this->arrayTags))
							{
								$ret[$tag] = array();
							}
							else $ret[$tag] = '';
							break;
						}
	*/					
						$count = count($xml->{$tag}); 
						if($count >= 1) //array of elements - loop
						{
							for ($i = 0; $i < $count; $i++)
							{
								$ret[$tag][$i] = $this->xml2array($xml->{$tag}[$i], $tag);
								$ret[$tag][$i] = $this->parseStringTags($ret[$tag][$i]);

							}
						}
						else //single element
						{
							$ret[$tag] = $this->xml2array($xml->{$tag}, $tag);
							$ret[$tag] = $this->parseStringTags($ret[$tag]);
						}
					break;
				}
			}
			$ret = $this->parseStringTags($ret);
			return $ret;
		}

		//parse value only
		$ret = trim((string) $xml);

		return ($this->_optForceArray ? array($this->_optValueKey => $ret) : $ret);
	}

	// OLD

	/**
	 * @param $xml
	 * @param $localFilter
	 * @param $stripComments
	 * @return array
	 */
	function xml_convert_to_array($xml, $localFilter = FALSE, $stripComments = TRUE)
	{
		if (is_object($xml))
		{
			$xml = (array) $xml;
		}
		if (is_array($xml))
		{
			foreach ($xml as $k=>$v)
			{
				if ($stripComments && ($k === 'comment'))
				{
					unset($xml[$k]);
					continue;
				}
				$enabled = FALSE;
				if ($localFilter === FALSE)
				{
					$enabled = TRUE;
					$onFilter = FALSE;
				}
				elseif (isset($localFilter[$k]))
				{
					$enabled = TRUE;
					$onFilter = $localFilter[$k];
				}
				if ($enabled)
				{
					if (is_object($v))
					{
						$v = (array) $v;
					}
					$xml[$k] = $this->xml_convert_to_array($v, $onFilter, $stripComments);
				}
				else
				{
					unset($xml[$k]);
				}
			}
			if (count($xml) == 1 && isset($xml[0]))
			{
				$xml = $xml[0];

			}
		}

		$xml = $this->parseArrayTags($xml);
	//	$xml = $this->parseStringTags($xml);

		return $xml;
	}


	/**
	 * Convert Array(0) to String based on specified Tags.
	 *
	 * @param array|string $vars
	 * @return string
	 */
	function parseStringTags($vars)
	{
		if(!$this->stringTags || !is_array($vars))
		{
			return $vars;
		}

		foreach($this->stringTags as $vl)
		{
			if(isset($vars[$vl]) && is_array($vars[$vl]) && isset($vars[$vl][0]))
			{
				$vars[$vl] = $vars[$vl][0];
			}
		}

		return $vars;
	}

	/**
	 * Return as an array, even when a single xml tag value is found
	 * Use setArrayTags() to set which tags are affected.
	 *
	 * @param array $vars
	 * @return array
	 */
	private function parseArrayTags($vars)
	{

		if(!$this->arrayTags)
		{
			return $vars;
		}
			
		foreach($this->arrayTags as $p)
		{
			if(strpos($p,"/")!==false)
			{
				list($vl,$sub) = explode("/",$p);	
			}
			else
			{
				$vl = $p;
				$sub = false;	
			}
			
			if($sub)
			{
				if(isset($vars[$vl][$sub]) && is_string($vars[$vl][$sub]))
				{
					$vars[$vl][$sub] = array($vars[$vl][$sub]);	
				}
				
				continue;	
			}

			if(isset($vars[$vl]) && is_array($vars[$vl]) && !varset($vars[$vl][0]))
			{
				
				$vars[$vl] = array($vars[$vl]);		
			}
		}
		
		return $vars;
	}

	/**
	 * Determine if the provided variable is an associative array
	 *
	 * This method is necessary because since PHP 7.2, get_object_vars() on
	 * a SimpleXMLElement object began returning sequential arrays, and
	 * xmlClass::xml2array() interpreted the sequence as XML tags.
	 *
	 * See https://github.com/e107inc/e107/issues/3018 for details.
	 *
	 * @param array $array The variable to check
	 * @return boolean true if the provided variable is an associative array,
	 *                 false if it's a sequential array or anything else
	 */
	private static function is_assoc($array)
	{
		if (!is_array($array) || array() === $array) return false;
		return array_keys($array) !== range(0, count($array) - 1);
	}

	/**
	 * Load XML file and parse it (optional)
	 *
	 * @param string $fname local or remote XML source file path
	 * @param boolean|string $parse false - no parse;
	 * 								true - use  xml_convert_to_array();
	 * 								in any other case  - use xml2array()
	 *
	 * @param boolean $replace_constants [optional]
	 * @return false|string
	 */
	function loadXMLfile($fname, $parse = false, $replace_constants = false)
	{
		$tp = e107::getParser();
	
		if($this->_feedUrl !== false)
		{
			$fname = $this->_feedUrl;	
		}
	
		if (empty($fname))
		{
			return false;
		}
		
		$xml = false;
		
		if (strpos($fname, '://') !== false)
		{
			$this->getRemoteFile($fname);
			$this->_feedUrl = false; // clear it to avoid conflicts. 
		}
		else
		{
			if ($xml = file_get_contents($fname))
			{
				$this->xmlFileContents = $xml;
			}
		}
		if ($this->xmlFileContents)
		{
	
			
			if ($replace_constants == true)
			{
				$this->xmlFileContents = $tp->replaceConstants($this->xmlFileContents, '', true);
			}
			if ($parse)
			{
				return $this->parseXml('', ($parse === true));
			}
			else
			{
				return $this->xmlFileContents;
			}
		}
		return false;
	}

	/**
	 * Convert file path for inclusion in XML file.

	 * @see e107ExportValue()
	 * @param string $text - callback function
	 * @return string converted file path
	 */
	private function replaceFilePaths($text)
	{
		$fullpath = e107::getParser()->replaceConstants($text[1]);
		$this->fileConvertLog[] = $fullpath;
		$file = basename($fullpath);

		return $this->filePathDestination.$file;

	}


	/**
	 * Process data values for XML file. If $this->convertFilePaths is TRUE, convert paths
	 *
	 * @see replaceFilePaths()
	 * @param mixed $val
	 * @param string $key key for the current value. Used for exception processing.
	 * @return mixed
	 */
	public function e107ExportValue($val, $key = '')
	{
		if($key && isset($this->filePathPrepend[$key]))
		{
			$val = $this->filePathPrepend[$key].$val;
		}

		if(is_array($val))
		{
			$val = e107::serialize($val);

			if($val === null)
			{
				return '<![CDATA[array ()]]>';
			}
		}

		if($this->convertFilePaths)
		{
			$types = implode("|",$this->convertFileTypes);
			$val = preg_replace_callback("#({e_.*?\.(".$types."))#i", array($this,'replaceFilePaths'), $val);
		}

		if((strpos($val,"<")!==FALSE) || (strpos($val,">")!==FALSE) || (strpos($val,"&")!==FALSE))
		{
			return "<![CDATA[". $val."]]>";
		}

		$val = str_replace(chr(1),'{\u0001}',$val);

		return $val;
	}

	/**
	 * Create an e107 Export File in XML format
	 * Note: If $this->filePathDestination has a value, then the file will be saved there.
	 *
	 * @param array $xmlprefs - see e_core_pref $aliases (eg. core, ipool etc)
	 * @param array $tables - table names without the prefix
	 * @param array|null $plugPrefs
	 * @param array|null $themePrefs
	 * @param array $options [optional] debug, return, query
	 * @return string text / file for download
	 */
	public function e107Export($xmlprefs, $tables, $plugPrefs=null, $themePrefs=null, $options = array())
	{
	//	error_reporting(0);
	//	$e107info = array();
	//	require_once(e_ADMIN."ver.php");

		$text = "<?xml version='1.0' encoding='utf-8' ?".">\n";
		$text .= "<e107Export version=\"".e_VERSION."\" timestamp=\"".time()."\" >\n";

		$default = array();
		$excludes = array();

		if($this->modifiedPrefsOnly == true)
		{
			$xmlArray = e107::getSingleton('xmlClass')->loadXMLfile(e_CORE."xml/default_install.xml",'advanced');
			$default = e107::getSingleton('xmlClass')->e107ImportPrefs($xmlArray,'core');
			$excludes = array('social_login','replyto_email','replyto_name','siteadminemail','lan_global_list','menuconfig_list','plug_installed','shortcode_legacy_list','siteurl','cookie_name','install_date', 'wysiwyg');
		}

		if(varset($xmlprefs)) // Export Core Preferences.
		{
			$text .= "\t<prefs>\n";
			foreach($xmlprefs as $type)
			{
				$theprefs = e107::getConfig($type)->getPref();
				ksort($theprefs);
				foreach($theprefs as $key=>$val)
				{
					if($type === 'core' && $this->modifiedPrefsOnly == true && (($val == $default[$key]) || in_array($key,$excludes) || strpos($key, 'e_') === 0))
					{
						continue;
					}
					elseif(!empty($options['debug']))
					{
						echo "<div>Original/Modiied <b>".$key."</b>";
						var_dump($default[$key],$val);
						echo "</div>";

					}

					if(isset($val))
					{
						$text .= "\t\t<".$type." name=\"".$key."\">".$this->e107ExportValue($val)."</".$type.">\n";
					}
				}
			}
			$text .= "\t</prefs>\n";
		}


		if(!empty($plugPrefs))
		{
			$text .= "\t<pluginPrefs>\n";

			foreach($plugPrefs as $plug)
			{
				$prefs = e107::getPlugConfig($plug)->getPref();

				foreach($prefs as $key=>$val)
				{
					if(isset($val))
					{
						$text .= "\t\t<".$plug." name=\"".$key."\">".$this->e107ExportValue($val)."</".$plug.">\n";
					}

				}

			}

			$text .= "\t</pluginPrefs>\n";
		}

		if(!empty($themePrefs))
		{
			$text .= "\t<themePrefs>\n";

			foreach($themePrefs as $plug)
			{
				$prefs = e107::getThemeConfig($plug)->getPref();

				foreach($prefs as $key=>$val)
				{
					if(isset($val))
					{
						$text .= "\t\t<".$plug." name=\"".$key."\">".$this->e107ExportValue($val)."</".$plug.">\n";
					}

				}

			}

			$text .= "\t</themePrefs>\n";
		}




		if(!empty($tables))
		{
			$text .= "\t<database>\n";
			foreach($tables as $tbl)
			{
				$primaryKey = '';
				$eTable= str_replace(MPREFIX,"",$tbl);
				$eQry = (!empty($options['query'])) ? $options['query'] : null;

				// order by the primary-key
				if($pri = e107::getDb()->index($eTable, 'PRIMARY', null, true))
				{
					$primaryKey = varset($pri[0]['Column_name']);
				}

				if(empty($eQry) && !empty($primaryKey))
				{
					$eQry = " 1 ORDER BY ".$primaryKey." ASC";
				}

				$fields = !empty($options['fields']) ? $options['fields'] : '*';
				e107::getDb()->select($eTable, $fields, $eQry);
				$text .= "\t<dbTable name=\"".$eTable."\">\n";
			//	$count = 1;
				while($row = e107::getDb()->fetch())
				{

					if($this->convertFilePaths == true && $eTable === 'core_media' && strpos($row['media_url'], '{e_MEDIA') !== 0)
					{
						continue;
					}


					$text .= "\t\t<item>\n";
					foreach($row as $key=>$val)
					{
						$text .= "\t\t\t<field name=\"".$key."\">".$this->e107ExportValue($val,$key)."</field>\n";
					}

					$text .= "\t\t</item>\n";
			//		$count++;
				}
				$text .= "\t</dbTable>\n";

			}
			$text .= "\t</database>\n";
		}



		$text .= "</e107Export>";


		if(!empty($options['return']))
		{
			return $text;
		}

		if(!empty($options['debug']))
		{
			echo "<pre>".htmlentities($text)."</pre>";
			return null;
		}
		else
		{
			if(!$text)
			{
				return FALSE;
			}

			$path = e107::getParser()->replaceConstants($this->filePathDestination);
			if($path)
			{
				$fileName= "install.xml";

				if(file_exists($path.$fileName))
				{
					$fileName = "install_".date('Y-m-d').".xml";
				}

				file_put_contents($path.$fileName,$text,FILE_TEXT);
				return true;
			}

			$fileName = (!empty($options['file'])) ? $options['file'] : "e107Export_" . date("Y-m-d").".xml";

			header('Content-type: application/xml', TRUE);
			header("Content-disposition: attachment; filename= ".$fileName);
			header("Cache-Control: max-age=30");
			header("Pragma: public");
			echo $text;
			exit;

		}
	}

	/**
	 * Return an Array of core preferences from e107 XML Dump data
	 *
	 * @param array $XMLData Raw XML e107 Export Data
	 * @param string $prefType [optional] the type of core pref: core|emote|ipool|menu etc or plugin-folder name
	 * @param string $mode core|plugin
	 * @return array preference array equivalent to the old $pref global;
	 */
	public function e107ImportPrefs($XMLData, $prefType='core', $mode='core')
	{

		switch($mode)
		{
			case "plugin":
				$key = 'pluginPrefs';
				break;

			case "theme":
				$key = 'themePrefs';
				break;

			case "core":
			default:
				$key = 'prefs';
		}

	//	$key = ($mode === 'core') ? 'prefs' : 'pluginPrefs';

		if(!vartrue($XMLData[$key][$prefType]))
		{
			return array();
		}

		//$mes = eMessage::getInstance();

		$pref = array();
		foreach($XMLData[$key][$prefType] as $val)
		{
			$name = $val['@attributes']['name'];
			// if(strpos($val['@value'], 'array (') === 0)
			// {
				// echo '<pre>'.$val['@value'];
				// echo "\n";
				// var_dump(e107::getArrayStorage()->ReadArray($val['@value']));
				// echo $val['@value'].'</pre>';
			// }
			$value = strpos($val['@value'], 'array (') === 0 ? e107::unserialize($val['@value']) : $val['@value'];
			$pref[$name] = $value;

			// $mes->add("Setting up ".$prefType." Pref [".$name."] => ".$value, E_MESSAGE_DEBUG);
		}


		return $pref;
	}

	/**
	 * Import an e107 XML file into site preferences and DB tables
	 *
	 * @param string $file - e107 XML file path
	 * @param string $mode[optional] - add|replace
	 * @param boolean $noLogs [optional] tells pref handler to disable admin logs when true (install issues)
	 * @param boolean $debug [optional]
	 * @return array with keys 'success' and 'failed' - DB table entry status.
	 */
	public function e107Import($file, $mode='replace', $noLogs = false, $debug=FALSE, $sql = null)
	{

		if($sql == null)
		{
			$sql = e107::getDb();
		}


		$xmlArray = $this->loadXMLfile($file, 'advanced');

		if($debug)
		{
			//$message = print_r($xmlArray);
			echo "<pre>".var_export($xmlArray,TRUE)."</pre>";
			return null;
		}

		$ret = array();

		// ----------------- Save Core Prefs ---------------------

		if(!empty($xmlArray['prefs']))
		{
			foreach($xmlArray['prefs'] as $type=>$array)
			{
				
				$pArray = $this->e107ImportPrefs($xmlArray,$type);
				
				if($mode == 'replace') // merge with existing, add new
				{
					e107::getConfig($type)->setPref($pArray);
				}
				else // 'add' only new prefs
				{
					foreach ($pArray as $pname => $pval)
					{
						e107::getConfig($type)->add($pname, $pval); // don't parse x/y/z
					}
				}

				if($debug == FALSE)
				{
					 e107::getConfig($type)
					 	->setParam('nologs', $noLogs)
					 	->save(FALSE,TRUE);
				}
			}
		}


		 // ---------------   Save Plugin Prefs  ---------------------

		if(!empty($xmlArray['pluginPrefs']))
		{
			foreach($xmlArray['pluginPrefs'] as $type=>$array)
			{

				$pArray = $this->e107ImportPrefs($xmlArray,$type, 'plugin');

				if($mode == 'replace') // merge with existing, add new
				{
					e107::getPlugConfig($type)->setPref($pArray);
				}
				elseif($mode === 'add') // 'add' only new prefs
				{
					foreach ($pArray as $pname => $pval)
					{
						e107::getPlugConfig($type)->add($pname, $pval); // don't parse x/y/z
					}
				}

				if($debug == false)
				{
					 e107::getPlugConfig($type)
					 	->setParam('nologs', $noLogs)
					 	->save(FALSE,TRUE);
				}
			}
		}

		 // ---------------   Save Theme Prefs  ---------------------

		if(!empty($xmlArray['themePrefs']))
		{
			foreach($xmlArray['themePrefs'] as $type=>$array)
			{

				$pArray = $this->e107ImportPrefs($xmlArray,$type, 'theme');

				if($mode == 'replace') // merge with existing, add new
				{
					e107::getThemeConfig($type)->setPref($pArray);
				}
				elseif($mode === 'add') // 'add' only new prefs
				{
					foreach ($pArray as $pname => $pval)
					{
						e107::getThemeConfig($type)->add($pname, $pval); // don't parse x/y/z
					}
				}

				if($debug == false)
				{
					 e107::getThemeConfig($type)
					 	->setParam('nologs', $noLogs)
					 	->save(FALSE,TRUE);
				}
			}
		}



		if(!empty($xmlArray['database']))
		{

			foreach($xmlArray['database']['dbTable'] as $val)
			{
				$table = $val['@attributes']['name'];

				if(!isset($val['item']))
				{
					continue;
				}

				if($mode === 'install' && !$sql->isEmpty($table)) // install mode - ignore import when table contains data.
				{
					continue;
				}

				foreach($val['item'] as $item)
				{
					$insert_array = array();
					foreach($item['field'] as $f)
					{
						$fieldkey = $f['@attributes']['name'];
						$fieldval = (isset($f['@value'])) ? $this->e107ImportValue($f['@value']) : "";

						$insert_array[$fieldkey] = $fieldval;

					}
					if(($mode === "replace") && $sql->replace($table, $insert_array)!==false)
					{
						$ret['success'][] = $table;
					}
					elseif($sql->insert($table, $insert_array)!==false)
					{
						$ret['success'][] = $table;
					}
					else
					{
						$error = $sql->getLastErrorText();
						$lastQry = $sql->getLastQuery();

						if(is_array($lastQry))
						{
							$lastQry = $lastQry['PREPARE'];
						}

						$ret['failed'][] = $table. "\n[".$error."]\n".$lastQry."\n\n";
					}
				}
			}
		}

		return $ret;
	}


	/**
	 * @param $val
	 * @return array|string|string[]
	 */
	function e107ImportValue($val)
	{
		$val = str_replace('{\u0001}', chr(1), $val);

		return $val;
	}


	/**
	 * @param $xml
	 * @return array|false
	 */
	function getErrors($xml)
	{
		libxml_use_internal_errors(true);
		$sxe = simplexml_load_string($xml);
		$errors = array();
		if (!$sxe)
		{
		    foreach(libxml_get_errors() as $error)
			{
		        $errors[] = $error->message. "Line:".$error->line." Column:".$error->column;
			}
			return $errors;
		}
		return FALSE;
	}


	/**
	 * @return mixed
	 */
	public function getLastErrorMessage()
	{
		return $this->errors;
	}




}



/**
 * DEPRECATED XML Class from v1.x
 */
class XMLParse
{
    var $rawXML;
    var $valueArray = array();
    var $keyArray = array();
    var $parsed = array();
    var $index = 0;
    var $attribKey = 'attributes';
    var $valueKey = 'value';
    var $cdataKey = 'cdata';
    var $isError = false;
    var $error = '';

	/**
	 * @param $xml
	 */
	function __construct($xml = NULL)
    {
        $this->rawXML = $xml;
		$mes = e107::getMessage();
		$mes->addDebug("Deprecated class XMLParse used. Please use 'xmlClass' instead");
    }

	/**
	 * @param $xml
	 * @return array|false
	 */
	function parse($xml = NULL)
    {
        if (!is_null($xml))
        {
            $this->rawXML = $xml;
        }

        $this->isError = false;

        if (!$this->parse_init())
        {
            return false;
        }

        $this->index = 0;
        $this->parsed = $this->parse_recurse();
        $this->status = 'parsing complete';

        return $this->parsed;
    }

	/**
	 * @return array
	 */
	function parse_recurse()
    {
        $found = array();
        $tagCount = array();

        while (isset($this->valueArray[$this->index]))
        {
            $tag = $this->valueArray[$this->index];
            $this->index++;

            if ($tag['type'] == 'close')
            {
                return $found;
            }

            if ($tag['type'] == 'cdata')
            {
                $tag['tag'] = $this->cdataKey;
                $tag['type'] = 'complete';
            }

            $tagName = $tag['tag'];

            if (isset($tagCount[$tagName]))
            {
                if ($tagCount[$tagName] == 1)
                {
                    $found[$tagName] = array($found[$tagName]);
                }

                $tagRef =& $found[$tagName][$tagCount[$tagName]];
                $tagCount[$tagName]++;
            }
            else
            {
                $tagCount[$tagName] = 1;
                $tagRef =& $found[$tagName];
            }

            switch ($tag['type'])
            {
                case 'open':
                    $tagRef = $this->parse_recurse();

                    if (isset($tag['attributes']))
                    {
                        $tagRef[$this->attribKey] = $tag['attributes'];
                    }

                    if (isset($tag['value']))
                    {
                        if (isset($tagRef[$this->cdataKey]))
                        {
                            $tagRef[$this->cdataKey] = (array)$tagRef[$this->cdataKey];
                            array_unshift($tagRef[$this->cdataKey], $tag['value']);
                        }
                        else
                        {
                            $tagRef[$this->cdataKey] = $tag['value'];
                        }
                    }
                    break;

                case 'complete':
                    if (isset($tag['attributes']))
                    {
                        $tagRef[$this->attribKey] = $tag['attributes'];
                        $tagRef =& $tagRef[$this->valueKey];
                    }

                    if (isset($tag['value']))
                    {
                        $tagRef = $tag['value'];
                    }
                    break;
            }
        }

        return $found;
    }

	/**
	 * @return bool
	 */
	function parse_init()
    {
        $this->parser = xml_parser_create();

        $parser = $this->parser;
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1);
        if (!$res = (bool)xml_parse_into_struct($parser, $this->rawXML, $this->valueArray, $this->keyArray))
        {
            $this->isError = true;
            $this->error = 'error: '.xml_error_string(xml_get_error_code($parser)).' at line '.xml_get_current_line_number($parser);
        }
        xml_parser_free($parser);

        return $res;
    }
}

