* @package SimpleSAMLphp */ use JaimePerez\TwigConfigurableI18n\Twig\Environment as Twig_Environment; use JaimePerez\TwigConfigurableI18n\Twig\Extensions\Extension\I18n as Twig_Extensions_Extension_I18n; class SimpleSAML_XHTML_Template { /** * The data associated with this template, accessible within the template itself. * * @var array */ public $data = array(); /** * A translator instance configured to work with this template. * * @var \SimpleSAML\Locale\Translate */ private $translator; /** * The localization backend * * @var \SimpleSAML\Locale\Localization */ private $localization; /** * The configuration to use in this template. * * @var SimpleSAML_Configuration */ private $configuration; /** * The file to load in this template. * * @var string */ private $template = 'default.php'; /** * The twig environment. * * @var false|Twig_Environment */ private $twig; /** * The template name. * * @var string */ private $twig_template; /** * Current module, if any. */ private $module; /** * A template controller, if any. * * Used to intercept certain parts of the template handling, while keeping away unwanted/unexpected hooks. Set * the 'theme.controller' configuration option to a class that implements the * SimpleSAML\XHTML\TemplateControllerInterface interface to use it. * * @var SimpleSAML\XHTML\TemplateControllerInterface */ private $controller; /** * Whether we are using a non-default theme or not. * * If we are using a theme, this variable holds an array with two keys: "module" and "name", those being the name * of the module and the name of the theme, respectively. If we are using the default theme, the variable defaults * to false. * * @var bool|array */ private $theme; /** * Constructor * * @param SimpleSAML_Configuration $configuration Configuration object * @param string $template Which template file to load * @param string|null $defaultDictionary The default dictionary where tags will come from. */ public function __construct(\SimpleSAML_Configuration $configuration, $template, $defaultDictionary = null) { $this->configuration = $configuration; $this->template = $template; // TODO: do not remove the slash from the beginning, change the templates instead! $this->data['baseurlpath'] = ltrim($this->configuration->getBasePath(), '/'); // parse module and template name list($this->module) = $this->findModuleAndTemplateName($template); // parse config to find theme and module theme is in, if any list($this->theme['module'], $this->theme['name']) = self::findModuleAndTemplateName( $this->configuration->getString('theme.use', 'default') ); // initialize internationalization system $this->translator = new SimpleSAML\Locale\Translate($configuration, $defaultDictionary); $this->localization = new \SimpleSAML\Locale\Localization($configuration); // check if we need to attach a theme controller $controller = $this->configuration->getString('theme.controller', false); if ($controller && class_exists($controller) && class_implements($controller, '\SimpleSAML\XHTML\TemplateControllerInterface') ) { $this->controller = new $controller(); } $this->twig = $this->setupTwig(); } /** * Normalize the name of the template to one of the possible alternatives. * * @param string $templateName The template name to normalize. * @return string The filename we need to look for. */ private function normalizeTemplateName($templateName) { if (strripos($templateName, '.twig')) { return $templateName; } $phppos = strripos($templateName, '.php'); if ($phppos) { $templateName = substr($templateName, 0, $phppos); } $tplpos = strripos($templateName, '.tpl'); if ($tplpos) { $templateName = substr($templateName, 0, $tplpos); } return $templateName.'.twig'; } /** * Set up the places where twig can look for templates. * * @return Twig_Loader_Filesystem The twig template loader or false if the template does not exist. * @throws Twig_Error_Loader In case a failure occurs. */ private function setupTwigTemplatepaths() { $filename = $this->normalizeTemplateName($this->template); // get namespace if any list($namespace, $filename) = self::findModuleAndTemplateName($filename); $this->twig_template = ($namespace !== null) ? '@'.$namespace.'/'.$filename : $filename; $loader = new \Twig_Loader_Filesystem(); $templateDirs = $this->findThemeTemplateDirs(); if ($this->module) { $templateDirs[] = array($this->module => $this->getModuleTemplateDir($this->module)); } if ($this->theme['module']) { try { $templateDirs[] = array($this->theme['module'] => $this->getModuleTemplateDir($this->theme['module'])); } catch (\InvalidArgumentException $e) { // either the module is not enabled or it has no "templates" directory, ignore } } // default, themeless templates are checked last $templateDirs[] = array( \Twig_Loader_Filesystem::MAIN_NAMESPACE => $this->configuration->resolvePath('templates') ); foreach ($templateDirs as $entry) { $loader->addPath($entry[key($entry)], key($entry)); } return $loader; } /** * Setup twig. */ private function setupTwig() { $auto_reload = $this->configuration->getBoolean('template.auto_reload', true); $cache = $this->configuration->getString('template.cache', false); // set up template paths $loader = $this->setupTwigTemplatepaths(); // abort if twig template does not exist if (!$loader->exists($this->twig_template)) { return false; } // load extra i18n domains if ($this->module) { $this->localization->addModuleDomain($this->module); } if ($this->theme['module'] !== null && $this->theme['module'] !== $this->module) { $this->localization->addModuleDomain($this->theme['module']); } $options = array( 'cache' => $cache, 'auto_reload' => $auto_reload, 'translation_function' => array('\SimpleSAML\Locale\Translate', 'translateSingularNativeGettext'), 'translation_function_plural' => array('\SimpleSAML\Locale\Translate', 'translatePluralNativeGettext'), ); // set up translation if ($this->localization->i18nBackend === \SimpleSAML\Locale\Localization::GETTEXT_I18N_BACKEND) { $options['translation_function'] = array('\SimpleSAML\Locale\Translate', 'translateSingularGettext'); $options['translation_function_plural'] = array( '\SimpleSAML\Locale\Translate', 'translatePluralGettext' ); } // TODO: add a branch for the old SimpleSAMLphp backend $twig = new Twig_Environment($loader, $options); $twig->addExtension(new Twig_Extensions_Extension_I18n()); // initialize some basic context $langParam = $this->configuration->getString('language.parameter.name', 'language'); $twig->addGlobal('languageParameterName', $langParam); $twig->addGlobal('localeBackend', $this->configuration->getString('language.i18n.backend', 'SimpleSAMLphp')); $twig->addGlobal('currentLanguage', $this->translator->getLanguage()->getLanguage()); $twig->addGlobal('isRTL', false); // language RTL configuration if ($this->translator->getLanguage()->isLanguageRTL()) { $twig->addGlobal('isRTL', true); } $queryParams = $_GET; // add query parameters, in case we need them in the template if (isset($queryParams[$langParam])) { unset($queryParams[$langParam]); } $twig->addGlobal('queryParams', $queryParams); $twig->addGlobal('templateId', str_replace('.twig', '', $this->normalizeTemplateName($this->template))); $twig->addGlobal('isProduction', $this->configuration->getBoolean('production', true)); // add a filter for translations out of arrays $twig->addFilter( new \Twig_SimpleFilter( 'translateFromArray', array('\SimpleSAML\Locale\Translate', 'translateFromArray'), array('needs_context' => true) ) ); if ($this->controller) { $this->controller->setUpTwig($twig); } return $twig; } /** * Add overriding templates from the configured theme. * * @return array An array of module => templatedir lookups. */ private function findThemeTemplateDirs() { if ($this->theme['module'] === null) { // no module involved return array(); } // setup directories & namespaces $themeDir = \SimpleSAML\Module::getModuleDir($this->theme['module']).'/themes/'.$this->theme['name']; $subdirs = scandir($themeDir); if (empty($subdirs)) { // no subdirectories in the theme directory, nothing to do here // this is probably wrong, log a message \SimpleSAML\Logger::warning('Empty theme directory for theme "'.$this->theme['name'].'".'); return array(); } $themeTemplateDirs = array(); foreach ($subdirs as $entry) { // discard anything that's not a directory. Expression is negated to profit from lazy evaluation if (!($entry !== '.' && $entry !== '..' && is_dir($themeDir.'/'.$entry))) { continue; } // set correct name for the default namespace $ns = ($entry === 'default') ? \Twig_Loader_Filesystem::MAIN_NAMESPACE : $entry; $themeTemplateDirs[] = array($ns => $themeDir.'/'.$entry); } return $themeTemplateDirs; } /** * Get the template directory of a module, if it exists. * * @return string The templates directory of a module. * * @throws InvalidArgumentException If the module is not enabled or it has no templates directory. */ private function getModuleTemplateDir($module) { if (!\SimpleSAML\Module::isModuleEnabled($module)) { throw new InvalidArgumentException('The module \''.$module.'\' is not enabled.'); } $moduledir = \SimpleSAML\Module::getModuleDir($module); // check if module has a /templates dir, if so, append $templatedir = $moduledir.'/templates'; if (!is_dir($templatedir)) { throw new InvalidArgumentException('The module \''.$module.'\' has no templates directory.'); } return $templatedir; } /** * Add the templates from a given module. * * Note that the module must be installed, enabled, and contain a "templates" directory. * * @param string $module The module where we need to search for templates. * * @throws InvalidArgumentException If the module is not enabled or it has no templates directory. */ public function addTemplatesFromModule($module) { $dir = $this->getModuleTemplateDir($module); /** @var Twig_Loader_Filesystem $loader */ $loader = $this->twig->getLoader(); $loader->addPath($dir, $module); } /** * Generate an array for its use in the language bar, indexed by the ISO 639-2 codes of the languages available, * containing their localized names and the URL that should be used in order to change to that language. * * @return array The array containing information of all available languages. */ private function generateLanguageBar() { $languages = $this->translator->getLanguage()->getLanguageList(); $langmap = null; if (count($languages) > 1) { $parameterName = $this->getTranslator()->getLanguage()->getLanguageParameterName(); $langmap = array(); foreach ($languages as $lang => $current) { $lang = strtolower($lang); $langname = $this->translator->getLanguage()->getLanguageLocalizedName($lang); $url = false; if (!$current) { $url = htmlspecialchars(\SimpleSAML\Utils\HTTP::addURLParameters( '', array($parameterName => $lang) )); } $langmap[$lang] = array( 'name' => $langname, 'url' => $url, ); } } return $langmap; } /** * Set some default context */ private function twigDefaultContext() { // show language bar by default if (!isset($this->data['hideLanguageBar'])) { $this->data['hideLanguageBar'] = false; } // get languagebar $this->data['languageBar'] = null; if ($this->data['hideLanguageBar'] === false) { $languageBar = $this->generateLanguageBar(); if (is_null($languageBar)) { $this->data['hideLanguageBar'] = true; } else { $this->data['languageBar'] = $languageBar; } } // assure that there is a