<?php
/*========================================================================*\
|| ###################################################################### ||
|| # vBulletin 6.2.3 - Licence Number L3138846
|| # ------------------------------------------------------------------ # ||
|| # Copyright 2000-2026 MH Sub I, LLC dba vBulletin. All Rights Reserved.  # ||
|| # This file may not be redistributed in whole or significant part.   # ||
|| # ----------------- VBULLETIN IS NOT FREE SOFTWARE ----------------- # ||
|| # http://www.vbulletin.com | http://www.vbulletin.com/license.html   # ||
|| ###################################################################### ||
\*========================================================================*/

/**
* Builds the languages datastore item
*
* @return	array	The data inserted into datastore
* @deprecated
*/
function build_language_datastore()
{
	return vB_Library::instance('language')->buildLanguageCache();
}

/**
* Reads a language or languages and updates the language db table with the denormalized phrase cache
*
* @param int	ID of language to be built; if -1, build all
* @param int	Not sure actually... any ideas?
* @param bool.  Wether to reset the static vars the function uses to cache items for recursing
* 			when we we build the master language.  Otherwise if we attempt to call this function
* 			twice in the same pageload we don't actually manage to update any changes after the first
* 			call.
* 			The better approach would be to use an internal function and a master function t
* 			hat generates the cached values once and passes them in.
* 			However that means unwinding this function which works but is... odd.
*/
function build_language($languageid = -1, $phrasearray = 0, $reset_static=true)
{
	static $masterlang = null, $jsphrases = null;
	require_once(DIR . '/includes/adminfunctions.php');

	if ($reset_static)
	{
		$masterlang = null;
		$jsphrases = null;
	}

	// load js safe phrases
	if ($jsphrases === null)
	{
		$xmlobj = new vB_XML_Parser(false, DIR . '/includes/xml/js_safe_phrases.xml');
		$safephrases = $xmlobj->parse();

		$jsphrases = [];

		if (is_array($safephrases['phrase']))
		{
			foreach ($safephrases['phrase'] AS $varname)
			{
				$jsphrases["$varname"] = true;
			}
		}
		unset($safephrases, $xmlobj);
	}


	// update all languages if this is the master language
	if ($languageid == -1)
	{
		$languages = vB::getDatastore()->getValue('languagecache');

		if (empty($languages))
		{
			// during install, the datastore is empty
			$languages = vB::getDbAssertor()->assertQuery('language', [
				vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_SELECT,
				vB_dB_Query::COLUMNS_KEY => ['languageid']
			]);
		}

		foreach ($languages as $language)
		{
			build_language($language['languageid'], 0, false);
		}

		return;
	}

	// get phrase types for language update
	$gettypes = [];
	$getphrasetypes = vB::getDbAssertor()->getRows('vBForum:fetchphrasetypes', [vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_STORED]);
	foreach ($getphrasetypes as $getphrasetype)
	{
		$gettypes[] = $getphrasetype['fieldname'];
	}
	unset($getphrasetype);

	//This can be called early in the install process, before phrases have been inserted
	if (empty($gettypes))
	{
		return true;
	}

	if (empty($masterlang))
	{
		$masterlang = [];

		$phrases = vB::getDbAssertor()->assertQuery('vBForum:phrase', [
			'languageid' => [-1, 0],
			'fieldname' => $gettypes,
			vB_dB_Query::COLUMNS_KEY => ['fieldname', 'varname', 'text']
		]);

		foreach ($phrases as $phrase)
		{
			if (isset($jsphrases["$phrase[varname]"]))
			{
				$phrase['text'] = fetch_js_safe_string($phrase['text']);
			}
			if (strpos($phrase['text'], '{1}') !== false)
			{
				$phrase['text'] = str_replace('%', '%%', $phrase['text']);
			}

			$phrase['text'] = str_replace(
				[''],
				'',
				$phrase['text']
			);

			$masterlang["{$phrase['fieldname']}"]["$phrase[varname]"] = $phrase['text'];
		}
	}

	// get phrases for language update
	$phrasearray = $masterlang;
	$phrasetemplate = [];

	$phrases = vB::getDbAssertor()->assertQuery('vBForum:phrase', [
		'languageid' => $languageid,
		'fieldname' => $gettypes,
		vB_dB_Query::COLUMNS_KEY => ['fieldname', 'varname', 'text']

	]);

	foreach ($phrases as $phrase)
	{
		if (isset($jsphrases["$phrase[varname]"]))
		{
			$phrase['text'] = fetch_js_safe_string($phrase['text']);
		}
		if (strpos($phrase['text'], '{1}') !== false)
		{
			$phrase['text'] = str_replace('%', '%%', $phrase['text']);
		}
		$phrasearray["{$phrase['fieldname']}"]["$phrase[varname]"] = $phrase['text'];
	}
	unset($phrase);

	if (!empty($phrasearray))
	{
		vB::getDbAssertor()->assertQuery('vBForum:rebuildLanguage', [
			vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_METHOD,
			'phrasearray' => $phrasearray,
			'languageid' => $languageid,
		]);
	}

	vB_Cache::instance()->event("vB_Language_languageCache");
	// If we hit this through the installer, then all the caches are set to vB_Cache_Null and above won't do anything...
	// Try to trigger this event on the STD cache that vB_Language uses.
	if (defined('VB_AREA') AND
		(VB_AREA == "Install" OR VB_AREA == "Upgrade") AND
		is_a(vB_Cache::instance(), "vB_Cache_Null", false)
	)
	{
		// See upgrade_final's "Reset all caches" step for where it reverts the config.Cache values.
		$config =& vB::getConfig();
		if (!empty($config['Backup']['Cache']['class'][vB_Cache::CACHE_STD]))
		{
			try
			{
				$config['Cache']['class']['temp_standard'] = $config['Backup']['Cache']['class'][vB_Cache::CACHE_STD];
				vB_Cache::instance("temp_standard")->event("vB_Language_languageCache");
				// we don't clear "temp_standard", and instead rely on upgrade_final's call to vB_Cache::resetAllCache() to
				// deal with it so that we avoid clearing the cache multiple times unnecessarily.
			}
			catch (Exception $e)
			{
				// If this failed, there's not much we can do.
				unset($config['Cache']['class']['temp_standard']);
			}
		}
	}
}

/**
* Fetches an array of phrases not present in the master language
*
* @param int	Language ID - language from which to fetch phrases
* @param int	Phrase fieldname - '' = all, -1 = all normal (special = 0), x = specified fieldname
*
* @return	array	[['varname' => 'phrase_varname', 'text' => 'Phrase Text'], [...]]
*/
function fetch_custom_phrases($languageid, $fieldname = '')
{
	if ($languageid == -1)
	{
		return [];
	}

	return fetch_phrases_internal($languageid, 0, $fieldname, 0);
}

/**
* Fetches an array of phrases found in the master set
*
* @param int	Language ID
* @param int	Phrase fieldname - '' = all, -1 = all normal (special = 0), x = specified fieldname
* @param int	Offset key for returned array
*
* @return	array	[['varname' => 'phrase_varname', 'text' => 'Phrase Text'], [...]]
*/
function fetch_standard_phrases($languageid, $fieldname = '', $offset = 0)
{
	return fetch_phrases_internal($languageid, -1, $fieldname, $offset);
}

function fetch_phrases_internal($languageid, $type, $fieldname, $offset)
{
	$phrases = vB::getDbAssertor()->assertQuery('fetchPhrases', [
		'languageid' => $languageid,
		'fieldname' => $fieldname,
		'type' => $type,
	]);

	$phrasearray = [];
	foreach ($phrases AS $phrase)
	{
		if ($phrase['p2var'] != null)
		{
			$phrase['varname'] = $phrase['p2var'];
		}
		else
		{
			$phrase['varname'] = $phrase['p1var'];
		}

		if ($phrase['found'] == 0)
		{
			$phrase['text'] = $phrase['default_text'];
		}
		$phrasearray[$offset] = $phrase;
		$offset++;
	}

	return $phrasearray;
}


/*
 *	This function requires that the new vb framework is initialized.
 */
function get_language_export_xml($languageid, $product, $custom, $just_phrases, $charset = 'UTF-8')
{
	//moved here from the top of language.php
	$default_skipped_groups = [
		'cphelptext',
		'pagemeta',
	];

	$vBUtilString = vB::getString();
	$datastore = vB::getDatastore();
	$db = vB::getDbAssertor();
	if ($languageid == -1)
	{
		$language = [
			'title' => new vB_Phrase('language', 'master_language'),
			'vblangcode' => '',
			'revision' => '',
		];

		/*
			We don't actually keep track of the master language charset...

			For now, just set it to the export charset so we skip the conversion, as we have absolutely no idea what the
			master phrases are encoded with.

			Later on, we might want to assume that it's the board's charset, after we ensure that every time a master
			phrase is saved or the board language is changed, the master pharses are converted to the board charset &
			updated.
		 */
		if (empty($language['charset']))
		{
			//$language['charset'] = $vBUtilString->getCharset();
			$language['charset'] = $charset;
		}
	}
	else
	{
		$language = $db->getRow('language', ['languageid' => $languageid]);
	}

	$title = str_replace('"', '\"', $language['title']);
	$version = str_replace('"', '\"', $datastore->getOption('templateversion'));
	$vblangcode = str_replace('"', '\"', $language['vblangcode']);
	$revision = str_replace('"', '\"', $language['revision']);

	$phrasetypes = vB_Api::instanceInternal('phrase')->fetch_phrasetypes();

	$phrases = [];

	$getphrases = $db->assertQuery('fetchPhrasesForExport', [
		'languageid'             => $languageid,
		'custom'                 => $custom,
		'product'                => $product,
		'default_skipped_groups' => $default_skipped_groups,
	]);
	foreach ($getphrases AS $getphrase)
	{
		if (!$custom AND $getphrase['iscustom'])
		{
			continue;
		}
		$phrases["$getphrase[fieldname]"]["$getphrase[varname]"] = $getphrase;
	}
	unset($getphrase);

	if (empty($phrases) AND $just_phrases)
	{
		throw new vB_Exception_AdminStopMessage('download_contains_no_customizations');
	}

	$xml = new vB_XML_Builder(null, $charset);

	$languagegroup = [
		'name'      => $title,
		'vbversion' => $version,
		'product'   => $product,
		'type'      => $languageid == -1 ? 'master' : ($just_phrases ? 'phrases' : 'custom')
	];
	$vb5_config =& vB::getConfig();
	if (!empty($vblangcode) AND !empty($revision))
	{
		$languagegroup += [
			'vblangcode' => $vblangcode,
			'revision'   => $revision,
		];
	}
	$xml->add_group('language', $languagegroup);


	/*
		If the language's charset or the target charset is not listed in the
		string class's mappings, trying to instance the class on the charset
		or do conversions between them with throw an exception.
		In that case, let's allow just exporting the language without doing
		conversions when the charsets are equal, and throw the exception when
		they're not equal.
	 */
	try
	{
		/*
			Trying to instance the string class on an unknown charset will throw an exception,
			while just running areCharsetsEqual() will return false if a charset is uknown OR if
			both charsets are known & different from each other. In the first case, trying to call toCharset()
			will throw an exception, so we have to check those cases in this fashion.
		 */
		$vBUtilString->toDefault("test", $charset);
		$vBUtilString->toDefault("test", $language['charset']);
		$doCharsetConversation = !($vBUtilString->areCharsetsEqual($language['charset'], $charset));

	}
	catch (Exception $e)
	{
		/*
			In the case where one or both charsets are unknown, just allow export if the language.charset
			& target charset are the same.
			If they're different AND at least one is unknown we don't know how to convert them so just
			rethrow the exception.
		 */
		if (strtolower($charset) == strtolower($language['charset']))
		{
			$doCharsetConversation = false;
		}
		else
		{
			// If you got to this point, please create a JIRA specifying what the language's charset &
			// export charset are, with a DB dump.
			// We'll have to determine if we can add the charset mappings (iff the internal conversion
			// functions know how to handle them) on a case by case basis.
			throw $e;
		}
	}

	if ($languageid != -1 AND !$just_phrases)
	{
		$xml->add_group('settings');
		$ignorefields = ['languageid', 'title', 'userselect', 'vblangcode', 'revision'];
		foreach ($language AS $fieldname => $value)
		{

			if ($doCharsetConversation AND $fieldname == "charset")
			{
				// We'll convert charsets in the next loop, so we should specify
				// language.charset as the output charset, not the input/native charset.
				// Note that we already specify the XML's encoding attribute to $charset at the very
				// end of this function.
				// Note, in some cases, this charset & the XML encoding may be different strings,
				// if there are multiple names for the given canonical charset name.
				$value = $charset;
			}

			if (substr($fieldname, 0, 12) != 'phrasegroup_' AND !in_array($fieldname, $ignorefields))
			{
				/*
					We skip charset conversion on $value here, because the only DB fields with charset
					sensitive strings would be in the phrasegroup_X fields and *actual* phrases are added
					to the XML in the next loop.
					Note that the phrasegroup_X fields are serialized, so if we want to convert charsets
					for whatever reason, we'd need to first unserialize, convert each element, then reserialize.
				 */
				$xml->add_tag($fieldname, $value, [], true);
			}
		}
		$xml->close_group();
	}

	if ($languageid == -1 AND !empty($default_skipped_groups))
	{
		$xml->add_group('skippedgroups');
		foreach ($default_skipped_groups AS $skipped_group)
		{
			$xml->add_tag('skippedgroup', $skipped_group);
		}
		$xml->close_group();
	}

	foreach ($phrases AS $_fieldname => $typephrases)
	{
		$xml->add_group('phrasetype', ['name' => $phrasetypes["$_fieldname"]['title'], 'fieldname' => $_fieldname]);
		foreach ($typephrases AS $phrase)
		{
			$attributes = [
				'name' => $phrase['varname']
			];

			if ($phrase['dateline'])
			{
				$attributes['date'] = $phrase['dateline'];
			}
			if ($phrase['username'])
			{
				$attributes['username'] = $phrase['username'];
			}
			if ($phrase['version'])
			{
				$attributes['version'] = htmlspecialchars_uni($phrase['version']);
			}
			if ($custom AND $phrase['languageid'] == 0)
			{
				$attributes['custom'] = 1;
			}

			if ($doCharsetConversation)
			{
				$phrase['text'] = $vBUtilString->toCharset($phrase['text'], $language['charset'], $charset);
			}

			$xml->add_tag('phrase', $phrase['text'], $attributes, true);
		}
		$xml->close_group();
	}

	$xml->close_group();

	$doc = "<?xml version=\"1.0\" encoding=\"{$charset}\"?>\r\n\r\n";
	$doc .= $xml->output();
	$xml = null;
	return $doc;
}

/**
* Imports a language from a language XML file
*
* @param	string	XML language string
* @param int	Language to overwrite
* @param	string	Override title for imported language
* @param bool	Allow import of language from mismatched vBulletin version
* @param bool	Allow user-select of imported language
* @param bool	Echo output..
*
* @return	Returns false if the custom language was not imported (used in final_upgrade) OR
*			returns the languageid if, if the custom language import was successful (also used in final_upgrade)
*/
function xml_import_language($xml, $languageid = -1, $title = '', $anyversion = false, $userselect = true, $output = true)
{
	//need to pass this in as a parameter or rewrite things so it doesn't matter.
	//in some cases we want to output the keepalive spaces but not any other output
	$showmessages = !(defined('VB_AREA') AND (VB_AREA == 'Install' OR VB_AREA == 'Upgrade'));

	global $vbulletin;

	$vbphrase = vB_Api::instanceInternal('phrase')->renderPhrasesNoShortcode([
		'please_wait' => 'please_wait',
		'importing_language' => 'importing_language',
		'master_language' => 'master_language',
	]);
	$vbphrase = $vbphrase['phrases'];
	print_dots_start('<b>' . $vbphrase['importing_language'] . "</b>, $vbphrase[please_wait]", ':', 'dspan');

	$xmlobj = new vB_XML_Parser($xml);
	if ($xmlobj->error_no())
	{
			print_dots_stop();
			print_stop_message('no_xml_and_no_path');
	}

	if (!$arr = $xmlobj->parse('UTF-8'))
	{
		print_dots_stop();
		print_stop_message(['xml_error_x_at_line_y', $xmlobj->error_string(), $xmlobj->error_line()]);
	}

	if (!$arr['phrasetype'])
	{
		print_dots_stop();
		print_stop_message('invalid_file_specified');
	}

	$title = (empty($title) ? $arr['name'] : $title);
	$version = $arr['vbversion'];
	$master = ($arr['type'] == 'master' ? 1 : 0);
	$just_phrases = ($arr['type'] == 'phrases' ? 1 : 0);
	// These are the fields typically defined in the "settings" block of a language translation XML.
	// Also see far below where we update the `language` record for a non-master, existing language.
	$preserveExistingSettings = [
		'options',
		'languagecode',
		'charset',
		'locale',
		'dateoverride',
		'timeoverride',
		'decimalsep',
		'thousandsep',
		'registereddateoverride',
		'calformat1override',
		'calformat2override',
		'eventdateformatoverride',
		'pickerdateformatoverride',
		'logdateoverride',
	];
	$assertor = vB::getDbAssertor();
	$existingLanguage = $assertor->getRow('language', ['languageid' => $languageid]);

	// for fetch_version_array().
	require_once(DIR . '/includes/adminfunctions.php');
	// VB6-587: Block uploads of vB3 or 4 XMLs, regardless of the $anyversion flag.
	[0 => $major_version] = fetch_version_array($version);
	if ($major_version <= 4)
	{
		// Seems like this import function does not have a "silent"/exception mode. It might be used in loop
		// in the upgrader (e.g. translation imports) where we don't want the print..() but there doesn't seem
		// to be a flag we can check for that... so we'll just print_stop_message() here like the other errors
		// in this function.
		print_dots_stop();
		print_stop_message(['cannot_import_old_vbversion_xml', $version]);
	}

	/*
		For translations, there's usually the "default" (product="vbulletin") translation that's analogous
		to the master language XML, and a product translation (product="{relevant productid, e.g. nativo}")
		that's analogous to the phrases block of a product XML.
		A product translation lacks the "settings" block, and that's meant to defer to the relevant "default"
		translation's settings block.
		Let's do so by pulling the existing language and deferring to its settings when we think that we're
		importing a translation. Otherwise, we have weird unintended behaviors like wiping out the text
		direction (LTR/1 -> RTL/0), decimal separator, etc
	 */
	if (!empty($arr['settings']))
	{
		$langinfo = $arr['settings'];
		/*
			Even if we have settings defined in XML, if this is an update we do not want to
			accidentally wipe out existing settings that aren't defined in this particular XML
			(mostly newer settings like eventdateformatoverride may not be defined in the XMLs)
			todo: what about any language setting changes that an admin made? Should probably
			throw up an admin message after the import.
		 */
		if (
			// If this is an official translation...
			!$master AND
			!empty($arr['revision']) AND
			!empty($arr['vblangcode']) AND
			$arr['product'] == "vbulletin" AND
			// and this is an upgrade, not a fresh import
			$languageid > 0 AND
			!empty($existingLanguage)
		)
		{
			/*
				Switch this to false to preserve any settings that an admin made.
				Switch this to true to make XML defined settings to overwrite any existing settings
				Starting off with overwrite to preserve old/default behavior. Also doing so will
				allow official translations to set/update older settings that were previously not
				defined or outdated/incorrect.
			 */
			$doOverwriteExistingSettings = true;
			foreach ($preserveExistingSettings AS $__key)
			{
				if (!isset($existingLanguage[$__key]))
				{
					// unlikely to hit this, but possibly when we get a new setting and an XML with
					// the new setting gets imported before the language table changes?
					// In any case, there's nothing to preserve in that case.
					continue;
				}
				if (!isset($langinfo[$__key]))
				{
					$langinfo[$__key] = $existingLanguage[$__key];
				}
				else if (!$doOverwriteExistingSettings)
				{
					$langinfo[$__key] = $existingLanguage[$__key];
				}
			}
		}
	}
	else
	{
		$langinfo = [];
		if (
			// If this is an official *product* translation...
			$languageid > 0 AND
			!$master AND
			!empty($arr['revision']) AND
			!empty($arr['vblangcode']) AND
			$arr['product'] != "vbulletin" AND
			!empty($existingLanguage) AND
			$existingLanguage['vblangcode'] === $arr['vblangcode']
		)
		{
			foreach ($preserveExistingSettings AS $__key)
			{
				if (!isset($existingLanguage[$__key]))
				{
					// unlikely to hit this, but possibly when we get a new setting and an XML with
					// the new setting gets imported before the language table changes?
					// In any case, there's nothing to preserve in that case.
					continue;
				}
				if (!isset($langinfo[$__key]))
				{
					$langinfo[$__key] = $existingLanguage[$__key];
				}
			}

			/*
				Revision... Let's just always go with the "default" revision.
				In most cases, the product's revision will be the same or lower than the "default"
				revision, unless they explicitly set it up to have an outdated default translation
				vs. a product translation.
				Note that if we don't set this here, we'll end up wiping the revision accidentally
				in the "officialcustom" check below
			 */
			if (!empty($existingLanguage['revision']))
			{
				$langinfo['revision'] = $existingLanguage['revision'];
			}
			// Note, we don't actually update the vblangcode in the UPDATE query below, but that might
			// just be an oversight. Set it so that if the update query changes, we do not lose it.
			// Unlike revision we already checked the vblangcode match above, so there's no decision here.
			if (!empty($existingLanguage['vblangcode']))
			{
				$langinfo['vblangcode'] = $existingLanguage['vblangcode'];
			}
		}
	}

	// Note, as of VB6-742, we're opening this up to unofficial custom translations as well.
	// As long as they have a vblangcode & revision set, we'll automatically import and overwrite during install and upgrade.
	// Previously, we were importing unofficial translations, but they would frequently have problems due to title conflicts,
	// and fail to overwrite an existing language of the same title because without the vblangcode, the system has no idea
	// which two languages are the "same".
	// Note: this was written before we had product translations.. as it turns out
	// product translations do not get flagged as "officialcustom" since the product != vbulletin
	// see the code above that pulls a number of info from the existing language setting if it's
	// a product translation.
	$officialcustom = false;
	//Check custom language revision. See also VBV-9215.
	if (!$master AND $arr['product'] == 'vbulletin' AND !empty($arr['revision']) AND !empty($arr['vblangcode']))
	{
		$test = $vbulletin->db->query_first("SELECT * FROM " . TABLE_PREFIX . "language WHERE vblangcode = '" . $vbulletin->db->escape_string($arr['vblangcode']) . "'");
		if (!empty($test['languageid']))
		{
			if (intval($test['revision']) >= intval($arr['revision']))
			{
				// Same or newer language revision has been installed
				// We shouldn't print_stop_message() as the upgrader may continue processing other custom languages
				return false;
			}

			$languageid = $test['languageid'];
		}
		$langinfo['revision'] = intval($arr['revision']);
		$langinfo['vblangcode'] = trim($arr['vblangcode']);
		$officialcustom = true;
	}
	else
	{
		/*
			As we only have 1 language record to keep track of default translations & product translations, there
			are some inconsistencies in handling various aspects of the two types of translations.

			One of those is that a product translation is allowed to overwrite at any time, while a default
			translation is only allowed to overwrite an existing language if its revision is greater than the
			existing one. This is more of an undefined behavior on the product translation part rather than
			an intentional difference in the two.

			In any case, this means that in the unorthdox case where a product translation is imported without/
			before a default translation (e.g. they upload them manually via adminCP in that order), iff we
			set a language revision from the XML for a product, we might not be able to actually import the
			default language file.

			That's why we just default it to 0 here and wait for the default file import to give us the revision
			number.
		 */
		if (empty($langinfo['revision']))
		{
			$langinfo['revision'] = 0;
		}

		if (empty($langinfo['vblangcode']))
		{
			$langinfo['vblangcode'] = (empty($arr['vblangcode']) ? '' : $arr['vblangcode']);
		}
	}


	$langinfo['product'] = (empty($arr['product']) ? 'vbulletin' : $arr['product']);

	// look for skipped groups
	$skipped_groups = [];
	if (!empty($arr['skippedgroups']))
	{
		$skippedgroups =& $arr['skippedgroups']['skippedgroup'];

		if (!is_array($skippedgroups))
		{
			$skippedgroups = [$skippedgroups];
		}

		foreach ($skippedgroups AS $skipped)
		{
			if (is_array($skipped))
			{
				$skipped_groups[] = $vbulletin->db->escape_string($skipped['value']);
			}
			else
			{
				$skipped_groups[] = $vbulletin->db->escape_string($skipped);
			}
		}
	}

	if ($skipped_groups)
	{
		$sql_skipped = "AND " . TABLE_PREFIX . "phrase.fieldname NOT IN ('" . implode("', '", $skipped_groups) . "')";
	}
	else
	{
		$sql_skipped = '';
	}

	// Escape data before DB insert
	foreach ($langinfo AS $key => $val)
	{
		$langinfo[$key] = $vbulletin->db->escape_string(trim($val));
	}
	$langinfo['options'] = intval($langinfo['options'] ?? 0);
	$langinfo['revision'] = intval($langinfo['revision']);
	$languageid = intval($languageid);
	// Also default some newer fields that legacy language files may not have
	$langinfo['eventdateformatoverride'] ??= '';
	$langinfo['pickerdateformatoverride'] ??= '';

	if ($version != $vbulletin->options['templateversion'] AND !$master)
	{
		if (strtok($version,'.') != strtok($vbulletin->options['templateversion'],'.'))
		{
			print_dots_stop();
			print_stop_message(['upload_file_created_with_different_major_version', $vbulletin->options['templateversion'], $version]);
		}
		if (!$anyversion)
		{
			print_dots_stop();
			print_stop_message(['upload_file_created_with_different_version', $vbulletin->options['templateversion'], $version]);
		}
	}


	//set up the phrase array
	$arr = vB_XML_Parser::getList($arr, 'phrasetype');

	/*
		Check if we need to convert the phrases to the current board or language charset.
		If we're re-importing or upgrading an existing language, prefer the language's
		charset as to not have charset conflicts/corruptions within a single language.
		E.g. if the board has french ISO-8859-1 and we're now importing product translations
		into the existing language, do NOT blindly change that to UTF-8 (since existing phrases
		will be in ISO-8859-1), instead convert the product phrases into ISO-8859-1.
	 */
	$convertPhrases = false;
	$preferredCharset = $boardCharset = vB_Template_Runtime::fetchStyleVar('charset');
	if (!empty($existingLanguage['charset']))
	{
		$preferredCharset = $existingLanguage['charset'];
	}

	/*
	Regardless of the $readcharset option, we need to get the current phrase charset from the
	xml object.
	The xml object gets the charset in the xml node's encoding attribute, then may or may not
	convert it to ISO-8859-1 (*that* part depends on the $readcharset option, but doesn't
	change the fact that the xml object is the only one who's sure of it at this point).

	If, for some whatever convoluted reason we have no idea what the phrases in memory's charset
	is, fall back to the settings.charset if that was specified in the XML (and assume that the
	parse did not convert the phrases to something else), then fall back to the board or existing
	charset. But it really shouldn't ever happen for well formed, standard vbulletin XMLs.
	 */
	$phrasesCharset = $xmlobj->get_target_encoding() ?? ($arr['settings']['charset'] ?? $preferredCharset);

	// TODO, vB_Utility_String::areCharsetsEqual()
	if (!empty($preferredCharset) AND (strtolower($preferredCharset) != strtolower($phrasesCharset)))
	{
		$convertPhrases = true;
		// since we're converting the phrases' charset, make sure the inserted language
		// matches this charset & informs the browser correctly.
		$langinfo['charset'] = $preferredCharset;
	}
	//spin through the phrases to check validity.  We want to do this *before* we prep for import
	//so that if we abort do to an error, we haven't made any changes first
	foreach ($arr AS $key => $phraseTypes)
	{
		if (is_array($phraseTypes['phrase']))
		{
			foreach ($phraseTypes['phrase'] AS $key2 => $phrase)
			{
				if (is_array($phrase))
				{
					$check = $phrase['value'];
				}
				else
				{
					$check = $phrase;
				}
				if ($convertPhrases)
				{
					// convert it from the language file's (or currently vB_XML_Parser-converted)
					// encoding to the board's (or preferrably existing language's) encoding
					$check = vB_String::toCharset($check, $phrasesCharset, $preferredCharset);
					if (is_array($phrase))
					{
						$arr[$key]['phrase'][$key2]['value'] = $check;
					}
					else
					{
						$arr[$key]['phrase'][$key2] = $check;
					}
				}

				if (!validate_string_for_interpolation($check))
				{
					print_dots_stop();
					print_stop_message(['phrase_text_not_safe',  $phrase['name']]);
				}
			}
		}
	}

	// prepare for import
	if ($master)
	{
		// lets stop it from dieing cause someone borked a previous update
		$assertor->delete('phrase', ['languageid' => -10]);
		if ($output AND $showmessages)
		{
			echo "<h3>$vbphrase[master_language]</h3>\n<p>$vbphrase[please_wait]</p>";
			vbflush();
		}

		$vbulletin->db->query_write("
			UPDATE " . TABLE_PREFIX . "phrase SET
				languageid = -10
			WHERE languageid = -1
				AND (product = '" . $vbulletin->db->escape_string($langinfo['product']) . "'" . iif($langinfo['product'] == 'vbulletin', " OR product = ''") . ")
				$sql_skipped
		");
		$languageid = -1;
	}
	else
	{
		if ($languageid == 0)
		{
			// creating a new language
			if ($just_phrases)
			{
				print_dots_stop();
				print_stop_message(['language_only_phrases',  $title]);
			}
			// else if ($test = $assertor->getRow('language', ['title' => $title, vB_dB_Query::COLUMNS_KEY => ['languageid']]))
			// {
			// 	if ($officialcustom)
			// 	{
			// 		// Rename the old language
			// 		//$condition = ['languageid' => $test['languageid']];
			// 		// Keeping the old logic -- if there are multiple languages with the same title for any reason, update all of
			// 		// them as ..._old.
			// 		$condition = ['title' => $title];
			// 		$assertor->update('language', ['title' => $title . '_old'], $condition);
			// 	}
			// 	else
			// 	{
			// 		print_dots_stop();
			// 		print_stop_message2(['language_already_exists',  $title]);
			// 	}
			// }

			if ($output AND $showmessages)
			{
				$phrase = vB_Api::instanceInternal('phrase')->renderPhrasesNoShortcode([
					['creating_a_new_language_called_x', $title]
				]);
				$phrase = reset($phrase['phrases']);
				echo "<h3><b>" . $phrase . "</b></h3>\n<p>$vbphrase[please_wait]</p>";
				vbflush();
			}

			/*insert query*/
			$vbulletin->db->query_write("
				INSERT INTO " . TABLE_PREFIX . "language (
					title, options, languagecode,
					charset, revision, vblangcode,
					dateoverride, timeoverride, decimalsep, thousandsep,
					registereddateoverride, calformat1override, calformat2override, eventdateformatoverride, pickerdateformatoverride, locale, logdateoverride
				) VALUES (
					'" . $vbulletin->db->escape_string($title) . "', $langinfo[options], '$langinfo[languagecode]',
					'$langinfo[charset]', $langinfo[revision], '" . $vbulletin->db->escape_string($langinfo['vblangcode']) . "',
					'$langinfo[dateoverride]', '$langinfo[timeoverride]', '$langinfo[decimalsep]', '$langinfo[thousandsep]',
					'$langinfo[registereddateoverride]', '$langinfo[calformat1override]', '$langinfo[calformat2override]', '$langinfo[eventdateformatoverride]', '$langinfo[pickerdateformatoverride]', '$langinfo[locale]', '$langinfo[logdateoverride]'
				)
			");
			$languageid = $vbulletin->db->insert_id();
		}
		else
		{
			// overwriting an existing language
			if ($getlanguage = $vbulletin->db->query_first("SELECT title FROM " . TABLE_PREFIX . "language WHERE languageid = $languageid"))
			{
				if (!$just_phrases)
				{
					if ($output AND $showmessages)
					{
						$phrase = vB_Api::instanceInternal('phrase')->renderPhrasesNoShortcode([
							['overwriting_language_x', $getlanguage['title']]
						]);
						$phrase = reset($phrase['phrases']);
						echo "<h3><b>" . $phrase . "</b></h3>\n<p>$vbphrase[please_wait]</p>";
						vbflush();
					}

					$vbulletin->db->query_write("
						UPDATE " . TABLE_PREFIX . "language SET
							options = $langinfo[options],
							languagecode = '$langinfo[languagecode]',
							charset = '$langinfo[charset]',
							locale = '$langinfo[locale]',
							dateoverride = '$langinfo[dateoverride]',
							timeoverride = '$langinfo[timeoverride]',
							decimalsep = '$langinfo[decimalsep]',
							thousandsep = '$langinfo[thousandsep]',
							registereddateoverride = '$langinfo[registereddateoverride]',
							calformat1override = '$langinfo[calformat1override]',
							calformat2override = '$langinfo[calformat2override]',
							eventdateformatoverride = '$langinfo[eventdateformatoverride]',
							pickerdateformatoverride = '$langinfo[pickerdateformatoverride]',
							logdateoverride = '$langinfo[logdateoverride]',
							revision = $langinfo[revision]
						WHERE languageid = $languageid
					");

					$vbulletin->db->query_write("
						UPDATE " . TABLE_PREFIX . "phrase, " . TABLE_PREFIX . "phrase AS phrase2
						SET " . TABLE_PREFIX . "phrase.languageid = -11
						WHERE " . TABLE_PREFIX . "phrase.languageid = $languageid
							AND (" . TABLE_PREFIX . "phrase.product = '" . $vbulletin->db->escape_string($langinfo['product']) . "'" . iif($langinfo['product'] == 'vbulletin', " OR " . TABLE_PREFIX . "phrase.product = ''") . ")
							AND (phrase2.product = '" . $vbulletin->db->escape_string($langinfo['product']) . "'" . iif($langinfo['product'] == 'vbulletin', " OR phrase2.product = ''") . ")
							AND " . TABLE_PREFIX . "phrase.varname = phrase2.varname
							AND phrase2.languageid = 0
							AND " . TABLE_PREFIX . "phrase.fieldname = phrase2.fieldname
							$sql_skipped
					");

					$vbulletin->db->query_write("
						UPDATE " . TABLE_PREFIX . "phrase SET
							languageid = -10
						WHERE languageid = $languageid
							AND (product = '" . $vbulletin->db->escape_string($langinfo['product']) . "'" . iif($langinfo['product'] == 'vbulletin', " OR product = ''") . ")
							$sql_skipped
					");
				}
			}
			else
			{
				print_stop_message2('cant_overwrite_non_existent_language');
			}
		}
	}

	// get current phrase types
	$current_phrasetypes = vB_Api::instanceInternal('phrase')->fetch_phrasetypes();

	if (!$master)
	{
		$globalPhrases = [];
		$result = $assertor->select('phrase', ['languageid' => [0, -1]], false, ['varname']);
		foreach ($result AS $row)
		{
			$globalPhrases[$row['varname']] = true;
		}
	}

	// import language

	// track new phrasetypes
	$new_phrasetypes = [];
	foreach ($arr AS $phraseTypes)
	{
		$sql = [];
		$strlen = 0;

		if ($phraseTypes['fieldname'] == '' OR !preg_match('#^[a-z0-9_]+$#i', $phraseTypes['fieldname'])) // match a-z, A-Z, 0-9,_ only
		{
			continue;
		}
		$fieldname = $phraseTypes['fieldname'];

		$phrases = vB_XML_Parser::getList($phraseTypes, 'phrase');

		// check if the phrasetype is new
		if (!isset($current_phrasetypes[$fieldname]) AND $phrases)
		{
			$new_phrasetypes[] = ['fieldname' => $fieldname, 'title' => $phraseTypes['name']];
		}

		// Send some output to the browser inside this loop so certain hosts
		// don't artificially kill the script. See bug #34585
		if ($output)
		{
			echo ' ';
			vbflush();
		}

		foreach ($phrases AS $phrase)
		{
			$phrase['date'] = $phrase['date'] ?? 0;
			$phrase['username'] = $phrase['username'] ?? '';
			$phrase['version'] = $phrase['version'] ?? '';

			if ($master)
			{
				$insertLanguageId = -1;
			}
			else if (!isset($globalPhrases[$phrase['name']]))
			{
				$insertLanguageId = 0;
			}
			else if (!empty($phrase['custom']))
			{
				// this is a custom phrase (language 0) -- we don't want it to end up in the custom language
				continue;
			}
			else
			{
				$insertLanguageId = $languageid;
			}

			$sql[] = "
				($insertLanguageId,
				'" . $vbulletin->db->escape_string($fieldname) . "',
				'" . $vbulletin->db->escape_string($phrase['name']) . "',
				'" . $vbulletin->db->escape_string($phrase['value']) . "',
				'" . $vbulletin->db->escape_string($langinfo['product']) . "',
				'" . $vbulletin->db->escape_string($phrase['username']) . "',
				" . intval($phrase['date']) . ",
				'" . $vbulletin->db->escape_string($phrase['version']) . "')
			";

			$strlen += strlen(end($sql));

			if ($strlen > 102400)
			{
				// insert max of 100k of phrases at a time
				/*insert query*/
				$vbulletin->db->query_write("
					REPLACE INTO " . TABLE_PREFIX . "phrase
						(languageid, fieldname, varname, text, product, username, dateline, version)
					VALUES
						" . implode(",\n", $sql)
				);

				$sql = [];
				$strlen = 0;
			}

			// Send some output to the browser inside this loop so certain hosts
			// don't artificially kill the script. See bug #34585
			if ($output)
			{
				echo ' ';
				vbflush();
			}
		}

		if ($sql)
		{
			/*insert query*/
			$vbulletin->db->query_write("
				REPLACE INTO " . TABLE_PREFIX . "phrase
					(languageid, fieldname, varname, text, product, username, dateline, version)
				VALUES
					" . implode(",\n", $sql)
			);
		}
	}
	unset($sql, $arr, $current_phrasetypes, $phraseTypes);

	// insert any new phrasetypes
	foreach ($new_phrasetypes AS $phrasetype)
	{
		add_phrase_type($phrasetype['fieldname'], $phrasetype['title'], $langinfo['product']);
	}

	$vbulletin->db->query_write("
		UPDATE IGNORE " . TABLE_PREFIX . "phrase
		SET " . TABLE_PREFIX . "phrase.languageid = $languageid
		WHERE " . TABLE_PREFIX . "phrase.languageid = -11
			AND (" . TABLE_PREFIX . "phrase.product = '" . $vbulletin->db->escape_string($langinfo['product']) . "'" . iif($langinfo['product'] == 'vbulletin', " OR " . TABLE_PREFIX . "phrase.product = ''") . ")
			$sql_skipped
	");

	// now delete any phrases that were moved into the temporary language for safe-keeping
	$vbulletin->db->query_write("
		DELETE FROM " . TABLE_PREFIX . "phrase
		WHERE languageid IN (-10, -11)
			AND (product = '" . $vbulletin->db->escape_string($langinfo['product']) . "'" . iif($langinfo['product'] == 'vbulletin', " OR product = ''") . ")
			$sql_skipped
	");

	vB_Library_Phrase::instance()->setPhraseDate();
	print_dots_stop();

	return $languageid;
}

/**
* Fetches a string specifying the type of a phrase
*
* @param int $languageid
* @param string $title
*
* @return	string -- Either $vbphrase['standard_phrase'], $vbphrase['custom_phrase'] or construct_phrase($vbphrase['x_translation'], $title)
*/
function fetch_language_type_string($languageid, $title)
{
	global $vbphrase;
	switch($languageid)
	{
		case -1:
			return $vbphrase['standard_phrase'];
		case  0:
			return $vbphrase['custom_phrase'];
		default:
			return construct_phrase($vbphrase['x_translation'], $title);
	}
}

/**
* Highlights search terms in text
*
* @param	string	Needle
* @param	string	Haystack
* @param bool	True if you want to ignore case (case insensitive)
*
* @return	string	Highlighted HTML
*/
function fetch_highlighted_search_results($searchstring, $text, $ignorecase = true)
{
	return preg_replace(
		'/(' . preg_quote(htmlspecialchars_uni($searchstring), '/') . ')/sU' . ($ignorecase ? 'i' : ''),
		'<span class="col-i" style="text-decoration:underline;">\\1</span>',
		htmlspecialchars_uni($text)
	);
}

/**
* Wraps an HTML tag around a string.
*
* @param	string	Text to be wrapped
* @param	string	Tag name and attributes (eg: 'span class="smallfont"')
* @param	mixed	Optional - if evaluates to false, wrapping will not occur
*
* @return	string
*/
function fetch_tag_wrap($text, $tag, $condition = '1=1')
{
	if ($condition)
	{
		if ($pos = strpos($tag, ' '))
		{
			$endtag = substr($tag, 0, $pos);
		}
		else
		{
			$endtag = $tag;
		}
		return "<$tag>$text</$endtag>";
	}
	else
	{
		return $text;
	}
}

/**
* Prints a phrase row for use in language.php?do=edit
*
* @param	array	Phrase array containing phraseid, varname, text, languageid
* @param int	Number of rows for textarea
* @param int	Not used?
* @param	string	ltr or rtl for direction
*/
function print_phrase_row($phrase, $editrows, $key, $dir)
{
	global $vbphrase;
	static $bgcount = 0;

	if ($bgcount++ % 2 == 0)
	{
		$class = 'alt1';
		$altclass = 'alt2';
	}
	else
	{
		$class = 'alt2';
		$altclass = 'alt1';
	}

	construct_hidden_code('def[' . urlencode($phrase['varname']) . ']', $phrase['text']);
	construct_hidden_code('prod[' . urlencode($phrase['varname']) . ']', (empty($phrase['product']) ? 'vbulletin' : $phrase['product']));

	if ($phrase['found'])
	{
		$phrasedfn = " <dfn><br /><label for=\"rvt$phrase[phraseid]\"><input type=\"checkbox\" name=\"rvt[$phrase[varname]]\"" .
			" id=\"rvt$phrase[phraseid]\" value=\"$phrase[phraseid]\" tabindex=\"1\" />$vbphrase[revert_gcpglobal]</label></dfn>";
		$label = "<label for=\"rvt$phrase[phraseid]\">" . nl2br(htmlspecialchars_uni($phrase['default_text'])) . "</label>";
		$code = 'c';
	}
	else
	{
		$phrasedfn = '';
		$label = nl2br(htmlspecialchars_uni($phrase['default_text']));
		$code = 'g';
	}

	print_label_row(
		"<span class=\"smallfont\" title=\"\$vbphrase['$phrase[varname]']\" style=\"word-spacing:-5px\">
			<b>" . str_replace('_', '_ ', $phrase['varname']) . "</b>
			</span>" . $phrasedfn,
		"<div class=\"$altclass\" style=\"padding:4px; border:inset 1px;\"><span class=\"smallfont\" title=\"" . $vbphrase['default_text'] . "\">" . $label .
			"</span></div><textarea class=\"code-" . $code . "\" name=\"phr[" . urlencode($phrase['varname']) .
			"]\" rows=\"$editrows\" cols=\"70\" tabindex=\"1\" dir=\"$dir\">" . htmlspecialchars_uni($phrase['text']) . "</textarea>",
		$class
	);
	print_description_row('<img src="images/clear.gif" width="1" height="1" alt="" />', 0, 2, 'thead');
}

/**
* Constructs a version of a phrase varname that is browser-wrappable for display
*
* @param string $varname -- Phrase varname
* @param string $classes
*
* @return	string -- span with formatting and suggested wordbreaks
*/
function construct_wrappable_varname(string $varname, string $classes = 'smallfont h-bold') : string
{
	return '<span class="' . $classes . '">' . str_replace('_', '_<wbr>', $varname) . '</span>';
}

/**
* Turns 'my_phrase_varname_global' into ['my_phrase_varname', 'global'];
*
* @param string $key -- Incoming phrase varname (my_phrase_varname_3)
* @return array -- [$varname, $filedname]
*/
function fetch_varname_fieldname(string $key) : array
{
	$firstatsignpos = strpos($key, '@');

	return [
		urldecode(substr($key, 0, $firstatsignpos)),
		substr($key, $firstatsignpos + 1),
	];
}


/**
* Allows plugins etc. to add a phrasetype easily
*
* @param	string	Phrasetype name
* @param	string	Phrasetype title
* @param	string	Product ID
*
* @return	mixed	If insert succeeds, returns inserted fieldname
*/
function add_phrase_type($phrasegroup_name, $phrasegroup_title, $productid = 'vbulletin')
{
	global $vbulletin;

	if (!preg_match('#^[a-z0-9_]+$#i', $phrasegroup_name)) // match a-z, A-Z, 0-9,_ only
	{
		return false;
	}

	// first lets check if it exists
	if ($check = $vbulletin->db->query_first("SELECT * FROM " . TABLE_PREFIX . "phrasetype WHERE fieldname = '$phrasegroup_name'"))
	{
		return false;
	}
	else
	{
		/*insert query*/
		$vbulletin->db->query_write("
			INSERT INTO " . TABLE_PREFIX . "phrasetype
				(fieldname, title, editrows, product)
			VALUES
				('" . $vbulletin->db->escape_string($phrasegroup_name) . "',
				'" . $vbulletin->db->escape_string($phrasegroup_title) . "',
				3,
				'" . $vbulletin->db->escape_string($productid) . "')
		");

		if (!$vbulletin->db->query_first($sql = "SHOW FULL COLUMNS FROM " . TABLE_PREFIX . "language LIKE 'phrasegroup_" . $vbulletin->db->escape_string($phrasegroup_name) . "'"))
		{
			$vbulletin->db->query_write("ALTER TABLE " . TABLE_PREFIX . "language ADD phrasegroup_" . $vbulletin->db->escape_string($phrasegroup_name) . " MEDIUMTEXT NOT NULL");
		}

		return $phrasegroup_name;
	}

	return false;
}

/*=========================================================================*\
|| #######################################################################
|| # Downloaded: 13:27, Sun Aug 30th 2026
|| # CVS: $RCSfile$ - $Revision: 122340 $
|| #######################################################################
\*=========================================================================*/
