<?php

/**
 * A minimalistic XHTML PHP based template system implemented for simpleSAMLphp.
 *
 * @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
 * @package simpleSAMLphp
 * @version $Id: Template.php 3217 2013-01-08 11:56:19Z comel.ah $
 */
class SimpleSAML_XHTML_Template {

	/**
	 * This is the default language map. It is used to map languages codes from the user agent to
	 * other language codes.
	 */
	private static $defaultLanguageMap = array('nb' => 'no');


	private $configuration = null;
	private $template = 'default.php';
	private $availableLanguages = array('en');
	private $language = null;
	
	private $langtext = array();
	
	public $data = null;


	/**
	 * Associative array of dictionaries.
	 */
	private $dictionaries = array();


	/**
	 * The default dictionary.
	 */
	private $defaultDictionary = NULL;


	/**
	 * HTTP GET language parameter name.
	 */
	private $languageParameterName = 'language';


	/**
	 * Constructor
	 *
	 * @param $configuration   Configuration object
	 * @param $template        Which template file to load
	 * @param $defaultDictionary  The default dictionary where tags will come from.
	 */
	function __construct(SimpleSAML_Configuration $configuration, $template, $defaultDictionary = NULL) {
		$this->configuration = $configuration;
		$this->template = $template;
		
		$this->data['baseurlpath'] = $this->configuration->getBaseURL();

		$this->availableLanguages = $this->configuration->getArray('language.available', array('en'));

		$this->languageParameterName = $this->configuration->getString('language.parameter.name', 'language');
		if (isset($_GET[$this->languageParameterName])) {
			$this->setLanguage($_GET[$this->languageParameterName], $this->configuration->getBoolean('language.parameter.setcookie', TRUE));
		}

		if($defaultDictionary !== NULL && substr($defaultDictionary, -4) === '.php') {
			/* For backwards compatibility - print warning. */
			$backtrace = debug_backtrace();
			$where = $backtrace[0]['file'] . ':' . $backtrace[0]['line'];
			SimpleSAML_Logger::warning('Deprecated use of new SimpleSAML_Template(...) at ' . $where .
				'. The last parameter is now a dictionary name, which should not end in ".php".');

			$this->defaultDictionary = substr($defaultDictionary, 0, -4);
		} else {
			$this->defaultDictionary = $defaultDictionary;
		}
	}
	
	/**
	 * setLanguage() will set a cookie for the user's browser to remember what language 
	 * was selected
	 * 
	 * @param $language    Language code for the language to set.
	 */
	public function setLanguage($language, $setLanguageCookie = TRUE) {
		$language = strtolower($language);
		if (in_array($language, $this->availableLanguages, TRUE)) {
			$this->language = $language;
			if ($setLanguageCookie === TRUE) {
				SimpleSAML_XHTML_Template::setLanguageCookie($language);
			}
		}
	}

	/**
	 * getLanguage() will return the language selected by the user, or the default language
	 * This function first looks for a cached language code,
	 * then checks for a language cookie,
	 * then it tries to calculate the preferred language from HTTP headers.
	 * Last it returns the default language.
	 */
	public function getLanguage() {

		// Language is set in object
		if (isset($this->language)) {
			return $this->language;
		}

		// Run custom getLanguage function if defined
		$customFunction = $this->configuration->getArray('language.get_language_function', NULL);
		if (isset($customFunction)) {
			assert('is_callable($customFunction)');
			$customLanguage = call_user_func($customFunction, $this);
			if ($customLanguage !== NULL && $customLanguage !== FALSE) {
				return $customLanguage;
			}
		}

		// Language is provided in a stored COOKIE
		$languageCookie = SimpleSAML_XHTML_Template::getLanguageCookie();
		if ($languageCookie !== NULL) {
			$this->language = $languageCookie;
			return $languageCookie;
		}

		/* Check if we can find a good language from the Accept-Language http header. */
		$httpLanguage = $this->getHTTPLanguage();
		if ($httpLanguage !== NULL) {
			return $httpLanguage;
		}

		// Language is not set, and we get the default language from the configuration.
		return $this->getDefaultLanguage();
	}


	/**
	 * This function gets the prefered language for the user based on the Accept-Language http header.
	 *
	 * @return The prefered language based on the Accept-Language http header, or NULL if none of the
	 *         languages in the header were available.
	 */
	private function getHTTPLanguage() {
		$languageScore = SimpleSAML_Utilities::getAcceptLanguage();

		/* For now we only use the default language map. We may use a configurable language map
		 * in the future.
		 */
		$languageMap = self::$defaultLanguageMap;

		/* Find the available language with the best score. */
		$bestLanguage = NULL;
		$bestScore = -1.0;

		foreach($languageScore as $language => $score) {

			/* Apply the language map to the language code. */
			if(array_key_exists($language, $languageMap)) {
				$language = $languageMap[$language];
			}

			if(!in_array($language, $this->availableLanguages, TRUE)) {
				/* Skip this language - we don't have it. */
				continue;
			}

			/* Some user agents use very limited precicion of the quality value, but order the
			 * elements in descending order. Therefore we rely on the order of the output from
			 * getAcceptLanguage() matching the order of the languages in the header when two
			 * languages have the same quality.
			 */
			if($score > $bestScore) {
				$bestLanguage = $language;
				$bestScore = $score;
			}
		}

		return $bestLanguage;
	}


	/**
	 * Returns the language default (from configuration)
	 */
	private function getDefaultLanguage() {
		return $this->configuration->getString('language.default', 'en');
	}

	/**
	 * Returns a list of all available languages.
	 */
	private function getLanguageList() {
		$thisLang = $this->getLanguage();
		$lang = array();
		foreach ($this->availableLanguages AS $nl) {
			$lang[$nl] = ($nl == $thisLang);
		}
		return $lang;
	}

	/**
	 * Return TRUE if language is Right-to-Left.
	 */
	private function isLanguageRTL() {
		$rtlLanguages = $this->configuration->getArray('language.rtl', array());
		$thisLang = $this->getLanguage();
		if (in_array($thisLang, $rtlLanguages)) {
			return TRUE;
		}
		return FALSE;
	}

	/**
	 * Includs a file relative to the template base directory.
	 * This function can be used to include headers and footers etc.
	 *
	 */	
	private function includeAtTemplateBase($file) {
		$data = $this->data;

		$filename = $this->findTemplatePath($file);
		
		include($filename);
	}


	/**
	 * Retrieve a dictionary.
	 *
	 * This function retrieves a dictionary with the given name.
	 *
	 * @param $name  The name of the dictionary, as the filename in the dictionary directory,
	 *               without the '.php'-ending.
	 * @return  An associative array with the dictionary.
	 */
	private function getDictionary($name) {
		assert('is_string($name)');

		if(!array_key_exists($name, $this->dictionaries)) {
			$sepPos = strpos($name, ':');
			if($sepPos !== FALSE) {
				$module = substr($name, 0, $sepPos);
				$fileName = substr($name, $sepPos + 1);
				$dictDir = SimpleSAML_Module::getModuleDir($module) . '/dictionaries/';
			} else {
				$dictDir = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
				$fileName = $name;
			}
			
			$this->dictionaries[$name] = $this->readDictionaryFile($dictDir . $fileName);
		}

		return $this->dictionaries[$name];
	}


	/**
	 * Retrieve a tag.
	 *
	 * This function retrieves a tag as an array with language => string mappings.
	 *
	 * @param $tag  The tag name. The tag name can also be on the form '{<dictionary>:<tag>}', to retrieve
	 *              a tag from the specific dictionary.
	 * @return As associative array with language => string mappings, or NULL if the tag wasn't found.
	 */
	public function getTag($tag) {
		assert('is_string($tag)');

		/* First check translations loaded by the includeInlineTranslation and includeLanguageFile methods. */
		if(array_key_exists($tag, $this->langtext)) {
			return $this->langtext[$tag];
		}

		/* Check whether we should use the default dictionary or a dictionary specified in the tag. */
		if(substr($tag, 0, 1) === '{' && preg_match('/^{((?:\w+:)?\w+?):(.*)}$/D', $tag, $matches)) {
			$dictionary = $matches[1];
			$tag = $matches[2];
		} else {
			$dictionary = $this->defaultDictionary;
			if($dictionary === NULL) {
				/* We don't have any dictionary to load the tag from. */
				return NULL;
			}
		}

		$dictionary = $this->getDictionary($dictionary);
		if(!array_key_exists($tag, $dictionary)) {
			return NULL;
		}

		return $dictionary[$tag];
	}


	/**
	 * Retrieve the preferred translation of a given text.
	 *
	 * @param $translations  The translations, as an associative array with language => text mappings.
	 * @return The preferred translation.
	 */
	public function getTranslation($translations) {
		assert('is_array($translations)');

		/* Look up translation of tag in the selected language. */
		$selected_language = $this->getLanguage();
		if (array_key_exists($selected_language, $translations)) {
			return $translations[$selected_language];
		}

		/* Look up translation of tag in the default language. */
		$default_language = $this->getDefaultLanguage();
		if(array_key_exists($default_language, $translations)) {
			return $translations[$default_language];
		}

		/* Check for english translation. */
		if(array_key_exists('en', $translations)) {
			return $translations['en'];
		}

		/* Pick the first translation available. */
		if(count($translations) > 0) {
			$languages = array_keys($translations);
			return $translations[$languages[0]];
		}

		/* We don't have anything to return. */
		throw new Exception('Nothing to return from translation.');
	}


	/**
	 * Translate a attribute name.
	 *
	 * @param string $name  The attribute name.
	 * @return string  The translated attribute name, or the original attribute name if no translation was found.
	 */
	public function getAttributeTranslation($name) {

		/* Normalize attribute name. */
		$normName = strtolower($name);
		$normName = str_replace(":", "_", $normName);

		/* Check for an extra dictionary. */
		$extraDict = $this->configuration->getString('attributes.extradictionary', NULL);
		if ($extraDict !== NULL) {
			$dict = $this->getDictionary($extraDict);
			if (array_key_exists($normName, $dict)) {
				return $this->getTranslation($dict[$normName]);
			}
		}

		/* Search the default attribute dictionary. */
		$dict = $this->getDictionary('attributes');
		if (array_key_exists('attribute_' . $normName, $dict)) {
			return $this->getTranslation($dict['attribute_' . $normName]);
		}

		/* No translations found. */
		return $name;
	}


	/**
	 * Translate a tag into the current language, with a fallback to english.
	 *
	 * This function is used to look up a translation tag in dictionaries, and return the
	 * translation into the current language. If no translation into the current language can be
	 * found, english will be tried, and if that fails, placeholder text will be returned.
	 *
	 * An array can be passed as the tag. In that case, the array will be assumed to be on the
	 * form (language => text), and will be used as the source of translations.
	 *
	 * This function can also do replacements into the translated tag. It will search the
	 * translated tag for the keys provided in $replacements, and replace any found occurances
	 * with the value of the key.
	 *
	 * @param string|array $tag  A tag name for the translation which should be looked up, or an
	 *                           array with (language => text) mappings.
	 * @param array $replacements  An associative array of keys that should be replaced with
	 *                             values in the translated string.
	 * @return string  The translated tag, or a placeholder value if the tag wasn't found.
	 */
	public function t($tag, $replacements = array(), $fallbackdefault = true, $oldreplacements = array(), $striptags = FALSE) {
		if(!is_array($replacements)) {

			/* Old style call to t(...). Print warning to log. */
			$backtrace = debug_backtrace();
			$where = $backtrace[0]['file'] . ':' . $backtrace[0]['line'];
			SimpleSAML_Logger::warning('Deprecated use of SimpleSAML_Template::t(...) at ' . $where .
				'. Please update the code to use the new style of parameters.');

			/* For backwards compatibility. */
			if(!$replacements && $this->getTag($tag) === NULL) {
				SimpleSAML_Logger::warning('Code which uses $fallbackdefault === FALSE shouls be' .
					' updated to use the getTag-method instead.');
				return NULL;
			}

			$replacements = $oldreplacements;
		}

		if(is_array($tag)) {
			$tagData = $tag;
		} else {
			$tagData = $this->getTag($tag);
			if($tagData === NULL) {
				/* Tag not found. */
				SimpleSAML_Logger::info('Template: Looking up [' . $tag . ']: not translated at all.');
				return $this->t_not_translated($tag, $fallbackdefault);
			}
		}

		$translated = $this->getTranslation($tagData);

#		if (!empty($replacements)){		echo('<pre> [' . $tag . ']'); print_r($replacements); exit; }
		foreach ($replacements as $k => $v) {
			/* try to translate if no replacement is given */
			if ($v == NULL) $v = $this->t($k);
			$translated = str_replace($k, $v, $translated);
		}
		return $translated;
	}
	
	/**
	 * Return the string that should be used when no translation was found.
	 *
	 * @param $tag				A name tag of the string that should be returned.
	 * @param $fallbacktag		If set to TRUE and string was not found in any languages, return 
	 * 					the tag it self. If FALSE return NULL.
	 */
	private function t_not_translated($tag, $fallbacktag) {
		if ($fallbacktag) {
			return 'not translated (' . $tag . ')';
		} else {
			return $tag;
		}
	}
	
	
	/**
	 * You can include translation inline instead of putting translation
	 * in dictionaries. This function is reccomended to only be used from dynamic
	 * data, or when the translation is already provided from an external source, as
	 * a database or in metadata.
	 *
	 * @param $tag         The tag that has a translation
	 * @param $translation The translation array
	 */
	public function includeInlineTranslation($tag, $translation) {
		
		if (is_string($translation)) {
			$translation = array('en' => $translation);
		} elseif (!is_array($translation)) {
			throw new Exception("Inline translation should be string or array. Is " . gettype($translation) . " now!");
		}
		
		SimpleSAML_Logger::debug('Template: Adding inline language translation for tag [' . $tag . ']');
		$this->langtext[$tag] = $translation;
	}
	
	/**
	 * Include language file from the dictionaries directory.
	 *
	 * @param $file         File name of dictionary to include
	 * @param $otherConfig  Optionally provide a different configuration object than
	 *  the one provided in the constructor to be used to find the dictionary directory.
	 *  This enables the possiblity of combining dictionaries inside simpleSAMLphp 
	 *  distribution with external dictionaries.
	 */
	public function includeLanguageFile($file, $otherConfig = null) {
		
		$filebase = null;
		if (!empty($otherConfig)) {
			$filebase = $otherConfig->getPathValue('dictionarydir', 'dictionaries/');
		} else {
			$filebase = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
		}
		

		$lang = $this->readDictionaryFile($filebase . $file);
		SimpleSAML_Logger::debug('Template: Merging language array. Loading [' . $file . ']');
		$this->langtext = array_merge($this->langtext, $lang);
	}


	/**
	 * Read a dictionary file in json format.
	 *
	 * @param string $filename  The absolute path to the dictionary file, minus the .definition.json ending.
	 * @return array  The translation array from the file.
	 */
	private function readDictionaryJSON($filename) {
		$definitionFile = $filename . '.definition.json';
		assert('file_exists($definitionFile)');

		$fileContent = file_get_contents($definitionFile);
		$lang = json_decode($fileContent, TRUE);

		if (empty($lang)) {
			SimpleSAML_Logger::error('Invalid dictionary definition file [' . $definitionFile . ']');
			return array();
		}

		$translationFile = $filename . '.translation.json';
		if (file_exists($translationFile)) {
			$fileContent = file_get_contents($translationFile);
			$moreTrans = json_decode($fileContent, TRUE);
			if (!empty($moreTrans)) {
				$lang = self::lang_merge($lang, $moreTrans);
			}
		}

		return $lang;
	}


	/**
	 * Read a dictionary file in PHP format.
	 *
	 * @param string $filename  The absolute path to the dictionary file.
	 * @return array  The translation array from the file.
	 */
	private function readDictionaryPHP($filename) {
		$phpFile = $filename . '.php';
		assert('file_exists($phpFile)');

		$lang = NULL;
		include($phpFile);
		if (isset($lang)) {
			return $lang;
		}

		return array();
	}


	/**
	 * Read a dictionary file.
	 *
	 * @param $filename  The absolute path to the dictionary file.
	 * @return The translation array which was found in the dictionary file.
	 */
	private function readDictionaryFile($filename) {
		assert('is_string($filename)');

		SimpleSAML_Logger::debug('Template: Reading [' . $filename . ']');

		$jsonFile = $filename . '.definition.json';
		if (file_exists($jsonFile)) {
			return $this->readDictionaryJSON($filename);
		}


		$phpFile = $filename . '.php';
		if (file_exists($phpFile)) {
			return $this->readDictionaryPHP($filename);
		}

		SimpleSAML_Logger::error($_SERVER['PHP_SELF'].' - Template: Could not find template file [' . $this->template . '] at [' . $filename . ']');
		return array();
	}


	// Merge two translation arrays.
	public static function lang_merge($def, $lang) {
		foreach($def AS $key => $value) {
			if (array_key_exists($key, $lang))
				$def[$key] = array_merge($value, $lang[$key]);
		}
		return $def;
	}


	/**
	 * Show the template to the user.
	 */
	public function show() {

		$filename = $this->findTemplatePath($this->template);
		require($filename);
	}


	/**
	 * Find template path.
	 *
	 * This function locates the given template based on the template name.
	 * It will first search for the template in the current theme directory, and
	 * then the default theme.
	 *
	 * The template name may be on the form <module name>:<template path>, in which case
	 * it will search for the template file in the given module.
	 *
	 * An error will be thrown if the template file couldn't be found.
	 *
	 * @param string $template  The relative path from the theme directory to the template file.
	 * @return string  The absolute path to the template file.
	 */
	private function findTemplatePath($template) {
		assert('is_string($template)');

		$tmp = explode(':', $template, 2);
		if (count($tmp) === 2) {
			$templateModule = $tmp[0];
			$templateName = $tmp[1];
		} else {
			$templateModule = 'default';
			$templateName = $tmp[0];
		}

		$tmp = explode(':', $this->configuration->getString('theme.use', 'default'), 2);
		if (count($tmp) === 2) {
			$themeModule = $tmp[0];
			$themeName = $tmp[1];
		} else {
			$themeModule = NULL;
			$themeName = $tmp[0];
		}


		/* First check the current theme. */
		if ($themeModule !== NULL) {
			/* .../module/<themeModule>/themes/<themeName>/<templateModule>/<templateName> */

			$filename = SimpleSAML_Module::getModuleDir($themeModule) . '/themes/' . $themeName . '/' . $templateModule . '/' . $templateName;
			
		} elseif ($templateModule !== 'default') {
			/* .../module/<templateModule>/templates/<themeName>/<templateName> */
			$filename = SimpleSAML_Module::getModuleDir($templateModule) . '/templates/' . $templateName;
			
		} else {
			/* .../templates/<theme>/<templateName> */
			$filename = $this->configuration->getPathValue('templatedir', 'templates/') . $templateName;
		}

		if (file_exists($filename)) {
			return $filename;
		}


		/* Not found in current theme. */
		SimpleSAML_Logger::debug($_SERVER['PHP_SELF'].' - Template: Could not find template file [' .
			$template . '] at [' . $filename . '] - now trying the base template');


		/* Try default theme. */
		if ($templateModule !== 'default') {
			/* .../module/<templateModule>/templates/<templateName> */
			$filename = SimpleSAML_Module::getModuleDir($templateModule) . '/templates/' . $templateName;
			
		} else {
			/* .../templates/<templateName> */
			$filename = $this->configuration->getPathValue('templatedir', 'templates/') . '/' . $templateName;
		}

		if (file_exists($filename)) {
			return $filename;
		}


		/* Not found in default template - log error and throw exception. */
		$error = 'Template: Could not find template file [' . $template . '] at [' . $filename . ']';
		SimpleSAML_Logger::critical($_SERVER['PHP_SELF'] . ' - ' . $error);

		throw new Exception($error);
	}


	/**
	 * Retrieve the user-selected language from a cookie.
	 *
	 * @return string|NULL  The language, or NULL if unset.
	 */
	public static function getLanguageCookie() {
		$config = SimpleSAML_Configuration::getInstance();
		$availableLanguages = $config->getArray('language.available', array('en'));
		$name = $config->getString('language.cookie.name', 'language');

		if (isset($_COOKIE[$name])) {
			$language = strtolower((string)$_COOKIE[$name]);
			if (in_array($language, $availableLanguages, TRUE)) {
				return $language;
			}
		}

		return NULL;
	}


	/**
	 * Set the user-selected language in a cookie.
	 *
	 * @param string $language  The language.
	 */
	public static function setLanguageCookie($language) {
		assert('is_string($language)');

		$language = strtolower($language);
		$config = SimpleSAML_Configuration::getInstance();
		$availableLanguages = $config->getArray('language.available', array('en'));

		if (!in_array($language, $availableLanguages, TRUE) || headers_sent()) {
			return;
		}

		$name = $config->getString('language.cookie.name', 'language');
		$domain = $config->getString('language.cookie.domain', NULL);
		$path = $config->getString('language.cookie.path', '/');
		$lifetime = $config->getInteger('language.cookie.lifetime', 60*60*24*900);

		if ($lifetime === 0) {
			$expire = 0;
		} else {
			$expire = time() + $lifetime;
		}

		setcookie($name, $language, $expire, $path, $domain);
	}

}

?>
