<?php
/**
* @version	$Id: language_import_helper.php 13901 2010-08-29 10:25:24Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

/**
* Language pack format version description
*
* v1
* ==========
* All language properties are separate nodes inside <LANGUAGE> node. There are
* two more nodes PHRASES and EVENTS for phrase and email event translations.
*
* v2
* ==========
* All data, that will end up in Language table is now attributes of LANGUAGE node
* and is name exactly as field name, that will be used to store that data.
*/

	defined('FULL_PATH') or die('restricted access!');

	define('LANG_OVERWRITE_EXISTING', 1);
	define('LANG_SKIP_EXISTING', 2);

	class LanguageImportHelper extends kHelper {

		/**
		 * Current Language in import
		 *
		 * @var LanguagesItem
		 */
		var $lang_object = null;

		/**
		 * Current user's IP address
		 *
		 * @var string
		 */
		var $ip_address = '';

		/**
		 * Event type + name mapping to id (from system)
		 *
		 * @var Array
		 */
		var $events_hash = Array ();

		/**
		 * Language pack import mode
		 *
		 * @var int
		 */
		var $import_mode = LANG_SKIP_EXISTING;

		/**
		 * Language IDs, that were imported
		 *
		 * @var Array
		 */
		var $_languages = Array ();

		/**
		 * Temporary table names to perform import on
		 *
		 * @var Array
		 */
		var $_tables = Array ();

		/**
		 * Phrase types allowed for import/export operations
		 *
		 * @var Array
		 */
		var $phrase_types_allowed = Array ();

		/**
		 * Encoding, used for language pack exporting
		 *
		 * @var string
		 */
		var $_exportEncoding = 'base64';

		/**
		 * Exported data limits (all or only specified ones)
		 *
		 * @var Array
		 */
		var $_exportLimits = Array (
			'phrases' => false,
			'emailevents' => false,
		);

		/**
		 * Debug language pack import process
		 *
		 * @var bool
		 */
		var $_debugMode = false;

		/**
		 * Latest version of language pack format. Versions are not backwards compatible!
		 *
		 * @var int
		 */
		var $_latestVersion = 3;

		/**
		 * Prefix-based serial numbers, that should be changed after import is finished
		 *
		 * @var Array
		 */
		var $changedPrefixes = Array ();

		function LanguageImportHelper()
		{
			parent::kHelper();

			// "core/install/english.lang", phrase count: 3318, xml parse time on windows: 10s, insert time: 0.058s
			set_time_limit(0);
			ini_set('memory_limit', -1);

			$this->lang_object =& $this->Application->recallObject('lang.import', null, Array ('skip_autoload' => true));

			if (!(defined('IS_INSTALL') && IS_INSTALL)) {
				// perform only, when not in installation mode
				$this->_updateEventsCache();
			}

			$this->ip_address = getenv('HTTP_X_FORWARDED_FOR') ? getenv('HTTP_X_FORWARDED_FOR') : getenv('REMOTE_ADDR');

//			$this->_debugMode = $this->Application->isDebugMode();
		}

		/**
		 * Performs import of given language pack (former Parse method)
		 *
		 * @param string $filename
		 * @param string $phrase_types
		 * @param Array $module_ids
		 * @param int $import_mode
		 * @return bool
		 */
		function performImport($filename, $phrase_types, $module_ids, $import_mode = LANG_SKIP_EXISTING)
		{
			// define the XML parsing routines/functions to call based on the handler path
			if (!file_exists($filename) || !$phrase_types /*|| !$module_ids*/) {
				return false;
			}

			if ($this->_debugMode) {
				$start_time = getmicrotime();
				$this->Application->Debugger->appendHTML(__CLASS__ . '::' . __FUNCTION__ . '("' . $filename . '")');
			}

			if (defined('IS_INSTALL') && IS_INSTALL) {
				// new events could be added during module upgrade
				$this->_updateEventsCache();
			}

			$this->_initImportTables();

			$phrase_types = explode('|', substr($phrase_types, 1, -1) );
//			$module_ids = explode('|', substr($module_ids, 1, -1) );

			$this->phrase_types_allowed = array_flip($phrase_types);
			$this->import_mode = $import_mode;

			$this->_parseXML($filename);

			// copy data from temp tables to live
			foreach ($this->_languages as $language_id) {
				$this->_performUpgrade($language_id, 'phrases', 'PhraseKey', Array ('l%s_Translation'));
				$this->_performUpgrade($language_id, 'emailevents', 'EventId', Array ('l%s_Subject', 'Headers', 'MessageType', 'l%s_Body'));
				$this->_performUpgrade($language_id, 'country-state', 'CountryStateId', Array ('l%s_Name'));
			}

			$this->_initImportTables(true);
			$this->changedPrefixes = array_unique($this->changedPrefixes);

			foreach ($this->changedPrefixes as $prefix) {
				$this->Application->incrementCacheSerial($prefix);
			}

			if ($this->_debugMode) {
				$this->Application->Debugger->appendHTML(__CLASS__ . '::' . __FUNCTION__ . '("' . $filename . '"): ' . (getmicrotime() - $start_time));
			}

			return true;
		}

		/**
		 * Creates XML file with exported language data (former Create method)
		 *
		 * @param string $filename filename to export into
		 * @param Array $phrase_types phrases types to export from modules passed in $module_ids
		 * @param Array $language_ids IDs of languages to export
		 * @param Array $module_ids IDs of modules to export phrases from
		 */
		function performExport($filename, $phrase_types, $language_ids, $module_ids)
		{
			$fp = fopen($filename,'w');
			if (!$fp || !$phrase_types || !$module_ids || !$language_ids) {
				return false;
			}

			$phrase_types = explode('|', substr($phrase_types, 1, -1) );
			$module_ids = explode('|', substr($module_ids, 1, -1) );

			$ret = '<LANGUAGES Version="' . $this->_latestVersion . '">' . "\n";

			$export_fields = $this->_getExportFields();

			$email_message_helper =& $this->Application->recallObject('EmailMessageHelper');
			/* @var $email_message_helper EmailMessageHelper */

			// get languages
			$sql = 'SELECT *
					FROM ' . $this->Application->getUnitOption('lang','TableName') . '
					WHERE LanguageId IN (' . implode(',', $language_ids) . ')';
			$languages = $this->Conn->Query($sql, 'LanguageId');

			// get phrases
			$phrase_modules = $module_ids;
			array_push($phrase_modules, ''); // for old language packs without module

			$phrase_modules = array_map(Array (&$this->Conn, 'qstr'), $phrase_modules);

			// apply phrase selection limit
			if ($this->_exportLimits['phrases']) {
				$escaped_phrases = array_map(Array (&$this->Conn, 'qstr'), $this->_exportLimits['phrases']);
				$limit_where = 'Phrase IN (' . implode(',', $escaped_phrases) . ')';
			}
			else {
				$limit_where = 'TRUE';
			}

			$sql = 'SELECT *
					FROM ' . $this->Application->getUnitOption('phrases','TableName') . '
					WHERE PhraseType IN (' . implode(',', $phrase_types) . ') AND Module IN (' . implode(',', $phrase_modules) . ') AND ' . $limit_where . '
					ORDER BY Phrase';
			$phrases = $this->Conn->Query($sql, 'PhraseId');

			// email events
			$module_sql = preg_replace('/(.*),/U', 'INSTR(Module,\'\\1\') OR ', implode(',', $module_ids) . ',');

			// apply event selection limit
			if ($this->_exportLimits['emailevents']) {
				$escaped_email_events = array_map(Array (&$this->Conn, 'qstr'), $this->_exportLimits['emailevents']);
				$limit_where = '`Event` IN (' . implode(',', $escaped_email_events) . ')';
			}
			else {
				$limit_where = 'TRUE';
			}

			$sql = 'SELECT *
					FROM ' . $this->Application->getUnitOption('emailevents', 'TableName') . '
					WHERE `Type` IN (' . implode(',', $phrase_types) . ') AND (' . substr($module_sql, 0, -4) . ') AND ' . $limit_where . '
					ORDER BY `Event`, `Type`';
			$events = $this->Conn->Query($sql, 'EventId');

			if (in_array('Core', $module_ids)) {
				// countries
				$sql = 'SELECT *
						FROM ' . $this->Application->getUnitOption('country-state', 'TableName') . '
						WHERE Type = ' . DESTINATION_TYPE_COUNTRY . '
						ORDER BY `IsoCode`';
				$countries = $this->Conn->Query($sql, 'CountryStateId');

				// states
				$sql = 'SELECT *
						FROM ' . $this->Application->getUnitOption('country-state', 'TableName') . '
						WHERE Type = ' . DESTINATION_TYPE_STATE . '
						ORDER BY `IsoCode`';
				$states = $this->Conn->Query($sql, 'CountryStateId');

				foreach ($states as $state_id => $state_data) {
					$country_id = $state_data['StateCountryId'];

					if (!array_key_exists('States', $countries[$country_id])) {
						$countries[$country_id]['States'] = Array ();
					}

					$countries[$country_id]['States'][] = $state_id;
				}
			}

			foreach ($languages as $language_id => $language_info) {
				// language
				$ret .= "\t" . '<LANGUAGE Encoding="' . $this->_exportEncoding . '"';

				foreach ($export_fields	as $export_field) {
					$ret .= ' ' . $export_field . '="' . htmlspecialchars($language_info[$export_field]) . '"';
				}

				$ret .= '>' . "\n";

				// filename replacements
				$replacements = $language_info['FilenameReplacements'];

				if ($replacements) {
					$ret .= "\t\t" . '<REPLACEMENTS>';
					$ret .= $this->_exportEncoding == 'base64' ? base64_encode($replacements) : '<![CDATA[' . $replacements . ']]>';
					$ret .= '</REPLACEMENTS>' . "\n";
				}

				// phrases
				if ($phrases) {
					$ret .= "\t\t" . '<PHRASES>' . "\n";
					foreach ($phrases as $phrase_id => $phrase) {
						$translation = $phrase['l' . $language_id . '_Translation'];

						if (!$translation) {
							// phrase is not translated on given language
							continue;
						}

						$data = $this->_exportEncoding == 'base64' ? base64_encode($translation) : '<![CDATA[' . $translation . ']]>';
						$ret .= "\t\t\t" . '<PHRASE Label="' . $phrase['Phrase'] . '" Module="' . $phrase['Module'] . '" Type="' . $phrase['PhraseType'] . '">' . $data . '</PHRASE>' . "\n";
					}

					$ret .= "\t\t" . '</PHRASES>' . "\n";
				}

				// email events
				if ($events) {
					$ret .= "\t\t" . '<EVENTS>' . "\n";

					foreach ($events as $event_id => $event) {
						$fields_hash = Array (
							'Headers' => $event['Headers'],
							'Subject' => $event['l' . $language_id . '_Subject'],
							'Body' => $event['l' . $language_id . '_Body'],
						);

						$template = $email_message_helper->buildTemplate($fields_hash);

						if (!$template) {
							// email event is not translated on given language
							continue;
						}

						$data = $this->_exportEncoding == 'base64' ? base64_encode($template) : '<![CDATA[' . $template . ']]>';
						$ret .= "\t\t\t" . '<EVENT MessageType="' . $event['MessageType'] . '" Event="' . $event['Event'] . '" Type="' . $event['Type'] . '">' . $data . '</EVENT>'."\n";
					}

					$ret .= "\t\t" . '</EVENTS>' . "\n";
				}

				if (in_array('Core', $module_ids) && $countries) {
					$ret .= "\t\t" . '<COUNTRIES>' . "\n";
					foreach ($countries as $country_id => $country_data) {
						$translation = $country_data['l' . $language_id . '_Name'];

						if (!$translation) {
							// country is not translated on given language
							continue;
						}

						$data = $this->_exportEncoding == 'base64' ? base64_encode($translation) : $translation;

						if (array_key_exists('States', $country_data)) {
							$ret .= "\t\t\t" . '<COUNTRY Iso="' . $country_data['IsoCode'] . '" Translation="' . $data . '">' . "\n";

							foreach ($country_data['States'] as $state_id) {
								$translation = $states[$state_id]['l' . $language_id . '_Name'];

								if (!$translation) {
									// state is not translated on given language
									continue;
								}

								$data = $this->_exportEncoding == 'base64' ? base64_encode($translation) : $translation;
								$ret .= "\t\t\t\t" . '<STATE Iso="' . $states[$state_id]['IsoCode'] . '" Translation="' . $data . '"/>' . "\n";
							}

							$ret  .= "\t\t\t" . '</COUNTRY>' . "\n";
						}
						else {
							$ret .= "\t\t\t" . '<COUNTRY Iso="' . $country_data['IsoCode'] . '" Translation="' . $data . '"/>' . "\n";
						}
					}

					$ret .= "\t\t" . '</COUNTRIES>' . "\n";
				}

				$ret .= "\t" . '</LANGUAGE>' . "\n";
			}

			$ret .= '</LANGUAGES>';
			fwrite($fp, $ret);
			fclose($fp);

			return true;
		}

		/**
		 * Sets language pack encoding (not charset) used during export
		 *
		 * @param string $encoding
		 */
		function setExportEncoding($encoding)
		{
			$this->_exportEncoding = $encoding;
		}

		/**
		 * Sets language pack data limits for export
		 *
		 * @param mixed $phrases
		 * @param mixed $email_events
		 */
		function setExportLimits($phrases, $email_events)
		{
			if (!is_array($phrases)) {
				$phrases = str_replace(',', "\n", $phrases);
				$phrases = preg_replace("/\n+/", "\n", str_replace("\r", '', trim($phrases)));
				$phrases = $phrases ? array_map('trim', explode("\n", $phrases)) : Array ();
			}

			if (!is_array($email_events)) {
				$email_events = str_replace(',', "\n", $email_events);
				$email_events = preg_replace("/\n+/", "\n", str_replace("\r", '', trim($email_events)));
				$email_events = $email_events ? array_map('trim', explode("\n", $email_events)) : Array ();
			}

			$this->_exportLimits = Array ('phrases' => $phrases, 'emailevents' => $email_events);
		}

		/**
		 * Performs upgrade of given language pack part
		 *
		 * @param int $language_id
		 * @param string $prefix
		 * @param string $unique_field
		 * @param string $data_fields
		 */
		function _performUpgrade($language_id, $prefix, $unique_field, $data_fields)
		{
			$live_records = $this->_getTableData($language_id, $prefix, $unique_field, $data_fields[0], false);
			$temp_records = $this->_getTableData($language_id, $prefix, $unique_field, $data_fields[0], true);

			if (!$temp_records) {
				// no data for given language
				return ;
			}

			// perform insert for records, that are missing in live table
			$to_insert = array_diff($temp_records, $live_records);

			if ($to_insert) {
				$to_insert = array_map(Array (&$this->Conn, 'qstr'), $to_insert);

				$sql = 'INSERT INTO ' . $this->Application->getUnitOption($prefix, 'TableName') . '
						SELECT *
						FROM ' . $this->_tables[$prefix] . '
						WHERE ' . $unique_field . ' IN (' . implode(',', $to_insert) . ')';
				$this->Conn->Query($sql);

				// new records were added
				$this->changedPrefixes[] = $prefix;
			}

			// perform update for records, that are present in live table
			$to_update = array_diff($temp_records, $to_insert);

			if ($to_update) {
				$to_update = array_map(Array (&$this->Conn, 'qstr'), $to_update);

				$sql = 'UPDATE ' . $this->Application->getUnitOption($prefix, 'TableName') . ' live
						SET ';

				foreach ($data_fields as $index => $data_field) {
					$data_field = sprintf($data_field, $language_id);

					$sql .= '	live.' . $data_field . ' = (
									SELECT temp' . $index . '.' . $data_field . '
									FROM ' . $this->_tables[$prefix] . ' temp' . $index . '
									WHERE temp' . $index . '.' . $unique_field . ' = live.' . $unique_field . '
								),';
				}

				$sql = substr($sql, 0, -1); // cut last comma

				$where_clause = Array (
					// this won't make any difference, but just in case
					$unique_field . ' IN (' . implode(',', $to_update) . ')',
				);

				if ($this->import_mode == LANG_SKIP_EXISTING) {
					// empty OR not set
					$data_field = sprintf($data_fields[0], $language_id);
					$where_clause[] = '(' . $data_field . ' = "") OR (' . $data_field . ' IS NULL)';
				}

				if ($where_clause) {
					$sql .= "\n" . 'WHERE (' . implode(') AND (', $where_clause) . ')';
				}

				$this->Conn->Query($sql);

				if ($this->Conn->getAffectedRows() > 0) {
					// existing records were updated
					$this->changedPrefixes[] = $prefix;
				}
			}
		}

		/**
		 * Returns data from given table used for language pack upgrade
		 *
		 * @param int $language_id
		 * @param string $prefix
		 * @param string $unique_field
		 * @param bool $temp_mode
		 * @return Array
		 */
		function _getTableData($language_id, $prefix, $unique_field, $data_field, $temp_mode = false)
		{
			$data_field = sprintf($data_field, $language_id);
			$table_name = $this->Application->getUnitOption($prefix, 'TableName');

			if ($temp_mode) {
				// for temp table get only records, that have contents on given language (not empty and isset)
				$sql = 'SELECT ' . $unique_field . '
						FROM ' . $this->Application->GetTempName($table_name, 'prefix:' . $prefix) . '
						WHERE (' . $data_field . ' <> "") AND (' . $data_field . ' IS NOT NULL)';
			}
			else {
				// for live table get all records, no matter on what language
				$sql = 'SELECT ' . $unique_field . '
						FROM ' . $table_name;
			}

			return $this->Conn->GetCol($sql);
		}

		function _parseXML($filename)
		{
			if ($this->_debugMode) {
				$start_time = getmicrotime();
				$this->Application->Debugger->appendHTML(__CLASS__ . '::' . __FUNCTION__ . '("' . $filename . '")');
			}

			$fdata = file_get_contents($filename);

			$xml_parser =& $this->Application->recallObject('kXMLHelper');
			/* @var $xml_parser kXMLHelper */

			$root_node =& $xml_parser->Parse($fdata);
			if (!is_object($root_node) || !is_a($root_node, 'kXMLNode')) {
				// invalid language pack contents
				return false;
			}

			if ($root_node->Children) {
				$this->_processLanguages($root_node->firstChild);
			}

			if ($this->_debugMode) {
				$this->Application->Debugger->appendHTML(__CLASS__ . '::' . __FUNCTION__ . '("' . $filename . '"): ' . (getmicrotime() - $start_time));
			}

			return true;
		}

		/**
		 * Creates temporary tables, used during language import
		 *
		 * @param bool $drop_only
		 */
		function _initImportTables($drop_only = false)
		{
			$this->_tables['phrases'] = $this->_prepareTempTable('phrases', $drop_only);
			$this->_tables['emailevents'] = $this->_prepareTempTable('emailevents', $drop_only);
			$this->_tables['country-state'] = $this->_prepareTempTable('country-state', $drop_only);
		}

		/**
		 * Create temp table for prefix, if table already exists, then delete it and create again
		 *
		 * @param string $prefix
		 */
		function _prepareTempTable($prefix, $drop_only = false)
		{
			$idfield = $this->Application->getUnitOption($prefix, 'IDField');
			$table = $this->Application->getUnitOption($prefix,'TableName');
			$temp_table = $this->Application->GetTempName($table);

			$sql = 'DROP TABLE IF EXISTS %s';
			$this->Conn->Query( sprintf($sql, $temp_table) );

			if (!$drop_only) {
				$sql = 'CREATE TABLE ' . $temp_table . ' SELECT * FROM ' . $table . ' WHERE 0';
				$this->Conn->Query($sql);

				$sql = 'ALTER TABLE %1$s CHANGE %2$s %2$s INT(11) NOT NULL DEFAULT "0"';
				$this->Conn->Query( sprintf($sql, $temp_table, $idfield) );

				switch ($prefix) {
					case 'phrases':
						$unique_field = 'PhraseKey';
						break;

					case 'emailevents':
						$unique_field = 'EventId';
						break;

					case 'country-state':
						$unique_field = 'CountryStateId';
						break;

					default:
						trigger_error('Unknown prefix "<strong>' . $prefix . '</strong>" during language pack import', E_USER_ERROR);
						break;
				}

				$sql = 'ALTER TABLE ' . $temp_table . ' ADD UNIQUE (' . $unique_field . ')';
				$this->Conn->Query($sql);
			}

			return $temp_table;
		}

		/**
		 * Prepares mapping between event name+type and their ids in database
		 *
		 */
		function _updateEventsCache()
		{
			$sql = 'SELECT EventId, CONCAT(Event,"_",Type) AS EventMix
					FROM ' . TABLE_PREFIX . 'Events';
			$this->events_hash = $this->Conn->GetCol($sql, 'EventMix');
		}

		/**
		 * Returns language fields to be exported
		 *
		 * @return Array
		 */
		function _getExportFields()
		{
			return Array (
				'PackName', 'LocalName', 'DateFormat', 'TimeFormat', 'InputDateFormat', 'InputTimeFormat',
				'DecimalPoint', 'ThousandSep', 'Charset', 'UnitSystem', 'Locale', 'UserDocsUrl'
			);
		}

		/**
		 * Processes parsed XML
		 *
		 * @param kXMLNode $language_node
		 */
		function _processLanguages(&$language_node)
		{
			if (array_key_exists('VERSION', $language_node->Parent->Attributes)) {
				// version present -> use it
				$version = $language_node->Parent->Attributes['VERSION'];
			}
			else {
				// version missing -> guess it
				if (is_object($language_node->FindChild('DATEFORMAT'))) {
					$version = 1;
				}
				elseif (array_key_exists('CHARSET', $language_node->Attributes)) {
					$version = 2;
				}
			}

			if ($version == 1) {
				$field_mapping = Array (
					'DATEFORMAT' => 'DateFormat',
					'TIMEFORMAT' => 'TimeFormat',
					'INPUTDATEFORMAT' => 'InputDateFormat',
					'INPUTTIMEFORMAT' => 'InputTimeFormat',
					'DECIMAL' => 'DecimalPoint',
					'THOUSANDS' => 'ThousandSep',
					'CHARSET' => 'Charset',
					'UNITSYSTEM' => 'UnitSystem',
					'DOCS_URL' => 'UserDocsUrl',
				);
			}
			else {
				$export_fields = $this->_getExportFields();
			}

			do {
				$language_id = false;

				$fields_hash = Array (
					'PackName' => $language_node->Attributes['PACKNAME'],
					'LocalName' => $language_node->Attributes['PACKNAME'],
					'Encoding' => $language_node->Attributes['ENCODING'],
					'Charset' => 'utf-8',
				);

				if ($version > 1) {
					foreach ($export_fields as $export_field) {
						$attribute_name = strtoupper($export_field);

						if (array_key_exists($attribute_name, $language_node->Attributes)) {
							$fields_hash[$export_field] = $language_node->Attributes[$attribute_name];
						}
					}
				}

				$sub_node =& $language_node->firstChild;
				/* @var $sub_node kXMLNode */

				do {
					switch ($sub_node->Name) {
						case 'PHRASES':
							if ($sub_node->Children) {
								if (!$language_id) {
									$language_id = $this->_processLanguage($fields_hash);
								}

								if ($this->_debugMode) {
									$start_time = getmicrotime();
								}

								$this->_processPhrases($sub_node->firstChild, $language_id, $fields_hash['Encoding']);

								if ($this->_debugMode) {
									$this->Application->Debugger->appendHTML(__CLASS__ . '::' . '_processPhrases: ' . (getmicrotime() - $start_time));
								}
							}
							break;

						case 'EVENTS':
							if ($sub_node->Children) {
								if (!$language_id) {
									$language_id = $this->_processLanguage($fields_hash);
								}

								$this->_processEvents($sub_node->firstChild, $language_id, $fields_hash['Encoding']);
							}
							break;

						case 'COUNTRIES':
							if ($sub_node->Children) {
								if (!$language_id) {
									$language_id = $this->_processLanguage($fields_hash);
								}

								$this->_processCountries($sub_node->firstChild, $language_id, $fields_hash['Encoding']);
							}
							break;

						case 'REPLACEMENTS':
							// added since v2
							$replacements = $sub_node->Data;

							if ($fields_hash['Encoding'] != 'plain') {
								$replacements = base64_decode($replacements);
							}

							$fields_hash['FilenameReplacements'] = $replacements;
							break;

						default:
							if ($version == 1) {
								$fields_hash[ $field_mapping[$sub_node->Name] ] = $sub_node->Data;
							}
							break;
					}
				} while (($sub_node =& $sub_node->NextSibling()));
			} while (($language_node =& $language_node->NextSibling()));
		}

		/**
		 * Performs phases import
		 *
		 * @param kXMLNode $phrase_node
		 * @param int $language_id
		 * @param string $language_encoding
		 */
		function _processPhrases(&$phrase_node, $language_id, $language_encoding)
		{
			static $other_translations = Array ();

			if ($this->Application->isDebugMode()) {
				$this->Application->Debugger->profileStart('L[' . $language_id . ']P', 'Language: ' . $language_id . '; Phrases Import');
			}

			do {
				$phrase_key = mb_strtoupper($phrase_node->Attributes['LABEL']);

				$fields_hash = Array (
					'Phrase' => $phrase_node->Attributes['LABEL'],
					'PhraseKey' => $phrase_key,
					'PhraseType' => $phrase_node->Attributes['TYPE'],
					'Module' => array_key_exists('MODULE', $phrase_node->Attributes) ? $phrase_node->Attributes['MODULE'] : 'Core',
					'LastChanged' => adodb_mktime(),
					'LastChangeIP' => $this->ip_address,
				);

				$translation = $phrase_node->Data;

				if (array_key_exists($fields_hash['PhraseType'], $this->phrase_types_allowed)) {
					if ($language_encoding != 'plain') {
						$translation = base64_decode($translation);
					}

					if (array_key_exists($phrase_key, $other_translations)) {
						$other_translations[$phrase_key]['l' . $language_id . '_Translation'] = $translation;
					}
					else {
						$other_translations[$phrase_key] = Array (
							'l' . $language_id . '_Translation' => $translation
						);
					}

					$fields_hash = array_merge($fields_hash, $other_translations[$phrase_key]);
					$this->Conn->doInsert($fields_hash, $this->_tables['phrases'], 'REPLACE', false);
				}
			} while (($phrase_node =& $phrase_node->NextSibling()));

			if ($this->Application->isDebugMode()) {
				$this->Application->Debugger->profileFinish('L[' . $language_id . ']P', 'Language: ' . $language_id . '; Phrases Import');
			}

			$this->Conn->doInsert($fields_hash, $this->_tables['phrases'], 'REPLACE');
		}

		/**
		 * Performs email event import
		 *
		 * @param kXMLNode $event_node
		 * @param int $language_id
		 * @param string $language_encoding
		 */
		function _processEvents(&$event_node, $language_id, $language_encoding)
		{
			static $other_translations = Array ();

			if ($this->Application->isDebugMode()) {
				$this->Application->Debugger->profileStart('L[' . $language_id . ']E', 'Language: ' . $language_id . '; Events Import');
			}

			$email_message_helper =& $this->Application->recallObject('EmailMessageHelper');
			/* @var $email_message_helper EmailMessageHelper */

			do {
				$event_id = $this->_getEventId($event_node->Attributes['EVENT'], $event_node->Attributes['TYPE']);
				if ($event_id) {
					if ($language_encoding == 'plain') {
						$template = rtrim($event_node->Data);
					}
					else {
						$template = base64_decode($event_node->Data);
					}

					$parsed = $email_message_helper->parseTemplate($template);

					$fields_hash = Array (
						'EventId' => $event_id,
						'Event' => $event_node->Attributes['EVENT'],
						'Type' => $event_node->Attributes['TYPE'],
						'MessageType' => $event_node->Attributes['MESSAGETYPE'],
					);

					if (array_key_exists($event_id, $other_translations)) {
						$other_translations[$event_id]['l' . $language_id . '_Subject'] = $parsed['Subject'];
						$other_translations[$event_id]['l' . $language_id . '_Body'] = $parsed['Body'];
					}
					else {
						$other_translations[$event_id] = Array (
							'l' . $language_id . '_Subject' => $parsed['Subject'],
							'l' . $language_id . '_Body' => $parsed['Body'],
						);
					}

					if ($parsed['Headers']) {
						$other_translations[$event_id]['Headers'] = $parsed['Headers'];
					}
					elseif (!$parsed['Headers'] && !array_key_exists('Headers', $other_translations[$event_id])) {
						$other_translations[$event_id]['Headers'] = $parsed['Headers'];
					}

					$fields_hash = array_merge($fields_hash, $other_translations[$event_id]);
					$this->Conn->doInsert($fields_hash, $this->_tables['emailevents'], 'REPLACE', false);
				}
			} while (($event_node =& $event_node->NextSibling()));

			if ($this->Application->isDebugMode()) {
				$this->Application->Debugger->profileFinish('L[' . $language_id . ']E', 'Language: ' . $language_id . '; Events Import');
			}

			if (isset($fields_hash)) {
				// at least one email event in language pack was found in database
				$this->Conn->doInsert($fields_hash, $this->_tables['emailevents'], 'REPLACE');
			}
		}

		/**
		 * Performs country_state translation import
		 *
		 * @param kXMLNode $country_state_node
		 * @param int $language_id
		 * @param string $language_encoding
		 */
		function _processCountries(&$country_state_node, $language_id, $language_encoding, $process_states = false)
		{
			static $other_translations = Array ();

			do {
				if ($process_states) {
					$country_state_id = $this->_getStateId($country_state_node->Parent->Attributes['ISO'], $country_state_node->Attributes['ISO']);
				}
				else {
					$country_state_id = $this->_getCountryId($country_state_node->Attributes['ISO']);
				}

				if ($country_state_id) {
					if ($language_encoding == 'plain') {
						$translation = rtrim($country_state_node->Attributes['TRANSLATION']);
					}
					else {
						$translation = base64_decode($country_state_node->Attributes['TRANSLATION']);
					}

					$fields_hash = Array (
						'CountryStateId' => $country_state_id,
					);

					if (array_key_exists($country_state_id, $other_translations)) {
						$other_translations[$country_state_id]['l' . $language_id . '_Name'] = $translation;
					}
					else {
						$other_translations[$country_state_id] = Array (
							'l' . $language_id . '_Name' => $translation,
						);
					}

					$fields_hash = array_merge($fields_hash, $other_translations[$country_state_id]);
					$this->Conn->doInsert($fields_hash, $this->_tables['country-state'], 'REPLACE', false);

					if (!$process_states && $country_state_node->Children) {
						$this->_processCountries($country_state_node->firstChild, $language_id, $language_encoding, true);
					}
				}
			} while (($country_state_node =& $country_state_node->NextSibling()));

			$this->Conn->doInsert($fields_hash, $this->_tables['country-state'], 'REPLACE');
		}

		/**
		 * Creates/updates language based on given fields and returns it's id
		 *
		 * @param Array $fields_hash
		 * @return int
		 */
		function _processLanguage($fields_hash)
		{
			// 1. get language from database
			$sql = 'SELECT ' . $this->lang_object->IDField . '
					FROM ' . $this->lang_object->TableName . '
					WHERE PackName = ' . $this->Conn->qstr($fields_hash['PackName']);
			$language_id = $this->Conn->GetOne($sql);

			if ($language_id) {
				// 2. language found -> update, when allowed
				$this->lang_object->Load($language_id);

				if ($this->import_mode == LANG_OVERWRITE_EXISTING) {
					// update live language record based on data from xml
					$this->lang_object->SetFieldsFromHash($fields_hash);
					$this->lang_object->Update();
				}
			}
			else {
				// 3. language not found -> create
				$this->lang_object->SetFieldsFromHash($fields_hash);
				$this->lang_object->SetDBField('Enabled', STATUS_ACTIVE);

				if ($this->lang_object->Create()) {
					$language_id = $this->lang_object->GetID();

					if (defined('IS_INSTALL') && IS_INSTALL) {
						// language created during install becomes admin interface language
						$this->lang_object->setPrimary(true, true);
					}
				}
			}

			// 4. collect ID of every processed language
			if (!in_array($language_id, $this->_languages)) {
				$this->_languages[] = $language_id;
			}

			return $language_id;
		}

		/**
		 * Returns event id based on it's name and type
		 *
		 * @param string $event_name
		 * @param string $event_type
		 * @return int
		 */
		function _getEventId($event_name, $event_type)
		{
			$cache_key = $event_name . '_' . $event_type;

			return array_key_exists($cache_key, $this->events_hash) ? $this->events_hash[$cache_key] : 0;
		}

		/**
		 * Returns country id based on it's 3letter ISO code
		 *
		 * @param string $iso
		 * @return int
		 */
		function _getCountryId($iso)
		{
			static $cache = null;

			if (!isset($cache)) {
				$sql = 'SELECT CountryStateId, IsoCode
						FROM ' . TABLE_PREFIX . 'CountryStates
						WHERE Type = ' . DESTINATION_TYPE_COUNTRY;
				$cache = $this->Conn->GetCol($sql, 'IsoCode');
			}

			return array_key_exists($iso, $cache) ? $cache[$iso] : false;
		}

		/**
		 * Returns state id based on 3letter country ISO code and 2letter state ISO code
		 *
		 * @param string $country_iso
		 * @param string $state_iso
		 * @return int
		 */
		function _getStateId($country_iso, $state_iso)
		{
			static $cache = null;

			if (!isset($cache)) {
				$sql = 'SELECT CountryStateId, CONCAT(StateCountryId, "-", IsoCode) AS IsoCode
						FROM ' . TABLE_PREFIX . 'CountryStates
						WHERE Type = ' . DESTINATION_TYPE_STATE;
				$cache = $this->Conn->GetCol($sql, 'IsoCode');
			}

			$country_id = $this->_getCountryId($country_iso);

			return array_key_exists($country_id . '-' . $state_iso, $cache) ? $cache[$country_id . '-' . $state_iso] : false;
		}
	}