runff 1.0 commit

This commit is contained in:
rock64
2019-04-29 16:09:00 +02:00
commit f1567f989b
1268 changed files with 98652 additions and 0 deletions

122
lib/SimpleSAML/XHTML/EMail.php Executable file
View File

@@ -0,0 +1,122 @@
<?php
/**
* A minimalistic Emailer class. Creates and sends HTML emails.
*
* @author Andreas kre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_XHTML_EMail
{
private $to = null;
private $cc = null;
private $body = null;
private $from = null;
private $replyto = null;
private $subject = null;
private $headers = array();
/**
* Constructor
*/
public function __construct($to, $subject, $from = null, $cc = null, $replyto = null)
{
$this->to = $to;
$this->cc = $cc;
$this->from = $from;
$this->replyto = $replyto;
$this->subject = $subject;
}
/*
* @param string $body
* @return void
*/
public function setBody($body)
{
$this->body = $body;
}
/*
* @param string $body
* @return void
*/
private function getHTML($body)
{
return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>SimpleSAMLphp Email report</title>
<style type="text/css">
pre, div.box {
margin: .4em 2em .4em 1em;
padding: 4px;
}
pre {
background: #eee;
border: 1px solid #aaa;
}
</style>
</head>
<body>
<div class="container" style="background: #fafafa; border: 1px solid #eee; margin: 2em; padding: .6em;">
' . $body . '
</div>
</body>
</html>';
}
/*
* @return void
*/
public function send()
{
if ($this->to === null) {
throw new Exception('EMail field [to] is required and not set.');
} elseif ($this->subject === null) {
throw new Exception('EMail field [subject] is required and not set.');
} elseif ($this->body === null) {
throw new Exception('EMail field [body] is required and not set.');
}
$random_hash = bin2hex(openssl_random_pseudo_bytes(16));
if (isset($this->from)) {
$this->headers[]= 'From: ' . $this->from;
}
if (isset($this->replyto)) {
$this->headers[]= 'Reply-To: ' . $this->replyto;
}
$this->headers[] = 'Content-Type: multipart/alternative; boundary="simplesamlphp-' . $random_hash . '"';
$message = '
--simplesamlphp-' . $random_hash . '
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit
' . strip_tags(html_entity_decode($this->body)) . '
--simplesamlphp-' . $random_hash . '
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 8bit
' . $this->getHTML($this->body) . '
--simplesamlphp-' . $random_hash . '--
';
$headers = implode("\n", $this->headers);
$mail_sent = @mail($this->to, $this->subject, $message, $headers);
SimpleSAML\Logger::debug('Email: Sending e-mail to [' . $this->to . '] : ' . ($mail_sent ? 'OK' : 'Failed'));
if (!$mail_sent) {
throw new Exception('Error when sending e-mail');
}
}
}

598
lib/SimpleSAML/XHTML/IdPDisco.php Executable file
View File

@@ -0,0 +1,598 @@
<?php
/**
* This class implements a generic IdP discovery service, for use in various IdP
* discovery service pages. This should reduce code duplication.
*
* Experimental support added for Extended IdP Metadata Discovery Protocol by Andreas 2008-08-28
* More information: https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-idp-discovery.pdf
*
* @author Jaime Pérez <jaime.perez@uninett.no>, UNINETT AS.
* @author Olav Morken, UNINETT AS.
* @author Andreas Åkre Solberg <andreas@uninett.no>, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_XHTML_IdPDisco
{
/**
* An instance of the configuration class.
*
* @var SimpleSAML_Configuration
*/
protected $config;
/**
* The identifier of this discovery service.
*
* @var string
*/
protected $instance;
/**
* An instance of the metadata handler, which will allow us to fetch metadata about IdPs.
*
* @var SimpleSAML_Metadata_MetaDataStorageHandler
*/
protected $metadata;
/**
* The users session.
*
* @var SimpleSAML_Session
*/
protected $session;
/**
* The metadata sets we find allowed entities in, in prioritized order.
*
* @var array
*/
protected $metadataSets;
/**
* The entity id of the SP which accesses this IdP discovery service.
*
* @var string
*/
protected $spEntityId;
/**
* HTTP parameter from the request, indicating whether the discovery service
* can interact with the user or not.
*
* @var boolean
*/
protected $isPassive;
/**
* The SP request to set the IdPentityID...
*
* @var string|null
*/
protected $setIdPentityID = null;
/**
* The name of the query parameter which should contain the users choice of IdP.
* This option default to 'entityID' for Shibboleth compatibility.
*
* @var string
*/
protected $returnIdParam;
/**
* The list of scoped idp's. The intersection between the metadata idpList
* and scopedIDPList (given as a $_GET IDPList[] parameter) is presented to
* the user. If the intersection is empty the metadata idpList is used.
*
* @var array
*/
protected $scopedIDPList = array();
/**
* The URL the user should be redirected to after choosing an IdP.
*
* @var string
*/
protected $returnURL;
/**
* Initializes this discovery service.
*
* The constructor does the parsing of the request. If this is an invalid request, it will throw an exception.
*
* @param array $metadataSets Array with metadata sets we find remote entities in.
* @param string $instance The name of this instance of the discovery service.
*
* @throws Exception If the request is invalid.
*/
public function __construct(array $metadataSets, $instance)
{
assert(is_string($instance));
// initialize standard classes
$this->config = SimpleSAML_Configuration::getInstance();
$this->metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$this->session = SimpleSAML_Session::getSessionFromRequest();
$this->instance = $instance;
$this->metadataSets = $metadataSets;
$this->log('Accessing discovery service.');
// standard discovery service parameters
if (!array_key_exists('entityID', $_GET)) {
throw new Exception('Missing parameter: entityID');
} else {
$this->spEntityId = $_GET['entityID'];
}
if (!array_key_exists('returnIDParam', $_GET)) {
$this->returnIdParam = 'entityID';
} else {
$this->returnIdParam = $_GET['returnIDParam'];
}
$this->log('returnIdParam initially set to ['.$this->returnIdParam.']');
if (!array_key_exists('return', $_GET)) {
throw new Exception('Missing parameter: return');
} else {
$this->returnURL = \SimpleSAML\Utils\HTTP::checkURLAllowed($_GET['return']);
}
$this->isPassive = false;
if (array_key_exists('isPassive', $_GET)) {
if ($_GET['isPassive'] === 'true') {
$this->isPassive = true;
}
}
$this->log('isPassive initially set to ['.($this->isPassive ? 'TRUE' : 'FALSE').']');
if (array_key_exists('IdPentityID', $_GET)) {
$this->setIdPentityID = $_GET['IdPentityID'];
}
if (array_key_exists('IDPList', $_REQUEST)) {
$this->scopedIDPList = $_REQUEST['IDPList'];
}
}
/**
* Log a message.
*
* This is an helper function for logging messages. It will prefix the messages with our
* discovery service type.
*
* @param string $message The message which should be logged.
*/
protected function log($message)
{
SimpleSAML\Logger::info('idpDisco.'.$this->instance.': '.$message);
}
/**
* Retrieve cookie with the given name.
*
* This function will retrieve a cookie with the given name for the current discovery
* service type.
*
* @param string $name The name of the cookie.
*
* @return string The value of the cookie with the given name, or null if no cookie with that name exists.
*/
protected function getCookie($name)
{
$prefixedName = 'idpdisco_'.$this->instance.'_'.$name;
if (array_key_exists($prefixedName, $_COOKIE)) {
return $_COOKIE[$prefixedName];
} else {
return null;
}
}
/**
* Save cookie with the given name and value.
*
* This function will save a cookie with the given name and value for the current discovery
* service type.
*
* @param string $name The name of the cookie.
* @param string $value The value of the cookie.
*/
protected function setCookie($name, $value)
{
$prefixedName = 'idpdisco_'.$this->instance.'_'.$name;
$params = array(
// we save the cookies for 90 days
'lifetime' => (60 * 60 * 24 * 90),
// the base path for cookies. This should be the installation directory for SimpleSAMLphp
'path' => $this->config->getBasePath(),
'httponly' => false,
);
\SimpleSAML\Utils\HTTP::setCookie($prefixedName, $value, $params, false);
}
/**
* Validates the given IdP entity id.
*
* Takes a string with the IdP entity id, and returns the entity id if it is valid, or
* null if not.
*
* @param string|null $idp The entity id we want to validate. This can be null, in which case we will return null.
*
* @return string|null The entity id if it is valid, null if not.
*/
protected function validateIdP($idp)
{
if ($idp === null) {
return null;
}
if (!$this->config->getBoolean('idpdisco.validate', true)) {
return $idp;
}
foreach ($this->metadataSets as $metadataSet) {
try {
$this->metadata->getMetaData($idp, $metadataSet);
return $idp;
} catch (Exception $e) {
// continue
}
}
$this->log('Unable to validate IdP entity id ['.$idp.'].');
// the entity id wasn't valid
return null;
}
/**
* Retrieve the users choice of IdP.
*
* This function finds out which IdP the user has manually chosen, if any.
*
* @return string The entity id of the IdP the user has chosen, or null if the user has made no choice.
*/
protected function getSelectedIdP()
{
/* Parameter set from the Extended IdP Metadata Discovery Service Protocol, indicating that the user prefers
* this IdP.
*/
if (!empty($this->setIdPentityID)) {
return $this->validateIdP($this->setIdPentityID);
}
// user has clicked on a link, or selected the IdP from a drop-down list
if (array_key_exists('idpentityid', $_GET)) {
return $this->validateIdP($_GET['idpentityid']);
}
/* Search for the IdP selection from the form used by the links view. This form uses a name which equals
* idp_<entityid>, so we search for that.
*
* Unfortunately, php replaces periods in the name with underscores, and there is no reliable way to get them
* back. Therefore we do some quick and dirty parsing of the query string.
*/
$qstr = $_SERVER['QUERY_STRING'];
$matches = array();
if (preg_match('/(?:^|&)idp_([^=]+)=/', $qstr, $matches)) {
return $this->validateIdP(urldecode($matches[1]));
}
// no IdP chosen
return null;
}
/**
* Retrieve the users saved choice of IdP.
*
* @return string The entity id of the IdP the user has saved, or null if the user hasn't saved any choice.
*/
protected function getSavedIdP()
{
if (!$this->config->getBoolean('idpdisco.enableremember', false)) {
// saving of IdP choices is disabled
return null;
}
if ($this->getCookie('remember') === '1') {
$this->log('Return previously saved IdP because of remember cookie set to 1');
return $this->getPreviousIdP();
}
if ($this->isPassive) {
$this->log('Return previously saved IdP because of isPassive');
return $this->getPreviousIdP();
}
return null;
}
/**
* Retrieve the previous IdP the user used.
*
* @return string The entity id of the previous IdP the user used, or null if this is the first time.
*/
protected function getPreviousIdP()
{
return $this->validateIdP($this->getCookie('lastidp'));
}
/**
* Retrieve a recommended IdP based on the IP address of the client.
*
* @return string|null The entity ID of the IdP if one is found, or null if not.
*/
protected function getFromCIDRhint()
{
foreach ($this->metadataSets as $metadataSet) {
$idp = $this->metadata->getPreferredEntityIdFromCIDRhint($metadataSet, $_SERVER['REMOTE_ADDR']);
if (!empty($idp)) {
return $idp;
}
}
return null;
}
/**
* Try to determine which IdP the user should most likely use.
*
* This function will first look at the previous IdP the user has chosen. If the user
* hasn't chosen an IdP before, it will look at the IP address.
*
* @return string The entity id of the IdP the user should most likely use.
*/
protected function getRecommendedIdP()
{
$idp = $this->getPreviousIdP();
if ($idp !== null) {
$this->log('Preferred IdP from previous use ['.$idp.'].');
return $idp;
}
$idp = $this->getFromCIDRhint();
if (!empty($idp)) {
$this->log('Preferred IdP from CIDR hint ['.$idp.'].');
return $idp;
}
return null;
}
/**
* Save the current IdP choice to a cookie.
*
* @param string $idp The entityID of the IdP.
*/
protected function setPreviousIdP($idp)
{
assert(is_string($idp));
$this->log('Choice made ['.$idp.'] Setting cookie.');
$this->setCookie('lastidp', $idp);
}
/**
* Determine whether the choice of IdP should be saved.
*
* @return boolean True if the choice should be saved, false otherwise.
*/
protected function saveIdP()
{
if (!$this->config->getBoolean('idpdisco.enableremember', false)) {
// saving of IdP choices is disabled
return false;
}
if (array_key_exists('remember', $_GET)) {
return true;
}
return false;
}
/**
* Determine which IdP the user should go to, if any.
*
* @return string The entity id of the IdP the user should be sent to, or null if the user should choose.
*/
protected function getTargetIdP()
{
// first, check if the user has chosen an IdP
$idp = $this->getSelectedIdP();
if ($idp !== null) {
// the user selected this IdP. Save the choice in a cookie
$this->setPreviousIdP($idp);
if ($this->saveIdP()) {
$this->setCookie('remember', '1');
} else {
$this->setCookie('remember', '0');
}
return $idp;
}
$this->log('getSelectedIdP() returned null');
// check if the user has saved an choice earlier
$idp = $this->getSavedIdP();
if ($idp !== null) {
$this->log('Using saved choice ['.$idp.'].');
return $idp;
}
// the user has made no choice
return null;
}
/**
* Retrieve the list of IdPs which are stored in the metadata.
*
* @return array An array with entityid => metadata mappings.
*/
protected function getIdPList()
{
$idpList = array();
foreach ($this->metadataSets as $metadataSet) {
$newList = $this->metadata->getList($metadataSet);
/*
* Note that we merge the entities in reverse order. This ensures that it is the entity in the first
* metadata set that "wins" if two metadata sets have the same entity.
*/
$idpList = array_merge($newList, $idpList);
}
return $idpList;
}
/**
* Return the list of scoped idp
*
* @return array An array of IdP entities
*/
protected function getScopedIDPList()
{
return $this->scopedIDPList;
}
/**
* Filter the list of IdPs.
*
* This method returns the IdPs that comply with the following conditions:
* - The IdP does not have the 'hide.from.discovery' configuration option.
*
* @param array $list An associative array containing metadata for the IdPs to apply the filtering to.
*
* @return array An associative array containing metadata for the IdPs that were not filtered out.
*/
protected function filterList($list)
{
foreach ($list as $entity => $metadata) {
if (array_key_exists('hide.from.discovery', $metadata) && $metadata['hide.from.discovery'] === true) {
unset($list[$entity]);
}
}
return $list;
}
/**
* Check if an IdP is set or if the request is passive, and redirect accordingly.
*
* @return void If there is no IdP targeted and this is not a passive request.
*/
protected function start()
{
$idp = $this->getTargetIdp();
if ($idp !== null) {
$extDiscoveryStorage = $this->config->getString('idpdisco.extDiscoveryStorage', null);
if ($extDiscoveryStorage !== null) {
$this->log('Choice made ['.$idp.'] (Forwarding to external discovery storage)');
\SimpleSAML\Utils\HTTP::redirectTrustedURL($extDiscoveryStorage, array(
'entityID' => $this->spEntityId,
'IdPentityID' => $idp,
'returnIDParam' => $this->returnIdParam,
'isPassive' => 'true',
'return' => $this->returnURL
));
} else {
$this->log(
'Choice made ['.$idp.'] (Redirecting the user back. returnIDParam='.$this->returnIdParam.')'
);
\SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL, array($this->returnIdParam => $idp));
}
}
if ($this->isPassive) {
$this->log('Choice not made. (Redirecting the user back without answer)');
\SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL);
}
}
/**
* Handles a request to this discovery service.
*
* The IdP disco parameters should be set before calling this function.
*/
public function handleRequest()
{
$this->start();
// no choice made. Show discovery service page
$idpList = $this->getIdPList();
$idpList = $this->filterList($idpList);
$preferredIdP = $this->getRecommendedIdP();
$idpintersection = array_intersect(array_keys($idpList), $this->getScopedIDPList());
if (sizeof($idpintersection) > 0) {
$idpList = array_intersect_key($idpList, array_fill_keys($idpintersection, null));
}
$idpintersection = array_values($idpintersection);
if (sizeof($idpintersection) == 1) {
$this->log(
'Choice made ['.$idpintersection[0].'] (Redirecting the user back. returnIDParam='.
$this->returnIdParam.')'
);
\SimpleSAML\Utils\HTTP::redirectTrustedURL(
$this->returnURL,
array($this->returnIdParam => $idpintersection[0])
);
}
/*
* Make use of an XHTML template to present the select IdP choice to the user. Currently the supported options
* is either a drop down menu or a list view.
*/
switch ($this->config->getString('idpdisco.layout', 'links')) {
case 'dropdown':
$templateFile = 'selectidp-dropdown.php';
break;
case 'links':
$templateFile = 'selectidp-links.php';
break;
default:
throw new Exception('Invalid value for the \'idpdisco.layout\' option.');
}
$t = new SimpleSAML_XHTML_Template($this->config, $templateFile, 'disco');
$t->data['idplist'] = $idpList;
$t->data['preferredidp'] = $preferredIdP;
$t->data['return'] = $this->returnURL;
$t->data['returnIDParam'] = $this->returnIdParam;
$t->data['entityID'] = $this->spEntityId;
$t->data['urlpattern'] = htmlspecialchars(\SimpleSAML\Utils\HTTP::getSelfURLNoQuery());
$t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', false);
$t->show();
}
}

724
lib/SimpleSAML/XHTML/Template.php Executable file
View File

@@ -0,0 +1,724 @@
<?php
/**
* A minimalistic XHTML PHP based template system implemented for SimpleSAMLphp.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @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 <title> and <h1>
if (isset($this->data['header']) && !isset($this->data['pagetitle'])) {
$this->data['pagetitle'] = $this->data['header'];
}
if (!isset($this->data['pagetitle'])) {
$this->data['pagetitle'] = 'SimpleSAMLphp';
}
}
/**
* Show the template to the user.
*/
public function show()
{
if ($this->twig !== false) {
$this->twigDefaultContext();
if ($this->controller) {
$this->controller->display($this->data);
}
echo $this->twig->render($this->twig_template, $this->data);
} else {
$filename = $this->findTemplatePath($this->template);
require($filename);
}
}
/**
* Find module the template is in, if any
*
* @param string $template The relative path from the theme directory to the template file.
*
* @return array An array with the name of the module and template
*/
private function findModuleAndTemplateName($template)
{
$tmp = explode(':', $template, 2);
return (count($tmp) === 2) ? array($tmp[0], $tmp[1]) : array(null, $tmp[0]);
}
/**
* 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.
*
* @param string $template The relative path from the theme directory to the template file.
*
* @return string The absolute path to the template file.
*
* @throws Exception If the template file couldn't be found.
*/
private function findTemplatePath($template, $throw_exception = true)
{
assert(is_string($template));
list($templateModule, $templateName) = $this->findModuleAndTemplateName($template);
$templateModule = ($templateModule !== null) ? $templateModule : 'default';
// first check the current theme
if ($this->theme['module'] !== null) {
// .../module/<themeModule>/themes/<themeName>/<templateModule>/<templateName>
$filename = \SimpleSAML\Module::getModuleDir($this->theme['module']).
'/themes/'.$this->theme['name'].'/'.$templateModule.'/'.$templateName;
} elseif ($templateModule !== 'default') {
// .../module/<templateModule>/templates/<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
if ($throw_exception) {
// 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);
} else {
// missing template expected, return NULL
return null;
}
}
/**
* Return the internal translator object used by this template.
*
* @return \SimpleSAML\Locale\Translate The translator that will be used with this template.
*/
public function getTranslator()
{
return $this->translator;
}
/**
* Get the current instance of Twig in use.
*
* @return false|Twig_Environment The Twig instance in use, or false if Twig is not used.
*/
public function getTwig()
{
return $this->twig;
}
/*
* Deprecated methods of this interface, all of them should go away.
*/
/**
* @param $name
*
* @return string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguage()
* instead.
*/
public function getAttributeTranslation($name)
{
return $this->translator->getAttributeTranslation($name);
}
/**
* @return string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguage()
* instead.
*/
public function getLanguage()
{
return $this->translator->getLanguage()->getLanguage();
}
/**
* @param $language
* @param bool $setLanguageCookie
*
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguage()
* instead.
*/
public function setLanguage($language, $setLanguageCookie = true)
{
$this->translator->getLanguage()->setLanguage($language, $setLanguageCookie);
}
/**
* @return null|string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguageCookie()
* instead.
*/
public static function getLanguageCookie()
{
return \SimpleSAML\Locale\Language::getLanguageCookie();
}
/**
* @param $language
*
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguageCookie()
* instead.
*/
public static function setLanguageCookie($language)
{
\SimpleSAML\Locale\Language::setLanguageCookie($language);
}
/**
* Wraps Language->getLanguageList
*/
private function getLanguageList()
{
return $this->translator->getLanguage()->getLanguageList();
}
/**
* @param $tag
*
* @return array
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::getTag() instead.
*/
public function getTag($tag)
{
return $this->translator->getTag($tag);
}
/**
* Temporary wrapper for \SimpleSAML\Locale\Translate::getPreferredTranslation().
*
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::getPreferredTranslation() instead.
*/
public function getTranslation($translations)
{
return $this->translator->getPreferredTranslation($translations);
}
/**
* Includes 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);
}
/**
* Wraps Translate->includeInlineTranslation()
*
* @see \SimpleSAML\Locale\Translate::includeInlineTranslation()
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::includeInlineTranslation() instead.
*/
public function includeInlineTranslation($tag, $translation)
{
$this->translator->includeInlineTranslation($tag, $translation);
}
/**
* @param $file
* @param null $otherConfig
*
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::includeLanguageFile() instead.
*/
public function includeLanguageFile($file, $otherConfig = null)
{
$this->translator->includeLanguageFile($file, $otherConfig);
}
/**
* Wrap Language->isLanguageRTL
*/
private function isLanguageRTL()
{
return $this->translator->getLanguage()->isLanguageRTL();
}
/**
* Merge two translation arrays.
*
* @param array $def The array holding string definitions.
* @param array $lang The array holding translations for every string.
*
* @return array The recursive merge of both arrays.
* @deprecated This method will be removed in SimpleSAMLphp 2.0. Please use array_merge_recursive() instead.
*/
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;
}
/**
* Behave like Language->noop to mark a tag for translation but actually do it later.
*
* @see \SimpleSAML\Locale\Translate::noop()
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::noop() instead.
*/
public static function noop($tag)
{
return $tag;
}
/**
* Wrap Language->t to translate tag into the current language, with a fallback to english.
*
* @see \SimpleSAML\Locale\Translate::t()
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::t() instead.
*/
public function t(
$tag,
$replacements = array(),
$fallbackdefault = true,
$oldreplacements = array(),
$striptags = false
) {
return $this->translator->t($tag, $replacements, $fallbackdefault, $oldreplacements, $striptags);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace SimpleSAML\XHTML;
/**
* Interface that allows modules to run several hooks for templates.
*
* @package SimpleSAMLphp
*/
interface TemplateControllerInterface
{
/**
* Implement to modify the twig environment after its initialization (e.g. add filters or extensions).
*
* @param \Twig_Environment $twig The current twig environment.
*
* @return void
*/
public function setUpTwig(\Twig_Environment &$twig);
/**
* Implement to add, delete or modify the data passed to the template.
*
* This method will be called right before displaying the template.
*
* @param array $data The current data used by the template.
*
* @return void
*/
public function display(&$data);
}