Initial Commit
This commit is contained in:
commit
89ac56f31b
|
@ -0,0 +1 @@
|
|||
build/
|
|
@ -0,0 +1 @@
|
|||
libsrc
|
|
@ -0,0 +1,9 @@
|
|||
clean:
|
||||
rm -rf build
|
||||
|
||||
build:
|
||||
mkdir build
|
||||
ppm --no-intro --compile="libsrc" --directory="build"
|
||||
|
||||
install:
|
||||
ppm --no-prompt --fix-conflict --install="build/net.intellivoid.accounts.saml.ppm"
|
|
@ -0,0 +1,4 @@
|
|||
# Intellivoid Accounts SAML
|
||||
|
||||
This library implements SAML support, a library ported from https://simplesamlphp.org
|
||||
but rebuilt for PPM and Intellivoid's current infrastructure.
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Auth;
|
||||
|
||||
use SAML\Configuration;
|
||||
use SAML\Session;
|
||||
|
||||
/**
|
||||
* Factory class to get instances of \SimpleSAML\Auth\Simple for a given authentication source.
|
||||
*/
|
||||
class AuthenticationFactory
|
||||
{
|
||||
/** @var Configuration */
|
||||
protected $config;
|
||||
|
||||
/** @var Session */
|
||||
protected $session;
|
||||
|
||||
|
||||
public function __construct(Configuration $config, Session $session)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new instance of \SimpleSAML\Auth\Simple for the given authentication source.
|
||||
*
|
||||
* @param string $as The identifier of the authentication source, as indexed in the authsources.php configuration
|
||||
* file.
|
||||
*
|
||||
* @return Simple
|
||||
*/
|
||||
public function create($as)
|
||||
{
|
||||
return new Simple($as, $this->config, $this->session);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Auth;
|
||||
|
||||
use Exception;
|
||||
use SAML\Module\saml\Auth\Source\SP;
|
||||
use SAML\Session;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* Implements the default behaviour for authentication.
|
||||
*
|
||||
* This class contains an implementation for default behaviour when authenticating. It will
|
||||
* save the session information it got from the authentication client in the users session.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*
|
||||
* @deprecated This class will be removed in SSP 2.0.
|
||||
*/
|
||||
|
||||
class DefaultAuth
|
||||
{
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Use Source::initLogin() instead.
|
||||
* @param string $authId
|
||||
* @param string $return
|
||||
* @param string|null $errorURL
|
||||
* @param array $params
|
||||
* @return void
|
||||
*/
|
||||
public static function initLogin(
|
||||
$authId,
|
||||
$return,
|
||||
$errorURL = null,
|
||||
array $params = []
|
||||
) {
|
||||
|
||||
$as = self::getAuthSource($authId);
|
||||
$as->initLogin($return, $errorURL, $params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use
|
||||
* State::getPersistentAuthData() instead.
|
||||
* @param array &$state
|
||||
* @return array
|
||||
*/
|
||||
public static function extractPersistentAuthState(array &$state)
|
||||
{
|
||||
return State::getPersistentAuthData($state);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use Source::loginCompleted() instead.
|
||||
* @param array $state
|
||||
* @return void
|
||||
*/
|
||||
public static function loginCompleted($state)
|
||||
{
|
||||
Source::loginCompleted($state);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0.
|
||||
* @param string $returnURL
|
||||
* @param string $authority
|
||||
* @return void
|
||||
*/
|
||||
public static function initLogoutReturn($returnURL, $authority)
|
||||
{
|
||||
assert(is_string($returnURL));
|
||||
assert(is_string($authority));
|
||||
|
||||
$session = Session::getSessionFromRequest();
|
||||
|
||||
$state = $session->getAuthData($authority, 'LogoutState');
|
||||
$session->doLogout($authority);
|
||||
|
||||
$state['\SimpleSAML\Auth\DefaultAuth.ReturnURL'] = $returnURL;
|
||||
$state['LogoutCompletedHandler'] = [get_class(), 'logoutCompleted'];
|
||||
|
||||
$as = Source::getById($authority);
|
||||
if ($as === null) {
|
||||
// The authority wasn't an authentication source...
|
||||
self::logoutCompleted($state);
|
||||
}
|
||||
|
||||
$as->logout($state);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0.
|
||||
* @param string $returnURL
|
||||
* @param string $authority
|
||||
* @return void
|
||||
*/
|
||||
public static function initLogout($returnURL, $authority)
|
||||
{
|
||||
assert(is_string($returnURL));
|
||||
assert(is_string($authority));
|
||||
|
||||
self::initLogoutReturn($returnURL, $authority);
|
||||
|
||||
Utils\HTTP::redirectTrustedURL($returnURL);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0.
|
||||
* @param array $state
|
||||
* @return void
|
||||
*/
|
||||
public static function logoutCompleted($state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
assert(array_key_exists('\SimpleSAML\Auth\DefaultAuth.ReturnURL', $state));
|
||||
|
||||
Utils\HTTP::redirectTrustedURL($state['\SimpleSAML\Auth\DefaultAuth.ReturnURL']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use Source::logoutCallback() instead.
|
||||
* @param array $state
|
||||
* @return void
|
||||
*/
|
||||
public static function logoutCallback($state)
|
||||
{
|
||||
Source::logoutCallback($state);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use
|
||||
* \SimpleSAML\Module\saml\Auth\Source\SP::handleUnsolicitedAuth() instead.
|
||||
* @param string $authId
|
||||
* @param array $state
|
||||
* @param string $redirectTo
|
||||
* @return void
|
||||
*/
|
||||
public static function handleUnsolicitedAuth($authId, array $state, $redirectTo)
|
||||
{
|
||||
SP::handleUnsolicitedAuth($authId, $state, $redirectTo);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return an authentication source by ID.
|
||||
*
|
||||
* @param string $id The id of the authentication source.
|
||||
* @return Source The authentication source.
|
||||
* @throws Exception If the $id does not correspond with an authentication source.
|
||||
*/
|
||||
private static function getAuthSource($id)
|
||||
{
|
||||
$as = Source::getById($id);
|
||||
if ($as === null) {
|
||||
throw new Exception('Invalid authentication source: ' . $id);
|
||||
}
|
||||
return $as;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Auth;
|
||||
|
||||
use Exception;
|
||||
use SAML\Logger;
|
||||
use SAML\Module\ldap\Auth\Ldap;
|
||||
|
||||
Logger::warning("The class \SimpleSAML\Auth\LDAP has been moved to the ldap module, please use \SimpleSAML\Module\saml\Auth\Ldap instead.");
|
||||
|
||||
/**
|
||||
* @deprecated To be removed in 2.0
|
||||
*/
|
||||
if (class_exists('\SimpleSAML\Module\ldap\Auth\Ldap')) {
|
||||
class_alias(Ldap::class, 'SimpleSAML\Auth\LDAP');
|
||||
} else {
|
||||
throw new Exception('The ldap module is either missing or disabled');
|
||||
}
|
|
@ -0,0 +1,391 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Auth;
|
||||
|
||||
use Exception;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Error\UnserializableException;
|
||||
use SAML\Logger;
|
||||
use SAML\Module;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* Class for implementing authentication processing chains for IdPs.
|
||||
*
|
||||
* This class implements a system for additional steps which should be taken by an IdP before
|
||||
* submitting a response to a SP. Examples of additional steps can be additional authentication
|
||||
* checks, or attribute consent requirements.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class ProcessingChain
|
||||
{
|
||||
/**
|
||||
* The list of remaining filters which should be applied to the state.
|
||||
*/
|
||||
const FILTERS_INDEX = '\SimpleSAML\Auth\ProcessingChain.filters';
|
||||
|
||||
|
||||
/**
|
||||
* The stage we use for completed requests.
|
||||
*/
|
||||
const COMPLETED_STAGE = '\SimpleSAML\Auth\ProcessingChain.completed';
|
||||
|
||||
|
||||
/**
|
||||
* The request parameter we will use to pass the state identifier when we redirect after
|
||||
* having completed processing of the state.
|
||||
*/
|
||||
const AUTHPARAM = 'AuthProcId';
|
||||
|
||||
|
||||
/**
|
||||
* All authentication processing filters, in the order they should be applied.
|
||||
*/
|
||||
private $filters;
|
||||
|
||||
|
||||
/**
|
||||
* Initialize an authentication processing chain for the given service provider
|
||||
* and identity provider.
|
||||
*
|
||||
* @param array $idpMetadata The metadata for the IdP.
|
||||
* @param array $spMetadata The metadata for the SP.
|
||||
* @param string $mode
|
||||
*/
|
||||
public function __construct($idpMetadata, $spMetadata, $mode = 'idp')
|
||||
{
|
||||
assert(is_array($idpMetadata));
|
||||
assert(is_array($spMetadata));
|
||||
|
||||
$this->filters = [];
|
||||
|
||||
$config = Configuration::getInstance();
|
||||
$configauthproc = $config->getArray('authproc.' . $mode, null);
|
||||
|
||||
if (!empty($configauthproc)) {
|
||||
$configfilters = self::parseFilterList($configauthproc);
|
||||
self::addFilters($this->filters, $configfilters);
|
||||
}
|
||||
|
||||
if (array_key_exists('authproc', $idpMetadata)) {
|
||||
$idpFilters = self::parseFilterList($idpMetadata['authproc']);
|
||||
self::addFilters($this->filters, $idpFilters);
|
||||
}
|
||||
|
||||
if (array_key_exists('authproc', $spMetadata)) {
|
||||
$spFilters = self::parseFilterList($spMetadata['authproc']);
|
||||
self::addFilters($this->filters, $spFilters);
|
||||
}
|
||||
|
||||
Logger::debug('Filter config for ' . $idpMetadata['entityid'] . '->' .
|
||||
$spMetadata['entityid'] . ': ' . str_replace("\n", '', var_export($this->filters, true)));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sort & merge filter configuration
|
||||
*
|
||||
* Inserts unsorted filters into sorted filter list. This sort operation is stable.
|
||||
*
|
||||
* @param array &$target Target filter list. This list must be sorted.
|
||||
* @param array $src Source filters. May be unsorted.
|
||||
* @return void
|
||||
*/
|
||||
private static function addFilters(&$target, $src)
|
||||
{
|
||||
assert(is_array($target));
|
||||
assert(is_array($src));
|
||||
|
||||
foreach ($src as $filter) {
|
||||
$fp = $filter->priority;
|
||||
|
||||
// Find insertion position for filter
|
||||
for ($i = count($target) - 1; $i >= 0; $i--) {
|
||||
if ($target[$i]->priority <= $fp) {
|
||||
// The new filter should be inserted after this one
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* $i now points to the filter which should preceede the current filter. */
|
||||
array_splice($target, $i + 1, 0, [$filter]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse an array of authentication processing filters.
|
||||
*
|
||||
* @param array $filterSrc Array with filter configuration.
|
||||
* @return array Array of ProcessingFilter objects.
|
||||
*/
|
||||
private static function parseFilterList($filterSrc)
|
||||
{
|
||||
assert(is_array($filterSrc));
|
||||
|
||||
$parsedFilters = [];
|
||||
|
||||
foreach ($filterSrc as $priority => $filter) {
|
||||
if (is_string($filter)) {
|
||||
$filter = ['class' => $filter];
|
||||
}
|
||||
|
||||
if (!is_array($filter)) {
|
||||
throw new Exception('Invalid authentication processing filter configuration: ' .
|
||||
'One of the filters wasn\'t a string or an array.');
|
||||
}
|
||||
|
||||
$parsedFilters[] = self::parseFilter($filter, $priority);
|
||||
}
|
||||
|
||||
return $parsedFilters;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse an authentication processing filter.
|
||||
*
|
||||
* @param array $config Array with the authentication processing filter configuration.
|
||||
* @param int $priority The priority of the current filter, (not included in the filter
|
||||
* definition.)
|
||||
* @return ProcessingFilter The parsed filter.
|
||||
*/
|
||||
private static function parseFilter($config, $priority)
|
||||
{
|
||||
assert(is_array($config));
|
||||
|
||||
if (!array_key_exists('class', $config)) {
|
||||
throw new Exception('Authentication processing filter without name given.');
|
||||
}
|
||||
|
||||
$className = Module::resolveClass(
|
||||
$config['class'],
|
||||
'Auth\Process',
|
||||
'\SimpleSAML\Auth\ProcessingFilter'
|
||||
);
|
||||
$config['%priority'] = $priority;
|
||||
unset($config['class']);
|
||||
|
||||
/** @var ProcessingFilter */
|
||||
return new $className($config, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process the given state.
|
||||
*
|
||||
* This function will only return if processing completes. If processing requires showing
|
||||
* a page to the user, we will not be able to return from this function. There are two ways
|
||||
* this can be handled:
|
||||
* - Redirect to a URL: We will redirect to the URL set in $state['ReturnURL'].
|
||||
* - Call a function: We will call the function set in $state['ReturnCall'].
|
||||
*
|
||||
* If an exception is thrown during processing, it should be handled by the caller of
|
||||
* this function. If the user has redirected to a different page, the exception will be
|
||||
* returned through the exception handler defined on the state array. See
|
||||
* State for more information.
|
||||
*
|
||||
* @param array &$state The state we are processing.
|
||||
* @return void
|
||||
*@throws UnserializableException
|
||||
* @throws \SAML\Error\Exception
|
||||
* @see State::EXCEPTION_HANDLER_URL
|
||||
* @see State::EXCEPTION_HANDLER_FUNC
|
||||
*
|
||||
* @see State
|
||||
*/
|
||||
public function processState(&$state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
assert(array_key_exists('ReturnURL', $state) || array_key_exists('ReturnCall', $state));
|
||||
assert(!array_key_exists('ReturnURL', $state) || !array_key_exists('ReturnCall', $state));
|
||||
|
||||
$state[self::FILTERS_INDEX] = $this->filters;
|
||||
|
||||
try {
|
||||
// TODO: remove this in SSP 2.0
|
||||
if (!array_key_exists('UserID', $state)) {
|
||||
// No unique user ID present. Attempt to add one.
|
||||
self::addUserID($state);
|
||||
}
|
||||
|
||||
while (count($state[self::FILTERS_INDEX]) > 0) {
|
||||
$filter = array_shift($state[self::FILTERS_INDEX]);
|
||||
$filter->process($state);
|
||||
}
|
||||
} catch (Error\Exception $e) {
|
||||
// No need to convert the exception
|
||||
throw $e;
|
||||
} catch (Exception $e) {
|
||||
/*
|
||||
* To be consistent with the exception we return after an redirect,
|
||||
* we convert this exception before returning it.
|
||||
*/
|
||||
throw new UnserializableException($e);
|
||||
}
|
||||
|
||||
// Completed
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Continues processing of the state.
|
||||
*
|
||||
* This function is used to resume processing by filters which for example needed to show
|
||||
* a page to the user.
|
||||
*
|
||||
* This function will never return. Exceptions thrown during processing will be passed
|
||||
* to whatever exception handler is defined in the state array.
|
||||
*
|
||||
* @param array $state The state we are processing.
|
||||
* @return void
|
||||
*/
|
||||
public static function resumeProcessing($state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
|
||||
while (count($state[self::FILTERS_INDEX]) > 0) {
|
||||
$filter = array_shift($state[self::FILTERS_INDEX]);
|
||||
try {
|
||||
$filter->process($state);
|
||||
} catch (Error\Exception $e) {
|
||||
State::throwException($state, $e);
|
||||
} catch (Exception $e) {
|
||||
$e = new UnserializableException($e);
|
||||
State::throwException($state, $e);
|
||||
}
|
||||
}
|
||||
|
||||
// Completed
|
||||
|
||||
assert(array_key_exists('ReturnURL', $state) || array_key_exists('ReturnCall', $state));
|
||||
assert(!array_key_exists('ReturnURL', $state) || !array_key_exists('ReturnCall', $state));
|
||||
|
||||
|
||||
if (array_key_exists('ReturnURL', $state)) {
|
||||
/*
|
||||
* Save state information, and redirect to the URL specified
|
||||
* in $state['ReturnURL'].
|
||||
*/
|
||||
$id = State::saveState($state, self::COMPLETED_STAGE);
|
||||
Utils\HTTP::redirectTrustedURL($state['ReturnURL'], [self::AUTHPARAM => $id]);
|
||||
} else {
|
||||
/* Pass the state to the function defined in $state['ReturnCall']. */
|
||||
|
||||
// We are done with the state array in the session. Delete it.
|
||||
State::deleteState($state);
|
||||
|
||||
$func = $state['ReturnCall'];
|
||||
assert(is_callable($func));
|
||||
|
||||
call_user_func($func, $state);
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process the given state passivly.
|
||||
*
|
||||
* Modules with user interaction are expected to throw an \SimpleSAML\Module\saml\Error\NoPassive exception
|
||||
* which are silently ignored. Exceptions of other types are passed further up the call stack.
|
||||
*
|
||||
* This function will only return if processing completes.
|
||||
*
|
||||
* @param array &$state The state we are processing.
|
||||
* @return void
|
||||
*/
|
||||
public function processStatePassive(&$state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
// Should not be set when calling this method
|
||||
assert(!array_key_exists('ReturnURL', $state));
|
||||
|
||||
// Notify filters about passive request
|
||||
$state['isPassive'] = true;
|
||||
|
||||
$state[self::FILTERS_INDEX] = $this->filters;
|
||||
|
||||
// TODO: remove this in SSP 2.0
|
||||
if (!array_key_exists('UserID', $state)) {
|
||||
// No unique user ID present. Attempt to add one.
|
||||
self::addUserID($state);
|
||||
}
|
||||
|
||||
while (count($state[self::FILTERS_INDEX]) > 0) {
|
||||
$filter = array_shift($state[self::FILTERS_INDEX]);
|
||||
try {
|
||||
$filter->process($state);
|
||||
} catch (Error\NoPassive $e) {
|
||||
// @deprecated will be removed in 2.0
|
||||
// Ignore \SimpleSAML\Error\NoPassive exceptions
|
||||
} catch (Module\saml\Error\NoPassive $e) {
|
||||
// Ignore \SimpleSAML\Module\saml\Error\NoPassive exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a state which has finished processing.
|
||||
*
|
||||
* @param string $id The state identifier.
|
||||
* @see State::parseStateID()
|
||||
* @return array|null The state referenced by the $id parameter.
|
||||
*/
|
||||
public static function fetchProcessedState($id)
|
||||
{
|
||||
assert(is_string($id));
|
||||
|
||||
return State::loadState($id, self::COMPLETED_STAGE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0.
|
||||
* @param array &$state
|
||||
* @return void
|
||||
*/
|
||||
private static function addUserID(&$state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
assert(array_key_exists('Attributes', $state));
|
||||
|
||||
if (isset($state['Destination']['userid.attribute'])) {
|
||||
$attributeName = $state['Destination']['userid.attribute'];
|
||||
Logger::debug("The 'userid.attribute' option has been deprecated.");
|
||||
} elseif (isset($state['Source']['userid.attribute'])) {
|
||||
$attributeName = $state['Source']['userid.attribute'];
|
||||
Logger::debug("The 'userid.attribute' option has been deprecated.");
|
||||
} else {
|
||||
// Default attribute
|
||||
$attributeName = 'eduPersonPrincipalName';
|
||||
}
|
||||
|
||||
if (!array_key_exists($attributeName, $state['Attributes'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$uid = $state['Attributes'][$attributeName];
|
||||
if (count($uid) === 0) {
|
||||
Logger::warning('Empty user id attribute [' . $attributeName . '].');
|
||||
return;
|
||||
}
|
||||
|
||||
if (count($uid) > 1) {
|
||||
Logger::warning('Multiple attribute values for user id attribute [' . $attributeName . '].');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: the attribute value should be trimmed
|
||||
$uid = $uid[0];
|
||||
|
||||
if (empty($uid)) {
|
||||
Logger::warning('Empty value in attribute ' . $attributeName . ". on user. Cannot set UserID.");
|
||||
return;
|
||||
}
|
||||
$state['UserID'] = $uid;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Auth;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Base class for authentication processing filters.
|
||||
*
|
||||
* All authentication processing filters must support serialization.
|
||||
*
|
||||
* The current request is stored in an associative array. It has the following defined attributes:
|
||||
* - 'Attributes' The attributes of the user.
|
||||
* - 'Destination' Metadata of the destination (SP).
|
||||
* - 'Source' Metadata of the source (IdP).
|
||||
*
|
||||
* It may also contain other attributes. If an authentication processing filter wishes to store other
|
||||
* information in it, it should have a name on the form 'module:filter:attributename', to avoid name
|
||||
* collisions.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
abstract class ProcessingFilter
|
||||
{
|
||||
/**
|
||||
* Priority of this filter.
|
||||
*
|
||||
* Used when merging IdP and SP processing chains.
|
||||
* The priority can be any integer. The default for most filters is 50. Filters may however
|
||||
* specify their own default, if they typically should be amongst the first or the last filters.
|
||||
*
|
||||
* The prioroty can also be overridden by the user by specifying the '%priority' option.
|
||||
*/
|
||||
public $priority = 50;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for a processing filter.
|
||||
*
|
||||
* Any processing filter which implements its own constructor must call this
|
||||
* constructor first.
|
||||
*
|
||||
* @param array &$config Configuration for this filter.
|
||||
* @param mixed $reserved For future use.
|
||||
*/
|
||||
public function __construct(&$config, $reserved)
|
||||
{
|
||||
assert(is_array($config));
|
||||
|
||||
if (array_key_exists('%priority', $config)) {
|
||||
$this->priority = $config['%priority'];
|
||||
if (!is_int($this->priority)) {
|
||||
throw new Exception('Invalid priority: ' . var_export($this->priority, true));
|
||||
}
|
||||
unset($config['%priority']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a request.
|
||||
*
|
||||
* When a filter returns from this function, it is assumed to have completed its task.
|
||||
*
|
||||
* @param array &$request The request we are currently processing.
|
||||
*/
|
||||
abstract public function process(&$request);
|
||||
}
|
|
@ -0,0 +1,399 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Auth;
|
||||
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Error\AuthSource;
|
||||
use SAML\Module;
|
||||
use SAML\Session;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* Helper class for simple authentication applications.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class Simple
|
||||
{
|
||||
/**
|
||||
* The id of the authentication source we are accessing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $authSource;
|
||||
|
||||
/** @var Configuration */
|
||||
protected $app_config;
|
||||
|
||||
/** @var Session */
|
||||
protected $session;
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance with the specified authsource.
|
||||
*
|
||||
* @param string $authSource The id of the authentication source.
|
||||
* @param Configuration|null $config Optional configuration to use.
|
||||
* @param Session|null $session Optional session to use.
|
||||
*/
|
||||
public function __construct($authSource, Configuration $config = null, Session $session = null)
|
||||
{
|
||||
assert(is_string($authSource));
|
||||
|
||||
if ($config === null) {
|
||||
$config = Configuration::getInstance();
|
||||
}
|
||||
$this->authSource = $authSource;
|
||||
$this->app_config = $config->getConfigItem('application');
|
||||
|
||||
if ($session === null) {
|
||||
$session = Session::getSessionFromRequest();
|
||||
}
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the implementing authentication source.
|
||||
*
|
||||
* @return Source The authentication source.
|
||||
*
|
||||
* @throws AuthSource If the requested auth source is unknown.
|
||||
*/
|
||||
public function getAuthSource()
|
||||
{
|
||||
$as = Source::getById($this->authSource);
|
||||
if ($as === null) {
|
||||
throw new AuthSource($this->authSource, 'Unknown authentication source.');
|
||||
}
|
||||
return $as;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the user is authenticated.
|
||||
*
|
||||
* This function checks if the user is authenticated with the default authentication source selected by the
|
||||
* 'default-authsource' option in 'config.php'.
|
||||
*
|
||||
* @return bool True if the user is authenticated, false if not.
|
||||
*/
|
||||
public function isAuthenticated()
|
||||
{
|
||||
return $this->session->isValid($this->authSource);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Require the user to be authenticated.
|
||||
*
|
||||
* If the user is authenticated, this function returns immediately.
|
||||
*
|
||||
* If the user isn't authenticated, this function will authenticate the user with the authentication source, and
|
||||
* then return the user to the current page.
|
||||
*
|
||||
* This function accepts an array $params, which controls some parts of the authentication. See the login()
|
||||
* method for a description.
|
||||
*
|
||||
* @param array $params Various options to the authentication request. See the documentation.
|
||||
* @return void
|
||||
*/
|
||||
public function requireAuth(array $params = [])
|
||||
{
|
||||
if ($this->session->isValid($this->authSource)) {
|
||||
// Already authenticated
|
||||
return;
|
||||
}
|
||||
|
||||
$this->login($params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start an authentication process.
|
||||
*
|
||||
* This function accepts an array $params, which controls some parts of the authentication. The accepted parameters
|
||||
* depends on the authentication source being used. Some parameters are generic:
|
||||
* - 'ErrorURL': A URL that should receive errors from the authentication.
|
||||
* - 'KeepPost': If the current request is a POST request, keep the POST data until after the authentication.
|
||||
* - 'ReturnTo': The URL the user should be returned to after authentication.
|
||||
* - 'ReturnCallback': The function we should call after the user has finished authentication.
|
||||
*
|
||||
* Please note: this function never returns.
|
||||
*
|
||||
* @param array $params Various options to the authentication request.
|
||||
* @return void
|
||||
*/
|
||||
public function login(array $params = [])
|
||||
{
|
||||
if (array_key_exists('KeepPost', $params)) {
|
||||
$keepPost = (bool) $params['KeepPost'];
|
||||
} else {
|
||||
$keepPost = true;
|
||||
}
|
||||
|
||||
if (array_key_exists('ReturnTo', $params)) {
|
||||
$returnTo = (string) $params['ReturnTo'];
|
||||
} else {
|
||||
if (array_key_exists('ReturnCallback', $params)) {
|
||||
$returnTo = (array) $params['ReturnCallback'];
|
||||
} else {
|
||||
$returnTo = Utils\HTTP::getSelfURL();
|
||||
}
|
||||
}
|
||||
|
||||
if (is_string($returnTo) && $keepPost && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$returnTo = Utils\HTTP::getPOSTRedirectURL($returnTo, $_POST);
|
||||
}
|
||||
|
||||
if (array_key_exists('ErrorURL', $params)) {
|
||||
$errorURL = (string) $params['ErrorURL'];
|
||||
} else {
|
||||
$errorURL = null;
|
||||
}
|
||||
|
||||
|
||||
if (!isset($params[State::RESTART]) && is_string($returnTo)) {
|
||||
/*
|
||||
* A URL to restart the authentication, in case the user bookmarks
|
||||
* something, e.g. the discovery service page.
|
||||
*/
|
||||
$restartURL = $this->getLoginURL($returnTo);
|
||||
$params[State::RESTART] = $restartURL;
|
||||
}
|
||||
|
||||
$as = $this->getAuthSource();
|
||||
$as->initLogin($returnTo, $errorURL, $params);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log the user out.
|
||||
*
|
||||
* This function logs the user out. It will never return. By default, it will cause a redirect to the current page
|
||||
* after logging the user out, but a different URL can be given with the $params parameter.
|
||||
*
|
||||
* Generic parameters are:
|
||||
* - 'ReturnTo': The URL the user should be returned to after logout.
|
||||
* - 'ReturnCallback': The function that should be called after logout.
|
||||
* - 'ReturnStateParam': The parameter we should return the state in when redirecting.
|
||||
* - 'ReturnStateStage': The stage the state array should be saved with.
|
||||
*
|
||||
* @param string|array|null $params Either the URL the user should be redirected to after logging out, or an array
|
||||
* with parameters for the logout. If this parameter is null, we will return to the current page.
|
||||
* @return void
|
||||
*/
|
||||
public function logout($params = null)
|
||||
{
|
||||
assert(is_array($params) || is_string($params) || $params === null);
|
||||
|
||||
if ($params === null) {
|
||||
$params = Utils\HTTP::getSelfURL();
|
||||
}
|
||||
|
||||
if (is_string($params)) {
|
||||
$params = [
|
||||
'ReturnTo' => $params,
|
||||
];
|
||||
}
|
||||
|
||||
assert(is_array($params));
|
||||
assert(isset($params['ReturnTo']) || isset($params['ReturnCallback']));
|
||||
|
||||
if (isset($params['ReturnStateParam']) || isset($params['ReturnStateStage'])) {
|
||||
assert(isset($params['ReturnStateParam'], $params['ReturnStateStage']));
|
||||
}
|
||||
|
||||
if ($this->session->isValid($this->authSource)) {
|
||||
$state = $this->session->getAuthData($this->authSource, 'LogoutState');
|
||||
if ($state !== null) {
|
||||
$params = array_merge($state, $params);
|
||||
}
|
||||
|
||||
$this->session->doLogout($this->authSource);
|
||||
|
||||
$params['LogoutCompletedHandler'] = [get_class(), 'logoutCompleted'];
|
||||
|
||||
$as = Source::getById($this->authSource);
|
||||
if ($as !== null) {
|
||||
$as->logout($params);
|
||||
}
|
||||
}
|
||||
|
||||
self::logoutCompleted($params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when logout operation completes.
|
||||
*
|
||||
* This function never returns.
|
||||
*
|
||||
* @param array $state The state after the logout.
|
||||
* @return void
|
||||
*/
|
||||
public static function logoutCompleted($state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
assert(isset($state['ReturnTo']) || isset($state['ReturnCallback']));
|
||||
|
||||
if (isset($state['ReturnCallback'])) {
|
||||
call_user_func($state['ReturnCallback'], $state);
|
||||
assert(false);
|
||||
} else {
|
||||
$params = [];
|
||||
if (isset($state['ReturnStateParam']) || isset($state['ReturnStateStage'])) {
|
||||
assert(isset($state['ReturnStateParam'], $state['ReturnStateStage']));
|
||||
$stateID = State::saveState($state, $state['ReturnStateStage']);
|
||||
$params[$state['ReturnStateParam']] = $stateID;
|
||||
}
|
||||
Utils\HTTP::redirectTrustedURL($state['ReturnTo'], $params);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve attributes of the current user.
|
||||
*
|
||||
* This function will retrieve the attributes of the current user if the user is authenticated. If the user isn't
|
||||
* authenticated, it will return an empty array.
|
||||
*
|
||||
* @return array The users attributes.
|
||||
*/
|
||||
public function getAttributes()
|
||||
{
|
||||
if (!$this->isAuthenticated()) {
|
||||
// Not authenticated
|
||||
return [];
|
||||
}
|
||||
|
||||
// Authenticated
|
||||
return $this->session->getAuthData($this->authSource, 'Attributes');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve authentication data.
|
||||
*
|
||||
* @param string $name The name of the parameter, e.g. 'Attributes', 'Expire' or 'saml:sp:IdP'.
|
||||
*
|
||||
* @return mixed|null The value of the parameter, or null if it isn't found or we are unauthenticated.
|
||||
*/
|
||||
public function getAuthData($name)
|
||||
{
|
||||
assert(is_string($name));
|
||||
|
||||
if (!$this->isAuthenticated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->session->getAuthData($this->authSource, $name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve all authentication data.
|
||||
*
|
||||
* @return array|null All persistent authentication data, or null if we aren't authenticated.
|
||||
*/
|
||||
public function getAuthDataArray()
|
||||
{
|
||||
if (!$this->isAuthenticated()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->session->getAuthState($this->authSource);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a URL that can be used to log the user in.
|
||||
*
|
||||
* @param string|null $returnTo The page the user should be returned to afterwards. If this parameter is null, the
|
||||
* user will be returned to the current page.
|
||||
*
|
||||
* @return string A URL which is suitable for use in link-elements.
|
||||
*/
|
||||
public function getLoginURL($returnTo = null)
|
||||
{
|
||||
assert($returnTo === null || is_string($returnTo));
|
||||
|
||||
if ($returnTo === null) {
|
||||
$returnTo = Utils\HTTP::getSelfURL();
|
||||
}
|
||||
|
||||
$login = Module::getModuleURL('core/as_login.php', [
|
||||
'AuthId' => $this->authSource,
|
||||
'ReturnTo' => $returnTo,
|
||||
]);
|
||||
|
||||
return $login;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a URL that can be used to log the user out.
|
||||
*
|
||||
* @param string|null $returnTo The page the user should be returned to afterwards. If this parameter is null, the
|
||||
* user will be returned to the current page.
|
||||
*
|
||||
* @return string A URL which is suitable for use in link-elements.
|
||||
*/
|
||||
public function getLogoutURL($returnTo = null)
|
||||
{
|
||||
assert($returnTo === null || is_string($returnTo));
|
||||
|
||||
if ($returnTo === null) {
|
||||
$returnTo = Utils\HTTP::getSelfURL();
|
||||
}
|
||||
|
||||
$logout = Module::getModuleURL('core/as_logout.php', [
|
||||
'AuthId' => $this->authSource,
|
||||
'ReturnTo' => $returnTo,
|
||||
]);
|
||||
|
||||
return $logout;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a URL and modify it according to the application/baseURL configuration option, if present.
|
||||
*
|
||||
* @param string|null $url The URL to process, or null if we want to use the current URL. Both partial and full
|
||||
* URLs can be used as a parameter. The maximum precedence is given to the application/baseURL configuration option,
|
||||
* then the URL specified (if it specifies scheme, host and port) and finally the environment observed in the
|
||||
* server.
|
||||
*
|
||||
* @return string The URL modified according to the precedence rules.
|
||||
*/
|
||||
protected function getProcessedURL($url = null)
|
||||
{
|
||||
if ($url === null) {
|
||||
$url = Utils\HTTP::getSelfURL();
|
||||
}
|
||||
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME);
|
||||
$host = parse_url($url, PHP_URL_HOST) ? : Utils\HTTP::getSelfHost();
|
||||
$port = parse_url($url, PHP_URL_PORT) ? : (
|
||||
$scheme ? '' : ltrim(Utils\HTTP::getServerPort(), ':')
|
||||
);
|
||||
$scheme = $scheme ? : (Utils\HTTP::getServerHTTPS() ? 'https' : 'http');
|
||||
$path = parse_url($url, PHP_URL_PATH) ? : '/';
|
||||
$query = parse_url($url, PHP_URL_QUERY) ? : '';
|
||||
$fragment = parse_url($url, PHP_URL_FRAGMENT) ? : '';
|
||||
|
||||
$port = !empty($port) ? ':' . $port : '';
|
||||
if (($scheme === 'http' && $port === ':80') || ($scheme === 'https' && $port === ':443')) {
|
||||
$port = '';
|
||||
}
|
||||
|
||||
$base = trim($this->app_config->getString(
|
||||
'baseURL',
|
||||
$scheme . '://' . $host . $port
|
||||
), '/');
|
||||
return $base . $path . ($query ? '?' . $query : '') . ($fragment ? '#' . $fragment : '');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,541 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Auth;
|
||||
|
||||
use Exception;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Logger;
|
||||
use SAML\Module;
|
||||
use SAML\Session;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* This class defines a base class for authentication source.
|
||||
*
|
||||
* An authentication source is any system which somehow authenticate the user.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
abstract class Source
|
||||
{
|
||||
/**
|
||||
* The authentication source identifier. This identifier can be used to look up this object, for example when
|
||||
* returning from a login form.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $authId;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for an authentication source.
|
||||
*
|
||||
* Any authentication source which implements its own constructor must call this
|
||||
* constructor first.
|
||||
*
|
||||
* @param array $info Information about this authentication source.
|
||||
* @param array &$config Configuration for this authentication source.
|
||||
*/
|
||||
public function __construct($info, &$config)
|
||||
{
|
||||
assert(is_array($info));
|
||||
assert(is_array($config));
|
||||
|
||||
assert(array_key_exists('AuthId', $info));
|
||||
$this->authId = $info['AuthId'];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get sources of a specific type.
|
||||
*
|
||||
* @param string $type The type of the authentication source.
|
||||
*
|
||||
* @return Source[] Array of \SimpleSAML\Auth\Source objects of the specified type.
|
||||
* @throws Exception If the authentication source is invalid.
|
||||
*/
|
||||
public static function getSourcesOfType($type)
|
||||
{
|
||||
assert(is_string($type));
|
||||
|
||||
$config = Configuration::getConfig('authsources.php');
|
||||
|
||||
$ret = [];
|
||||
|
||||
$sources = $config->getOptions();
|
||||
foreach ($sources as $id) {
|
||||
$source = $config->getArray($id);
|
||||
|
||||
self::validateSource($source, $id);
|
||||
|
||||
if ($source[0] !== $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ret[] = self::parseAuthSource($id, $source);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the ID of this authentication source.
|
||||
*
|
||||
* @return string The ID of this authentication source.
|
||||
*/
|
||||
public function getAuthId()
|
||||
{
|
||||
return $this->authId;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a request.
|
||||
*
|
||||
* If an authentication source returns from this function, it is assumed to have
|
||||
* authenticated the user, and should have set elements in $state with the attributes
|
||||
* of the user.
|
||||
*
|
||||
* If the authentication process requires additional steps which make it impossible to
|
||||
* complete before returning from this function, the authentication source should
|
||||
* save the state, and at a later stage, load the state, update it with the authentication
|
||||
* information about the user, and call completeAuth with the state array.
|
||||
*
|
||||
* @param array &$state Information about the current authentication.
|
||||
* @return void
|
||||
*/
|
||||
abstract public function authenticate(&$state);
|
||||
|
||||
|
||||
/**
|
||||
* Reauthenticate an user.
|
||||
*
|
||||
* This function is called by the IdP to give the authentication source a chance to
|
||||
* interact with the user even in the case when the user is already authenticated.
|
||||
*
|
||||
* @param array &$state Information about the current authentication.
|
||||
* @return void
|
||||
*/
|
||||
public function reauthenticate(array &$state)
|
||||
{
|
||||
assert(isset($state['ReturnCallback']));
|
||||
|
||||
// the default implementation just copies over the previous authentication data
|
||||
$session = Session::getSessionFromRequest();
|
||||
$data = $session->getAuthState($this->authId);
|
||||
if ($data === null) {
|
||||
throw new Error\NoState();
|
||||
}
|
||||
|
||||
foreach ($data as $k => $v) {
|
||||
$state[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Complete authentication.
|
||||
*
|
||||
* This function should be called if authentication has completed. It will never return,
|
||||
* except in the case of exceptions. Exceptions thrown from this page should not be caught,
|
||||
* but should instead be passed to the top-level exception handler.
|
||||
*
|
||||
* @param array &$state Information about the current authentication.
|
||||
* @return void
|
||||
*/
|
||||
public static function completeAuth(&$state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
assert(array_key_exists('LoginCompletedHandler', $state));
|
||||
|
||||
State::deleteState($state);
|
||||
|
||||
$func = $state['LoginCompletedHandler'];
|
||||
assert(is_callable($func));
|
||||
|
||||
call_user_func($func, $state);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start authentication.
|
||||
*
|
||||
* This method never returns.
|
||||
*
|
||||
* @param string|array $return The URL or function we should direct the user to after authentication. If using a
|
||||
* URL obtained from user input, please make sure to check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
|
||||
* @param string|null $errorURL The URL we should direct the user to after failed authentication. Can be null, in
|
||||
* which case a standard error page will be shown. If using a URL obtained from user input, please make sure to
|
||||
* check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
|
||||
* @param array $params Extra information about the login. Different authentication requestors may provide different
|
||||
* information. Optional, will default to an empty array.
|
||||
* @return void
|
||||
*/
|
||||
public function initLogin($return, $errorURL = null, array $params = [])
|
||||
{
|
||||
assert(is_string($return) || is_array($return));
|
||||
assert(is_string($errorURL) || $errorURL === null);
|
||||
|
||||
$state = array_merge($params, [
|
||||
'\SimpleSAML\Auth\DefaultAuth.id' => $this->authId, // TODO: remove in 2.0
|
||||
'\SimpleSAML\Auth\Source.id' => $this->authId,
|
||||
'\SimpleSAML\Auth\DefaultAuth.Return' => $return, // TODO: remove in 2.0
|
||||
'\SimpleSAML\Auth\Source.Return' => $return,
|
||||
'\SimpleSAML\Auth\DefaultAuth.ErrorURL' => $errorURL, // TODO: remove in 2.0
|
||||
'\SimpleSAML\Auth\Source.ErrorURL' => $errorURL,
|
||||
'LoginCompletedHandler' => [get_class(), 'loginCompleted'],
|
||||
'LogoutCallback' => [get_class(), 'logoutCallback'],
|
||||
'LogoutCallbackState' => [
|
||||
'\SimpleSAML\Auth\DefaultAuth.logoutSource' => $this->authId, // TODO: remove in 2.0
|
||||
'\SimpleSAML\Auth\Source.logoutSource' => $this->authId,
|
||||
],
|
||||
]);
|
||||
|
||||
if (is_string($return)) {
|
||||
$state['\SimpleSAML\Auth\DefaultAuth.ReturnURL'] = $return; // TODO: remove in 2.0
|
||||
$state['\SimpleSAML\Auth\Source.ReturnURL'] = $return;
|
||||
}
|
||||
|
||||
if ($errorURL !== null) {
|
||||
$state[State::EXCEPTION_HANDLER_URL] = $errorURL;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->authenticate($state);
|
||||
} catch (Error\Exception $e) {
|
||||
State::throwException($state, $e);
|
||||
} catch (Exception $e) {
|
||||
$e = new Error\UnserializableException($e);
|
||||
State::throwException($state, $e);
|
||||
}
|
||||
self::loginCompleted($state);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when a login operation has finished.
|
||||
*
|
||||
* This method never returns.
|
||||
*
|
||||
* @param array $state The state after the login has completed.
|
||||
* @return void
|
||||
*/
|
||||
public static function loginCompleted($state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
assert(array_key_exists('\SimpleSAML\Auth\Source.Return', $state));
|
||||
assert(array_key_exists('\SimpleSAML\Auth\Source.id', $state));
|
||||
assert(array_key_exists('Attributes', $state));
|
||||
assert(!array_key_exists('LogoutState', $state) || is_array($state['LogoutState']));
|
||||
|
||||
$return = $state['\SimpleSAML\Auth\Source.Return'];
|
||||
|
||||
// save session state
|
||||
$session = Session::getSessionFromRequest();
|
||||
$authId = $state['\SimpleSAML\Auth\Source.id'];
|
||||
$session->doLogin($authId, State::getPersistentAuthData($state));
|
||||
|
||||
if (is_string($return)) {
|
||||
// redirect...
|
||||
Utils\HTTP::redirectTrustedURL($return);
|
||||
} else {
|
||||
call_user_func($return, $state);
|
||||
}
|
||||
assert(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log out from this authentication source.
|
||||
*
|
||||
* This function should be overridden if the authentication source requires special
|
||||
* steps to complete a logout operation.
|
||||
*
|
||||
* If the logout process requires a redirect, the state should be saved. Once the
|
||||
* logout operation is completed, the state should be restored, and completeLogout
|
||||
* should be called with the state. If this operation can be completed without
|
||||
* showing the user a page, or redirecting, this function should return.
|
||||
*
|
||||
* @param array &$state Information about the current logout operation.
|
||||
* @return void
|
||||
*/
|
||||
public function logout(&$state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
// default logout handler which doesn't do anything
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Complete logout.
|
||||
*
|
||||
* This function should be called after logout has completed. It will never return,
|
||||
* except in the case of exceptions. Exceptions thrown from this page should not be caught,
|
||||
* but should instead be passed to the top-level exception handler.
|
||||
*
|
||||
* @param array &$state Information about the current authentication.
|
||||
* @return void
|
||||
*/
|
||||
public static function completeLogout(&$state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
assert(array_key_exists('LogoutCompletedHandler', $state));
|
||||
|
||||
State::deleteState($state);
|
||||
|
||||
$func = $state['LogoutCompletedHandler'];
|
||||
assert(is_callable($func));
|
||||
|
||||
call_user_func($func, $state);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create authentication source object from configuration array.
|
||||
*
|
||||
* This function takes an array with the configuration for an authentication source object,
|
||||
* and returns the object.
|
||||
*
|
||||
* @param string $authId The authentication source identifier.
|
||||
* @param array $config The configuration.
|
||||
*
|
||||
* @return Source The parsed authentication source.
|
||||
* @throws Exception If the authentication source is invalid.
|
||||
*/
|
||||
private static function parseAuthSource($authId, $config)
|
||||
{
|
||||
assert(is_string($authId));
|
||||
assert(is_array($config));
|
||||
|
||||
self::validateSource($config, $authId);
|
||||
|
||||
$id = $config[0];
|
||||
$info = ['AuthId' => $authId];
|
||||
$authSource = null;
|
||||
|
||||
unset($config[0]);
|
||||
|
||||
try {
|
||||
// Check whether or not there's a factory responsible for instantiating our Auth Source instance
|
||||
$factoryClass = Module::resolveClass(
|
||||
$id,
|
||||
'Auth\Source\Factory',
|
||||
'\SimpleSAML\Auth\SourceFactory'
|
||||
);
|
||||
|
||||
/** @var SourceFactory $factory */
|
||||
$factory = new $factoryClass();
|
||||
$authSource = $factory->create($info, $config);
|
||||
} catch (Exception $e) {
|
||||
// If not, instantiate the Auth Source here
|
||||
$className = Module::resolveClass($id, 'Auth\Source', '\SimpleSAML\Auth\Source');
|
||||
$authSource = new $className($info, $config);
|
||||
}
|
||||
|
||||
/** @var Source */
|
||||
return $authSource;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve authentication source.
|
||||
*
|
||||
* This function takes an id of an authentication source, and returns the
|
||||
* AuthSource object. If no authentication source with the given id can be found,
|
||||
* NULL will be returned.
|
||||
*
|
||||
* If the $type parameter is specified, this function will return an
|
||||
* authentication source of the given type. If no authentication source or if an
|
||||
* authentication source of a different type is found, an exception will be thrown.
|
||||
*
|
||||
* @param string $authId The authentication source identifier.
|
||||
* @param string|null $type The type of authentication source. If NULL, any type will be accepted.
|
||||
*
|
||||
* @return Source|null The AuthSource object, or NULL if no authentication
|
||||
* source with the given identifier is found.
|
||||
* @throws \SAML\Error\Exception If no such authentication source is found or it is invalid.
|
||||
*/
|
||||
public static function getById($authId, $type = null)
|
||||
{
|
||||
assert(is_string($authId));
|
||||
assert($type === null || is_string($type));
|
||||
|
||||
// for now - load and parse config file
|
||||
$config = Configuration::getConfig('authsources.php');
|
||||
|
||||
$authConfig = $config->getArray($authId, null);
|
||||
if ($authConfig === null) {
|
||||
if ($type !== null) {
|
||||
throw new Error\Exception(
|
||||
'No authentication source with id ' .
|
||||
var_export($authId, true) . ' found.'
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$ret = self::parseAuthSource($authId, $authConfig);
|
||||
|
||||
if ($type === null || $ret instanceof $type) {
|
||||
return $ret;
|
||||
}
|
||||
|
||||
// the authentication source doesn't have the correct type
|
||||
throw new Error\Exception(
|
||||
'Invalid type of authentication source ' .
|
||||
var_export($authId, true) . '. Was ' . var_export(get_class($ret), true) .
|
||||
', should be ' . var_export($type, true) . '.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when the authentication source receives an external logout request.
|
||||
*
|
||||
* @param array $state State array for the logout operation.
|
||||
* @return void
|
||||
*/
|
||||
public static function logoutCallback($state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
assert(array_key_exists('\SimpleSAML\Auth\Source.logoutSource', $state));
|
||||
|
||||
$source = $state['\SimpleSAML\Auth\Source.logoutSource'];
|
||||
|
||||
$session = Session::getSessionFromRequest();
|
||||
if (!$session->isValid($source)) {
|
||||
Logger::warning(
|
||||
'Received logout from an invalid authentication source ' .
|
||||
var_export($source, true)
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
$session->doLogout($source);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a logout callback association.
|
||||
*
|
||||
* This function adds a logout callback association, which allows us to initiate
|
||||
* a logout later based on the $assoc-value.
|
||||
*
|
||||
* Note that logout-associations exists per authentication source. A logout association
|
||||
* from one authentication source cannot be called from a different authentication source.
|
||||
*
|
||||
* @param string $assoc The identifier for this logout association.
|
||||
* @param array $state The state array passed to the authenticate-function.
|
||||
* @return void
|
||||
*/
|
||||
protected function addLogoutCallback($assoc, $state)
|
||||
{
|
||||
assert(is_string($assoc));
|
||||
assert(is_array($state));
|
||||
|
||||
if (!array_key_exists('LogoutCallback', $state)) {
|
||||
// the authentication requester doesn't have a logout callback
|
||||
return;
|
||||
}
|
||||
$callback = $state['LogoutCallback'];
|
||||
|
||||
if (array_key_exists('LogoutCallbackState', $state)) {
|
||||
$callbackState = $state['LogoutCallbackState'];
|
||||
} else {
|
||||
$callbackState = [];
|
||||
}
|
||||
|
||||
$id = strlen($this->authId) . ':' . $this->authId . $assoc;
|
||||
|
||||
$data = [
|
||||
'callback' => $callback,
|
||||
'state' => $callbackState,
|
||||
];
|
||||
|
||||
$session = Session::getSessionFromRequest();
|
||||
$session->setData(
|
||||
'\SimpleSAML\Auth\Source.LogoutCallbacks',
|
||||
$id,
|
||||
$data,
|
||||
Session::DATA_TIMEOUT_SESSION_END
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call a logout callback based on association.
|
||||
*
|
||||
* This function calls a logout callback based on an association saved with
|
||||
* addLogoutCallback(...).
|
||||
*
|
||||
* This function always returns.
|
||||
*
|
||||
* @param string $assoc The logout association which should be called.
|
||||
* @return void
|
||||
*/
|
||||
protected function callLogoutCallback($assoc)
|
||||
{
|
||||
assert(is_string($assoc));
|
||||
|
||||
$id = strlen($this->authId) . ':' . $this->authId . $assoc;
|
||||
|
||||
$session = Session::getSessionFromRequest();
|
||||
|
||||
$data = $session->getData('\SimpleSAML\Auth\Source.LogoutCallbacks', $id);
|
||||
if ($data === null) {
|
||||
// FIXME: fix for IdP-first flow (issue 397) -> reevaluate logout callback infrastructure
|
||||
$session->doLogout($this->authId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
assert(is_array($data));
|
||||
assert(array_key_exists('callback', $data));
|
||||
assert(array_key_exists('state', $data));
|
||||
|
||||
$callback = $data['callback'];
|
||||
$callbackState = $data['state'];
|
||||
|
||||
$session->deleteData('\SimpleSAML\Auth\Source.LogoutCallbacks', $id);
|
||||
call_user_func($callback, $callbackState);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve list of authentication sources.
|
||||
*
|
||||
* @return array The id of all authentication sources.
|
||||
*/
|
||||
public static function getSources()
|
||||
{
|
||||
$config = Configuration::getOptionalConfig('authsources.php');
|
||||
|
||||
return $config->getOptions();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make sure that the first element of an auth source is its identifier.
|
||||
*
|
||||
* @param array $source An array with the auth source configuration.
|
||||
* @param string $id The auth source identifier.
|
||||
*
|
||||
* @throws Exception If the first element of $source is not an identifier for the auth source.
|
||||
* @return void
|
||||
*/
|
||||
protected static function validateSource($source, $id)
|
||||
{
|
||||
if (!array_key_exists(0, $source) || !is_string($source[0])) {
|
||||
throw new Exception(
|
||||
'Invalid authentication source \'' . $id .
|
||||
'\': First element must be a string which identifies the authentication source.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Auth;
|
||||
|
||||
interface SourceFactory
|
||||
{
|
||||
/**
|
||||
* @param array $info
|
||||
* @param array $config
|
||||
* @return Source
|
||||
*/
|
||||
public function create(array $info, array $config);
|
||||
}
|
|
@ -0,0 +1,435 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Auth;
|
||||
|
||||
use Exception;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Error\NoState;
|
||||
use SAML\Logger;
|
||||
use SAML\Session;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* This is a helper class for saving and loading state information.
|
||||
*
|
||||
* The state must be an associative array. This class will add additional keys to this
|
||||
* array. These keys will always start with '\SimpleSAML\Auth\State.'.
|
||||
*
|
||||
* It is also possible to add a restart URL to the state. If state information is lost, for
|
||||
* example because it timed out, or the user loaded a bookmarked page, the loadState function
|
||||
* will redirect to this URL. To use this, set $state[\SimpleSAML\Auth\State::RESTART] to this
|
||||
* URL.
|
||||
*
|
||||
* Both the saveState and the loadState function takes in a $stage parameter. This parameter is
|
||||
* a security feature, and is used to prevent the user from taking a state saved one place and
|
||||
* using it as input a different place.
|
||||
*
|
||||
* The $stage parameter must be a unique string. To maintain uniqueness, it must be on the form
|
||||
* "<classname>.<identifier>" or "<module>:<identifier>".
|
||||
*
|
||||
* There is also support for passing exceptions through the state.
|
||||
* By defining an exception handler when creating the state array, users of the state
|
||||
* array can call throwException with the state and the exception. This exception will
|
||||
* be passed to the handler defined by the EXCEPTION_HANDLER_URL or EXCEPTION_HANDLER_FUNC
|
||||
* elements of the state array. Note that internally this uses the request parameter name
|
||||
* defined in EXCEPTION_PARAM, which, for technical reasons, cannot contain a ".".
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class State
|
||||
{
|
||||
/**
|
||||
* The index in the state array which contains the identifier.
|
||||
*/
|
||||
const ID = '\SimpleSAML\Auth\State.id';
|
||||
|
||||
|
||||
/**
|
||||
* The index in the cloned state array which contains the identifier of the
|
||||
* original state.
|
||||
*/
|
||||
const CLONE_ORIGINAL_ID = '\SimpleSAML\Auth\State.cloneOriginalId';
|
||||
|
||||
|
||||
/**
|
||||
* The index in the state array which contains the current stage.
|
||||
*/
|
||||
const STAGE = '\SimpleSAML\Auth\State.stage';
|
||||
|
||||
|
||||
/**
|
||||
* The index in the state array which contains the restart URL.
|
||||
*/
|
||||
const RESTART = '\SimpleSAML\Auth\State.restartURL';
|
||||
|
||||
|
||||
/**
|
||||
* The index in the state array which contains the exception handler URL.
|
||||
*/
|
||||
const EXCEPTION_HANDLER_URL = '\SimpleSAML\Auth\State.exceptionURL';
|
||||
|
||||
|
||||
/**
|
||||
* The index in the state array which contains the exception handler function.
|
||||
*/
|
||||
const EXCEPTION_HANDLER_FUNC = '\SimpleSAML\Auth\State.exceptionFunc';
|
||||
|
||||
|
||||
/**
|
||||
* The index in the state array which contains the exception data.
|
||||
*/
|
||||
const EXCEPTION_DATA = '\SimpleSAML\Auth\State.exceptionData';
|
||||
|
||||
|
||||
/**
|
||||
* The stage of a state with an exception.
|
||||
*/
|
||||
const EXCEPTION_STAGE = '\SimpleSAML\Auth\State.exceptionStage';
|
||||
|
||||
|
||||
/**
|
||||
* The URL parameter which contains the exception state id.
|
||||
* Note that this does not contain a "." since it's used in the
|
||||
* _REQUEST superglobal that does not allow dots.
|
||||
*/
|
||||
const EXCEPTION_PARAM = '\SimpleSAML\Auth\State_exceptionId';
|
||||
|
||||
|
||||
/**
|
||||
* State timeout.
|
||||
*/
|
||||
private static $stateTimeout = null;
|
||||
|
||||
|
||||
/**
|
||||
* Get the persistent authentication state from the state array.
|
||||
*
|
||||
* @param array $state The state array to analyze.
|
||||
*
|
||||
* @return array The persistent authentication state.
|
||||
*/
|
||||
public static function getPersistentAuthData(array $state)
|
||||
{
|
||||
// save persistent authentication data
|
||||
$persistent = [];
|
||||
|
||||
if (array_key_exists('PersistentAuthData', $state)) {
|
||||
foreach ($state['PersistentAuthData'] as $key) {
|
||||
if (isset($state[$key])) {
|
||||
$persistent[$key] = $state[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add those that should always be included
|
||||
$mandatory = [
|
||||
'Attributes',
|
||||
'Expire',
|
||||
'LogoutState',
|
||||
'AuthInstant',
|
||||
'RememberMe',
|
||||
'saml:sp:NameID'
|
||||
];
|
||||
foreach ($mandatory as $key) {
|
||||
if (isset($state[$key])) {
|
||||
$persistent[$key] = $state[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $persistent;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the ID of a state array.
|
||||
*
|
||||
* Note that this function will not save the state.
|
||||
*
|
||||
* @param array &$state The state array.
|
||||
* @param bool $rawId Return a raw ID, without a restart URL. Defaults to FALSE.
|
||||
*
|
||||
* @return string Identifier which can be used to retrieve the state later.
|
||||
*/
|
||||
public static function getStateId(&$state, $rawId = false)
|
||||
{
|
||||
assert(is_array($state));
|
||||
assert(is_bool($rawId));
|
||||
|
||||
if (!array_key_exists(self::ID, $state)) {
|
||||
$state[self::ID] = Utils\Random::generateID();
|
||||
}
|
||||
|
||||
$id = $state[self::ID];
|
||||
|
||||
if ($rawId || !array_key_exists(self::RESTART, $state)) {
|
||||
// Either raw ID or no restart URL. In any case, return the raw ID.
|
||||
return $id;
|
||||
}
|
||||
|
||||
// We have a restart URL. Return the ID with that URL.
|
||||
return $id . ':' . $state[self::RESTART];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve state timeout.
|
||||
*
|
||||
* @return integer State timeout.
|
||||
*/
|
||||
private static function getStateTimeout()
|
||||
{
|
||||
if (self::$stateTimeout === null) {
|
||||
$globalConfig = Configuration::getInstance();
|
||||
self::$stateTimeout = $globalConfig->getInteger('session.state.timeout', 60 * 60);
|
||||
}
|
||||
|
||||
return self::$stateTimeout;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save the state.
|
||||
*
|
||||
* This function saves the state, and returns an id which can be used to
|
||||
* retrieve it later. It will also update the $state array with the identifier.
|
||||
*
|
||||
* @param array &$state The login request state.
|
||||
* @param string $stage The current stage in the login process.
|
||||
* @param bool $rawId Return a raw ID, without a restart URL.
|
||||
*
|
||||
* @return string Identifier which can be used to retrieve the state later.
|
||||
*/
|
||||
public static function saveState(&$state, $stage, $rawId = false)
|
||||
{
|
||||
assert(is_array($state));
|
||||
assert(is_string($stage));
|
||||
assert(is_bool($rawId));
|
||||
|
||||
$return = self::getStateId($state, $rawId);
|
||||
$id = $state[self::ID];
|
||||
|
||||
// Save stage
|
||||
$state[self::STAGE] = $stage;
|
||||
|
||||
// Save state
|
||||
$serializedState = serialize($state);
|
||||
$session = Session::getSessionFromRequest();
|
||||
$session->setData('\SimpleSAML\Auth\State', $id, $serializedState, self::getStateTimeout());
|
||||
|
||||
Logger::debug('Saved state: ' . var_export($return, true));
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clone the state.
|
||||
*
|
||||
* This function clones and returns the new cloned state.
|
||||
*
|
||||
* @param array $state The original request state.
|
||||
*
|
||||
* @return array Cloned state data.
|
||||
*/
|
||||
public static function cloneState(array $state)
|
||||
{
|
||||
$clonedState = $state;
|
||||
|
||||
if (array_key_exists(self::ID, $state)) {
|
||||
$clonedState[self::CLONE_ORIGINAL_ID] = $state[self::ID];
|
||||
unset($clonedState[self::ID]);
|
||||
|
||||
Logger::debug('Cloned state: ' . var_export($state[self::ID], true));
|
||||
} else {
|
||||
Logger::debug('Cloned state with undefined id.');
|
||||
}
|
||||
|
||||
return $clonedState;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve saved state.
|
||||
*
|
||||
* This function retrieves saved state information. If the state information has been lost,
|
||||
* it will attempt to restart the request by calling the restart URL which is embedded in the
|
||||
* state information. If there is no restart information available, an exception will be thrown.
|
||||
*
|
||||
* @param string $id State identifier (with embedded restart information).
|
||||
* @param string $stage The stage the state should have been saved in.
|
||||
* @param bool $allowMissing Whether to allow the state to be missing.
|
||||
*
|
||||
* @return array|null State information, or NULL if the state is missing and $allowMissing is true.
|
||||
*@throws Exception If the stage of the state is invalid and there's no URL defined to redirect to.
|
||||
*
|
||||
* @throws NoState If we couldn't find the state and there's no URL defined to redirect to.
|
||||
*/
|
||||
public static function loadState($id, $stage, $allowMissing = false)
|
||||
{
|
||||
assert(is_string($id));
|
||||
assert(is_string($stage));
|
||||
assert(is_bool($allowMissing));
|
||||
Logger::debug('Loading state: ' . var_export($id, true));
|
||||
|
||||
$sid = self::parseStateID($id);
|
||||
|
||||
$session = Session::getSessionFromRequest();
|
||||
$state = $session->getData('\SimpleSAML\Auth\State', $sid['id']);
|
||||
|
||||
if ($state === null) {
|
||||
// Could not find saved data
|
||||
if ($allowMissing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($sid['url'] === null) {
|
||||
throw new NoState();
|
||||
}
|
||||
|
||||
Utils\HTTP::redirectUntrustedURL($sid['url']);
|
||||
}
|
||||
|
||||
$state = unserialize($state);
|
||||
assert(is_array($state));
|
||||
assert(array_key_exists(self::ID, $state));
|
||||
assert(array_key_exists(self::STAGE, $state));
|
||||
|
||||
// Verify stage
|
||||
if ($state[self::STAGE] !== $stage) {
|
||||
/* This could be a user trying to bypass security, but most likely it is just
|
||||
* someone using the back-button in the browser. We try to restart the
|
||||
* request if that is possible. If not, show an error.
|
||||
*/
|
||||
|
||||
$msg = 'Wrong stage in state. Was \'' . $state[self::STAGE] .
|
||||
'\', should be \'' . $stage . '\'.';
|
||||
|
||||
Logger::warning($msg);
|
||||
|
||||
if ($sid['url'] === null) {
|
||||
throw new Exception($msg);
|
||||
}
|
||||
|
||||
Utils\HTTP::redirectUntrustedURL($sid['url']);
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete state.
|
||||
*
|
||||
* This function deletes the given state to prevent the user from reusing it later.
|
||||
*
|
||||
* @param array &$state The state which should be deleted.
|
||||
* @return void
|
||||
*/
|
||||
public static function deleteState(&$state)
|
||||
{
|
||||
assert(is_array($state));
|
||||
|
||||
if (!array_key_exists(self::ID, $state)) {
|
||||
// This state hasn't been saved
|
||||
return;
|
||||
}
|
||||
|
||||
Logger::debug('Deleting state: ' . var_export($state[self::ID], true));
|
||||
|
||||
$session = Session::getSessionFromRequest();
|
||||
$session->deleteData('\SimpleSAML\Auth\State', $state[self::ID]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Throw exception to the state exception handler.
|
||||
*
|
||||
* @param array $state The state array.
|
||||
* @param \SAML\Error\Exception $exception The exception.
|
||||
*
|
||||
* @return void
|
||||
*@throws \SAML\Error\Exception If there is no exception handler defined, it will just throw the $exception.
|
||||
*/
|
||||
public static function throwException($state, Error\Exception $exception)
|
||||
{
|
||||
assert(is_array($state));
|
||||
|
||||
if (array_key_exists(self::EXCEPTION_HANDLER_URL, $state)) {
|
||||
// Save the exception
|
||||
$state[self::EXCEPTION_DATA] = $exception;
|
||||
$id = self::saveState($state, self::EXCEPTION_STAGE);
|
||||
|
||||
// Redirect to the exception handler
|
||||
Utils\HTTP::redirectTrustedURL(
|
||||
$state[self::EXCEPTION_HANDLER_URL],
|
||||
[self::EXCEPTION_PARAM => $id]
|
||||
);
|
||||
} elseif (array_key_exists(self::EXCEPTION_HANDLER_FUNC, $state)) {
|
||||
// Call the exception handler
|
||||
$func = $state[self::EXCEPTION_HANDLER_FUNC];
|
||||
assert(is_callable($func));
|
||||
|
||||
call_user_func($func, $exception, $state);
|
||||
assert(false);
|
||||
} else {
|
||||
/*
|
||||
* No exception handler is defined for the current state.
|
||||
*/
|
||||
throw $exception;
|
||||
}
|
||||
throw new Exception(); // This should never happen
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve an exception state.
|
||||
*
|
||||
* @param string|null $id The exception id. Can be NULL, in which case it will be retrieved from the request.
|
||||
*
|
||||
* @return array|null The state array with the exception, or NULL if no exception was thrown.
|
||||
*/
|
||||
public static function loadExceptionState($id = null)
|
||||
{
|
||||
assert(is_string($id) || $id === null);
|
||||
|
||||
if ($id === null) {
|
||||
if (!array_key_exists(self::EXCEPTION_PARAM, $_REQUEST)) {
|
||||
// No exception
|
||||
return null;
|
||||
}
|
||||
$id = $_REQUEST[self::EXCEPTION_PARAM];
|
||||
}
|
||||
|
||||
/** @var array $state */
|
||||
$state = self::loadState($id, self::EXCEPTION_STAGE);
|
||||
assert(array_key_exists(self::EXCEPTION_DATA, $state));
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the ID and (optionally) a URL embedded in a StateID, in the form 'id:url'.
|
||||
*
|
||||
* @param string $stateId The state ID to use.
|
||||
*
|
||||
* @return array A hashed array with the ID and the URL (if any), in the 'id' and 'url' keys, respectively. If
|
||||
* there's no URL in the input parameter, NULL will be returned as the value for the 'url' key.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function parseStateID($stateId)
|
||||
{
|
||||
$tmp = explode(':', $stateId, 2);
|
||||
$id = $tmp[0];
|
||||
$url = null;
|
||||
if (count($tmp) === 2) {
|
||||
$url = $tmp[1];
|
||||
}
|
||||
return ['id' => $id, 'url' => $url];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Auth;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* A class that generates and verifies time-limited tokens.
|
||||
*
|
||||
* @deprecated This class was deprecated in 1.18 and will be removed in a future release
|
||||
*/
|
||||
|
||||
class TimeLimitedToken
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $secretSalt;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $lifetime;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $skew;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $algo;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new time-limited token.
|
||||
*
|
||||
* @param int $lifetime Token lifetime in seconds. Defaults to 900 (15 min).
|
||||
* @param string $secretSalt A random and unique salt per installation. Defaults to the salt in the configuration.
|
||||
* @param int $skew The allowed time skew (in seconds) to correct clock deviations. Defaults to 1 second.
|
||||
* @param string $algo The hash algorithm to use to generate the tokens. Defaults to SHA-256.
|
||||
*
|
||||
* @throws InvalidArgumentException if the given parameters are invalid.
|
||||
*/
|
||||
public function __construct($lifetime = 900, $secretSalt = null, $skew = 1, $algo = 'sha256')
|
||||
{
|
||||
if ($secretSalt === null) {
|
||||
$secretSalt = Utils\Config::getSecretSalt();
|
||||
}
|
||||
|
||||
if (!in_array($algo, hash_algos(), true)) {
|
||||
throw new InvalidArgumentException('Invalid hash algorithm "' . $algo . '"');
|
||||
}
|
||||
|
||||
$this->secretSalt = $secretSalt;
|
||||
$this->lifetime = $lifetime;
|
||||
$this->skew = $skew;
|
||||
$this->algo = $algo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add some given data to the current token. This data will be needed later too for token validation.
|
||||
*
|
||||
* This mechanism can be used to provide context for a token, such as a user identifier of the only subject
|
||||
* authorised to use it. Note also that multiple data can be added to the token. This means that upon validation,
|
||||
* not only the same data must be added, but also in the same order.
|
||||
*
|
||||
* @param string $data The data to incorporate into the current token.
|
||||
* @return void
|
||||
*/
|
||||
public function addVerificationData($data)
|
||||
{
|
||||
$this->secretSalt .= '|' . $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates a token value for a given offset.
|
||||
*
|
||||
* @param int $offset The offset to use.
|
||||
* @param int|null $time The time stamp to which the offset is relative to. Defaults to the current time.
|
||||
*
|
||||
* @return string The token for the given time and offset.
|
||||
*/
|
||||
private function calculateTokenValue($offset, $time = null)
|
||||
{
|
||||
if ($time === null) {
|
||||
$time = time();
|
||||
}
|
||||
// a secret salt that should be randomly generated for each installation
|
||||
return hash(
|
||||
$this->algo,
|
||||
$offset . ':' . floor(($time - $offset) / ($this->lifetime + $this->skew)) . ':' . $this->secretSalt
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a token that contains an offset and a token value, using the current offset.
|
||||
*
|
||||
* @return string A time-limited token with the offset respect to the beginning of its time slot prepended.
|
||||
*/
|
||||
public function generate()
|
||||
{
|
||||
$time = time();
|
||||
$current_offset = ($time - $this->skew) % ($this->lifetime + $this->skew);
|
||||
return dechex($current_offset) . '-' . $this->calculateTokenValue($current_offset, $time);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see generate
|
||||
* @deprecated This method will be removed in SSP 2.0. Use generate() instead.
|
||||
* @return string
|
||||
*/
|
||||
public function generate_token()
|
||||
{
|
||||
return $this->generate();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validates a token by calculating the token value for the provided offset and comparing it.
|
||||
*
|
||||
* @param string $token The token to validate.
|
||||
*
|
||||
* @return bool True if the given token is currently valid, false otherwise.
|
||||
*/
|
||||
public function validate($token)
|
||||
{
|
||||
$splittoken = explode('-', $token);
|
||||
if (count($splittoken) !== 2) {
|
||||
return false;
|
||||
}
|
||||
$offset = intval(hexdec($splittoken[0]));
|
||||
$value = $splittoken[1];
|
||||
return ($this->calculateTokenValue($offset) === $value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see validate
|
||||
* @deprecated This method will be removed in SSP 2.0. Use validate() instead.
|
||||
* @param string $token
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_token($token)
|
||||
{
|
||||
return $this->validate($token);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Implementation of the Shibboleth 1.3 Artifact binding.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
* @deprecated This class will be removed in a future release
|
||||
*/
|
||||
|
||||
namespace SAML\Bindings\Shib13;
|
||||
|
||||
use Exception;
|
||||
use SAML2\DOMDocumentFactory;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Utils;
|
||||
|
||||
class Artifact
|
||||
{
|
||||
/**
|
||||
* Parse the query string, and extract the SAMLart parameters.
|
||||
*
|
||||
* This function is required because each query contains multiple
|
||||
* artifact with the same parameter name.
|
||||
*
|
||||
* @return array The artifacts.
|
||||
*/
|
||||
private static function getArtifacts()
|
||||
{
|
||||
assert(array_key_exists('QUERY_STRING', $_SERVER));
|
||||
|
||||
// We need to process the query string manually, to capture all SAMLart parameters
|
||||
|
||||
$artifacts = [];
|
||||
|
||||
$elements = explode('&', $_SERVER['QUERY_STRING']);
|
||||
foreach ($elements as $element) {
|
||||
list($name, $value) = explode('=', $element, 2);
|
||||
$name = urldecode($name);
|
||||
$value = urldecode($value);
|
||||
|
||||
if ($name === 'SAMLart') {
|
||||
$artifacts[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $artifacts;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build the request we will send to the IdP.
|
||||
*
|
||||
* @param array $artifacts The artifacts we will request.
|
||||
* @return string The request, as an XML string.
|
||||
*/
|
||||
private static function buildRequest(array $artifacts)
|
||||
{
|
||||
$msg = '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'.
|
||||
'<SOAP-ENV:Body>'.
|
||||
'<samlp:Request xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol"'.
|
||||
' RequestID="'.Utils\Random::generateID().'"'.
|
||||
' MajorVersion="1" MinorVersion="1"'.
|
||||
' IssueInstant="'.Utils\Time::generateTimestamp().'"'.
|
||||
'>';
|
||||
|
||||
foreach ($artifacts as $a) {
|
||||
$msg .= '<samlp:AssertionArtifact>'.htmlspecialchars($a).'</samlp:AssertionArtifact>';
|
||||
}
|
||||
|
||||
$msg .= '</samlp:Request>'.
|
||||
'</SOAP-ENV:Body>'.
|
||||
'</SOAP-ENV:Envelope>';
|
||||
|
||||
return $msg;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract the response element from the SOAP response.
|
||||
*
|
||||
* @param string $soapResponse The SOAP response.
|
||||
* @return string The <saml1p:Response> element, as a string.
|
||||
* @throws Error\Exception
|
||||
*/
|
||||
private static function extractResponse($soapResponse)
|
||||
{
|
||||
assert(is_string($soapResponse));
|
||||
|
||||
try {
|
||||
$doc = DOMDocumentFactory::fromString($soapResponse);
|
||||
} catch (Exception $e) {
|
||||
throw new Error\Exception('Error parsing SAML 1 artifact response.');
|
||||
}
|
||||
|
||||
$soapEnvelope = $doc->firstChild;
|
||||
if (!Utils\XML::isDOMNodeOfType($soapEnvelope, 'Envelope', 'http://schemas.xmlsoap.org/soap/envelope/')) {
|
||||
throw new Error\Exception('Expected artifact response to contain a <soap:Envelope> element.');
|
||||
}
|
||||
|
||||
$soapBody = Utils\XML::getDOMChildren($soapEnvelope, 'Body', 'http://schemas.xmlsoap.org/soap/envelope/');
|
||||
if (count($soapBody) === 0) {
|
||||
throw new Error\Exception('Couldn\'t find <soap:Body> in <soap:Envelope>.');
|
||||
}
|
||||
$soapBody = $soapBody[0];
|
||||
|
||||
|
||||
$responseElement = Utils\XML::getDOMChildren($soapBody, 'Response', 'urn:oasis:names:tc:SAML:1.0:protocol');
|
||||
if (count($responseElement) === 0) {
|
||||
throw new Error\Exception('Couldn\'t find <saml1p:Response> in <soap:Body>.');
|
||||
}
|
||||
$responseElement = $responseElement[0];
|
||||
|
||||
/*
|
||||
* Save the <saml1p:Response> element. Note that we need to import it
|
||||
* into a new document, in order to preserve namespace declarations.
|
||||
*/
|
||||
$newDoc = DOMDocumentFactory::create();
|
||||
$newDoc->appendChild($newDoc->importNode($responseElement, true));
|
||||
$responseXML = $newDoc->saveXML();
|
||||
|
||||
return $responseXML;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function receives a SAML 1.1 artifact.
|
||||
*
|
||||
* @param Configuration $spMetadata The metadata of the SP.
|
||||
* @param Configuration $idpMetadata The metadata of the IdP.
|
||||
* @return string The <saml1p:Response> element, as an XML string.
|
||||
* @throws Error\Exception
|
||||
*/
|
||||
public static function receive(Configuration $spMetadata, Configuration $idpMetadata)
|
||||
{
|
||||
$artifacts = self::getArtifacts();
|
||||
$request = self::buildRequest($artifacts);
|
||||
|
||||
Utils\XML::debugSAMLMessage($request, 'out');
|
||||
|
||||
/** @var array $url */
|
||||
$url = $idpMetadata->getDefaultEndpoint(
|
||||
'ArtifactResolutionService',
|
||||
['urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding']
|
||||
);
|
||||
$url = $url['Location'];
|
||||
|
||||
$peerPublicKeys = $idpMetadata->getPublicKeys('signing', true);
|
||||
$certData = '';
|
||||
foreach ($peerPublicKeys as $key) {
|
||||
if ($key['type'] !== 'X509Certificate') {
|
||||
continue;
|
||||
}
|
||||
$certData .= "-----BEGIN CERTIFICATE-----\n".
|
||||
chunk_split($key['X509Certificate'], 64).
|
||||
"-----END CERTIFICATE-----\n";
|
||||
}
|
||||
|
||||
$file = Utils\System::getTempDir().DIRECTORY_SEPARATOR.sha1($certData).'.crt';
|
||||
if (!file_exists($file)) {
|
||||
Utils\System::writeFile($file, $certData);
|
||||
}
|
||||
|
||||
$spKeyCertFile = Utils\Config::getCertPath($spMetadata->getString('privatekey'));
|
||||
|
||||
$opts = [
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'cafile' => $file,
|
||||
'local_cert' => $spKeyCertFile,
|
||||
'capture_peer_cert' => true,
|
||||
'capture_peer_chain' => true,
|
||||
],
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'content' => $request,
|
||||
'header' => 'SOAPAction: http://www.oasis-open.org/committees/security'."\r\n".
|
||||
'Content-Type: text/xml',
|
||||
],
|
||||
];
|
||||
|
||||
// Fetch the artifact
|
||||
/** @var string $response */
|
||||
$response = Utils\HTTP::fetch($url, $opts);
|
||||
Utils\XML::debugSAMLMessage($response, 'in');
|
||||
|
||||
// Find the response in the SOAP message
|
||||
$response = self::extractResponse($response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of the Shibboleth 1.3 HTTP-POST binding.
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
* @deprecated This class will be removed in a future release
|
||||
*/
|
||||
|
||||
namespace SAML\Bindings\Shib13;
|
||||
|
||||
use Exception;
|
||||
use SAML2\DOMDocumentFactory;
|
||||
use SAML\Configuration;
|
||||
use SAML\Metadata\MetaDataStorageHandler;
|
||||
use SAML\Utils;
|
||||
use SAML\XML\Shib13\AuthnResponse;
|
||||
use SAML\XML\Signer;
|
||||
|
||||
class HTTPPost
|
||||
{
|
||||
/**
|
||||
* @var Configuration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* @var MetaDataStorageHandler
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for the \SimpleSAML\Bindings\Shib13\HTTPPost class.
|
||||
*
|
||||
* @param Configuration $configuration The configuration to use.
|
||||
* @param MetaDataStorageHandler $metadatastore A store where to find metadata.
|
||||
*/
|
||||
public function __construct(
|
||||
Configuration $configuration,
|
||||
MetaDataStorageHandler $metadatastore
|
||||
) {
|
||||
$this->configuration = $configuration;
|
||||
$this->metadata = $metadatastore;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send an authenticationResponse using HTTP-POST.
|
||||
*
|
||||
* @param string $response The response which should be sent.
|
||||
* @param Configuration $idpmd The metadata of the IdP which is sending the response.
|
||||
* @param Configuration $spmd The metadata of the SP which is receiving the response.
|
||||
* @param string|null $relayState The relaystate for the SP.
|
||||
* @param string $shire The shire which should receive the response.
|
||||
* @return void
|
||||
*/
|
||||
public function sendResponse(
|
||||
$response,
|
||||
Configuration $idpmd,
|
||||
Configuration $spmd,
|
||||
$relayState,
|
||||
$shire
|
||||
) {
|
||||
Utils\XML::checkSAMLMessage($response, 'saml11');
|
||||
|
||||
$privatekey = Utils\Crypto::loadPrivateKey($idpmd, true);
|
||||
$publickey = Utils\Crypto::loadPublicKey($idpmd, true);
|
||||
|
||||
$responsedom = DOMDocumentFactory::fromString(str_replace("\r", "", $response));
|
||||
|
||||
$responseroot = $responsedom->getElementsByTagName('Response')->item(0);
|
||||
$firstassertionroot = $responsedom->getElementsByTagName('Assertion')->item(0);
|
||||
|
||||
/* Determine what we should sign - either the Response element or the Assertion. The default is to sign the
|
||||
* Assertion, but that can be overridden by the 'signresponse' option in the SP metadata or
|
||||
* 'saml20.signresponse' in the global configuration.
|
||||
*
|
||||
* TODO: neither 'signresponse' nor 'shib13.signresponse' are valid options any longer. Remove!
|
||||
*/
|
||||
if ($spmd->hasValue('signresponse')) {
|
||||
$signResponse = $spmd->getBoolean('signresponse');
|
||||
} else {
|
||||
$signResponse = $this->configuration->getBoolean('shib13.signresponse', true);
|
||||
}
|
||||
|
||||
// check if we have an assertion to sign. Force to sign the response if not
|
||||
if ($firstassertionroot === null) {
|
||||
$signResponse = true;
|
||||
}
|
||||
|
||||
$signer = new Signer([
|
||||
'privatekey_array' => $privatekey,
|
||||
'publickey_array' => $publickey,
|
||||
'id' => ($signResponse ? 'ResponseID' : 'AssertionID'),
|
||||
]);
|
||||
|
||||
if ($idpmd->hasValue('certificatechain')) {
|
||||
$signer->addCertificate($idpmd->getString('certificatechain'));
|
||||
}
|
||||
|
||||
if ($signResponse) {
|
||||
// sign the response - this must be done after encrypting the assertion
|
||||
// we insert the signature before the saml2p:Status element
|
||||
$statusElements = Utils\XML::getDOMChildren($responseroot, 'Status', '@saml1p');
|
||||
assert(count($statusElements) === 1);
|
||||
$signer->sign($responseroot, $responseroot, $statusElements[0]);
|
||||
} else {
|
||||
// Sign the assertion
|
||||
$signer->sign($firstassertionroot, $firstassertionroot);
|
||||
}
|
||||
|
||||
$response = $responsedom->saveXML();
|
||||
|
||||
Utils\XML::debugSAMLMessage($response, 'out');
|
||||
|
||||
Utils\HTTP::submitPOSTData($shire, [
|
||||
'TARGET' => $relayState,
|
||||
'SAMLResponse' => base64_encode($response),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decode a received response.
|
||||
*
|
||||
* @param array $post POST data received.
|
||||
* @return AuthnResponse The response decoded into an object.
|
||||
* @throws Exception If there is no SAMLResponse parameter.
|
||||
*/
|
||||
public function decodeResponse($post)
|
||||
{
|
||||
assert(is_array($post));
|
||||
|
||||
if (!array_key_exists('SAMLResponse', $post)) {
|
||||
throw new Exception('Missing required SAMLResponse parameter.');
|
||||
}
|
||||
$rawResponse = $post['SAMLResponse'];
|
||||
$samlResponseXML = base64_decode($rawResponse);
|
||||
|
||||
Utils\XML::debugSAMLMessage($samlResponseXML, 'in');
|
||||
|
||||
Utils\XML::checkSAMLMessage($samlResponseXML, 'saml11');
|
||||
|
||||
$samlResponse = new AuthnResponse();
|
||||
$samlResponse->setXML($samlResponseXML);
|
||||
|
||||
if (array_key_exists('TARGET', $post)) {
|
||||
$samlResponse->setRelayState($post['TARGET']);
|
||||
}
|
||||
|
||||
return $samlResponse;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,298 @@
|
|||
<?php
|
||||
|
||||
namespace SAML;
|
||||
|
||||
use Exception;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use PDOStatement;
|
||||
|
||||
/**
|
||||
* This file implements functions to read and write to a group of database servers.
|
||||
*
|
||||
* This database class supports a single database, or a master/slave configuration with as many defined slaves as a
|
||||
* user would like.
|
||||
*
|
||||
* The goal of this class is to provide a single mechanism to connect to a database that can be reused by any component
|
||||
* within SimpleSAMLphp including modules. When using this class, the global configuration should be passed here, but in
|
||||
* the case of a module that has a good reason to use a different database, such as sqlauth, an alternative config file
|
||||
* can be provided.
|
||||
*
|
||||
* @author Tyler Antonio, University of Alberta. <tantonio@ualberta.ca>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class Database
|
||||
{
|
||||
/**
|
||||
* This variable holds the instance of the session - Singleton approach.
|
||||
*/
|
||||
private static $instance = [];
|
||||
|
||||
/**
|
||||
* PDO Object for the Master database server
|
||||
*/
|
||||
private $dbMaster;
|
||||
|
||||
/**
|
||||
* Array of PDO Objects for configured database slaves
|
||||
*/
|
||||
private $dbSlaves = [];
|
||||
|
||||
/**
|
||||
* Prefix to apply to the tables
|
||||
*/
|
||||
private $tablePrefix;
|
||||
|
||||
/**
|
||||
* Array with information on the last error occurred.
|
||||
*/
|
||||
private $lastError;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the current database instance. Will create a new one if there isn't an existing connection.
|
||||
*
|
||||
* @param Configuration $altConfig Optional: Instance of a \SimpleSAML\Configuration class
|
||||
*
|
||||
* @return Database The shared database connection.
|
||||
*/
|
||||
public static function getInstance($altConfig = null)
|
||||
{
|
||||
$config = ($altConfig) ? $altConfig : Configuration::getInstance();
|
||||
$instanceId = self::generateInstanceId($config);
|
||||
|
||||
// check if we already have initialized the session
|
||||
if (isset(self::$instance[$instanceId])) {
|
||||
return self::$instance[$instanceId];
|
||||
}
|
||||
|
||||
// create a new session
|
||||
self::$instance[$instanceId] = new Database($config);
|
||||
return self::$instance[$instanceId];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Private constructor that restricts instantiation to getInstance().
|
||||
*
|
||||
* @param Configuration $config Instance of the \SimpleSAML\Configuration class
|
||||
*/
|
||||
private function __construct($config)
|
||||
{
|
||||
$driverOptions = $config->getArray('database.driver_options', []);
|
||||
if ($config->getBoolean('database.persistent', true)) {
|
||||
$driverOptions = [PDO::ATTR_PERSISTENT => true];
|
||||
}
|
||||
|
||||
// connect to the master
|
||||
$this->dbMaster = $this->connect(
|
||||
$config->getString('database.dsn'),
|
||||
$config->getString('database.username', null),
|
||||
$config->getString('database.password', null),
|
||||
$driverOptions
|
||||
);
|
||||
|
||||
// connect to any configured slaves
|
||||
$slaves = $config->getArray('database.slaves', []);
|
||||
foreach ($slaves as $slave) {
|
||||
array_push(
|
||||
$this->dbSlaves,
|
||||
$this->connect(
|
||||
$slave['dsn'],
|
||||
$slave['username'],
|
||||
$slave['password'],
|
||||
$driverOptions
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->tablePrefix = $config->getString('database.prefix', '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate an Instance ID based on the database configuration.
|
||||
*
|
||||
* @param Configuration $config Configuration class
|
||||
*
|
||||
* @return string $instanceId
|
||||
*/
|
||||
private static function generateInstanceId($config)
|
||||
{
|
||||
$assembledConfig = [
|
||||
'master' => [
|
||||
'database.dsn' => $config->getString('database.dsn'),
|
||||
'database.username' => $config->getString('database.username', null),
|
||||
'database.password' => $config->getString('database.password', null),
|
||||
'database.prefix' => $config->getString('database.prefix', ''),
|
||||
'database.persistent' => $config->getBoolean('database.persistent', false),
|
||||
],
|
||||
'slaves' => $config->getArray('database.slaves', []),
|
||||
];
|
||||
|
||||
return sha1(serialize($assembledConfig));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function connects to a database.
|
||||
*
|
||||
* @param string $dsn Database connection string
|
||||
* @param string $username SQL user
|
||||
* @param string $password SQL password
|
||||
* @param array $options PDO options
|
||||
*
|
||||
* @return PDO object
|
||||
*@throws Exception If an error happens while trying to connect to the database.
|
||||
*/
|
||||
private function connect($dsn, $username, $password, $options)
|
||||
{
|
||||
try {
|
||||
$db = new PDO($dsn, $username, $password, $options);
|
||||
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
return $db;
|
||||
} catch (PDOException $e) {
|
||||
throw new Exception("Database error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function randomly selects a slave database server to query. In the event no slaves are configured, it will
|
||||
* return the master.
|
||||
*
|
||||
* @return PDO object
|
||||
*/
|
||||
private function getSlave()
|
||||
{
|
||||
if (count($this->dbSlaves) > 0) {
|
||||
$slaveId = rand(0, count($this->dbSlaves) - 1);
|
||||
return $this->dbSlaves[$slaveId];
|
||||
} else {
|
||||
return $this->dbMaster;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function simply applies the table prefix to a supplied table name.
|
||||
*
|
||||
* @param string $table Table to apply prefix to, if configured
|
||||
*
|
||||
* @return string Table with configured prefix
|
||||
*/
|
||||
public function applyPrefix($table)
|
||||
{
|
||||
return $this->tablePrefix . $table;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function queries the database
|
||||
*
|
||||
* @param PDO $db PDO object to use
|
||||
* @param string $stmt Prepared SQL statement
|
||||
* @param array $params Parameters
|
||||
*
|
||||
* @return PDOStatement object
|
||||
*@throws Exception If an error happens while trying to execute the query.
|
||||
*/
|
||||
private function query($db, $stmt, $params)
|
||||
{
|
||||
assert(is_object($db));
|
||||
assert(is_string($stmt));
|
||||
assert(is_array($params));
|
||||
|
||||
try {
|
||||
$query = $db->prepare($stmt);
|
||||
|
||||
foreach ($params as $param => $value) {
|
||||
if (is_array($value)) {
|
||||
$query->bindValue(":$param", $value[0], ($value[1]) ? $value[1] : PDO::PARAM_STR);
|
||||
} else {
|
||||
$query->bindValue(":$param", $value, PDO::PARAM_STR);
|
||||
}
|
||||
}
|
||||
|
||||
$query->execute();
|
||||
|
||||
return $query;
|
||||
} catch (PDOException $e) {
|
||||
$this->lastError = $db->errorInfo();
|
||||
throw new Exception("Database error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function queries the database without using a prepared statement.
|
||||
*
|
||||
* @param PDO $db PDO object to use
|
||||
* @param string $stmt An SQL statement to execute, previously escaped.
|
||||
*
|
||||
* @return int The number of rows affected.
|
||||
*@throws Exception If an error happens while trying to execute the query.
|
||||
*/
|
||||
private function exec($db, $stmt)
|
||||
{
|
||||
assert(is_object($db));
|
||||
assert(is_string($stmt));
|
||||
|
||||
try {
|
||||
return $db->exec($stmt);
|
||||
} catch (PDOException $e) {
|
||||
$this->lastError = $db->errorInfo();
|
||||
throw new Exception("Database error: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This executes queries directly on the master.
|
||||
*
|
||||
* @param string $stmt Prepared SQL statement
|
||||
* @param array $params Parameters
|
||||
*
|
||||
* @return int|false The number of rows affected by the query or false on error.
|
||||
*/
|
||||
public function write($stmt, $params = [])
|
||||
{
|
||||
$db = $this->dbMaster;
|
||||
|
||||
if (is_array($params)) {
|
||||
$obj = $this->query($db, $stmt, $params);
|
||||
return ($obj === false) ? $obj : $obj->rowCount();
|
||||
} else {
|
||||
return $this->exec($db, $stmt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This executes queries on a database server that is determined by this::getSlave().
|
||||
*
|
||||
* @param string $stmt Prepared SQL statement
|
||||
* @param array $params Parameters
|
||||
*
|
||||
* @return PDOStatement object
|
||||
*/
|
||||
public function read($stmt, $params = [])
|
||||
{
|
||||
$db = $this->getSlave();
|
||||
|
||||
return $this->query($db, $stmt, $params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return an array with information about the last operation performed in the database.
|
||||
*
|
||||
* @return array The array with error information.
|
||||
*/
|
||||
public function getLastError()
|
||||
{
|
||||
return $this->lastError;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Class for creating exceptions from assertion failures.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class Assertion extends Exception
|
||||
{
|
||||
/**
|
||||
* The assertion which failed, or null if only an expression was passed to the
|
||||
* assert-function.
|
||||
*/
|
||||
private $assertion;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for the assertion exception.
|
||||
*
|
||||
* Should only be called from the onAssertion handler.
|
||||
*
|
||||
* @param string|null $assertion The assertion which failed, or null if the assert-function was
|
||||
* given an expression.
|
||||
*/
|
||||
public function __construct($assertion = null)
|
||||
{
|
||||
assert($assertion === null || is_string($assertion));
|
||||
|
||||
$msg = 'Assertion failed: ' . var_export($assertion, true);
|
||||
parent::__construct($msg);
|
||||
|
||||
$this->assertion = $assertion;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the assertion which failed.
|
||||
*
|
||||
* @return string|null The assertion which failed, or null if the assert-function was called with an expression.
|
||||
*/
|
||||
public function getAssertion()
|
||||
{
|
||||
return $this->assertion;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Install this assertion handler.
|
||||
*
|
||||
* This function will register this assertion handler. If will not enable assertions if they are
|
||||
* disabled.
|
||||
* @return void
|
||||
*/
|
||||
public static function installHandler()
|
||||
{
|
||||
|
||||
assert_options(ASSERT_WARNING, 0);
|
||||
assert_options(ASSERT_QUIET_EVAL, 0);
|
||||
assert_options(ASSERT_CALLBACK, [Assertion::class, 'onAssertion']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle assertion.
|
||||
*
|
||||
* This function handles an assertion.
|
||||
*
|
||||
* @param string $file The file assert was called from.
|
||||
* @param int $line The line assert was called from.
|
||||
* @param mixed $message The expression which was passed to the assert-function.
|
||||
* @return void
|
||||
*/
|
||||
public static function onAssertion($file, $line, $message)
|
||||
{
|
||||
|
||||
if (!empty($message)) {
|
||||
$exception = new self($message);
|
||||
} else {
|
||||
$exception = new self();
|
||||
}
|
||||
|
||||
$exception->logError();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Baseclass for auth source exceptions.
|
||||
*
|
||||
* @package SimpleSAMLphp_base
|
||||
*
|
||||
*/
|
||||
|
||||
class AuthSource extends Error
|
||||
{
|
||||
/**
|
||||
* Authsource module name
|
||||
* @var string
|
||||
*/
|
||||
private $authsource;
|
||||
|
||||
|
||||
/**
|
||||
* Reason why this request was invalid.
|
||||
* @var string
|
||||
*/
|
||||
private $reason;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new AuthSource error.
|
||||
*
|
||||
* @param string $authsource Authsource module name from where this error was thrown.
|
||||
* @param string $reason Description of the error.
|
||||
* @param \Exception|null $cause
|
||||
*/
|
||||
public function __construct($authsource, $reason, $cause = null)
|
||||
{
|
||||
assert(is_string($authsource));
|
||||
assert(is_string($reason));
|
||||
|
||||
$this->authsource = $authsource;
|
||||
$this->reason = $reason;
|
||||
parent::__construct(
|
||||
[
|
||||
'AUTHSOURCEERROR',
|
||||
'%AUTHSOURCE%' => htmlspecialchars(var_export($this->authsource, true)),
|
||||
'%REASON%' => htmlspecialchars(var_export($this->reason, true))
|
||||
],
|
||||
$cause
|
||||
);
|
||||
|
||||
$this->message = "Error with authentication source '$authsource': $reason";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the authsource module name from where this error was thrown.
|
||||
*
|
||||
* @return string Authsource module name.
|
||||
*/
|
||||
public function getAuthSource()
|
||||
{
|
||||
return $this->authsource;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the reason why the request was invalid.
|
||||
*
|
||||
* @return string The reason why the request was invalid.
|
||||
*/
|
||||
public function getReason()
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Exception which will show a 400 Bad Request error page.
|
||||
*
|
||||
* This exception can be thrown from within an module page handler. The user will then be
|
||||
* shown a 400 Bad Request error page.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class BadRequest extends Error
|
||||
{
|
||||
/**
|
||||
* Reason why this request was invalid.
|
||||
* @var string
|
||||
*/
|
||||
private $reason;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new BadRequest error.
|
||||
*
|
||||
* @param string $reason Description of why the request was unacceptable.
|
||||
*/
|
||||
public function __construct($reason)
|
||||
{
|
||||
assert(is_string($reason));
|
||||
|
||||
$this->reason = $reason;
|
||||
parent::__construct(['BADREQUEST', '%REASON%' => $this->reason]);
|
||||
$this->httpCode = 400;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the reason why the request was invalid.
|
||||
*
|
||||
* @return string The reason why the request was invalid.
|
||||
*/
|
||||
public function getReason()
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Exception indicating illegal innput from user.
|
||||
*
|
||||
* @author Thomas Graff <thomas.graff@uninett.no>
|
||||
* @package SimpleSAMLphp_base
|
||||
*
|
||||
*/
|
||||
|
||||
class BadUserInput extends User
|
||||
{
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Exception to indicate that we cannot set a cookie.
|
||||
*
|
||||
* @author Jaime Pérez Crespo <jaime.perez@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class CannotSetCookie extends Exception
|
||||
{
|
||||
/**
|
||||
* The exception was thrown for unknown reasons.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const UNKNOWN = 0;
|
||||
|
||||
/**
|
||||
* The exception was due to the HTTP headers being already sent, and therefore we cannot send additional headers to
|
||||
* set the cookie.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const HEADERS_SENT = 1;
|
||||
|
||||
/**
|
||||
* The exception was due to trying to set a secure cookie over an insecure channel.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const SECURE_COOKIE = 2;
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* This exception represents a configuration error.
|
||||
*
|
||||
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class ConfigurationError extends Error
|
||||
{
|
||||
/**
|
||||
* The reason for this exception.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
protected $reason;
|
||||
|
||||
/**
|
||||
* The configuration file that caused this exception.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
protected $config_file;
|
||||
|
||||
|
||||
/**
|
||||
* ConfigurationError constructor.
|
||||
*
|
||||
* @param string|null $reason The reason for this exception.
|
||||
* @param string|null $file The configuration file that originated this error.
|
||||
* @param array|null $config The configuration array that led to this problem.
|
||||
*/
|
||||
public function __construct($reason = null, $file = null, array $config = null)
|
||||
{
|
||||
$file_str = '';
|
||||
$reason_str = '.';
|
||||
$params = ['CONFIG'];
|
||||
if ($file !== null) {
|
||||
$params['%FILE%'] = $file;
|
||||
$basepath = dirname(dirname(dirname(dirname(__FILE__)))) . '/';
|
||||
$file_str = '(' . str_replace($basepath, '', $file) . ') ';
|
||||
}
|
||||
if ($reason !== null) {
|
||||
$params['%REASON%'] = $reason;
|
||||
$reason_str = ': ' . $reason;
|
||||
}
|
||||
$this->reason = $reason;
|
||||
$this->config_file = $file;
|
||||
parent::__construct($params);
|
||||
$this->message = 'The configuration ' . $file_str . 'is invalid' . $reason_str;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the reason for this exception.
|
||||
*
|
||||
* @return null|string The reason for this exception.
|
||||
*/
|
||||
public function getReason()
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the configuration file that caused this exception.
|
||||
*
|
||||
* @return null|string The configuration file that caused this exception.
|
||||
*/
|
||||
public function getConfFile()
|
||||
{
|
||||
return $this->config_file;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* This exception represents a configuration error that we cannot recover from.
|
||||
*
|
||||
* Throwing a critical configuration error indicates that the configuration available is not usable, and as such
|
||||
* SimpleSAMLphp should not try to use it. However, in certain situations we might find a specific configuration
|
||||
* error that makes part of the configuration unusable, while the rest we can still use. In those cases, we can
|
||||
* just pass a configuration array to the constructor, making sure the offending configuration options are removed,
|
||||
* reset to defaults or guessed to some usable value.
|
||||
*
|
||||
* If, for example, we have an error in the 'baseurlpath' configuration option, we can still load the configuration
|
||||
* and substitute the value of that option with one guessed from the environment, using
|
||||
* \SimpleSAML\Utils\HTTP::guessPath(). Doing so, the error is still critical, but at least we can recover up to a
|
||||
* certain point and inform about the error in an ordered manner, without blank pages, logs out of place or even
|
||||
* segfaults.
|
||||
*
|
||||
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class CriticalConfigurationError extends ConfigurationError
|
||||
{
|
||||
/**
|
||||
* This is the bare minimum configuration that we can use.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $minimum_config = [
|
||||
'logging.handler' => 'errorlog',
|
||||
'logging.level' => Logger::DEBUG,
|
||||
'errorreporting' => false,
|
||||
'debug' => true,
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* CriticalConfigurationError constructor.
|
||||
*
|
||||
* @param string|null $reason The reason for this critical error.
|
||||
* @param string|null $file The configuration file that originated this error.
|
||||
* @param array|null $config The configuration array that led to this problem.
|
||||
*/
|
||||
public function __construct($reason = null, $file = null, $config = null)
|
||||
{
|
||||
if ($config === null) {
|
||||
$config = self::$minimum_config;
|
||||
$config['baseurlpath'] = Utils\HTTP::guessBasePath();
|
||||
}
|
||||
|
||||
Configuration::loadFromArray(
|
||||
$config,
|
||||
'',
|
||||
'simplesaml'
|
||||
);
|
||||
parent::__construct($reason, $file);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param \Exception $exception
|
||||
*
|
||||
* @return CriticalConfigurationError
|
||||
*/
|
||||
public static function fromException(\Exception $exception)
|
||||
{
|
||||
$reason = null;
|
||||
$file = null;
|
||||
if ($exception instanceof ConfigurationError) {
|
||||
$reason = $exception->getReason();
|
||||
$file = $exception->getConfFile();
|
||||
} else {
|
||||
$reason = $exception->getMessage();
|
||||
}
|
||||
return new CriticalConfigurationError($reason, $file);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
use SAML\Session;
|
||||
use SAML\Utils;
|
||||
use SAML\XHTML\Template;
|
||||
|
||||
/**
|
||||
* Class that wraps SimpleSAMLphp errors in exceptions.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class Error extends Exception
|
||||
{
|
||||
/**
|
||||
* The error code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $errorCode;
|
||||
|
||||
/**
|
||||
* The http code.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $httpCode = 500;
|
||||
|
||||
/**
|
||||
* The error title tag in dictionary.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $dictTitle;
|
||||
|
||||
/**
|
||||
* The error description tag in dictionary.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $dictDescr;
|
||||
|
||||
/**
|
||||
* The name of module that threw the error.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $module = null;
|
||||
|
||||
/**
|
||||
* The parameters for the error.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $parameters;
|
||||
|
||||
/**
|
||||
* Name of custom include template for the error.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $includeTemplate = null;
|
||||
|
||||
/**
|
||||
* Constructor for this error.
|
||||
*
|
||||
* The error can either be given as a string, or as an array. If it is an array, the first element in the array
|
||||
* (with index 0), is the error code, while the other elements are replacements for the error text.
|
||||
*
|
||||
* @param mixed $errorCode One of the error codes defined in the errors dictionary.
|
||||
* @param \Exception $cause The exception which caused this fatal error (if any). Optional.
|
||||
* @param int|null $httpCode The HTTP response code to use. Optional.
|
||||
*/
|
||||
public function __construct($errorCode, \Exception $cause = null, $httpCode = null)
|
||||
{
|
||||
assert(is_string($errorCode) || is_array($errorCode));
|
||||
|
||||
if (is_array($errorCode)) {
|
||||
$this->parameters = $errorCode;
|
||||
unset($this->parameters[0]);
|
||||
$this->errorCode = $errorCode[0];
|
||||
} else {
|
||||
$this->parameters = [];
|
||||
$this->errorCode = $errorCode;
|
||||
}
|
||||
|
||||
if (isset($httpCode)) {
|
||||
$this->httpCode = $httpCode;
|
||||
}
|
||||
|
||||
$this->dictTitle = ErrorCodes::getErrorCodeTitle($this->errorCode);
|
||||
$this->dictDescr = ErrorCodes::getErrorCodeDescription($this->errorCode);
|
||||
|
||||
if (!empty($this->parameters)) {
|
||||
$msg = $this->errorCode . '(';
|
||||
foreach ($this->parameters as $k => $v) {
|
||||
if ($k === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$msg .= var_export($k, true) . ' => ' . var_export($v, true) . ', ';
|
||||
}
|
||||
$msg = substr($msg, 0, -2) . ')';
|
||||
} else {
|
||||
$msg = $this->errorCode;
|
||||
}
|
||||
parent::__construct($msg, -1, $cause);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the error code given when throwing this error.
|
||||
*
|
||||
* @return string The error code.
|
||||
*/
|
||||
public function getErrorCode()
|
||||
{
|
||||
return $this->errorCode;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the error parameters given when throwing this error.
|
||||
*
|
||||
* @return array The parameters.
|
||||
*/
|
||||
public function getParameters()
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the error title tag in dictionary.
|
||||
*
|
||||
* @return string The error title tag.
|
||||
*/
|
||||
public function getDictTitle()
|
||||
{
|
||||
return $this->dictTitle;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the error description tag in dictionary.
|
||||
*
|
||||
* @return string The error description tag.
|
||||
*/
|
||||
public function getDictDescr()
|
||||
{
|
||||
return $this->dictDescr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the HTTP return code for this error.
|
||||
*
|
||||
* This should be overridden by subclasses who want a different return code than 500 Internal Server Error.
|
||||
* @return void
|
||||
*/
|
||||
protected function setHTTPCode()
|
||||
{
|
||||
http_response_code($this->httpCode);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save an error report.
|
||||
*
|
||||
* @return array The array with the error report data.
|
||||
*/
|
||||
protected function saveError()
|
||||
{
|
||||
$data = $this->format(true);
|
||||
$emsg = array_shift($data);
|
||||
$etrace = implode("\n", $data);
|
||||
|
||||
$reportId = bin2hex(openssl_random_pseudo_bytes(4));
|
||||
Logger::error('Error report with id ' . $reportId . ' generated.');
|
||||
|
||||
$config = Configuration::getInstance();
|
||||
$session = Session::getSessionFromRequest();
|
||||
|
||||
if (isset($_SERVER['HTTP_REFERER'])) {
|
||||
$referer = $_SERVER['HTTP_REFERER'];
|
||||
// remove anything after the first '?' or ';', just in case it contains any sensitive data
|
||||
$referer = explode('?', $referer, 2);
|
||||
$referer = $referer[0];
|
||||
$referer = explode(';', $referer, 2);
|
||||
$referer = $referer[0];
|
||||
} else {
|
||||
$referer = 'unknown';
|
||||
}
|
||||
$errorData = [
|
||||
'exceptionMsg' => $emsg,
|
||||
'exceptionTrace' => $etrace,
|
||||
'reportId' => $reportId,
|
||||
'trackId' => $session->getTrackID(),
|
||||
'url' => Utils\HTTP::getSelfURLNoQuery(),
|
||||
'version' => $config->getVersion(),
|
||||
'referer' => $referer,
|
||||
];
|
||||
$session->setData('core:errorreport', $reportId, $errorData);
|
||||
|
||||
return $errorData;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display this error.
|
||||
*
|
||||
* This method displays a standard SimpleSAMLphp error page and exits.
|
||||
* @return void
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
$this->setHTTPCode();
|
||||
|
||||
// log the error message
|
||||
$this->logError();
|
||||
|
||||
$errorData = $this->saveError();
|
||||
$config = Configuration::getInstance();
|
||||
|
||||
$data = [];
|
||||
$data['showerrors'] = $config->getBoolean('showerrors', true);
|
||||
$data['error'] = $errorData;
|
||||
$data['errorCode'] = $this->errorCode;
|
||||
$data['parameters'] = $this->parameters;
|
||||
$data['module'] = $this->module;
|
||||
$data['dictTitle'] = $this->dictTitle;
|
||||
$data['dictDescr'] = $this->dictDescr;
|
||||
$data['includeTemplate'] = $this->includeTemplate;
|
||||
$data['clipboard.js'] = true;
|
||||
|
||||
// check if there is a valid technical contact email address
|
||||
if (
|
||||
$config->getBoolean('errorreporting', true)
|
||||
&& $config->getString('technicalcontact_email', 'na@example.org') !== 'na@example.org'
|
||||
) {
|
||||
// enable error reporting
|
||||
$baseurl = Utils\HTTP::getBaseURL();
|
||||
$data['errorReportAddress'] = $baseurl . 'errorreport.php';
|
||||
}
|
||||
|
||||
$data['email'] = '';
|
||||
$session = Session::getSessionFromRequest();
|
||||
$authorities = $session->getAuthorities();
|
||||
foreach ($authorities as $authority) {
|
||||
$attributes = $session->getAuthData($authority, 'Attributes');
|
||||
if ($attributes !== null && array_key_exists('mail', $attributes) && count($attributes['mail']) > 0) {
|
||||
$data['email'] = $attributes['mail'][0];
|
||||
break; // enough, don't need to get all available mails, if more than one
|
||||
}
|
||||
}
|
||||
|
||||
$show_function = $config->getArray('errors.show_function', null);
|
||||
if (isset($show_function)) {
|
||||
assert(is_callable($show_function));
|
||||
call_user_func($show_function, $config, $data);
|
||||
assert(false);
|
||||
} else {
|
||||
$t = new Template($config, 'error.php', 'errors');
|
||||
$translator = $t->getTranslator();
|
||||
$t->data = array_merge($t->data, $data);
|
||||
$t->data['dictTitleTranslated'] = $translator->t($t->data['dictTitle']);
|
||||
$t->data['dictDescrTranslated'] = $translator->t($t->data['dictDescr'], $t->data['parameters']);
|
||||
$t->show();
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
use SAML\Locale\Translate;
|
||||
|
||||
/**
|
||||
* Class that maps SimpleSAMLphp error codes to translateable strings.
|
||||
*
|
||||
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class ErrorCodes
|
||||
{
|
||||
/**
|
||||
* Fetch all default translation strings for error code titles.
|
||||
*
|
||||
* @return array A map from error code to error code title
|
||||
*/
|
||||
final public static function defaultGetAllErrorCodeTitles()
|
||||
{
|
||||
return [
|
||||
'ACSPARAMS' => Translate::noop('{errors:title_ACSPARAMS}'),
|
||||
'ARSPARAMS' => Translate::noop('{errors:title_ARSPARAMS}'),
|
||||
'AUTHSOURCEERROR' => Translate::noop('{errors:title_AUTHSOURCEERROR}'),
|
||||
'BADREQUEST' => Translate::noop('{errors:title_BADREQUEST}'),
|
||||
'CASERROR' => Translate::noop('{errors:title_CASERROR}'),
|
||||
'CONFIG' => Translate::noop('{errors:title_CONFIG}'),
|
||||
'CREATEREQUEST' => Translate::noop('{errors:title_CREATEREQUEST}'),
|
||||
'DISCOPARAMS' => Translate::noop('{errors:title_DISCOPARAMS}'),
|
||||
'GENERATEAUTHNRESPONSE' => Translate::noop('{errors:title_GENERATEAUTHNRESPONSE}'),
|
||||
'INVALIDCERT' => Translate::noop('{errors:title_INVALIDCERT}'),
|
||||
'LDAPERROR' => Translate::noop('{errors:title_LDAPERROR}'),
|
||||
'LOGOUTINFOLOST' => Translate::noop('{errors:title_LOGOUTINFOLOST}'),
|
||||
'LOGOUTREQUEST' => Translate::noop('{errors:title_LOGOUTREQUEST}'),
|
||||
'MEMCACHEDOWN' => Translate::noop('{errors:title_MEMCACHEDOWN}'),
|
||||
'METADATA' => Translate::noop('{errors:title_METADATA}'),
|
||||
'METADATANOTFOUND' => Translate::noop('{errors:title_METADATANOTFOUND}'),
|
||||
'NOACCESS' => Translate::noop('{errors:title_NOACCESS}'),
|
||||
'NOCERT' => Translate::noop('{errors:title_NOCERT}'),
|
||||
'NORELAYSTATE' => Translate::noop('{errors:title_NORELAYSTATE}'),
|
||||
'NOSTATE' => Translate::noop('{errors:title_NOSTATE}'),
|
||||
'NOTFOUND' => Translate::noop('{errors:title_NOTFOUND}'),
|
||||
'NOTFOUNDREASON' => Translate::noop('{errors:title_NOTFOUNDREASON}'),
|
||||
'NOTSET' => Translate::noop('{errors:title_NOTSET}'),
|
||||
'NOTVALIDCERT' => Translate::noop('{errors:title_NOTVALIDCERT}'),
|
||||
'PROCESSASSERTION' => Translate::noop('{errors:title_PROCESSASSERTION}'),
|
||||
'PROCESSAUTHNREQUEST' => Translate::noop('{errors:title_PROCESSAUTHNREQUEST}'),
|
||||
'RESPONSESTATUSNOSUCCESS' => Translate::noop('{errors:title_RESPONSESTATUSNOSUCCESS}'),
|
||||
'SLOSERVICEPARAMS' => Translate::noop('{errors:title_SLOSERVICEPARAMS}'),
|
||||
'SSOPARAMS' => Translate::noop('{errors:title_SSOPARAMS}'),
|
||||
'UNHANDLEDEXCEPTION' => Translate::noop('{errors:title_UNHANDLEDEXCEPTION}'),
|
||||
'UNKNOWNCERT' => Translate::noop('{errors:title_UNKNOWNCERT}'),
|
||||
'USERABORTED' => Translate::noop('{errors:title_USERABORTED}'),
|
||||
'WRONGUSERPASS' => Translate::noop('{errors:title_WRONGUSERPASS}'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch all translation strings for error code titles.
|
||||
*
|
||||
* Extend this to add error codes.
|
||||
*
|
||||
* @return array A map from error code to error code title
|
||||
*/
|
||||
public static function getAllErrorCodeTitles()
|
||||
{
|
||||
return self::defaultGetAllErrorCodeTitles();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch all default translation strings for error code descriptions.
|
||||
*
|
||||
* @return array A map from error code to error code description
|
||||
*/
|
||||
final public static function defaultGetAllErrorCodeDescriptions()
|
||||
{
|
||||
return [
|
||||
'ACSPARAMS' => Translate::noop('{errors:descr_ACSPARAMS}'),
|
||||
'ARSPARAMS' => Translate::noop('{errors:descr_ARSPARAMS}'),
|
||||
'AUTHSOURCEERROR' => Translate::noop('{errors:descr_AUTHSOURCEERROR}'),
|
||||
'BADREQUEST' => Translate::noop('{errors:descr_BADREQUEST}'),
|
||||
'CASERROR' => Translate::noop('{errors:descr_CASERROR}'),
|
||||
'CONFIG' => Translate::noop('{errors:descr_CONFIG}'),
|
||||
'CREATEREQUEST' => Translate::noop('{errors:descr_CREATEREQUEST}'),
|
||||
'DISCOPARAMS' => Translate::noop('{errors:descr_DISCOPARAMS}'),
|
||||
'GENERATEAUTHNRESPONSE' => Translate::noop('{errors:descr_GENERATEAUTHNRESPONSE}'),
|
||||
'INVALIDCERT' => Translate::noop('{errors:descr_INVALIDCERT}'),
|
||||
'LDAPERROR' => Translate::noop('{errors:descr_LDAPERROR}'),
|
||||
'LOGOUTINFOLOST' => Translate::noop('{errors:descr_LOGOUTINFOLOST}'),
|
||||
'LOGOUTREQUEST' => Translate::noop('{errors:descr_LOGOUTREQUEST}'),
|
||||
'MEMCACHEDOWN' => Translate::noop('{errors:descr_MEMCACHEDOWN}'),
|
||||
'METADATA' => Translate::noop('{errors:descr_METADATA}'),
|
||||
'METADATANOTFOUND' => Translate::noop('{errors:descr_METADATANOTFOUND}'),
|
||||
'NOACCESS' => Translate::noop('{errors:descr_NOACCESS}'),
|
||||
'NOCERT' => Translate::noop('{errors:descr_NOCERT}'),
|
||||
'NORELAYSTATE' => Translate::noop('{errors:descr_NORELAYSTATE}'),
|
||||
'NOSTATE' => Translate::noop('{errors:descr_NOSTATE}'),
|
||||
'NOTFOUND' => Translate::noop('{errors:descr_NOTFOUND}'),
|
||||
'NOTFOUNDREASON' => Translate::noop('{errors:descr_NOTFOUNDREASON}'),
|
||||
'NOTSET' => Translate::noop('{errors:descr_NOTSET}'),
|
||||
'NOTVALIDCERT' => Translate::noop('{errors:descr_NOTVALIDCERT}'),
|
||||
'PROCESSASSERTION' => Translate::noop('{errors:descr_PROCESSASSERTION}'),
|
||||
'PROCESSAUTHNREQUEST' => Translate::noop('{errors:descr_PROCESSAUTHNREQUEST}'),
|
||||
'RESPONSESTATUSNOSUCCESS' => Translate::noop('{errors:descr_RESPONSESTATUSNOSUCCESS}'),
|
||||
'SLOSERVICEPARAMS' => Translate::noop('{errors:descr_SLOSERVICEPARAMS}'),
|
||||
'SSOPARAMS' => Translate::noop('{errors:descr_SSOPARAMS}'),
|
||||
'UNHANDLEDEXCEPTION' => Translate::noop('{errors:descr_UNHANDLEDEXCEPTION}'),
|
||||
'UNKNOWNCERT' => Translate::noop('{errors:descr_UNKNOWNCERT}'),
|
||||
'USERABORTED' => Translate::noop('{errors:descr_USERABORTED}'),
|
||||
'WRONGUSERPASS' => Translate::noop('{errors:descr_WRONGUSERPASS}'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all translation strings for error code descriptions.
|
||||
*
|
||||
* Extend this to add error codes.
|
||||
*
|
||||
* @return array A map from error code to error code description
|
||||
*/
|
||||
public static function getAllErrorCodeDescriptions()
|
||||
{
|
||||
return self::defaultGetAllErrorCodeDescriptions();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a map of both errorcode titles and descriptions
|
||||
*
|
||||
* Convenience-method for template-callers
|
||||
*
|
||||
* @return array An array containing both errorcode maps.
|
||||
*/
|
||||
public static function getAllErrorCodeMessages()
|
||||
{
|
||||
return [
|
||||
'title' => self::getAllErrorCodeTitles(),
|
||||
'descr' => self::getAllErrorCodeDescriptions(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch a translation string for a title for a given error code.
|
||||
*
|
||||
* @param string $errorCode The error code to look up
|
||||
*
|
||||
* @return string A string to translate
|
||||
*/
|
||||
public static function getErrorCodeTitle($errorCode)
|
||||
{
|
||||
$errorCodeTitles = self::getAllErrorCodeTitles();
|
||||
return $errorCodeTitles[$errorCode];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch a translation string for a description for a given error code.
|
||||
*
|
||||
* @param string $errorCode The error code to look up
|
||||
*
|
||||
* @return string A string to translate
|
||||
*/
|
||||
public static function getErrorCodeDescription($errorCode)
|
||||
{
|
||||
$errorCodeDescriptions = self::getAllErrorCodeDescriptions();
|
||||
return $errorCodeDescriptions[$errorCode];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get both title and description for a specific error code
|
||||
*
|
||||
* Convenience-method for template-callers
|
||||
*
|
||||
* @param string $errorCode The error code to look up
|
||||
*
|
||||
* @return array An array containing both errorcode strings.
|
||||
*/
|
||||
public static function getErrorCodeMessage($errorCode)
|
||||
{
|
||||
return [
|
||||
'title' => self::getErrorCodeTitle($errorCode),
|
||||
'descr' => self::getErrorCodeDescription($errorCode),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
|
||||
/**
|
||||
* Base class for SimpleSAMLphp Exceptions
|
||||
*
|
||||
* This class tries to make sure that every exception is serializable.
|
||||
*
|
||||
* @author Thomas Graff <thomas.graff@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class Exception extends \Exception
|
||||
{
|
||||
/**
|
||||
* The backtrace for this exception.
|
||||
*
|
||||
* We need to save the backtrace, since we cannot rely on
|
||||
* serializing the Exception::trace-variable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $backtrace = [];
|
||||
|
||||
|
||||
/**
|
||||
* The cause of this exception.
|
||||
*
|
||||
* @var Exception|null
|
||||
*/
|
||||
private $cause = null;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for this error.
|
||||
*
|
||||
* Note that the cause will be converted to a SimpleSAML\Error\UnserializableException unless it is a subclass of
|
||||
* SimpleSAML\Error\Exception.
|
||||
*
|
||||
* @param string $message Exception message
|
||||
* @param int $code Error code
|
||||
* @param \Exception|null $cause The cause of this exception.
|
||||
*/
|
||||
public function __construct($message, $code = 0, \Exception $cause = null)
|
||||
{
|
||||
assert(is_string($message));
|
||||
assert(is_int($code));
|
||||
|
||||
parent::__construct($message, $code);
|
||||
|
||||
$this->initBacktrace($this);
|
||||
|
||||
if ($cause !== null) {
|
||||
$this->cause = Exception::fromException($cause);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert any exception into a \SimpleSAML\Error\Exception.
|
||||
*
|
||||
* @param \Exception $e The exception.
|
||||
*
|
||||
* @return Exception The new exception.
|
||||
*/
|
||||
public static function fromException(\Exception $e)
|
||||
{
|
||||
if ($e instanceof Exception) {
|
||||
return $e;
|
||||
}
|
||||
return new UnserializableException($e);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load the backtrace from the given exception.
|
||||
*
|
||||
* @param \Exception $exception The exception we should fetch the backtrace from.
|
||||
* @return void
|
||||
*/
|
||||
protected function initBacktrace(\Exception $exception)
|
||||
{
|
||||
$this->backtrace = [];
|
||||
|
||||
// position in the top function on the stack
|
||||
$pos = $exception->getFile() . ':' . $exception->getLine();
|
||||
|
||||
foreach ($exception->getTrace() as $t) {
|
||||
$function = $t['function'];
|
||||
if (array_key_exists('class', $t)) {
|
||||
$function = $t['class'] . '::' . $function;
|
||||
}
|
||||
|
||||
$this->backtrace[] = $pos . ' (' . $function . ')';
|
||||
|
||||
if (array_key_exists('file', $t)) {
|
||||
$pos = $t['file'] . ':' . $t['line'];
|
||||
} else {
|
||||
$pos = '[builtin]';
|
||||
}
|
||||
}
|
||||
|
||||
$this->backtrace[] = $pos . ' (N/A)';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the backtrace.
|
||||
*
|
||||
* @return array An array where each function call is a single item.
|
||||
*/
|
||||
public function getBacktrace()
|
||||
{
|
||||
return $this->backtrace;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the cause of this exception.
|
||||
*
|
||||
* @return Exception|null The cause of this exception.
|
||||
*/
|
||||
public function getCause()
|
||||
{
|
||||
return $this->cause;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the class of this exception.
|
||||
*
|
||||
* @return string The name of the class.
|
||||
*/
|
||||
public function getClass()
|
||||
{
|
||||
return get_class($this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format this exception for logging.
|
||||
*
|
||||
* Create an array of lines for logging.
|
||||
*
|
||||
* @param boolean $anonymize Whether the resulting messages should be anonymized or not.
|
||||
*
|
||||
* @return array Log lines that should be written out.
|
||||
*/
|
||||
public function format($anonymize = false)
|
||||
{
|
||||
$ret = [
|
||||
$this->getClass() . ': ' . $this->getMessage(),
|
||||
];
|
||||
return array_merge($ret, $this->formatBacktrace($anonymize));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format the backtrace for logging.
|
||||
*
|
||||
* Create an array of lines for logging from the backtrace.
|
||||
*
|
||||
* @param boolean $anonymize Whether the resulting messages should be anonymized or not.
|
||||
*
|
||||
* @return array All lines of the backtrace, properly formatted.
|
||||
*/
|
||||
public function formatBacktrace($anonymize = false)
|
||||
{
|
||||
$ret = [];
|
||||
$basedir = Configuration::getInstance()->getBaseDir();
|
||||
|
||||
$e = $this;
|
||||
do {
|
||||
if ($e !== $this) {
|
||||
$ret[] = 'Caused by: ' . $e->getClass() . ': ' . $e->getMessage();
|
||||
}
|
||||
$ret[] = 'Backtrace:';
|
||||
|
||||
$depth = count($e->backtrace);
|
||||
foreach ($e->backtrace as $i => $trace) {
|
||||
if ($anonymize) {
|
||||
$trace = str_replace($basedir, '', $trace);
|
||||
}
|
||||
|
||||
$ret[] = ($depth - $i - 1) . ' ' . $trace;
|
||||
}
|
||||
$e = $e->cause;
|
||||
} while ($e !== null);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Print the backtrace to the log if the 'debug' option is enabled in the configuration.
|
||||
* @param int $level
|
||||
* @return void
|
||||
*/
|
||||
protected function logBacktrace($level = Logger::DEBUG)
|
||||
{
|
||||
// see if debugging is enabled for backtraces
|
||||
$debug = Configuration::getInstance()->getArrayize('debug', ['backtraces' => false]);
|
||||
|
||||
if (
|
||||
!(in_array('backtraces', $debug, true) // implicitly enabled
|
||||
|| (array_key_exists('backtraces', $debug)
|
||||
&& $debug['backtraces'] === true)
|
||||
// explicitly set
|
||||
// TODO: deprecate the old style and remove it in 2.0
|
||||
|| (array_key_exists(0, $debug)
|
||||
&& $debug[0] === true)) // old style 'debug' configuration option
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$backtrace = $this->formatBacktrace();
|
||||
|
||||
$callback = [Logger::class];
|
||||
$functions = [
|
||||
Logger::ERR => 'error',
|
||||
Logger::WARNING => 'warning',
|
||||
Logger::INFO => 'info',
|
||||
Logger::DEBUG => 'debug',
|
||||
];
|
||||
$callback[] = $functions[$level];
|
||||
|
||||
foreach ($backtrace as $line) {
|
||||
call_user_func($callback, $line);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Print the exception to the log, by default with log level error.
|
||||
*
|
||||
* Override to allow errors extending this class to specify the log level themselves.
|
||||
*
|
||||
* @param int $default_level The log level to use if this method was not overridden.
|
||||
* @return void
|
||||
*/
|
||||
public function log($default_level)
|
||||
{
|
||||
$fn = [
|
||||
Logger::ERR => 'logError',
|
||||
Logger::WARNING => 'logWarning',
|
||||
Logger::INFO => 'logInfo',
|
||||
Logger::DEBUG => 'logDebug',
|
||||
];
|
||||
call_user_func([$this, $fn[$default_level]], $default_level);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Print the exception to the log with log level error.
|
||||
*
|
||||
* This function will write this exception to the log, including a full backtrace.
|
||||
* @return void
|
||||
*/
|
||||
public function logError()
|
||||
{
|
||||
Logger::error($this->getClass() . ': ' . $this->getMessage());
|
||||
$this->logBacktrace(Logger::ERR);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Print the exception to the log with log level warning.
|
||||
*
|
||||
* This function will write this exception to the log, including a full backtrace.
|
||||
* @return void
|
||||
*/
|
||||
public function logWarning()
|
||||
{
|
||||
Logger::warning($this->getClass() . ': ' . $this->getMessage());
|
||||
$this->logBacktrace(Logger::WARNING);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Print the exception to the log with log level info.
|
||||
*
|
||||
* This function will write this exception to the log, including a full backtrace.
|
||||
* @return void
|
||||
*/
|
||||
public function logInfo()
|
||||
{
|
||||
Logger::info($this->getClass() . ': ' . $this->getMessage());
|
||||
$this->logBacktrace(Logger::INFO);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Print the exception to the log with log level debug.
|
||||
*
|
||||
* This function will write this exception to the log, including a full backtrace.
|
||||
* @return void
|
||||
*/
|
||||
public function logDebug()
|
||||
{
|
||||
Logger::debug($this->getClass() . ': ' . $this->getMessage());
|
||||
$this->logBacktrace(Logger::DEBUG);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Function for serialization.
|
||||
*
|
||||
* This function builds a list of all variables which should be serialized. It will serialize all variables except
|
||||
* the Exception::trace variable.
|
||||
*
|
||||
* @return array Array with the variables that should be serialized.
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
$ret = array_keys((array) $this);
|
||||
|
||||
foreach ($ret as $i => $e) {
|
||||
if ($e === "\0Exception\0trace") {
|
||||
unset($ret[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Exception indicating wrong password given by user.
|
||||
*
|
||||
* @author Thomas Graff <thomas.graff@uninett.no>
|
||||
* @package SimpleSAMLphp_base
|
||||
*
|
||||
*/
|
||||
|
||||
class InvalidCredential extends User
|
||||
{
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Error for missing metadata.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class MetadataNotFound extends Error
|
||||
{
|
||||
/**
|
||||
* Create the error
|
||||
*
|
||||
* @param string $entityId The entityID we were unable to locate.
|
||||
*/
|
||||
public function __construct($entityId)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
|
||||
$this->includeTemplate = 'core:no_metadata.tpl.php';
|
||||
parent::__construct([
|
||||
'METADATANOTFOUND',
|
||||
'%ENTITYID%' => htmlspecialchars(var_export($entityId, true))
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Class NoPassive
|
||||
*
|
||||
* @deprecated This class has been deprecated and will be removed in SimpleSAMLphp 2.0. Please use
|
||||
* \SimpleSAML\Module\saml\Error\NoPassive instead.
|
||||
*
|
||||
* @see \SAML\Module\saml\Error\NoPassive
|
||||
*/
|
||||
|
||||
class NoPassive extends Exception
|
||||
{
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Exception which will show a page telling the user
|
||||
* that we don't know what to do.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class NoState extends Error
|
||||
{
|
||||
/**
|
||||
* Create the error
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->includeTemplate = 'core:no_state.tpl.php';
|
||||
parent::__construct('NOSTATE');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* Exception which will show a 404 Not Found error page.
|
||||
*
|
||||
* This exception can be thrown from within a module page handler. The user will then be shown a 404 Not Found error
|
||||
* page.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class NotFound extends Error
|
||||
{
|
||||
/**
|
||||
* Reason why the given page could not be found.
|
||||
*/
|
||||
private $reason;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new NotFound error
|
||||
*
|
||||
* @param string $reason Optional description of why the given page could not be found.
|
||||
*/
|
||||
public function __construct($reason = null)
|
||||
{
|
||||
assert($reason === null || is_string($reason));
|
||||
|
||||
$url = Utils\HTTP::getSelfURL();
|
||||
|
||||
if ($reason === null) {
|
||||
parent::__construct(['NOTFOUND', '%URL%' => $url]);
|
||||
$this->message = "The requested page '$url' could not be found.";
|
||||
} else {
|
||||
parent::__construct(['NOTFOUNDREASON', '%URL%' => $url, '%REASON%' => $reason]);
|
||||
$this->message = "The requested page '$url' could not be found. " . $reason;
|
||||
}
|
||||
|
||||
$this->reason = $reason;
|
||||
$this->httpCode = 404;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the reason why the given page could not be found.
|
||||
*
|
||||
* @return string|null The reason why the page could not be found.
|
||||
*/
|
||||
public function getReason()
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* NotFound exceptions don't need to display a backtrace, as they are very simple and the trace is usually trivial,
|
||||
* so just log the message without any backtrace at all.
|
||||
*
|
||||
* @param bool $anonymize Whether to anonymize the trace or not.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function format($anonymize = false)
|
||||
{
|
||||
return [
|
||||
$this->getClass() . ': ' . $this->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Class ProxyCountExceeded
|
||||
*
|
||||
* @deprecated This class has been deprecated and will be removed in SimpleSAMLphp 2.0. Please use
|
||||
* \SimpleSAML\Module\saml\Error\ProxyCountExceeded instead.
|
||||
*
|
||||
* @see \SAML\Module\saml\Error\ProxyCountExceeded
|
||||
*/
|
||||
|
||||
class ProxyCountExceeded extends Exception
|
||||
{
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
use PDOException;
|
||||
|
||||
/**
|
||||
* Class for saving normal exceptions for serialization.
|
||||
*
|
||||
* This class is used by the \SimpleSAML\Auth\State class when it needs
|
||||
* to serialize an exception which doesn't subclass the
|
||||
* \SimpleSAML\Error\Exception class.
|
||||
*
|
||||
* It creates a new exception which contains the backtrace and message
|
||||
* of the original exception.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class UnserializableException extends Exception
|
||||
{
|
||||
/**
|
||||
* The classname of the original exception.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $class;
|
||||
|
||||
|
||||
/**
|
||||
* Create a serializable exception representing an unserializable exception.
|
||||
*
|
||||
* @param \Exception $original The original exception.
|
||||
*/
|
||||
public function __construct(\Exception $original)
|
||||
{
|
||||
|
||||
$this->class = get_class($original);
|
||||
$msg = $original->getMessage();
|
||||
$code = $original->getCode();
|
||||
|
||||
if ($original instanceof PDOException) {
|
||||
// PDOException uses a string as the code. Filter it out here.
|
||||
$code = -1;
|
||||
}
|
||||
|
||||
parent::__construct($msg, $code);
|
||||
$this->initBacktrace($original);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the class of this exception.
|
||||
*
|
||||
* @return string The classname.
|
||||
*/
|
||||
public function getClass()
|
||||
{
|
||||
return $this->class;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Baseclass for user error exceptions
|
||||
*
|
||||
*
|
||||
* @author Thomas Graff <thomas.graff@uninett.no>
|
||||
* @package SimpleSAMLphp_base
|
||||
*
|
||||
*/
|
||||
|
||||
class User extends Exception
|
||||
{
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Exception indicating user aborting the authentication process.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class UserAborted extends Error
|
||||
{
|
||||
/**
|
||||
* Create the error
|
||||
*
|
||||
* @param \Exception|null $cause The exception that caused this error.
|
||||
*/
|
||||
public function __construct(\Exception $cause = null)
|
||||
{
|
||||
parent::__construct('USERABORTED', $cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Error;
|
||||
|
||||
/**
|
||||
* Exception indicating user not found by authsource.
|
||||
*
|
||||
* @author Thomas Graff <thomas.graff@uninett.no>
|
||||
* @package SimpleSAMLphp_base
|
||||
*
|
||||
*/
|
||||
|
||||
class UserNotFound extends User
|
||||
{
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\HTTP;
|
||||
|
||||
use Exception;
|
||||
use SAML\Configuration;
|
||||
use SAML\Module\ControllerResolver;
|
||||
use SAML\Session;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
|
||||
use Symfony\Component\HttpKernel\HttpKernel;
|
||||
use Symfony\Component\Routing\RequestContext;
|
||||
|
||||
/**
|
||||
* Class that routes requests to responses.
|
||||
*
|
||||
* @package SimpleSAML
|
||||
*/
|
||||
class Router
|
||||
{
|
||||
/** @var ArgumentResolver */
|
||||
protected $arguments;
|
||||
|
||||
/** @var Configuration|null */
|
||||
protected $config = null;
|
||||
|
||||
/** @var RequestContext */
|
||||
protected $context;
|
||||
|
||||
/** @var EventDispatcher */
|
||||
protected $dispatcher;
|
||||
|
||||
/** @var Request|null */
|
||||
protected $request = null;
|
||||
|
||||
/** @var ControllerResolver */
|
||||
protected $resolver;
|
||||
|
||||
/** @var Session|null */
|
||||
protected $session = null;
|
||||
|
||||
/** @var RequestStack|null */
|
||||
protected $stack = null;
|
||||
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*
|
||||
* @param string $module
|
||||
*/
|
||||
public function __construct($module)
|
||||
{
|
||||
$this->arguments = new ArgumentResolver();
|
||||
$this->context = new RequestContext();
|
||||
$this->resolver = new ControllerResolver($module);
|
||||
$this->dispatcher = new EventDispatcher();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a given request.
|
||||
*
|
||||
* If no specific arguments are given, the default instances will be used (configuration, session, etc).
|
||||
*
|
||||
* @param Request|null $request
|
||||
* The request to process. Defaults to the current one.
|
||||
*
|
||||
* @return Response A response suitable for the given request.
|
||||
*
|
||||
* @throws Exception If an error occurs.
|
||||
*/
|
||||
public function process(Request $request = null)
|
||||
{
|
||||
if ($this->config === null) {
|
||||
$this->setConfiguration(Configuration::getInstance());
|
||||
}
|
||||
if ($this->session === null) {
|
||||
$this->setSession(Session::getSessionFromRequest());
|
||||
}
|
||||
|
||||
if ($request === null) {
|
||||
$this->request = Request::createFromGlobals();
|
||||
} else {
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
$stack = new RequestStack();
|
||||
$stack->push($this->request);
|
||||
$this->context->fromRequest($this->request);
|
||||
$kernel = new HttpKernel($this->dispatcher, $this->resolver, $stack, $this->resolver);
|
||||
return $kernel->handle($this->request);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send a given response to the browser.
|
||||
*
|
||||
* @param Response $response The response to send.
|
||||
* @return void
|
||||
*/
|
||||
public function send(Response $response)
|
||||
{
|
||||
if ($this->request === null) {
|
||||
throw new Exception("No request found to respond to");
|
||||
}
|
||||
$response->prepare($this->request);
|
||||
$response->send();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the configuration to use by the controller.
|
||||
*
|
||||
* @param Configuration $config
|
||||
* @return void
|
||||
*/
|
||||
public function setConfiguration(Configuration $config)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->resolver->setConfiguration($config);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the session to use by the controller.
|
||||
*
|
||||
* @param Session $session
|
||||
* @return void
|
||||
*/
|
||||
public function setSession(Session $session)
|
||||
{
|
||||
$this->session = $session;
|
||||
$this->resolver->setSession($session);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\HTTP;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Class modelling a response that consists on running some function.
|
||||
*
|
||||
* This is a helper class that allows us to have the new and the old architecture coexist. This way, classes and files
|
||||
* that aren't PSR-7-aware can still be plugged into a PSR-7-compatible environment.
|
||||
*
|
||||
* @package SimpleSAML
|
||||
*/
|
||||
class RunnableResponse extends Response
|
||||
{
|
||||
/** @var array */
|
||||
protected $arguments;
|
||||
|
||||
/** @var callable */
|
||||
protected $callable;
|
||||
|
||||
|
||||
/**
|
||||
* RunnableResponse constructor.
|
||||
*
|
||||
* @param callable $callable A callable that we should run as part of this response.
|
||||
* @param array $args An array of arguments to be passed to the callable. Note that each element of the array
|
||||
*/
|
||||
public function __construct(callable $callable, $args = [])
|
||||
{
|
||||
$this->arguments = $args;
|
||||
$this->callable = $callable;
|
||||
$this->charset = 'UTF-8';
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the callable for this response.
|
||||
*
|
||||
* @return callable
|
||||
*/
|
||||
public function getCallable()
|
||||
{
|
||||
return $this->callable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the arguments to the callable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getArguments()
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* "Send" this response by actually running the callable.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function sendContent()
|
||||
{
|
||||
return call_user_func_array($this->callable, $this->arguments);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,579 @@
|
|||
<?php
|
||||
|
||||
namespace SAML;
|
||||
|
||||
use SAML\Error\Exception;
|
||||
use SAML2\Constants as SAML2;
|
||||
use SAML\Auth;
|
||||
use SAML\Error;
|
||||
use SAML\Metadata\MetaDataStorageHandler;
|
||||
use SAML\Module\saml\Error\NoPassive;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* IdP class.
|
||||
*
|
||||
* This class implements the various functions used by IdP.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class IdP
|
||||
{
|
||||
/**
|
||||
* A cache for resolving IdP id's.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $idpCache = [];
|
||||
|
||||
/**
|
||||
* The identifier for this IdP.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* The "association group" for this IdP.
|
||||
*
|
||||
* We use this to support cross-protocol logout until
|
||||
* we implement a cross-protocol IdP.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $associationGroup;
|
||||
|
||||
/**
|
||||
* The configuration for this IdP.
|
||||
*
|
||||
* @var Configuration
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* Our authsource.
|
||||
*
|
||||
* @var Auth\Simple
|
||||
*/
|
||||
private $authSource;
|
||||
|
||||
/**
|
||||
* Initialize an IdP.
|
||||
*
|
||||
* @param string $id The identifier of this IdP.
|
||||
*
|
||||
* @throws Exception If the IdP is disabled or no such auth source was found.
|
||||
*/
|
||||
private function __construct($id)
|
||||
{
|
||||
assert(is_string($id));
|
||||
|
||||
$this->id = $id;
|
||||
$this->associationGroup = $id;
|
||||
|
||||
$metadata = MetaDataStorageHandler::getMetadataHandler();
|
||||
$globalConfig = Configuration::getInstance();
|
||||
|
||||
if (substr($id, 0, 6) === 'saml2:') {
|
||||
if (!$globalConfig->getBoolean('enable.saml20-idp', false)) {
|
||||
throw new Exception('enable.saml20-idp disabled in config.php.');
|
||||
}
|
||||
$this->config = $metadata->getMetaDataConfig(substr($id, 6), 'saml20-idp-hosted');
|
||||
} elseif (substr($id, 0, 6) === 'saml1:') {
|
||||
if (!$globalConfig->getBoolean('enable.shib13-idp', false)) {
|
||||
throw new Exception('enable.shib13-idp disabled in config.php.');
|
||||
}
|
||||
$this->config = $metadata->getMetaDataConfig(substr($id, 6), 'shib13-idp-hosted');
|
||||
} elseif (substr($id, 0, 5) === 'adfs:') {
|
||||
if (!$globalConfig->getBoolean('enable.adfs-idp', false)) {
|
||||
throw new Exception('enable.adfs-idp disabled in config.php.');
|
||||
}
|
||||
$this->config = $metadata->getMetaDataConfig(substr($id, 5), 'adfs-idp-hosted');
|
||||
|
||||
try {
|
||||
// this makes the ADFS IdP use the same SP associations as the SAML 2.0 IdP
|
||||
$saml2EntityId = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted');
|
||||
$this->associationGroup = 'saml2:' . $saml2EntityId;
|
||||
} catch (\Exception $e) {
|
||||
// probably no SAML 2 IdP configured for this host. Ignore the error
|
||||
}
|
||||
} else {
|
||||
throw new \Exception("Protocol not implemented.");
|
||||
}
|
||||
|
||||
$auth = $this->config->getString('auth');
|
||||
if (Auth\Source::getById($auth) !== null) {
|
||||
$this->authSource = new Auth\Simple($auth);
|
||||
} else {
|
||||
throw new Exception('No such "' . $auth . '" auth source found.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the ID of this IdP.
|
||||
*
|
||||
* @return string The ID of this IdP.
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve an IdP by ID.
|
||||
*
|
||||
* @param string $id The identifier of the IdP.
|
||||
*
|
||||
* @return IdP The IdP.
|
||||
*/
|
||||
public static function getById($id)
|
||||
{
|
||||
assert(is_string($id));
|
||||
|
||||
if (isset(self::$idpCache[$id])) {
|
||||
return self::$idpCache[$id];
|
||||
}
|
||||
|
||||
$idp = new self($id);
|
||||
self::$idpCache[$id] = $idp;
|
||||
return $idp;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the IdP "owning" the state.
|
||||
*
|
||||
* @param array &$state The state array.
|
||||
*
|
||||
* @return IdP The IdP.
|
||||
*/
|
||||
public static function getByState(array &$state)
|
||||
{
|
||||
assert(isset($state['core:IdP']));
|
||||
|
||||
return self::getById($state['core:IdP']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the configuration for this IdP.
|
||||
*
|
||||
* @return Configuration The configuration object.
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get SP name.
|
||||
*
|
||||
* @param string $assocId The association identifier.
|
||||
*
|
||||
* @return array|null The name of the SP, as an associative array of language => text, or null if this isn't an SP.
|
||||
*/
|
||||
public function getSPName($assocId)
|
||||
{
|
||||
assert(is_string($assocId));
|
||||
|
||||
$prefix = substr($assocId, 0, 4);
|
||||
$spEntityId = substr($assocId, strlen($prefix) + 1);
|
||||
$metadata = MetaDataStorageHandler::getMetadataHandler();
|
||||
|
||||
if ($prefix === 'saml') {
|
||||
try {
|
||||
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
|
||||
} catch (\Exception $e) {
|
||||
try {
|
||||
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'shib13-sp-remote');
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($prefix === 'adfs') {
|
||||
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'adfs-sp-remote');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($spMetadata->hasValue('name')) {
|
||||
return $spMetadata->getLocalizedString('name');
|
||||
} elseif ($spMetadata->hasValue('OrganizationDisplayName')) {
|
||||
return $spMetadata->getLocalizedString('OrganizationDisplayName');
|
||||
} else {
|
||||
return ['en' => $spEntityId];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add an SP association.
|
||||
*
|
||||
* @param array $association The SP association.
|
||||
* @return void
|
||||
*/
|
||||
public function addAssociation(array $association)
|
||||
{
|
||||
assert(isset($association['id']));
|
||||
assert(isset($association['Handler']));
|
||||
|
||||
$association['core:IdP'] = $this->id;
|
||||
|
||||
$session = Session::getSessionFromRequest();
|
||||
$session->addAssociation($this->associationGroup, $association);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve list of SP associations.
|
||||
*
|
||||
* @return array List of SP associations.
|
||||
*/
|
||||
public function getAssociations()
|
||||
{
|
||||
$session = Session::getSessionFromRequest();
|
||||
return $session->getAssociations($this->associationGroup);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove an SP association.
|
||||
*
|
||||
* @param string $assocId The association id.
|
||||
* @return void
|
||||
*/
|
||||
public function terminateAssociation($assocId)
|
||||
{
|
||||
assert(is_string($assocId));
|
||||
|
||||
$session = Session::getSessionFromRequest();
|
||||
$session->terminateAssociation($this->associationGroup, $assocId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is the current user authenticated?
|
||||
*
|
||||
* @return boolean True if the user is authenticated, false otherwise.
|
||||
*/
|
||||
public function isAuthenticated()
|
||||
{
|
||||
return $this->authSource->isAuthenticated();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called after authproc has run.
|
||||
*
|
||||
* @param array $state The authentication request state array.
|
||||
* @return void
|
||||
*/
|
||||
public static function postAuthProc(array $state)
|
||||
{
|
||||
assert(is_callable($state['Responder']));
|
||||
|
||||
if (isset($state['core:SP'])) {
|
||||
$session = Session::getSessionFromRequest();
|
||||
$session->setData(
|
||||
'core:idp-ssotime',
|
||||
$state['core:IdP'] . ';' . $state['core:SP'],
|
||||
time(),
|
||||
Session::DATA_TIMEOUT_SESSION_END
|
||||
);
|
||||
}
|
||||
|
||||
call_user_func($state['Responder'], $state);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The user is authenticated.
|
||||
*
|
||||
* @param array $state The authentication request state array.
|
||||
*
|
||||
* @return void
|
||||
*@throws Exception If we are not authenticated.
|
||||
*/
|
||||
public static function postAuth(array $state)
|
||||
{
|
||||
$idp = IdP::getByState($state);
|
||||
|
||||
if (!$idp->isAuthenticated()) {
|
||||
throw new Exception('Not authenticated.');
|
||||
}
|
||||
|
||||
$state['Attributes'] = $idp->authSource->getAttributes();
|
||||
|
||||
if (isset($state['SPMetadata'])) {
|
||||
$spMetadata = $state['SPMetadata'];
|
||||
} else {
|
||||
$spMetadata = [];
|
||||
}
|
||||
|
||||
if (isset($state['core:SP'])) {
|
||||
$session = Session::getSessionFromRequest();
|
||||
$previousSSOTime = $session->getData('core:idp-ssotime', $state['core:IdP'] . ';' . $state['core:SP']);
|
||||
if ($previousSSOTime !== null) {
|
||||
$state['PreviousSSOTimestamp'] = $previousSSOTime;
|
||||
}
|
||||
}
|
||||
|
||||
$idpMetadata = $idp->getConfig()->toArray();
|
||||
|
||||
$pc = new Auth\ProcessingChain($idpMetadata, $spMetadata, 'idp');
|
||||
|
||||
$state['ReturnCall'] = ['\SimpleSAML\IdP', 'postAuthProc'];
|
||||
$state['Destination'] = $spMetadata;
|
||||
$state['Source'] = $idpMetadata;
|
||||
|
||||
$pc->processState($state);
|
||||
|
||||
self::postAuthProc($state);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Authenticate the user.
|
||||
*
|
||||
* This function authenticates the user.
|
||||
*
|
||||
* @param array &$state The authentication request state.
|
||||
*
|
||||
* @return void
|
||||
*@throws NoPassive If we were asked to do passive authentication.
|
||||
*/
|
||||
private function authenticate(array &$state)
|
||||
{
|
||||
if (isset($state['isPassive']) && (bool) $state['isPassive']) {
|
||||
throw new NoPassive(SAML2::STATUS_RESPONDER, 'Passive authentication not supported.');
|
||||
}
|
||||
|
||||
$this->authSource->login($state);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Re-authenticate the user.
|
||||
*
|
||||
* This function re-authenticates an user with an existing session. This gives the authentication source a chance
|
||||
* to do additional work when re-authenticating for SSO.
|
||||
*
|
||||
* Note: This function is not used when ForceAuthn=true.
|
||||
*
|
||||
* @param array &$state The authentication request state.
|
||||
*
|
||||
* @throws \Exception If there is no auth source defined for this IdP.
|
||||
* @return void
|
||||
*/
|
||||
private function reauthenticate(array &$state)
|
||||
{
|
||||
$sourceImpl = $this->authSource->getAuthSource();
|
||||
$sourceImpl->reauthenticate($state);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process authentication requests.
|
||||
*
|
||||
* @param array &$state The authentication request state.
|
||||
* @return void
|
||||
*/
|
||||
public function handleAuthenticationRequest(array &$state)
|
||||
{
|
||||
assert(isset($state['Responder']));
|
||||
|
||||
$state['core:IdP'] = $this->id;
|
||||
|
||||
if (isset($state['SPMetadata']['entityid'])) {
|
||||
$spEntityId = $state['SPMetadata']['entityid'];
|
||||
} elseif (isset($state['SPMetadata']['entityID'])) {
|
||||
$spEntityId = $state['SPMetadata']['entityID'];
|
||||
} else {
|
||||
$spEntityId = null;
|
||||
}
|
||||
$state['core:SP'] = $spEntityId;
|
||||
|
||||
// first, check whether we need to authenticate the user
|
||||
if (isset($state['ForceAuthn']) && (bool) $state['ForceAuthn']) {
|
||||
// force authentication is in effect
|
||||
$needAuth = true;
|
||||
} else {
|
||||
$needAuth = !$this->isAuthenticated();
|
||||
}
|
||||
|
||||
$state['IdPMetadata'] = $this->getConfig()->toArray();
|
||||
$state['ReturnCallback'] = ['\SimpleSAML\IdP', 'postAuth'];
|
||||
|
||||
try {
|
||||
if ($needAuth) {
|
||||
$this->authenticate($state);
|
||||
assert(false);
|
||||
} else {
|
||||
$this->reauthenticate($state);
|
||||
}
|
||||
$this->postAuth($state);
|
||||
} catch (Exception $e) {
|
||||
Auth\State::throwException($state, $e);
|
||||
} catch (\Exception $e) {
|
||||
$e = new Error\UnserializableException($e);
|
||||
Auth\State::throwException($state, $e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the logout handler of this IdP.
|
||||
*
|
||||
* @return IdP\LogoutHandlerInterface The logout handler class.
|
||||
* @throws \Exception If we cannot find a logout handler.
|
||||
*/
|
||||
public function getLogoutHandler()
|
||||
{
|
||||
// find the logout handler
|
||||
$logouttype = $this->getConfig()->getString('logouttype', 'traditional');
|
||||
switch ($logouttype) {
|
||||
case 'traditional':
|
||||
$handler = '\SimpleSAML\IdP\TraditionalLogoutHandler';
|
||||
break;
|
||||
case 'iframe':
|
||||
$handler = '\SimpleSAML\IdP\IFrameLogoutHandler';
|
||||
break;
|
||||
default:
|
||||
throw new Exception('Unknown logout handler: ' . var_export($logouttype, true));
|
||||
}
|
||||
|
||||
/** @var IdP\LogoutHandlerInterface */
|
||||
return new $handler($this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finish the logout operation.
|
||||
*
|
||||
* This function will never return.
|
||||
*
|
||||
* @param array &$state The logout request state.
|
||||
* @return void
|
||||
*/
|
||||
public function finishLogout(array &$state)
|
||||
{
|
||||
assert(isset($state['Responder']));
|
||||
|
||||
$idp = IdP::getByState($state);
|
||||
call_user_func($state['Responder'], $idp, $state);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a logout request.
|
||||
*
|
||||
* This function will never return.
|
||||
*
|
||||
* @param array &$state The logout request state.
|
||||
* @param string|null $assocId The association we received the logout request from, or null if there was no
|
||||
* association.
|
||||
* @return void
|
||||
*/
|
||||
public function handleLogoutRequest(array &$state, $assocId)
|
||||
{
|
||||
assert(isset($state['Responder']));
|
||||
assert(is_string($assocId) || $assocId === null);
|
||||
|
||||
$state['core:IdP'] = $this->id;
|
||||
$state['core:TerminatedAssocId'] = $assocId;
|
||||
|
||||
if ($assocId !== null) {
|
||||
$this->terminateAssociation($assocId);
|
||||
$session = Session::getSessionFromRequest();
|
||||
$session->deleteData('core:idp-ssotime', $this->id . ';' . $state['saml:SPEntityId']);
|
||||
}
|
||||
|
||||
// terminate the local session
|
||||
$id = Auth\State::saveState($state, 'core:Logout:afterbridge');
|
||||
$returnTo = Module::getModuleURL('core/idp/resumelogout.php', ['id' => $id]);
|
||||
|
||||
$this->authSource->logout($returnTo);
|
||||
|
||||
if ($assocId !== null) {
|
||||
$handler = $this->getLogoutHandler();
|
||||
$handler->startLogout($state, $assocId);
|
||||
}
|
||||
assert(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a logout response.
|
||||
*
|
||||
* This function will never return.
|
||||
*
|
||||
* @param string $assocId The association that is terminated.
|
||||
* @param string|null $relayState The RelayState from the start of the logout.
|
||||
* @param Exception|null $error The error that occurred during session termination (if any).
|
||||
* @return void
|
||||
*/
|
||||
public function handleLogoutResponse($assocId, $relayState, Exception $error = null)
|
||||
{
|
||||
assert(is_string($assocId));
|
||||
assert(is_string($relayState) || $relayState === null);
|
||||
|
||||
$index = strpos($assocId, ':');
|
||||
assert(is_int($index));
|
||||
|
||||
$session = Session::getSessionFromRequest();
|
||||
$session->deleteData('core:idp-ssotime', $this->id . ';' . substr($assocId, $index + 1));
|
||||
|
||||
$handler = $this->getLogoutHandler();
|
||||
$handler->onResponse($assocId, $relayState, $error);
|
||||
|
||||
assert(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log out, then redirect to a URL.
|
||||
*
|
||||
* This function never returns.
|
||||
*
|
||||
* @param string $url The URL the user should be returned to after logout.
|
||||
* @return void
|
||||
*/
|
||||
public function doLogoutRedirect($url)
|
||||
{
|
||||
assert(is_string($url));
|
||||
|
||||
$state = [
|
||||
'Responder' => ['\SimpleSAML\IdP', 'finishLogoutRedirect'],
|
||||
'core:Logout:URL' => $url,
|
||||
];
|
||||
|
||||
$this->handleLogoutRequest($state, null);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Redirect to a URL after logout.
|
||||
*
|
||||
* This function never returns.
|
||||
*
|
||||
* @param IdP $idp Deprecated. Will be removed.
|
||||
* @param array &$state The logout state from doLogoutRedirect().
|
||||
* @return void
|
||||
*/
|
||||
public static function finishLogoutRedirect(IdP $idp, array $state)
|
||||
{
|
||||
assert(isset($state['core:Logout:URL']));
|
||||
|
||||
Utils\HTTP::redirectTrustedURL($state['core:Logout:URL']);
|
||||
assert(false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\IdP;
|
||||
|
||||
use SAML\Auth;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Error\Exception;
|
||||
use SAML\IdP;
|
||||
use SAML\Module;
|
||||
use SAML\Utils;
|
||||
use SAML\XHTML\Template;
|
||||
|
||||
/**
|
||||
* Class that handles iframe logout.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class IFrameLogoutHandler implements LogoutHandlerInterface
|
||||
{
|
||||
/**
|
||||
* The IdP we are logging out from.
|
||||
*
|
||||
* @var IdP
|
||||
*/
|
||||
private $idp;
|
||||
|
||||
/**
|
||||
* LogoutIFrame constructor.
|
||||
*
|
||||
* @param IdP $idp The IdP to log out from.
|
||||
*/
|
||||
public function __construct(IdP $idp)
|
||||
{
|
||||
$this->idp = $idp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the logout operation.
|
||||
*
|
||||
* @param array &$state The logout state.
|
||||
* @param string|null $assocId The SP we are logging out from.
|
||||
* @return void
|
||||
*/
|
||||
public function startLogout(array &$state, $assocId)
|
||||
{
|
||||
assert(is_string($assocId) || $assocId === null);
|
||||
|
||||
$associations = $this->idp->getAssociations();
|
||||
|
||||
if (count($associations) === 0) {
|
||||
$this->idp->finishLogout($state);
|
||||
}
|
||||
|
||||
foreach ($associations as $id => &$association) {
|
||||
$idp = IdP::getByState($association);
|
||||
$association['core:Logout-IFrame:Name'] = $idp->getSPName($id);
|
||||
$association['core:Logout-IFrame:State'] = 'onhold';
|
||||
}
|
||||
$state['core:Logout-IFrame:Associations'] = $associations;
|
||||
|
||||
if (!is_null($assocId)) {
|
||||
$spName = $this->idp->getSPName($assocId);
|
||||
if ($spName === null) {
|
||||
$spName = ['en' => $assocId];
|
||||
}
|
||||
|
||||
$state['core:Logout-IFrame:From'] = $spName;
|
||||
} else {
|
||||
$state['core:Logout-IFrame:From'] = null;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'id' => Auth\State::saveState($state, 'core:Logout-IFrame'),
|
||||
];
|
||||
if (isset($state['core:Logout-IFrame:InitType'])) {
|
||||
$params['type'] = $state['core:Logout-IFrame:InitType'];
|
||||
}
|
||||
|
||||
$url = Module::getModuleURL('core/idp/logout-iframe.php', $params);
|
||||
Utils\HTTP::redirectTrustedURL($url);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Continue the logout operation.
|
||||
*
|
||||
* This function will never return.
|
||||
*
|
||||
* @param string $assocId The association that is terminated.
|
||||
* @param string|null $relayState The RelayState from the start of the logout.
|
||||
* @param Exception|null $error The error that occurred during session termination (if any).
|
||||
* @return void
|
||||
*/
|
||||
public function onResponse($assocId, $relayState, Exception $error = null)
|
||||
{
|
||||
assert(is_string($assocId));
|
||||
|
||||
$this->idp->terminateAssociation($assocId);
|
||||
|
||||
$config = Configuration::getInstance();
|
||||
|
||||
$t = new Template($config, 'IFrameLogoutHandler.tpl.php');
|
||||
$t->data['assocId'] = var_export($assocId, true);
|
||||
$t->data['spId'] = sha1($assocId);
|
||||
if (!is_null($error)) {
|
||||
$t->data['errorMsg'] = $error->getMessage();
|
||||
}
|
||||
|
||||
$t->show();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\IdP;
|
||||
|
||||
use SAML\Error;
|
||||
use SAML\Error\Exception;
|
||||
use SAML\IdP;
|
||||
|
||||
/**
|
||||
* Interface that all logout handlers must implement.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
interface LogoutHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Initialize this logout handler.
|
||||
*
|
||||
* @param IdP $idp The IdP we are logging out from.
|
||||
*/
|
||||
public function __construct(IdP $idp);
|
||||
|
||||
|
||||
/**
|
||||
* Start a logout operation.
|
||||
*
|
||||
* This function must never return.
|
||||
*
|
||||
* @param array &$state The logout state.
|
||||
* @param string $assocId The association that started the logout.
|
||||
* @return void
|
||||
*/
|
||||
public function startLogout(array &$state, $assocId);
|
||||
|
||||
|
||||
/**
|
||||
* Handles responses to our logout requests.
|
||||
*
|
||||
* This function will never return.
|
||||
*
|
||||
* @param string $assocId The association that is terminated.
|
||||
* @param string|null $relayState The RelayState from the start of the logout.
|
||||
* @param Exception|null $error The error that occurred during session termination (if any).
|
||||
* @return void
|
||||
*/
|
||||
public function onResponse($assocId, $relayState, Exception $error = null);
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\IdP;
|
||||
|
||||
use SAML\Auth;
|
||||
use SAML\Error;
|
||||
use SAML\Error\Exception;
|
||||
use SAML\IdP;
|
||||
use SAML\Logger;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* Class that handles traditional logout.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class TraditionalLogoutHandler implements LogoutHandlerInterface
|
||||
{
|
||||
/**
|
||||
* The IdP we are logging out from.
|
||||
*
|
||||
* @var IdP
|
||||
*/
|
||||
private $idp;
|
||||
|
||||
|
||||
/**
|
||||
* TraditionalLogout constructor.
|
||||
*
|
||||
* @param IdP $idp The IdP to log out from.
|
||||
*/
|
||||
public function __construct(IdP $idp)
|
||||
{
|
||||
$this->idp = $idp;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Picks the next SP and issues a logout request.
|
||||
*
|
||||
* This function never returns.
|
||||
*
|
||||
* @param array &$state The logout state.
|
||||
* @return void
|
||||
*/
|
||||
private function logoutNextSP(array &$state)
|
||||
{
|
||||
$association = array_pop($state['core:LogoutTraditional:Remaining']);
|
||||
if ($association === null) {
|
||||
$this->idp->finishLogout($state);
|
||||
}
|
||||
|
||||
$relayState = Auth\State::saveState($state, 'core:LogoutTraditional', true);
|
||||
|
||||
$id = $association['id'];
|
||||
Logger::info('Logging out of ' . var_export($id, true) . '.');
|
||||
|
||||
try {
|
||||
$idp = IdP::getByState($association);
|
||||
$url = call_user_func([$association['Handler'], 'getLogoutURL'], $idp, $association, $relayState);
|
||||
Utils\HTTP::redirectTrustedURL($url);
|
||||
} catch (\Exception $e) {
|
||||
Logger::warning('Unable to initialize logout to ' . var_export($id, true) . '.');
|
||||
$this->idp->terminateAssociation($id);
|
||||
$state['core:Failed'] = true;
|
||||
|
||||
// Try the next SP
|
||||
$this->logoutNextSP($state);
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the logout operation.
|
||||
*
|
||||
* This function never returns.
|
||||
*
|
||||
* @param array &$state The logout state.
|
||||
* @param string $assocId The association that started the logout.
|
||||
* @return void
|
||||
*/
|
||||
public function startLogout(array &$state, $assocId)
|
||||
{
|
||||
$state['core:LogoutTraditional:Remaining'] = $this->idp->getAssociations();
|
||||
|
||||
$this->logoutNextSP($state);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Continue the logout operation.
|
||||
*
|
||||
* This function will never return.
|
||||
*
|
||||
* @param string $assocId The association that is terminated.
|
||||
* @param string|null $relayState The RelayState from the start of the logout.
|
||||
* @param Exception|null $error The error that occurred during session termination (if any).
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception If the RelayState was lost during logout.
|
||||
*/
|
||||
public function onResponse($assocId, $relayState, Exception $error = null)
|
||||
{
|
||||
assert(is_string($assocId));
|
||||
assert(is_string($relayState) || $relayState === null);
|
||||
|
||||
if ($relayState === null) {
|
||||
throw new Exception('RelayState lost during logout.');
|
||||
}
|
||||
|
||||
$state = Auth\State::loadState($relayState, 'core:LogoutTraditional');
|
||||
|
||||
if ($error === null) {
|
||||
Logger::info('Logged out of ' . var_export($assocId, true) . '.');
|
||||
$this->idp->terminateAssociation($assocId);
|
||||
} else {
|
||||
Logger::warning('Error received from ' . var_export($assocId, true) . ' during logout:');
|
||||
$error->logWarning();
|
||||
$state['core:Failed'] = true;
|
||||
}
|
||||
|
||||
$this->logoutNextSP($state);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,432 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Choosing the language to localize to for our minimalistic XHTML PHP based template system.
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML\Locale;
|
||||
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
use SAML\Utils;
|
||||
|
||||
class Language
|
||||
{
|
||||
|
||||
/**
|
||||
* This is the default language map. It is used to map languages codes from the user agent to other language codes.
|
||||
*/
|
||||
private static $defaultLanguageMap = ['nb' => 'no'];
|
||||
|
||||
/**
|
||||
* The configuration to use.
|
||||
*
|
||||
* @var Configuration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* An array holding a list of languages available.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $availableLanguages;
|
||||
|
||||
/**
|
||||
* The language currently in use.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
private $language = null;
|
||||
|
||||
/**
|
||||
* The language to use by default.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $defaultLanguage;
|
||||
|
||||
/**
|
||||
* An array holding a list of languages that are written from right to left.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $rtlLanguages;
|
||||
|
||||
/**
|
||||
* HTTP GET language parameter name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $languageParameterName;
|
||||
|
||||
/**
|
||||
* A custom function to use in order to determine the language in use.
|
||||
*
|
||||
* @var callable|null
|
||||
*/
|
||||
private $customFunction;
|
||||
|
||||
/**
|
||||
* A list of languages supported with their names localized.
|
||||
* Indexed by something that mostly resembles ISO 639-1 code,
|
||||
* with some charming SimpleSAML-specific variants...
|
||||
* that must remain before 2.0 due to backwards compatibility
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $language_names = [
|
||||
'no' => 'Bokmål', // Norwegian Bokmål
|
||||
'nn' => 'Nynorsk', // Norwegian Nynorsk
|
||||
'se' => 'Sámegiella', // Northern Sami
|
||||
'sma' => 'Åarjelh-saemien giele', // Southern Sami
|
||||
'da' => 'Dansk', // Danish
|
||||
'en' => 'English',
|
||||
'de' => 'Deutsch', // German
|
||||
'sv' => 'Svenska', // Swedish
|
||||
'fi' => 'Suomeksi', // Finnish
|
||||
'es' => 'Español', // Spanish
|
||||
'ca' => 'Català', // Catalan
|
||||
'fr' => 'Français', // French
|
||||
'it' => 'Italiano', // Italian
|
||||
'nl' => 'Nederlands', // Dutch
|
||||
'lb' => 'Lëtzebuergesch', // Luxembourgish
|
||||
'cs' => 'Čeština', // Czech
|
||||
'sl' => 'Slovenščina', // Slovensk
|
||||
'lt' => 'Lietuvių kalba', // Lithuanian
|
||||
'hr' => 'Hrvatski', // Croatian
|
||||
'hu' => 'Magyar', // Hungarian
|
||||
'pl' => 'Język polski', // Polish
|
||||
'pt' => 'Português', // Portuguese
|
||||
'pt-br' => 'Português brasileiro', // Portuguese
|
||||
'ru' => 'русский язык', // Russian
|
||||
'et' => 'eesti keel', // Estonian
|
||||
'tr' => 'Türkçe', // Turkish
|
||||
'el' => 'ελληνικά', // Greek
|
||||
'ja' => '日本語', // Japanese
|
||||
'zh' => '简体中文', // Chinese (simplified)
|
||||
'zh-tw' => '繁體中文', // Chinese (traditional)
|
||||
'ar' => 'العربية', // Arabic
|
||||
'fa' => 'پارسی', // Persian
|
||||
'ur' => 'اردو', // Urdu
|
||||
'he' => 'עִבְרִית', // Hebrew
|
||||
'id' => 'Bahasa Indonesia', // Indonesian
|
||||
'sr' => 'Srpski', // Serbian
|
||||
'lv' => 'Latviešu', // Latvian
|
||||
'ro' => 'Românește', // Romanian
|
||||
'eu' => 'Euskara', // Basque
|
||||
'af' => 'Afrikaans', // Afrikaans
|
||||
'zu' => 'IsiZulu', // Zulu
|
||||
'xh' => 'isiXhosa', // Xhosa
|
||||
];
|
||||
|
||||
/**
|
||||
* A mapping of SSP languages to locales
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $languagePosixMapping = [
|
||||
'no' => 'nb_NO',
|
||||
'nn' => 'nn_NO',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Configuration $configuration Configuration object
|
||||
*/
|
||||
public function __construct(Configuration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->availableLanguages = $this->getInstalledLanguages();
|
||||
$this->defaultLanguage = $this->configuration->getString('language.default', 'en');
|
||||
$this->languageParameterName = $this->configuration->getString('language.parameter.name', 'language');
|
||||
$this->customFunction = $this->configuration->getArray('language.get_language_function', null);
|
||||
$this->rtlLanguages = $this->configuration->getArray('language.rtl', []);
|
||||
if (isset($_GET[$this->languageParameterName])) {
|
||||
$this->setLanguage(
|
||||
$_GET[$this->languageParameterName],
|
||||
$this->configuration->getBoolean('language.parameter.setcookie', true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter configured (available) languages against installed languages.
|
||||
*
|
||||
* @return array The set of languages both in 'language.available' and self::$language_names.
|
||||
*/
|
||||
private function getInstalledLanguages()
|
||||
{
|
||||
$configuredAvailableLanguages = $this->configuration->getArray('language.available', ['en']);
|
||||
$availableLanguages = [];
|
||||
foreach ($configuredAvailableLanguages as $code) {
|
||||
if (array_key_exists($code, self::$language_names) && isset(self::$language_names[$code])) {
|
||||
$availableLanguages[] = $code;
|
||||
} else {
|
||||
Logger::error("Language \"$code\" not installed. Check config.");
|
||||
}
|
||||
}
|
||||
return $availableLanguages;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rename to non-idiosyncratic language code.
|
||||
*
|
||||
* @param string $language Language code for the language to rename, if necessary.
|
||||
*
|
||||
* @return string The language code.
|
||||
*/
|
||||
public function getPosixLanguage($language)
|
||||
{
|
||||
if (isset($this->languagePosixMapping[$language])) {
|
||||
return $this->languagePosixMapping[$language];
|
||||
}
|
||||
return $language;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method will set a cookie for the user's browser to remember what language was selected.
|
||||
*
|
||||
* @param string $language Language code for the language to set.
|
||||
* @param boolean $setLanguageCookie Whether to set the language cookie or not. Defaults to true.
|
||||
* @return void
|
||||
*/
|
||||
public function setLanguage($language, $setLanguageCookie = true)
|
||||
{
|
||||
$language = strtolower($language);
|
||||
if (in_array($language, $this->availableLanguages, true)) {
|
||||
$this->language = $language;
|
||||
if ($setLanguageCookie === true) {
|
||||
self::setLanguageCookie($language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method will return the language selected by the user, or the default language. It looks first for a cached
|
||||
* language code, then checks for a language cookie, then it tries to calculate the preferred language from HTTP
|
||||
* headers.
|
||||
*
|
||||
* @return string The language selected by the user according to the processing rules specified, or the default
|
||||
* language in any other case.
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
// language is set in object
|
||||
if (isset($this->language)) {
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
// run custom getLanguage function if defined
|
||||
if (isset($this->customFunction) && is_callable($this->customFunction)) {
|
||||
$customLanguage = call_user_func($this->customFunction, $this);
|
||||
if ($customLanguage !== null && $customLanguage !== false) {
|
||||
return $customLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
// language is provided in a stored cookie
|
||||
$languageCookie = self::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();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the localized name of a language, by ISO 639-2 code.
|
||||
*
|
||||
* @param string $code The ISO 639-2 code of the language.
|
||||
*
|
||||
* @return string|null The localized name of the language.
|
||||
*/
|
||||
public function getLanguageLocalizedName($code)
|
||||
{
|
||||
if (array_key_exists($code, self::$language_names) && isset(self::$language_names[$code])) {
|
||||
return self::$language_names[$code];
|
||||
}
|
||||
Logger::error("Name for language \"$code\" not found. Check config.");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the language parameter name.
|
||||
*
|
||||
* @return string The language parameter name.
|
||||
*/
|
||||
public function getLanguageParameterName()
|
||||
{
|
||||
return $this->languageParameterName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method returns the preferred language for the user based on the Accept-Language HTTP header.
|
||||
*
|
||||
* @return string|null The preferred language based on the Accept-Language HTTP header,
|
||||
* or null if none of the languages in the header is available.
|
||||
*/
|
||||
private function getHTTPLanguage()
|
||||
{
|
||||
$languageScore = Utils\HTTP::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 precision 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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the default language according to configuration.
|
||||
*
|
||||
* @return string The default language that has been configured. Defaults to english if not configured.
|
||||
*/
|
||||
public function getDefaultLanguage()
|
||||
{
|
||||
return $this->defaultLanguage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return an alias for a language code, if any.
|
||||
*
|
||||
* @param string $langcode
|
||||
* @return string|null The alias, or null if the alias was not found.
|
||||
*/
|
||||
public function getLanguageCodeAlias($langcode)
|
||||
{
|
||||
if (isset(self::$defaultLanguageMap[$langcode])) {
|
||||
return self::$defaultLanguageMap[$langcode];
|
||||
}
|
||||
// No alias found, which is fine
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return an indexed list of all languages available.
|
||||
*
|
||||
* @return array An array holding all the languages available as the keys of the array. The value for each key is
|
||||
* true in case that the language specified by that key is currently active, or false otherwise.
|
||||
*/
|
||||
public function getLanguageList()
|
||||
{
|
||||
$current = $this->getLanguage();
|
||||
$list = array_fill_keys($this->availableLanguages, false);
|
||||
$list[$current] = true;
|
||||
return $list;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether a language is written from the right to the left or not.
|
||||
*
|
||||
* @return boolean True if the language is right-to-left, false otherwise.
|
||||
*/
|
||||
public function isLanguageRTL()
|
||||
{
|
||||
return in_array($this->getLanguage(), $this->rtlLanguages, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the user-selected language from a cookie.
|
||||
*
|
||||
* @return string|null The selected language or null if unset.
|
||||
*/
|
||||
public static function getLanguageCookie()
|
||||
{
|
||||
$config = Configuration::getInstance();
|
||||
$availableLanguages = $config->getArray('language.available', ['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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method will attempt to set the user-selected language in a cookie. It will do nothing if the language
|
||||
* specified is not in the list of available languages, or the headers have already been sent to the browser.
|
||||
*
|
||||
* @param string $language The language set by the user.
|
||||
* @return void
|
||||
*/
|
||||
public static function setLanguageCookie($language)
|
||||
{
|
||||
assert(is_string($language));
|
||||
|
||||
$language = strtolower($language);
|
||||
$config = Configuration::getInstance();
|
||||
$availableLanguages = $config->getArray('language.available', ['en']);
|
||||
|
||||
if (!in_array($language, $availableLanguages, true) || headers_sent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = $config->getString('language.cookie.name', 'language');
|
||||
$params = [
|
||||
'lifetime' => ($config->getInteger('language.cookie.lifetime', 60 * 60 * 24 * 900)),
|
||||
'domain' => ($config->getString('language.cookie.domain', null)),
|
||||
'path' => ($config->getString('language.cookie.path', '/')),
|
||||
'secure' => ($config->getBoolean('language.cookie.secure', false)),
|
||||
'httponly' => ($config->getBoolean('language.cookie.httponly', false)),
|
||||
'samesite' => ($config->getString('language.cookie.samesite', null)),
|
||||
];
|
||||
|
||||
Utils\HTTP::setCookie($name, $language, $params, false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,309 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Glue to connect one or more translation/locale systems to the rest
|
||||
*
|
||||
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML\Locale;
|
||||
|
||||
use Exception;
|
||||
use Gettext\Translations;
|
||||
use Gettext\Translator;
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
|
||||
class Localization
|
||||
{
|
||||
/**
|
||||
* The configuration to use.
|
||||
*
|
||||
* @var Configuration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* The default gettext domain.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const DEFAULT_DOMAIN = 'messages';
|
||||
|
||||
/**
|
||||
* Old internationalization backend included in SimpleSAMLphp.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SSP_I18N_BACKEND = 'SimpleSAMLphp';
|
||||
|
||||
/**
|
||||
* An internationalization backend implemented purely in PHP.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const GETTEXT_I18N_BACKEND = 'gettext/gettext';
|
||||
|
||||
/**
|
||||
* The default locale directory
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $localeDir;
|
||||
|
||||
/**
|
||||
* Where specific domains are stored
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $localeDomainMap = [];
|
||||
|
||||
/**
|
||||
* Pointer to currently active translator
|
||||
*
|
||||
* @var Translator
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* Pointer to current Language
|
||||
*
|
||||
* @var Language
|
||||
*/
|
||||
private $language;
|
||||
|
||||
/**
|
||||
* Language code representing the current Language
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $langcode;
|
||||
|
||||
|
||||
/**
|
||||
* The language backend to use
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $i18nBackend;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Configuration $configuration Configuration object
|
||||
*/
|
||||
public function __construct(Configuration $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
/** @var string $locales */
|
||||
$locales = $this->configuration->resolvePath('locales');
|
||||
$this->localeDir = $locales;
|
||||
$this->language = new Language($configuration);
|
||||
$this->langcode = $this->language->getPosixLanguage($this->language->getLanguage());
|
||||
$this->i18nBackend = (
|
||||
$this->configuration->getBoolean('usenewui', false)
|
||||
? self::GETTEXT_I18N_BACKEND
|
||||
: self::SSP_I18N_BACKEND
|
||||
);
|
||||
$this->setupL10N();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dump the default locale directory
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocaleDir()
|
||||
{
|
||||
return $this->localeDir;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the default locale dir for a specific module aka. domain
|
||||
*
|
||||
* @param string $domain Name of module/domain
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDomainLocaleDir($domain)
|
||||
{
|
||||
/** @var string $base */
|
||||
$base = $this->configuration->resolvePath('modules');
|
||||
$localeDir = $base . '/' . $domain . '/locales';
|
||||
return $localeDir;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a new translation domain from a module
|
||||
* (We're assuming that each domain only exists in one place)
|
||||
*
|
||||
* @param string $module Module name
|
||||
* @param string $localeDir Absolute path if the module is housed elsewhere
|
||||
* @return void
|
||||
*/
|
||||
public function addModuleDomain($module, $localeDir = null)
|
||||
{
|
||||
if (!$localeDir) {
|
||||
$localeDir = $this->getDomainLocaleDir($module);
|
||||
}
|
||||
$this->addDomain($localeDir, $module);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a new translation domain
|
||||
* (We're assuming that each domain only exists in one place)
|
||||
*
|
||||
* @param string $localeDir Location of translations
|
||||
* @param string $domain Domain at location
|
||||
* @return void
|
||||
*/
|
||||
public function addDomain($localeDir, $domain)
|
||||
{
|
||||
$this->localeDomainMap[$domain] = $localeDir;
|
||||
Logger::debug("Localization: load domain '$domain' at '$localeDir'");
|
||||
$this->loadGettextGettextFromPO($domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get and check path of localization file
|
||||
*
|
||||
* @param string $domain Name of localization domain
|
||||
* @throws Exception If the path does not exist even for the default, fallback language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLangPath($domain = self::DEFAULT_DOMAIN)
|
||||
{
|
||||
$langcode = explode('_', $this->langcode);
|
||||
$langcode = $langcode[0];
|
||||
$localeDir = $this->localeDomainMap[$domain];
|
||||
$langPath = $localeDir . '/' . $langcode . '/LC_MESSAGES/';
|
||||
Logger::debug("Trying langpath for '$langcode' as '$langPath'");
|
||||
if (is_dir($langPath) && is_readable($langPath)) {
|
||||
return $langPath;
|
||||
}
|
||||
|
||||
// Some langcodes have aliases..
|
||||
$alias = $this->language->getLanguageCodeAlias($langcode);
|
||||
if (isset($alias)) {
|
||||
$langPath = $localeDir . '/' . $alias . '/LC_MESSAGES/';
|
||||
Logger::debug("Trying langpath for alternative '$alias' as '$langPath'");
|
||||
if (is_dir($langPath) && is_readable($langPath)) {
|
||||
return $langPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Language not found, fall back to default
|
||||
$defLangcode = $this->language->getDefaultLanguage();
|
||||
$langPath = $localeDir . '/' . $defLangcode . '/LC_MESSAGES/';
|
||||
if (is_dir($langPath) && is_readable($langPath)) {
|
||||
// Report that the localization for the preferred language is missing
|
||||
$error = "Localization not found for langcode '$langcode' at '$langPath', falling back to langcode '" .
|
||||
$defLangcode . "'";
|
||||
Logger::error($_SERVER['PHP_SELF'] . ' - ' . $error);
|
||||
return $langPath;
|
||||
}
|
||||
|
||||
// Locale for default language missing even, error out
|
||||
$error = "Localization directory missing/broken for langcode '$langcode' and domain '$domain'";
|
||||
Logger::critical($_SERVER['PHP_SELF'] . ' - ' . $error);
|
||||
throw new Exception($error);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setup the translator
|
||||
* @return void
|
||||
*/
|
||||
private function setupTranslator()
|
||||
{
|
||||
$this->translator = new Translator();
|
||||
$this->translator->register();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load translation domain from Gettext/Gettext using .po
|
||||
*
|
||||
* Note: Since Twig I18N does not support domains, all loaded files are
|
||||
* merged. Use contexts if identical strings need to be disambiguated.
|
||||
*
|
||||
* @param string $domain Name of domain
|
||||
* @param boolean $catchException Whether to catch an exception on error or return early
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception If something is wrong with the locale file for the domain and activated language
|
||||
*/
|
||||
private function loadGettextGettextFromPO($domain = self::DEFAULT_DOMAIN, $catchException = true)
|
||||
{
|
||||
try {
|
||||
$langPath = $this->getLangPath($domain);
|
||||
} catch (Exception $e) {
|
||||
$error = "Something went wrong when trying to get path to language file, cannot load domain '$domain'.";
|
||||
Logger::debug($_SERVER['PHP_SELF'] . ' - ' . $error);
|
||||
if ($catchException) {
|
||||
// bail out!
|
||||
return;
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$poFile = $domain . '.po';
|
||||
$poPath = $langPath . $poFile;
|
||||
if (file_exists($poPath) && is_readable($poPath)) {
|
||||
$translations = Translations::fromPoFile($poPath);
|
||||
$this->translator->loadTranslations($translations);
|
||||
} else {
|
||||
$error = "Localization file '$poFile' not found in '$langPath', falling back to default";
|
||||
Logger::debug($_SERVER['PHP_SELF'] . ' - ' . $error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test to check if backend is set to default
|
||||
*
|
||||
* (if false: backend unset/there's an error)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isI18NBackendDefault()
|
||||
{
|
||||
if ($this->i18nBackend === $this::SSP_I18N_BACKEND) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set up L18N if configured or fallback to old system
|
||||
* @return void
|
||||
*/
|
||||
private function setupL10N()
|
||||
{
|
||||
if ($this->i18nBackend === self::SSP_I18N_BACKEND) {
|
||||
Logger::debug("Localization: using old system");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setupTranslator();
|
||||
// setup default domain
|
||||
$this->addDomain($this->localeDir, self::DEFAULT_DOMAIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show which domains are registered
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRegisteredDomains()
|
||||
{
|
||||
return $this->localeDomainMap;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,566 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The translation-relevant bits from our original minimalistic XHTML PHP based template system.
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML\Locale;
|
||||
|
||||
use Exception;
|
||||
use Gettext\BaseTranslator;
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
use SAML\Module;
|
||||
|
||||
class Translate
|
||||
{
|
||||
/**
|
||||
* The configuration to be used for this translator.
|
||||
*
|
||||
* @var Configuration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* Associative array of languages.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $langtext = [];
|
||||
|
||||
/**
|
||||
* Associative array of dictionaries.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $dictionaries = [];
|
||||
|
||||
/**
|
||||
* The default dictionary.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $defaultDictionary = null;
|
||||
|
||||
/**
|
||||
* The language object we'll use internally.
|
||||
*
|
||||
* @var Language
|
||||
*/
|
||||
private $language;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Configuration $configuration Configuration object
|
||||
* @param string|null $defaultDictionary The default dictionary where tags will come from.
|
||||
*/
|
||||
public function __construct(Configuration $configuration, $defaultDictionary = null)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->language = new Language($configuration);
|
||||
|
||||
if ($defaultDictionary !== null && substr($defaultDictionary, -4) === '.php') {
|
||||
// TODO: drop this entire if clause for 2.0
|
||||
// for backwards compatibility - print warning
|
||||
$backtrace = debug_backtrace();
|
||||
$where = $backtrace[0]['file'] . ':' . $backtrace[0]['line'];
|
||||
Logger::warning(
|
||||
'Deprecated use of new SimpleSAML\Locale\Translate(...) 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the internal language object used by this translator.
|
||||
*
|
||||
* @return Language
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method retrieves a dictionary with the name given.
|
||||
*
|
||||
* @param string $name The name of the dictionary, as the filename in the dictionary directory, without the
|
||||
* '.php' ending.
|
||||
*
|
||||
* @return array 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 = Module::getModuleDir($module) . '/dictionaries/';
|
||||
} else {
|
||||
$dictDir = $this->configuration->getPathValue('dictionarydir', 'dictionaries/') ?: 'dictionaries/';
|
||||
$fileName = $name;
|
||||
}
|
||||
|
||||
$this->dictionaries[$name] = $this->readDictionaryFile($dictDir . $fileName);
|
||||
}
|
||||
|
||||
return $this->dictionaries[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method retrieves a tag as an array with language => string mappings.
|
||||
*
|
||||
* @param string $tag The tag name. The tag name can also be on the form '{<dictionary>:<tag>}', to retrieve a tag
|
||||
* from the specific dictionary.
|
||||
*
|
||||
* @return array|null An 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 array $translations The translations, as an associative array with language => text mappings.
|
||||
*
|
||||
* @return string The preferred translation.
|
||||
*
|
||||
* @throws Exception If there's no suitable translation.
|
||||
*/
|
||||
public function getPreferredTranslation($translations)
|
||||
{
|
||||
assert(is_array($translations));
|
||||
|
||||
// look up translation of tag in the selected language
|
||||
$selected_language = $this->language->getLanguage();
|
||||
if (array_key_exists($selected_language, $translations)) {
|
||||
return $translations[$selected_language];
|
||||
}
|
||||
|
||||
// look up translation of tag in the default language
|
||||
$default_language = $this->language->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 the name of an attribute.
|
||||
*
|
||||
* @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->getPreferredTranslation($dict[$normName]);
|
||||
}
|
||||
}
|
||||
|
||||
// search the default attribute dictionary
|
||||
$dict = $this->getDictionary('attributes');
|
||||
if (array_key_exists('attribute_' . $normName, $dict)) {
|
||||
return $this->getPreferredTranslation($dict['attribute_' . $normName]);
|
||||
}
|
||||
|
||||
// no translations found
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a string for translation without translating it.
|
||||
*
|
||||
* @param string $tag A tag name to mark for translation.
|
||||
*
|
||||
* @return string The tag, unchanged.
|
||||
*/
|
||||
public static function noop($tag)
|
||||
{
|
||||
return $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 occurrences 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. The array version will go away in 2.0
|
||||
* @param array $replacements An associative array of keys that should be replaced with values in the
|
||||
* translated string.
|
||||
* @param boolean $fallbackdefault Default translation to use as a fallback if no valid translation was found.
|
||||
* @param array $oldreplacements
|
||||
* @param bool $striptags
|
||||
* @deprecated Not used in twig, gettext
|
||||
*
|
||||
* @return string|null The translated tag, or a placeholder value if the tag wasn't found.
|
||||
*/
|
||||
public function t(
|
||||
$tag,
|
||||
$replacements = [],
|
||||
// TODO: remove this for 2.0. Assume true
|
||||
$fallbackdefault = true,
|
||||
// TODO: remove this for 2.0
|
||||
$oldreplacements = [],
|
||||
// TODO: remove this for 2.0
|
||||
$striptags = false
|
||||
) {
|
||||
$backtrace = debug_backtrace();
|
||||
$where = $backtrace[0]['file'] . ':' . $backtrace[0]['line'];
|
||||
if (!$fallbackdefault) {
|
||||
Logger::warning(
|
||||
'Deprecated use of new SimpleSAML\Locale\Translate::t(...) at ' . $where .
|
||||
'. This parameter will go away, the fallback will become' .
|
||||
' identical to the $tag in 2.0.'
|
||||
);
|
||||
}
|
||||
if (!is_array($replacements)) {
|
||||
// TODO: remove this entire if for 2.0
|
||||
|
||||
// old style call to t(...). Print warning to log
|
||||
Logger::warning(
|
||||
'Deprecated use of SimpleSAML\Locale\Translate::t(...) at ' . $where .
|
||||
'. Please update the code to use the new style of parameters.'
|
||||
);
|
||||
|
||||
// for backwards compatibility
|
||||
/** @psalm-suppress PossiblyInvalidArgument */
|
||||
if (!$replacements && ($this->getTag($tag) === null)) {
|
||||
Logger::warning(
|
||||
'Code which uses $fallbackdefault === FALSE should be updated to use the getTag() method instead.'
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$replacements = $oldreplacements;
|
||||
}
|
||||
|
||||
if (is_array($tag)) {
|
||||
$tagData = $tag;
|
||||
Logger::warning(
|
||||
'Deprecated use of new SimpleSAML\Locale\Translate::t(...) at ' . $where .
|
||||
'. The $tag-parameter can only be a string in 2.0.'
|
||||
);
|
||||
} else {
|
||||
$tagData = $this->getTag($tag);
|
||||
if ($tagData === null) {
|
||||
// tag not found
|
||||
Logger::info('Translate: Looking up [' . $tag . ']: not translated at all.');
|
||||
return $this->getStringNotTranslated($tag, $fallbackdefault);
|
||||
}
|
||||
}
|
||||
|
||||
$translated = $this->getPreferredTranslation($tagData);
|
||||
|
||||
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 string $tag A name tag of the string that should be returned.
|
||||
* @param boolean $fallbacktag If set to true and string was not found in any languages, return the tag itself. If
|
||||
* false return null.
|
||||
*
|
||||
* @return string The string that should be used, or the tag name if $fallbacktag is set to false.
|
||||
*/
|
||||
private function getStringNotTranslated($tag, $fallbacktag)
|
||||
{
|
||||
if ($fallbacktag) {
|
||||
return 'not translated (' . $tag . ')';
|
||||
} else {
|
||||
return $tag;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Include a translation inline instead of putting translations in dictionaries. This function is recommended to be
|
||||
* used ONLY for variable data, or when the translation is already provided by an external source, as a database
|
||||
* or in metadata.
|
||||
*
|
||||
* @param string $tag The tag that has a translation
|
||||
* @param array|string $translation The translation array
|
||||
*
|
||||
* @throws Exception If $translation is neither a string nor an array.
|
||||
* @return void
|
||||
*/
|
||||
public function includeInlineTranslation($tag, $translation)
|
||||
{
|
||||
if (is_string($translation)) {
|
||||
$translation = ['en' => $translation];
|
||||
} elseif (!is_array($translation)) {
|
||||
throw new Exception(
|
||||
"Inline translation should be string or array. Is " . gettype($translation) . " now!"
|
||||
);
|
||||
}
|
||||
|
||||
Logger::debug('Translate: Adding inline language translation for tag [' . $tag . ']');
|
||||
$this->langtext[$tag] = $translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include a language file from the dictionaries directory.
|
||||
*
|
||||
* @param string $file File name of dictionary to include
|
||||
* @param Configuration|null $otherConfig Optionally provide a different configuration object than the
|
||||
* one provided in the constructor to be used to find the directory of the dictionary. This allows to combine
|
||||
* dictionaries inside the SimpleSAMLphp main code distribution together with external dictionaries. Defaults to
|
||||
* null.
|
||||
* @return void
|
||||
*/
|
||||
public function includeLanguageFile($file, $otherConfig = null)
|
||||
{
|
||||
if (!empty($otherConfig)) {
|
||||
$filebase = $otherConfig->getPathValue('dictionarydir', 'dictionaries/');
|
||||
} else {
|
||||
$filebase = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
|
||||
}
|
||||
$filebase = $filebase ?: 'dictionaries/';
|
||||
|
||||
$lang = $this->readDictionaryFile($filebase . $file);
|
||||
Logger::debug('Translate: 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 An array holding all the translations in 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)) {
|
||||
Logger::error('Invalid dictionary definition file [' . $definitionFile . ']');
|
||||
return [];
|
||||
}
|
||||
|
||||
$translationFile = $filename . '.translation.json';
|
||||
if (file_exists($translationFile)) {
|
||||
$fileContent = file_get_contents($translationFile);
|
||||
$moreTrans = json_decode($fileContent, true);
|
||||
if (!empty($moreTrans)) {
|
||||
$lang = array_merge_recursive($lang, $moreTrans);
|
||||
}
|
||||
}
|
||||
|
||||
return $lang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a dictionary file in PHP format.
|
||||
*
|
||||
* @param string $filename The absolute path to the dictionary file.
|
||||
*
|
||||
* @return array An array holding all the translations in the file.
|
||||
*/
|
||||
private function readDictionaryPHP($filename)
|
||||
{
|
||||
$phpFile = $filename . '.php';
|
||||
assert(file_exists($phpFile));
|
||||
|
||||
$lang = null;
|
||||
include($phpFile);
|
||||
if (isset($lang)) {
|
||||
return $lang;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a dictionary file.
|
||||
*
|
||||
* @param string $filename The absolute path to the dictionary file.
|
||||
*
|
||||
* @return array An array holding all the translations in the file.
|
||||
*/
|
||||
private function readDictionaryFile($filename)
|
||||
{
|
||||
assert(is_string($filename));
|
||||
|
||||
Logger::debug('Translate: Reading dictionary [' . $filename . ']');
|
||||
|
||||
$jsonFile = $filename . '.definition.json';
|
||||
if (file_exists($jsonFile)) {
|
||||
return $this->readDictionaryJSON($filename);
|
||||
}
|
||||
|
||||
$phpFile = $filename . '.php';
|
||||
if (file_exists($phpFile)) {
|
||||
return $this->readDictionaryPHP($filename);
|
||||
}
|
||||
|
||||
Logger::error(
|
||||
$_SERVER['PHP_SELF'] . ' - Translate: Could not find dictionary file at [' . $filename . ']'
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a singular text.
|
||||
*
|
||||
* @param string $original The string before translation.
|
||||
*
|
||||
* @return string The translated string.
|
||||
*/
|
||||
public static function translateSingularGettext($original)
|
||||
{
|
||||
$text = BaseTranslator::$current->gettext($original);
|
||||
|
||||
if (func_num_args() === 1) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$args = array_slice(func_get_args(), 1);
|
||||
|
||||
return strtr($text, is_array($args[0]) ? $args[0] : $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a plural text.
|
||||
*
|
||||
* @param string $original The string before translation.
|
||||
* @param string $plural
|
||||
* @param string $value
|
||||
*
|
||||
* @return string The translated string.
|
||||
*/
|
||||
public static function translatePluralGettext($original, $plural, $value)
|
||||
{
|
||||
$text = BaseTranslator::$current->ngettext($original, $plural, $value);
|
||||
|
||||
if (func_num_args() === 3) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$args = array_slice(func_get_args(), 3);
|
||||
|
||||
return strtr($text, is_array($args[0]) ? $args[0] : $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a translation from a given array of translations for the current language.
|
||||
*
|
||||
* @param array|null $context An array of options. The current language must be specified
|
||||
* as an ISO 639 code accessible with the key "currentLanguage" in the array.
|
||||
* @param array|null $translations An array of translations. Each translation has an
|
||||
* ISO 639 code as its key, identifying the language it corresponds to.
|
||||
*
|
||||
* @return null|string The translation appropriate for the current language, or null if none found. If the
|
||||
* $context or $translations arrays are null, or $context['currentLanguage'] is not defined, null is also returned.
|
||||
*/
|
||||
public static function translateFromArray($context, $translations)
|
||||
{
|
||||
if (!is_array($translations)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_array($context) || !isset($context['currentLanguage'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($translations[$context['currentLanguage']])) {
|
||||
return $translations[$context['currentLanguage']];
|
||||
}
|
||||
|
||||
// we don't have a translation for the current language, load alternative priorities
|
||||
$sspcfg = Configuration::getInstance();
|
||||
$langcfg = $sspcfg->getConfigItem('language');
|
||||
$priorities = $langcfg->getArray('priorities', []);
|
||||
|
||||
if (!empty($priorities[$context['currentLanguage']])) {
|
||||
foreach ($priorities[$context['currentLanguage']] as $lang) {
|
||||
if (isset($translations[$lang])) {
|
||||
return $translations[$lang];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nothing we can use, return null so that we can set a default
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,528 @@
|
|||
<?php
|
||||
|
||||
namespace SAML;
|
||||
|
||||
use Exception;
|
||||
use SAML\Logger\ErrorLogLoggingHandler;
|
||||
use SAML\Logger\LoggingHandlerInterface;
|
||||
use SAML\Logger\StandardErrorLoggingHandler;
|
||||
|
||||
/**
|
||||
* The main logger class for SimpleSAMLphp.
|
||||
*
|
||||
* @author Lasse Birnbaum Jensen, SDU.
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class Logger
|
||||
{
|
||||
/**
|
||||
* @var LoggingHandlerInterface
|
||||
*/
|
||||
private static $loggingHandler;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $initializing = false;
|
||||
|
||||
/**
|
||||
* @var integer|null
|
||||
*/
|
||||
private static $logLevel = null;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
private static $captureLog = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private static $capturedLog = [];
|
||||
|
||||
/**
|
||||
* Array with messages logged before the logging handler was initialized.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $earlyLog = [];
|
||||
|
||||
/**
|
||||
* List of log levels.
|
||||
*
|
||||
* This list is used to restore the log levels after some log levels have been disabled.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $logLevelStack = [];
|
||||
|
||||
/**
|
||||
* The current mask of log levels disabled.
|
||||
*
|
||||
* Note: this mask is not directly related to the PHP error reporting level.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $logMask = 0;
|
||||
|
||||
|
||||
/**
|
||||
* This constant defines the string we set the track ID to while we are fetching the track ID from the session
|
||||
* class. This is used to prevent infinite recursion.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const NO_TRACKID = '_NOTRACKIDYET_';
|
||||
|
||||
/**
|
||||
* This variable holds the track ID we have retrieved from the session class. It can also be NULL, in which case
|
||||
* we haven't fetched the track ID yet, or self::NO_TRACKID, which means that we are fetching the track ID now.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $trackid = self::NO_TRACKID;
|
||||
|
||||
/**
|
||||
* This variable holds the format used to log any message. Its use varies depending on the log handler used (for
|
||||
* instance, you cannot control here how dates are displayed when using syslog or errorlog handlers), but in
|
||||
* general the options are:
|
||||
*
|
||||
* - %date{<format>}: the date and time, with its format specified inside the brackets. See the PHP documentation
|
||||
* of the strftime() function for more information on the format. If the brackets are omitted, the standard
|
||||
* format is applied. This can be useful if you just want to control the placement of the date, but don't care
|
||||
* about the format.
|
||||
*
|
||||
* - %process: the name of the SimpleSAMLphp process. Remember you can configure this in the 'logging.processname'
|
||||
* option. The SyslogLoggingHandler will just remove this.
|
||||
*
|
||||
* - %level: the log level (name or number depending on the handler used). Please note different logging handlers
|
||||
* will print the log level differently.
|
||||
*
|
||||
* - %stat: if the log entry is intended for statistical purposes, it will print the string 'STAT ' (bear in mind
|
||||
* the trailing space).
|
||||
*
|
||||
* - %trackid: the track ID, an identifier that allows you to track a single session.
|
||||
*
|
||||
* - %srcip: the IP address of the client. If you are behind a proxy, make sure to modify the
|
||||
* $_SERVER['REMOTE_ADDR'] variable on your code accordingly to the X-Forwarded-For header.
|
||||
*
|
||||
* - %msg: the message to be logged.
|
||||
*
|
||||
* @var string The format of the log line.
|
||||
*/
|
||||
private static $format = '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg';
|
||||
|
||||
/**
|
||||
* This variable tells if we have a shutdown function registered or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $shutdownRegistered = false;
|
||||
|
||||
/**
|
||||
* This variable tells if we are shutting down.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $shuttingDown = false;
|
||||
|
||||
/** @var int */
|
||||
const EMERG = 0;
|
||||
|
||||
/** @var int */
|
||||
const ALERT = 1;
|
||||
|
||||
/** @var int */
|
||||
const CRIT = 2;
|
||||
|
||||
/** @var int */
|
||||
const ERR = 3;
|
||||
|
||||
/** @var int */
|
||||
const WARNING = 4;
|
||||
|
||||
/** @var int */
|
||||
const NOTICE = 5;
|
||||
|
||||
/** @var int */
|
||||
const INFO = 6;
|
||||
|
||||
/** @var int */
|
||||
const DEBUG = 7;
|
||||
|
||||
|
||||
/**
|
||||
* Log an emergency message.
|
||||
*
|
||||
* @param string $string The message to log.
|
||||
* @return void
|
||||
*/
|
||||
public static function emergency($string)
|
||||
{
|
||||
self::log(self::EMERG, $string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log a critical message.
|
||||
*
|
||||
* @param string $string The message to log.
|
||||
* @return void
|
||||
*/
|
||||
public static function critical($string)
|
||||
{
|
||||
self::log(self::CRIT, $string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log an alert.
|
||||
*
|
||||
* @param string $string The message to log.
|
||||
* @return void
|
||||
*/
|
||||
public static function alert($string)
|
||||
{
|
||||
self::log(self::ALERT, $string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log an error.
|
||||
*
|
||||
* @param string $string The message to log.
|
||||
* @return void
|
||||
*/
|
||||
public static function error($string)
|
||||
{
|
||||
self::log(self::ERR, $string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log a warning.
|
||||
*
|
||||
* @param string $string The message to log.
|
||||
* @return void
|
||||
*/
|
||||
public static function warning($string)
|
||||
{
|
||||
self::log(self::WARNING, $string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* We reserve the notice level for statistics, so do not use this level for other kind of log messages.
|
||||
*
|
||||
* @param string $string The message to log.
|
||||
* @return void
|
||||
*/
|
||||
public static function notice($string)
|
||||
{
|
||||
self::log(self::NOTICE, $string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Info messages are a bit less verbose than debug messages. This is useful to trace a session.
|
||||
*
|
||||
* @param string $string The message to log.
|
||||
* @return void
|
||||
*/
|
||||
public static function info($string)
|
||||
{
|
||||
self::log(self::INFO, $string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Debug messages are very verbose, and will contain more information than what is necessary for a production
|
||||
* system.
|
||||
*
|
||||
* @param string $string The message to log.
|
||||
* @return void
|
||||
*/
|
||||
public static function debug($string)
|
||||
{
|
||||
self::log(self::DEBUG, $string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Statistics.
|
||||
*
|
||||
* @param string $string The message to log.
|
||||
* @return void
|
||||
*/
|
||||
public static function stats($string)
|
||||
{
|
||||
self::log(self::NOTICE, $string, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the logger to capture logs.
|
||||
*
|
||||
* @param boolean $val Whether to capture logs or not. Defaults to TRUE.
|
||||
* @return void
|
||||
*/
|
||||
public static function setCaptureLog($val = true)
|
||||
{
|
||||
self::$captureLog = $val;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the captured log.
|
||||
* @return array
|
||||
*/
|
||||
public static function getCapturedLog()
|
||||
{
|
||||
return self::$capturedLog;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the track identifier to use in all logs.
|
||||
*
|
||||
* @param string $trackId The track identifier to use during this session.
|
||||
* @return void
|
||||
*/
|
||||
public static function setTrackId($trackId)
|
||||
{
|
||||
self::$trackid = $trackId;
|
||||
self::flush();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Flush any pending log messages to the logging handler.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function flush()
|
||||
{
|
||||
foreach (self::$earlyLog as $msg) {
|
||||
self::log($msg['level'], $msg['string'], $msg['statsLog']);
|
||||
}
|
||||
self::$earlyLog = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Flush any pending deferred logs during shutdown.
|
||||
*
|
||||
* This method is intended to be registered as a shutdown handler, so that any pending messages that weren't sent
|
||||
* to the logging handler at that point, can still make it. It is therefore not intended to be called manually.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function shutdown()
|
||||
{
|
||||
if (self::$trackid === self::NO_TRACKID) {
|
||||
try {
|
||||
$s = Session::getSessionFromRequest();
|
||||
} catch (Exception $e) {
|
||||
// loading session failed. We don't care why, at this point we have a transient session, so we use that
|
||||
$s = Session::getSessionFromRequest();
|
||||
}
|
||||
self::$trackid = $s->getTrackID();
|
||||
}
|
||||
self::$shuttingDown = true;
|
||||
self::flush();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Evaluate whether errors of a certain error level are masked or not.
|
||||
*
|
||||
* @param int $errno The level of the error to check.
|
||||
*
|
||||
* @return bool True if the error is masked, false otherwise.
|
||||
*/
|
||||
public static function isErrorMasked($errno)
|
||||
{
|
||||
return ($errno & self::$logMask) || !($errno & error_reporting());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disable error reporting for the given log levels.
|
||||
*
|
||||
* Every call to this function must be followed by a call to popErrorMask().
|
||||
*
|
||||
* @param int $mask The log levels that should be masked.
|
||||
* @return void
|
||||
*/
|
||||
public static function maskErrors($mask)
|
||||
{
|
||||
assert(is_int($mask));
|
||||
|
||||
$currentEnabled = error_reporting();
|
||||
self::$logLevelStack[] = [$currentEnabled, self::$logMask];
|
||||
|
||||
$currentEnabled &= ~$mask;
|
||||
error_reporting($currentEnabled);
|
||||
self::$logMask |= $mask;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pop an error mask.
|
||||
*
|
||||
* This function restores the previous error mask.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function popErrorMask()
|
||||
{
|
||||
$lastMask = array_pop(self::$logLevelStack);
|
||||
error_reporting($lastMask[0]);
|
||||
self::$logMask = $lastMask[1];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defer a message for later logging.
|
||||
*
|
||||
* @param int $level The log level corresponding to this message.
|
||||
* @param string $message The message itself to log.
|
||||
* @param boolean $stats Whether this is a stats message or a regular one.
|
||||
* @return void
|
||||
*/
|
||||
private static function defer($level, $message, $stats)
|
||||
{
|
||||
// save the message for later
|
||||
self::$earlyLog[] = ['level' => $level, 'string' => $message, 'statsLog' => $stats];
|
||||
|
||||
// register a shutdown handler if needed
|
||||
if (!self::$shutdownRegistered) {
|
||||
register_shutdown_function([self::class, 'shutdown']);
|
||||
self::$shutdownRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|null $handler
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function createLoggingHandler($handler = null)
|
||||
{
|
||||
self::$initializing = true;
|
||||
|
||||
// a set of known logging handlers
|
||||
$known_handlers = [
|
||||
'syslog' => 'SimpleSAML\Logger\SyslogLoggingHandler',
|
||||
'file' => 'SimpleSAML\Logger\FileLoggingHandler',
|
||||
'errorlog' => 'SimpleSAML\Logger\ErrorLogLoggingHandler',
|
||||
'stderr' => 'SimpleSAML\Logger\StandardErrorLoggingHandler',
|
||||
];
|
||||
|
||||
// get the configuration
|
||||
$config = Configuration::getInstance();
|
||||
assert($config instanceof Configuration);
|
||||
|
||||
// setting minimum log_level
|
||||
self::$logLevel = $config->getInteger('logging.level', self::INFO);
|
||||
|
||||
// get the metadata handler option from the configuration
|
||||
if (is_null($handler)) {
|
||||
$handler = $config->getString('logging.handler', 'syslog');
|
||||
}
|
||||
|
||||
if (!array_key_exists($handler, $known_handlers) && class_exists($handler)) {
|
||||
if (!in_array('SimpleSAML\Logger\LoggingHandlerInterface', class_implements($handler), true)) {
|
||||
throw new Exception("The logging handler '$handler' is invalid.");
|
||||
}
|
||||
} else {
|
||||
$handler = strtolower($handler);
|
||||
if (!array_key_exists($handler, $known_handlers)) {
|
||||
throw new Exception(
|
||||
"Invalid value for the 'logging.handler' configuration option. Unknown handler '" . $handler . "'."
|
||||
);
|
||||
}
|
||||
$handler = $known_handlers[$handler];
|
||||
}
|
||||
|
||||
self::$format = $config->getString('logging.format', self::$format);
|
||||
|
||||
try {
|
||||
/** @var LoggingHandlerInterface */
|
||||
self::$loggingHandler = new $handler($config);
|
||||
self::$loggingHandler->setLogFormat(self::$format);
|
||||
self::$initializing = false;
|
||||
} catch (Exception $e) {
|
||||
self::$loggingHandler = new ErrorLogLoggingHandler($config);
|
||||
self::$initializing = false;
|
||||
self::log(self::CRIT, $e->getMessage(), false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param int $level
|
||||
* @param string $string
|
||||
* @param bool $statsLog
|
||||
* @return void
|
||||
*/
|
||||
private static function log($level, $string, $statsLog = false)
|
||||
{
|
||||
if (self::$initializing) {
|
||||
// some error occurred while initializing logging
|
||||
self::defer($level, $string, $statsLog);
|
||||
return;
|
||||
} elseif (php_sapi_name() === 'cli' || defined('STDIN')) {
|
||||
// we are being executed from the CLI, nowhere to log
|
||||
if (!isset(self::$loggingHandler)) {
|
||||
self::createLoggingHandler(StandardErrorLoggingHandler::class);
|
||||
}
|
||||
$_SERVER['REMOTE_ADDR'] = "CLI";
|
||||
if (self::$trackid === self::NO_TRACKID) {
|
||||
self::$trackid = 'CL' . bin2hex(openssl_random_pseudo_bytes(4));
|
||||
}
|
||||
} elseif (!isset(self::$loggingHandler)) {
|
||||
// Initialize logging
|
||||
self::createLoggingHandler();
|
||||
}
|
||||
|
||||
if (self::$captureLog) {
|
||||
$ts = microtime(true);
|
||||
$msecs = (int) (($ts - (int) $ts) * 1000);
|
||||
$ts = gmdate('H:i:s', $ts) . sprintf('.%03d', $msecs) . 'Z';
|
||||
self::$capturedLog[] = $ts . ' ' . $string;
|
||||
}
|
||||
|
||||
if (self::$logLevel >= $level || $statsLog) {
|
||||
if (is_array($string)) {
|
||||
$string = implode(",", $string);
|
||||
}
|
||||
|
||||
$formats = ['%trackid', '%msg', '%srcip', '%stat'];
|
||||
$replacements = [self::$trackid, $string, $_SERVER['REMOTE_ADDR']];
|
||||
|
||||
$stat = '';
|
||||
if ($statsLog) {
|
||||
$stat = 'STAT ';
|
||||
}
|
||||
array_push($replacements, $stat);
|
||||
|
||||
if (self::$trackid === self::NO_TRACKID && !self::$shuttingDown) {
|
||||
// we have a log without track ID and we are not still shutting down, so defer logging
|
||||
self::defer($level, $string, $statsLog);
|
||||
return;
|
||||
} elseif (self::$trackid === self::NO_TRACKID) {
|
||||
// shutting down without a track ID, prettify it
|
||||
array_shift($replacements);
|
||||
array_unshift($replacements, 'N/A');
|
||||
}
|
||||
|
||||
// we either have a track ID or we are shutting down, so just log the message
|
||||
$string = str_replace($formats, $replacements, self::$format);
|
||||
self::$loggingHandler->log($level, $string);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Logger;
|
||||
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
|
||||
/**
|
||||
* A class for logging to the default php error log.
|
||||
*
|
||||
* @author Lasse Birnbaum Jensen, SDU.
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class ErrorLogLoggingHandler implements LoggingHandlerInterface
|
||||
{
|
||||
/**
|
||||
* This array contains the mappings from syslog log level to names.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $levelNames = [
|
||||
Logger::EMERG => 'EMERG',
|
||||
Logger::ALERT => 'ALERT',
|
||||
Logger::CRIT => 'CRIT',
|
||||
Logger::ERR => 'ERR',
|
||||
Logger::WARNING => 'WARNING',
|
||||
Logger::NOTICE => 'NOTICE',
|
||||
Logger::INFO => 'INFO',
|
||||
Logger::DEBUG => 'DEBUG',
|
||||
];
|
||||
|
||||
/**
|
||||
* The name of this process.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $processname;
|
||||
|
||||
|
||||
/**
|
||||
* ErrorLogLoggingHandler constructor.
|
||||
*
|
||||
* @param Configuration $config The configuration object for this handler.
|
||||
*/
|
||||
public function __construct(Configuration $config)
|
||||
{
|
||||
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the format desired for the logs.
|
||||
*
|
||||
* @param string $format The format used for logs.
|
||||
* @return void
|
||||
*/
|
||||
public function setLogFormat($format)
|
||||
{
|
||||
// we don't need the format here
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log a message to syslog.
|
||||
*
|
||||
* @param int $level The log level.
|
||||
* @param string $string The formatted message to log.
|
||||
* @return void
|
||||
*/
|
||||
public function log($level, $string)
|
||||
{
|
||||
if (array_key_exists($level, self::$levelNames)) {
|
||||
$levelName = self::$levelNames[$level];
|
||||
} else {
|
||||
$levelName = sprintf('UNKNOWN%d', $level);
|
||||
}
|
||||
|
||||
$formats = ['%process', '%level'];
|
||||
$replacements = [$this->processname, $levelName];
|
||||
$string = str_replace($formats, $replacements, $string);
|
||||
$string = preg_replace('/%\w+(\{[^\}]+\})?/', '', $string);
|
||||
$string = trim($string);
|
||||
|
||||
error_log($string);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Logger;
|
||||
|
||||
use Exception;
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
use SAML\Utils;
|
||||
use const PHP_EOL;
|
||||
|
||||
/**
|
||||
* A logging handler that dumps logs to files.
|
||||
*
|
||||
* @author Lasse Birnbaum Jensen, SDU.
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class FileLoggingHandler implements LoggingHandlerInterface
|
||||
{
|
||||
/**
|
||||
* A string with the path to the file where we should log our messages.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
protected $logFile = null;
|
||||
|
||||
/**
|
||||
* This array contains the mappings from syslog log levels to names. Copied more or less directly from
|
||||
* SimpleSAML\Logger\ErrorLogLoggingHandler.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $levelNames = [
|
||||
Logger::EMERG => 'EMERGENCY',
|
||||
Logger::ALERT => 'ALERT',
|
||||
Logger::CRIT => 'CRITICAL',
|
||||
Logger::ERR => 'ERROR',
|
||||
Logger::WARNING => 'WARNING',
|
||||
Logger::NOTICE => 'NOTICE',
|
||||
Logger::INFO => 'INFO',
|
||||
Logger::DEBUG => 'DEBUG',
|
||||
];
|
||||
|
||||
/** @var string|null */
|
||||
protected $processname = null;
|
||||
|
||||
/** @var string */
|
||||
protected $format = "%b %d %H:%M:%S";
|
||||
|
||||
|
||||
/**
|
||||
* Build a new logging handler based on files.
|
||||
* @param Configuration $config
|
||||
*/
|
||||
public function __construct(Configuration $config)
|
||||
{
|
||||
// get the metadata handler option from the configuration
|
||||
$this->logFile = $config->getPathValue('loggingdir', 'log/') .
|
||||
$config->getString('logging.logfile', 'simplesamlphp.log');
|
||||
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
|
||||
|
||||
if (@file_exists($this->logFile)) {
|
||||
if (!@is_writeable($this->logFile)) {
|
||||
throw new Exception("Could not write to logfile: " . $this->logFile);
|
||||
}
|
||||
} else {
|
||||
if (!@touch($this->logFile)) {
|
||||
throw new Exception(
|
||||
"Could not create logfile: " . $this->logFile .
|
||||
" The logging directory is not writable for the web server user."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Utils\Time::initTimezone();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the format desired for the logs.
|
||||
*
|
||||
* @param string $format The format used for logs.
|
||||
* @return void
|
||||
*/
|
||||
public function setLogFormat($format)
|
||||
{
|
||||
$this->format = $format;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log a message to the log file.
|
||||
*
|
||||
* @param int $level The log level.
|
||||
* @param string $string The formatted message to log.
|
||||
* @return void
|
||||
*/
|
||||
public function log($level, $string)
|
||||
{
|
||||
if (!is_null($this->logFile)) {
|
||||
// set human-readable log level. Copied from SimpleSAML\Logger\ErrorLogLoggingHandler.
|
||||
$levelName = sprintf('UNKNOWN%d', $level);
|
||||
if (array_key_exists($level, self::$levelNames)) {
|
||||
$levelName = self::$levelNames[$level];
|
||||
}
|
||||
|
||||
$formats = ['%process', '%level'];
|
||||
$replacements = [$this->processname, $levelName];
|
||||
|
||||
$matches = [];
|
||||
if (preg_match('/%date(?:\{([^\}]+)\})?/', $this->format, $matches)) {
|
||||
$format = "%b %d %H:%M:%S";
|
||||
if (isset($matches[1])) {
|
||||
$format = $matches[1];
|
||||
}
|
||||
|
||||
array_push($formats, $matches[0]);
|
||||
array_push($replacements, strftime($format));
|
||||
}
|
||||
|
||||
$string = str_replace($formats, $replacements, $string);
|
||||
file_put_contents($this->logFile, $string . PHP_EOL, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Logger;
|
||||
|
||||
use SAML\Configuration;
|
||||
|
||||
/**
|
||||
* The interface that must be implemented by any log handler.
|
||||
*
|
||||
* @author Jaime Perez Crespo, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
interface LoggingHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Constructor for log handlers. It must accept receiving a \SimpleSAML\Configuration object.
|
||||
*
|
||||
* @param Configuration $config The configuration to use in this log handler.
|
||||
*/
|
||||
public function __construct(Configuration $config);
|
||||
|
||||
|
||||
/**
|
||||
* Log a message to its destination.
|
||||
*
|
||||
* @param int $level The log level.
|
||||
* @param string $string The message to log.
|
||||
* @return void
|
||||
*/
|
||||
public function log($level, $string);
|
||||
|
||||
|
||||
/**
|
||||
* Set the format desired for the logs.
|
||||
*
|
||||
* @param string $format The format used for logs.
|
||||
* @return void
|
||||
*/
|
||||
public function setLogFormat($format);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Logger;
|
||||
|
||||
use SAML\Configuration;
|
||||
|
||||
/**
|
||||
* A logging handler that outputs all messages to standard error.
|
||||
*
|
||||
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class StandardErrorLoggingHandler extends FileLoggingHandler
|
||||
{
|
||||
/**
|
||||
* StandardError constructor.
|
||||
*
|
||||
* It runs the parent constructor and sets the log file to be the standard error descriptor.
|
||||
*
|
||||
* @param Configuration $config
|
||||
*/
|
||||
public function __construct(Configuration $config)
|
||||
{
|
||||
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
|
||||
$this->logFile = 'php://stderr';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Logger;
|
||||
|
||||
use SAML\Configuration;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* A logger that sends messages to syslog.
|
||||
*
|
||||
* @author Lasse Birnbaum Jensen, SDU.
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class SyslogLoggingHandler implements LoggingHandlerInterface
|
||||
{
|
||||
/** @var bool */
|
||||
private $isWindows = false;
|
||||
|
||||
/** @var string */
|
||||
protected $format = "%b %d %H:%M:%S";
|
||||
|
||||
|
||||
/**
|
||||
* Build a new logging handler based on syslog.
|
||||
* @param Configuration $config
|
||||
*/
|
||||
public function __construct(Configuration $config)
|
||||
{
|
||||
$facility = $config->getInteger('logging.facility', defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER);
|
||||
|
||||
$processname = $config->getString('logging.processname', 'SimpleSAMLphp');
|
||||
|
||||
// Setting facility to LOG_USER (only valid in Windows), enable log level rewrite on windows systems
|
||||
if (Utils\System::getOS() === Utils\System::WINDOWS) {
|
||||
$this->isWindows = true;
|
||||
$facility = LOG_USER;
|
||||
}
|
||||
|
||||
openlog($processname, LOG_PID, $facility);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the format desired for the logs.
|
||||
*
|
||||
* @param string $format The format used for logs.
|
||||
* @return void
|
||||
*/
|
||||
public function setLogFormat($format)
|
||||
{
|
||||
$this->format = $format;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log a message to syslog.
|
||||
*
|
||||
* @param int $level The log level.
|
||||
* @param string $string The formatted message to log.
|
||||
* @return void
|
||||
*/
|
||||
public function log($level, $string)
|
||||
{
|
||||
// changing log level to supported levels if OS is Windows
|
||||
if ($this->isWindows) {
|
||||
if ($level <= 4) {
|
||||
$level = LOG_ERR;
|
||||
} else {
|
||||
$level = LOG_INFO;
|
||||
}
|
||||
}
|
||||
|
||||
$formats = ['%process', '%level'];
|
||||
$replacements = ['', $level];
|
||||
$string = str_replace($formats, $replacements, $string);
|
||||
$string = preg_replace('/%\w+(\{[^\}]+\})?/', '', $string);
|
||||
$string = trim($string);
|
||||
|
||||
syslog($level, $string);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,514 @@
|
|||
<?php
|
||||
|
||||
namespace SAML;
|
||||
|
||||
use Exception;
|
||||
use Memcached;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* This file implements functions to read and write to a group of memcache
|
||||
* servers.
|
||||
*
|
||||
* The goals of this storage class is to provide failover, redudancy and load
|
||||
* balancing. This is accomplished by storing the data object to several
|
||||
* groups of memcache servers. Each data object is replicated to every group
|
||||
* of memcache servers, but it is only stored to one server in each group.
|
||||
*
|
||||
* For this code to work correctly, all web servers accessing the data must
|
||||
* have the same clock (as measured by the time()-function). Different clock
|
||||
* values will lead to incorrect behaviour.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class Memcache
|
||||
{
|
||||
/**
|
||||
* Cache of the memcache servers we are using.
|
||||
*
|
||||
* @var \Memcache[]|Memcached[]|null
|
||||
*/
|
||||
private static $serverGroups = null;
|
||||
|
||||
|
||||
/**
|
||||
* The flavor of memcache PHP extension we are using.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $extension = '';
|
||||
|
||||
|
||||
/**
|
||||
* Find data stored with a given key.
|
||||
*
|
||||
* @param string $key The key of the data.
|
||||
*
|
||||
* @return mixed The data stored with the given key, or null if no data matching the key was found.
|
||||
*/
|
||||
public static function get($key)
|
||||
{
|
||||
Logger::debug("loading key $key from memcache");
|
||||
|
||||
$latestInfo = null;
|
||||
$latestTime = 0.0;
|
||||
$latestData = null;
|
||||
$mustUpdate = false;
|
||||
$allDown = true;
|
||||
|
||||
// search all the servers for the given id
|
||||
foreach (self::getMemcacheServers() as $server) {
|
||||
$serializedInfo = $server->get($key);
|
||||
if ($serializedInfo === false) {
|
||||
// either the server is down, or we don't have the value stored on that server
|
||||
$mustUpdate = true;
|
||||
$up = $server->getStats();
|
||||
if ($up !== false) {
|
||||
$allDown = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$allDown = false;
|
||||
|
||||
// unserialize the object
|
||||
/** @var string $serializedInfo */
|
||||
$info = unserialize($serializedInfo);
|
||||
|
||||
/*
|
||||
* Make sure that this is an array with two keys:
|
||||
* - 'timestamp': The time the data was saved.
|
||||
* - 'data': The data.
|
||||
*/
|
||||
if (!is_array($info)) {
|
||||
Logger::warning(
|
||||
'Retrieved invalid data from a memcache server. Data was not an array.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists('timestamp', $info)) {
|
||||
Logger::warning(
|
||||
'Retrieved invalid data from a memcache server. Missing timestamp.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists('data', $info)) {
|
||||
Logger::warning(
|
||||
'Retrieved invalid data from a memcache server. Missing data.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($latestInfo === null) {
|
||||
// first info found
|
||||
$latestInfo = $serializedInfo;
|
||||
$latestTime = $info['timestamp'];
|
||||
$latestData = $info['data'];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($info['timestamp'] === $latestTime && $serializedInfo === $latestInfo) {
|
||||
// this data matches the data from the other server(s)
|
||||
continue;
|
||||
}
|
||||
|
||||
// different data from different servers. We need to update at least one of them to maintain sync
|
||||
$mustUpdate = true;
|
||||
|
||||
// update if data in $info is newer than $latestData
|
||||
if ($latestTime < $info['timestamp']) {
|
||||
$latestInfo = $serializedInfo;
|
||||
$latestTime = $info['timestamp'];
|
||||
$latestData = $info['data'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($latestData === null) {
|
||||
if ($allDown) {
|
||||
// all servers are down, panic!
|
||||
$e = new Error\Error('MEMCACHEDOWN', null, 503);
|
||||
throw new Error\Exception('All memcache servers are down', 503, $e);
|
||||
}
|
||||
// we didn't find any data matching the key
|
||||
Logger::debug("key $key not found in memcache");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($mustUpdate) {
|
||||
// we found data matching the key, but some of the servers need updating
|
||||
Logger::debug("Memcache servers out of sync for $key, forcing sync");
|
||||
self::set($key, $latestData);
|
||||
}
|
||||
|
||||
return $latestData;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save a key-value pair to the memcache servers.
|
||||
*
|
||||
* @param string $key The key of the data.
|
||||
* @param mixed $value The value of the data.
|
||||
* @param integer|null $expire The expiration timestamp of the data.
|
||||
* @return void
|
||||
*/
|
||||
public static function set($key, $value, $expire = null)
|
||||
{
|
||||
Logger::debug("saving key $key to memcache");
|
||||
$savedInfo = [
|
||||
'timestamp' => microtime(true),
|
||||
'data' => $value
|
||||
];
|
||||
|
||||
if ($expire === null) {
|
||||
$expire = self::getExpireTime();
|
||||
}
|
||||
|
||||
$savedInfoSerialized = serialize($savedInfo);
|
||||
|
||||
// store this object to all groups of memcache servers
|
||||
foreach (self::getMemcacheServers() as $server) {
|
||||
if (self::$extension === Memcached::class) {
|
||||
$server->set($key, $savedInfoSerialized, $expire);
|
||||
} else {
|
||||
$server->set($key, $savedInfoSerialized, 0, $expire);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a key-value pair from the memcache servers.
|
||||
*
|
||||
* @param string $key The key we should delete.
|
||||
* @return void
|
||||
*/
|
||||
public static function delete($key)
|
||||
{
|
||||
assert(is_string($key));
|
||||
Logger::debug("deleting key $key from memcache");
|
||||
|
||||
// store this object to all groups of memcache servers
|
||||
foreach (self::getMemcacheServers() as $server) {
|
||||
$server->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function adds a server from the 'memcache_store.servers'
|
||||
* configuration option to a Memcache object.
|
||||
*
|
||||
* The server parameter is an array with the following keys:
|
||||
* - hostname
|
||||
* Hostname or ip address to the memcache server.
|
||||
* - port (optional)
|
||||
* port number the memcache server is running on. This
|
||||
* defaults to memcache.default_port if no value is given.
|
||||
* The default value of memcache.default_port is 11211.
|
||||
* - weight (optional)
|
||||
* The weight of this server in the load balancing
|
||||
* cluster.
|
||||
* - timeout (optional)
|
||||
* The timeout for contacting this server, in seconds.
|
||||
* The default value is 3 seconds.
|
||||
*
|
||||
* @param \Memcache|Memcached $memcache The Memcache object we should add this server to.
|
||||
* @param array $server An associative array with the configuration options for the server to add.
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception If any configuration option for the server is invalid.
|
||||
*/
|
||||
private static function addMemcacheServer($memcache, $server)
|
||||
{
|
||||
// the hostname option is required
|
||||
if (!array_key_exists('hostname', $server)) {
|
||||
throw new Exception(
|
||||
"hostname setting missing from server in the 'memcache_store.servers' configuration option."
|
||||
);
|
||||
}
|
||||
|
||||
$hostname = $server['hostname'];
|
||||
|
||||
// the hostname must be a valid string
|
||||
if (!is_string($hostname)) {
|
||||
throw new Exception(
|
||||
"Invalid hostname for server in the 'memcache_store.servers' configuration option. The hostname is" .
|
||||
' supposed to be a string.'
|
||||
);
|
||||
}
|
||||
|
||||
// check if the user has specified a port number
|
||||
if (strpos($hostname, 'unix:///') === 0) {
|
||||
// force port to be 0 for sockets
|
||||
$port = 0;
|
||||
} elseif (array_key_exists('port', $server)) {
|
||||
// get the port number from the array, and validate it
|
||||
$port = (int) $server['port'];
|
||||
if (($port <= 0) || ($port > 65535)) {
|
||||
throw new Exception(
|
||||
"Invalid port for server in the 'memcache_store.servers' configuration option. The port number" .
|
||||
' is supposed to be an integer between 0 and 65535.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// use the default port number from the ini-file
|
||||
$port = (int) ini_get('memcache.default_port');
|
||||
if ($port <= 0 || $port > 65535) {
|
||||
// invalid port number from the ini-file. fall back to the default
|
||||
$port = 11211;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the user has specified a weight for this server
|
||||
if (array_key_exists('weight', $server)) {
|
||||
// get the weight and validate it
|
||||
$weight = (int) $server['weight'];
|
||||
if ($weight <= 0) {
|
||||
throw new Exception(
|
||||
"Invalid weight for server in the 'memcache_store.servers' configuration option. The weight is" .
|
||||
' supposed to be a positive integer.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// use a default weight of 1
|
||||
$weight = 1;
|
||||
}
|
||||
|
||||
// check if the user has specified a timeout for this server
|
||||
if (array_key_exists('timeout', $server)) {
|
||||
// get the timeout and validate it
|
||||
$timeout = (int) $server['timeout'];
|
||||
if ($timeout <= 0) {
|
||||
throw new Exception(
|
||||
"Invalid timeout for server in the 'memcache_store.servers' configuration option. The timeout is" .
|
||||
' supposed to be a positive integer.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// use a default timeout of 3 seconds
|
||||
$timeout = 3;
|
||||
}
|
||||
|
||||
// add this server to the Memcache object
|
||||
if ($memcache instanceof Memcached) {
|
||||
$memcache->addServer($hostname, $port);
|
||||
} else {
|
||||
$memcache->addServer($hostname, $port, true, $weight, $timeout, $timeout, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function takes in a list of servers belonging to a group and
|
||||
* creates a Memcache object from the servers in the group.
|
||||
*
|
||||
* @param array $group Array of servers which should be created as a group.
|
||||
* @param string $index The index for this group. Specify if persistent connections are desired.
|
||||
*
|
||||
* @return \Memcache|Memcached A Memcache object of the servers in the group
|
||||
*
|
||||
* @throws Exception If the servers configuration is invalid.
|
||||
*/
|
||||
private static function loadMemcacheServerGroup(array $group, $index = null)
|
||||
{
|
||||
if (class_exists(Memcached::class)) {
|
||||
if (is_string($index)) {
|
||||
$memcache = new Memcached($index);
|
||||
} else {
|
||||
$memcache = new Memcached();
|
||||
}
|
||||
if (array_key_exists('options', $group)) {
|
||||
$memcache->setOptions($group['options']);
|
||||
unset($group['options']);
|
||||
}
|
||||
self::$extension = Memcached::class;
|
||||
|
||||
$servers = $memcache->getServerList();
|
||||
if (count($servers) === count($group) && !$memcache->isPristine()) {
|
||||
return $memcache;
|
||||
}
|
||||
$memcache->resetServerList();
|
||||
} elseif (class_exists(\Memcache::class)) {
|
||||
$memcache = new \Memcache();
|
||||
self::$extension = \Memcache::class;
|
||||
} else {
|
||||
throw new Exception(
|
||||
'Missing Memcached implementation. You must install either the Memcache or Memcached extension.'
|
||||
);
|
||||
}
|
||||
|
||||
if (self::$extension === \Memcache::class) {
|
||||
Logger::warning(
|
||||
"The use of PHP-extension memcache is deprecated. Please migrate to the memcached extension."
|
||||
);
|
||||
}
|
||||
|
||||
// iterate over all the servers in the group and add them to the Memcache object
|
||||
foreach ($group as $index => $server) {
|
||||
// make sure that we don't have an index. An index would be a sign of invalid configuration
|
||||
if (!is_int($index)) {
|
||||
throw new Exception(
|
||||
"Invalid index on element in the 'memcache_store.servers' configuration option. Perhaps you" .
|
||||
' have forgotten to add an array(...) around one of the server groups? The invalid index was: ' .
|
||||
$index
|
||||
);
|
||||
}
|
||||
|
||||
// make sure that the server object is an array. Each server is an array with name-value pairs
|
||||
if (!is_array($server)) {
|
||||
throw new Exception(
|
||||
'Invalid value for the server with index ' . $index .
|
||||
'. Remeber that the \'memcache_store.servers\' configuration option' .
|
||||
' contains an array of arrays of arrays.'
|
||||
);
|
||||
}
|
||||
|
||||
self::addMemcacheServer($memcache, $server);
|
||||
}
|
||||
|
||||
/** @var \Memcache|Memcached */
|
||||
return $memcache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function gets a list of all configured memcache servers. This list is initialized based
|
||||
* on the content of 'memcache_store.servers' in the configuration.
|
||||
*
|
||||
* @return \Memcache[]|Memcached[] Array with Memcache objects.
|
||||
*
|
||||
* @throws Exception If the servers configuration is invalid.
|
||||
*/
|
||||
private static function getMemcacheServers()
|
||||
{
|
||||
// check if we have loaded the servers already
|
||||
if (self::$serverGroups != null) {
|
||||
return self::$serverGroups;
|
||||
}
|
||||
|
||||
// initialize the servers-array
|
||||
self::$serverGroups = [];
|
||||
|
||||
// load the configuration
|
||||
$config = Configuration::getInstance();
|
||||
|
||||
|
||||
$groups = $config->getArray('memcache_store.servers');
|
||||
|
||||
// iterate over all the groups in the 'memcache_store.servers' configuration option
|
||||
foreach ($groups as $index => $group) {
|
||||
/*
|
||||
* Make sure that the group is an array. Each group is an array of servers. Each server is
|
||||
* an array of name => value pairs for that server.
|
||||
*/
|
||||
if (!is_array($group)) {
|
||||
throw new Exception(
|
||||
"Invalid value for the server with index " . $index .
|
||||
". Remeber that the 'memcache_store.servers' configuration option" .
|
||||
' contains an array of arrays of arrays.'
|
||||
);
|
||||
}
|
||||
|
||||
// make sure that the group doesn't have an index. An index would be a sign of invalid configuration
|
||||
if (is_int($index)) {
|
||||
$index = null;
|
||||
}
|
||||
|
||||
// parse and add this group to the server group list
|
||||
self::$serverGroups[] = self::loadMemcacheServerGroup($group, $index);
|
||||
}
|
||||
|
||||
return self::$serverGroups;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a helper-function which returns the expire value of data
|
||||
* we should store to the memcache servers.
|
||||
*
|
||||
* The value is set depending on the configuration. If no value is
|
||||
* set in the configuration, then we will use a default value of 0.
|
||||
* 0 means that the item will never expire.
|
||||
*
|
||||
* @return integer The value which should be passed in the set(...) calls to the memcache objects.
|
||||
*
|
||||
* @throws Exception If the option 'memcache_store.expires' has a negative value.
|
||||
*/
|
||||
private static function getExpireTime()
|
||||
{
|
||||
// get the configuration instance
|
||||
$config = Configuration::getInstance();
|
||||
assert($config instanceof Configuration);
|
||||
|
||||
// get the expire-value from the configuration
|
||||
$expire = $config->getInteger('memcache_store.expires', 0);
|
||||
|
||||
// it must be a positive integer
|
||||
if ($expire < 0) {
|
||||
throw new Exception(
|
||||
"The value of 'memcache_store.expires' in the configuration can't be a negative integer."
|
||||
);
|
||||
}
|
||||
|
||||
/* If the configuration option is 0, then we should return 0. This allows the user to specify that the data
|
||||
* shouldn't expire.
|
||||
*/
|
||||
if ($expire == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The expire option is given as the number of seconds into the future an item should expire. We convert this
|
||||
* to an actual timestamp.
|
||||
*/
|
||||
return (time() + $expire);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function retrieves statistics about all memcache server groups.
|
||||
*
|
||||
* @return array Array with the names of each stat and an array with the value for each server group.
|
||||
*
|
||||
* @throws Exception If memcache server status couldn't be retrieved.
|
||||
*/
|
||||
public static function getStats()
|
||||
{
|
||||
$ret = [];
|
||||
|
||||
foreach (self::getMemcacheServers() as $sg) {
|
||||
$stats = method_exists($sg, 'getExtendedStats') ? $sg->getExtendedStats() : $sg->getStats();
|
||||
foreach ($stats as $server => $data) {
|
||||
if ($data === false) {
|
||||
throw new Exception('Failed to get memcache server status.');
|
||||
}
|
||||
}
|
||||
|
||||
$stats = Utils\Arrays::transpose($stats);
|
||||
|
||||
$ret = array_merge_recursive($ret, $stats);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve statistics directly in the form returned by getExtendedStats, for
|
||||
* all server groups.
|
||||
*
|
||||
* @return array An array with the extended stats output for each server group.
|
||||
*/
|
||||
public static function getRawStats()
|
||||
{
|
||||
$ret = [];
|
||||
|
||||
foreach (self::getMemcacheServers() as $sg) {
|
||||
$stats = method_exists($sg, 'getExtendedStats') ? $sg->getExtendedStats() : $sg->getStats();
|
||||
$ret[] = $stats;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,416 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Metadata;
|
||||
|
||||
use Exception;
|
||||
use SAML\Utils\Time;
|
||||
use SAML2\Constants;
|
||||
use SAML2\XML\saml\Issuer;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Logger;
|
||||
use SAML\Utils;
|
||||
use SAML\Error\MetadataNotFound;
|
||||
use SAML\Utils\ClearableState;
|
||||
|
||||
/**
|
||||
* This file defines a class for metadata handling.
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class MetaDataStorageHandler implements ClearableState
|
||||
{
|
||||
/**
|
||||
* This static variable contains a reference to the current
|
||||
* instance of the metadata handler. This variable will be null if
|
||||
* we haven't instantiated a metadata handler yet.
|
||||
*
|
||||
* @var MetaDataStorageHandler|null
|
||||
*/
|
||||
private static $metadataHandler = null;
|
||||
|
||||
|
||||
/**
|
||||
* This is a list of all the metadata sources we have in our metadata
|
||||
* chain. When we need metadata, we will look through this chain from start to end.
|
||||
*
|
||||
* @var MetaDataStorageSource[]
|
||||
*/
|
||||
private $sources;
|
||||
|
||||
|
||||
/**
|
||||
* This function retrieves the current instance of the metadata handler.
|
||||
* The metadata handler will be instantiated if this is the first call
|
||||
* to this function.
|
||||
*
|
||||
* @return MetaDataStorageHandler The current metadata handler instance.
|
||||
*/
|
||||
public static function getMetadataHandler()
|
||||
{
|
||||
if (self::$metadataHandler === null) {
|
||||
self::$metadataHandler = new MetaDataStorageHandler();
|
||||
}
|
||||
|
||||
return self::$metadataHandler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This constructor initializes this metadata storage handler. It will load and
|
||||
* parse the configuration, and initialize the metadata source list.
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
$config = Configuration::getInstance();
|
||||
|
||||
$sourcesConfig = $config->getArray('metadata.sources', null);
|
||||
|
||||
// for backwards compatibility, and to provide a default configuration
|
||||
if ($sourcesConfig === null) {
|
||||
$type = $config->getString('metadata.handler', 'flatfile');
|
||||
$sourcesConfig = [['type' => $type]];
|
||||
}
|
||||
|
||||
try {
|
||||
$this->sources = MetaDataStorageSource::parseSources($sourcesConfig);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception(
|
||||
"Invalid configuration of the 'metadata.sources' configuration option: " . $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function is used to generate some metadata elements automatically.
|
||||
*
|
||||
* @param string $property The metadata property which should be auto-generated.
|
||||
* @param string $set The set we the property comes from.
|
||||
*
|
||||
* @return string The auto-generated metadata property.
|
||||
* @throws Exception If the metadata cannot be generated automatically.
|
||||
*/
|
||||
public function getGenerated($property, $set)
|
||||
{
|
||||
// first we check if the user has overridden this property in the metadata
|
||||
try {
|
||||
$metadataSet = $this->getMetaDataCurrent($set);
|
||||
if (array_key_exists($property, $metadataSet)) {
|
||||
return $metadataSet[$property];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// probably metadata wasn't found. In any case we continue by generating the metadata
|
||||
}
|
||||
|
||||
// get the configuration
|
||||
$config = Configuration::getInstance();
|
||||
assert($config instanceof Configuration);
|
||||
|
||||
$baseurl = Utils\HTTP::getSelfURLHost() . $config->getBasePath();
|
||||
|
||||
if ($set == 'saml20-sp-hosted') {
|
||||
if ($property === 'SingleLogoutServiceBinding') {
|
||||
return Constants::BINDING_HTTP_REDIRECT;
|
||||
}
|
||||
} elseif ($set == 'saml20-idp-hosted') {
|
||||
switch ($property) {
|
||||
case 'SingleSignOnService':
|
||||
return $baseurl . 'saml2/idp/SSOService.php';
|
||||
|
||||
case 'SingleSignOnServiceBinding':
|
||||
return Constants::BINDING_HTTP_REDIRECT;
|
||||
|
||||
case 'SingleLogoutService':
|
||||
return $baseurl . 'saml2/idp/SingleLogoutService.php';
|
||||
|
||||
case 'SingleLogoutServiceBinding':
|
||||
return Constants::BINDING_HTTP_REDIRECT;
|
||||
}
|
||||
} elseif ($set == 'shib13-idp-hosted') {
|
||||
if ($property === 'SingleSignOnService') {
|
||||
return $baseurl . 'shib13/idp/SSOService.php';
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception('Could not generate metadata property ' . $property . ' for set ' . $set . '.');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function lists all known metadata in the given set. It is returned as an associative array
|
||||
* where the key is the entity id.
|
||||
*
|
||||
* @param string $set The set we want to list metadata from.
|
||||
* @param bool $showExpired A boolean specifying whether expired entities should be returned
|
||||
*
|
||||
* @return array An associative array with the metadata from from the given set.
|
||||
*/
|
||||
public function getList($set = 'saml20-idp-remote', $showExpired = false)
|
||||
{
|
||||
assert(is_string($set));
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($this->sources as $source) {
|
||||
$srcList = $source->getMetadataSet($set);
|
||||
|
||||
if ($showExpired === false) {
|
||||
foreach ($srcList as $key => $le) {
|
||||
if (array_key_exists('expire', $le) && ($le['expire'] < time())) {
|
||||
unset($srcList[$key]);
|
||||
Logger::warning(
|
||||
"Dropping metadata entity " . var_export($key, true) . ", expired " .
|
||||
Time::generateTimestamp($le['expire']) . "."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* $result is the last argument to array_merge because we want the content already
|
||||
* in $result to have precedence.
|
||||
*/
|
||||
$result = array_merge($srcList, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function retrieves metadata for the current entity based on the hostname/path the request
|
||||
* was directed to. It will throw an exception if it is unable to locate the metadata.
|
||||
*
|
||||
* @param string $set The set we want metadata from.
|
||||
*
|
||||
* @return array An associative array with the metadata.
|
||||
*/
|
||||
public function getMetaDataCurrent($set)
|
||||
{
|
||||
return $this->getMetaData(null, $set);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function locates the current entity id based on the hostname/path combination the user accessed.
|
||||
* It will throw an exception if it is unable to locate the entity id.
|
||||
*
|
||||
* @param string $set The set we look for the entity id in.
|
||||
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
|
||||
*
|
||||
* @return string The entity id which is associated with the current hostname/path combination.
|
||||
* @throws Exception If no default metadata can be found in the set for the current host.
|
||||
*/
|
||||
public function getMetaDataCurrentEntityID($set, $type = 'entityid')
|
||||
{
|
||||
assert(is_string($set));
|
||||
|
||||
// first we look for the hostname/path combination
|
||||
$currenthostwithpath = Utils\HTTP::getSelfHostWithPath(); // sp.example.org/university
|
||||
|
||||
foreach ($this->sources as $source) {
|
||||
$index = $source->getEntityIdFromHostPath($currenthostwithpath, $set, $type);
|
||||
if ($index !== null) {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
// then we look for the hostname
|
||||
$currenthost = Utils\HTTP::getSelfHost(); // sp.example.org
|
||||
|
||||
foreach ($this->sources as $source) {
|
||||
$index = $source->getEntityIdFromHostPath($currenthost, $set, $type);
|
||||
if ($index !== null) {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
// then we look for the DEFAULT entry
|
||||
foreach ($this->sources as $source) {
|
||||
$entityId = $source->getEntityIdFromHostPath('__DEFAULT__', $set, $type);
|
||||
if ($entityId !== null) {
|
||||
return $entityId;
|
||||
}
|
||||
}
|
||||
|
||||
// we were unable to find the hostname/path in any metadata source
|
||||
throw new Exception(
|
||||
'Could not find any default metadata entities in set [' . $set . '] for host [' . $currenthost . ' : ' .
|
||||
$currenthostwithpath . ']'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method will call getPreferredEntityIdFromCIDRhint() on all of the
|
||||
* sources.
|
||||
*
|
||||
* @param string $set Which set of metadata we are looking it up in.
|
||||
* @param string $ip IP address
|
||||
*
|
||||
* @return string|null The entity id of a entity which have a CIDR hint where the provided
|
||||
* IP address match.
|
||||
*/
|
||||
public function getPreferredEntityIdFromCIDRhint($set, $ip)
|
||||
{
|
||||
foreach ($this->sources as $source) {
|
||||
$entityId = $source->getPreferredEntityIdFromCIDRhint($set, $ip);
|
||||
if ($entityId !== null) {
|
||||
return $entityId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function loads the metadata for entity IDs in $entityIds. It is returned as an associative array
|
||||
* where the key is the entity id. An empty array may be returned if no matching entities were found
|
||||
* @param array $entityIds The entity ids to load
|
||||
* @param string $set The set we want to get metadata from.
|
||||
* @return array An associative array with the metadata for the requested entities, if found.
|
||||
*/
|
||||
public function getMetaDataForEntities(array $entityIds, $set)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->sources as $source) {
|
||||
$srcList = $source->getMetaDataForEntities($entityIds, $set);
|
||||
foreach ($srcList as $key => $le) {
|
||||
if (array_key_exists('expire', $le)) {
|
||||
if ($le['expire'] < time()) {
|
||||
unset($srcList[$key]);
|
||||
Logger::warning(
|
||||
"Dropping metadata entity " . var_export($key, true) . ", expired " .
|
||||
Time::generateTimestamp($le['expire']) . "."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// We found the entity id so remove it from the list that needs resolving
|
||||
unset($entityIds[array_search($key, $entityIds)]);
|
||||
}
|
||||
$result = array_merge($srcList, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function looks up the metadata for the given entity id in the given set. It will throw an
|
||||
* exception if it is unable to locate the metadata.
|
||||
*
|
||||
* @param string|null $index The entity id we are looking up. This parameter may be NULL, in which case we look up
|
||||
* the current entity id based on the current hostname/path.
|
||||
* @param string $set The set of metadata we are looking up the entity id in.
|
||||
*
|
||||
* @return array The metadata array describing the specified entity.
|
||||
* @throws Exception If metadata for the specified entity is expired.
|
||||
* @throws MetadataNotFound If no metadata for the entity specified can be found.
|
||||
*/
|
||||
public function getMetaData($index, $set)
|
||||
{
|
||||
assert(is_string($set));
|
||||
|
||||
if ($index === null) {
|
||||
$index = $this->getMetaDataCurrentEntityID($set, 'metaindex');
|
||||
}
|
||||
|
||||
assert(is_string($index));
|
||||
|
||||
foreach ($this->sources as $source) {
|
||||
$metadata = $source->getMetaData($index, $set);
|
||||
|
||||
if ($metadata !== null) {
|
||||
if (array_key_exists('expire', $metadata)) {
|
||||
if ($metadata['expire'] < time()) {
|
||||
throw new Exception(
|
||||
'Metadata for the entity [' . $index . '] expired ' .
|
||||
(time() - $metadata['expire']) . ' seconds ago.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$metadata['metadata-index'] = $index;
|
||||
$metadata['metadata-set'] = $set;
|
||||
assert(array_key_exists('entityid', $metadata));
|
||||
return $metadata;
|
||||
}
|
||||
}
|
||||
|
||||
throw new MetadataNotFound($index);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the metadata as a configuration object.
|
||||
*
|
||||
* This function will throw an exception if it is unable to locate the metadata.
|
||||
*
|
||||
* @param string $entityId The entity ID we are looking up.
|
||||
* @param string $set The metadata set we are searching.
|
||||
*
|
||||
* @return Configuration The configuration object representing the metadata.
|
||||
* @throws MetadataNotFound If no metadata for the entity specified can be found.
|
||||
*/
|
||||
public function getMetaDataConfig($entityId, $set)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
assert(is_string($set));
|
||||
|
||||
$metadata = $this->getMetaData($entityId, $set);
|
||||
return Configuration::loadFromArray($metadata, $set . '/' . var_export($entityId, true));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Search for an entity's metadata, given the SHA1 digest of its entity ID.
|
||||
*
|
||||
* @param string $sha1 The SHA1 digest of the entity ID.
|
||||
* @param string $set The metadata set we are searching.
|
||||
*
|
||||
* @return null|Configuration The metadata corresponding to the entity, or null if the entity cannot be
|
||||
* found.
|
||||
*/
|
||||
public function getMetaDataConfigForSha1($sha1, $set)
|
||||
{
|
||||
assert(is_string($sha1));
|
||||
assert(is_string($set));
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($this->sources as $source) {
|
||||
$srcList = $source->getMetadataSet($set);
|
||||
|
||||
/* $result is the last argument to array_merge because we want the content already
|
||||
* in $result to have precedence.
|
||||
*/
|
||||
$result = array_merge($srcList, $result);
|
||||
}
|
||||
foreach ($result as $remote_provider) {
|
||||
if (sha1($remote_provider['entityid']) == $sha1) {
|
||||
$remote_provider['metadata-set'] = $set;
|
||||
|
||||
return Configuration::loadFromArray(
|
||||
$remote_provider,
|
||||
$set . '/' . var_export($remote_provider['entityid'], true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear any metadata cached.
|
||||
* Allows for metadata configuration to be changed and reloaded during a given request. Most useful
|
||||
* when running phpunit tests and needing to alter config.php and metadata sources between test cases
|
||||
* @return void
|
||||
*/
|
||||
public static function clearInternalState()
|
||||
{
|
||||
self::$metadataHandler = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Metadata;
|
||||
|
||||
use Exception;
|
||||
use SAML\Configuration;
|
||||
|
||||
/**
|
||||
* This file defines a flat file metadata source.
|
||||
* Instantiation of session handler objects should be done through
|
||||
* the class method getMetadataHandler().
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class MetaDataStorageHandlerFlatFile extends MetaDataStorageSource
|
||||
{
|
||||
/**
|
||||
* This is the directory we will load metadata files from. The path will always end
|
||||
* with a '/'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $directory = '/';
|
||||
|
||||
|
||||
/**
|
||||
* This is an associative array which stores the different metadata sets we have loaded.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $cachedMetadata = [];
|
||||
|
||||
|
||||
/**
|
||||
* This constructor initializes the flatfile metadata storage handler with the
|
||||
* specified configuration. The configuration is an associative array with the following
|
||||
* possible elements:
|
||||
* - 'directory': The directory we should load metadata from. The default directory is
|
||||
* set in the 'metadatadir' configuration option in 'config.php'.
|
||||
*
|
||||
* @param array $config An associative array with the configuration for this handler.
|
||||
*/
|
||||
protected function __construct($config)
|
||||
{
|
||||
assert(is_array($config));
|
||||
|
||||
// get the configuration
|
||||
$globalConfig = Configuration::getInstance();
|
||||
|
||||
// find the path to the directory we should search for metadata in
|
||||
if (array_key_exists('directory', $config)) {
|
||||
$this->directory = $config['directory'] ?: 'metadata/';
|
||||
} else {
|
||||
$this->directory = $globalConfig->getString('metadatadir', 'metadata/');
|
||||
}
|
||||
|
||||
/* Resolve this directory relative to the SimpleSAMLphp directory (unless it is
|
||||
* an absolute path).
|
||||
*/
|
||||
|
||||
/** @var string $base */
|
||||
$base = $globalConfig->resolvePath($this->directory);
|
||||
$this->directory = $base . '/';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function loads the given set of metadata from a file our metadata directory.
|
||||
* This function returns null if it is unable to locate the given set in the metadata directory.
|
||||
*
|
||||
* @param string $set The set of metadata we are loading.
|
||||
*
|
||||
* @return array|null An associative array with the metadata,
|
||||
* or null if we are unable to load metadata from the given file.
|
||||
* @throws Exception If the metadata set cannot be loaded.
|
||||
*/
|
||||
private function load($set)
|
||||
{
|
||||
$metadatasetfile = $this->directory . $set . '.php';
|
||||
|
||||
if (!file_exists($metadatasetfile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$metadata = [];
|
||||
|
||||
include($metadatasetfile);
|
||||
|
||||
if (!is_array($metadata)) {
|
||||
throw new Exception('Could not load metadata set [' . $set . '] from file: ' . $metadatasetfile);
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function retrieves the given set of metadata. It will return an empty array if it is
|
||||
* unable to locate it.
|
||||
*
|
||||
* @param string $set The set of metadata we are retrieving.
|
||||
*
|
||||
* @return array An associative array with the metadata. Each element in the array is an entity, and the
|
||||
* key is the entity id.
|
||||
*/
|
||||
public function getMetadataSet($set)
|
||||
{
|
||||
if (array_key_exists($set, $this->cachedMetadata)) {
|
||||
return $this->cachedMetadata[$set];
|
||||
}
|
||||
|
||||
/** @var array|null $metadataSet */
|
||||
$metadataSet = $this->load($set);
|
||||
if ($metadataSet === null) {
|
||||
$metadataSet = [];
|
||||
}
|
||||
|
||||
// add the entity id of an entry to each entry in the metadata
|
||||
foreach ($metadataSet as $entityId => &$entry) {
|
||||
$entry = $this->updateEntityID($set, $entityId, $entry);
|
||||
}
|
||||
|
||||
$this->cachedMetadata[$set] = $metadataSet;
|
||||
return $metadataSet;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Metadata;
|
||||
|
||||
use SAML\Database;
|
||||
use SAML\Error;
|
||||
use SAML\Error\Exception;
|
||||
|
||||
/**
|
||||
* Class for handling metadata files stored in a database.
|
||||
*
|
||||
* This class has been based off a previous version written by
|
||||
* mooknarf@gmail.com and patched to work with the latest version
|
||||
* of SimpleSAMLphp
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class MetaDataStorageHandlerPdo extends MetaDataStorageSource
|
||||
{
|
||||
/**
|
||||
* The PDO object
|
||||
*/
|
||||
private $db;
|
||||
|
||||
/**
|
||||
* Prefix to apply to the metadata table
|
||||
*/
|
||||
private $tablePrefix;
|
||||
|
||||
/**
|
||||
* This is an associative array which stores the different metadata sets we have loaded.
|
||||
*/
|
||||
private $cachedMetadata = [];
|
||||
|
||||
/**
|
||||
* All the metadata sets supported by this MetaDataStorageHandler
|
||||
*/
|
||||
public $supportedSets = [
|
||||
'adfs-idp-hosted',
|
||||
'adfs-sp-remote',
|
||||
'saml20-idp-hosted',
|
||||
'saml20-idp-remote',
|
||||
'saml20-sp-remote',
|
||||
'shib13-idp-hosted',
|
||||
'shib13-idp-remote',
|
||||
'shib13-sp-hosted',
|
||||
'shib13-sp-remote'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* This constructor initializes the PDO metadata storage handler with the specified
|
||||
* configuration. The configuration is an associative array with the following
|
||||
* possible elements (set in config.php):
|
||||
* - 'usePersistentConnection': TRUE/FALSE if database connection should be persistent.
|
||||
* - 'dsn': The database connection string.
|
||||
* - 'username': Database user name
|
||||
* - 'password': Password for the database user.
|
||||
*
|
||||
* @param array $config An associative array with the configuration for this handler.
|
||||
*/
|
||||
public function __construct($config)
|
||||
{
|
||||
assert(is_array($config));
|
||||
|
||||
$this->db = Database::getInstance();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function loads the given set of metadata from a file to a configured database.
|
||||
* This function returns NULL if it is unable to locate the given set in the metadata directory.
|
||||
*
|
||||
* @param string $set The set of metadata we are loading.
|
||||
*
|
||||
* @return array|null $metadata Associative array with the metadata, or NULL if we are unable to load
|
||||
* metadata from the given file.
|
||||
*
|
||||
* @throws \Exception If a database error occurs.
|
||||
* @throws Exception If the metadata can be retrieved from the database, but cannot be decoded.
|
||||
*/
|
||||
private function load($set)
|
||||
{
|
||||
assert(is_string($set));
|
||||
|
||||
$tableName = $this->getTableName($set);
|
||||
|
||||
if (!in_array($set, $this->supportedSets, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $this->db->read("SELECT entity_id, entity_data FROM $tableName");
|
||||
if ($stmt->execute()) {
|
||||
$metadata = [];
|
||||
|
||||
while ($d = $stmt->fetch()) {
|
||||
$data = json_decode($d['entity_data'], true);
|
||||
if ($data === null) {
|
||||
throw new Exception("Cannot decode metadata for entity '${d['entity_id']}'");
|
||||
}
|
||||
if (!array_key_exists('entityid', $data)) {
|
||||
$data['entityid'] = $d['entity_id'];
|
||||
}
|
||||
$metadata[$d['entity_id']] = $data;
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
} else {
|
||||
throw new \Exception(
|
||||
'PDO metadata handler: Database error: ' . var_export($this->db->getLastError(), true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a list of all available metadata for a given set.
|
||||
*
|
||||
* @param string $set The set we are looking for metadata in.
|
||||
*
|
||||
* @return array $metadata An associative array with all the metadata for the given set.
|
||||
*/
|
||||
public function getMetadataSet($set)
|
||||
{
|
||||
assert(is_string($set));
|
||||
|
||||
if (array_key_exists($set, $this->cachedMetadata)) {
|
||||
return $this->cachedMetadata[$set];
|
||||
}
|
||||
|
||||
$metadataSet = $this->load($set);
|
||||
if ($metadataSet === null) {
|
||||
$metadataSet = [];
|
||||
}
|
||||
|
||||
/** @var array $metadataSet */
|
||||
foreach ($metadataSet as $entityId => &$entry) {
|
||||
$entry = $this->updateEntityID($set, $entityId, $entry);
|
||||
}
|
||||
|
||||
$this->cachedMetadata[$set] = $metadataSet;
|
||||
return $metadataSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a metadata entry.
|
||||
*
|
||||
* @param string $entityId The entityId we are looking up.
|
||||
* @param string $set The set we are looking for metadata in.
|
||||
*
|
||||
* @return array|null An associative array with metadata for the given entity, or NULL if we are unable to
|
||||
* locate the entity.
|
||||
*/
|
||||
public function getMetaData($entityId, $set)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
assert(is_string($set));
|
||||
|
||||
// validate the metadata set is valid
|
||||
if (!in_array($set, $this->supportedSets, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// support caching
|
||||
if (isset($this->cachedMetadata[$entityId][$set])) {
|
||||
return $this->cachedMetadata[$entityId][$set];
|
||||
}
|
||||
|
||||
$tableName = $this->getTableName($set);
|
||||
|
||||
// according to the docs, it looks like *-idp-hosted metadata are the types
|
||||
// that allow the __DYNAMIC:*__ entity id. with the current table design
|
||||
// we need to lookup the specific metadata entry but also we need to lookup
|
||||
// any dynamic entries to see if the dynamic hosted entity id matches
|
||||
if (substr($set, -10) == 'idp-hosted') {
|
||||
$stmt = $this->db->read(
|
||||
"SELECT entity_id, entity_data FROM {$tableName} "
|
||||
. "WHERE (entity_id LIKE :dynamicId OR entity_id = :entityId)",
|
||||
['dynamicId' => '__DYNAMIC%', 'entityId' => $entityId]
|
||||
);
|
||||
} else {
|
||||
// other metadata types should be able to match on entity id
|
||||
$stmt = $this->db->read(
|
||||
"SELECT entity_id, entity_data FROM {$tableName} WHERE entity_id = :entityId",
|
||||
['entityId' => $entityId]
|
||||
);
|
||||
}
|
||||
|
||||
// throw pdo exception upon execution failure
|
||||
if (!$stmt->execute()) {
|
||||
throw new \Exception(
|
||||
'PDO metadata handler: Database error: ' . var_export($this->db->getLastError(), true)
|
||||
);
|
||||
}
|
||||
|
||||
// load the metadata into an array
|
||||
$metadataSet = [];
|
||||
while ($d = $stmt->fetch()) {
|
||||
$data = json_decode($d['entity_data'], true);
|
||||
if (json_last_error() != JSON_ERROR_NONE) {
|
||||
throw new Exception(
|
||||
"Cannot decode metadata for entity '${d['entity_id']}'"
|
||||
);
|
||||
}
|
||||
|
||||
// update the entity id to either the key (if not dynamic or generate the dynamic hosted url)
|
||||
$metadataSet[$d['entity_id']] = $this->updateEntityID($set, $entityId, $data);
|
||||
}
|
||||
|
||||
$indexLookup = $this->lookupIndexFromEntityId($entityId, $metadataSet);
|
||||
if (isset($indexLookup) && array_key_exists($indexLookup, $metadataSet)) {
|
||||
$this->cachedMetadata[$indexLookup][$set] = $metadataSet[$indexLookup];
|
||||
return $this->cachedMetadata[$indexLookup][$set];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add metadata to the configured database
|
||||
*
|
||||
* @param string $index Entity ID
|
||||
* @param string $set The set to add the metadata to
|
||||
* @param array $entityData Metadata
|
||||
*
|
||||
* @return bool True/False if entry was successfully added
|
||||
*/
|
||||
public function addEntry($index, $set, $entityData)
|
||||
{
|
||||
assert(is_string($index));
|
||||
assert(is_string($set));
|
||||
assert(is_array($entityData));
|
||||
|
||||
if (!in_array($set, $this->supportedSets, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tableName = $this->getTableName($set);
|
||||
|
||||
$metadata = $this->db->read(
|
||||
"SELECT entity_id, entity_data FROM $tableName WHERE entity_id = :entity_id",
|
||||
[
|
||||
'entity_id' => $index,
|
||||
]
|
||||
);
|
||||
|
||||
$retrivedEntityIDs = $metadata->fetch();
|
||||
|
||||
$params = [
|
||||
'entity_id' => $index,
|
||||
'entity_data' => json_encode($entityData),
|
||||
];
|
||||
|
||||
if ($retrivedEntityIDs !== false && count($retrivedEntityIDs) > 0) {
|
||||
$rows = $this->db->write(
|
||||
"UPDATE $tableName SET entity_data = :entity_data WHERE entity_id = :entity_id",
|
||||
$params
|
||||
);
|
||||
} else {
|
||||
$rows = $this->db->write(
|
||||
"INSERT INTO $tableName (entity_id, entity_data) VALUES (:entity_id, :entity_data)",
|
||||
$params
|
||||
);
|
||||
}
|
||||
|
||||
return $rows === 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Replace the -'s to an _ in table names for Metadata sets
|
||||
* since SQL does not allow a - in a table name.
|
||||
*
|
||||
* @param string $table Table
|
||||
*
|
||||
* @return string Replaced table name
|
||||
*/
|
||||
private function getTableName($table)
|
||||
{
|
||||
assert(is_string($table));
|
||||
|
||||
return $this->db->applyPrefix(str_replace("-", "_", $this->tablePrefix . $table));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the configured database
|
||||
*
|
||||
* @return int|false The number of SQL statements successfully executed, false if some error occurred.
|
||||
*/
|
||||
public function initDatabase()
|
||||
{
|
||||
$stmt = 0;
|
||||
$fine = true;
|
||||
foreach ($this->supportedSets as $set) {
|
||||
$tableName = $this->getTableName($set);
|
||||
$rows = $this->db->write(
|
||||
"CREATE TABLE IF NOT EXISTS $tableName (entity_id VARCHAR(255) PRIMARY KEY NOT NULL, entity_data " .
|
||||
"TEXT NOT NULL)"
|
||||
);
|
||||
if ($rows === false) {
|
||||
$fine = false;
|
||||
} else {
|
||||
$stmt += $rows;
|
||||
}
|
||||
}
|
||||
if (!$fine) {
|
||||
return false;
|
||||
}
|
||||
return $stmt;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,311 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Metadata;
|
||||
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* Class for handling metadata files in serialized format.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class MetaDataStorageHandlerSerialize extends MetaDataStorageSource
|
||||
{
|
||||
/**
|
||||
* The file extension we use for our metadata files.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const EXTENSION = '.serialized';
|
||||
|
||||
|
||||
/**
|
||||
* The base directory where metadata is stored.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $directory = '/';
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for this metadata handler.
|
||||
*
|
||||
* Parses configuration.
|
||||
*
|
||||
* @param array $config The configuration for this metadata handler.
|
||||
*/
|
||||
public function __construct($config)
|
||||
{
|
||||
assert(is_array($config));
|
||||
|
||||
$globalConfig = Configuration::getInstance();
|
||||
|
||||
$cfgHelp = Configuration::loadFromArray($config, 'serialize metadata source');
|
||||
|
||||
$this->directory = $cfgHelp->getString('directory');
|
||||
|
||||
/* Resolve this directory relative to the SimpleSAMLphp directory (unless it is
|
||||
* an absolute path).
|
||||
*/
|
||||
$this->directory = Utils\System::resolvePath($this->directory, $globalConfig->getBaseDir());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper function for retrieving the path of a metadata file.
|
||||
*
|
||||
* @param string $entityId The entity ID.
|
||||
* @param string $set The metadata set.
|
||||
*
|
||||
* @return string The path to the metadata file.
|
||||
*/
|
||||
private function getMetadataPath($entityId, $set)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
assert(is_string($set));
|
||||
|
||||
return $this->directory . '/' . rawurlencode($set) . '/' . rawurlencode($entityId) . self::EXTENSION;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a list of all available metadata sets.
|
||||
*
|
||||
* @return array An array with the available sets.
|
||||
*/
|
||||
public function getMetadataSets()
|
||||
{
|
||||
$ret = [];
|
||||
|
||||
$dh = @opendir($this->directory);
|
||||
if ($dh === false) {
|
||||
Logger::warning(
|
||||
'Serialize metadata handler: Unable to open directory: ' . var_export($this->directory, true)
|
||||
);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
while (($entry = readdir($dh)) !== false) {
|
||||
if ($entry[0] === '.') {
|
||||
// skip '..', '.' and hidden files
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $this->directory . '/' . $entry;
|
||||
|
||||
if (!is_dir($path)) {
|
||||
Logger::warning(
|
||||
'Serialize metadata handler: Metadata directory contained a file where only directories should ' .
|
||||
'exist: ' . var_export($path, true)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$ret[] = rawurldecode($entry);
|
||||
}
|
||||
|
||||
closedir($dh);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a list of all available metadata for a given set.
|
||||
*
|
||||
* @param string $set The set we are looking for metadata in.
|
||||
*
|
||||
* @return array An associative array with all the metadata for the given set.
|
||||
*/
|
||||
public function getMetadataSet($set)
|
||||
{
|
||||
assert(is_string($set));
|
||||
|
||||
$ret = [];
|
||||
|
||||
$dir = $this->directory . '/' . rawurlencode($set);
|
||||
if (!is_dir($dir)) {
|
||||
// probably some code asked for a metadata set which wasn't available
|
||||
return $ret;
|
||||
}
|
||||
|
||||
$dh = @opendir($dir);
|
||||
if ($dh === false) {
|
||||
Logger::warning(
|
||||
'Serialize metadata handler: Unable to open directory: ' . var_export($dir, true)
|
||||
);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
$extLen = strlen(self::EXTENSION);
|
||||
|
||||
while (($file = readdir($dh)) !== false) {
|
||||
if (strlen($file) <= $extLen) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (substr($file, -$extLen) !== self::EXTENSION) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entityId = substr($file, 0, -$extLen);
|
||||
$entityId = rawurldecode($entityId);
|
||||
|
||||
$md = $this->getMetaData($entityId, $set);
|
||||
if ($md !== null) {
|
||||
$ret[$entityId] = $md;
|
||||
}
|
||||
}
|
||||
|
||||
closedir($dh);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a metadata entry.
|
||||
*
|
||||
* @param string $entityId The entityId we are looking up.
|
||||
* @param string $set The set we are looking for metadata in.
|
||||
*
|
||||
* @return array|null An associative array with metadata for the given entity, or NULL if we are unable to
|
||||
* locate the entity.
|
||||
*/
|
||||
public function getMetaData($entityId, $set)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
assert(is_string($set));
|
||||
|
||||
$filePath = $this->getMetadataPath($entityId, $set);
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = @file_get_contents($filePath);
|
||||
if ($data === false) {
|
||||
/** @var array $error */
|
||||
$error = error_get_last();
|
||||
Logger::warning(
|
||||
'Error reading file ' . $filePath . ': ' . $error['message']
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = @unserialize($data);
|
||||
if ($data === false) {
|
||||
Logger::warning('Error unserializing file: ' . $filePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!array_key_exists('entityid', $data)) {
|
||||
$data['entityid'] = $entityId;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save a metadata entry.
|
||||
*
|
||||
* @param string $entityId The entityId of the metadata entry.
|
||||
* @param string $set The metadata set this metadata entry belongs to.
|
||||
* @param array $metadata The metadata.
|
||||
*
|
||||
* @return bool True if successfully saved, false otherwise.
|
||||
*/
|
||||
public function saveMetadata($entityId, $set, $metadata)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
assert(is_string($set));
|
||||
assert(is_array($metadata));
|
||||
|
||||
$filePath = $this->getMetadataPath($entityId, $set);
|
||||
$newPath = $filePath . '.new';
|
||||
|
||||
$dir = dirname($filePath);
|
||||
if (!is_dir($dir)) {
|
||||
Logger::info('Creating directory: ' . $dir);
|
||||
$res = @mkdir($dir, 0777, true);
|
||||
if ($res === false) {
|
||||
/** @var array $error */
|
||||
$error = error_get_last();
|
||||
Logger::error('Failed to create directory ' . $dir . ': ' . $error['message']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$data = serialize($metadata);
|
||||
|
||||
Logger::debug('Writing: ' . $newPath);
|
||||
|
||||
$res = file_put_contents($newPath, $data);
|
||||
if ($res === false) {
|
||||
/** @var array $error */
|
||||
$error = error_get_last();
|
||||
Logger::error('Error saving file ' . $newPath . ': ' . $error['message']);
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = rename($newPath, $filePath);
|
||||
if ($res === false) {
|
||||
/** @var array $error */
|
||||
$error = error_get_last();
|
||||
Logger::error('Error renaming ' . $newPath . ' to ' . $filePath . ': ' . $error['message']);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a metadata entry.
|
||||
*
|
||||
* @param string $entityId The entityId of the metadata entry.
|
||||
* @param string $set The metadata set this metadata entry belongs to.
|
||||
* @return void
|
||||
*/
|
||||
public function deleteMetadata($entityId, $set)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
assert(is_string($set));
|
||||
|
||||
$filePath = $this->getMetadataPath($entityId, $set);
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
Logger::warning(
|
||||
'Attempted to erase nonexistent metadata entry ' .
|
||||
var_export($entityId, true) . ' in set ' . var_export($set, true) . '.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$res = unlink($filePath);
|
||||
if ($res === false) {
|
||||
/** @var array $error */
|
||||
$error = error_get_last();
|
||||
Logger::error(
|
||||
'Failed to delete file ' . $filePath .
|
||||
': ' . $error['message']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function loads the metadata for entity IDs in $entityIds. It is returned as an associative array
|
||||
* where the key is the entity id. An empty array may be returned if no matching entities were found
|
||||
* @param array $entityIds The entity ids to load
|
||||
* @param string $set The set we want to get metadata from.
|
||||
* @return array An associative array with the metadata for the requested entities, if found.
|
||||
*/
|
||||
public function getMetaDataForEntities(array $entityIds, $set)
|
||||
{
|
||||
return $this->getMetaDataForEntitiesIndividually($entityIds, $set);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Metadata;
|
||||
|
||||
use Exception;
|
||||
use SAML\Configuration;
|
||||
|
||||
/**
|
||||
* This class implements a metadata source which loads metadata from XML files.
|
||||
* The XML files should be in the SAML 2.0 metadata format.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class MetaDataStorageHandlerXML extends MetaDataStorageSource
|
||||
{
|
||||
/**
|
||||
* This variable contains an associative array with the parsed metadata.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $metadata;
|
||||
|
||||
|
||||
/**
|
||||
* This function initializes the XML metadata source. The configuration must contain one of
|
||||
* the following options:
|
||||
* - 'file': Path to a file with the metadata. This path is relative to the SimpleSAMLphp
|
||||
* base directory.
|
||||
* - 'url': URL we should download the metadata from. This is only meant for testing.
|
||||
*
|
||||
* @param array $config The configuration for this instance of the XML metadata source.
|
||||
*
|
||||
* @throws Exception If neither the 'file' or 'url' options are defined in the configuration.
|
||||
*/
|
||||
protected function __construct($config)
|
||||
{
|
||||
$src = $srcXml = null;
|
||||
if (array_key_exists('file', $config)) {
|
||||
// get the configuration
|
||||
$globalConfig = Configuration::getInstance();
|
||||
$src = $globalConfig->resolvePath($config['file']);
|
||||
} elseif (array_key_exists('url', $config)) {
|
||||
$src = $config['url'];
|
||||
} elseif (array_key_exists('xml', $config)) {
|
||||
$srcXml = $config['xml'];
|
||||
} else {
|
||||
throw new Exception("Missing one of 'file', 'url' and 'xml' in XML metadata source configuration.");
|
||||
}
|
||||
|
||||
|
||||
$SP1x = [];
|
||||
$IdP1x = [];
|
||||
$SP20 = [];
|
||||
$IdP20 = [];
|
||||
$AAD = [];
|
||||
|
||||
if (isset($src)) {
|
||||
$entities = SAMLParser::parseDescriptorsFile($src);
|
||||
} elseif (isset($srcXml)) {
|
||||
$entities = SAMLParser::parseDescriptorsString($srcXml);
|
||||
} else {
|
||||
throw new Exception("Neither source file path/URI nor string data provided");
|
||||
}
|
||||
foreach ($entities as $entityId => $entity) {
|
||||
$md = $entity->getMetadata1xSP();
|
||||
if ($md !== null) {
|
||||
$SP1x[$entityId] = $md;
|
||||
}
|
||||
|
||||
$md = $entity->getMetadata1xIdP();
|
||||
if ($md !== null) {
|
||||
$IdP1x[$entityId] = $md;
|
||||
}
|
||||
|
||||
$md = $entity->getMetadata20SP();
|
||||
if ($md !== null) {
|
||||
$SP20[$entityId] = $md;
|
||||
}
|
||||
|
||||
$md = $entity->getMetadata20IdP();
|
||||
if ($md !== null) {
|
||||
$IdP20[$entityId] = $md;
|
||||
}
|
||||
|
||||
$md = $entity->getAttributeAuthorities();
|
||||
if (count($md) > 0) {
|
||||
$AAD[$entityId] = $md[0];
|
||||
}
|
||||
}
|
||||
|
||||
$this->metadata = [
|
||||
'shib13-sp-remote' => $SP1x,
|
||||
'shib13-idp-remote' => $IdP1x,
|
||||
'saml20-sp-remote' => $SP20,
|
||||
'saml20-idp-remote' => $IdP20,
|
||||
'attributeauthority-remote' => $AAD,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function returns an associative array with metadata for all entities in the given set. The
|
||||
* key of the array is the entity id.
|
||||
*
|
||||
* @param string $set The set we want to list metadata for.
|
||||
*
|
||||
* @return array An associative array with all entities in the given set.
|
||||
*/
|
||||
public function getMetadataSet($set)
|
||||
{
|
||||
if (array_key_exists($set, $this->metadata)) {
|
||||
return $this->metadata[$set];
|
||||
}
|
||||
|
||||
// we don't have this metadata set
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,380 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Metadata;
|
||||
|
||||
use Exception;
|
||||
use SAML\Error;
|
||||
use SAML\Module;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* This abstract class defines an interface for metadata storage sources.
|
||||
*
|
||||
* It also contains the overview of the different metadata storage sources.
|
||||
* A metadata storage source can be loaded by passing the configuration of it
|
||||
* to the getSource static function.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @author Andreas Aakre Solberg, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
abstract class MetaDataStorageSource
|
||||
{
|
||||
/**
|
||||
* Parse array with metadata sources.
|
||||
*
|
||||
* This function accepts an array with metadata sources, and returns an array with
|
||||
* each metadata source as an object.
|
||||
*
|
||||
* @param array $sourcesConfig Array with metadata source configuration.
|
||||
*
|
||||
* @return array Parsed metadata configuration.
|
||||
*
|
||||
* @throws Exception If something is wrong in the configuration.
|
||||
*/
|
||||
public static function parseSources($sourcesConfig)
|
||||
{
|
||||
assert(is_array($sourcesConfig));
|
||||
|
||||
$sources = [];
|
||||
|
||||
foreach ($sourcesConfig as $sourceConfig) {
|
||||
if (!is_array($sourceConfig)) {
|
||||
throw new Exception("Found an element in metadata source configuration which wasn't an array.");
|
||||
}
|
||||
|
||||
$sources[] = self::getSource($sourceConfig);
|
||||
}
|
||||
|
||||
return $sources;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function creates a metadata source based on the given configuration.
|
||||
* The type of source is based on the 'type' parameter in the configuration.
|
||||
* The default type is 'flatfile'.
|
||||
*
|
||||
* @param array $sourceConfig Associative array with the configuration for this metadata source.
|
||||
*
|
||||
* @return MetaDataStorageSource An instance of a metadata source with the given configuration.
|
||||
*
|
||||
* @throws Exception If the metadata source type is invalid.
|
||||
*/
|
||||
public static function getSource($sourceConfig)
|
||||
{
|
||||
assert(is_array($sourceConfig));
|
||||
|
||||
if (array_key_exists('type', $sourceConfig)) {
|
||||
$type = $sourceConfig['type'];
|
||||
} else {
|
||||
$type = 'flatfile';
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'flatfile':
|
||||
return new MetaDataStorageHandlerFlatFile($sourceConfig);
|
||||
case 'xml':
|
||||
return new MetaDataStorageHandlerXML($sourceConfig);
|
||||
case 'serialize':
|
||||
return new MetaDataStorageHandlerSerialize($sourceConfig);
|
||||
case 'mdx':
|
||||
case 'mdq':
|
||||
return new Sources\MDQ($sourceConfig);
|
||||
case 'pdo':
|
||||
return new MetaDataStorageHandlerPdo($sourceConfig);
|
||||
default:
|
||||
// metadata store from module
|
||||
try {
|
||||
$className = Module::resolveClass(
|
||||
$type,
|
||||
'MetadataStore',
|
||||
'\SimpleSAML\Metadata\MetaDataStorageSource'
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
throw new Error\CriticalConfigurationError(
|
||||
"Invalid 'type' for metadata source. Cannot find store '$type'.",
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/** @var MetaDataStorageSource */
|
||||
return new $className($sourceConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function attempts to generate an associative array with metadata for all entities in the
|
||||
* given set. The key of the array is the entity id.
|
||||
*
|
||||
* A subclass should override this function if it is able to easily generate this list.
|
||||
*
|
||||
* @param string $set The set we want to list metadata for.
|
||||
*
|
||||
* @return array An associative array with all entities in the given set, or an empty array if we are
|
||||
* unable to generate this list.
|
||||
*/
|
||||
public function getMetadataSet($set)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function resolves an host/path combination to an entity id.
|
||||
*
|
||||
* This class implements this function using the getMetadataSet-function. A subclass should
|
||||
* override this function if it doesn't implement the getMetadataSet function, or if the
|
||||
* implementation of getMetadataSet is slow.
|
||||
*
|
||||
* @param string $hostPath The host/path combination we are looking up.
|
||||
* @param string $set Which set of metadata we are looking it up in.
|
||||
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
|
||||
*
|
||||
* @return string|null An entity id which matches the given host/path combination, or NULL if
|
||||
* we are unable to locate one which matches.
|
||||
*/
|
||||
public function getEntityIdFromHostPath($hostPath, $set, $type = 'entityid')
|
||||
{
|
||||
|
||||
$metadataSet = $this->getMetadataSet($set);
|
||||
/** @psalm-suppress DocblockTypeContradiction */
|
||||
if ($metadataSet === null) {
|
||||
// this metadata source does not have this metadata set
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($metadataSet as $index => $entry) {
|
||||
if (!array_key_exists('host', $entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($hostPath === $entry['host']) {
|
||||
if ($type === 'entityid') {
|
||||
return $entry['entityid'];
|
||||
} else {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no entries matched, we should return null
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function will go through all the metadata, and check the DiscoHints->IPHint
|
||||
* parameter, which defines a network space (ip range) for each remote entry.
|
||||
* This function returns the entityID for any of the entities that have an
|
||||
* IP range which the IP falls within.
|
||||
*
|
||||
* @param string $set Which set of metadata we are looking it up in.
|
||||
* @param string $ip IP address
|
||||
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
|
||||
*
|
||||
* @return string|null The entity id of a entity which have a CIDR hint where the provided
|
||||
* IP address match.
|
||||
*/
|
||||
public function getPreferredEntityIdFromCIDRhint($set, $ip, $type = 'entityid')
|
||||
{
|
||||
$metadataSet = $this->getMetadataSet($set);
|
||||
|
||||
foreach ($metadataSet as $index => $entry) {
|
||||
$cidrHints = [];
|
||||
|
||||
// support hint.cidr for idp discovery
|
||||
if (array_key_exists('hint.cidr', $entry) && is_array($entry['hint.cidr'])) {
|
||||
$cidrHints = $entry['hint.cidr'];
|
||||
}
|
||||
|
||||
// support discohints in idp metadata for idp discovery
|
||||
if (
|
||||
array_key_exists('DiscoHints', $entry)
|
||||
&& array_key_exists('IPHint', $entry['DiscoHints'])
|
||||
&& is_array($entry['DiscoHints']['IPHint'])
|
||||
) {
|
||||
// merge with hints derived from discohints, but prioritize hint.cidr in case it is used
|
||||
$cidrHints = array_merge($entry['DiscoHints']['IPHint'], $cidrHints);
|
||||
}
|
||||
|
||||
if (empty($cidrHints)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($cidrHints as $hint_entry) {
|
||||
if (Utils\Net::ipCIDRcheck($hint_entry, $ip)) {
|
||||
if ($type === 'entityid') {
|
||||
return $entry['entityid'];
|
||||
} else {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no entries matched, we should return null
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function retrieves metadata for the given entity id in the given set of metadata.
|
||||
* It will return NULL if it is unable to locate the metadata.
|
||||
*
|
||||
* This class implements this function using the getMetadataSet-function. A subclass should
|
||||
* override this function if it doesn't implement the getMetadataSet function, or if the
|
||||
* implementation of getMetadataSet is slow.
|
||||
*
|
||||
* @param string $index The entityId or metaindex we are looking up.
|
||||
* @param string $set The set we are looking for metadata in.
|
||||
*
|
||||
* @return array|null An associative array with metadata for the given entity, or NULL if we are unable to
|
||||
* locate the entity.
|
||||
*/
|
||||
public function getMetaData($index, $set)
|
||||
{
|
||||
|
||||
assert(is_string($index));
|
||||
assert(isset($set));
|
||||
|
||||
$metadataSet = $this->getMetadataSet($set);
|
||||
|
||||
$indexLookup = $this->lookupIndexFromEntityId($index, $metadataSet);
|
||||
if (isset($indexLookup) && array_key_exists($indexLookup, $metadataSet)) {
|
||||
return $metadataSet[$indexLookup];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function loads the metadata for entity IDs in $entityIds. It is returned as an associative array
|
||||
* where the key is the entity id. An empty array may be returned if no matching entities were found.
|
||||
* Subclasses should override if their getMetadataSet returns nothing or is slow. Subclasses may want to
|
||||
* delegate to getMetaDataForEntitiesIndividually if loading entities one at a time is faster.
|
||||
* @param array $entityIds The entity ids to load
|
||||
* @param string $set The set we want to get metadata from.
|
||||
* @return array An associative array with the metadata for the requested entities, if found.
|
||||
*/
|
||||
public function getMetaDataForEntities(array $entityIds, $set)
|
||||
{
|
||||
if (count($entityIds) === 1) {
|
||||
return $this->getMetaDataForEntitiesIndividually($entityIds, $set);
|
||||
}
|
||||
$entities = $this->getMetadataSet($set);
|
||||
return array_intersect_key($entities, array_flip($entityIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads metadata entities one at a time, rather than the default implementation of loading all entities
|
||||
* and filtering.
|
||||
* @see MetaDataStorageSource::getMetaDataForEntities()
|
||||
* @param array $entityIds The entity ids to load
|
||||
* @param string $set The set we want to get metadata from.
|
||||
* @return array An associative array with the metadata for the requested entities, if found.
|
||||
*/
|
||||
protected function getMetaDataForEntitiesIndividually(array $entityIds, $set)
|
||||
{
|
||||
$entities = [];
|
||||
foreach ($entityIds as $entityId) {
|
||||
$metadata = $this->getMetaData($entityId, $set);
|
||||
if ($metadata !== null) {
|
||||
$entities[$entityId] = $metadata;
|
||||
}
|
||||
}
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the full metadata set for a given entity id or null if the entity id cannot be found
|
||||
* in the given metadata set.
|
||||
*
|
||||
* @param string $entityId
|
||||
* @param array $metadataSet the already loaded metadata set
|
||||
* @return mixed|null
|
||||
*/
|
||||
protected function lookupIndexFromEntityId($entityId, array $metadataSet)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
assert(is_array($metadataSet));
|
||||
|
||||
// check for hostname
|
||||
$currentHost = Utils\HTTP::getSelfHost(); // sp.example.org
|
||||
|
||||
foreach ($metadataSet as $index => $entry) {
|
||||
// explicit index match
|
||||
if ($index === $entityId) {
|
||||
return $index;
|
||||
}
|
||||
|
||||
if ($entry['entityid'] === $entityId) {
|
||||
if ($entry['host'] === '__DEFAULT__' || $entry['host'] === $currentHost) {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $set
|
||||
* @throws Exception
|
||||
* @return string
|
||||
*/
|
||||
private function getDynamicHostedUrl($set)
|
||||
{
|
||||
assert(is_string($set));
|
||||
|
||||
// get the configuration
|
||||
$baseUrl = Utils\HTTP::getBaseURL();
|
||||
|
||||
if ($set === 'saml20-idp-hosted') {
|
||||
return $baseUrl . 'saml2/idp/metadata.php';
|
||||
} elseif ($set === 'saml20-sp-hosted') {
|
||||
return $baseUrl . 'saml2/sp/metadata.php';
|
||||
} elseif ($set === 'shib13-idp-hosted') {
|
||||
return $baseUrl . 'shib13/idp/metadata.php';
|
||||
} elseif ($set === 'shib13-sp-hosted') {
|
||||
return $baseUrl . 'shib13/sp/metadata.php';
|
||||
} elseif ($set === 'adfs-idp-hosted') {
|
||||
return 'urn:federation:' . Utils\HTTP::getSelfHost() . ':idp';
|
||||
} else {
|
||||
throw new Exception('Can not generate dynamic EntityID for metadata of this type: [' . $set . ']');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the metadata entry's entity id and returns the modified array. If the entity id is __DYNAMIC:*__ a
|
||||
* the current url is assigned. If it is explicit the entityid array key is updated to the entityId that was
|
||||
* provided.
|
||||
*
|
||||
* @param string $metadataSet a metadata set (saml20-idp-hosted, saml20-sp-remote, etc)
|
||||
* @param string $entityId the entity id we are modifying
|
||||
* @param array $metadataEntry the fully populated metadata entry
|
||||
* @return array modified metadata to include the valid entityid
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function updateEntityID($metadataSet, $entityId, array $metadataEntry)
|
||||
{
|
||||
assert(is_string($metadataSet));
|
||||
assert(is_string($entityId));
|
||||
assert(is_array($metadataEntry));
|
||||
|
||||
$modifiedMetadataEntry = $metadataEntry;
|
||||
|
||||
// generate a dynamic hosted url
|
||||
if (preg_match('/__DYNAMIC(:[0-9]+)?__/', $entityId)) {
|
||||
$modifiedMetadataEntry['entityid'] = $this->getDynamicHostedUrl($metadataSet);
|
||||
} else {
|
||||
// set the entityid metadata array key to the provided entity id
|
||||
$modifiedMetadataEntry['entityid'] = $entityId;
|
||||
}
|
||||
|
||||
return $modifiedMetadataEntry;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,835 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Metadata;
|
||||
|
||||
use DOMElement;
|
||||
use SAML2\Constants;
|
||||
use SAML2\XML\md\AttributeAuthorityDescriptor;
|
||||
use SAML2\XML\md\AttributeConsumingService;
|
||||
use SAML2\XML\md\ContactPerson;
|
||||
use SAML2\XML\md\EndpointType;
|
||||
use SAML2\XML\md\EntityDescriptor;
|
||||
use SAML2\XML\md\IDPSSODescriptor;
|
||||
use SAML2\XML\md\IndexedEndpointType;
|
||||
use SAML2\XML\md\Organization;
|
||||
use SAML2\XML\md\RequestedAttribute;
|
||||
use SAML2\XML\md\RoleDescriptor;
|
||||
use SAML2\XML\md\SPSSODescriptor;
|
||||
use SAML2\XML\mdattr\EntityAttributes;
|
||||
use SAML2\XML\mdrpi\RegistrationInfo;
|
||||
use SAML2\XML\mdui\DiscoHints;
|
||||
use SAML2\XML\mdui\Keywords;
|
||||
use SAML2\XML\mdui\Logo;
|
||||
use SAML2\XML\mdui\UIInfo;
|
||||
use SAML2\XML\saml\Attribute;
|
||||
use SAML2\XML\saml\AttributeValue;
|
||||
use SAML2\XML\shibmd\Scope;
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
use SAML\Module\adfs\SAML2\XML\fed\SecurityTokenServiceType;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* Class for generating SAML 2.0 metadata from SimpleSAMLphp metadata arrays.
|
||||
*
|
||||
* This class builds SAML 2.0 metadata for an entity by examining the metadata for the entity.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class SAMLBuilder
|
||||
{
|
||||
/**
|
||||
* The EntityDescriptor we are building.
|
||||
*
|
||||
* @var EntityDescriptor
|
||||
*/
|
||||
private $entityDescriptor;
|
||||
|
||||
|
||||
/**
|
||||
* The maximum time in seconds the metadata should be cached.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $maxCache = null;
|
||||
|
||||
|
||||
/**
|
||||
* The maximum time in seconds since the current time that this metadata should be considered valid.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $maxDuration = null;
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the SAML builder.
|
||||
*
|
||||
* @param string $entityId The entity id of the entity.
|
||||
* @param int|null $maxCache The maximum time in seconds the metadata should be cached. Defaults to null
|
||||
* @param int|null $maxDuration The maximum time in seconds this metadata should be considered valid. Defaults
|
||||
* to null.
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($entityId, $maxCache = null, $maxDuration = null)
|
||||
{
|
||||
assert(is_string($entityId));
|
||||
|
||||
$this->maxCache = $maxCache;
|
||||
$this->maxDuration = $maxDuration;
|
||||
|
||||
$this->entityDescriptor = new EntityDescriptor();
|
||||
$this->entityDescriptor->setEntityID($entityId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array $metadata
|
||||
* @return void
|
||||
*/
|
||||
private function setExpiration($metadata)
|
||||
{
|
||||
if (array_key_exists('expire', $metadata)) {
|
||||
if ($metadata['expire'] - time() < $this->maxDuration) {
|
||||
$this->maxDuration = $metadata['expire'] - time();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->maxCache !== null) {
|
||||
$this->entityDescriptor->setCacheDuration('PT' . $this->maxCache . 'S');
|
||||
}
|
||||
if ($this->maxDuration !== null) {
|
||||
$this->entityDescriptor->setValidUntil(time() + $this->maxDuration);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the EntityDescriptor element which is generated for this entity.
|
||||
*
|
||||
* @return DOMElement The EntityDescriptor element of this entity.
|
||||
*/
|
||||
public function getEntityDescriptor()
|
||||
{
|
||||
$xml = $this->entityDescriptor->toXML();
|
||||
$xml->ownerDocument->appendChild($xml);
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the EntityDescriptor as text.
|
||||
*
|
||||
* This function serializes this EntityDescriptor, and returns it as text.
|
||||
*
|
||||
* @param bool $formatted Whether the returned EntityDescriptor should be formatted first.
|
||||
*
|
||||
* @return string The serialized EntityDescriptor.
|
||||
*/
|
||||
public function getEntityDescriptorText($formatted = true)
|
||||
{
|
||||
assert(is_bool($formatted));
|
||||
|
||||
$xml = $this->getEntityDescriptor();
|
||||
if ($formatted) {
|
||||
Utils\XML::formatDOMElement($xml);
|
||||
}
|
||||
|
||||
return $xml->ownerDocument->saveXML();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a SecurityTokenServiceType for ADFS metadata.
|
||||
*
|
||||
* @param array $metadata The metadata with the information about the SecurityTokenServiceType.
|
||||
* @return void
|
||||
*/
|
||||
public function addSecurityTokenServiceType($metadata)
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
$defaultEndpoint = $metadata->getDefaultEndpoint('SingleSignOnService');
|
||||
$e = new SecurityTokenServiceType();
|
||||
$e->setLocation($defaultEndpoint['Location']);
|
||||
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
$this->entityDescriptor->addRoleDescriptor($e);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add extensions to the metadata.
|
||||
*
|
||||
* @param Configuration $metadata The metadata to get extensions from.
|
||||
* @param RoleDescriptor $e Reference to the element where the Extensions element should be included.
|
||||
* @return void
|
||||
*/
|
||||
private function addExtensions(Configuration $metadata, RoleDescriptor $e)
|
||||
{
|
||||
if ($metadata->hasValue('tags')) {
|
||||
$a = new Attribute();
|
||||
$a->setName('tags');
|
||||
foreach ($metadata->getArray('tags') as $tag) {
|
||||
$a->addAttributeValue(new AttributeValue($tag));
|
||||
}
|
||||
$e->setExtensions(array_merge($e->getExtensions(), [$a]));
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('hint.cidr')) {
|
||||
$a = new Attribute();
|
||||
$a->setName('hint.cidr');
|
||||
foreach ($metadata->getArray('hint.cidr') as $hint) {
|
||||
$a->addAttributeValue(new AttributeValue($hint));
|
||||
}
|
||||
$e->setExtensions(array_merge($e->getExtensions(), [$a]));
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('scope')) {
|
||||
foreach ($metadata->getArray('scope') as $scopetext) {
|
||||
$s = new Scope();
|
||||
$s->setScope($scopetext);
|
||||
// Check whether $ ^ ( ) * | \ are in a scope -> assume regex.
|
||||
if (1 === preg_match('/[\$\^\)\(\*\|\\\\]/', $scopetext)) {
|
||||
$s->setIsRegexpScope(true);
|
||||
} else {
|
||||
$s->setIsRegexpScope(false);
|
||||
}
|
||||
$e->setExtensions(array_merge($e->getExtensions(), [$s]));
|
||||
}
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('EntityAttributes')) {
|
||||
$ea = new EntityAttributes();
|
||||
foreach ($metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) {
|
||||
$a = new Attribute();
|
||||
$a->setName($attributeName);
|
||||
$a->setNameFormat('urn:oasis:names:tc:SAML:2.0:attrname-format:uri');
|
||||
|
||||
// Attribute names that is not URI is prefixed as this: '{nameformat}name'
|
||||
if (preg_match('/^\{(.*?)\}(.*)$/', $attributeName, $matches)) {
|
||||
$a->setName($matches[2]);
|
||||
$nameFormat = $matches[1];
|
||||
if ($nameFormat !== Constants::NAMEFORMAT_UNSPECIFIED) {
|
||||
$a->setNameFormat($nameFormat);
|
||||
}
|
||||
}
|
||||
foreach ($attributeValues as $attributeValue) {
|
||||
$a->addAttributeValue(new AttributeValue($attributeValue));
|
||||
}
|
||||
$ea->addChildren($a);
|
||||
}
|
||||
$this->entityDescriptor->setExtensions(
|
||||
array_merge($this->entityDescriptor->getExtensions(), [$ea])
|
||||
);
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('RegistrationInfo')) {
|
||||
$ri = new RegistrationInfo();
|
||||
foreach ($metadata->getArray('RegistrationInfo') as $riName => $riValues) {
|
||||
switch ($riName) {
|
||||
case 'authority':
|
||||
$ri->setRegistrationAuthority($riValues);
|
||||
break;
|
||||
case 'instant':
|
||||
$ri->setRegistrationInstant(\SAML2\Utils::xsDateTimeToTimestamp($riValues));
|
||||
break;
|
||||
case 'policies':
|
||||
$ri->setRegistrationPolicy($riValues);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->entityDescriptor->setExtensions(
|
||||
array_merge($this->entityDescriptor->getExtensions(), [$ri])
|
||||
);
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('UIInfo')) {
|
||||
$ui = new UIInfo();
|
||||
foreach ($metadata->getArray('UIInfo') as $uiName => $uiValues) {
|
||||
switch ($uiName) {
|
||||
case 'DisplayName':
|
||||
$ui->setDisplayName($uiValues);
|
||||
break;
|
||||
case 'Description':
|
||||
$ui->setDescription($uiValues);
|
||||
break;
|
||||
case 'InformationURL':
|
||||
$ui->setInformationURL($uiValues);
|
||||
break;
|
||||
case 'PrivacyStatementURL':
|
||||
$ui->setPrivacyStatementURL($uiValues);
|
||||
break;
|
||||
case 'Keywords':
|
||||
foreach ($uiValues as $lang => $keywords) {
|
||||
$uiItem = new Keywords();
|
||||
$uiItem->setLanguage($lang);
|
||||
$uiItem->setKeywords($keywords);
|
||||
$ui->addKeyword($uiItem);
|
||||
}
|
||||
break;
|
||||
case 'Logo':
|
||||
foreach ($uiValues as $logo) {
|
||||
$uiItem = new Logo();
|
||||
$uiItem->setUrl($logo['url']);
|
||||
$uiItem->setWidth($logo['width']);
|
||||
$uiItem->setHeight($logo['height']);
|
||||
if (isset($logo['lang'])) {
|
||||
$uiItem->setLanguage($logo['lang']);
|
||||
}
|
||||
$ui->addLogo($uiItem);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
$e->setExtensions(array_merge($e->getExtensions(), [$ui]));
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('DiscoHints')) {
|
||||
$dh = new DiscoHints();
|
||||
foreach ($metadata->getArray('DiscoHints') as $dhName => $dhValues) {
|
||||
switch ($dhName) {
|
||||
case 'IPHint':
|
||||
$dh->setIPHint($dhValues);
|
||||
break;
|
||||
case 'DomainHint':
|
||||
$dh->setDomainHint($dhValues);
|
||||
break;
|
||||
case 'GeolocationHint':
|
||||
$dh->setGeolocationHint($dhValues);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$e->setExtensions(array_merge($e->getExtensions(), [$dh]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add an Organization element based on data passed as parameters
|
||||
*
|
||||
* @param array $orgName An array with the localized OrganizationName.
|
||||
* @param array $orgDisplayName An array with the localized OrganizationDisplayName.
|
||||
* @param array $orgURL An array with the localized OrganizationURL.
|
||||
* @return void
|
||||
*/
|
||||
public function addOrganization(array $orgName, array $orgDisplayName, array $orgURL)
|
||||
{
|
||||
$org = new Organization();
|
||||
|
||||
$org->setOrganizationName($orgName);
|
||||
$org->setOrganizationDisplayName($orgDisplayName);
|
||||
$org->setOrganizationURL($orgURL);
|
||||
|
||||
$this->entityDescriptor->setOrganization($org);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add an Organization element based on metadata array.
|
||||
*
|
||||
* @param array $metadata The metadata we should extract the organization information from.
|
||||
* @return void
|
||||
*/
|
||||
public function addOrganizationInfo(array $metadata)
|
||||
{
|
||||
if (
|
||||
empty($metadata['OrganizationName']) ||
|
||||
empty($metadata['OrganizationDisplayName']) ||
|
||||
empty($metadata['OrganizationURL'])
|
||||
) {
|
||||
// empty or incomplete organization information
|
||||
return;
|
||||
}
|
||||
|
||||
$orgName = Utils\Arrays::arrayize($metadata['OrganizationName'], 'en');
|
||||
$orgDisplayName = Utils\Arrays::arrayize($metadata['OrganizationDisplayName'], 'en');
|
||||
$orgURL = Utils\Arrays::arrayize($metadata['OrganizationURL'], 'en');
|
||||
|
||||
$this->addOrganization($orgName, $orgDisplayName, $orgURL);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a list of endpoints to metadata.
|
||||
*
|
||||
* @param array $endpoints The endpoints.
|
||||
* @param bool $indexed Whether the endpoints should be indexed.
|
||||
*
|
||||
* @return array An array of endpoint objects,
|
||||
* either \SAML2\XML\md\EndpointType or \SAML2\XML\md\IndexedEndpointType.
|
||||
*/
|
||||
private static function createEndpoints(array $endpoints, $indexed)
|
||||
{
|
||||
assert(is_bool($indexed));
|
||||
|
||||
$ret = [];
|
||||
|
||||
foreach ($endpoints as &$ep) {
|
||||
if ($indexed) {
|
||||
$t = new IndexedEndpointType();
|
||||
if (!isset($ep['index'])) {
|
||||
// Find the maximum index
|
||||
$maxIndex = -1;
|
||||
foreach ($endpoints as $ep) {
|
||||
if (!isset($ep['index'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ep['index'] > $maxIndex) {
|
||||
$maxIndex = $ep['index'];
|
||||
}
|
||||
}
|
||||
|
||||
$ep['index'] = $maxIndex + 1;
|
||||
}
|
||||
|
||||
$t->setIndex($ep['index']);
|
||||
} else {
|
||||
$t = new EndpointType();
|
||||
}
|
||||
|
||||
$t->setBinding($ep['Binding']);
|
||||
$t->setLocation($ep['Location']);
|
||||
if (isset($ep['ResponseLocation'])) {
|
||||
$t->setResponseLocation($ep['ResponseLocation']);
|
||||
}
|
||||
if (isset($ep['hoksso:ProtocolBinding'])) {
|
||||
$t->setAttributeNS(
|
||||
Constants::NS_HOK,
|
||||
'hoksso:ProtocolBinding',
|
||||
Constants::BINDING_HTTP_REDIRECT
|
||||
);
|
||||
}
|
||||
|
||||
$ret[] = $t;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add an AttributeConsumingService element to the metadata.
|
||||
*
|
||||
* @param SPSSODescriptor $spDesc The SPSSODescriptor element.
|
||||
* @param Configuration $metadata The metadata.
|
||||
* @return void
|
||||
*/
|
||||
private function addAttributeConsumingService(
|
||||
SPSSODescriptor $spDesc,
|
||||
Configuration $metadata
|
||||
) {
|
||||
$attributes = $metadata->getArray('attributes', []);
|
||||
$name = $metadata->getLocalizedString('name', null);
|
||||
|
||||
if ($name === null || count($attributes) == 0) {
|
||||
// we cannot add an AttributeConsumingService without name and attributes
|
||||
return;
|
||||
}
|
||||
|
||||
$attributesrequired = $metadata->getArray('attributes.required', []);
|
||||
|
||||
/*
|
||||
* Add an AttributeConsumingService element with information as name and description and list
|
||||
* of requested attributes
|
||||
*/
|
||||
$attributeconsumer = new AttributeConsumingService();
|
||||
|
||||
$attributeconsumer->setIndex($metadata->getInteger('attributes.index', 0));
|
||||
|
||||
if ($metadata->hasValue('attributes.isDefault')) {
|
||||
$attributeconsumer->setIsDefault($metadata->getBoolean('attributes.isDefault', false));
|
||||
}
|
||||
|
||||
$attributeconsumer->setServiceName($name);
|
||||
$attributeconsumer->setServiceDescription($metadata->getLocalizedString('description', []));
|
||||
|
||||
$nameFormat = $metadata->getString('attributes.NameFormat', Constants::NAMEFORMAT_UNSPECIFIED);
|
||||
foreach ($attributes as $friendlyName => $attribute) {
|
||||
$t = new RequestedAttribute();
|
||||
$t->setName($attribute);
|
||||
if (!is_int($friendlyName)) {
|
||||
$t->setFriendlyName($friendlyName);
|
||||
}
|
||||
if ($nameFormat !== Constants::NAMEFORMAT_UNSPECIFIED) {
|
||||
$t->setNameFormat($nameFormat);
|
||||
}
|
||||
if (in_array($attribute, $attributesrequired, true)) {
|
||||
$t->setIsRequired(true);
|
||||
}
|
||||
$attributeconsumer->addRequestedAttribute($t);
|
||||
}
|
||||
|
||||
$spDesc->addAttributeConsumingService($attributeconsumer);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a specific type of metadata to an entity.
|
||||
*
|
||||
* @param string $set The metadata set this metadata comes from.
|
||||
* @param array $metadata The metadata.
|
||||
* @return void
|
||||
*/
|
||||
public function addMetadata($set, $metadata)
|
||||
{
|
||||
assert(is_string($set));
|
||||
assert(is_array($metadata));
|
||||
|
||||
$this->setExpiration($metadata);
|
||||
|
||||
switch ($set) {
|
||||
case 'saml20-sp-remote':
|
||||
$this->addMetadataSP20($metadata);
|
||||
break;
|
||||
case 'saml20-idp-remote':
|
||||
$this->addMetadataIdP20($metadata);
|
||||
break;
|
||||
case 'shib13-sp-remote':
|
||||
$this->addMetadataSP11($metadata);
|
||||
break;
|
||||
case 'shib13-idp-remote':
|
||||
$this->addMetadataIdP11($metadata);
|
||||
break;
|
||||
case 'attributeauthority-remote':
|
||||
$this->addAttributeAuthority($metadata);
|
||||
break;
|
||||
default:
|
||||
Logger::warning('Unable to generate metadata for unknown type \'' . $set . '\'.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add SAML 2.0 SP metadata.
|
||||
*
|
||||
* @param array $metadata The metadata.
|
||||
* @param array $protocols The protocols supported. Defaults to \SAML2\Constants::NS_SAMLP.
|
||||
* @return void
|
||||
*/
|
||||
public function addMetadataSP20($metadata, $protocols = [Constants::NS_SAMLP])
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(is_array($protocols));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
|
||||
$e = new SPSSODescriptor();
|
||||
$e->setProtocolSupportEnumeration($protocols);
|
||||
|
||||
if ($metadata->hasValue('saml20.sign.assertion')) {
|
||||
$e->setWantAssertionsSigned($metadata->getBoolean('saml20.sign.assertion'));
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('redirect.validate')) {
|
||||
$e->setAuthnRequestsSigned($metadata->getBoolean('redirect.validate'));
|
||||
} elseif ($metadata->hasValue('validate.authnrequest')) {
|
||||
$e->setAuthnRequestsSigned($metadata->getBoolean('validate.authnrequest'));
|
||||
}
|
||||
|
||||
$this->addExtensions($metadata, $e);
|
||||
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
$e->setSingleLogoutService(self::createEndpoints($metadata->getEndpoints('SingleLogoutService'), false));
|
||||
|
||||
$e->setNameIDFormat($metadata->getArrayizeString('NameIDFormat', []));
|
||||
|
||||
$endpoints = $metadata->getEndpoints('AssertionConsumerService');
|
||||
foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', []) as $acs) {
|
||||
$endpoints[] = [
|
||||
'Binding' => Constants::BINDING_HTTP_ARTIFACT,
|
||||
'Location' => $acs,
|
||||
];
|
||||
}
|
||||
$e->setAssertionConsumerService(self::createEndpoints($endpoints, true));
|
||||
|
||||
$this->addAttributeConsumingService($e, $metadata);
|
||||
|
||||
$this->entityDescriptor->addRoleDescriptor($e);
|
||||
|
||||
foreach ($metadata->getArray('contacts', []) as $contact) {
|
||||
if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
|
||||
$this->addContact($contact['contactType'], Utils\Config\Metadata::getContact($contact));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add metadata of a SAML 2.0 identity provider.
|
||||
*
|
||||
* @param array $metadata The metadata.
|
||||
* @return void
|
||||
*/
|
||||
public function addMetadataIdP20($metadata)
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
|
||||
$e = new IDPSSODescriptor();
|
||||
$e->setProtocolSupportEnumeration(array_merge($e->getProtocolSupportEnumeration(), [Constants::NS_SAMLP]));
|
||||
|
||||
if ($metadata->hasValue('sign.authnrequest')) {
|
||||
$e->setWantAuthnRequestsSigned($metadata->getBoolean('sign.authnrequest'));
|
||||
} elseif ($metadata->hasValue('redirect.sign')) {
|
||||
$e->setWantAuthnRequestsSigned($metadata->getBoolean('redirect.sign'));
|
||||
}
|
||||
|
||||
$this->addExtensions($metadata, $e);
|
||||
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
if ($metadata->hasValue('ArtifactResolutionService')) {
|
||||
$e->setArtifactResolutionService(self::createEndpoints(
|
||||
$metadata->getEndpoints('ArtifactResolutionService'),
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
$e->setSingleLogoutService(self::createEndpoints($metadata->getEndpoints('SingleLogoutService'), false));
|
||||
|
||||
$e->setNameIDFormat($metadata->getArrayizeString('NameIDFormat', []));
|
||||
|
||||
$e->setSingleSignOnService(self::createEndpoints($metadata->getEndpoints('SingleSignOnService'), false));
|
||||
|
||||
$this->entityDescriptor->addRoleDescriptor($e);
|
||||
|
||||
foreach ($metadata->getArray('contacts', []) as $contact) {
|
||||
if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
|
||||
$this->addContact($contact['contactType'], Utils\Config\Metadata::getContact($contact));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add metadata of a SAML 1.1 service provider.
|
||||
*
|
||||
* @param array $metadata The metadata.
|
||||
* @return void
|
||||
*/
|
||||
public function addMetadataSP11($metadata)
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
|
||||
$e = new SPSSODescriptor();
|
||||
$e->setProtocolSupportEnumeration(
|
||||
array_merge(
|
||||
$e->getProtocolSupportEnumeration(),
|
||||
['urn:oasis:names:tc:SAML:1.1:protocol']
|
||||
)
|
||||
);
|
||||
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
$e->setNameIDFormat($metadata->getArrayizeString('NameIDFormat', []));
|
||||
|
||||
$endpoints = $metadata->getEndpoints('AssertionConsumerService');
|
||||
foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', []) as $acs) {
|
||||
$endpoints[] = [
|
||||
'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01',
|
||||
'Location' => $acs,
|
||||
];
|
||||
}
|
||||
$e->setAssertionConsumerService(self::createEndpoints($endpoints, true));
|
||||
|
||||
$this->addAttributeConsumingService($e, $metadata);
|
||||
|
||||
$this->entityDescriptor->addRoleDescriptor($e);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add metadata of a SAML 1.1 identity provider.
|
||||
*
|
||||
* @param array $metadata The metadata.
|
||||
* @return void
|
||||
*/
|
||||
public function addMetadataIdP11($metadata)
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
|
||||
$e = new IDPSSODescriptor();
|
||||
$e->setProtocolSupportEnumeration(
|
||||
array_merge($e->getProtocolSupportEnumeration(), [
|
||||
'urn:oasis:names:tc:SAML:1.1:protocol',
|
||||
'urn:mace:shibboleth:1.0'
|
||||
])
|
||||
);
|
||||
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
$e->setNameIDFormat($metadata->getArrayizeString('NameIDFormat', []));
|
||||
|
||||
$e->setSingleSignOnService(self::createEndpoints($metadata->getEndpoints('SingleSignOnService'), false));
|
||||
|
||||
$this->entityDescriptor->addRoleDescriptor($e);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add metadata of a SAML attribute authority.
|
||||
*
|
||||
* @param array $metadata The AttributeAuthorityDescriptor, in the format returned by
|
||||
* \SimpleSAML\Metadata\SAMLParser.
|
||||
* @return void
|
||||
*/
|
||||
public function addAttributeAuthority(array $metadata)
|
||||
{
|
||||
assert(is_array($metadata));
|
||||
assert(isset($metadata['entityid']));
|
||||
assert(isset($metadata['metadata-set']));
|
||||
|
||||
$metadata = Configuration::loadFromArray($metadata, $metadata['entityid']);
|
||||
|
||||
$e = new AttributeAuthorityDescriptor();
|
||||
$e->setProtocolSupportEnumeration($metadata->getArray('protocols', [Constants::NS_SAMLP]));
|
||||
|
||||
$this->addExtensions($metadata, $e);
|
||||
$this->addCertificate($e, $metadata);
|
||||
|
||||
$e->setAttributeService(self::createEndpoints($metadata->getEndpoints('AttributeService'), false));
|
||||
$e->setAssertionIDRequestService(self::createEndpoints(
|
||||
$metadata->getEndpoints('AssertionIDRequestService'),
|
||||
false
|
||||
));
|
||||
|
||||
$e->setNameIDFormat($metadata->getArrayizeString('NameIDFormat', []));
|
||||
|
||||
$this->entityDescriptor->addRoleDescriptor($e);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add contact information.
|
||||
*
|
||||
* Accepts a contact type, and a contact array that must be previously sanitized.
|
||||
*
|
||||
* WARNING: This function will change its signature and no longer parse a 'name' element.
|
||||
*
|
||||
* @param string $type The type of contact. Deprecated.
|
||||
* @param array $details The details about the contact.
|
||||
*
|
||||
* @return void
|
||||
* @todo Change the signature to remove $type.
|
||||
* @todo Remove the capability to pass a name and parse it inside the method.
|
||||
*/
|
||||
public function addContact($type, $details)
|
||||
{
|
||||
assert(is_string($type));
|
||||
assert(is_array($details));
|
||||
assert(in_array($type, ['technical', 'support', 'administrative', 'billing', 'other'], true));
|
||||
|
||||
// TODO: remove this check as soon as getContact() is called always before calling this function
|
||||
$details = Utils\Config\Metadata::getContact($details);
|
||||
|
||||
$e = new ContactPerson();
|
||||
$e->setContactType($type);
|
||||
|
||||
if (!empty($details['attributes'])) {
|
||||
$e->setContactPersonAttributes($details['attributes']);
|
||||
}
|
||||
|
||||
if (isset($details['company'])) {
|
||||
$e->setCompany($details['company']);
|
||||
}
|
||||
if (isset($details['givenName'])) {
|
||||
$e->setGivenName($details['givenName']);
|
||||
}
|
||||
if (isset($details['surName'])) {
|
||||
$e->setSurName($details['surName']);
|
||||
}
|
||||
|
||||
if (isset($details['emailAddress'])) {
|
||||
$eas = $details['emailAddress'];
|
||||
if (!is_array($eas)) {
|
||||
$eas = [$eas];
|
||||
}
|
||||
foreach ($eas as $ea) {
|
||||
$e->addEmailAddress($ea);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($details['telephoneNumber'])) {
|
||||
$tlfNrs = $details['telephoneNumber'];
|
||||
if (!is_array($tlfNrs)) {
|
||||
$tlfNrs = [$tlfNrs];
|
||||
}
|
||||
foreach ($tlfNrs as $tlfNr) {
|
||||
$e->addTelephoneNumber($tlfNr);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityDescriptor->addContactPerson($e);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a KeyDescriptor with an X509 certificate.
|
||||
*
|
||||
* @param RoleDescriptor $rd The RoleDescriptor the certificate should be added to.
|
||||
* @param string $use The value of the 'use' attribute.
|
||||
* @param string $x509data The certificate data.
|
||||
* @return void
|
||||
*/
|
||||
private function addX509KeyDescriptor(RoleDescriptor $rd, $use, $x509data)
|
||||
{
|
||||
assert(in_array($use, ['encryption', 'signing'], true));
|
||||
assert(is_string($x509data));
|
||||
|
||||
$keyDescriptor = \SAML2\Utils::createKeyDescriptor($x509data);
|
||||
$keyDescriptor->setUse($use);
|
||||
$rd->addKeyDescriptor($keyDescriptor);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a certificate.
|
||||
*
|
||||
* Helper function for adding a certificate to the metadata.
|
||||
*
|
||||
* @param RoleDescriptor $rd The RoleDescriptor the certificate should be added to.
|
||||
* @param Configuration $metadata The metadata of the entity.
|
||||
* @return void
|
||||
*/
|
||||
private function addCertificate(RoleDescriptor $rd, Configuration $metadata)
|
||||
{
|
||||
$keys = $metadata->getPublicKeys();
|
||||
foreach ($keys as $key) {
|
||||
if ($key['type'] !== 'X509Certificate') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($key['signing']) || $key['signing'] === true) {
|
||||
$this->addX509KeyDescriptor($rd, 'signing', $key['X509Certificate']);
|
||||
}
|
||||
if (!isset($key['encryption']) || $key['encryption'] === true) {
|
||||
$this->addX509KeyDescriptor($rd, 'encryption', $key['X509Certificate']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($metadata->hasValue('https.certData')) {
|
||||
$this->addX509KeyDescriptor($rd, 'signing', $metadata->getString('https.certData'));
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,302 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Metadata;
|
||||
|
||||
use DOMElement;
|
||||
use Exception;
|
||||
use RobRichards\XMLSecLibs\XMLSecurityKey;
|
||||
use RobRichards\XMLSecLibs\XMLSecurityDSig;
|
||||
use SAML\Error\CriticalConfigurationError;
|
||||
use SAML2\DOMDocumentFactory;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* This class implements a helper function for signing of metadata.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class Signer
|
||||
{
|
||||
/**
|
||||
* This functions finds what key & certificate files should be used to sign the metadata
|
||||
* for the given entity.
|
||||
*
|
||||
* @param Configuration $config Our \SimpleSAML\Configuration instance.
|
||||
* @param array $entityMetadata The metadata of the entity.
|
||||
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or
|
||||
* 'Shib 1.3 SP'.
|
||||
*
|
||||
* @return array An associative array with the keys 'privatekey', 'certificate', and optionally 'privatekey_pass'.
|
||||
* @throws Exception If the key and certificate used to sign is unknown.
|
||||
*/
|
||||
private static function findKeyCert($config, $entityMetadata, $type)
|
||||
{
|
||||
// first we look for metadata.privatekey and metadata.certificate in the metadata
|
||||
if (
|
||||
array_key_exists('metadata.sign.privatekey', $entityMetadata)
|
||||
|| array_key_exists('metadata.sign.certificate', $entityMetadata)
|
||||
) {
|
||||
if (
|
||||
!array_key_exists('metadata.sign.privatekey', $entityMetadata)
|
||||
|| !array_key_exists('metadata.sign.certificate', $entityMetadata)
|
||||
) {
|
||||
throw new Exception(
|
||||
'Missing either the "metadata.sign.privatekey" or the' .
|
||||
' "metadata.sign.certificate" configuration option in the metadata for' .
|
||||
' the ' . $type . ' "' . $entityMetadata['entityid'] . '". If one of' .
|
||||
' these options is specified, then the other must also be specified.'
|
||||
);
|
||||
}
|
||||
|
||||
$ret = [
|
||||
'privatekey' => $entityMetadata['metadata.sign.privatekey'],
|
||||
'certificate' => $entityMetadata['metadata.sign.certificate']
|
||||
];
|
||||
|
||||
if (array_key_exists('metadata.sign.privatekey_pass', $entityMetadata)) {
|
||||
$ret['privatekey_pass'] = $entityMetadata['metadata.sign.privatekey_pass'];
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
// then we look for default values in the global configuration
|
||||
$privatekey = $config->getString('metadata.sign.privatekey', null);
|
||||
$certificate = $config->getString('metadata.sign.certificate', null);
|
||||
if ($privatekey !== null || $certificate !== null) {
|
||||
if ($privatekey === null || $certificate === null) {
|
||||
throw new Exception(
|
||||
'Missing either the "metadata.sign.privatekey" or the' .
|
||||
' "metadata.sign.certificate" configuration option in the global' .
|
||||
' configuration. If one of these options is specified, then the other' .
|
||||
' must also be specified.'
|
||||
);
|
||||
}
|
||||
$ret = ['privatekey' => $privatekey, 'certificate' => $certificate];
|
||||
|
||||
$privatekey_pass = $config->getString('metadata.sign.privatekey_pass', null);
|
||||
if ($privatekey_pass !== null) {
|
||||
$ret['privatekey_pass'] = $privatekey_pass;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
// as a last resort we attempt to use the privatekey and certificate option from the metadata
|
||||
if (
|
||||
array_key_exists('privatekey', $entityMetadata)
|
||||
|| array_key_exists('certificate', $entityMetadata)
|
||||
) {
|
||||
if (
|
||||
!array_key_exists('privatekey', $entityMetadata)
|
||||
|| !array_key_exists('certificate', $entityMetadata)
|
||||
) {
|
||||
throw new Exception(
|
||||
'Both the "privatekey" and the "certificate" option must' .
|
||||
' be set in the metadata for the ' . $type . ' "' .
|
||||
$entityMetadata['entityid'] . '" before it is possible to sign metadata' .
|
||||
' from this entity.'
|
||||
);
|
||||
}
|
||||
|
||||
$ret = [
|
||||
'privatekey' => $entityMetadata['privatekey'],
|
||||
'certificate' => $entityMetadata['certificate']
|
||||
];
|
||||
|
||||
if (array_key_exists('privatekey_pass', $entityMetadata)) {
|
||||
$ret['privatekey_pass'] = $entityMetadata['privatekey_pass'];
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
'Could not find what key & certificate should be used to sign the metadata' .
|
||||
' for the ' . $type . ' "' . $entityMetadata['entityid'] . '".'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine whether metadata signing is enabled for the given metadata.
|
||||
*
|
||||
* @param Configuration $config Our \SimpleSAML\Configuration instance.
|
||||
* @param array $entityMetadata The metadata of the entity.
|
||||
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or
|
||||
* 'Shib 1.3 SP'.
|
||||
*
|
||||
* @return boolean True if metadata signing is enabled, false otherwise.
|
||||
* @throws Exception If the value of the 'metadata.sign.enable' option is not a boolean.
|
||||
*/
|
||||
private static function isMetadataSigningEnabled($config, $entityMetadata, $type)
|
||||
{
|
||||
// first check the metadata for the entity
|
||||
if (array_key_exists('metadata.sign.enable', $entityMetadata)) {
|
||||
if (!is_bool($entityMetadata['metadata.sign.enable'])) {
|
||||
throw new Exception(
|
||||
'Invalid value for the "metadata.sign.enable" configuration option for' .
|
||||
' the ' . $type . ' "' . $entityMetadata['entityid'] . '". This option' .
|
||||
' should be a boolean.'
|
||||
);
|
||||
}
|
||||
|
||||
return $entityMetadata['metadata.sign.enable'];
|
||||
}
|
||||
|
||||
$enabled = $config->getBoolean('metadata.sign.enable', false);
|
||||
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine the signature and digest algorithms to use when signing metadata.
|
||||
*
|
||||
* This method will look for the 'metadata.sign.algorithm' key in the $entityMetadata array, or look for such
|
||||
* a configuration option in the $config object.
|
||||
*
|
||||
* @param Configuration $config The global configuration.
|
||||
* @param array $entityMetadata An array containing the metadata related to this entity.
|
||||
* @param string $type A string describing the type of entity. E.g. 'SAML 2 IdP' or 'Shib 1.3 SP'.
|
||||
*
|
||||
* @return array An array with two keys, 'algorithm' and 'digest', corresponding to the signature and digest
|
||||
* algorithms to use, respectively.
|
||||
*
|
||||
* @throws CriticalConfigurationError
|
||||
*/
|
||||
private static function getMetadataSigningAlgorithm($config, $entityMetadata, $type)
|
||||
{
|
||||
// configure the algorithm to use
|
||||
if (array_key_exists('metadata.sign.algorithm', $entityMetadata)) {
|
||||
if (!is_string($entityMetadata['metadata.sign.algorithm'])) {
|
||||
throw new CriticalConfigurationError(
|
||||
"Invalid value for the 'metadata.sign.algorithm' configuration option for the " . $type .
|
||||
"'" . $entityMetadata['entityid'] . "'. This option has restricted values"
|
||||
);
|
||||
}
|
||||
$alg = $entityMetadata['metadata.sign.algorithm'];
|
||||
} else {
|
||||
$alg = $config->getString('metadata.sign.algorithm', XMLSecurityKey::RSA_SHA256);
|
||||
}
|
||||
|
||||
$supported_algs = [
|
||||
XMLSecurityKey::RSA_SHA1,
|
||||
XMLSecurityKey::RSA_SHA256,
|
||||
XMLSecurityKey::RSA_SHA384,
|
||||
XMLSecurityKey::RSA_SHA512,
|
||||
];
|
||||
|
||||
if (!in_array($alg, $supported_algs, true)) {
|
||||
throw new CriticalConfigurationError("Unknown signature algorithm '$alg'");
|
||||
}
|
||||
|
||||
switch ($alg) {
|
||||
case XMLSecurityKey::RSA_SHA256:
|
||||
$digest = XMLSecurityDSig::SHA256;
|
||||
break;
|
||||
case XMLSecurityKey::RSA_SHA384:
|
||||
$digest = XMLSecurityDSig::SHA384;
|
||||
break;
|
||||
case XMLSecurityKey::RSA_SHA512:
|
||||
$digest = XMLSecurityDSig::SHA512;
|
||||
break;
|
||||
default:
|
||||
$digest = XMLSecurityDSig::SHA1;
|
||||
}
|
||||
|
||||
return [
|
||||
'algorithm' => $alg,
|
||||
'digest' => $digest,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Signs the given metadata if metadata signing is enabled.
|
||||
*
|
||||
* @param string $metadataString A string with the metadata.
|
||||
* @param array $entityMetadata The metadata of the entity.
|
||||
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or 'Shib 1.3 SP'.
|
||||
*
|
||||
* @return string The $metadataString with the signature embedded.
|
||||
* @throws Exception If the certificate or private key cannot be loaded, or the metadata doesn't parse properly.
|
||||
*/
|
||||
public static function sign($metadataString, $entityMetadata, $type)
|
||||
{
|
||||
$config = Configuration::getInstance();
|
||||
|
||||
// check if metadata signing is enabled
|
||||
if (!self::isMetadataSigningEnabled($config, $entityMetadata, $type)) {
|
||||
return $metadataString;
|
||||
}
|
||||
|
||||
// find the key & certificate which should be used to sign the metadata
|
||||
$keyCertFiles = self::findKeyCert($config, $entityMetadata, $type);
|
||||
|
||||
$keyFile = Utils\Config::getCertPath($keyCertFiles['privatekey']);
|
||||
if (!file_exists($keyFile)) {
|
||||
throw new Exception(
|
||||
'Could not find private key file [' . $keyFile . '], which is needed to sign the metadata'
|
||||
);
|
||||
}
|
||||
$keyData = file_get_contents($keyFile);
|
||||
|
||||
$certFile = Utils\Config::getCertPath($keyCertFiles['certificate']);
|
||||
if (!file_exists($certFile)) {
|
||||
throw new Exception(
|
||||
'Could not find certificate file [' . $certFile . '], which is needed to sign the metadata'
|
||||
);
|
||||
}
|
||||
$certData = file_get_contents($certFile);
|
||||
|
||||
|
||||
// convert the metadata to a DOM tree
|
||||
try {
|
||||
$xml = DOMDocumentFactory::fromString($metadataString);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('Error parsing self-generated metadata.');
|
||||
}
|
||||
|
||||
$signature_cf = self::getMetadataSigningAlgorithm($config, $entityMetadata, $type);
|
||||
|
||||
// load the private key
|
||||
$objKey = new XMLSecurityKey($signature_cf['algorithm'], ['type' => 'private']);
|
||||
if (array_key_exists('privatekey_pass', $keyCertFiles)) {
|
||||
$objKey->passphrase = $keyCertFiles['privatekey_pass'];
|
||||
}
|
||||
$objKey->loadKey($keyData, false);
|
||||
|
||||
// get the EntityDescriptor node we should sign
|
||||
/** @var DOMElement $rootNode */
|
||||
$rootNode = $xml->firstChild;
|
||||
$rootNode->setAttribute('ID', '_' . hash('sha256', $metadataString));
|
||||
|
||||
// sign the metadata with our private key
|
||||
$objXMLSecDSig = new XMLSecurityDSig();
|
||||
|
||||
$objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
|
||||
|
||||
$objXMLSecDSig->addReferenceList(
|
||||
[$rootNode],
|
||||
$signature_cf['digest'],
|
||||
['http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N],
|
||||
['id_name' => 'ID', 'overwrite' => false]
|
||||
);
|
||||
|
||||
$objXMLSecDSig->sign($objKey);
|
||||
|
||||
// add the certificate to the signature
|
||||
$objXMLSecDSig->add509Cert($certData, true);
|
||||
|
||||
// add the signature to the metadata
|
||||
$objXMLSecDSig->insertSignature($rootNode, $rootNode->firstChild);
|
||||
|
||||
// return the DOM tree as a string
|
||||
return $xml->saveXML();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Metadata\Sources;
|
||||
|
||||
use Exception;
|
||||
use RobRichards\XMLSecLibs\XMLSecurityDSig;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Logger;
|
||||
use SAML\Metadata\MetaDataStorageSource;
|
||||
use SAML\Metadata\SAMLParser;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* This class implements SAML Metadata Query Protocol
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS.
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @author Tamas Frank, NIIFI
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class MDQ extends MetaDataStorageSource
|
||||
{
|
||||
/**
|
||||
* The URL of MDQ server (url:port)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $server;
|
||||
|
||||
/**
|
||||
* The fingerprint of the certificate used to sign the metadata. You don't need this option if you don't want to
|
||||
* validate the signature on the metadata.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $validateFingerprint;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $validateFingerprintAlgorithm;
|
||||
|
||||
/**
|
||||
* The cache directory, or null if no cache directory is configured.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $cacheDir;
|
||||
|
||||
|
||||
/**
|
||||
* The maximum cache length, in seconds.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
private $cacheLength;
|
||||
|
||||
|
||||
/**
|
||||
* This function initializes the dynamic XML metadata source.
|
||||
*
|
||||
* Options:
|
||||
* - 'server': URL of the MDQ server (url:port). Mandatory.
|
||||
* - 'validateFingerprint': The fingerprint of the certificate used to sign the metadata.
|
||||
* You don't need this option if you don't want to validate the signature on the metadata.
|
||||
* Optional.
|
||||
* - 'cachedir': Directory where metadata can be cached. Optional.
|
||||
* - 'cachelength': Maximum time metadata cah be cached, in seconds. Default to 24
|
||||
* hours (86400 seconds).
|
||||
*
|
||||
* @param array $config The configuration for this instance of the XML metadata source.
|
||||
*
|
||||
* @throws Exception If no server option can be found in the configuration.
|
||||
*/
|
||||
protected function __construct($config)
|
||||
{
|
||||
assert(is_array($config));
|
||||
|
||||
if (!array_key_exists('server', $config)) {
|
||||
throw new Exception(__CLASS__ . ": the 'server' configuration option is not set.");
|
||||
} else {
|
||||
$this->server = $config['server'];
|
||||
}
|
||||
|
||||
if (array_key_exists('validateFingerprint', $config)) {
|
||||
$this->validateFingerprint = $config['validateFingerprint'];
|
||||
} else {
|
||||
$this->validateFingerprint = null;
|
||||
}
|
||||
if (isset($config['validateFingerprintAlgorithm'])) {
|
||||
$this->validateFingerprintAlgorithm = $config['validateFingerprintAlgorithm'];
|
||||
} else {
|
||||
$this->validateFingerprintAlgorithm = XMLSecurityDSig::SHA1;
|
||||
}
|
||||
|
||||
if (array_key_exists('cachedir', $config)) {
|
||||
$globalConfig = Configuration::getInstance();
|
||||
$this->cacheDir = $globalConfig->resolvePath($config['cachedir']);
|
||||
} else {
|
||||
$this->cacheDir = null;
|
||||
}
|
||||
|
||||
if (array_key_exists('cachelength', $config)) {
|
||||
$this->cacheLength = $config['cachelength'];
|
||||
} else {
|
||||
$this->cacheLength = 86400;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function is not implemented.
|
||||
*
|
||||
* @param string $set The set we want to list metadata for.
|
||||
*
|
||||
* @return array An empty array.
|
||||
*/
|
||||
public function getMetadataSet($set)
|
||||
{
|
||||
// we don't have this metadata set
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the cache file name for an entity,
|
||||
*
|
||||
* @param string $set The metadata set this entity belongs to.
|
||||
* @param string $entityId The entity id of this entity.
|
||||
*
|
||||
* @return string The full path to the cache file.
|
||||
*/
|
||||
private function getCacheFilename($set, $entityId)
|
||||
{
|
||||
assert(is_string($set));
|
||||
assert(is_string($entityId));
|
||||
|
||||
if ($this->cacheDir === null) {
|
||||
throw new Error\ConfigurationError("Missing cache directory configuration.");
|
||||
}
|
||||
|
||||
$cachekey = sha1($entityId);
|
||||
return $this->cacheDir . '/' . $set . '-' . $cachekey . '.cached.xml';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load a entity from the cache.
|
||||
*
|
||||
* @param string $set The metadata set this entity belongs to.
|
||||
* @param string $entityId The entity id of this entity.
|
||||
*
|
||||
* @return array|NULL The associative array with the metadata for this entity, or NULL
|
||||
* if the entity could not be found.
|
||||
* @throws Exception If an error occurs while loading metadata from cache.
|
||||
*/
|
||||
private function getFromCache($set, $entityId)
|
||||
{
|
||||
assert(is_string($set));
|
||||
assert(is_string($entityId));
|
||||
|
||||
if (empty($this->cacheDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cachefilename = $this->getCacheFilename($set, $entityId);
|
||||
if (!file_exists($cachefilename)) {
|
||||
return null;
|
||||
}
|
||||
if (!is_readable($cachefilename)) {
|
||||
throw new Exception(__CLASS__ . ': could not read cache file for entity [' . $cachefilename . ']');
|
||||
}
|
||||
Logger::debug(__CLASS__ . ': reading cache [' . $entityId . '] => [' . $cachefilename . ']');
|
||||
|
||||
/* Ensure that this metadata isn't older that the cachelength option allows. This
|
||||
* must be verified based on the file, since this option may be changed after the
|
||||
* file is written.
|
||||
*/
|
||||
$stat = stat($cachefilename);
|
||||
if ($stat['mtime'] + $this->cacheLength <= time()) {
|
||||
Logger::debug(__CLASS__ . ': cache file older that the cachelength option allows.');
|
||||
return null;
|
||||
}
|
||||
|
||||
$rawData = file_get_contents($cachefilename);
|
||||
if (empty($rawData)) {
|
||||
/** @var array $error */
|
||||
$error = error_get_last();
|
||||
throw new Exception(
|
||||
__CLASS__ . ': error reading metadata from cache file "' . $cachefilename . '": ' . $error['message']
|
||||
);
|
||||
}
|
||||
|
||||
$data = unserialize($rawData);
|
||||
if ($data === false) {
|
||||
throw new Exception(__CLASS__ . ': error unserializing cached data from file "' . $cachefilename . '".');
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
throw new Exception(__CLASS__ . ': Cached metadata from "' . $cachefilename . '" wasn\'t an array.');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save a entity to the cache.
|
||||
*
|
||||
* @param string $set The metadata set this entity belongs to.
|
||||
* @param string $entityId The entity id of this entity.
|
||||
* @param array $data The associative array with the metadata for this entity.
|
||||
*
|
||||
* @throws Exception If metadata cannot be written to cache.
|
||||
* @return void
|
||||
*/
|
||||
private function writeToCache($set, $entityId, $data)
|
||||
{
|
||||
assert(is_string($set));
|
||||
assert(is_string($entityId));
|
||||
assert(is_array($data));
|
||||
|
||||
if (empty($this->cacheDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cachefilename = $this->getCacheFilename($set, $entityId);
|
||||
if (!is_writable(dirname($cachefilename))) {
|
||||
throw new Exception(__CLASS__ . ': could not write cache file for entity [' . $cachefilename . ']');
|
||||
}
|
||||
Logger::debug(__CLASS__ . ': Writing cache [' . $entityId . '] => [' . $cachefilename . ']');
|
||||
file_put_contents($cachefilename, serialize($data));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve metadata for the correct set from a SAML2Parser.
|
||||
*
|
||||
* @param SAMLParser $entity A SAML2Parser representing an entity.
|
||||
* @param string $set The metadata set we are looking for.
|
||||
*
|
||||
* @return array|NULL The associative array with the metadata, or NULL if no metadata for
|
||||
* the given set was found.
|
||||
*/
|
||||
private static function getParsedSet(SAMLParser $entity, $set)
|
||||
{
|
||||
assert(is_string($set));
|
||||
|
||||
switch ($set) {
|
||||
case 'saml20-idp-remote':
|
||||
return $entity->getMetadata20IdP();
|
||||
case 'saml20-sp-remote':
|
||||
return $entity->getMetadata20SP();
|
||||
case 'shib13-idp-remote':
|
||||
return $entity->getMetadata1xIdP();
|
||||
case 'shib13-sp-remote':
|
||||
return $entity->getMetadata1xSP();
|
||||
case 'attributeauthority-remote':
|
||||
$ret = $entity->getAttributeAuthorities();
|
||||
return $ret[0];
|
||||
|
||||
default:
|
||||
Logger::warning(__CLASS__ . ': unknown metadata set: \'' . $set . '\'.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overriding this function from the superclass \SimpleSAML\Metadata\MetaDataStorageSource.
|
||||
*
|
||||
* This function retrieves metadata for the given entity id in the given set of metadata.
|
||||
* It will return NULL if it is unable to locate the metadata.
|
||||
*
|
||||
* This class implements this function using the getMetadataSet-function. A subclass should
|
||||
* override this function if it doesn't implement the getMetadataSet function, or if the
|
||||
* implementation of getMetadataSet is slow.
|
||||
*
|
||||
* @param string $index The entityId or metaindex we are looking up.
|
||||
* @param string $set The set we are looking for metadata in.
|
||||
*
|
||||
* @return array|null An associative array with metadata for the given entity, or NULL if we are unable to
|
||||
* locate the entity.
|
||||
* @throws Exception If an error occurs while validating the signature or the metadata is in an
|
||||
* incorrect set.
|
||||
*/
|
||||
public function getMetaData($index, $set)
|
||||
{
|
||||
assert(is_string($index));
|
||||
assert(is_string($set));
|
||||
|
||||
Logger::info(__CLASS__ . ': loading metadata entity [' . $index . '] from [' . $set . ']');
|
||||
|
||||
// read from cache if possible
|
||||
try {
|
||||
$data = $this->getFromCache($set, $index);
|
||||
} catch (Exception $e) {
|
||||
Logger::error($e->getMessage());
|
||||
// proceed with fetching metadata even if the cache is broken
|
||||
$data = null;
|
||||
}
|
||||
|
||||
if ($data !== null && array_key_exists('expires', $data) && $data['expires'] < time()) {
|
||||
// metadata has expired
|
||||
$data = null;
|
||||
}
|
||||
|
||||
if (isset($data)) {
|
||||
// metadata found in cache and not expired
|
||||
Logger::debug(__CLASS__ . ': using cached metadata for: ' . $index . '.');
|
||||
return $data;
|
||||
}
|
||||
|
||||
// look at Metadata Query Protocol: https://github.com/iay/md-query/blob/master/draft-young-md-query.txt
|
||||
$mdq_url = $this->server . '/entities/' . urlencode($index);
|
||||
|
||||
Logger::debug(__CLASS__ . ': downloading metadata for "' . $index . '" from [' . $mdq_url . ']');
|
||||
try {
|
||||
$xmldata = Utils\HTTP::fetch($mdq_url);
|
||||
} catch (Exception $e) {
|
||||
// Avoid propagating the exception, make sure we can handle the error later
|
||||
$xmldata = false;
|
||||
}
|
||||
|
||||
if (empty($xmldata)) {
|
||||
$error = error_get_last();
|
||||
Logger::info('Unable to fetch metadata for "' . $index . '" from ' . $mdq_url . ': ' .
|
||||
(is_array($error) ? $error['message'] : 'no error available'));
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var string $xmldata */
|
||||
$entity = SAMLParser::parseString($xmldata);
|
||||
Logger::debug(__CLASS__ . ': completed parsing of [' . $mdq_url . ']');
|
||||
|
||||
if ($this->validateFingerprint !== null) {
|
||||
if (
|
||||
!$entity->validateFingerprint(
|
||||
$this->validateFingerprint,
|
||||
$this->validateFingerprintAlgorithm
|
||||
)
|
||||
) {
|
||||
throw new Exception(__CLASS__ . ': error, could not verify signature for entity: ' . $index . '".');
|
||||
}
|
||||
}
|
||||
|
||||
$data = self::getParsedSet($entity, $set);
|
||||
if ($data === null) {
|
||||
throw new Exception(__CLASS__ . ': no metadata for set "' . $set . '" available from "' . $index . '".');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->writeToCache($set, $index, $data);
|
||||
} catch (Exception $e) {
|
||||
// Proceed without writing to cache
|
||||
Logger::error('Error writing MDQ result to cache: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function loads the metadata for entity IDs in $entityIds. It is returned as an associative array
|
||||
* where the key is the entity id. An empty array may be returned if no matching entities were found
|
||||
* @param array $entityIds The entity ids to load
|
||||
* @param string $set The set we want to get metadata from.
|
||||
* @return array An associative array with the metadata for the requested entities, if found.
|
||||
*/
|
||||
public function getMetaDataForEntities(array $entityIds, $set)
|
||||
{
|
||||
return $this->getMetaDataForEntitiesIndividually($entityIds, $set);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,580 @@
|
|||
<?php
|
||||
|
||||
namespace SAML;
|
||||
|
||||
use SAML\Error\Exception;
|
||||
use SAML\HTTP\Router;
|
||||
use SAML\Utils;
|
||||
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Helper class for accessing information about modules.
|
||||
*
|
||||
* @author Olav Morken <olav.morken@uninett.no>, UNINETT AS.
|
||||
* @author Boy Baukema, SURFnet.
|
||||
* @author Jaime Perez <jaime.perez@uninett.no>, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class Module
|
||||
{
|
||||
/**
|
||||
* Index pages: file names to attempt when accessing directories.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $indexFiles = ['index.php', 'index.html', 'index.htm', 'index.txt'];
|
||||
|
||||
/**
|
||||
* MIME Types
|
||||
*
|
||||
* The key is the file extension and the value the corresponding MIME type.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $mimeTypes = [
|
||||
'bmp' => 'image/x-ms-bmp',
|
||||
'css' => 'text/css',
|
||||
'gif' => 'image/gif',
|
||||
'htm' => 'text/html',
|
||||
'html' => 'text/html',
|
||||
'shtml' => 'text/html',
|
||||
'ico' => 'image/vnd.microsoft.icon',
|
||||
'jpe' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'jpg' => 'image/jpeg',
|
||||
'js' => 'text/javascript',
|
||||
'pdf' => 'application/pdf',
|
||||
'png' => 'image/png',
|
||||
'svg' => 'image/svg+xml',
|
||||
'svgz' => 'image/svg+xml',
|
||||
'swf' => 'application/x-shockwave-flash',
|
||||
'swfl' => 'application/x-shockwave-flash',
|
||||
'txt' => 'text/plain',
|
||||
'xht' => 'application/xhtml+xml',
|
||||
'xhtml' => 'application/xhtml+xml',
|
||||
];
|
||||
|
||||
/**
|
||||
* A list containing the modules currently installed.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [];
|
||||
|
||||
/**
|
||||
* A cache containing specific information for modules, like whether they are enabled or not, or their hooks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $module_info = [];
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the base directory for a module.
|
||||
*
|
||||
* The returned path name will be an absolute path.
|
||||
*
|
||||
* @param string $module Name of the module
|
||||
*
|
||||
* @return string The base directory of a module.
|
||||
*/
|
||||
public static function getModuleDir($module)
|
||||
{
|
||||
$baseDir = dirname(dirname(dirname(__FILE__))) . '/modules';
|
||||
$moduleDir = $baseDir . '/' . $module;
|
||||
|
||||
return $moduleDir;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine whether a module is enabled.
|
||||
*
|
||||
* Will return false if the given module doesn't exist.
|
||||
*
|
||||
* @param string $module Name of the module
|
||||
*
|
||||
* @return bool True if the given module is enabled, false otherwise.
|
||||
*
|
||||
* @throws \Exception If module.enable is set and is not boolean.
|
||||
*/
|
||||
public static function isModuleEnabled($module)
|
||||
{
|
||||
$config = Configuration::getOptionalConfig();
|
||||
return self::isModuleEnabledWithConf($module, $config->getArray('module.enable', []));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for module requests.
|
||||
*
|
||||
* This controller receives requests for pages hosted by modules, and processes accordingly. Depending on the
|
||||
* configuration and the actual request, it will run a PHP script and exit, or return a Response produced either
|
||||
* by another controller or by a static file.
|
||||
*
|
||||
* @param Request|null $request The request to process. Defaults to the current one.
|
||||
*
|
||||
* @return Response|BinaryFileResponse Returns a Response object that can be sent to the browser.
|
||||
* @throws Error\BadRequest In case the request URI is malformed.
|
||||
* @throws Error\NotFound In case the request URI is invalid or the resource it points to cannot be found.
|
||||
*/
|
||||
public static function process(Request $request = null)
|
||||
{
|
||||
if ($request === null) {
|
||||
$request = Request::createFromGlobals();
|
||||
}
|
||||
|
||||
if ($request->server->get('PATH_INFO') === '/') {
|
||||
throw new Error\NotFound('No PATH_INFO to module.php');
|
||||
}
|
||||
|
||||
$url = $request->server->get('PATH_INFO');
|
||||
assert(substr($url, 0, 1) === '/');
|
||||
|
||||
/* clear the PATH_INFO option, so that a script can detect whether it is called with anything following the
|
||||
*'.php'-ending.
|
||||
*/
|
||||
unset($_SERVER['PATH_INFO']);
|
||||
|
||||
$modEnd = strpos($url, '/', 1);
|
||||
if ($modEnd === false) {
|
||||
// the path must always be on the form /module/
|
||||
throw new Error\NotFound('The URL must at least contain a module name followed by a slash.');
|
||||
}
|
||||
|
||||
$module = substr($url, 1, $modEnd - 1);
|
||||
$url = substr($url, $modEnd + 1);
|
||||
if ($url === false) {
|
||||
$url = '';
|
||||
}
|
||||
|
||||
if (!self::isModuleEnabled($module)) {
|
||||
throw new Error\NotFound('The module \'' . $module . '\' was either not found, or wasn\'t enabled.');
|
||||
}
|
||||
|
||||
/* Make sure that the request isn't suspicious (contains references to current directory or parent directory or
|
||||
* anything like that. Searching for './' in the URL will detect both '../' and './'. Searching for '\' will
|
||||
* detect attempts to use Windows-style paths.
|
||||
*/
|
||||
if (strpos($url, '\\') !== false) {
|
||||
throw new Error\BadRequest('Requested URL contained a backslash.');
|
||||
} elseif (strpos($url, './') !== false) {
|
||||
throw new Error\BadRequest('Requested URL contained \'./\'.');
|
||||
}
|
||||
|
||||
$config = Configuration::getInstance();
|
||||
|
||||
// rebuild REQUEST_URI and SCRIPT_NAME just in case we need to. This is needed for server aliases and rewrites
|
||||
$translated_uri = $config->getBasePath() . 'module.php/' . $module . '/' . $url;
|
||||
$request->server->set('REQUEST_URI', $translated_uri);
|
||||
$request->server->set('SCRIPT_NAME', $config->getBasePath() . 'module.php');
|
||||
$request_files = array_filter(
|
||||
$request->files->all(),
|
||||
function ($val) {
|
||||
return !is_null($val);
|
||||
}
|
||||
);
|
||||
$request->initialize(
|
||||
$request->query->all(),
|
||||
$request->request->all(),
|
||||
$request->attributes->all(),
|
||||
$request->cookies->all(),
|
||||
$request_files,
|
||||
$request->server->all(),
|
||||
$request->getContent()
|
||||
);
|
||||
|
||||
if ($config->getBoolean('usenewui', false) === true) {
|
||||
$router = new Router($module);
|
||||
try {
|
||||
return $router->process($request);
|
||||
} catch (FileLocatorFileNotFoundException $e) {
|
||||
// no routes configured for this module, fall back to the old system
|
||||
} catch (NotFoundHttpException $e) {
|
||||
// this module has been migrated, but the route wasn't found
|
||||
}
|
||||
}
|
||||
|
||||
$moduleDir = self::getModuleDir($module) . '/www/';
|
||||
|
||||
// check for '.php/' in the path, the presence of which indicates that another php-script should handle the
|
||||
// request
|
||||
for ($phpPos = strpos($url, '.php/'); $phpPos !== false; $phpPos = strpos($url, '.php/', $phpPos + 1)) {
|
||||
$newURL = substr($url, 0, $phpPos + 4);
|
||||
$param = substr($url, $phpPos + 4);
|
||||
|
||||
if (is_file($moduleDir . $newURL)) {
|
||||
/* $newPath points to a normal file. Point execution to that file, and save the remainder of the path
|
||||
* in PATH_INFO.
|
||||
*/
|
||||
$url = $newURL;
|
||||
$request->server->set('PATH_INFO', $param);
|
||||
$_SERVER['PATH_INFO'] = $param;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$path = $moduleDir . $url;
|
||||
|
||||
if ($path[strlen($path) - 1] === '/') {
|
||||
// path ends with a slash - directory reference. Attempt to find index file in directory
|
||||
foreach (self::$indexFiles as $if) {
|
||||
if (file_exists($path . $if)) {
|
||||
$path .= $if;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (is_dir($path)) {
|
||||
/* Path is a directory - maybe no index file was found in the previous step, or maybe the path didn't end
|
||||
* with a slash. Either way, we don't do directory listings.
|
||||
*/
|
||||
throw new Error\NotFound('Directory listing not available.');
|
||||
}
|
||||
|
||||
if (!file_exists($path)) {
|
||||
// file not found
|
||||
Logger::info('Could not find file \'' . $path . '\'.');
|
||||
throw new Error\NotFound('The URL wasn\'t found in the module.');
|
||||
}
|
||||
|
||||
if (mb_strtolower(substr($path, -4), 'UTF-8') === '.php') {
|
||||
// PHP file - attempt to run it
|
||||
|
||||
/* In some environments, $_SERVER['SCRIPT_NAME'] is already set with $_SERVER['PATH_INFO']. Check for that
|
||||
* case, and append script name only if necessary.
|
||||
*
|
||||
* Contributed by Travis Hegner.
|
||||
*/
|
||||
$script = "/$module/$url";
|
||||
if (strpos($request->getScriptName(), $script) === false) {
|
||||
$request->server->set('SCRIPT_NAME', $request->getScriptName() . '/' . $module . '/' . $url);
|
||||
}
|
||||
|
||||
require($path);
|
||||
exit();
|
||||
}
|
||||
|
||||
// some other file type - attempt to serve it
|
||||
|
||||
// find MIME type for file, based on extension
|
||||
$contentType = null;
|
||||
if (preg_match('#\.([^/\.]+)$#D', $path, $type)) {
|
||||
$type = strtolower($type[1]);
|
||||
if (array_key_exists($type, self::$mimeTypes)) {
|
||||
$contentType = self::$mimeTypes[$type];
|
||||
}
|
||||
}
|
||||
|
||||
if ($contentType === null) {
|
||||
/* We were unable to determine the MIME type from the file extension. Fall back to mime_content_type (if it
|
||||
* exists).
|
||||
*/
|
||||
if (function_exists('mime_content_type')) {
|
||||
$contentType = mime_content_type($path);
|
||||
} else {
|
||||
// mime_content_type doesn't exist. Return a default MIME type
|
||||
Logger::warning('Unable to determine mime content type of file: ' . $path);
|
||||
$contentType = 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
$assetConfig = $config->getConfigItem('assets');
|
||||
$cacheConfig = $assetConfig->getConfigItem('caching');
|
||||
$response = new BinaryFileResponse($path);
|
||||
$response->setCache([
|
||||
// "public" allows response caching even if the request was authenticated,
|
||||
// which is exactly what we want for static resources
|
||||
'public' => true,
|
||||
'max_age' => (string)$cacheConfig->getInteger('max_age', 86400)
|
||||
]);
|
||||
$response->setAutoLastModified();
|
||||
if ($cacheConfig->getBoolean('etag', false)) {
|
||||
$response->setAutoEtag();
|
||||
}
|
||||
$response->isNotModified($request);
|
||||
$response->headers->set('Content-Type', $contentType);
|
||||
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE);
|
||||
$response->prepare($request);
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $module
|
||||
* @param array $mod_config
|
||||
* @return bool
|
||||
*/
|
||||
private static function isModuleEnabledWithConf($module, $mod_config)
|
||||
{
|
||||
if (isset(self::$module_info[$module]['enabled'])) {
|
||||
return self::$module_info[$module]['enabled'];
|
||||
}
|
||||
|
||||
if (!empty(self::$modules) && !in_array($module, self::$modules, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$moduleDir = self::getModuleDir($module);
|
||||
|
||||
if (!is_dir($moduleDir)) {
|
||||
self::$module_info[$module]['enabled'] = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($mod_config[$module])) {
|
||||
if (is_bool($mod_config[$module])) {
|
||||
self::$module_info[$module]['enabled'] = $mod_config[$module];
|
||||
return $mod_config[$module];
|
||||
}
|
||||
|
||||
throw new \Exception("Invalid module.enable value for the '$module' module.");
|
||||
}
|
||||
|
||||
if (
|
||||
assert_options(ASSERT_ACTIVE)
|
||||
&& !file_exists($moduleDir . '/default-enable')
|
||||
&& !file_exists($moduleDir . '/default-disable')
|
||||
) {
|
||||
Logger::error("Missing default-enable or default-disable file for the module $module");
|
||||
}
|
||||
|
||||
if (file_exists($moduleDir . '/enable')) {
|
||||
self::$module_info[$module]['enabled'] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!file_exists($moduleDir . '/disable') && file_exists($moduleDir . '/default-enable')) {
|
||||
self::$module_info[$module]['enabled'] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
self::$module_info[$module]['enabled'] = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get available modules.
|
||||
*
|
||||
* @return array One string for each module.
|
||||
*
|
||||
* @throws \Exception If we cannot open the module's directory.
|
||||
*/
|
||||
public static function getModules()
|
||||
{
|
||||
if (!empty(self::$modules)) {
|
||||
return self::$modules;
|
||||
}
|
||||
|
||||
$path = self::getModuleDir('.');
|
||||
|
||||
$dh = scandir($path);
|
||||
if ($dh === false) {
|
||||
throw new \Exception('Unable to open module directory "' . $path . '".');
|
||||
}
|
||||
|
||||
foreach ($dh as $f) {
|
||||
if ($f[0] === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_dir($path . '/' . $f)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self::$modules[] = $f;
|
||||
}
|
||||
|
||||
return self::$modules;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolve module class.
|
||||
*
|
||||
* This function takes a string on the form "<module>:<class>" and converts it to a class
|
||||
* name. It can also check that the given class is a subclass of a specific class. The
|
||||
* resolved classname will be "\SimleSAML\Module\<module>\<$type>\<class>.
|
||||
*
|
||||
* It is also possible to specify a full classname instead of <module>:<class>.
|
||||
*
|
||||
* An exception will be thrown if the class can't be resolved.
|
||||
*
|
||||
* @param string $id The string we should resolve.
|
||||
* @param string $type The type of the class.
|
||||
* @param string|null $subclass The class should be a subclass of this class. Optional.
|
||||
*
|
||||
* @return string The classname.
|
||||
*
|
||||
* @throws \Exception If the class cannot be resolved.
|
||||
*/
|
||||
public static function resolveClass($id, $type, $subclass = null)
|
||||
{
|
||||
assert(is_string($id));
|
||||
assert(is_string($type));
|
||||
assert(is_string($subclass) || $subclass === null);
|
||||
|
||||
$tmp = explode(':', $id, 2);
|
||||
if (count($tmp) === 1) {
|
||||
// no module involved
|
||||
$className = $tmp[0];
|
||||
if (!class_exists($className)) {
|
||||
throw new \Exception("Could not resolve '$id': no class named '$className'.");
|
||||
}
|
||||
} else {
|
||||
// should be a module
|
||||
// make sure empty types are handled correctly
|
||||
$type = (empty($type)) ? '\\' : '\\' . $type . '\\';
|
||||
|
||||
$className = 'SimpleSAML\\Module\\' . $tmp[0] . $type . $tmp[1];
|
||||
if (!class_exists($className)) {
|
||||
// check for the old-style class names
|
||||
$type = str_replace('\\', '_', $type);
|
||||
$oldClassName = 'sspmod_' . $tmp[0] . $type . $tmp[1];
|
||||
|
||||
if (!class_exists($oldClassName)) {
|
||||
throw new \Exception("Could not resolve '$id': no class named '$className' or '$oldClassName'.");
|
||||
}
|
||||
$className = $oldClassName;
|
||||
}
|
||||
}
|
||||
|
||||
if ($subclass !== null && !is_subclass_of($className, $subclass)) {
|
||||
throw new \Exception(
|
||||
'Could not resolve \'' . $id . '\': The class \'' . $className
|
||||
. '\' isn\'t a subclass of \'' . $subclass . '\'.'
|
||||
);
|
||||
}
|
||||
|
||||
return $className;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get absolute URL to a specified module resource.
|
||||
*
|
||||
* This function creates an absolute URL to a resource stored under ".../modules/<module>/www/".
|
||||
*
|
||||
* @param string $resource Resource path, on the form "<module name>/<resource>"
|
||||
* @param array $parameters Extra parameters which should be added to the URL. Optional.
|
||||
*
|
||||
* @return string The absolute URL to the given resource.
|
||||
*/
|
||||
public static function getModuleURL($resource, array $parameters = [])
|
||||
{
|
||||
assert(is_string($resource));
|
||||
assert($resource[0] !== '/');
|
||||
|
||||
$url = Utils\HTTP::getBaseURL() . 'module.php/' . $resource;
|
||||
if (!empty($parameters)) {
|
||||
$url = Utils\HTTP::addURLParameters($url, $parameters);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the available hooks for a given module.
|
||||
*
|
||||
* @param string $module The module where we should look for hooks.
|
||||
*
|
||||
* @return array An array with the hooks available for this module. Each element is an array with two keys: 'file'
|
||||
* points to the file that contains the hook, and 'func' contains the name of the function implementing that hook.
|
||||
* When there are no hooks defined, an empty array is returned.
|
||||
*/
|
||||
public static function getModuleHooks($module)
|
||||
{
|
||||
if (isset(self::$modules[$module]['hooks'])) {
|
||||
return self::$modules[$module]['hooks'];
|
||||
}
|
||||
|
||||
$hook_dir = self::getModuleDir($module) . '/hooks';
|
||||
if (!is_dir($hook_dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$hooks = [];
|
||||
$files = scandir($hook_dir);
|
||||
foreach ($files as $file) {
|
||||
if ($file[0] === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!preg_match('/hook_(\w+)\.php/', $file, $matches)) {
|
||||
continue;
|
||||
}
|
||||
$hook_name = $matches[1];
|
||||
$hook_func = $module . '_hook_' . $hook_name;
|
||||
$hooks[$hook_name] = ['file' => $hook_dir . '/' . $file, 'func' => $hook_func];
|
||||
}
|
||||
return $hooks;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call a hook in all enabled modules.
|
||||
*
|
||||
* This function iterates over all enabled modules and calls a hook in each module.
|
||||
*
|
||||
* @param string $hook The name of the hook.
|
||||
* @param mixed &$data The data which should be passed to each hook. Will be passed as a reference.
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception If an invalid hook is found in a module.
|
||||
*/
|
||||
public static function callHooks($hook, &$data = null)
|
||||
{
|
||||
assert(is_string($hook));
|
||||
|
||||
$modules = self::getModules();
|
||||
$config = Configuration::getOptionalConfig()->getArray('module.enable', []);
|
||||
sort($modules);
|
||||
foreach ($modules as $module) {
|
||||
if (!self::isModuleEnabledWithConf($module, $config)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset(self::$module_info[$module]['hooks'])) {
|
||||
self::$module_info[$module]['hooks'] = self::getModuleHooks($module);
|
||||
}
|
||||
|
||||
if (!isset(self::$module_info[$module]['hooks'][$hook])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
require_once(self::$module_info[$module]['hooks'][$hook]['file']);
|
||||
|
||||
if (!is_callable(self::$module_info[$module]['hooks'][$hook]['func'])) {
|
||||
throw new Error\Exception('Invalid hook \'' . $hook . '\' for module \'' . $module . '\'.');
|
||||
}
|
||||
|
||||
$fn = self::$module_info[$module]['hooks'][$hook]['func'];
|
||||
$fn($data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle a valid request that ends with a trailing slash.
|
||||
*
|
||||
* This method removes the trailing slash and redirects to the resulting URL.
|
||||
*
|
||||
* @param Request $request The request to process by this controller method.
|
||||
*
|
||||
* @return RedirectResponse A redirection to the URI specified in the request, but without the trailing slash.
|
||||
*/
|
||||
public static function removeTrailingSlash(Request $request)
|
||||
{
|
||||
$pathInfo = $request->server->get('PATH_INFO');
|
||||
$url = str_replace($pathInfo, rtrim($pathInfo, ' /'), $request->getRequestUri());
|
||||
return new RedirectResponse($url, 308);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Module;
|
||||
|
||||
use SAML\Auth\AuthenticationFactory;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error\Exception;
|
||||
use SAML\Module;
|
||||
use SAML\Session;
|
||||
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
|
||||
use Symfony\Component\HttpKernel\Controller\ControllerResolver as SymfonyControllerResolver;
|
||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
|
||||
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
|
||||
use Symfony\Component\Routing\Loader\YamlFileLoader;
|
||||
use Symfony\Component\Routing\Matcher\UrlMatcher;
|
||||
use Symfony\Component\Routing\RequestContext;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* A class to resolve module controllers based on a given request.
|
||||
*
|
||||
* This class allows us to find a controller (a callable) that's configured for a given URL.
|
||||
*
|
||||
* @package SimpleSAML
|
||||
*/
|
||||
class ControllerResolver extends SymfonyControllerResolver implements ArgumentResolverInterface
|
||||
{
|
||||
/** @var ArgumentMetadataFactory */
|
||||
protected $argFactory;
|
||||
|
||||
/** @var ContainerBuilder */
|
||||
protected $container;
|
||||
|
||||
/** @var string */
|
||||
protected $module;
|
||||
|
||||
/** @var array */
|
||||
protected $params = [];
|
||||
|
||||
/** @var RouteCollection|null */
|
||||
protected $routes;
|
||||
|
||||
|
||||
/**
|
||||
* Build a module controller resolver.
|
||||
*
|
||||
* @param string $module The name of the module.
|
||||
*/
|
||||
public function __construct($module)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->module = $module;
|
||||
|
||||
$loader = new YamlFileLoader(
|
||||
new FileLocator(Module::getModuleDir($this->module))
|
||||
);
|
||||
|
||||
$this->argFactory = new ArgumentMetadataFactory();
|
||||
$this->container = new ContainerBuilder();
|
||||
$this->container->autowire(AuthenticationFactory::class, AuthenticationFactory::class);
|
||||
|
||||
try {
|
||||
$this->routes = $loader->load('routes.yaml');
|
||||
$redirect = new Route(
|
||||
'/{url}',
|
||||
['_controller' => '\SimpleSAML\Module::removeTrailingSlash'],
|
||||
['url' => '.*/$']
|
||||
);
|
||||
$this->routes->add('trailing-slash', $redirect);
|
||||
$this->routes->addPrefix('/' . $this->module);
|
||||
} catch (FileLocatorFileNotFoundException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the controller associated with a given URL, based on a request.
|
||||
*
|
||||
* This method searches for a 'routes.yaml' file in the root of the module, defining valid routes for the module
|
||||
* and mapping them given controllers. It's input is a Request object with the request that we want to serve.
|
||||
*
|
||||
* @param Request $request The request we need to find a controller for.
|
||||
*
|
||||
* @return callable|false A controller (as a callable) that can handle the request, or false if we cannot find
|
||||
* one suitable for the given request.
|
||||
*/
|
||||
public function getController(Request $request)
|
||||
{
|
||||
if ($this->routes === null) {
|
||||
return false;
|
||||
}
|
||||
$ctxt = new RequestContext();
|
||||
$ctxt->fromRequest($request);
|
||||
|
||||
try {
|
||||
$matcher = new UrlMatcher($this->routes, $ctxt);
|
||||
$this->params = $matcher->match($ctxt->getPathInfo());
|
||||
list($class, $method) = explode('::', $this->params['_controller']);
|
||||
$this->container->register($class, $class)->setAutowired(true)->setPublic(true);
|
||||
$this->container->compile();
|
||||
return [$this->container->get($class), $method];
|
||||
} catch (ResourceNotFoundException $e) {
|
||||
// no route defined matching this request
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the arguments that should be passed to a controller from a given request.
|
||||
*
|
||||
* When the signature of the controller includes arguments with type Request, the given request will be passed to
|
||||
* those. Otherwise, they'll be matched by name. If no value is available for a given argument, the method will
|
||||
* try to set a default value or null, if possible.
|
||||
*
|
||||
* @param Request $request The request that holds all the information needed by the controller.
|
||||
* @param callable $controller A controller for the given request.
|
||||
*
|
||||
* @return array An array of arguments that should be passed to the controller, in order.
|
||||
*
|
||||
* @throws Exception If we don't find anything suitable for an argument in the controller's
|
||||
* signature.
|
||||
*/
|
||||
public function getArguments(Request $request, $controller)
|
||||
{
|
||||
$args = [];
|
||||
$metadata = $this->argFactory->createArgumentMetadata($controller);
|
||||
|
||||
/** @var ArgumentMetadata $argMeta */
|
||||
foreach ($metadata as $argMeta) {
|
||||
if ($argMeta->getType() === Request::class) {
|
||||
// add request argument
|
||||
$args[] = $request;
|
||||
continue;
|
||||
}
|
||||
|
||||
$argName = $argMeta->getName();
|
||||
if (array_key_exists($argName, $this->params)) {
|
||||
// add argument by name
|
||||
$args[] = $this->params[$argName];
|
||||
continue;
|
||||
}
|
||||
|
||||
// URL does not contain value for this argument
|
||||
if ($argMeta->hasDefaultValue()) {
|
||||
// it has a default value
|
||||
$args[] = $argMeta->getDefaultValue();
|
||||
}
|
||||
|
||||
// no default value
|
||||
if ($argMeta->isNullable()) {
|
||||
$args[] = null;
|
||||
}
|
||||
|
||||
throw new Exception('Missing value for argument ' . $argName . '. This is probably a bug.');
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the configuration to use by the controllers.
|
||||
*
|
||||
* @param Configuration $config
|
||||
* @return void
|
||||
*/
|
||||
public function setConfiguration(Configuration $config)
|
||||
{
|
||||
$this->container->set(Configuration::class, $config);
|
||||
$this->container->register(Configuration::class)->setSynthetic(true)->setAutowired(true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the session to use by the controllers.
|
||||
*
|
||||
* @param Session $session
|
||||
* @return void
|
||||
*/
|
||||
public function setSession(Session $session)
|
||||
{
|
||||
$this->container->set(Session::class, $session);
|
||||
$this->container->register(Session::class)
|
||||
->setSynthetic(true)
|
||||
->setAutowired(true)
|
||||
->addMethodCall('setConfiguration', [new Reference(Configuration::class)]);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of SimpleSAMLphp. See the file COPYING in the
|
||||
* root of the distribution for licence information.
|
||||
*
|
||||
* This file defines a base class for session handling.
|
||||
* Instantiation of session handler objects should be done through
|
||||
* the class method getSessionHandler().
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML;
|
||||
|
||||
use Exception;
|
||||
use SAML\Error\CannotSetCookie;
|
||||
|
||||
abstract class SessionHandler
|
||||
{
|
||||
/**
|
||||
* This static variable contains a reference to the current
|
||||
* instance of the session handler. This variable will be NULL if
|
||||
* we haven't instantiated a session handler yet.
|
||||
*
|
||||
* @var SessionHandler
|
||||
*/
|
||||
protected static $sessionHandler;
|
||||
|
||||
|
||||
/**
|
||||
* This function retrieves the current instance of the session handler.
|
||||
* The session handler will be instantiated if this is the first call
|
||||
* to this function.
|
||||
*
|
||||
* @return SessionHandler The current session handler.
|
||||
*
|
||||
* @throws Exception If we cannot instantiate the session handler.
|
||||
*/
|
||||
public static function getSessionHandler()
|
||||
{
|
||||
if (self::$sessionHandler === null) {
|
||||
self::createSessionHandler();
|
||||
}
|
||||
|
||||
return self::$sessionHandler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This constructor is included in case it is needed in the
|
||||
* future. Including it now allows us to write parent::__construct() in
|
||||
* the subclasses of this class.
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new session id.
|
||||
*
|
||||
* @return string The new session id.
|
||||
*/
|
||||
abstract public function newSessionId();
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the session ID saved in the session cookie, if there's one.
|
||||
*
|
||||
* @return string|null The session id saved in the cookie or null if no session cookie was set.
|
||||
*/
|
||||
abstract public function getCookieSessionId();
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the session cookie name.
|
||||
*
|
||||
* @return string The session cookie name.
|
||||
*/
|
||||
abstract public function getSessionCookieName();
|
||||
|
||||
|
||||
/**
|
||||
* Save the session.
|
||||
*
|
||||
* @param Session $session The session object we should save.
|
||||
*/
|
||||
abstract public function saveSession(Session $session);
|
||||
|
||||
|
||||
/**
|
||||
* Load the session.
|
||||
*
|
||||
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
|
||||
*
|
||||
* @return Session|null The session object, or null if it doesn't exist.
|
||||
*/
|
||||
abstract public function loadSession($sessionId = null);
|
||||
|
||||
|
||||
/**
|
||||
* Check whether the session cookie is set.
|
||||
*
|
||||
* This function will only return false if is is certain that the cookie isn't set.
|
||||
*
|
||||
* @return bool True if it was set, false if not.
|
||||
*/
|
||||
abstract public function hasSessionCookie();
|
||||
|
||||
|
||||
/**
|
||||
* Set a session cookie.
|
||||
*
|
||||
* @param string $sessionName The name of the session.
|
||||
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
|
||||
* @param array|null $cookieParams Additional parameters to use for the session cookie.
|
||||
*
|
||||
* @throws CannotSetCookie If we can't set the cookie.
|
||||
*/
|
||||
abstract public function setCookie($sessionName, $sessionID, array $cookieParams = null);
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the session handler.
|
||||
*
|
||||
* This function creates an instance of the session handler which is
|
||||
* selected in the 'store.type' configuration directive. If no
|
||||
* session handler is selected, then we will fall back to the default
|
||||
* PHP session handler.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Exception If we cannot instantiate the session handler.
|
||||
*/
|
||||
private static function createSessionHandler()
|
||||
{
|
||||
$store = Store::getInstance();
|
||||
if ($store === false) {
|
||||
self::$sessionHandler = new SessionHandlerPHP();
|
||||
} else {
|
||||
/** @var Store $store At this point, $store can only be an object */
|
||||
self::$sessionHandler = new SessionHandlerStore($store);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the cookie parameters that should be used for session cookies.
|
||||
*
|
||||
* @return array An array with the cookie parameters.
|
||||
* @link http://www.php.net/manual/en/function.session-get-cookie-params.php
|
||||
*/
|
||||
public function getCookieParams()
|
||||
{
|
||||
$config = Configuration::getInstance();
|
||||
|
||||
return [
|
||||
'lifetime' => $config->getInteger('session.cookie.lifetime', 0),
|
||||
'path' => $config->getString('session.cookie.path', '/'),
|
||||
'domain' => $config->getString('session.cookie.domain', null),
|
||||
'secure' => $config->getBoolean('session.cookie.secure', false),
|
||||
'samesite' => $config->getString('session.cookie.samesite', null),
|
||||
'httponly' => true,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of SimpleSAMLphp. See the file COPYING in the root of the distribution for licence information.
|
||||
*
|
||||
* This file defines a base class for session handlers that need to store the session id in a cookie. It takes care of
|
||||
* storing and retrieving the session id.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
* @abstract
|
||||
*/
|
||||
|
||||
namespace SAML;
|
||||
|
||||
use SAML\Error\CannotSetCookie;
|
||||
use SAML\Utils;
|
||||
|
||||
abstract class SessionHandlerCookie extends SessionHandler
|
||||
{
|
||||
/**
|
||||
* This variable contains the current session id.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $session_id = null;
|
||||
|
||||
|
||||
/**
|
||||
* This variable contains the session cookie name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $cookie_name;
|
||||
|
||||
|
||||
/**
|
||||
* This constructor initializes the session id based on what we receive in a cookie. We create a new session id and
|
||||
* set a cookie with this id if we don't have a session id.
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
// call the constructor in the base class in case it should become necessary in the future
|
||||
parent::__construct();
|
||||
|
||||
$config = Configuration::getInstance();
|
||||
$this->cookie_name = $config->getString('session.cookie.name', 'SimpleSAMLSessionID');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new session id.
|
||||
*
|
||||
* @return string The new session id.
|
||||
*/
|
||||
public function newSessionId()
|
||||
{
|
||||
$this->session_id = self::createSessionID();
|
||||
Session::createSession($this->session_id);
|
||||
|
||||
return $this->session_id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the session ID saved in the session cookie, if there's one.
|
||||
*
|
||||
* @return string|null The session id saved in the cookie or null if no session cookie was set.
|
||||
*/
|
||||
public function getCookieSessionId()
|
||||
{
|
||||
if ($this->session_id === null) {
|
||||
if ($this->hasSessionCookie()) {
|
||||
// attempt to retrieve the session id from the cookie
|
||||
$this->session_id = $_COOKIE[$this->cookie_name];
|
||||
}
|
||||
|
||||
// check if we have a valid session id
|
||||
if (!self::isValidSessionID($this->session_id)) {
|
||||
// invalid, disregard this session
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->session_id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the session cookie name.
|
||||
*
|
||||
* @return string The session cookie name.
|
||||
*/
|
||||
public function getSessionCookieName()
|
||||
{
|
||||
return $this->cookie_name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This static function creates a session id. A session id consists of 32 random hexadecimal characters.
|
||||
*
|
||||
* @return string A random session id.
|
||||
*/
|
||||
private static function createSessionID()
|
||||
{
|
||||
return bin2hex(openssl_random_pseudo_bytes(16));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This static function validates a session id. A session id is valid if it only consists of characters which are
|
||||
* allowed in a session id and it is the correct length.
|
||||
*
|
||||
* @param string $session_id The session ID we should validate.
|
||||
*
|
||||
* @return boolean True if this session ID is valid, false otherwise.
|
||||
*/
|
||||
private static function isValidSessionID($session_id)
|
||||
{
|
||||
if (!is_string($session_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($session_id) != 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preg_match('/[^0-9a-f]/', $session_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether the session cookie is set.
|
||||
*
|
||||
* This function will only return false if is is certain that the cookie isn't set.
|
||||
*
|
||||
* @return boolean True if it was set, false otherwise.
|
||||
*/
|
||||
public function hasSessionCookie()
|
||||
{
|
||||
return array_key_exists($this->cookie_name, $_COOKIE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set a session cookie.
|
||||
*
|
||||
* @param string $sessionName The name of the session.
|
||||
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
|
||||
* @param array|null $cookieParams Additional parameters to use for the session cookie.
|
||||
* @return void
|
||||
*
|
||||
* @throws CannotSetCookie If we can't set the cookie.
|
||||
*/
|
||||
public function setCookie($sessionName, $sessionID, array $cookieParams = null)
|
||||
{
|
||||
assert(is_string($sessionName));
|
||||
assert(is_string($sessionID) || $sessionID === null);
|
||||
|
||||
if ($cookieParams !== null) {
|
||||
$params = array_merge($this->getCookieParams(), $cookieParams);
|
||||
} else {
|
||||
$params = $this->getCookieParams();
|
||||
}
|
||||
|
||||
Utils\HTTP::setCookie($sessionName, $sessionID, $params, true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,393 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of SimpleSAMLphp. See the file COPYING in the root of the distribution for licence information.
|
||||
*
|
||||
* This file defines a session handler which uses the default php session handler for storage.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML;
|
||||
|
||||
use SAML\Error;
|
||||
use SAML\Error\CannotSetCookie;
|
||||
use SAML\Error\Exception;
|
||||
use SAML\Utils;
|
||||
|
||||
class SessionHandlerPHP extends SessionHandler
|
||||
{
|
||||
/**
|
||||
* This variable contains the session cookie name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $cookie_name;
|
||||
|
||||
/**
|
||||
* An associative array containing the details of a session existing previously to creating or loading one with this
|
||||
* session handler. The keys of the array will be:
|
||||
*
|
||||
* - id: the ID of the session, as returned by session_id().
|
||||
* - name: the name of the session, as returned by session_name().
|
||||
* - cookie_params: the parameters of the session cookie, as returned by session_get_cookie_params().
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $previous_session = [];
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the PHP session handling. This constructor is protected because it should only be called from
|
||||
* \SimpleSAML\SessionHandler::createSessionHandler(...).
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
// call the parent constructor in case it should become necessary in the future
|
||||
parent::__construct();
|
||||
|
||||
$config = Configuration::getInstance();
|
||||
$this->cookie_name = $config->getString('session.phpsession.cookiename', null);
|
||||
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
if (session_name() === $this->cookie_name || $this->cookie_name === null) {
|
||||
Logger::warning(
|
||||
'There is already a PHP session with the same name as SimpleSAMLphp\'s session, or the ' .
|
||||
"'session.phpsession.cookiename' configuration option is not set. Make sure to set " .
|
||||
"SimpleSAMLphp's cookie name with a value not used by any other applications."
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* We shouldn't have a session at this point, so it might be an application session. Save the details to
|
||||
* retrieve it later and commit.
|
||||
*/
|
||||
$this->previous_session['cookie_params'] = session_get_cookie_params();
|
||||
$this->previous_session['id'] = session_id();
|
||||
$this->previous_session['name'] = session_name();
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
|
||||
if (empty($this->cookie_name)) {
|
||||
$this->cookie_name = session_name();
|
||||
} elseif (!headers_sent() || version_compare(PHP_VERSION, '7.2', '<')) {
|
||||
session_name($this->cookie_name);
|
||||
}
|
||||
|
||||
$params = $this->getCookieParams();
|
||||
|
||||
if (!headers_sent()) {
|
||||
if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
|
||||
/** @psalm-suppress InvalidArgument This annotation may be removed in Psalm >=3.0.15 */
|
||||
session_set_cookie_params([
|
||||
'lifetime' => $params['lifetime'],
|
||||
'path' => $params['path'],
|
||||
'domain' => $params['domain'],
|
||||
'secure' => $params['secure'],
|
||||
'httponly' => $params['httponly'],
|
||||
'samesite' => $params['samesite'],
|
||||
]);
|
||||
} else {
|
||||
session_set_cookie_params(
|
||||
$params['lifetime'],
|
||||
$params['path'],
|
||||
is_null($params['domain']) ? '' : $params['domain'],
|
||||
$params['secure'],
|
||||
$params['httponly']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$savepath = $config->getString('session.phpsession.savepath', null);
|
||||
if (!empty($savepath)) {
|
||||
session_save_path($savepath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Restore a previously-existing session.
|
||||
*
|
||||
* Use this method to restore a previous PHP session existing before SimpleSAMLphp initialized its own session.
|
||||
*
|
||||
* WARNING: do not use this method directly, unless you know what you are doing. Calling this method directly,
|
||||
* outside of \SimpleSAML\Session, could cause SimpleSAMLphp's session to be lost or mess the application's one. The
|
||||
* session must always be saved properly before calling this method. If you don't understand what this is about,
|
||||
* don't use this method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function restorePrevious()
|
||||
{
|
||||
if (empty($this->previous_session)) {
|
||||
return; // nothing to do here
|
||||
}
|
||||
|
||||
// close our own session
|
||||
session_write_close();
|
||||
|
||||
session_name($this->previous_session['name']);
|
||||
if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
|
||||
session_set_cookie_params($this->previous_session['cookie_params']);
|
||||
} else {
|
||||
session_set_cookie_params(
|
||||
$this->previous_session['cookie_params']['lifetime'],
|
||||
$this->previous_session['cookie_params']['path'],
|
||||
$this->previous_session['cookie_params']['domain'],
|
||||
$this->previous_session['cookie_params']['secure'],
|
||||
$this->previous_session['cookie_params']['httponly']
|
||||
);
|
||||
}
|
||||
session_id($this->previous_session['id']);
|
||||
$this->previous_session = [];
|
||||
@session_start();
|
||||
|
||||
/*
|
||||
* At this point, we have restored a previously-existing session, so we can't continue to use our session here.
|
||||
* Therefore, we need to load our session again in case we need it. We remove this handler from the parent
|
||||
* class so that the handler is initialized again if we ever need to do something with the session.
|
||||
*/
|
||||
parent::$sessionHandler = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a new session id.
|
||||
*
|
||||
* @return string The new session id.
|
||||
*/
|
||||
public function newSessionId()
|
||||
{
|
||||
$sessionId = false;
|
||||
if (function_exists('session_create_id') && version_compare(PHP_VERSION, '7.2', '<')) {
|
||||
// generate new (secure) session id
|
||||
$sid_length = (int) ini_get('session.sid_length');
|
||||
$sid_bits_per_char = (int) ini_get('session.sid_bits_per_character');
|
||||
|
||||
if (($sid_length * $sid_bits_per_char) < 128) {
|
||||
Logger::warning("Unsafe defaults used for sessionId generation!");
|
||||
}
|
||||
|
||||
/**
|
||||
* This annotation may be removed as soon as we start using vimeo/psalm 3.x
|
||||
* @psalm-suppress TooFewArguments
|
||||
*/
|
||||
$sessionId = session_create_id();
|
||||
}
|
||||
|
||||
if (!$sessionId) {
|
||||
Logger::warning("Secure session ID generation failed, falling back to custom ID generation.");
|
||||
$sessionId = bin2hex(openssl_random_pseudo_bytes(16));
|
||||
}
|
||||
|
||||
Session::createSession($sessionId);
|
||||
return $sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the session ID saved in the session cookie, if there's one.
|
||||
*
|
||||
* @return string|null The session id saved in the cookie or null if no session cookie was set.
|
||||
*
|
||||
* @throws Exception If the cookie is marked as secure but we are not using HTTPS.
|
||||
*/
|
||||
public function getCookieSessionId()
|
||||
{
|
||||
if (!$this->hasSessionCookie()) {
|
||||
return null; // there's no session cookie, can't return ID
|
||||
}
|
||||
|
||||
if (version_compare(PHP_VERSION, '7.2', 'ge') && headers_sent()) {
|
||||
// latest versions of PHP don't allow loading a session when output sent, get the ID from the cookie
|
||||
return $_COOKIE[$this->cookie_name];
|
||||
}
|
||||
|
||||
// do not rely on session_id() as it can return the ID of a previous session. Get it from the cookie instead.
|
||||
session_id($_COOKIE[$this->cookie_name]);
|
||||
|
||||
$session_cookie_params = session_get_cookie_params();
|
||||
|
||||
if ($session_cookie_params['secure'] && !Utils\HTTP::isHTTPS()) {
|
||||
throw new Exception('Session start with secure cookie not allowed on http.');
|
||||
}
|
||||
|
||||
@session_start();
|
||||
return session_id();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the session cookie name.
|
||||
*
|
||||
* @return string The session cookie name.
|
||||
*/
|
||||
public function getSessionCookieName()
|
||||
{
|
||||
return $this->cookie_name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save the current session to the PHP session array.
|
||||
*
|
||||
* @param Session $session The session object we should save.
|
||||
* @return void
|
||||
*/
|
||||
public function saveSession(Session $session)
|
||||
{
|
||||
$_SESSION['SimpleSAMLphp_SESSION'] = serialize($session);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load the session from the PHP session array.
|
||||
*
|
||||
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
|
||||
*
|
||||
* @return Session|null The session object, or null if it doesn't exist.
|
||||
*
|
||||
* @throws Exception If it wasn't possible to disable session cookies or we are trying to load a
|
||||
* PHP session with a specific identifier and it doesn't match with the current session identifier.
|
||||
*/
|
||||
public function loadSession($sessionId = null)
|
||||
{
|
||||
assert(is_string($sessionId) || $sessionId === null);
|
||||
|
||||
if ($sessionId !== null) {
|
||||
if (session_id() === '' && !(version_compare(PHP_VERSION, '7.2', 'ge') && headers_sent())) {
|
||||
// session not initiated with getCookieSessionId(), start session without setting cookie
|
||||
$ret = ini_set('session.use_cookies', '0');
|
||||
if ($ret === false) {
|
||||
throw new Exception('Disabling PHP option session.use_cookies failed.');
|
||||
}
|
||||
|
||||
session_id($sessionId);
|
||||
@session_start();
|
||||
} elseif ($sessionId !== session_id()) {
|
||||
throw new Exception('Cannot load PHP session with a specific ID.');
|
||||
}
|
||||
} elseif (session_id() === '') {
|
||||
$this->getCookieSessionId();
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['SimpleSAMLphp_SESSION'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$session = $_SESSION['SimpleSAMLphp_SESSION'];
|
||||
assert(is_string($session));
|
||||
|
||||
$session = unserialize($session);
|
||||
|
||||
return ($session !== false) ? $session : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether the session cookie is set.
|
||||
*
|
||||
* This function will only return false if is is certain that the cookie isn't set.
|
||||
*
|
||||
* @return boolean True if it was set, false otherwise.
|
||||
*/
|
||||
public function hasSessionCookie()
|
||||
{
|
||||
return array_key_exists($this->cookie_name, $_COOKIE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the cookie parameters that should be used for session cookies.
|
||||
*
|
||||
* This function contains some adjustments from the default to provide backwards-compatibility.
|
||||
*
|
||||
* @return array The cookie parameters for our sessions.
|
||||
* @throws Exception If both 'session.phpsession.limitedpath' and 'session.cookie.path' options
|
||||
* are set at the same time in the configuration.
|
||||
*@link http://www.php.net/manual/en/function.session-get-cookie-params.php
|
||||
*
|
||||
*/
|
||||
public function getCookieParams()
|
||||
{
|
||||
$config = Configuration::getInstance();
|
||||
|
||||
$ret = parent::getCookieParams();
|
||||
|
||||
if ($config->hasValue('session.phpsession.limitedpath') && $config->hasValue('session.cookie.path')) {
|
||||
throw new Exception(
|
||||
'You cannot set both the session.phpsession.limitedpath and session.cookie.path options.'
|
||||
);
|
||||
} elseif ($config->hasValue('session.phpsession.limitedpath')) {
|
||||
$ret['path'] = $config->getBoolean(
|
||||
'session.phpsession.limitedpath',
|
||||
false
|
||||
) ? $config->getBasePath() : '/';
|
||||
}
|
||||
|
||||
$ret['httponly'] = $config->getBoolean('session.phpsession.httponly', true);
|
||||
|
||||
if (version_compare(PHP_VERSION, '7.3.0', '<')) {
|
||||
// in older versions of PHP we need a nasty hack to set RFC6265bis SameSite attribute
|
||||
if ($ret['samesite'] !== null and !preg_match('/;\s+samesite/i', $ret['path'])) {
|
||||
$ret['path'] .= '; SameSite=' . $ret['samesite'];
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set a session cookie.
|
||||
*
|
||||
* @param string $sessionName The name of the session.
|
||||
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
|
||||
* @param array|null $cookieParams Additional parameters to use for the session cookie.
|
||||
* @return void
|
||||
*
|
||||
* @throws CannotSetCookie If we can't set the cookie.
|
||||
*/
|
||||
public function setCookie($sessionName, $sessionID, array $cookieParams = null)
|
||||
{
|
||||
if ($cookieParams === null) {
|
||||
$cookieParams = session_get_cookie_params();
|
||||
}
|
||||
|
||||
if ($cookieParams['secure'] && !Utils\HTTP::isHTTPS()) {
|
||||
throw new CannotSetCookie(
|
||||
'Setting secure cookie on plain HTTP is not allowed.',
|
||||
CannotSetCookie::SECURE_COOKIE
|
||||
);
|
||||
}
|
||||
|
||||
if (headers_sent()) {
|
||||
throw new CannotSetCookie(
|
||||
'Headers already sent.',
|
||||
CannotSetCookie::HEADERS_SENT
|
||||
);
|
||||
}
|
||||
|
||||
if (session_id() !== '') {
|
||||
// session already started, close it
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
if (version_compare(PHP_VERSION, '7.3.0', '>=')) {
|
||||
/** @psalm-suppress InvalidArgument This annotation may be removed in Psalm >=3.0.15 */
|
||||
session_set_cookie_params($cookieParams);
|
||||
} else {
|
||||
session_set_cookie_params(
|
||||
$cookieParams['lifetime'],
|
||||
$cookieParams['path'],
|
||||
is_null($cookieParams['domain']) ? '' : $cookieParams['domain'],
|
||||
$cookieParams['secure'],
|
||||
$cookieParams['httponly']
|
||||
);
|
||||
}
|
||||
|
||||
session_id(strval($sessionID));
|
||||
@session_start();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Session storage in the data store.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML;
|
||||
|
||||
class SessionHandlerStore extends SessionHandlerCookie
|
||||
{
|
||||
/**
|
||||
* The data store we save the session to.
|
||||
*
|
||||
* @var Store
|
||||
*/
|
||||
private $store;
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the session.
|
||||
*
|
||||
* @param Store $store The store to use.
|
||||
*/
|
||||
protected function __construct(Store $store)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->store = $store;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load a session from the data store.
|
||||
*
|
||||
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
|
||||
*
|
||||
* @return Session|null The session object, or null if it doesn't exist.
|
||||
*/
|
||||
public function loadSession($sessionId = null)
|
||||
{
|
||||
assert(is_string($sessionId) || $sessionId === null);
|
||||
|
||||
if ($sessionId === null) {
|
||||
$sessionId = $this->getCookieSessionId();
|
||||
if ($sessionId === null) {
|
||||
// no session cookie, nothing to load
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$session = $this->store->get('session', $sessionId);
|
||||
if ($session !== null) {
|
||||
assert($session instanceof Session);
|
||||
return $session;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save a session to the data store.
|
||||
*
|
||||
* @param Session $session The session object we should save.
|
||||
* @return void
|
||||
*/
|
||||
public function saveSession(Session $session)
|
||||
{
|
||||
if ($session->isTransient()) {
|
||||
// transient session, nothing to save
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var string $sessionId */
|
||||
$sessionId = $session->getSessionId();
|
||||
|
||||
$config = Configuration::getInstance();
|
||||
$sessionDuration = $config->getInteger('session.duration', 8 * 60 * 60);
|
||||
$expire = time() + $sessionDuration;
|
||||
|
||||
$this->store->set('session', $sessionId, $session, $expire);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace SAML;
|
||||
|
||||
/**
|
||||
* Statistics handler class.
|
||||
*
|
||||
* This class is responsible for taking a statistics event and logging it.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class Stats
|
||||
{
|
||||
/**
|
||||
* Whether this class is initialized.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private static $initialized = false;
|
||||
|
||||
|
||||
/**
|
||||
* The statistics output callbacks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $outputs = null;
|
||||
|
||||
|
||||
/**
|
||||
* Create an output from a configuration object.
|
||||
*
|
||||
* @param Configuration $config The configuration object.
|
||||
*
|
||||
* @return mixed A new instance of the configured class.
|
||||
*/
|
||||
private static function createOutput(Configuration $config)
|
||||
{
|
||||
$cls = $config->getString('class');
|
||||
$cls = Module::resolveClass($cls, 'Stats\Output', '\SimpleSAML\Stats\Output');
|
||||
|
||||
$output = new $cls($config);
|
||||
return $output;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the outputs.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function initOutputs()
|
||||
{
|
||||
|
||||
$config = Configuration::getInstance();
|
||||
$outputCfgs = $config->getConfigList('statistics.out');
|
||||
|
||||
self::$outputs = [];
|
||||
foreach ($outputCfgs as $cfg) {
|
||||
self::$outputs[] = self::createOutput($cfg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Notify about an event.
|
||||
*
|
||||
* @param string $event The event.
|
||||
* @param array $data Event data. Optional.
|
||||
*
|
||||
* @return void|boolean False if output is not enabled, void otherwise.
|
||||
*/
|
||||
public static function log($event, array $data = [])
|
||||
{
|
||||
assert(is_string($event));
|
||||
assert(!isset($data['op']));
|
||||
assert(!isset($data['time']));
|
||||
assert(!isset($data['_id']));
|
||||
|
||||
if (!self::$initialized) {
|
||||
self::initOutputs();
|
||||
self::$initialized = true;
|
||||
}
|
||||
|
||||
if (empty(self::$outputs)) {
|
||||
// not enabled
|
||||
return false;
|
||||
}
|
||||
|
||||
$data['op'] = $event;
|
||||
$data['time'] = microtime(true);
|
||||
|
||||
// the ID generation is designed to cluster IDs related in time close together
|
||||
$int_t = (int) $data['time'];
|
||||
$hd = openssl_random_pseudo_bytes(16);
|
||||
$data['_id'] = sprintf('%016x%s', $int_t, bin2hex($hd));
|
||||
|
||||
foreach (self::$outputs as $out) {
|
||||
$out->emit($data);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Stats;
|
||||
|
||||
use SAML\Configuration;
|
||||
|
||||
/**
|
||||
* Interface for statistics outputs.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
abstract class Output
|
||||
{
|
||||
/**
|
||||
* Initialize the output.
|
||||
*
|
||||
* @param Configuration $config The configuration for this output.
|
||||
*/
|
||||
public function __construct(Configuration $config)
|
||||
{
|
||||
// do nothing by default
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write a stats event.
|
||||
*
|
||||
* @param array $data The event.
|
||||
*/
|
||||
abstract public function emit(array $data);
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
namespace SAML;
|
||||
|
||||
use Exception;
|
||||
use SAML\Error;
|
||||
use SAML\Error\CriticalConfigurationError;
|
||||
|
||||
/**
|
||||
* Base class for data stores.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
abstract class Store implements Utils\ClearableState
|
||||
{
|
||||
/**
|
||||
* Our singleton instance.
|
||||
*
|
||||
* This is false if the data store isn't enabled, and null if we haven't attempted to initialize it.
|
||||
*
|
||||
* @var Store|false|null
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve our singleton instance.
|
||||
*
|
||||
* @return Store|false The data store, or false if it isn't enabled.
|
||||
*
|
||||
* @throws CriticalConfigurationError
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance !== null) {
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
$config = Configuration::getInstance();
|
||||
$storeType = $config->getString('store.type', 'phpsession');
|
||||
|
||||
switch ($storeType) {
|
||||
case 'phpsession':
|
||||
// we cannot support advanced features with the PHP session store
|
||||
self::$instance = false;
|
||||
break;
|
||||
case 'memcache':
|
||||
self::$instance = new Store\Memcache();
|
||||
break;
|
||||
case 'sql':
|
||||
self::$instance = new Store\SQL();
|
||||
break;
|
||||
case 'redis':
|
||||
self::$instance = new Store\Redis();
|
||||
break;
|
||||
default:
|
||||
// datastore from module
|
||||
try {
|
||||
$className = Module::resolveClass($storeType, 'Store', '\SimpleSAML\Store');
|
||||
} catch (Exception $e) {
|
||||
$c = $config->toArray();
|
||||
$c['store.type'] = 'phpsession';
|
||||
throw new CriticalConfigurationError(
|
||||
"Invalid 'store.type' configuration option. Cannot find store '$storeType'.",
|
||||
null,
|
||||
$c
|
||||
);
|
||||
}
|
||||
/** @var Store|false */
|
||||
self::$instance = new $className();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a value from the data store.
|
||||
*
|
||||
* @param string $type The data type.
|
||||
* @param string $key The key.
|
||||
*
|
||||
* @return mixed|null The value.
|
||||
*/
|
||||
abstract public function get($type, $key);
|
||||
|
||||
|
||||
/**
|
||||
* Save a value to the data store.
|
||||
*
|
||||
* @param string $type The data type.
|
||||
* @param string $key The key.
|
||||
* @param mixed $value The value.
|
||||
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
|
||||
*/
|
||||
abstract public function set($type, $key, $value, $expire = null);
|
||||
|
||||
|
||||
/**
|
||||
* Delete a value from the data store.
|
||||
*
|
||||
* @param string $type The data type.
|
||||
* @param string $key The key.
|
||||
*/
|
||||
abstract public function delete($type, $key);
|
||||
|
||||
|
||||
/**
|
||||
* Clear any SSP specific state, such as SSP environmental variables or cached internals.
|
||||
* @return void
|
||||
*/
|
||||
public static function clearInternalState()
|
||||
{
|
||||
self::$instance = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Store;
|
||||
|
||||
use SAML\Configuration;
|
||||
use SAML\Store;
|
||||
|
||||
/**
|
||||
* A memcache based data store.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class Memcache extends Store
|
||||
{
|
||||
/**
|
||||
* This variable contains the session name prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $prefix;
|
||||
|
||||
|
||||
/**
|
||||
* This function implements the constructor for this class. It loads the Memcache configuration.
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
$config = Configuration::getInstance();
|
||||
$this->prefix = $config->getString('memcache_store.prefix', 'simpleSAMLphp');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a value from the data store.
|
||||
*
|
||||
* @param string $type The data type.
|
||||
* @param string $key The key.
|
||||
* @return mixed|null The value.
|
||||
*/
|
||||
public function get($type, $key)
|
||||
{
|
||||
assert(is_string($type));
|
||||
assert(is_string($key));
|
||||
|
||||
return \SAML\Memcache::get($this->prefix . '.' . $type . '.' . $key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save a value to the data store.
|
||||
*
|
||||
* @param string $type The data type.
|
||||
* @param string $key The key.
|
||||
* @param mixed $value The value.
|
||||
* @param int|null $expire The expiration time (unix timestamp), or NULL if it never expires.
|
||||
* @return void
|
||||
*/
|
||||
public function set($type, $key, $value, $expire = null)
|
||||
{
|
||||
assert(is_string($type));
|
||||
assert(is_string($key));
|
||||
assert($expire === null || (is_int($expire) && $expire > 2592000));
|
||||
|
||||
if ($expire === null) {
|
||||
$expire = 0;
|
||||
}
|
||||
|
||||
\SAML\Memcache::set($this->prefix . '.' . $type . '.' . $key, $value, $expire);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a value from the data store.
|
||||
*
|
||||
* @param string $type The data type.
|
||||
* @param string $key The key.
|
||||
* @return void
|
||||
*/
|
||||
public function delete($type, $key)
|
||||
{
|
||||
assert(is_string($type));
|
||||
assert(is_string($key));
|
||||
|
||||
\SAML\Memcache::delete($this->prefix . '.' . $type . '.' . $key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Store;
|
||||
|
||||
use Predis\Client;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Store;
|
||||
|
||||
/**
|
||||
* A data store using Redis to keep the data.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class Redis extends Store
|
||||
{
|
||||
/** @var Client */
|
||||
public $redis;
|
||||
|
||||
/**
|
||||
* Initialize the Redis data store.
|
||||
* @param Client|null $redis
|
||||
*/
|
||||
public function __construct($redis = null)
|
||||
{
|
||||
assert($redis === null || is_subclass_of($redis, Client::class));
|
||||
|
||||
if (!class_exists(Client::class)) {
|
||||
throw new Error\CriticalConfigurationError('predis/predis is not available.');
|
||||
}
|
||||
|
||||
if ($redis === null) {
|
||||
$config = Configuration::getInstance();
|
||||
|
||||
$host = $config->getString('store.redis.host', 'localhost');
|
||||
$port = $config->getInteger('store.redis.port', 6379);
|
||||
$prefix = $config->getString('store.redis.prefix', 'SimpleSAMLphp');
|
||||
$password = $config->getString('store.redis.password', '');
|
||||
$database = $config->getInteger('store.redis.database', 0);
|
||||
|
||||
$redis = new Client(
|
||||
[
|
||||
'scheme' => 'tcp',
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'database' => $database,
|
||||
] + (!empty($password) ? ['password' => $password] : []),
|
||||
[
|
||||
'prefix' => $prefix,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->redis = $redis;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deconstruct the Redis data store.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (method_exists($this->redis, 'disconnect')) {
|
||||
$this->redis->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a value from the data store.
|
||||
*
|
||||
* @param string $type The type of the data.
|
||||
* @param string $key The key to retrieve.
|
||||
*
|
||||
* @return mixed|null The value associated with that key, or null if there's no such key.
|
||||
*/
|
||||
public function get($type, $key)
|
||||
{
|
||||
assert(is_string($type));
|
||||
assert(is_string($key));
|
||||
|
||||
$result = $this->redis->get("{$type}.{$key}");
|
||||
|
||||
if ($result === false || $result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return unserialize($result);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save a value in the data store.
|
||||
*
|
||||
* @param string $type The type of the data.
|
||||
* @param string $key The key to insert.
|
||||
* @param mixed $value The value itself.
|
||||
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
|
||||
* @return void
|
||||
*/
|
||||
public function set($type, $key, $value, $expire = null)
|
||||
{
|
||||
assert(is_string($type));
|
||||
assert(is_string($key));
|
||||
assert($expire === null || (is_int($expire) && $expire > 2592000));
|
||||
|
||||
$serialized = serialize($value);
|
||||
|
||||
if ($expire === null) {
|
||||
$this->redis->set("{$type}.{$key}", $serialized);
|
||||
} else {
|
||||
// setex expire time is in seconds (not unix timestamp)
|
||||
$this->redis->setex("{$type}.{$key}", $expire - time(), $serialized);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete an entry from the data store.
|
||||
*
|
||||
* @param string $type The type of the data
|
||||
* @param string $key The key to delete.
|
||||
* @return void
|
||||
*/
|
||||
public function delete($type, $key)
|
||||
{
|
||||
assert(is_string($type));
|
||||
assert(is_string($key));
|
||||
|
||||
$this->redis->del("{$type}.{$key}");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,411 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Store;
|
||||
|
||||
use Exception;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
use SAML\Store;
|
||||
|
||||
/**
|
||||
* A data store using a RDBMS to keep the data.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class SQL extends Store
|
||||
{
|
||||
/**
|
||||
* The PDO object for our database.
|
||||
*
|
||||
* @var PDO
|
||||
*/
|
||||
public $pdo;
|
||||
|
||||
|
||||
/**
|
||||
* Our database driver.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $driver;
|
||||
|
||||
|
||||
/**
|
||||
* The prefix we should use for our tables.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $prefix;
|
||||
|
||||
|
||||
/**
|
||||
* Associative array of table versions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $tableVersions;
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the SQL data store.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$config = Configuration::getInstance();
|
||||
|
||||
$dsn = $config->getString('store.sql.dsn');
|
||||
$username = $config->getString('store.sql.username', null);
|
||||
$password = $config->getString('store.sql.password', null);
|
||||
$options = $config->getArray('store.sql.options', null);
|
||||
$this->prefix = $config->getString('store.sql.prefix', 'simpleSAMLphp');
|
||||
try {
|
||||
$this->pdo = new PDO($dsn, $username, $password, $options);
|
||||
} catch (PDOException $e) {
|
||||
throw new Exception("Database error: " . $e->getMessage());
|
||||
}
|
||||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$this->driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
|
||||
if ($this->driver === 'mysql') {
|
||||
$this->pdo->exec('SET time_zone = "+00:00"');
|
||||
}
|
||||
|
||||
$this->initTableVersionTable();
|
||||
$this->initKVTable();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the table-version table.
|
||||
* @return void
|
||||
*/
|
||||
private function initTableVersionTable()
|
||||
{
|
||||
$this->tableVersions = [];
|
||||
|
||||
try {
|
||||
$fetchTableVersion = $this->pdo->query('SELECT _name, _version FROM ' . $this->prefix . '_tableVersion');
|
||||
} catch (PDOException $e) {
|
||||
$this->pdo->exec(
|
||||
'CREATE TABLE ' . $this->prefix .
|
||||
'_tableVersion (_name VARCHAR(30) NOT NULL UNIQUE, _version INTEGER NOT NULL)'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
while (($row = $fetchTableVersion->fetch(PDO::FETCH_ASSOC)) !== false) {
|
||||
$this->tableVersions[$row['_name']] = (int) $row['_version'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize key-value table.
|
||||
* @return void
|
||||
*/
|
||||
private function initKVTable()
|
||||
{
|
||||
$current_version = $this->getTableVersion('kvstore');
|
||||
|
||||
$text_t = 'TEXT';
|
||||
$time_field = 'TIMESTAMP';
|
||||
if ($this->driver === 'mysql') {
|
||||
// TEXT data type has size constraints that can be hit at some point, so we use LONGTEXT instead
|
||||
$text_t = 'LONGTEXT';
|
||||
}
|
||||
if ($this->driver === 'sqlsrv') {
|
||||
// TIMESTAMP will not work for MSSQL. TIMESTAMP is automatically generated and cannot be inserted
|
||||
// so we use DATETIME instead
|
||||
$time_field = 'DATETIME';
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries for updates, grouped by version.
|
||||
* New updates can be added as a new array in this array
|
||||
*/
|
||||
$table_updates = [
|
||||
[
|
||||
'CREATE TABLE ' . $this->prefix .
|
||||
'_kvstore (_type VARCHAR(30) NOT NULL, _key VARCHAR(50) NOT NULL, _value ' . $text_t .
|
||||
' NOT NULL, _expire ' . $time_field . ', PRIMARY KEY (_key, _type))',
|
||||
$this->driver === 'sqlite' || $this->driver === 'sqlsrv' || $this->driver === 'pgsql' ?
|
||||
'CREATE INDEX ' . $this->prefix . '_kvstore_expire ON ' . $this->prefix . '_kvstore (_expire)' :
|
||||
'ALTER TABLE ' . $this->prefix . '_kvstore ADD INDEX ' . $this->prefix . '_kvstore_expire (_expire)'
|
||||
],
|
||||
/**
|
||||
* This upgrade removes the default NOT NULL constraint on the _expire field in MySQL.
|
||||
* Because SQLite does not support field alterations, the approach is to:
|
||||
* Create a new table without the NOT NULL constraint
|
||||
* Copy the current data to the new table
|
||||
* Drop the old table
|
||||
* Rename the new table correctly
|
||||
* Read the index
|
||||
*/
|
||||
[
|
||||
'CREATE TABLE ' . $this->prefix .
|
||||
'_kvstore_new (_type VARCHAR(30) NOT NULL, _key VARCHAR(50) NOT NULL, _value ' . $text_t .
|
||||
' NOT NULL, _expire ' . $time_field . ' NULL, PRIMARY KEY (_key, _type))',
|
||||
'INSERT INTO ' . $this->prefix . '_kvstore_new SELECT * FROM ' . $this->prefix . '_kvstore',
|
||||
'DROP TABLE ' . $this->prefix . '_kvstore',
|
||||
// FOR MSSQL use EXEC sp_rename to rename a table (RENAME won't work)
|
||||
$this->driver === 'sqlsrv' ?
|
||||
'EXEC sp_rename ' . $this->prefix . '_kvstore_new, ' . $this->prefix . '_kvstore' :
|
||||
'ALTER TABLE ' . $this->prefix . '_kvstore_new RENAME TO ' . $this->prefix . '_kvstore',
|
||||
$this->driver === 'sqlite' || $this->driver === 'sqlsrv' || $this->driver === 'pgsql' ?
|
||||
'CREATE INDEX ' . $this->prefix . '_kvstore_expire ON ' . $this->prefix . '_kvstore (_expire)' :
|
||||
'ALTER TABLE ' . $this->prefix . '_kvstore ADD INDEX ' . $this->prefix . '_kvstore_expire (_expire)'
|
||||
]
|
||||
];
|
||||
|
||||
$latest_version = count($table_updates);
|
||||
|
||||
if ($current_version == $latest_version) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only run queries for after the current version
|
||||
$updates_to_run = array_slice($table_updates, $current_version);
|
||||
|
||||
foreach ($updates_to_run as $version_updates) {
|
||||
foreach ($version_updates as $query) {
|
||||
$this->pdo->exec($query);
|
||||
}
|
||||
}
|
||||
|
||||
$this->setTableVersion('kvstore', $latest_version);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get table version.
|
||||
*
|
||||
* @param string $name Table name.
|
||||
*
|
||||
* @return int The table version, or 0 if the table doesn't exist.
|
||||
*/
|
||||
public function getTableVersion($name)
|
||||
{
|
||||
assert(is_string($name));
|
||||
|
||||
if (!isset($this->tableVersions[$name])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->tableVersions[$name];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set table version.
|
||||
*
|
||||
* @param string $name Table name.
|
||||
* @param int $version Table version.
|
||||
* @return void
|
||||
*/
|
||||
public function setTableVersion($name, $version)
|
||||
{
|
||||
assert(is_string($name));
|
||||
assert(is_int($version));
|
||||
|
||||
$this->insertOrUpdate(
|
||||
$this->prefix . '_tableVersion',
|
||||
['_name'],
|
||||
['_name' => $name, '_version' => $version]
|
||||
);
|
||||
$this->tableVersions[$name] = $version;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert or update a key-value in the store.
|
||||
*
|
||||
* Since various databases implement different methods for doing this, we abstract it away here.
|
||||
*
|
||||
* @param string $table The table we should update.
|
||||
* @param array $keys The key columns.
|
||||
* @param array $data Associative array with columns.
|
||||
* @return void
|
||||
*/
|
||||
public function insertOrUpdate($table, array $keys, array $data)
|
||||
{
|
||||
assert(is_string($table));
|
||||
|
||||
$colNames = '(' . implode(', ', array_keys($data)) . ')';
|
||||
$values = 'VALUES(:' . implode(', :', array_keys($data)) . ')';
|
||||
|
||||
switch ($this->driver) {
|
||||
case 'mysql':
|
||||
$query = 'REPLACE INTO ' . $table . ' ' . $colNames . ' ' . $values;
|
||||
$query = $this->pdo->prepare($query);
|
||||
$query->execute($data);
|
||||
break;
|
||||
case 'sqlite':
|
||||
$query = 'INSERT OR REPLACE INTO ' . $table . ' ' . $colNames . ' ' . $values;
|
||||
$query = $this->pdo->prepare($query);
|
||||
$query->execute($data);
|
||||
break;
|
||||
default:
|
||||
$updateCols = [];
|
||||
$condCols = [];
|
||||
$condData = [];
|
||||
|
||||
foreach ($data as $col => $value) {
|
||||
$tmp = $col . ' = :' . $col;
|
||||
|
||||
if (in_array($col, $keys, true)) {
|
||||
$condCols[] = $tmp;
|
||||
$condData[$col] = $value;
|
||||
} else {
|
||||
$updateCols[] = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
$selectQuery = 'SELECT * FROM ' . $table . ' WHERE ' . implode(' AND ', $condCols);
|
||||
$selectQuery = $this->pdo->prepare($selectQuery);
|
||||
$selectQuery->execute($condData);
|
||||
|
||||
if (count($selectQuery->fetchAll()) > 0) {
|
||||
// Update
|
||||
$insertOrUpdateQuery = 'UPDATE ' . $table . ' SET ' . implode(',', $updateCols);
|
||||
$insertOrUpdateQuery .= ' WHERE ' . implode(' AND ', $condCols);
|
||||
$insertOrUpdateQuery = $this->pdo->prepare($insertOrUpdateQuery);
|
||||
} else {
|
||||
// Insert
|
||||
$insertOrUpdateQuery = 'INSERT INTO ' . $table . ' ' . $colNames . ' ' . $values;
|
||||
$insertOrUpdateQuery = $this->pdo->prepare($insertOrUpdateQuery);
|
||||
}
|
||||
$insertOrUpdateQuery->execute($data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clean the key-value table of expired entries.
|
||||
* @return void
|
||||
*/
|
||||
private function cleanKVStore()
|
||||
{
|
||||
Logger::debug('store.sql: Cleaning key-value store.');
|
||||
|
||||
$query = 'DELETE FROM ' . $this->prefix . '_kvstore WHERE _expire < :now';
|
||||
$params = ['now' => gmdate('Y-m-d H:i:s')];
|
||||
|
||||
$query = $this->pdo->prepare($query);
|
||||
$query->execute($params);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a value from the data store.
|
||||
*
|
||||
* @param string $type The type of the data.
|
||||
* @param string $key The key to retrieve.
|
||||
*
|
||||
* @return mixed|null The value associated with that key, or null if there's no such key.
|
||||
*/
|
||||
public function get($type, $key)
|
||||
{
|
||||
assert(is_string($type));
|
||||
assert(is_string($key));
|
||||
|
||||
if (strlen($key) > 50) {
|
||||
$key = sha1($key);
|
||||
}
|
||||
|
||||
$query = 'SELECT _value FROM ' . $this->prefix .
|
||||
'_kvstore WHERE _type = :type AND _key = :key AND (_expire IS NULL OR _expire > :now)';
|
||||
$params = ['type' => $type, 'key' => $key, 'now' => gmdate('Y-m-d H:i:s')];
|
||||
|
||||
$query = $this->pdo->prepare($query);
|
||||
$query->execute($params);
|
||||
|
||||
$row = $query->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $row['_value'];
|
||||
if (is_resource($value)) {
|
||||
$value = stream_get_contents($value);
|
||||
}
|
||||
$value = urldecode($value);
|
||||
$value = unserialize($value);
|
||||
|
||||
if ($value === false) {
|
||||
return null;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save a value in the data store.
|
||||
*
|
||||
* @param string $type The type of the data.
|
||||
* @param string $key The key to insert.
|
||||
* @param mixed $value The value itself.
|
||||
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
|
||||
* @return void
|
||||
*/
|
||||
public function set($type, $key, $value, $expire = null)
|
||||
{
|
||||
assert(is_string($type));
|
||||
assert(is_string($key));
|
||||
assert($expire === null || (is_int($expire) && $expire > 2592000));
|
||||
|
||||
if (rand(0, 1000) < 10) {
|
||||
$this->cleanKVStore();
|
||||
}
|
||||
|
||||
if (strlen($key) > 50) {
|
||||
$key = sha1($key);
|
||||
}
|
||||
|
||||
if ($expire !== null) {
|
||||
$expire = gmdate('Y-m-d H:i:s', $expire);
|
||||
}
|
||||
|
||||
$value = serialize($value);
|
||||
$value = rawurlencode($value);
|
||||
|
||||
$data = [
|
||||
'_type' => $type,
|
||||
'_key' => $key,
|
||||
'_value' => $value,
|
||||
'_expire' => $expire,
|
||||
];
|
||||
|
||||
$this->insertOrUpdate($this->prefix . '_kvstore', ['_type', '_key'], $data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete an entry from the data store.
|
||||
*
|
||||
* @param string $type The type of the data
|
||||
* @param string $key The key to delete.
|
||||
* @return void
|
||||
*/
|
||||
public function delete($type, $key)
|
||||
{
|
||||
assert(is_string($type));
|
||||
assert(is_string($key));
|
||||
|
||||
if (strlen($key) > 50) {
|
||||
$key = sha1($key);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'_type' => $type,
|
||||
'_key' => $key,
|
||||
];
|
||||
|
||||
$query = 'DELETE FROM ' . $this->prefix . '_kvstore WHERE _type=:_type AND _key=:_key';
|
||||
$query = $this->pdo->prepare($query);
|
||||
$query->execute($data);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,886 @@
|
|||
<?php
|
||||
|
||||
namespace SAML;
|
||||
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use Exception;
|
||||
use SAML\Auth\State;
|
||||
use SAML\Error\Error;
|
||||
use SAML\Utils\Arrays;
|
||||
use SAML\Utils\Attributes;
|
||||
use SAML\Utils\Auth;
|
||||
use SAML\Utils\Config;
|
||||
use SAML\Utils\Config\Metadata;
|
||||
use SAML\Utils\Crypto;
|
||||
use SAML\Utils\HTTP;
|
||||
use SAML\Utils\Net;
|
||||
use SAML\Utils\Random;
|
||||
use SAML\Utils\System;
|
||||
use SAML\Utils\Time;
|
||||
use SAML\Utils\XML;
|
||||
use SAML\XML\Validator;
|
||||
use SAML2\Utils;
|
||||
|
||||
/**
|
||||
* Misc static functions that is used several places.in example parsing and id generation.
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*
|
||||
* @deprecated This entire class will be removed in SimpleSAMLphp 2.0.
|
||||
*/
|
||||
|
||||
class Utilities
|
||||
{
|
||||
/**
|
||||
* @deprecated This property will be removed in SSP 2.0. Please use SimpleSAML\Logger::isErrorMasked() instead.
|
||||
* @var int
|
||||
*/
|
||||
public static $logMask = 0;
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfHost() instead.
|
||||
* @return string
|
||||
*/
|
||||
public static function getSelfHost()
|
||||
{
|
||||
return HTTP::getSelfHost();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURLHost() instead.
|
||||
* @return string
|
||||
*/
|
||||
public static function selfURLhost()
|
||||
{
|
||||
return HTTP::getSelfURLHost();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::isHTTPS() instead.
|
||||
* @return bool
|
||||
*/
|
||||
public static function isHTTPS()
|
||||
{
|
||||
return HTTP::isHTTPS();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURLNoQuery()
|
||||
* instead.
|
||||
* @return string
|
||||
*/
|
||||
public static function selfURLNoQuery()
|
||||
{
|
||||
return HTTP::getSelfURLNoQuery();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfHostWithPath()
|
||||
* instead.
|
||||
* @return string
|
||||
*/
|
||||
public static function getSelfHostWithPath()
|
||||
{
|
||||
return HTTP::getSelfHostWithPath();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getFirstPathElement()
|
||||
* instead.
|
||||
* @param bool $trailingslash
|
||||
* @return string
|
||||
*/
|
||||
public static function getFirstPathElement($trailingslash = true)
|
||||
{
|
||||
return HTTP::getFirstPathElement($trailingslash);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURL() instead.
|
||||
* @return string
|
||||
*/
|
||||
public static function selfURL()
|
||||
{
|
||||
return HTTP::getSelfURL();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getBaseURL() instead.
|
||||
* @return string
|
||||
*/
|
||||
public static function getBaseURL()
|
||||
{
|
||||
return HTTP::getBaseURL();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::addURLParameters() instead.
|
||||
* @param string $url
|
||||
* @param array $parameters
|
||||
* @return string
|
||||
*/
|
||||
public static function addURLparameter($url, $parameters)
|
||||
{
|
||||
return HTTP::addURLParameters($url, $parameters);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\HTTP::checkURLAllowed() instead.
|
||||
* @param string $url
|
||||
* @param array|null $trustedSites
|
||||
* @return string
|
||||
*/
|
||||
public static function checkURLAllowed($url, array $trustedSites = null)
|
||||
{
|
||||
return HTTP::checkURLAllowed($url, $trustedSites);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Auth\State::parseStateID() instead.
|
||||
* @param string $stateId
|
||||
* @return array
|
||||
*/
|
||||
public static function parseStateID($stateId)
|
||||
{
|
||||
return State::parseStateID($stateId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0.
|
||||
* @param string|null $start
|
||||
* @param string|null $end
|
||||
* @return bool
|
||||
*/
|
||||
public static function checkDateConditions($start = null, $end = null)
|
||||
{
|
||||
$currentTime = time();
|
||||
|
||||
if (!empty($start)) {
|
||||
$startTime = Utils::xsDateTimeToTimestamp($start);
|
||||
// Allow for a 10 minute difference in Time
|
||||
if (($startTime < 0) || (($startTime - 600) > $currentTime)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!empty($end)) {
|
||||
$endTime = Utils::xsDateTimeToTimestamp($end);
|
||||
if (($endTime < 0) || ($endTime <= $currentTime)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Random::generateID() instead.
|
||||
* @return string
|
||||
*/
|
||||
public static function generateID()
|
||||
{
|
||||
return Random::generateID();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\Time::generateTimestamp()
|
||||
* instead.
|
||||
* @param int|null $instant
|
||||
* @return string
|
||||
*/
|
||||
public static function generateTimestamp($instant = null)
|
||||
{
|
||||
return Time::generateTimestamp($instant);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\Time::parseDuration() instead.
|
||||
* @param string $duration
|
||||
* @param int|null $timestamp
|
||||
* @return int
|
||||
*/
|
||||
public static function parseDuration($duration, $timestamp = null)
|
||||
{
|
||||
return Time::parseDuration($duration, $timestamp);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $trackId
|
||||
* @param int|null $errorCode
|
||||
* @param Exception|null $e
|
||||
* @return void
|
||||
*@throws Error
|
||||
* @deprecated This method will be removed in SSP 2.0. Please raise a SimpleSAML\Error\Error exception instead.
|
||||
*/
|
||||
public static function fatalError($trackId = 'na', $errorCode = null, Exception $e = null)
|
||||
{
|
||||
throw new Error($errorCode, $e);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in version 2.0. Use SimpleSAML\Utils\Net::ipCIDRcheck() instead.
|
||||
* @param string $cidr
|
||||
* @param string|null $ip
|
||||
* @return bool
|
||||
*/
|
||||
public static function ipCIDRcheck($cidr, $ip = null)
|
||||
{
|
||||
return Net::ipCIDRcheck($cidr, $ip);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param array $parameters
|
||||
* @return void
|
||||
*/
|
||||
private static function doRedirect($url, $parameters = [])
|
||||
{
|
||||
assert(is_string($url));
|
||||
assert(!empty($url));
|
||||
assert(is_array($parameters));
|
||||
|
||||
if (!empty($parameters)) {
|
||||
$url = self::addURLparameter($url, $parameters);
|
||||
}
|
||||
|
||||
/* Set the HTTP result code. This is either 303 See Other or
|
||||
* 302 Found. HTTP 303 See Other is sent if the HTTP version
|
||||
* is HTTP/1.1 and the request type was a POST request.
|
||||
*/
|
||||
if ($_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1' &&
|
||||
$_SERVER['REQUEST_METHOD'] === 'POST'
|
||||
) {
|
||||
$code = 303;
|
||||
} else {
|
||||
$code = 302;
|
||||
}
|
||||
|
||||
if (strlen($url) > 2048) {
|
||||
Logger::warning('Redirecting to a URL longer than 2048 bytes.');
|
||||
}
|
||||
|
||||
// Set the location header
|
||||
header('Location: '.$url, true, $code);
|
||||
|
||||
// Disable caching of this response
|
||||
header('Pragma: no-cache');
|
||||
header('Cache-Control: no-cache, must-revalidate');
|
||||
|
||||
// Show a minimal web page with a clickable link to the URL
|
||||
echo '<?xml version="1.0" encoding="UTF-8"?>'."\n";
|
||||
echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'.
|
||||
' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'."\n";
|
||||
echo '<html xmlns="http://www.w3.org/1999/xhtml">';
|
||||
echo '<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Redirect</title>
|
||||
</head>';
|
||||
echo '<body>';
|
||||
echo '<h1>Redirect</h1>';
|
||||
echo '<p>';
|
||||
echo 'You were redirected to: ';
|
||||
echo '<a id="redirlink" href="'.
|
||||
htmlspecialchars($url).'">'.htmlspecialchars($url).'</a>';
|
||||
echo '<script type="text/javascript">document.getElementById("redirlink").focus();</script>';
|
||||
echo '</p>';
|
||||
echo '</body>';
|
||||
echo '</html>';
|
||||
|
||||
// End script execution
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated 1.12.0 This method will be removed from the API. Instead, use the redirectTrustedURL() or
|
||||
* redirectUntrustedURL() functions accordingly.
|
||||
* @param string $url
|
||||
* @param array $parameters
|
||||
* @param array|null $allowed_redirect_hosts
|
||||
* @return void
|
||||
*/
|
||||
public static function redirect($url, $parameters = [], $allowed_redirect_hosts = null)
|
||||
{
|
||||
assert(is_string($url));
|
||||
assert(strlen($url) > 0);
|
||||
assert(is_array($parameters));
|
||||
|
||||
if ($allowed_redirect_hosts !== null) {
|
||||
$url = self::checkURLAllowed($url, $allowed_redirect_hosts);
|
||||
} else {
|
||||
$url = self::normalizeURL($url);
|
||||
}
|
||||
self::doRedirect($url, $parameters);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::redirectTrustedURL()
|
||||
* instead.
|
||||
* @param string $url
|
||||
* @param array $parameters
|
||||
* @return void
|
||||
*/
|
||||
public static function redirectTrustedURL($url, $parameters = [])
|
||||
{
|
||||
HTTP::redirectTrustedURL($url, $parameters);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::redirectUntrustedURL()
|
||||
* instead.
|
||||
* @param string $url
|
||||
* @param array $parameters
|
||||
* @return void
|
||||
*/
|
||||
public static function redirectUntrustedURL($url, $parameters = [])
|
||||
{
|
||||
HTTP::redirectUntrustedURL($url, $parameters);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Arrays::transpose() instead.
|
||||
* @param array $in
|
||||
* @return mixed
|
||||
*/
|
||||
public static function transposeArray($in)
|
||||
{
|
||||
return Arrays::transpose($in);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::isDOMNodeOfType()
|
||||
* instead.
|
||||
* @param DOMNode $element
|
||||
* @param string $name
|
||||
* @param string $nsURI
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDOMElementOfType(DOMNode $element, $name, $nsURI)
|
||||
{
|
||||
return XML::isDOMNodeOfType($element, $name, $nsURI);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::getDOMChildren() instead.
|
||||
* @param DOMElement $element
|
||||
* @param string $localName
|
||||
* @param string $namespaceURI
|
||||
* @return array
|
||||
*/
|
||||
public static function getDOMChildren(DOMElement $element, $localName, $namespaceURI)
|
||||
{
|
||||
return XML::getDOMChildren($element, $localName, $namespaceURI);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::getDOMText() instead.
|
||||
* @param DOMNode $element
|
||||
* @return string
|
||||
*/
|
||||
public static function getDOMText($element)
|
||||
{
|
||||
return XML::getDOMText($element);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getAcceptLanguage()
|
||||
* instead.
|
||||
* @return array
|
||||
*/
|
||||
public static function getAcceptLanguage()
|
||||
{
|
||||
return HTTP::getAcceptLanguage();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::isValid() instead.
|
||||
* @param string $xml
|
||||
* @param string $schema
|
||||
* @return string|false
|
||||
*/
|
||||
public static function validateXML($xml, $schema)
|
||||
{
|
||||
$result = XML::isValid($xml, $schema);
|
||||
return ($result === true) ? '' : $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::checkSAMLMessage() instead.
|
||||
* @param string $message
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public static function validateXMLDocument($message, $type)
|
||||
{
|
||||
XML::checkSAMLMessage($message, $type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use openssl_random_pseudo_bytes() instead.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public static function generateRandomBytes($length)
|
||||
{
|
||||
assert(is_int($length));
|
||||
|
||||
return openssl_random_pseudo_bytes($length);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use bin2hex() instead.
|
||||
* @param string $bytes
|
||||
* @return string
|
||||
*/
|
||||
public static function stringToHex($bytes)
|
||||
{
|
||||
$ret = '';
|
||||
for ($i = 0; $i < strlen($bytes); $i++) {
|
||||
$ret .= sprintf('%02x', ord($bytes[$i]));
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::resolvePath() instead.
|
||||
* @param string $path
|
||||
* @param string|null $base
|
||||
* @return string
|
||||
*/
|
||||
public static function resolvePath($path, $base = null)
|
||||
{
|
||||
return System::resolvePath($path, $base);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::resolveURL() instead.
|
||||
* @param string $url
|
||||
* @param string|null $base
|
||||
* @return string
|
||||
*/
|
||||
public static function resolveURL($url, $base = null)
|
||||
{
|
||||
return HTTP::resolveURL($url, $base);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::normalizeURL() instead.
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
public static function normalizeURL($url)
|
||||
{
|
||||
return HTTP::normalizeURL($url);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::parseQueryString() instead.
|
||||
* @param string $query_string
|
||||
* @return array
|
||||
*/
|
||||
public static function parseQueryString($query_string)
|
||||
{
|
||||
return HTTP::parseQueryString($query_string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use
|
||||
* SimpleSAML\Utils\Attributes::normalizeAttributesArray() instead.
|
||||
* @param array $attributes
|
||||
* @return array
|
||||
*/
|
||||
public static function parseAttributes($attributes)
|
||||
{
|
||||
return Attributes::normalizeAttributesArray($attributes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Config::getSecretSalt() instead.
|
||||
* @return string
|
||||
*/
|
||||
public static function getSecretSalt()
|
||||
{
|
||||
return Config::getSecretSalt();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please call error_get_last() directly.
|
||||
* @return string
|
||||
*/
|
||||
public static function getLastError()
|
||||
{
|
||||
|
||||
if (!function_exists('error_get_last')) {
|
||||
return '[Cannot get error message]';
|
||||
}
|
||||
|
||||
$error = error_get_last();
|
||||
if ($error === null) {
|
||||
return '[No error message found]';
|
||||
}
|
||||
|
||||
return $error['message'];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Config::getCertPath() instead.
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
public static function resolveCert($path)
|
||||
{
|
||||
return Config::getCertPath($path);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Configuration $metadata
|
||||
* @param bool $required
|
||||
* @param string $prefix
|
||||
* @return array|null
|
||||
*@deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::loadPublicKey() instead.
|
||||
*/
|
||||
public static function loadPublicKey(Configuration $metadata, $required = false, $prefix = '')
|
||||
{
|
||||
return Crypto::loadPublicKey($metadata, $required, $prefix);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Configuration $metadata
|
||||
* @param bool $required
|
||||
* @param string $prefix
|
||||
* @return array|null
|
||||
*@deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::loadPrivateKey() instead.
|
||||
*/
|
||||
public static function loadPrivateKey(Configuration $metadata, $required = false, $prefix = '')
|
||||
{
|
||||
return Crypto::loadPrivateKey($metadata, $required, $prefix);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::formatDOMElement() instead.
|
||||
* @param DOMElement $root
|
||||
* @param string $indentBase
|
||||
* @return void
|
||||
*/
|
||||
public static function formatDOMElement(DOMElement $root, $indentBase = '')
|
||||
{
|
||||
XML::formatDOMElement($root, $indentBase);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::formatXMLString() instead.
|
||||
* @param string $xml
|
||||
* @param string $indentBase
|
||||
* @return string
|
||||
*/
|
||||
public static function formatXMLString($xml, $indentBase = '')
|
||||
{
|
||||
return XML::formatXMLString($xml, $indentBase);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Arrays::arrayize() instead.
|
||||
* @param mixed $data
|
||||
* @param int $index
|
||||
* @return array
|
||||
*/
|
||||
public static function arrayize($data, $index = 0)
|
||||
{
|
||||
return Arrays::arrayize($data, $index);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::isAdmin() instead.
|
||||
* @return bool
|
||||
*/
|
||||
public static function isAdmin()
|
||||
{
|
||||
return Auth::isAdmin();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::getAdminLoginURL instead();
|
||||
* @param string|null $returnTo
|
||||
* @return string
|
||||
*/
|
||||
public static function getAdminLoginURL($returnTo = null)
|
||||
{
|
||||
return Auth::getAdminLoginURL($returnTo);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::requireAdmin() instead.
|
||||
* @return void
|
||||
*/
|
||||
public static function requireAdmin()
|
||||
{
|
||||
Auth::requireAdmin();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::submitPOSTData() instead.
|
||||
* @param string $destination
|
||||
* @param array $post
|
||||
* @return void
|
||||
*/
|
||||
public static function postRedirect($destination, $post)
|
||||
{
|
||||
HTTP::submitPOSTData($destination, $post);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. PLease use SimpleSAML\Utils\HTTP::getPOSTRedirectURL()
|
||||
* instead.
|
||||
* @param string $destination
|
||||
* @param array $post
|
||||
* @return string
|
||||
*/
|
||||
public static function createPostRedirectLink($destination, $post)
|
||||
{
|
||||
return HTTP::getPOSTRedirectURL($destination, $post);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getPOSTRedirectURL()
|
||||
* instead.
|
||||
* @param string $destination
|
||||
* @param array $post
|
||||
* @return string
|
||||
* @throws Error If the current session is a transient session.
|
||||
*/
|
||||
public static function createHttpPostRedirectLink($destination, $post)
|
||||
{
|
||||
assert(is_string($destination));
|
||||
assert(is_array($post));
|
||||
|
||||
$postId = Random::generateID();
|
||||
$postData = [
|
||||
'post' => $post,
|
||||
'url' => $destination,
|
||||
];
|
||||
|
||||
$session = Session::getSessionFromRequest();
|
||||
if ($session->isTransient()) {
|
||||
throw new Error('Cannot save data to a transient session');
|
||||
}
|
||||
|
||||
$session->setData('core_postdatalink', $postId, $postData);
|
||||
|
||||
$redirInfo = base64_encode(Crypto::aesEncrypt($session->getSessionId().':'.$postId));
|
||||
|
||||
$url = Module::getModuleURL('core/postredirect.php', ['RedirInfo' => $redirInfo]);
|
||||
$url = preg_replace("#^https:#", "http:", $url);
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0.
|
||||
* @param string $certificate
|
||||
* @param string $caFile
|
||||
* @return void
|
||||
*/
|
||||
public static function validateCA($certificate, $caFile)
|
||||
{
|
||||
Validator::validateCertificate($certificate, $caFile);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Time::initTimezone() instead.
|
||||
* @return void
|
||||
*/
|
||||
public static function initTimezone()
|
||||
{
|
||||
Time::initTimezone();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::writeFile() instead.
|
||||
* @param string $filename
|
||||
* @param string $data
|
||||
* @param int $mode
|
||||
* @return void
|
||||
*/
|
||||
public static function writeFile($filename, $data, $mode = 0600)
|
||||
{
|
||||
System::writeFile($filename, $data, $mode);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::getTempDir instead.
|
||||
* @return string
|
||||
*/
|
||||
public static function getTempDir()
|
||||
{
|
||||
return System::getTempDir();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Logger::maskErrors() instead.
|
||||
* @param int $mask
|
||||
* @return void
|
||||
*/
|
||||
public static function maskErrors($mask)
|
||||
{
|
||||
Logger::maskErrors($mask);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Logger::popErrorMask() instead.
|
||||
* @return void
|
||||
*/
|
||||
public static function popErrorMask()
|
||||
{
|
||||
Logger::popErrorMask();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use
|
||||
* SimpleSAML\Utils\Config\Metadata::getDefaultEndpoint() instead.
|
||||
* @param array $endpoints
|
||||
* @param array|null $bindings
|
||||
* @return array|null
|
||||
*/
|
||||
public static function getDefaultEndpoint(array $endpoints, array $bindings = null)
|
||||
{
|
||||
return Metadata::getDefaultEndpoint($endpoints, $bindings);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::checkSessionCookie()
|
||||
* instead.
|
||||
* @param string|null $retryURL
|
||||
* @return void
|
||||
*/
|
||||
public static function checkCookie($retryURL = null)
|
||||
{
|
||||
HTTP::checkSessionCookie($retryURL);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|DOMElement $message
|
||||
* @param string $type
|
||||
* @return void
|
||||
*@deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::debugSAMLMessage() instead.
|
||||
*/
|
||||
public static function debugMessage($message, $type)
|
||||
{
|
||||
XML::debugSAMLMessage($message, $type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::fetch() instead.
|
||||
* @param string $path
|
||||
* @param array $context
|
||||
* @param bool $getHeaders
|
||||
* @return string|array
|
||||
*/
|
||||
public static function fetch($path, $context = [], $getHeaders = false)
|
||||
{
|
||||
return HTTP::fetch($path, $context, $getHeaders);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::aesEncrypt() instead.
|
||||
* @param string $clear
|
||||
* @return string
|
||||
*/
|
||||
public static function aesEncrypt($clear)
|
||||
{
|
||||
return Crypto::aesEncrypt($clear);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::aesDecrypt() instead.
|
||||
* @param string $encData
|
||||
* @return string
|
||||
*/
|
||||
public static function aesDecrypt($encData)
|
||||
{
|
||||
return Crypto::aesDecrypt($encData);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::getOS() instead.
|
||||
* @return bool
|
||||
*/
|
||||
public static function isWindowsOS()
|
||||
{
|
||||
return System::getOS() === System::WINDOWS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::setCookie() instead.
|
||||
* @param string $name
|
||||
* @param string|null $value
|
||||
* @param array|null $params
|
||||
* @param bool $throw
|
||||
* @return void
|
||||
*/
|
||||
public static function setCookie($name, $value, array $params = null, $throw = true)
|
||||
{
|
||||
HTTP::setCookie($name, $value, $params, $throw);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
/**
|
||||
* Array-related utility methods.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class Arrays
|
||||
{
|
||||
/**
|
||||
* Put a non-array variable into an array.
|
||||
*
|
||||
* @param mixed $data The data to place into an array.
|
||||
* @param mixed $index The index or key of the array where to place the data. Defaults to 0.
|
||||
*
|
||||
* @return array An array with one element containing $data, with key $index, or $data itself if it's already an
|
||||
* array.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function arrayize($data, $index = 0)
|
||||
{
|
||||
return (is_array($data)) ? $data : [$index => $data];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function transposes a two-dimensional array, so that $a['k1']['k2'] becomes $a['k2']['k1'].
|
||||
*
|
||||
* @param array $array The two-dimensional array to transpose.
|
||||
*
|
||||
* @return mixed The transposed array, or false if $array is not a valid two-dimensional array.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
*/
|
||||
public static function transpose($array)
|
||||
{
|
||||
if (!is_array($array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ret = [];
|
||||
foreach ($array as $k1 => $a2) {
|
||||
if (!is_array($a2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($a2 as $k2 => $v) {
|
||||
if (!array_key_exists($k2, $ret)) {
|
||||
$ret[$k2] = [];
|
||||
}
|
||||
$ret[$k2][$k1] = $v;
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SAML\Error;
|
||||
use SAML\Error\Exception;
|
||||
|
||||
/**
|
||||
* Attribute-related utility methods.
|
||||
*
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
* @package SimpleSAML
|
||||
*/
|
||||
|
||||
class Attributes
|
||||
{
|
||||
/**
|
||||
* Look for an attribute in a normalized attributes array, failing if it's not there.
|
||||
*
|
||||
* @param array $attributes The normalized array containing attributes.
|
||||
* @param string $expected The name of the attribute we are looking for.
|
||||
* @param bool $allow_multiple Whether to allow multiple values in the attribute or not.
|
||||
*
|
||||
* @return mixed The value of the attribute we are expecting. If the attribute has multiple values and
|
||||
* $allow_multiple is set to true, the first value will be returned.
|
||||
*
|
||||
* @throws InvalidArgumentException If $attributes is not an array or $expected is not a string.
|
||||
* @throws Exception If the expected attribute was not found in the attributes array.
|
||||
*/
|
||||
public static function getExpectedAttribute($attributes, $expected, $allow_multiple = false)
|
||||
{
|
||||
if (!is_array($attributes)) {
|
||||
throw new InvalidArgumentException(
|
||||
'The attributes array is not an array, it is: ' . print_r($attributes, true) . '.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!is_string($expected)) {
|
||||
throw new InvalidArgumentException(
|
||||
'The expected attribute is not a string, it is: ' . print_r($expected, true) . '.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!array_key_exists($expected, $attributes)) {
|
||||
throw new Exception("No such attribute '" . $expected . "' found.");
|
||||
}
|
||||
$attribute = $attributes[$expected];
|
||||
|
||||
if (!is_array($attribute)) {
|
||||
throw new InvalidArgumentException('The attributes array is not normalized, values should be arrays.');
|
||||
}
|
||||
|
||||
if (count($attribute) === 0) {
|
||||
throw new Exception("Empty attribute '" . $expected . "'.'");
|
||||
} elseif (count($attribute) > 1) {
|
||||
if ($allow_multiple === false) {
|
||||
throw new Exception(
|
||||
'More than one value found for the attribute, multiple values not allowed.'
|
||||
);
|
||||
}
|
||||
}
|
||||
return reset($attribute);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Validate and normalize an array with attributes.
|
||||
*
|
||||
* This function takes in an associative array with attributes, and parses and validates
|
||||
* this array. On success, it will return a normalized array, where each attribute name
|
||||
* is an index to an array of one or more strings. On failure an exception will be thrown.
|
||||
* This exception will contain an message describing what is wrong.
|
||||
*
|
||||
* @param array $attributes The array containing attributes that we should validate and normalize.
|
||||
*
|
||||
* @return array The normalized attributes array.
|
||||
* @throws InvalidArgumentException If input is not an array, array keys are not strings or attribute values are
|
||||
* not strings.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function normalizeAttributesArray($attributes)
|
||||
{
|
||||
if (!is_array($attributes)) {
|
||||
throw new InvalidArgumentException(
|
||||
'The attributes array is not an array, it is: ' . print_r($attributes, true) . '".'
|
||||
);
|
||||
}
|
||||
|
||||
$newAttrs = [];
|
||||
foreach ($attributes as $name => $values) {
|
||||
if (!is_string($name)) {
|
||||
throw new InvalidArgumentException('Invalid attribute name: "' . print_r($name, true) . '".');
|
||||
}
|
||||
|
||||
$values = Arrays::arrayize($values);
|
||||
|
||||
foreach ($values as $value) {
|
||||
if (!is_string($value)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Invalid attribute value for attribute ' . $name . ': "' . print_r($value, true) . '".'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$newAttrs[$name] = $values;
|
||||
}
|
||||
|
||||
return $newAttrs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract an attribute's namespace, or revert to default.
|
||||
*
|
||||
* This function takes in a namespaced attribute name and splits it in a namespace/attribute name tuple.
|
||||
* When no namespace is found in the attribute name, it will be namespaced with the default namespace.
|
||||
* This default namespace can be overriden by supplying a second parameter to this function.
|
||||
*
|
||||
* @param string $name The namespaced attribute name.
|
||||
* @param string $defaultns The default namespace that should be used when no namespace is found.
|
||||
*
|
||||
* @return array The attribute name, split to the namespace and the actual attribute name.
|
||||
*/
|
||||
public static function getAttributeNamespace($name, $defaultns)
|
||||
{
|
||||
$slash = strrpos($name, '/');
|
||||
if ($slash !== false) {
|
||||
$defaultns = substr($name, 0, $slash);
|
||||
$name = substr($name, $slash + 1);
|
||||
}
|
||||
return [htmlspecialchars($defaultns), htmlspecialchars($name)];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SAML\Auth as Authentication;
|
||||
use SAML\Error;
|
||||
use SAML\Error\Exception;
|
||||
use SAML\Module;
|
||||
use SAML\Session;
|
||||
|
||||
/**
|
||||
* Auth-related utility methods.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
/**
|
||||
* Retrieve a admin login URL.
|
||||
*
|
||||
* @param string|NULL $returnTo The URL the user should arrive on after admin authentication. Defaults to null.
|
||||
*
|
||||
* @return string A URL which can be used for admin authentication.
|
||||
* @throws InvalidArgumentException If $returnTo is neither a string nor null.
|
||||
*/
|
||||
public static function getAdminLoginURL($returnTo = null)
|
||||
{
|
||||
if (!(is_string($returnTo) || is_null($returnTo))) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
|
||||
if ($returnTo === null) {
|
||||
$returnTo = HTTP::getSelfURL();
|
||||
}
|
||||
|
||||
return Module::getModuleURL('core/login-admin.php', ['ReturnTo' => $returnTo]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a admin logout URL.
|
||||
*
|
||||
* @param string|NULL $returnTo The URL the user should arrive on after admin authentication. Defaults to null.
|
||||
*
|
||||
* @return string A URL which can be used for logging out.
|
||||
* @throws InvalidArgumentException If $returnTo is neither a string nor null.
|
||||
*/
|
||||
public static function getAdminLogoutURL($returnTo = null)
|
||||
{
|
||||
if (!(is_string($returnTo) || is_null($returnTo))) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
|
||||
$as = new Authentication\Simple('admin');
|
||||
return $as->getLogoutURL($returnTo = null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check whether the current user is admin.
|
||||
*
|
||||
* @return boolean True if the current user is an admin user, false otherwise.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function isAdmin()
|
||||
{
|
||||
$session = Session::getSessionFromRequest();
|
||||
return $session->isValid('admin') || $session->isValid('login-admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Require admin access to the current page.
|
||||
*
|
||||
* This is a helper function for limiting a page to those with administrative access. It will redirect the user to
|
||||
* a login page if the current user doesn't have admin access.
|
||||
*
|
||||
* @return void This function will only return if the user is admin.
|
||||
* @throws Exception If no "admin" authentication source was configured.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function requireAdmin()
|
||||
{
|
||||
if (self::isAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// not authenticated as admin user, start authentication
|
||||
if (Authentication\Source::getById('admin') !== null) {
|
||||
$as = new Authentication\Simple('admin');
|
||||
$as->login();
|
||||
} else {
|
||||
throw new Exception(
|
||||
'Cannot find "admin" auth source, and admin privileges are required.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
/**
|
||||
* Indicates an implementation caches state internally and may be cleared.
|
||||
*
|
||||
* Primarily designed to allow SSP state to be cleared between unit tests.
|
||||
* @package SimpleSAML\Utils
|
||||
*/
|
||||
interface ClearableState
|
||||
{
|
||||
/**
|
||||
* Clear any cached internal state.
|
||||
*/
|
||||
public static function clearInternalState();
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SAML\Configuration;
|
||||
|
||||
/**
|
||||
* Utility class for SimpleSAMLphp configuration management and manipulation.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
/**
|
||||
* Resolves a path that may be relative to the cert-directory.
|
||||
*
|
||||
* @param string $path The (possibly relative) path to the file.
|
||||
*
|
||||
* @return string The file path.
|
||||
* @throws InvalidArgumentException If $path is not a string.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function getCertPath($path)
|
||||
{
|
||||
if (!is_string($path)) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
|
||||
$globalConfig = Configuration::getInstance();
|
||||
$base = $globalConfig->getPathValue('certdir', 'cert/');
|
||||
return System::resolvePath($path, $base);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the secret salt.
|
||||
*
|
||||
* This function retrieves the value which is configured as the secret salt. It will check that the value exists
|
||||
* and is set to a non-default value. If it isn't, an exception will be thrown.
|
||||
*
|
||||
* The secret salt can be used as a component in hash functions, to make it difficult to test all possible values
|
||||
* in order to retrieve the original value. It can also be used as a simple method for signing data, by hashing the
|
||||
* data together with the salt.
|
||||
*
|
||||
* @return string The secret salt.
|
||||
* @throws InvalidArgumentException If the secret salt hasn't been configured.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function getSecretSalt()
|
||||
{
|
||||
$secretSalt = Configuration::getInstance()->getString('secretsalt');
|
||||
if ($secretSalt === 'defaultsecretsalt') {
|
||||
throw new InvalidArgumentException('The "secretsalt" configuration option must be set to a secret value.');
|
||||
}
|
||||
|
||||
return $secretSalt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the config dir
|
||||
*
|
||||
* If the SIMPLESAMLPHP_CONFIG_DIR environment variable has been set, it takes precedence over the default
|
||||
* $simplesamldir/config directory.
|
||||
*
|
||||
* @return string The path to the configuration directory.
|
||||
*/
|
||||
public static function getConfigDir()
|
||||
{
|
||||
$configDir = dirname(dirname(dirname(__DIR__))) . '/config';
|
||||
/** @var string|false $configDirEnv */
|
||||
$configDirEnv = getenv('SIMPLESAMLPHP_CONFIG_DIR');
|
||||
|
||||
if ($configDirEnv === false) {
|
||||
$configDirEnv = getenv('REDIRECT_SIMPLESAMLPHP_CONFIG_DIR');
|
||||
}
|
||||
|
||||
if ($configDirEnv !== false) {
|
||||
if (!is_dir($configDirEnv)) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
'Config directory specified by environment variable SIMPLESAMLPHP_CONFIG_DIR is not a ' .
|
||||
'directory. Given: "%s"',
|
||||
$configDirEnv
|
||||
)
|
||||
);
|
||||
}
|
||||
$configDir = $configDirEnv;
|
||||
}
|
||||
|
||||
return $configDir;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,343 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils\Config;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SAML2\Constants;
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
|
||||
/**
|
||||
* Class with utilities to fetch different configuration objects from metadata configuration arrays.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
* @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
class Metadata
|
||||
{
|
||||
/**
|
||||
* The string that identities Entity Categories.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $ENTITY_CATEGORY = 'http://macedir.org/entity-category';
|
||||
|
||||
|
||||
/**
|
||||
* The string the identifies the REFEDS "Hide From Discovery" Entity Category.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $HIDE_FROM_DISCOVERY = 'http://refeds.org/category/hide-from-discovery';
|
||||
|
||||
|
||||
/**
|
||||
* Valid options for the ContactPerson element
|
||||
*
|
||||
* The 'attributes' option isn't defined in section 2.3.2.2 of the OASIS document, but
|
||||
* it is required to allow additons to the main contact person element for trust
|
||||
* frameworks.
|
||||
*
|
||||
* @var array The valid configuration options for a contact configuration array.
|
||||
* @see "Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0", section 2.3.2.2.
|
||||
*/
|
||||
public static $VALID_CONTACT_OPTIONS = [
|
||||
'contactType',
|
||||
'emailAddress',
|
||||
'givenName',
|
||||
'surName',
|
||||
'telephoneNumber',
|
||||
'company',
|
||||
'attributes',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* @var array The valid types of contact for a contact configuration array.
|
||||
* @see "Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0", section 2.3.2.2.
|
||||
*/
|
||||
public static $VALID_CONTACT_TYPES = [
|
||||
'technical',
|
||||
'support',
|
||||
'administrative',
|
||||
'billing',
|
||||
'other',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Parse and sanitize a contact from an array.
|
||||
*
|
||||
* Accepts an array with the following elements:
|
||||
* - contactType The type of the contact (as string). Mandatory.
|
||||
* - emailAddress Email address (as string), or array of email addresses. Optional.
|
||||
* - telephoneNumber Telephone number of contact (as string), or array of telephone numbers. Optional.
|
||||
* - name Full name of contact, either as <GivenName> <SurName>, or as <SurName>, <GivenName>. Optional.
|
||||
* - surName Surname of contact (as string). Optional.
|
||||
* - givenName Given name of contact (as string). Optional.
|
||||
* - company Company name of contact (as string). Optional.
|
||||
*
|
||||
* The following values are allowed as "contactType":
|
||||
* - technical
|
||||
* - support
|
||||
* - administrative
|
||||
* - billing
|
||||
* - other
|
||||
*
|
||||
* If given a "name" it will try to decompose it into its given name and surname, only if neither givenName nor
|
||||
* surName are present. It works as follows:
|
||||
* - "surname1 surname2, given_name1 given_name2"
|
||||
* givenName: "given_name1 given_name2"
|
||||
* surname: "surname1 surname2"
|
||||
* - "given_name surname"
|
||||
* givenName: "given_name"
|
||||
* surname: "surname"
|
||||
*
|
||||
* otherwise it will just return the name as "givenName" in the resulting array.
|
||||
*
|
||||
* @param array $contact The contact to parse and sanitize.
|
||||
*
|
||||
* @return array An array holding valid contact configuration options. If a key 'name' was part of the input array,
|
||||
* it will try to decompose the name into its parts, and place the parts into givenName and surName, if those are
|
||||
* missing.
|
||||
* @throws InvalidArgumentException If $contact is neither an array nor null, or the contact does not conform to
|
||||
* valid configuration rules for contacts.
|
||||
*/
|
||||
public static function getContact($contact)
|
||||
{
|
||||
if (!(is_array($contact) || is_null($contact))) {
|
||||
throw new InvalidArgumentException('Invalid input parameters');
|
||||
}
|
||||
|
||||
// check the type
|
||||
if (!isset($contact['contactType']) || !in_array($contact['contactType'], self::$VALID_CONTACT_TYPES, true)) {
|
||||
$types = join(', ', array_map(
|
||||
/**
|
||||
* @param string $t
|
||||
* @return string
|
||||
*/
|
||||
function ($t) {
|
||||
return '"' . $t . '"';
|
||||
},
|
||||
self::$VALID_CONTACT_TYPES
|
||||
));
|
||||
throw new InvalidArgumentException('"contactType" is mandatory and must be one of ' . $types . ".");
|
||||
}
|
||||
|
||||
// check attributes is an associative array
|
||||
if (isset($contact['attributes'])) {
|
||||
if (
|
||||
empty($contact['attributes'])
|
||||
|| !is_array($contact['attributes'])
|
||||
|| count(array_filter(array_keys($contact['attributes']), 'is_string')) === 0
|
||||
) {
|
||||
throw new InvalidArgumentException('"attributes" must be an array and cannot be empty.');
|
||||
}
|
||||
}
|
||||
|
||||
// try to fill in givenName and surName from name
|
||||
if (isset($contact['name']) && !isset($contact['givenName']) && !isset($contact['surName'])) {
|
||||
// first check if it's comma separated
|
||||
$names = explode(',', $contact['name'], 2);
|
||||
if (count($names) === 2) {
|
||||
$contact['surName'] = preg_replace('/\s+/', ' ', trim($names[0]));
|
||||
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($names[1]));
|
||||
} else {
|
||||
// check if it's in "given name surname" format
|
||||
$names = explode(' ', preg_replace('/\s+/', ' ', trim($contact['name'])));
|
||||
if (count($names) === 2) {
|
||||
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($names[0]));
|
||||
$contact['surName'] = preg_replace('/\s+/', ' ', trim($names[1]));
|
||||
} else {
|
||||
// nothing works, return it as given name
|
||||
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($contact['name']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check givenName
|
||||
if (
|
||||
isset($contact['givenName'])
|
||||
&& (
|
||||
empty($contact['givenName'])
|
||||
|| !is_string($contact['givenName'])
|
||||
)
|
||||
) {
|
||||
throw new InvalidArgumentException('"givenName" must be a string and cannot be empty.');
|
||||
}
|
||||
|
||||
// check surName
|
||||
if (
|
||||
isset($contact['surName'])
|
||||
&& (
|
||||
empty($contact['surName'])
|
||||
|| !is_string($contact['surName'])
|
||||
)
|
||||
) {
|
||||
throw new InvalidArgumentException('"surName" must be a string and cannot be empty.');
|
||||
}
|
||||
|
||||
// check company
|
||||
if (
|
||||
isset($contact['company'])
|
||||
&& (
|
||||
empty($contact['company'])
|
||||
|| !is_string($contact['company'])
|
||||
)
|
||||
) {
|
||||
throw new InvalidArgumentException('"company" must be a string and cannot be empty.');
|
||||
}
|
||||
|
||||
// check emailAddress
|
||||
if (isset($contact['emailAddress'])) {
|
||||
if (
|
||||
empty($contact['emailAddress'])
|
||||
|| !(
|
||||
is_string($contact['emailAddress'])
|
||||
|| is_array($contact['emailAddress'])
|
||||
)
|
||||
) {
|
||||
throw new InvalidArgumentException('"emailAddress" must be a string or an array and cannot be empty.');
|
||||
}
|
||||
if (is_array($contact['emailAddress'])) {
|
||||
foreach ($contact['emailAddress'] as $address) {
|
||||
if (!is_string($address) || empty($address)) {
|
||||
throw new InvalidArgumentException('Email addresses must be a string and cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check telephoneNumber
|
||||
if (isset($contact['telephoneNumber'])) {
|
||||
if (
|
||||
empty($contact['telephoneNumber'])
|
||||
|| !(
|
||||
is_string($contact['telephoneNumber'])
|
||||
|| is_array($contact['telephoneNumber'])
|
||||
)
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
'"telephoneNumber" must be a string or an array and cannot be empty.'
|
||||
);
|
||||
}
|
||||
if (is_array($contact['telephoneNumber'])) {
|
||||
foreach ($contact['telephoneNumber'] as $address) {
|
||||
if (!is_string($address) || empty($address)) {
|
||||
throw new InvalidArgumentException('Telephone numbers must be a string and cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make sure only valid options are outputted
|
||||
return array_intersect_key($contact, array_flip(self::$VALID_CONTACT_OPTIONS));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the default endpoint in an endpoint array.
|
||||
*
|
||||
* @param array $endpoints An array with endpoints.
|
||||
* @param array $bindings An array with acceptable bindings. Can be null if any binding is allowed.
|
||||
*
|
||||
* @return array|NULL The default endpoint, or null if no acceptable endpoints are used.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function getDefaultEndpoint(array $endpoints, array $bindings = null)
|
||||
{
|
||||
$firstNotFalse = null;
|
||||
$firstAllowed = null;
|
||||
|
||||
// look through the endpoint list for acceptable endpoints
|
||||
foreach ($endpoints as $ep) {
|
||||
if ($bindings !== null && !in_array($ep['Binding'], $bindings, true)) {
|
||||
// unsupported binding, skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($ep['isDefault'])) {
|
||||
if ($ep['isDefault'] === true) {
|
||||
// this is the first endpoint with isDefault set to true
|
||||
return $ep;
|
||||
}
|
||||
// isDefault is set to false, but the endpoint is still usable as a last resort
|
||||
if ($firstAllowed === null) {
|
||||
// this is the first endpoint that we can use
|
||||
$firstAllowed = $ep;
|
||||
}
|
||||
} else {
|
||||
if ($firstNotFalse === null) {
|
||||
// this is the first endpoint without isDefault set
|
||||
$firstNotFalse = $ep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($firstNotFalse !== null) {
|
||||
// we have an endpoint without isDefault set to false
|
||||
return $firstNotFalse;
|
||||
}
|
||||
|
||||
/* $firstAllowed either contains the first endpoint we can use, or it contains null if we cannot use any of the
|
||||
* endpoints. Either way we return its value.
|
||||
*/
|
||||
return $firstAllowed;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if an entity should be hidden in the discovery service.
|
||||
*
|
||||
* This method searches for the "Hide From Discovery" REFEDS Entity Category, and tells if the entity should be
|
||||
* hidden or not depending on it.
|
||||
*
|
||||
* @see https://refeds.org/category/hide-from-discovery
|
||||
*
|
||||
* @param array $metadata An associative array with the metadata representing an entity.
|
||||
*
|
||||
* @return boolean True if the entity should be hidden, false otherwise.
|
||||
*/
|
||||
public static function isHiddenFromDiscovery(array $metadata)
|
||||
{
|
||||
Logger::maskErrors(E_ALL);
|
||||
$hidden = in_array(self::$HIDE_FROM_DISCOVERY, $metadata['EntityAttributes'][self::$ENTITY_CATEGORY], true);
|
||||
Logger::popErrorMask();
|
||||
return $hidden === true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method parses the different possible values of the NameIDPolicy metadata configuration.
|
||||
*
|
||||
* @param mixed $nameIdPolicy
|
||||
*
|
||||
* @return null|array
|
||||
*/
|
||||
public static function parseNameIdPolicy($nameIdPolicy)
|
||||
{
|
||||
$policy = null;
|
||||
|
||||
if (is_string($nameIdPolicy)) {
|
||||
// handle old configurations where 'NameIDPolicy' was used to specify just the format
|
||||
$policy = ['Format' => $nameIdPolicy, 'AllowCreate' => true];
|
||||
} elseif (is_array($nameIdPolicy)) {
|
||||
// handle current configurations specifying an array in the NameIDPolicy config option
|
||||
$nameIdPolicy_cf = Configuration::loadFromArray($nameIdPolicy);
|
||||
$policy = [
|
||||
'Format' => $nameIdPolicy_cf->getString('Format', Constants::NAMEID_TRANSIENT),
|
||||
'AllowCreate' => $nameIdPolicy_cf->getBoolean('AllowCreate', true),
|
||||
];
|
||||
$spNameQualifier = $nameIdPolicy_cf->getString('SPNameQualifier', false);
|
||||
if ($spNameQualifier !== false) {
|
||||
$policy['SPNameQualifier'] = $spNameQualifier;
|
||||
}
|
||||
} elseif ($nameIdPolicy === null) {
|
||||
// when NameIDPolicy is unset or set to null, default to transient as before
|
||||
$policy = ['Format' => Constants::NAMEID_TRANSIENT, 'AllowCreate' => true];
|
||||
}
|
||||
|
||||
return $policy;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,470 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
|
||||
/**
|
||||
* A class for cryptography-related functions.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class Crypto
|
||||
{
|
||||
/**
|
||||
* Decrypt data using AES-256-CBC and the key provided as a parameter.
|
||||
*
|
||||
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
|
||||
* @param string $secret The secret to use to decrypt the data.
|
||||
*
|
||||
* @return string The decrypted data.
|
||||
* @throws InvalidArgumentException If $ciphertext is not a string.
|
||||
* @throws Error\Exception If the openssl module is not loaded.
|
||||
*
|
||||
* @see \SAML\Utils\Crypto::aesDecrypt()
|
||||
*/
|
||||
private static function aesDecryptInternal($ciphertext, $secret)
|
||||
{
|
||||
if (!is_string($ciphertext)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
|
||||
);
|
||||
}
|
||||
/** @var int $len */
|
||||
$len = mb_strlen($ciphertext, '8bit');
|
||||
if ($len < 48) {
|
||||
throw new InvalidArgumentException(
|
||||
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
|
||||
);
|
||||
}
|
||||
if (!function_exists("openssl_decrypt")) {
|
||||
throw new Error\Exception("The openssl PHP module is not loaded.");
|
||||
}
|
||||
|
||||
// derive encryption and authentication keys from the secret
|
||||
$key = openssl_digest($secret, 'sha512');
|
||||
|
||||
$hmac = mb_substr($ciphertext, 0, 32, '8bit');
|
||||
$iv = mb_substr($ciphertext, 32, 16, '8bit');
|
||||
$msg = mb_substr($ciphertext, 48, $len - 48, '8bit');
|
||||
|
||||
// authenticate the ciphertext
|
||||
if (self::secureCompare(hash_hmac('sha256', $iv . $msg, substr($key, 64, 64), true), $hmac)) {
|
||||
$plaintext = openssl_decrypt(
|
||||
$msg,
|
||||
'AES-256-CBC',
|
||||
substr($key, 0, 64),
|
||||
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
|
||||
$iv
|
||||
);
|
||||
|
||||
if ($plaintext !== false) {
|
||||
return $plaintext;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error\Exception("Failed to decrypt ciphertext.");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decrypt data using AES-256-CBC and the system-wide secret salt as key.
|
||||
*
|
||||
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
|
||||
*
|
||||
* @return string The decrypted data.
|
||||
* @throws InvalidArgumentException If $ciphertext is not a string.
|
||||
* @throws Error\Exception If the openssl module is not loaded.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function aesDecrypt($ciphertext)
|
||||
{
|
||||
return self::aesDecryptInternal($ciphertext, Config::getSecretSalt());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt data using AES-256-CBC and the key provided as a parameter.
|
||||
*
|
||||
* @param string $data The data to encrypt.
|
||||
* @param string $secret The secret to use to encrypt the data.
|
||||
*
|
||||
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
|
||||
* @throws InvalidArgumentException If $data is not a string.
|
||||
* @throws Error\Exception If the openssl module is not loaded.
|
||||
*
|
||||
* @see \SAML\Utils\Crypto::aesEncrypt()
|
||||
*/
|
||||
private static function aesEncryptInternal($data, $secret)
|
||||
{
|
||||
if (!is_string($data)) {
|
||||
throw new InvalidArgumentException('Input parameter "$data" must be a string.');
|
||||
}
|
||||
|
||||
if (!function_exists("openssl_encrypt")) {
|
||||
throw new Error\Exception('The openssl PHP module is not loaded.');
|
||||
}
|
||||
|
||||
// derive encryption and authentication keys from the secret
|
||||
$key = openssl_digest($secret, 'sha512');
|
||||
|
||||
// generate a random IV
|
||||
$iv = openssl_random_pseudo_bytes(16);
|
||||
|
||||
// encrypt the message
|
||||
/** @var string|false $ciphertext */
|
||||
$ciphertext = openssl_encrypt(
|
||||
$data,
|
||||
'AES-256-CBC',
|
||||
substr($key, 0, 64),
|
||||
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
|
||||
$iv
|
||||
);
|
||||
|
||||
if ($ciphertext === false) {
|
||||
throw new Error\Exception("Failed to encrypt plaintext.");
|
||||
}
|
||||
|
||||
// return the ciphertext with proper authentication
|
||||
return hash_hmac('sha256', $iv . $ciphertext, substr($key, 64, 64), true) . $iv . $ciphertext;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt data using AES-256-CBC and the system-wide secret salt as key.
|
||||
*
|
||||
* @param string $data The data to encrypt.
|
||||
*
|
||||
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
|
||||
* @throws InvalidArgumentException If $data is not a string.
|
||||
* @throws Error\Exception If the openssl module is not loaded.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function aesEncrypt($data)
|
||||
{
|
||||
return self::aesEncryptInternal($data, Config::getSecretSalt());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert data from DER to PEM encoding.
|
||||
*
|
||||
* @param string $der Data encoded in DER format.
|
||||
* @param string $type The type of data we are encoding, as expressed by the PEM header. Defaults to "CERTIFICATE".
|
||||
* @return string The same data encoded in PEM format.
|
||||
* @see RFC7648 for known types and PEM format specifics.
|
||||
*/
|
||||
public static function der2pem($der, $type = 'CERTIFICATE')
|
||||
{
|
||||
return "-----BEGIN " . $type . "-----\n" .
|
||||
chunk_split(base64_encode($der), 64, "\n") .
|
||||
"-----END " . $type . "-----\n";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load a private key from metadata.
|
||||
*
|
||||
* This function loads a private key from a metadata array. It looks for the following elements:
|
||||
* - 'privatekey': Name of a private key file in the cert-directory.
|
||||
* - 'privatekey_pass': Password for the private key.
|
||||
*
|
||||
* It returns and array with the following elements:
|
||||
* - 'PEM': Data for the private key, in PEM-format.
|
||||
* - 'password': Password for the private key.
|
||||
*
|
||||
* @param Configuration $metadata The metadata array the private key should be loaded from.
|
||||
* @param bool $required Whether the private key is required. If this is true, a
|
||||
* missing key will cause an exception. Defaults to false.
|
||||
* @param string $prefix The prefix which should be used when reading from the metadata
|
||||
* array. Defaults to ''.
|
||||
* @param bool $full_path Whether the filename found in the configuration contains the
|
||||
* full path to the private key or not. Default to false.
|
||||
*
|
||||
* @return array|NULL Extracted private key, or NULL if no private key is present.
|
||||
* @throws InvalidArgumentException If $required is not boolean or $prefix is not a string.
|
||||
* @throws Error\Exception If no private key is found in the metadata, or it was not possible to load
|
||||
* it.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function loadPrivateKey(Configuration $metadata, $required = false, $prefix = '', $full_path = false)
|
||||
{
|
||||
if (!is_bool($required) || !is_string($prefix) || !is_bool($full_path)) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
|
||||
$file = $metadata->getString($prefix . 'privatekey', null);
|
||||
if ($file === null) {
|
||||
// no private key found
|
||||
if ($required) {
|
||||
throw new Error\Exception('No private key found in metadata.');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$full_path) {
|
||||
$file = Config::getCertPath($file);
|
||||
}
|
||||
|
||||
$data = @file_get_contents($file);
|
||||
if ($data === false) {
|
||||
throw new Error\Exception('Unable to load private key from file "' . $file . '"');
|
||||
}
|
||||
|
||||
$ret = [
|
||||
'PEM' => $data,
|
||||
'password' => $metadata->getString($prefix . 'privatekey_pass', null),
|
||||
];
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get public key or certificate from metadata.
|
||||
*
|
||||
* This function implements a function to retrieve the public key or certificate from a metadata array.
|
||||
*
|
||||
* It will search for the following elements in the metadata:
|
||||
* - 'certData': The certificate as a base64-encoded string.
|
||||
* - 'certificate': A file with a certificate or public key in PEM-format.
|
||||
* - 'certFingerprint': The fingerprint of the certificate. Can be a single fingerprint, or an array of multiple
|
||||
* valid fingerprints. (deprecated)
|
||||
*
|
||||
* This function will return an array with these elements:
|
||||
* - 'PEM': The public key/certificate in PEM-encoding.
|
||||
* - 'certData': The certificate data, base64 encoded, on a single line. (Only present if this is a certificate.)
|
||||
* - 'certFingerprint': Array of valid certificate fingerprints. (Deprecated. Only present if this is a
|
||||
* certificate.)
|
||||
*
|
||||
* @param Configuration $metadata The metadata.
|
||||
* @param bool $required Whether the public key is required. If this is TRUE, a missing key
|
||||
* will cause an exception. Default is FALSE.
|
||||
* @param string $prefix The prefix which should be used when reading from the metadata array.
|
||||
* Defaults to ''.
|
||||
*
|
||||
* @return array|NULL Public key or certificate data, or NULL if no public key or certificate was found.
|
||||
* @throws InvalidArgumentException If $metadata is not an instance of \SimpleSAML\Configuration, $required is not
|
||||
* boolean or $prefix is not a string.
|
||||
* @throws Error\Exception If no public key is found in the metadata, or it was not possible to load
|
||||
* it.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
* @author Lasse Birnbaum Jensen
|
||||
*/
|
||||
public static function loadPublicKey(Configuration $metadata, $required = false, $prefix = '')
|
||||
{
|
||||
if (!is_bool($required) || !is_string($prefix)) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
|
||||
$keys = $metadata->getPublicKeys(null, false, $prefix);
|
||||
if (!empty($keys)) {
|
||||
foreach ($keys as $key) {
|
||||
if ($key['type'] !== 'X509Certificate') {
|
||||
continue;
|
||||
}
|
||||
if ($key['signing'] !== true) {
|
||||
continue;
|
||||
}
|
||||
$certData = $key['X509Certificate'];
|
||||
$pem = "-----BEGIN CERTIFICATE-----\n" .
|
||||
chunk_split($certData, 64) .
|
||||
"-----END CERTIFICATE-----\n";
|
||||
$certFingerprint = strtolower(sha1(base64_decode($certData)));
|
||||
|
||||
return [
|
||||
'certData' => $certData,
|
||||
'PEM' => $pem,
|
||||
'certFingerprint' => [$certFingerprint],
|
||||
];
|
||||
}
|
||||
// no valid key found
|
||||
} elseif ($metadata->hasValue($prefix . 'certFingerprint')) {
|
||||
// we only have a fingerprint available
|
||||
$fps = $metadata->getArrayizeString($prefix . 'certFingerprint');
|
||||
|
||||
// normalize fingerprint(s) - lowercase and no colons
|
||||
foreach ($fps as &$fp) {
|
||||
assert(is_string($fp));
|
||||
$fp = strtolower(str_replace(':', '', $fp));
|
||||
}
|
||||
|
||||
/*
|
||||
* We can't build a full certificate from a fingerprint, and may as well return an array with only the
|
||||
* fingerprint(s) immediately.
|
||||
*/
|
||||
return ['certFingerprint' => $fps];
|
||||
}
|
||||
|
||||
// no public key/certificate available
|
||||
if ($required) {
|
||||
throw new Error\Exception('No public key / certificate found in metadata.');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert from PEM to DER encoding.
|
||||
*
|
||||
* @param string $pem Data encoded in PEM format.
|
||||
* @return string The same data encoded in DER format.
|
||||
* @throws InvalidArgumentException If $pem is not encoded in PEM format.
|
||||
* @see RFC7648 for PEM format specifics.
|
||||
*/
|
||||
public static function pem2der($pem)
|
||||
{
|
||||
$pem = trim($pem);
|
||||
$begin = "-----BEGIN ";
|
||||
$end = "-----END ";
|
||||
$lines = explode("\n", $pem);
|
||||
$last = count($lines) - 1;
|
||||
|
||||
if (strpos($lines[0], $begin) !== 0) {
|
||||
throw new InvalidArgumentException("pem2der: input is not encoded in PEM format.");
|
||||
}
|
||||
unset($lines[0]);
|
||||
if (strpos($lines[$last], $end) !== 0) {
|
||||
throw new InvalidArgumentException("pem2der: input is not encoded in PEM format.");
|
||||
}
|
||||
unset($lines[$last]);
|
||||
|
||||
return base64_decode(implode($lines));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function hashes a password with a given algorithm.
|
||||
*
|
||||
* @param string $password The password to hash.
|
||||
* @param string|null $algorithm @deprecated The hashing algorithm, uppercase, optionally
|
||||
* prepended with 'S' (salted). See hash_algos() for a complete list of hashing algorithms.
|
||||
* @param string|null $salt @deprecated An optional salt to use.
|
||||
*
|
||||
* @return string The hashed password.
|
||||
* @throws InvalidArgumentException If the input parameter is not a string.
|
||||
* @throws Error\Exception If the algorithm specified is not supported.
|
||||
*
|
||||
* @see hash_algos()
|
||||
*
|
||||
* @author Dyonisius Visser, TERENA <visser@terena.org>
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function pwHash($password, $algorithm = null, $salt = null)
|
||||
{
|
||||
if (!is_null($algorithm)) {
|
||||
// @deprecated Old-style
|
||||
if (!is_string($algorithm) || !is_string($password)) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
// hash w/o salt
|
||||
if (in_array(strtolower($algorithm), hash_algos(), true)) {
|
||||
$alg_str = '{' . str_replace('SHA1', 'SHA', $algorithm) . '}'; // LDAP compatibility
|
||||
$hash = hash(strtolower($algorithm), $password, true);
|
||||
return $alg_str . base64_encode($hash);
|
||||
}
|
||||
// hash w/ salt
|
||||
if ($salt === null) {
|
||||
// no salt provided, generate one
|
||||
// default 8 byte salt, but 4 byte for LDAP SHA1 hashes
|
||||
$bytes = ($algorithm == 'SSHA1') ? 4 : 8;
|
||||
$salt = openssl_random_pseudo_bytes($bytes);
|
||||
}
|
||||
|
||||
if ($algorithm[0] == 'S' && in_array(substr(strtolower($algorithm), 1), hash_algos(), true)) {
|
||||
$alg = substr(strtolower($algorithm), 1); // 'sha256' etc
|
||||
$alg_str = '{' . str_replace('SSHA1', 'SSHA', $algorithm) . '}'; // LDAP compatibility
|
||||
$hash = hash($alg, $password . $salt, true);
|
||||
return $alg_str . base64_encode($hash . $salt);
|
||||
}
|
||||
throw new Error\Exception('Hashing algorithm \'' . strtolower($algorithm) . '\' is not supported');
|
||||
} else {
|
||||
if (!is_string($password)) {
|
||||
throw new InvalidArgumentException('Invalid input parameter.');
|
||||
} elseif (!is_string($hash = password_hash($password, PASSWORD_DEFAULT))) {
|
||||
throw new InvalidArgumentException('Error while hashing password.');
|
||||
}
|
||||
return $hash;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare two strings securely.
|
||||
*
|
||||
* This method checks if two strings are equal in constant time, avoiding timing attacks. Use it every time we need
|
||||
* to compare a string with a secret that shouldn't be leaked, i.e. when verifying passwords, one-time codes, etc.
|
||||
*
|
||||
* @param string $known A known string.
|
||||
* @param string $user A user-provided string to compare with the known string.
|
||||
*
|
||||
* @return bool True if both strings are equal, false otherwise.
|
||||
*/
|
||||
public static function secureCompare($known, $user)
|
||||
{
|
||||
return hash_equals($known, $user);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function checks if a password is valid
|
||||
*
|
||||
* @param string $hash The password as it appears in password file, optionally prepended with algorithm.
|
||||
* @param string $password The password to check in clear.
|
||||
*
|
||||
* @return boolean True if the hash corresponds with the given password, false otherwise.
|
||||
* @throws InvalidArgumentException If the input parameters are not strings.
|
||||
* @throws Error\Exception If the algorithm specified is not supported.
|
||||
*
|
||||
* @author Dyonisius Visser, TERENA <visser@terena.org>
|
||||
*/
|
||||
public static function pwValid($hash, $password)
|
||||
{
|
||||
if (!is_string($hash) || !is_string($password)) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
|
||||
if (password_verify($password, $hash)) {
|
||||
return true;
|
||||
}
|
||||
// return $hash === $password
|
||||
|
||||
// @deprecated remove everything below this line for 2.0
|
||||
// match algorithm string (e.g. '{SSHA256}', '{MD5}')
|
||||
if (preg_match('/^{(.*?)}(.*)$/', $hash, $matches)) {
|
||||
// LDAP compatibility
|
||||
$alg = preg_replace('/^(S?SHA)$/', '${1}1', $matches[1]);
|
||||
|
||||
// hash w/o salt
|
||||
if (in_array(strtolower($alg), hash_algos(), true)) {
|
||||
return self::secureCompare($hash, self::pwHash($password, $alg));
|
||||
}
|
||||
|
||||
// hash w/ salt
|
||||
if ($alg[0] === 'S' && in_array(substr(strtolower($alg), 1), hash_algos(), true)) {
|
||||
$php_alg = substr(strtolower($alg), 1);
|
||||
|
||||
// get hash length of this algorithm to learn how long the salt is
|
||||
$hash_length = strlen(hash($php_alg, '', true));
|
||||
$salt = substr(base64_decode($matches[2]), $hash_length);
|
||||
return self::secureCompare($hash, self::pwHash($password, $alg, $salt));
|
||||
}
|
||||
throw new Error\Exception('Hashing algorithm \'' . strtolower($alg) . '\' is not supported');
|
||||
} else {
|
||||
return $hash === $password;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,307 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
use SAML\XHTML\Template;
|
||||
|
||||
/**
|
||||
* E-mailer class that can generate a formatted e-mail from array
|
||||
* input data.
|
||||
*
|
||||
* @author Jørn Åne de Jong, Uninett AS <jorn.dejong@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class EMail
|
||||
{
|
||||
/** @var array Dictionary with multivalues */
|
||||
private $data = [];
|
||||
|
||||
/** @var string Introduction text */
|
||||
private $text = '';
|
||||
|
||||
/** @var PHPMailer The mailer instance */
|
||||
private $mail;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* If $from or $to is not provided, the <code>technicalcontact_email</code>
|
||||
* from the configuration is used.
|
||||
*
|
||||
* @param string $subject The subject of the e-mail
|
||||
* @param string $from The from-address (both envelope and header)
|
||||
* @param string $to The recipient
|
||||
*
|
||||
* @throws \PHPMailer\PHPMailer\Exception
|
||||
*/
|
||||
public function __construct($subject, $from = null, $to = null)
|
||||
{
|
||||
$this->mail = new PHPMailer(true);
|
||||
$this->mail->Subject = $subject;
|
||||
$this->mail->setFrom($from ?: static::getDefaultMailAddress());
|
||||
$this->mail->addAddress($to ?: static::getDefaultMailAddress());
|
||||
|
||||
static::initFromConfig($this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the default e-mail address from the configuration
|
||||
* This is used both as source and destination address
|
||||
* unless something else is provided at the constructor.
|
||||
*
|
||||
* It will refuse to return the SimpleSAMLphp default address,
|
||||
* which is na@example.org.
|
||||
*
|
||||
* @return string Default mail address
|
||||
*/
|
||||
public static function getDefaultMailAddress()
|
||||
{
|
||||
$config = Configuration::getInstance();
|
||||
$address = $config->getString('technicalcontact_email', 'na@example.org');
|
||||
$address = preg_replace('/^mailto:/i', '', $address);
|
||||
if ('na@example.org' === $address) {
|
||||
throw new Exception('technicalcontact_email must be changed from the default value');
|
||||
}
|
||||
return $address;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the data that should be embedded in the e-mail body
|
||||
*
|
||||
* @param array $data The data that should be embedded in the e-mail body
|
||||
* @return void
|
||||
*/
|
||||
public function setData(array $data)
|
||||
{
|
||||
/*
|
||||
* Convert every non-array value to an array with the original
|
||||
* as its only element. This guarantees that every value of $data
|
||||
* can be iterated over.
|
||||
*/
|
||||
$this->data = array_map(
|
||||
/**
|
||||
* @param mixed $v
|
||||
* @return array
|
||||
*/
|
||||
function ($v) {
|
||||
return is_array($v) ? $v : [$v];
|
||||
},
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set an introduction text for the e-mail
|
||||
*
|
||||
* @param string $text Introduction text
|
||||
* @return void
|
||||
*/
|
||||
public function setText($text)
|
||||
{
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a Reply-To address to the mail
|
||||
*
|
||||
* @param string $address Reply-To e-mail address
|
||||
* @return void
|
||||
*/
|
||||
public function addReplyTo($address)
|
||||
{
|
||||
$this->mail->addReplyTo($address);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send the mail
|
||||
*
|
||||
* @param bool $plainTextOnly Do not send HTML payload
|
||||
* @return void
|
||||
*
|
||||
* @throws \PHPMailer\PHPMailer\Exception
|
||||
*/
|
||||
public function send($plainTextOnly = false)
|
||||
{
|
||||
if ($plainTextOnly) {
|
||||
$this->mail->isHTML(false);
|
||||
$this->mail->Body = $this->generateBody('mailtxt.twig');
|
||||
} else {
|
||||
$this->mail->isHTML(true);
|
||||
$this->mail->Body = $this->generateBody('mailhtml.twig');
|
||||
$this->mail->AltBody = $this->generateBody('mailtxt.twig');
|
||||
}
|
||||
|
||||
$this->mail->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the method by which the email will be sent. Currently supports what
|
||||
* PHPMailer supports: sendmail, mail and smtp.
|
||||
*
|
||||
* @param string $transportMethod the transport method
|
||||
* @param array $transportOptions options for the transport method
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setTransportMethod($transportMethod, array $transportOptions = [])
|
||||
{
|
||||
assert(is_string($transportMethod));
|
||||
assert(is_array($transportOptions));
|
||||
|
||||
|
||||
switch (strtolower($transportMethod)) {
|
||||
// smtp transport method
|
||||
case 'smtp':
|
||||
$this->mail->isSMTP();
|
||||
|
||||
// set the host (required)
|
||||
if (isset($transportOptions['host'])) {
|
||||
$this->mail->Host = $transportOptions['host'];
|
||||
} else {
|
||||
// throw an exception otherwise
|
||||
throw new InvalidArgumentException("Missing Required Email Transport Parameter 'host'");
|
||||
}
|
||||
|
||||
// set the port (optional, assume standard SMTP port 25 if not provided)
|
||||
$this->mail->Port = (isset($transportOptions['port'])) ? (int)$transportOptions['port'] : 25;
|
||||
|
||||
// smtp auth: enabled if username or password is set
|
||||
if (isset($transportOptions['username']) || isset($transportOptions['password'])) {
|
||||
$this->mail->SMTPAuth = true;
|
||||
}
|
||||
|
||||
// smtp auth: username
|
||||
if (isset($transportOptions['username'])) {
|
||||
$this->mail->Username = $transportOptions['username'];
|
||||
}
|
||||
|
||||
// smtp auth: password
|
||||
if (isset($transportOptions['password'])) {
|
||||
$this->mail->Password = $transportOptions['password'];
|
||||
}
|
||||
|
||||
// smtp security: encryption type
|
||||
if (isset($transportOptions['secure'])) {
|
||||
$this->mail->SMTPSecure = $transportOptions['secure'];
|
||||
}
|
||||
|
||||
// smtp security: enable or disable smtp auto tls
|
||||
if (isset($transportOptions['autotls'])) {
|
||||
$this->mail->SMTPAutoTLS = (bool)$transportOptions['autotls'];
|
||||
}
|
||||
break;
|
||||
//mail transport method
|
||||
case 'mail':
|
||||
$this->mail->isMail();
|
||||
break;
|
||||
// sendmail transport method
|
||||
case 'sendmail':
|
||||
$this->mail->isSendmail();
|
||||
|
||||
// override the default path of the sendmail executable
|
||||
if (isset($transportOptions['path'])) {
|
||||
$this->mail->Sendmail = $transportOptions['path'];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException(
|
||||
"Invalid Mail Transport Method - Check 'mail.transport.method' Configuration Option"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the provided EMail object with the configuration provided from the SimpleSAMLphp configuration.
|
||||
*
|
||||
* @param EMail $EMail
|
||||
* @return EMail
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function initFromConfig(EMail $EMail)
|
||||
{
|
||||
assert($EMail instanceof EMail);
|
||||
|
||||
$config = Configuration::getInstance();
|
||||
$EMail->setTransportMethod(
|
||||
$config->getString('mail.transport.method', 'mail'),
|
||||
$config->getArrayize('mail.transport.options', [])
|
||||
);
|
||||
|
||||
return $EMail;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate the body of the e-mail
|
||||
*
|
||||
* @param string $template The name of the template to use
|
||||
*
|
||||
* @return string The body of the e-mail
|
||||
*/
|
||||
public function generateBody($template)
|
||||
{
|
||||
$config = Configuration::getInstance();
|
||||
$newui = $config->getBoolean('usenewui', false);
|
||||
|
||||
if ($newui === false) {
|
||||
$result = '<!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>
|
||||
<h1>' . htmlspecialchars($this->mail->Subject) . '</h1>
|
||||
<div class="container" style="background: #fafafa; border: 1px solid #eee; margin: 2em; padding: .6em;">
|
||||
<blockquote>"' . htmlspecialchars($this->text) . '"</blockquote>
|
||||
</div>';
|
||||
foreach ($this->data as $name => $values) {
|
||||
$result .= '<h2>' . htmlspecialchars($name) . '</h2><ul>';
|
||||
foreach ($values as $value) {
|
||||
$result .= '<li><pre>' . htmlspecialchars($value) . '</pre></li>';
|
||||
}
|
||||
$result .= '</ul>';
|
||||
}
|
||||
} else {
|
||||
$t = new Template($config, $template);
|
||||
$twig = $t->getTwig();
|
||||
if (!isset($twig)) {
|
||||
throw new Exception(
|
||||
'Even though we explicitly configure that we want Twig,'
|
||||
. ' the Template class does not give us Twig. This is a bug.'
|
||||
);
|
||||
}
|
||||
$result = $twig->render($template, [
|
||||
'subject' => $this->mail->Subject,
|
||||
'text' => $this->text,
|
||||
'data' => $this->data
|
||||
]);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,273 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
/**
|
||||
* Provides a non-static wrapper for the HTTP utility class.
|
||||
*
|
||||
* @package SimpleSAML\Utils
|
||||
*/
|
||||
class HttpAdapter
|
||||
{
|
||||
/**
|
||||
* @see HTTP::getServerHTTPS()
|
||||
* @return bool
|
||||
*/
|
||||
public function getServerHTTPS()
|
||||
{
|
||||
return HTTP::getServerHTTPS();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getServerPort()
|
||||
* @return string
|
||||
*/
|
||||
public function getServerPort()
|
||||
{
|
||||
return HTTP::getServerPort();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::addURLParameters()
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $parameters
|
||||
* @return string
|
||||
*/
|
||||
public function addURLParameters($url, $parameters)
|
||||
{
|
||||
return HTTP::addURLParameters($url, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::checkSessionCookie()
|
||||
*
|
||||
* @param string|null $retryURL
|
||||
* @return void
|
||||
*/
|
||||
public function checkSessionCookie($retryURL = null)
|
||||
{
|
||||
HTTP::checkSessionCookie($retryURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::checkURLAllowed()
|
||||
*
|
||||
* @param string $url
|
||||
* @param array|null $trustedSites
|
||||
* @return string
|
||||
*/
|
||||
public function checkURLAllowed($url, array $trustedSites = null)
|
||||
{
|
||||
return HTTP::checkURLAllowed($url, $trustedSites);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::fetch()
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $context
|
||||
* @param bool $getHeaders
|
||||
* @return array|string
|
||||
*/
|
||||
public function fetch($url, $context = [], $getHeaders = false)
|
||||
{
|
||||
return HTTP::fetch($url, $context, $getHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getAcceptLanguage()
|
||||
* @return array
|
||||
*/
|
||||
public function getAcceptLanguage()
|
||||
{
|
||||
return HTTP::getAcceptLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::guessBasePath()
|
||||
* @return string
|
||||
*/
|
||||
public function guessBasePath()
|
||||
{
|
||||
return HTTP::guessBasePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getBaseURL()
|
||||
* @return string
|
||||
*/
|
||||
public function getBaseURL()
|
||||
{
|
||||
return HTTP::getBaseURL();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getFirstPathElement()
|
||||
*
|
||||
* @param bool $trailingslash
|
||||
* @return string
|
||||
*/
|
||||
public function getFirstPathElement($trailingslash = true)
|
||||
{
|
||||
return HTTP::getFirstPathElement($trailingslash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getPOSTRedirectURL()
|
||||
*
|
||||
* @param string $destination
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public function getPOSTRedirectURL($destination, $data)
|
||||
{
|
||||
return HTTP::getPOSTRedirectURL($destination, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getSelfHost()
|
||||
* @return string
|
||||
*/
|
||||
public function getSelfHost()
|
||||
{
|
||||
return HTTP::getSelfHost();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getSelfHostWithNonStandardPort()
|
||||
* @return string
|
||||
*/
|
||||
public function getSelfHostWithNonStandardPort()
|
||||
{
|
||||
return HTTP::getSelfHostWithNonStandardPort();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getSelfHostWithPath()
|
||||
* @return string
|
||||
*/
|
||||
public function getSelfHostWithPath()
|
||||
{
|
||||
return HTTP::getSelfHostWithPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getSelfURL()
|
||||
* @return string
|
||||
*/
|
||||
public function getSelfURL()
|
||||
{
|
||||
return HTTP::getSelfURL();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getSelfURLHost()
|
||||
* @return string
|
||||
*/
|
||||
public function getSelfURLHost()
|
||||
{
|
||||
return HTTP::getSelfURLHost();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::getSelfURLNoQuery()
|
||||
* @return string
|
||||
*/
|
||||
public function getSelfURLNoQuery()
|
||||
{
|
||||
return HTTP::getSelfURLNoQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::isHTTPS()
|
||||
* @return bool
|
||||
*/
|
||||
public function isHTTPS()
|
||||
{
|
||||
return HTTP::isHTTPS();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::normalizeURL()
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
public function normalizeURL($url)
|
||||
{
|
||||
return HTTP::normalizeURL($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::parseQueryString()
|
||||
*
|
||||
* @param string $query_string
|
||||
* @return array
|
||||
*/
|
||||
public function parseQueryString($query_string)
|
||||
{
|
||||
return HTTP::parseQueryString($query_string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::redirectTrustedURL()
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $parameters
|
||||
* @return void
|
||||
*/
|
||||
public function redirectTrustedURL($url, $parameters = [])
|
||||
{
|
||||
HTTP::redirectTrustedURL($url, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::redirectUntrustedURL()
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $parameters
|
||||
* @return void
|
||||
*/
|
||||
public function redirectUntrustedURL($url, $parameters = [])
|
||||
{
|
||||
HTTP::redirectUntrustedURL($url, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::resolveURL()
|
||||
*
|
||||
* @param string $url
|
||||
* @param string|null $base
|
||||
* @return string
|
||||
*/
|
||||
public function resolveURL($url, $base = null)
|
||||
{
|
||||
return HTTP::resolveURL($url, $base);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::setCookie()
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @param array|null $params
|
||||
* @param bool $throw
|
||||
* @return void
|
||||
*/
|
||||
public function setCookie($name, $value, $params = null, $throw = true)
|
||||
{
|
||||
HTTP::setCookie($name, $value, $params, $throw);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see HTTP::submitPOSTData()
|
||||
*
|
||||
* @param string $destination
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function submitPOSTData($destination, $data)
|
||||
{
|
||||
HTTP::submitPOSTData($destination, $data);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
/**
|
||||
* Net-related utility methods.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class Net
|
||||
{
|
||||
/**
|
||||
* Check whether an IP address is part of a CIDR.
|
||||
*
|
||||
* @param string $cidr The network CIDR address.
|
||||
* @param string $ip The IP address to check. Optional. Current remote address will be used if none specified. Do
|
||||
* not rely on default parameter if running behind load balancers.
|
||||
*
|
||||
* @return boolean True if the IP address belongs to the specified CIDR, false otherwise.
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
* @author Brook Schofield, GÉANT
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function ipCIDRcheck($cidr, $ip = null)
|
||||
{
|
||||
if ($ip === null) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
if (strpos($cidr, '/') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
list ($net, $mask) = explode('/', $cidr);
|
||||
$mask = intval($mask);
|
||||
|
||||
$ip_ip = [];
|
||||
$ip_net = [];
|
||||
if (strstr($ip, ':') || strstr($net, ':')) {
|
||||
// Validate IPv6 with inet_pton, convert to hex with bin2hex
|
||||
// then store as a long with hexdec
|
||||
|
||||
$ip_pack = @inet_pton($ip);
|
||||
$net_pack = @inet_pton($net);
|
||||
|
||||
if ($ip_pack === false || $net_pack === false) {
|
||||
// not valid IPv6 address (warning silenced)
|
||||
return false;
|
||||
}
|
||||
|
||||
$ip_ip = str_split(bin2hex($ip_pack), 8);
|
||||
foreach ($ip_ip as &$value) {
|
||||
$value = hexdec($value);
|
||||
}
|
||||
|
||||
$ip_net = str_split(bin2hex($net_pack), 8);
|
||||
foreach ($ip_net as &$value) {
|
||||
$value = hexdec($value);
|
||||
}
|
||||
} else {
|
||||
$ip_ip[0] = ip2long($ip);
|
||||
$ip_net[0] = ip2long($net);
|
||||
}
|
||||
|
||||
for ($i = 0; $mask > 0 && $i < sizeof($ip_ip); $i++) {
|
||||
if ($mask > 32) {
|
||||
$iteration_mask = 32;
|
||||
} else {
|
||||
$iteration_mask = $mask;
|
||||
}
|
||||
$mask -= 32;
|
||||
|
||||
$ip_mask = ~((1 << (32 - $iteration_mask)) - 1);
|
||||
|
||||
$ip_net_mask = $ip_net[$i] & $ip_mask;
|
||||
$ip_ip_mask = $ip_ip[$i] & $ip_mask;
|
||||
|
||||
if ($ip_ip_mask != $ip_net_mask) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
/**
|
||||
* Utility class for random data generation and manipulation.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class Random
|
||||
{
|
||||
/**
|
||||
* The fixed length of random identifiers.
|
||||
*/
|
||||
const ID_LENGTH = 43;
|
||||
|
||||
/**
|
||||
* Generate a random identifier, ID_LENGTH bytes long.
|
||||
*
|
||||
* @return string A ID_LENGTH-bytes long string with a random, hex-encoded string.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function generateID()
|
||||
{
|
||||
return '_' . bin2hex(openssl_random_pseudo_bytes((int) ((self::ID_LENGTH - 1) / 2)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
|
||||
/**
|
||||
* System-related utility methods.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
class System
|
||||
{
|
||||
const WINDOWS = 1;
|
||||
const LINUX = 2;
|
||||
const OSX = 3;
|
||||
const HPUX = 4;
|
||||
const UNIX = 5;
|
||||
const BSD = 6;
|
||||
const IRIX = 7;
|
||||
const SUNOS = 8;
|
||||
|
||||
|
||||
/**
|
||||
* This function returns the Operating System we are running on.
|
||||
*
|
||||
* @return mixed A predefined constant identifying the OS we are running on. False if we are unable to determine it.
|
||||
*
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function getOS()
|
||||
{
|
||||
if (stristr(PHP_OS, 'LINUX')) {
|
||||
return self::LINUX;
|
||||
}
|
||||
if (stristr(PHP_OS, 'DARWIN')) {
|
||||
return self::OSX;
|
||||
}
|
||||
if (stristr(PHP_OS, 'WIN')) {
|
||||
return self::WINDOWS;
|
||||
}
|
||||
if (stristr(PHP_OS, 'BSD')) {
|
||||
return self::BSD;
|
||||
}
|
||||
if (stristr(PHP_OS, 'UNIX')) {
|
||||
return self::UNIX;
|
||||
}
|
||||
if (stristr(PHP_OS, 'HP-UX')) {
|
||||
return self::HPUX;
|
||||
}
|
||||
if (stristr(PHP_OS, 'IRIX')) {
|
||||
return self::IRIX;
|
||||
}
|
||||
if (stristr(PHP_OS, 'SUNOS')) {
|
||||
return self::SUNOS;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function retrieves the path to a directory where temporary files can be saved.
|
||||
*
|
||||
* @return string Path to a temporary directory, without a trailing directory separator.
|
||||
* @throws Error\Exception If the temporary directory cannot be created or it exists and does not belong
|
||||
* to the current user.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function getTempDir()
|
||||
{
|
||||
$globalConfig = Configuration::getInstance();
|
||||
|
||||
$tempDir = rtrim(
|
||||
$globalConfig->getString(
|
||||
'tempdir',
|
||||
sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'simplesaml'
|
||||
),
|
||||
DIRECTORY_SEPARATOR
|
||||
);
|
||||
|
||||
if (!is_dir($tempDir)) {
|
||||
if (!mkdir($tempDir, 0700, true)) {
|
||||
$error = error_get_last();
|
||||
throw new Error\Exception(
|
||||
'Error creating temporary directory "' . $tempDir . '": ' .
|
||||
(is_array($error) ? $error['message'] : 'no error available')
|
||||
);
|
||||
}
|
||||
} elseif (function_exists('posix_getuid')) {
|
||||
// check that the owner of the temp directory is the current user
|
||||
$stat = lstat($tempDir);
|
||||
if ($stat['uid'] !== posix_getuid()) {
|
||||
throw new Error\Exception(
|
||||
'Temporary directory "' . $tempDir . '" does not belong to the current user.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tempDir;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolve a (possibly) relative path from the given base path.
|
||||
*
|
||||
* A path which starts with a stream wrapper pattern (e.g. s3://) will not be touched
|
||||
* and returned as is - regardles of the value given as base path.
|
||||
* If it starts with a '/' it is assumed to be absolute, all others are assumed to be
|
||||
* relative. The default base path is the root of the SimpleSAMLphp installation.
|
||||
*
|
||||
* @param string $path The path we should resolve.
|
||||
* @param string|null $base The base path, where we should search for $path from. Default value is the root of the
|
||||
* SimpleSAMLphp installation.
|
||||
*
|
||||
* @return string An absolute path referring to $path.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function resolvePath($path, $base = null)
|
||||
{
|
||||
if ($base === null) {
|
||||
$config = Configuration::getInstance();
|
||||
$base = $config->getBaseDir();
|
||||
}
|
||||
|
||||
// normalise directory separator
|
||||
$base = str_replace('\\', '/', $base);
|
||||
$path = str_replace('\\', '/', $path);
|
||||
|
||||
// remove trailing slashes
|
||||
$base = rtrim($base, '/');
|
||||
$path = rtrim($path, '/');
|
||||
|
||||
// check for absolute path
|
||||
if (substr($path, 0, 1) === '/') {
|
||||
// absolute path. */
|
||||
$ret = '/';
|
||||
} elseif (static::pathContainsDriveLetter($path)) {
|
||||
$ret = '';
|
||||
} else {
|
||||
// path relative to base
|
||||
$ret = $base;
|
||||
}
|
||||
|
||||
if (static::pathContainsStreamWrapper($path)) {
|
||||
$ret = $path;
|
||||
} else {
|
||||
$path = explode('/', $path);
|
||||
foreach ($path as $d) {
|
||||
if ($d === '.') {
|
||||
continue;
|
||||
} elseif ($d === '..') {
|
||||
$ret = dirname($ret);
|
||||
} else {
|
||||
if ($ret && substr($ret, -1) !== '/') {
|
||||
$ret .= '/';
|
||||
}
|
||||
$ret .= $d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Atomically write a file.
|
||||
*
|
||||
* This is a helper function for writing data atomically to a file. It does this by writing the file data to a
|
||||
* temporary file, then renaming it to the required file name.
|
||||
*
|
||||
* @param string $filename The path to the file we want to write to.
|
||||
* @param string $data The data we should write to the file.
|
||||
* @param int $mode The permissions to apply to the file. Defaults to 0600.
|
||||
*
|
||||
* @throws InvalidArgumentException If any of the input parameters doesn't have the proper types.
|
||||
* @throws Error\Exception If the file cannot be saved, permissions cannot be changed or it is not
|
||||
* possible to write to the target file.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
* @author Andjelko Horvat
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function writeFile($filename, $data, $mode = 0600)
|
||||
{
|
||||
if (!is_string($filename) || !is_string($data) || !is_numeric($mode)) {
|
||||
throw new InvalidArgumentException('Invalid input parameters');
|
||||
}
|
||||
|
||||
$tmpFile = self::getTempDir() . DIRECTORY_SEPARATOR . rand();
|
||||
|
||||
$res = @file_put_contents($tmpFile, $data);
|
||||
if ($res === false) {
|
||||
$error = error_get_last();
|
||||
throw new Error\Exception(
|
||||
'Error saving file "' . $tmpFile . '": ' .
|
||||
(is_array($error) ? $error['message'] : 'no error available')
|
||||
);
|
||||
}
|
||||
|
||||
if (self::getOS() !== self::WINDOWS) {
|
||||
if (!chmod($tmpFile, $mode)) {
|
||||
unlink($tmpFile);
|
||||
$error = error_get_last();
|
||||
//$error = (is_array($error) ? $error['message'] : 'no error available');
|
||||
throw new Error\Exception(
|
||||
'Error changing file mode of "' . $tmpFile . '": ' .
|
||||
(is_array($error) ? $error['message'] : 'no error available')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!rename($tmpFile, $filename)) {
|
||||
unlink($tmpFile);
|
||||
$error = error_get_last();
|
||||
throw new Error\Exception(
|
||||
'Error moving "' . $tmpFile . '" to "' . $filename . '": ' .
|
||||
(is_array($error) ? $error['message'] : 'no error available')
|
||||
);
|
||||
}
|
||||
|
||||
if (function_exists('opcache_invalidate')) {
|
||||
opcache_invalidate($filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the supplied path contains a Windows-style drive letter.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function pathContainsDriveLetter($path)
|
||||
{
|
||||
$letterAsciiValue = ord(strtoupper(substr($path, 0, 1)));
|
||||
return substr($path, 1, 1) === ':'
|
||||
&& $letterAsciiValue >= 65 && $letterAsciiValue <= 90;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the supplied path contains a stream wrapper
|
||||
* @param string $path
|
||||
* @return bool
|
||||
*/
|
||||
private static function pathContainsStreamWrapper($path)
|
||||
{
|
||||
return preg_match('/^[\w\d]*:\/{2}/', $path) === 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Time-related utility methods.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Error\Exception;
|
||||
use SAML\Logger;
|
||||
|
||||
class Time
|
||||
{
|
||||
/**
|
||||
* Whether the timezone has been initialized or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $tz_initialized = false;
|
||||
|
||||
|
||||
/**
|
||||
* This function generates a timestamp on the form used by the SAML protocols.
|
||||
*
|
||||
* @param int $instant The time the timestamp should represent. Defaults to current time.
|
||||
*
|
||||
* @return string The timestamp.
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function generateTimestamp($instant = null)
|
||||
{
|
||||
if ($instant === null) {
|
||||
$instant = time();
|
||||
}
|
||||
return gmdate('Y-m-d\TH:i:s\Z', $instant);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the timezone.
|
||||
*
|
||||
* This function should be called before any calls to date().
|
||||
*
|
||||
* @return void
|
||||
*@throws Exception If the timezone set in the configuration is invalid.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*
|
||||
*/
|
||||
public static function initTimezone()
|
||||
{
|
||||
if (self::$tz_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
$globalConfig = Configuration::getInstance();
|
||||
|
||||
$timezone = $globalConfig->getString('timezone', null);
|
||||
if ($timezone !== null) {
|
||||
if (!date_default_timezone_set($timezone)) {
|
||||
throw new Exception('Invalid timezone set in the "timezone" option in config.php.');
|
||||
}
|
||||
self::$tz_initialized = true;
|
||||
return;
|
||||
}
|
||||
// we don't have a timezone configured
|
||||
|
||||
Logger::maskErrors(E_ALL);
|
||||
$serverTimezone = date_default_timezone_get();
|
||||
Logger::popErrorMask();
|
||||
|
||||
// set the timezone to the default
|
||||
date_default_timezone_set($serverTimezone);
|
||||
self::$tz_initialized = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interpret a ISO8601 duration value relative to a given timestamp. Please note no fractions are allowed, neither
|
||||
* durations specified in the formats PYYYYMMDDThhmmss nor P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss].
|
||||
*
|
||||
* @param string $duration The duration, as a string.
|
||||
* @param int $timestamp The unix timestamp we should apply the duration to. Optional, default to the current
|
||||
* time.
|
||||
*
|
||||
* @return int The new timestamp, after the duration is applied.
|
||||
* @throws InvalidArgumentException If $duration is not a valid ISO 8601 duration or if the input parameters do
|
||||
* not have the right data types.
|
||||
*/
|
||||
public static function parseDuration($duration, $timestamp = null)
|
||||
{
|
||||
if (!(is_string($duration) && (is_int($timestamp) || is_null($timestamp)))) {
|
||||
throw new InvalidArgumentException('Invalid input parameters');
|
||||
}
|
||||
|
||||
// parse the duration. We use a very strict pattern
|
||||
$durationRegEx = '#^(-?)P(?:(?:(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)' .
|
||||
'(?:[.,]\d+)?S)?)?)|(?:(\\d+)W))$#D';
|
||||
if (!preg_match($durationRegEx, $duration, $matches)) {
|
||||
throw new InvalidArgumentException('Invalid ISO 8601 duration: ' . $duration);
|
||||
}
|
||||
|
||||
$durYears = (empty($matches[2]) ? 0 : (int) $matches[2]);
|
||||
$durMonths = (empty($matches[3]) ? 0 : (int) $matches[3]);
|
||||
$durDays = (empty($matches[4]) ? 0 : (int) $matches[4]);
|
||||
$durHours = (empty($matches[5]) ? 0 : (int) $matches[5]);
|
||||
$durMinutes = (empty($matches[6]) ? 0 : (int) $matches[6]);
|
||||
$durSeconds = (empty($matches[7]) ? 0 : (int) $matches[7]);
|
||||
$durWeeks = (empty($matches[8]) ? 0 : (int) $matches[8]);
|
||||
|
||||
if (!empty($matches[1])) {
|
||||
// negative
|
||||
$durYears = -$durYears;
|
||||
$durMonths = -$durMonths;
|
||||
$durDays = -$durDays;
|
||||
$durHours = -$durHours;
|
||||
$durMinutes = -$durMinutes;
|
||||
$durSeconds = -$durSeconds;
|
||||
$durWeeks = -$durWeeks;
|
||||
}
|
||||
|
||||
if ($timestamp === null) {
|
||||
$timestamp = time();
|
||||
}
|
||||
|
||||
if ($durYears !== 0 || $durMonths !== 0) {
|
||||
/* Special handling of months and years, since they aren't a specific interval, but
|
||||
* instead depend on the current time.
|
||||
*/
|
||||
|
||||
/* We need the year and month from the timestamp. Unfortunately, PHP doesn't have the
|
||||
* gmtime function. Instead we use the gmdate function, and split the result.
|
||||
*/
|
||||
$yearmonth = explode(':', gmdate('Y:n', $timestamp));
|
||||
$year = (int) ($yearmonth[0]);
|
||||
$month = (int) ($yearmonth[1]);
|
||||
|
||||
// remove the year and month from the timestamp
|
||||
$timestamp -= gmmktime(0, 0, 0, $month, 1, $year);
|
||||
|
||||
// add years and months, and normalize the numbers afterwards
|
||||
$year += $durYears;
|
||||
$month += $durMonths;
|
||||
while ($month > 12) {
|
||||
$year += 1;
|
||||
$month -= 12;
|
||||
}
|
||||
while ($month < 1) {
|
||||
$year -= 1;
|
||||
$month += 12;
|
||||
}
|
||||
|
||||
// add year and month back into timestamp
|
||||
$timestamp += gmmktime(0, 0, 0, $month, 1, $year);
|
||||
}
|
||||
|
||||
// add the other elements
|
||||
$timestamp += $durWeeks * 7 * 24 * 60 * 60;
|
||||
$timestamp += $durDays * 24 * 60 * 60;
|
||||
$timestamp += $durHours * 60 * 60;
|
||||
$timestamp += $durMinutes * 60;
|
||||
$timestamp += $durSeconds;
|
||||
|
||||
return $timestamp;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,480 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Utility class for XML and DOM manipulation.
|
||||
*
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML\Utils;
|
||||
|
||||
use DOMComment;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMException;
|
||||
use DOMNode;
|
||||
use DOMText;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use SAML2\DOMDocumentFactory;
|
||||
use SAML\Configuration;
|
||||
use SAML\Error;
|
||||
use SAML\Logger;
|
||||
use SAML\XML\Errors;
|
||||
|
||||
class XML
|
||||
{
|
||||
/**
|
||||
* This function performs some sanity checks on XML documents, and optionally validates them against their schema
|
||||
* if the 'validatexml' debugging option is enabled. A warning will be printed to the log if validation fails.
|
||||
*
|
||||
* @param string $message The SAML document we want to check.
|
||||
* @param string $type The type of document. Can be one of:
|
||||
* - 'saml20'
|
||||
* - 'saml11'
|
||||
* - 'saml-meta'
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \SAML\Error\Exception If $message contains a doctype declaration.
|
||||
*
|
||||
* @throws InvalidArgumentException If $message is not a string or $type is not a string containing one of the
|
||||
* values allowed.
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
|
||||
*/
|
||||
public static function checkSAMLMessage($message, $type)
|
||||
{
|
||||
$allowed_types = ['saml20', 'saml11', 'saml-meta'];
|
||||
if (!(is_string($message) && in_array($type, $allowed_types, true))) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
|
||||
// a SAML message should not contain a doctype-declaration
|
||||
if (strpos($message, '<!DOCTYPE') !== false) {
|
||||
throw new Error\Exception('XML contained a doctype declaration.');
|
||||
}
|
||||
|
||||
// see if debugging is enabled for XML validation
|
||||
$debug = Configuration::getInstance()->getArrayize('debug', ['validatexml' => false]);
|
||||
$enabled = Configuration::getInstance()->getBoolean('debug.validatexml', false);
|
||||
|
||||
if (
|
||||
!(in_array('validatexml', $debug, true) // implicitly enabled
|
||||
|| (array_key_exists('validatexml', $debug)
|
||||
&& $debug['validatexml'] === true)
|
||||
// explicitly enabled
|
||||
// TODO: deprecate this option and remove it in 2.0
|
||||
|| $enabled) // old 'debug.validatexml' configuration option
|
||||
) {
|
||||
// XML validation is disabled
|
||||
return;
|
||||
}
|
||||
|
||||
$result = true;
|
||||
switch ($type) {
|
||||
case 'saml11':
|
||||
$result = self::isValid($message, 'oasis-sstc-saml-schema-protocol-1.1.xsd');
|
||||
break;
|
||||
case 'saml20':
|
||||
$result = self::isValid($message, 'saml-schema-protocol-2.0.xsd');
|
||||
break;
|
||||
case 'saml-meta':
|
||||
$result = self::isValid($message, 'saml-schema-metadata-2.0.xsd');
|
||||
}
|
||||
if (is_string($result)) {
|
||||
Logger::warning($result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper function to log SAML messages that we send or receive.
|
||||
*
|
||||
* @param string|DOMElement $message The message, as an string containing the XML or an XML element.
|
||||
* @param string $type Whether this message is sent or received, encrypted or decrypted. The following
|
||||
* values are supported:
|
||||
* - 'in': for messages received.
|
||||
* - 'out': for outgoing messages.
|
||||
* - 'decrypt': for decrypted messages.
|
||||
* - 'encrypt': for encrypted messages.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws InvalidArgumentException If $type is not a string or $message is neither a string nor a \DOMElement.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function debugSAMLMessage($message, $type)
|
||||
{
|
||||
if (!(is_string($type) && (is_string($message) || $message instanceof DOMElement))) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
|
||||
// see if debugging is enabled for SAML messages
|
||||
$debug = Configuration::getInstance()->getArrayize('debug', ['saml' => false]);
|
||||
|
||||
if (
|
||||
!(in_array('saml', $debug, true) // implicitly enabled
|
||||
|| (array_key_exists('saml', $debug)
|
||||
&& $debug['saml'] === true)
|
||||
// explicitly enabled
|
||||
// TODO: deprecate the old style and remove it in 2.0
|
||||
|| (array_key_exists(0, $debug)
|
||||
&& $debug[0] === true)) // old style 'debug'
|
||||
) {
|
||||
// debugging messages is disabled
|
||||
return;
|
||||
}
|
||||
|
||||
if ($message instanceof DOMElement) {
|
||||
$message = $message->ownerDocument->saveXML($message);
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'in':
|
||||
Logger::debug('Received message:');
|
||||
break;
|
||||
case 'out':
|
||||
Logger::debug('Sending message:');
|
||||
break;
|
||||
case 'decrypt':
|
||||
Logger::debug('Decrypted message:');
|
||||
break;
|
||||
case 'encrypt':
|
||||
Logger::debug('Encrypted message:');
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
|
||||
$str = self::formatXMLString($message);
|
||||
foreach (explode("\n", $str) as $line) {
|
||||
Logger::debug($line);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format a DOM element.
|
||||
*
|
||||
* This function takes in a DOM element, and inserts whitespace to make it more readable. Note that whitespace
|
||||
* added previously will be removed.
|
||||
*
|
||||
* @param DOMNode $root The root element which should be formatted.
|
||||
* @param string $indentBase The indentation this element should be assumed to have. Defaults to an empty
|
||||
* string.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws InvalidArgumentException If $root is not a DOMElement or $indentBase is not a string.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function formatDOMElement(DOMNode $root, $indentBase = '')
|
||||
{
|
||||
if (!is_string($indentBase)) {
|
||||
throw new InvalidArgumentException('Invalid input parameters');
|
||||
}
|
||||
|
||||
// check what this element contains
|
||||
$fullText = ''; // all text in this element
|
||||
$textNodes = []; // text nodes which should be deleted
|
||||
$childNodes = []; // other child nodes
|
||||
for ($i = 0; $i < $root->childNodes->length; $i++) {
|
||||
/** @var DOMNode $child */
|
||||
$child = $root->childNodes->item($i);
|
||||
|
||||
if ($child instanceof DOMText) {
|
||||
$textNodes[] = $child;
|
||||
$fullText .= $child->wholeText;
|
||||
} elseif ($child instanceof DOMComment || $child instanceof DOMElement) {
|
||||
$childNodes[] = $child;
|
||||
} else {
|
||||
// unknown node type. We don't know how to format this
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$fullText = trim($fullText);
|
||||
if (strlen($fullText) > 0) {
|
||||
// we contain textelf
|
||||
$hasText = true;
|
||||
} else {
|
||||
$hasText = false;
|
||||
}
|
||||
|
||||
$hasChildNode = (count($childNodes) > 0);
|
||||
|
||||
if ($hasText && $hasChildNode) {
|
||||
// element contains both text and child nodes - we don't know how to format this one
|
||||
return;
|
||||
}
|
||||
|
||||
// remove text nodes
|
||||
foreach ($textNodes as $node) {
|
||||
$root->removeChild($node);
|
||||
}
|
||||
|
||||
if ($hasText) {
|
||||
// only text - add a single text node to the element with the full text
|
||||
$root->appendChild(new DOMText($fullText));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$hasChildNode) {
|
||||
// empty node. Nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
/* Element contains only child nodes - add indentation before each one, and
|
||||
* format child elements.
|
||||
*/
|
||||
$childIndentation = $indentBase . ' ';
|
||||
foreach ($childNodes as $node) {
|
||||
// add indentation before node
|
||||
$root->insertBefore(new DOMText("\n" . $childIndentation), $node);
|
||||
|
||||
// format child elements
|
||||
if ($node instanceof DOMElement) {
|
||||
self::formatDOMElement($node, $childIndentation);
|
||||
}
|
||||
}
|
||||
|
||||
// add indentation before closing tag
|
||||
$root->appendChild(new DOMText("\n" . $indentBase));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format an XML string.
|
||||
*
|
||||
* This function formats an XML string using the formatDOMElement() function.
|
||||
*
|
||||
* @param string $xml An XML string which should be formatted.
|
||||
* @param string $indentBase Optional indentation which should be applied to all the output. Optional, defaults
|
||||
* to ''.
|
||||
*
|
||||
* @return string The formatted string.
|
||||
* @throws InvalidArgumentException If the parameters are not strings.
|
||||
* @throws DOMException If the input does not parse correctly as an XML string.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function formatXMLString($xml, $indentBase = '')
|
||||
{
|
||||
if (!is_string($xml) || !is_string($indentBase)) {
|
||||
throw new InvalidArgumentException('Invalid input parameters');
|
||||
}
|
||||
|
||||
try {
|
||||
$doc = DOMDocumentFactory::fromString($xml);
|
||||
} catch (Exception $e) {
|
||||
throw new DOMException('Error parsing XML string.');
|
||||
}
|
||||
|
||||
$root = $doc->firstChild;
|
||||
self::formatDOMElement($root, $indentBase);
|
||||
|
||||
return $doc->saveXML($root);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function finds direct descendants of a DOM element with the specified
|
||||
* localName and namespace. They are returned in an array.
|
||||
*
|
||||
* This function accepts the same shortcuts for namespaces as the isDOMNodeOfType function.
|
||||
*
|
||||
* @param DOMNode $element The element we should look in.
|
||||
* @param string $localName The name the element should have.
|
||||
* @param string $namespaceURI The namespace the element should have.
|
||||
*
|
||||
* @return array Array with the matching elements in the order they are found. An empty array is
|
||||
* returned if no elements match.
|
||||
* @throws InvalidArgumentException If $element is not an instance of DOMElement, $localName is not a string or
|
||||
* $namespaceURI is not a string.
|
||||
*/
|
||||
public static function getDOMChildren(DOMNode $element, $localName, $namespaceURI)
|
||||
{
|
||||
if (!is_string($localName) || !is_string($namespaceURI)) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
|
||||
$ret = [];
|
||||
|
||||
for ($i = 0; $i < $element->childNodes->length; $i++) {
|
||||
/** @var DOMNode $child */
|
||||
$child = $element->childNodes->item($i);
|
||||
|
||||
// skip text nodes and comment elements
|
||||
if ($child instanceof DOMText || $child instanceof DOMComment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self::isDOMNodeOfType($child, $localName, $namespaceURI) === true) {
|
||||
$ret[] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function extracts the text from DOMElements which should contain only text content.
|
||||
*
|
||||
* @param DOMElement $element The element we should extract text from.
|
||||
*
|
||||
* @return string The text content of the element.
|
||||
* @throws \SAML\Error\Exception If the element contains a non-text child node.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function getDOMText(DOMElement $element)
|
||||
{
|
||||
$txt = '';
|
||||
|
||||
for ($i = 0; $i < $element->childNodes->length; $i++) {
|
||||
/** @var DOMElement $child */
|
||||
$child = $element->childNodes->item($i);
|
||||
if (!($child instanceof DOMText)) {
|
||||
throw new Error\Exception($element->localName . ' contained a non-text child node.');
|
||||
}
|
||||
|
||||
$txt .= $child->wholeText;
|
||||
}
|
||||
|
||||
$txt = trim($txt);
|
||||
return $txt;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function checks if the DOMElement has the correct localName and namespaceURI.
|
||||
*
|
||||
* We also define the following shortcuts for namespaces:
|
||||
* - '@ds': 'http://www.w3.org/2000/09/xmldsig#'
|
||||
* - '@md': 'urn:oasis:names:tc:SAML:2.0:metadata'
|
||||
* - '@saml1': 'urn:oasis:names:tc:SAML:1.0:assertion'
|
||||
* - '@saml1md': 'urn:oasis:names:tc:SAML:profiles:v1metadata'
|
||||
* - '@saml1p': 'urn:oasis:names:tc:SAML:1.0:protocol'
|
||||
* - '@saml2': 'urn:oasis:names:tc:SAML:2.0:assertion'
|
||||
* - '@saml2p': 'urn:oasis:names:tc:SAML:2.0:protocol'
|
||||
*
|
||||
* @param DOMNode $element The element we should check.
|
||||
* @param string $name The local name the element should have.
|
||||
* @param string $nsURI The namespaceURI the element should have.
|
||||
*
|
||||
* @return boolean True if both namespace and local name matches, false otherwise.
|
||||
* @throws InvalidArgumentException If the namespace shortcut is unknown.
|
||||
*
|
||||
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function isDOMNodeOfType(DOMNode $element, $name, $nsURI)
|
||||
{
|
||||
if (!is_string($name) || !is_string($nsURI) || strlen($nsURI) === 0) {
|
||||
// most likely a comment-node
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if the namespace is a shortcut, and expand it if it is
|
||||
if ($nsURI[0] === '@') {
|
||||
// the defined shortcuts
|
||||
$shortcuts = [
|
||||
'@ds' => 'http://www.w3.org/2000/09/xmldsig#',
|
||||
'@md' => 'urn:oasis:names:tc:SAML:2.0:metadata',
|
||||
'@saml1' => 'urn:oasis:names:tc:SAML:1.0:assertion',
|
||||
'@saml1md' => 'urn:oasis:names:tc:SAML:profiles:v1metadata',
|
||||
'@saml1p' => 'urn:oasis:names:tc:SAML:1.0:protocol',
|
||||
'@saml2' => 'urn:oasis:names:tc:SAML:2.0:assertion',
|
||||
'@saml2p' => 'urn:oasis:names:tc:SAML:2.0:protocol',
|
||||
'@shibmd' => 'urn:mace:shibboleth:metadata:1.0',
|
||||
];
|
||||
|
||||
// check if it is a valid shortcut
|
||||
if (!array_key_exists($nsURI, $shortcuts)) {
|
||||
throw new InvalidArgumentException('Unknown namespace shortcut: ' . $nsURI);
|
||||
}
|
||||
|
||||
// expand the shortcut
|
||||
$nsURI = $shortcuts[$nsURI];
|
||||
}
|
||||
if ($element->localName !== $name) {
|
||||
return false;
|
||||
}
|
||||
if ($element->namespaceURI !== $nsURI) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function attempts to validate an XML string against the specified schema. It will parse the string into a
|
||||
* DOM document and validate this document against the schema.
|
||||
*
|
||||
* Note that this function returns values that are evaluated as a logical true, both when validation works and when
|
||||
* it doesn't. Please use strict comparisons to check the values returned.
|
||||
*
|
||||
* @param string|DOMDocument $xml The XML string or document which should be validated.
|
||||
* @param string $schema The filename of the schema that should be used to validate the document.
|
||||
*
|
||||
* @return bool|string Returns a string with errors found if validation fails. True if validation passes ok.
|
||||
* @throws InvalidArgumentException If $schema is not a string, or $xml is neither a string nor a \DOMDocument.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
|
||||
*/
|
||||
public static function isValid($xml, $schema)
|
||||
{
|
||||
if (!(is_string($schema) && (is_string($xml) || $xml instanceof DOMDocument))) {
|
||||
throw new InvalidArgumentException('Invalid input parameters.');
|
||||
}
|
||||
|
||||
Errors::begin();
|
||||
|
||||
if ($xml instanceof DOMDocument) {
|
||||
$dom = $xml;
|
||||
$res = true;
|
||||
} else {
|
||||
try {
|
||||
$dom = DOMDocumentFactory::fromString($xml);
|
||||
$res = true;
|
||||
} catch (Exception $e) {
|
||||
$res = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($res) {
|
||||
$config = Configuration::getInstance();
|
||||
/** @var string $schemaPath */
|
||||
$schemaPath = $config->resolvePath('schemas');
|
||||
$schemaFile = $schemaPath . '/' . $schema;
|
||||
|
||||
libxml_set_external_entity_loader(
|
||||
function ($public, $system, $context) {
|
||||
if (filter_var($system, FILTER_VALIDATE_URL) === $system) {
|
||||
return null;
|
||||
}
|
||||
return $system;
|
||||
}
|
||||
);
|
||||
|
||||
$res = $dom->schemaValidate($schemaFile);
|
||||
if ($res) {
|
||||
Errors::end();
|
||||
return true;
|
||||
}
|
||||
|
||||
$errorText = "Schema validation failed on XML string:\n";
|
||||
} else {
|
||||
$errorText = "Failed to parse XML string for schema validation:\n";
|
||||
}
|
||||
|
||||
$errors = Errors::end();
|
||||
$errorText .= Errors::formatErrors($errors);
|
||||
|
||||
return $errorText;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,666 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\XHTML;
|
||||
|
||||
use Exception;
|
||||
use SAML\Configuration;
|
||||
use SAML\Logger;
|
||||
use SAML\Metadata\MetaDataStorageHandler;
|
||||
use SAML\Session;
|
||||
use SAML\Utils;
|
||||
|
||||
/**
|
||||
* 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 IdPDisco
|
||||
{
|
||||
/**
|
||||
* An instance of the configuration class.
|
||||
*
|
||||
* @var 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 MetaDataStorageHandler
|
||||
*/
|
||||
protected $metadata;
|
||||
|
||||
/**
|
||||
* The users session.
|
||||
*
|
||||
* @var 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 = [];
|
||||
|
||||
/**
|
||||
* 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 = Configuration::getInstance();
|
||||
$this->metadata = MetaDataStorageHandler::getMetadataHandler();
|
||||
$this->session = 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 = 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.
|
||||
* @return void
|
||||
*/
|
||||
protected function log($message)
|
||||
{
|
||||
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|null 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.
|
||||
* @return void
|
||||
*/
|
||||
protected function setCookie($name, $value)
|
||||
{
|
||||
$prefixedName = 'idpdisco_' . $this->instance . '_' . $name;
|
||||
|
||||
$params = [
|
||||
// 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,
|
||||
];
|
||||
|
||||
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|null 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 = [];
|
||||
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|null 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|null 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|null 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.
|
||||
* @return void
|
||||
*/
|
||||
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|null 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 = [];
|
||||
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)');
|
||||
Utils\HTTP::redirectTrustedURL($extDiscoveryStorage, [
|
||||
'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 . ')'
|
||||
);
|
||||
Utils\HTTP::redirectTrustedURL($this->returnURL, [$this->returnIdParam => $idp]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isPassive) {
|
||||
$this->log('Choice not made. (Redirecting the user back without answer)');
|
||||
Utils\HTTP::redirectTrustedURL($this->returnURL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles a request to this discovery service.
|
||||
*
|
||||
* The IdP disco parameters should be set before calling this function.
|
||||
* @return void
|
||||
*/
|
||||
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 . ')'
|
||||
);
|
||||
Utils\HTTP::redirectTrustedURL(
|
||||
$this->returnURL,
|
||||
[$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 Template($this->config, $templateFile, 'disco');
|
||||
|
||||
$fallbackLanguage = 'en';
|
||||
$defaultLanguage = $this->config->getString('language.default', $fallbackLanguage);
|
||||
$translator = $t->getTranslator();
|
||||
$language = $translator->getLanguage()->getLanguage();
|
||||
$tryLanguages = [0 => $language, 1 => $defaultLanguage, 2 => $fallbackLanguage];
|
||||
|
||||
$newlist = [];
|
||||
foreach ($idpList as $entityid => $data) {
|
||||
$newlist[$entityid]['entityid'] = $entityid;
|
||||
foreach ($tryLanguages as $lang) {
|
||||
if ($name = $this->getEntityDisplayName($data, $lang)) {
|
||||
$newlist[$entityid]['name'] = $name;
|
||||
break 1;
|
||||
}
|
||||
}
|
||||
if (empty($newlist[$entityid]['name'])) {
|
||||
$newlist[$entityid]['name'] = $entityid;
|
||||
}
|
||||
foreach ($tryLanguages as $lang) {
|
||||
if (!empty($data['description'][$lang])) {
|
||||
$newlist[$entityid]['description'] = $data['description'][$lang];
|
||||
break 1;
|
||||
}
|
||||
}
|
||||
if (!empty($data['icon'])) {
|
||||
$newlist[$entityid]['icon'] = $data['icon'];
|
||||
$newlist[$entityid]['iconurl'] = Utils\HTTP::resolveURL($data['icon']);
|
||||
}
|
||||
}
|
||||
usort(
|
||||
$newlist,
|
||||
/**
|
||||
* @param array $idpentry1
|
||||
* @param array $idpentry2
|
||||
* @return int
|
||||
*/
|
||||
function (array $idpentry1, array $idpentry2) {
|
||||
return strcasecmp($idpentry1['name'], $idpentry2['name']);
|
||||
}
|
||||
);
|
||||
|
||||
$t->data['idplist'] = $newlist;
|
||||
$t->data['preferredidp'] = $preferredIdP;
|
||||
$t->data['return'] = $this->returnURL;
|
||||
$t->data['returnIDParam'] = $this->returnIdParam;
|
||||
$t->data['entityID'] = $this->spEntityId;
|
||||
$t->data['urlpattern'] = htmlspecialchars(Utils\HTTP::getSelfURLNoQuery());
|
||||
$t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', false);
|
||||
$t->show();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array $idpData
|
||||
* @param string $language
|
||||
* @return string|null
|
||||
*/
|
||||
private function getEntityDisplayName(array $idpData, $language)
|
||||
{
|
||||
if (isset($idpData['UIInfo']['DisplayName'][$language])) {
|
||||
return $idpData['UIInfo']['DisplayName'][$language];
|
||||
} elseif (isset($idpData['name'][$language])) {
|
||||
return $idpData['name'][$language];
|
||||
} elseif (isset($idpData['OrganizationDisplayName'][$language])) {
|
||||
return $idpData['OrganizationDisplayName'][$language];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,907 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* A minimalistic XHTML PHP based template system implemented for SimpleSAMLphp.
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML\XHTML;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use SAML\Configuration;
|
||||
use SAML\Locale\Language;
|
||||
use SAML\Locale\Localization;
|
||||
use SAML\Locale\Translate;
|
||||
use SAML\Logger;
|
||||
use SAML\Module;
|
||||
use SAML\TwigConfigurableI18n\Twig\Environment as Twig_Environment;
|
||||
use SAML\TwigConfigurableI18n\Twig\Extensions\Extension\I18n as Twig_Extensions_Extension_I18n;
|
||||
use SAML\Utils;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Twig\Environment;
|
||||
use Twig\Error\LoaderError;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class Template extends Response
|
||||
{
|
||||
/**
|
||||
* The data associated with this template, accessible within the template itself.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* A translator instance configured to work with this template.
|
||||
*
|
||||
* @var Translate
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* The localization backend
|
||||
*
|
||||
* @var Localization
|
||||
*/
|
||||
private $localization;
|
||||
|
||||
/**
|
||||
* The configuration to use in this template.
|
||||
*
|
||||
* @var Configuration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
/**
|
||||
* The file to load in this template.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $template = 'default.php';
|
||||
|
||||
/**
|
||||
* The twig environment.
|
||||
*
|
||||
* @var Environment
|
||||
* @psalm-suppress PropertyNotSetInConstructor Remove this annotation in 2.0
|
||||
*/
|
||||
private $twig;
|
||||
|
||||
/**
|
||||
* The template name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $twig_template;
|
||||
|
||||
/**
|
||||
* Current module, if any.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $module;
|
||||
|
||||
/**
|
||||
* Whether to use the new user interface or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $useNewUI = false;
|
||||
|
||||
|
||||
/**
|
||||
* 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 TemplateControllerInterface|null
|
||||
*/
|
||||
private $controller = null;
|
||||
|
||||
|
||||
/**
|
||||
* 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 has
|
||||
* the 'default' string in the "name" key, and 'null' in the "module" key.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $theme = ['module' => null, 'name' => 'default'];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param 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(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']) = $this->findModuleAndTemplateName(
|
||||
$this->configuration->getString('theme.use', 'default')
|
||||
);
|
||||
|
||||
// initialize internationalization system
|
||||
$this->translator = new Translate($configuration, $defaultDictionary);
|
||||
$this->localization = new Localization($configuration);
|
||||
|
||||
// check if we are supposed to use the new UI
|
||||
$this->useNewUI = $this->configuration->getBoolean('usenewui', false);
|
||||
|
||||
if ($this->useNewUI) {
|
||||
// check if we need to attach a theme controller
|
||||
$controller = $this->configuration->getString('theme.controller', false);
|
||||
if (
|
||||
$controller
|
||||
&& class_exists($controller)
|
||||
&& in_array(TemplateControllerInterface::class, class_implements($controller))
|
||||
) {
|
||||
/** @var TemplateControllerInterface $this->controller */
|
||||
$this->controller = new $controller();
|
||||
}
|
||||
|
||||
$this->twig = $this->setupTwig();
|
||||
}
|
||||
|
||||
$this->charset = 'UTF-8';
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the URL of an asset, including a cache-buster parameter that depends on the last modification time of
|
||||
* the original file.
|
||||
* @param string $asset
|
||||
* @param string|null $module
|
||||
* @return string
|
||||
*/
|
||||
public function asset($asset, $module = null)
|
||||
{
|
||||
$baseDir = $this->configuration->getBaseDir();
|
||||
if (is_null($module)) {
|
||||
$file = $baseDir . 'www/assets/' . $asset;
|
||||
$basePath = $this->configuration->getBasePath();
|
||||
$path = $basePath . 'assets/' . $asset;
|
||||
} else {
|
||||
$file = $baseDir . 'modules/' . $module . '/www/assets/' . $asset;
|
||||
$path = Module::getModuleUrl($module . '/assets/' . $asset);
|
||||
}
|
||||
|
||||
if (!file_exists($file)) {
|
||||
// don't be too harsh if an asset is missing, just pretend it's there...
|
||||
return $path;
|
||||
}
|
||||
|
||||
$tag = $this->configuration->getVersion();
|
||||
if ($tag === 'master') {
|
||||
$tag = strval(filemtime($file));
|
||||
}
|
||||
$tag = substr(hash('md5', $tag), 0, 5);
|
||||
|
||||
return $path . '?tag=' . $tag;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the normalized template name.
|
||||
*
|
||||
* @return string The name of the template to use.
|
||||
*/
|
||||
public function getTemplateName()
|
||||
{
|
||||
return $this->normalizeTemplateName($this->template);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
if ($this->useNewUI) {
|
||||
return $templateName . '.twig';
|
||||
}
|
||||
return $templateName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set up the places where twig can look for templates.
|
||||
*
|
||||
* @return TemplateLoader The twig template loader or false if the template does not exist.
|
||||
* @throws LoaderError In case a failure occurs.
|
||||
*/
|
||||
private function setupTwigTemplatepaths()
|
||||
{
|
||||
$filename = $this->normalizeTemplateName($this->template);
|
||||
|
||||
// get namespace if any
|
||||
list($namespace, $filename) = $this->findModuleAndTemplateName($filename);
|
||||
$this->twig_template = ($namespace !== null) ? '@' . $namespace . '/' . $filename : $filename;
|
||||
$loader = new TemplateLoader();
|
||||
$templateDirs = $this->findThemeTemplateDirs();
|
||||
if ($this->module && $this->module != 'core') {
|
||||
$modDir = TemplateLoader::getModuleTemplateDir($this->module);
|
||||
$templateDirs[] = [$this->module => $modDir];
|
||||
$templateDirs[] = ['__parent__' => $modDir];
|
||||
}
|
||||
if ($this->theme['module']) {
|
||||
try {
|
||||
$templateDirs[] = [
|
||||
$this->theme['module'] => TemplateLoader::getModuleTemplateDir($this->theme['module'])
|
||||
];
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// either the module is not enabled or it has no "templates" directory, ignore
|
||||
}
|
||||
}
|
||||
|
||||
$templateDirs[] = ['core' => TemplateLoader::getModuleTemplateDir('core')];
|
||||
|
||||
// default, themeless templates are checked last
|
||||
$templateDirs[] = [
|
||||
FilesystemLoader::MAIN_NAMESPACE => $this->configuration->resolvePath('templates')
|
||||
];
|
||||
foreach ($templateDirs as $entry) {
|
||||
$loader->addPath($entry[key($entry)], key($entry));
|
||||
}
|
||||
return $loader;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setup twig.
|
||||
* @return Environment
|
||||
* @throws Exception if the template does not exist
|
||||
*/
|
||||
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)) {
|
||||
throw new Exception('Template-file \"' . $this->getTemplateName() . '\" does not exist.');
|
||||
}
|
||||
|
||||
// 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']);
|
||||
}
|
||||
|
||||
// set up translation
|
||||
$options = [
|
||||
'cache' => $cache,
|
||||
'auto_reload' => $auto_reload,
|
||||
'translation_function' => [Translate::class, 'translateSingularGettext'],
|
||||
'translation_function_plural' => [Translate::class, 'translatePluralGettext'],
|
||||
];
|
||||
|
||||
$twig = new Twig_Environment($loader, $options);
|
||||
$twig->addExtension(new Twig_Extensions_Extension_I18n());
|
||||
|
||||
$twig->addFunction(new TwigFunction('moduleURL', [Module::class, 'getModuleURL']));
|
||||
|
||||
// initialize some basic context
|
||||
$langParam = $this->configuration->getString('language.parameter.name', 'language');
|
||||
$twig->addGlobal('languageParameterName', $langParam);
|
||||
$twig->addGlobal('localeBackend', Localization::GETTEXT_I18N_BACKEND);
|
||||
$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));
|
||||
$twig->addGlobal('baseurlpath', ltrim($this->configuration->getBasePath(), '/'));
|
||||
|
||||
// add a filter for translations out of arrays
|
||||
$twig->addFilter(
|
||||
new TwigFilter(
|
||||
'translateFromArray',
|
||||
[Translate::class, 'translateFromArray'],
|
||||
['needs_context' => true]
|
||||
)
|
||||
);
|
||||
|
||||
// add an asset() function
|
||||
$twig->addFunction(new TwigFunction('asset', [$this, 'asset']));
|
||||
|
||||
if ($this->controller !== null) {
|
||||
$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 (!isset($this->theme['module'])) {
|
||||
// no module involved
|
||||
return [];
|
||||
}
|
||||
|
||||
// setup directories & namespaces
|
||||
$themeDir = 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
|
||||
Logger::warning('Empty theme directory for theme "' . $this->theme['name'] . '".');
|
||||
return [];
|
||||
}
|
||||
|
||||
$themeTemplateDirs = [];
|
||||
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') ? FilesystemLoader::MAIN_NAMESPACE : $entry;
|
||||
$themeTemplateDirs[] = [$ns => $themeDir . '/' . $entry];
|
||||
}
|
||||
return $themeTemplateDirs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the template directory of a module, if it exists.
|
||||
*
|
||||
* @param string $module
|
||||
* @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 (!Module::isModuleEnabled($module)) {
|
||||
throw new InvalidArgumentException('The module \'' . $module . '\' is not enabled.');
|
||||
}
|
||||
$moduledir = 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.
|
||||
* @return void
|
||||
*/
|
||||
public function addTemplatesFromModule($module)
|
||||
{
|
||||
$dir = TemplateLoader::getModuleTemplateDir($module);
|
||||
/** @var FilesystemLoader $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|null The array containing information of all available languages.
|
||||
*/
|
||||
private function generateLanguageBar()
|
||||
{
|
||||
$languages = $this->translator->getLanguage()->getLanguageList();
|
||||
ksort($languages);
|
||||
$langmap = null;
|
||||
if (count($languages) > 1) {
|
||||
$parameterName = $this->getTranslator()->getLanguage()->getLanguageParameterName();
|
||||
$langmap = [];
|
||||
foreach ($languages as $lang => $current) {
|
||||
$lang = strtolower($lang);
|
||||
$langname = $this->translator->getLanguage()->getLanguageLocalizedName($lang);
|
||||
$url = false;
|
||||
if (!$current) {
|
||||
$url = htmlspecialchars(Utils\HTTP::addURLParameters(
|
||||
'',
|
||||
[$parameterName => $lang]
|
||||
));
|
||||
}
|
||||
$langmap[$lang] = [
|
||||
'name' => $langname,
|
||||
'url' => $url,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $langmap;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set some default context
|
||||
* @return void
|
||||
*/
|
||||
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';
|
||||
}
|
||||
|
||||
$this->data['year'] = date('Y');
|
||||
|
||||
$this->data['header'] = $this->configuration->getValue('theme.header', 'SimpleSAMLphp');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the contents produced by this template.
|
||||
*
|
||||
* @return string The HTML rendered by this template, as a string.
|
||||
* @throws Exception if the template cannot be found.
|
||||
*/
|
||||
protected function getContents()
|
||||
{
|
||||
$this->twigDefaultContext();
|
||||
if ($this->controller) {
|
||||
$this->controller->display($this->data);
|
||||
}
|
||||
return $this->twig->render($this->twig_template, $this->data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send this template as a response.
|
||||
*
|
||||
* @return Response This response.
|
||||
* @throws Exception if the template cannot be found.
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
$this->content = $this->getContents();
|
||||
return parent::send();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show the template to the user.
|
||||
*
|
||||
* This method is a remnant of the old templating system, where templates where shown manually instead of
|
||||
* returning a response.
|
||||
*
|
||||
* @return void
|
||||
* @deprecated Do not use this method, use Twig + send() instead. Will be removed in 2.0
|
||||
*/
|
||||
public function show()
|
||||
{
|
||||
if ($this->useNewUI) {
|
||||
echo $this->getContents();
|
||||
} else {
|
||||
require($this->findTemplatePath($this->template));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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) ? [$tmp[0], $tmp[1]] : [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.
|
||||
* @param bool $throw_exception
|
||||
*
|
||||
* @return string|null 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));
|
||||
$extensions = ['.tpl.php', '.php'];
|
||||
|
||||
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 = Module::getModuleDir($this->theme['module']) .
|
||||
'/themes/' . $this->theme['name'] . '/' . $templateModule . '/' . $templateName;
|
||||
} elseif ($templateModule !== 'default') {
|
||||
// .../module/<templateModule>/templates/<templateName>
|
||||
$filename = Module::getModuleDir($templateModule) . '/templates/' . $templateName;
|
||||
} else {
|
||||
// .../templates/<theme>/<templateName>
|
||||
$base = $this->configuration->getPathValue('templatedir', 'templates/') ?: 'templates/';
|
||||
$filename = $base . $templateName;
|
||||
}
|
||||
|
||||
$filename = $this->normalizeTemplateName($filename);
|
||||
foreach ($extensions as $extension) {
|
||||
if (file_exists($filename . $extension)) {
|
||||
return $filename . $extension;
|
||||
}
|
||||
}
|
||||
|
||||
// not found in current theme
|
||||
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 = Module::getModuleDir($templateModule) . '/templates/' . $templateName;
|
||||
} else {
|
||||
// .../templates/<templateName>
|
||||
$base = $this->configuration->getPathValue('templatedir', 'templates/') ?: 'templates/';
|
||||
$filename = $base . '/' . $templateName;
|
||||
}
|
||||
|
||||
$filename = $this->normalizeTemplateName($filename);
|
||||
foreach ($extensions as $extension) {
|
||||
if (file_exists($filename . $extension)) {
|
||||
return $filename . $extension;
|
||||
}
|
||||
}
|
||||
|
||||
// not found in default template
|
||||
if ($throw_exception) {
|
||||
// log error and throw exception
|
||||
$error = 'Template: Could not find template file [' . $template . '] at [' . $filename . ']';
|
||||
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 Translate The translator that will be used with this template.
|
||||
*/
|
||||
public function getTranslator()
|
||||
{
|
||||
return $this->translator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the internal localization object used by this template.
|
||||
*
|
||||
* @return Localization The localization object that will be used with this template.
|
||||
*/
|
||||
public function getLocalization()
|
||||
{
|
||||
return $this->localization;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current instance of Twig in use.
|
||||
*
|
||||
* @return Environment The Twig instance in use, or null if Twig is not used.
|
||||
*/
|
||||
public function getTwig()
|
||||
{
|
||||
return $this->twig;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Deprecated methods of this interface, all of them should go away.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @param string $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 string $language
|
||||
* @param bool $setLanguageCookie
|
||||
* @return void
|
||||
*
|
||||
* @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 Language::getLanguageCookie();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $language
|
||||
* @return void
|
||||
*
|
||||
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguageCookie()
|
||||
* instead.
|
||||
*/
|
||||
public static function setLanguageCookie($language)
|
||||
{
|
||||
Language::setLanguageCookie($language);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wraps Language->getLanguageList
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getLanguageList()
|
||||
{
|
||||
return $this->translator->getLanguage()->getLanguageList();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $tag
|
||||
*
|
||||
* @return array|null
|
||||
* @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.
|
||||
*
|
||||
* @param array $translations
|
||||
* @return string
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @deprecated This function will be removed in SSP 2.0. Use Twig-templates instead
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
private function includeAtTemplateBase($file)
|
||||
{
|
||||
$data = $this->data;
|
||||
|
||||
$filename = $this->findTemplatePath($file);
|
||||
|
||||
include($filename);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wraps Translate->includeInlineTranslation()
|
||||
*
|
||||
* @param string $tag
|
||||
* @param string $translation
|
||||
* @return void
|
||||
*@deprecated This method will be removed in SSP 2.0. Please use
|
||||
* \SimpleSAML\Locale\Translate::includeInlineTranslation() instead.
|
||||
*
|
||||
* @see \SAML\Locale\Translate::includeInlineTranslation()
|
||||
*/
|
||||
public function includeInlineTranslation($tag, $translation)
|
||||
{
|
||||
$this->translator->includeInlineTranslation($tag, $translation);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
* @param Configuration|null $otherConfig
|
||||
* @return void
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param string $tag
|
||||
* @return string
|
||||
*@see \SAML\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.
|
||||
*
|
||||
* @param string $tag
|
||||
* @param array $replacements
|
||||
* @param bool $fallbackdefault
|
||||
* @param array $oldreplacements
|
||||
* @param bool $striptags
|
||||
* @return string|null
|
||||
*@see \SAML\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 = [],
|
||||
$fallbackdefault = true,
|
||||
$oldreplacements = [],
|
||||
$striptags = false
|
||||
) {
|
||||
return $this->translator->t($tag, $replacements, $fallbackdefault, $oldreplacements, $striptags);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\XHTML;
|
||||
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* 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 Environment $twig The current twig environment.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUpTwig(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);
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace SAML\XHTML;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SAML\Module;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
/**
|
||||
* This class extends the Twig\Loader\FilesystemLoader so that we can load templates from modules in twig, even
|
||||
* when the main template is not part of a module (or the same one).
|
||||
*
|
||||
* @package simplesamlphp/simplesamlphp
|
||||
*
|
||||
* @psalm-suppress DeprecatedInterface This suppress may be removed when Twig 3.0 becomes the default
|
||||
*/
|
||||
class TemplateLoader extends FilesystemLoader
|
||||
{
|
||||
/**
|
||||
* This method adds a namespace dynamically so that we can load templates from modules whenever we want.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param string $name
|
||||
* @param bool $throw
|
||||
* @return string|false|null
|
||||
*/
|
||||
protected function findTemplate($name, $throw = true)
|
||||
{
|
||||
list($namespace, $shortname) = $this->parseModuleName($name);
|
||||
if (!in_array($namespace, $this->paths, true) && $namespace !== self::MAIN_NAMESPACE) {
|
||||
$this->addPath(self::getModuleTemplateDir($namespace), $namespace);
|
||||
}
|
||||
return parent::findTemplate($name, $throw);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse the name of a template in a module.
|
||||
*
|
||||
* @param string $name The full name of the template, including namespace and template name / path.
|
||||
* @param string $default
|
||||
*
|
||||
* @return array An array with the corresponding namespace and name of the template. The namespace defaults to
|
||||
* \Twig\Loader\FilesystemLoader::MAIN_NAMESPACE, if none was specified in $name.
|
||||
*/
|
||||
protected function parseModuleName($name, $default = self::MAIN_NAMESPACE)
|
||||
{
|
||||
if (strpos($name, ':')) {
|
||||
// we have our old SSP format
|
||||
list($namespace, $shortname) = explode(':', $name, 2);
|
||||
$shortname = strtr($shortname, [
|
||||
'.tpl.php' => '.twig',
|
||||
'.php' => '.twig',
|
||||
]);
|
||||
return [$namespace, $shortname];
|
||||
}
|
||||
return [$default, $name];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the template directory of a module, if it exists.
|
||||
*
|
||||
* @param string $module
|
||||
* @return string The templates directory of a module.
|
||||
*
|
||||
* @throws InvalidArgumentException If the module is not enabled or it has no templates directory.
|
||||
*/
|
||||
public static function getModuleTemplateDir($module)
|
||||
{
|
||||
if (!Module::isModuleEnabled($module)) {
|
||||
throw new InvalidArgumentException('The module \'' . $module . '\' is not enabled.');
|
||||
}
|
||||
$moduledir = 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This class defines an interface for accessing errors from the XML library.
|
||||
*
|
||||
* In PHP versions which doesn't support accessing error information, this class
|
||||
* will hide that, and pretend that no errors were logged.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML\XML;
|
||||
|
||||
use LibXMLError;
|
||||
|
||||
class Errors
|
||||
{
|
||||
/**
|
||||
* @var array This is an stack of error logs. The topmost element is the one we are currently working on.
|
||||
*/
|
||||
private static $errorStack = [];
|
||||
|
||||
/**
|
||||
* @var bool This is the xml error state we had before we began logging.
|
||||
*/
|
||||
private static $xmlErrorState;
|
||||
|
||||
|
||||
/**
|
||||
* Append current XML errors to the current stack level.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function addErrors()
|
||||
{
|
||||
$currentErrors = libxml_get_errors();
|
||||
libxml_clear_errors();
|
||||
|
||||
$level = count(self::$errorStack) - 1;
|
||||
self::$errorStack[$level] = array_merge(self::$errorStack[$level], $currentErrors);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start error logging.
|
||||
*
|
||||
* A call to this function will begin a new error logging context. Every call must have
|
||||
* a corresponding call to end().
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function begin()
|
||||
{
|
||||
|
||||
// Check whether the error access functions are present
|
||||
if (!function_exists('libxml_use_internal_errors')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count(self::$errorStack) === 0) {
|
||||
// No error logging is currently in progress. Initialize it.
|
||||
self::$xmlErrorState = libxml_use_internal_errors(true);
|
||||
libxml_clear_errors();
|
||||
} else {
|
||||
/* We have already started error logging. Append the current errors to the
|
||||
* list of errors in this level.
|
||||
*/
|
||||
self::addErrors();
|
||||
}
|
||||
|
||||
// Add a new level to the error stack
|
||||
self::$errorStack[] = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* End error logging.
|
||||
*
|
||||
* @return array An array with the LibXMLErrors which has occurred since begin() was called.
|
||||
*/
|
||||
public static function end()
|
||||
{
|
||||
// Check whether the error access functions are present
|
||||
if (!function_exists('libxml_use_internal_errors')) {
|
||||
// Pretend that no errors occurred
|
||||
return [];
|
||||
}
|
||||
|
||||
// Add any errors which may have occurred
|
||||
self::addErrors();
|
||||
|
||||
|
||||
$ret = array_pop(self::$errorStack);
|
||||
|
||||
if (count(self::$errorStack) === 0) {
|
||||
// Disable our error logging and restore the previous state
|
||||
libxml_use_internal_errors(self::$xmlErrorState);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format an error as a string.
|
||||
*
|
||||
* This function formats the given LibXMLError object as a string.
|
||||
*
|
||||
* @param LibXMLError $error The LibXMLError which should be formatted.
|
||||
* @return string A string representing the given LibXMLError.
|
||||
*/
|
||||
public static function formatError($error)
|
||||
{
|
||||
assert($error instanceof LibXMLError);
|
||||
return 'level=' . $error->level
|
||||
. ',code=' . $error->code
|
||||
. ',line=' . $error->line
|
||||
. ',col=' . $error->column
|
||||
. ',msg=' . trim($error->message);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format a list of errors as a string.
|
||||
*
|
||||
* This fucntion takes an array of LibXMLError objects and creates a string with all the errors.
|
||||
* Each error will be separated by a newline, and the string will end with a newline-character.
|
||||
*
|
||||
* @param array $errors An array of errors.
|
||||
* @return string A string representing the errors. An empty string will be returned if there were no
|
||||
* errors in the array.
|
||||
*/
|
||||
public static function formatErrors($errors)
|
||||
{
|
||||
assert(is_array($errors));
|
||||
|
||||
$ret = '';
|
||||
foreach ($errors as $error) {
|
||||
$ret .= self::formatError($error) . "\n";
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* This file will help doing XPath queries in SAML 2 XML documents.
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
|
||||
namespace SAML\XML;
|
||||
|
||||
use Exception;
|
||||
use SimpleXMLElement;
|
||||
|
||||
class Parser
|
||||
{
|
||||
/** @var SimpleXMLElement */
|
||||
public $simplexml;
|
||||
|
||||
/**
|
||||
* @param string $xml
|
||||
*/
|
||||
public function __construct($xml)
|
||||
{
|
||||
$this->simplexml = new SimpleXMLElement($xml);
|
||||
$this->simplexml->registerXPathNamespace('saml2', 'urn:oasis:names:tc:SAML:2.0:assertion');
|
||||
$this->simplexml->registerXPathNamespace('saml2meta', 'urn:oasis:names:tc:SAML:2.0:metadata');
|
||||
$this->simplexml->registerXPathNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param SimpleXMLElement $element
|
||||
* @return Parser
|
||||
* @psalm-return \SimpleSAML\XML\Parser
|
||||
*/
|
||||
public static function fromSimpleXMLElement(SimpleXMLElement $element)
|
||||
{
|
||||
// Traverse all existing namespaces in element
|
||||
$namespaces = $element->getNamespaces();
|
||||
foreach ($namespaces as $prefix => $ns) {
|
||||
$element[(($prefix === '') ? 'xmlns' : 'xmlns:' . $prefix)] = $ns;
|
||||
}
|
||||
|
||||
/* Create a new parser with the xml document where the namespace definitions
|
||||
* are added.
|
||||
*/
|
||||
$xml = $element->asXML();
|
||||
if ($xml === false) {
|
||||
throw new Exception('Error converting SimpleXMLElement to well-formed XML string.');
|
||||
}
|
||||
return new Parser($xml);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $xpath
|
||||
* @param string $defvalue
|
||||
* @throws Exception
|
||||
* @return string
|
||||
*/
|
||||
public function getValueDefault($xpath, $defvalue)
|
||||
{
|
||||
try {
|
||||
/** @var string */
|
||||
return $this->getValue($xpath, true);
|
||||
} catch (Exception $e) {
|
||||
return $defvalue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $xpath
|
||||
* @param bool $required
|
||||
* @throws Exception
|
||||
* @return string|null
|
||||
*/
|
||||
public function getValue($xpath, $required = false)
|
||||
{
|
||||
$result = $this->simplexml->xpath($xpath);
|
||||
if (!is_array($result) || empty($result)) {
|
||||
if ($required) {
|
||||
throw new Exception(
|
||||
'Could not get value from XML document using the following XPath expression: ' . $xpath
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return (string) $result[0];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array $xpath
|
||||
* @param bool $required
|
||||
* @throws Exception
|
||||
* @return string|null
|
||||
*/
|
||||
public function getValueAlternatives(array $xpath, $required = false)
|
||||
{
|
||||
foreach ($xpath as $x) {
|
||||
$seek = $this->getValue($x);
|
||||
if ($seek) {
|
||||
return $seek;
|
||||
}
|
||||
}
|
||||
if ($required) {
|
||||
throw new Exception(
|
||||
'Could not get value from XML document using multiple alternative XPath expressions.'
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The Shibboleth 1.3 Authentication Request. Not part of SAML 1.1,
|
||||
* but an extension using query paramters no XML.
|
||||
*
|
||||
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
|
||||
* @package SimpleSAMLphp
|
||||
* @deprecated This class will be removed in a future release
|
||||
*/
|
||||
|
||||
namespace SAML\XML\Shib13;
|
||||
|
||||
use SAML\Metadata\MetaDataStorageHandler;
|
||||
|
||||
class AuthnRequest
|
||||
{
|
||||
/** @var string|null */
|
||||
private $issuer = null;
|
||||
|
||||
/** @var string|null */
|
||||
private $relayState = null;
|
||||
|
||||
|
||||
/**
|
||||
* @param string|null $relayState
|
||||
* @return void
|
||||
*/
|
||||
public function setRelayState($relayState)
|
||||
{
|
||||
$this->relayState = $relayState;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRelayState()
|
||||
{
|
||||
return $this->relayState;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|null $issuer
|
||||
* @return void
|
||||
*/
|
||||
public function setIssuer($issuer)
|
||||
{
|
||||
$this->issuer = $issuer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getIssuer()
|
||||
{
|
||||
return $this->issuer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $destination
|
||||
* @param string $shire
|
||||
* @return string
|
||||
*/
|
||||
public function createRedirect($destination, $shire)
|
||||
{
|
||||
$metadata = MetaDataStorageHandler::getMetadataHandler();
|
||||
$idpmetadata = $metadata->getMetaDataConfig($destination, 'shib13-idp-remote');
|
||||
|
||||
$desturl = $idpmetadata->getDefaultEndpoint(
|
||||
'SingleSignOnService',
|
||||
['urn:mace:shibboleth:1.0:profiles:AuthnRequest']
|
||||
);
|
||||
$desturl = $desturl['Location'];
|
||||
|
||||
$target = $this->getRelayState();
|
||||
$issuer = $this->getIssuer();
|
||||
assert($issuer !== null);
|
||||
|
||||
$url = $desturl . '?' .
|
||||
'providerId=' . urlencode($issuer) .
|
||||
'&shire=' . urlencode($shire) .
|
||||
(isset($target) ? '&target=' . urlencode($target) : '');
|
||||
return $url;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue