<?php
/*
**	Copyright (C) 1995-2021 by the ITools Authors.
**	This file is part of ITools - the Internet Tools Library
**
**	ITools is free software; you can redistribute it and/or modify
**	it under the terms of the GNU General Public License as published by
**	the Free Software Foundation; either version 3 of the License, or
**	(at your option) any later version.
**
**	ITools is distributed in the hope that it will be useful,
**	but WITHOUT ANY WARRANTY; without even the implied warranty of
**	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
**	GNU General Public License for more details.
**
**	You should have received a copy of the GNU General Public License
**	along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

class it_text
{
	var $actlanguage;			# Selected language
	var $defaultlanguage;			# Browser language
	var $languages = array();		# Active languages
	var $languages_available = array();	# Available languages
	var $statictext = array();		# Text array, read from php file on init
	var $label_to_service = array(); # which label belongs to which service - only used for debug parameter texts
	var $allowedfuncs;
	var $p; # Constructor Parameters

/**
 * Constructor
 * Loads all texts.php in include path for translated labels. Singleton; if instanciated mutiple times, texts are merged
 * Example texts.php: <?php return array('_' => array('en'=>"English", 'de'=>"Deutsch"), 'edit'=>array('en'=>"Edit", 'de'=>("Editieren")));
 * @param $p['fallbacklanguage'] optional language to use for undefined texts (useful for partially translated projects)
 * @param $p['forcelanguage'] optional language to use instead of user's preferred language
 * @param $p['global'] store text object in global it_text for global functions (default: true)
 * @param $p['phpfile'] optional texts file(s), default: all texts.php in include path
 * @param $p['phpfiles'] text files to load in addition to $p['phpfile'] (Note: $p['phpfile'] defaults to all texts.php files in include path)
 * @param $p['transmogrifiers'] comma separated functions that may be called by using {foo:bar} (foo will be called with bar as argument) in T()
 */
function __construct($p = array())
{
	if (!$p['phpfile'])
	{
		# Find all texts.php in path (abs path in case we need to save)
		foreach (explode(PATH_SEPARATOR, ini_get('include_path')) as $dir)
			if (file_exists($phpfile = "$dir/texts.php"))
				$p['phpfiles'][] = $phpfile;
	}

	$this->p = ($p += (array)$GLOBALS['it_text_defaultconfig'] + array(
		'global' => true,
		'phpfiles' => array_unique(array_merge((array)$p['phpfiles'], (array)$p['phpfile'])),
	));
	$this->allowedfuncs = array_flip(explode(",", $p['transmogrifiers']));

	# Read and merge texts from php files if none defined yet
	foreach ($p['phpfiles'] as $phpfile)
	{
		$this->statictext += ($ret = include($phpfile));

		if ($GLOBALS['debug_texts'])
		{
			$service = strpos($phpfile, $GLOBALS['ULTRAHOME']) !== false ? '' : it::match('/www/([^/.]+)', $phpfile);
			$this->label_to_service += array_combine(array_keys($ret), array_fill(0, count($ret), $service));
		}
	}

	# Get array of supported languages and their names
	$this->languages_available = (array)$this->statictext['_'];
	foreach ($this->languages_available as $code => $languagename)
	{
		# Only use a language in browser detection below if it's not disabled by a leading '-'
		if (substr($languagename, 0, 1) != '-')
		{
			$this->languages[$code] = $languagename;
			if (!$this->actlanguage)
				$this->initlang($code); # failsafe lang
		}
	}

	# Set our default language according to browser preference
	if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
	{
		foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $code)
			if ($this->initlang($code) || $this->initlang(substr($code, 0, 2))) # lang from browser/lang group from browser
				break;
	}
	$this->defaultlanguage = $this->actlanguage;

	$this->initlang(it::match('\.([a-z]{2})\.[^./]+$', $_SERVER['PHP_SELF'])); # lang from url override
	$this->initlang($p['forcelanguage']); # programmer override

	# Create empty array to activate sampling; dont kill any existing one
	if (!it::is_live() || rand(1, $_POST ? 10 : 100) == 1)
		$GLOBALS['it_text_sampling'] = (array)$GLOBALS['it_text_sampling'];

	# Make this object available under $GLOBALS['it_text'], or add my texts to $GLOBALS['it_text'] if it exists
	if ($p['global'])
	{
		if (!$GLOBALS['it_text'])
			$GLOBALS['it_text'] =& $this;
		else
			$GLOBALS['it_text']->statictext += $this->statictext;
	}
}


# internal: overwrite language setting if code is valid, return success
function initlang($code)
{
	if ($this->languages[$code])
		$this->actlanguage = $code;

	return $this->languages[$code];
}


/**
 * Instanciate singleton if necessary
 */
static function init()
{
	if (!$GLOBALS['it_text'])
		new it_text;
}



/**
 * INTERNAL function for T(): : Return translated text in the selected language
 */
function text($label, $language = null)
{
	if (!$language)
		$language = $this->actlanguage;

	$text = $this->statictext[$label][$language];
	if (!isset($text))
	{
		$text = $this->statictext[$label][$this->p['fallbacklanguage']];
		if (!isset($text))
		{
			$text = "<span style='background:#F88' title='" . Q("$label (" . it_debug::backtrace(array('levels'=>1, 'skipfiles'=>"text|auto_prepend")) . ")") . "'>" . Q($label) . "</span><a href='/admin/texts.html?edit=$label'>.</a>";
			it::error(array('title'=>"unknown label $label language $language - see /tmp/alertdata/alertlog", 'backtraceskip'=>2, 'blockmail'=>21600));
		}
	}

	if ($GLOBALS['debug_texts'] && !preg_match('/submit|button|servicedomain/i', $label) && (!$_GET['it_texts_mark'] || $label == $_GET['it_texts_mark']))
	{
		$host = 'http' . (isset($_SERVER['HTTPS']) ? 's' : '') . '://' . preg_replace('/\.texts/', '', $_SERVER['HTTP_HOST']);
		if ($service = $this->label_to_service[$label])
			$host = preg_replace('#//(admin\.)?[^.]*\.#', "//$service.", $host);

		$text = "<span style='background:#8F8' title='$label (" . it_debug::backtrace(array('levels'=>1, 'skipfiles'=>"text|auto_prepend")) . ")'>" . ($text ? $text : $label) . "</span><a href='$host/admin/texts.html?edit=$label'>.</a>";
	}

	if (isset($GLOBALS['it_text_sampling']))
		$GLOBALS['it_text_sampling'][$label] = true;

	return $text;
}


/**
 * INTERNAL function for ET(): Return translated text with values replaced
 */
function etext($label, $values = null, $language = null)
{
	return self::transmogrify($this->text($label, $language), $values, $label, $this->allowedfuncs);
}


/**
 * INTERNAL function for T_set_language()
 */
function set_language($language)
{
	$this->actlanguage = is_scalar($language) && $this->languages_available[$language] ? $language : $this->defaultlanguage;
}


/**
 * INTERNAL function for T_lang(): Get active language
 */
function get_language()
{
	return $this->actlanguage;
}


/**
 * INTERNAL function for T_lang(): Get active language
 */
function get_defaultlanguage()
{
	return $this->defaultlanguage;
}


/**
 * INTERNAL function for T_exists(): Check if a text entry for a specific label exists
 */
function text_exists($label, $language = null)
{
	return (isset($this->statictext[$label][isset($language) ? $language : $this->actlanguage]) || $this->p['fallbacklanguage'] && isset($this->statictext[$label][$this->p['fallbacklanguage']])) ? $label : false;
}


/**
 * Create / overwrite a text in the selected language. Call dump_php() to make the change permanent.
 * @param $label Label of text to change
 * @param $text New text to set
 * @param $language Optional language that is to be manipulated
 */
function set($label, $text = null, $language = null)
{
	if (!isset($language))
		$language = $this->actlanguage;

	$this->statictext[$label][$language] = $text;
}


/**
 * Replaces variables of the form {obj.var} with value from $values, e.g. {user.name}, or result of a func, e.g. {LU(//www/terms)}
 * NOTE: Invalid object names or non-existing variables are simply deleted.
 */
static function transmogrify($text, $values = null, $label = null, $allowedfuncs = null)
{
	foreach (preg_split('#{([^}]*)}#', $text, -1, PREG_SPLIT_DELIM_CAPTURE) as $i => $part)
	{
		if ($i % 2) # odd offsets are delimiters, i.e. braces to be replaced
		{
			if (it::match('^[\w.]+$', $part))
			{
				$value = $values ? $values : $GLOBALS;
				foreach (explode(".", $part) as $key)
				{
					$value = is_object($value) ? $value->$key : $value[$key];
					if ($value === null && $values && $label) # do not test in $GLOBALS mode
						it::error(array('title' => "No value given for text variable {" . $key ."} in label $label", 'backtraceskip' => 3));
				}
			}
			else
				$value = (list($func, $arg) = it::match('^([\w:]+)\((.*)\)$', $part)) && isset($allowedfuncs[$func]) ? $func($arg) : "{" . $part . "}";

			$result .= $GLOBALS['debug_texts'] ? "</span>$value<span style='background:#8F8'>" : $value;
		}
		else
			$result .= $part;
	}

	return $result;
}



/**
 * Re-create php text file from $this->statictext
 * @return true if successful, false if not (usually if file is not writeable by user www)
 */
function dump_php()
{
	$result = false;

	# Special sorting: natural, but _ is the first entry
	uksort($this->statictext, "strnatcmp");
	$this->statictext = array_merge(['_' => $this->statictext['_']], $this->statictext);

	$oldmask = umask(002);
	$filename = $this->p['phpfiles'][0];
	if ((count($this->p['phpfiles']) == 1))
	{
		foreach ($this->statictext as $label => $texts)
		{
			$dump .= var_export($label, true) . " => [\n";
			foreach ($texts as $lang => $text)
				$dump .= "    " . var_export($lang, true) . " => " . var_export($text, true) . ",\n";
			$dump .= "],\n";
		}
		$result = it::file_put_contents($filename, "<?php return [\n$dump];\n");
	}

	umask($oldmask);
	return $result;
}

} /* End class it_text */