Skip to content
Snippets Groups Projects
Unverified Commit 2ae9f3b1 authored by Jaime Pérez Crespo's avatar Jaime Pérez Crespo
Browse files

Initial version of routing, requests and responses.

This introduces the following:

- The use of Request objects to handle request data to controllers.
- The use of Response objects to model responses that should be sent to the browser.
- The use of "controllers" that are responsible for translating a request into a response.
- The possibility to define your own URLs on each module by specifying them, together with their controllers, in a "routes.yaml" file in the root of a module.
- The new UI is completely separated from the old, so "usenewui" must be set to "true" in the configuration.
- Twigified templates are not used unless we're using the new UI, or the twig template is part of a theme.
parent 92528ec4
No related branches found
No related tags found
No related merge requests found
......@@ -2,6 +2,12 @@
namespace SimpleSAML;
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;
/**
* Helper class for accessing information about modules.
*
......@@ -12,6 +18,44 @@ namespace SimpleSAML;
*/
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.
*
......@@ -63,6 +107,172 @@ class Module
}
/**
* 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->getPathInfo() === '/') {
throw new Error\NotFound('No PATH_INFO to module.php');
}
$url = $request->getPathInfo();
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();
if ($config->getBoolean('usenewui', false) === true) {
$router = new Router($module);
try {
return $router->process();
} catch (\Symfony\Component\Config\Exception\FileLocatorFileNotFoundException $e) {
// no routes configured for this module, fall back to the old system
} catch (\Symfony\Component\HttpKernel\Exception\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 (preg_match('#\.php$#D', $path)) {
// 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 (stripos($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';
}
}
$response = new BinaryFileResponse($path);
$response->setCache(['public' => true, 'max_age' => 86400]);
$response->setExpires(new \DateTime(gmdate('D, j M Y H:i:s \G\M\T', time() + 10 * 60)));
$response->setLastModified(new \DateTime(gmdate('D, j M Y H:i:s \G\M\T', filemtime($path))));
$response->headers->set('Content-Type', $contentType);
$response->headers->set('Content-Length', sprintf('%u', filesize($path))); // force file size to an unsigned
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_INLINE);
$response->prepare($request);
return $response;
}
private static function isModuleEnabledWithConf($module, $mod_config)
{
if (isset(self::$module_info[$module]['enabled'])) {
......@@ -310,4 +520,21 @@ class Module
$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->getPathInfo();
$url = str_replace($pathInfo, rtrim($pathInfo, ' /'), $request->getRequestUri());
return new RedirectResponse($url, 308);
}
}
<?php
namespace SimpleSAML;
use SimpleSAML\Error\Exception;
use Symfony\Component\Config\Exception\FileLocatorFileNotFoundException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
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 ModuleControllerResolver extends ControllerResolver implements ArgumentResolverInterface
{
/** @var ArgumentMetadataFactory */
protected $argFactory;
/** @var string */
protected $module;
/** @var array */
protected $params;
/** @var RouteCollection */
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();
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());
return self::createController($this->params['_controller']);
} 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 \SimpleSAML\Error\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() === 'Symfony\Component\HttpFoundation\Request') {
// 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;
}
}
<?php
namespace SimpleSAML;
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
{
protected $arguments;
/** @var RequestContext */
protected $context;
/** @var ModuleControllerResolver */
protected $resolver;
protected $dispatcher;
protected $request;
protected $stack;
/**
* Router constructor.
*
* @param string $module
*/
public function __construct($module)
{
$this->arguments = new ArgumentResolver();
$this->context = new RequestContext();
$this->resolver = new ModuleControllerResolver($module);
$this->dispatcher = new EventDispatcher();
}
/**
* Process a given request.
*
* @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)
{
$this->request = $request;
if ($request === null) {
$this->request = Request::createFromGlobals();
}
$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.
*/
public function send(Response $response)
{
$response->prepare($this->request);
$response->send();
}
}
......@@ -11,8 +11,9 @@ namespace SimpleSAML\XHTML;
use JaimePerez\TwigConfigurableI18n\Twig\Environment as Twig_Environment;
use JaimePerez\TwigConfigurableI18n\Twig\Extensions\Extension\I18n as Twig_Extensions_Extension_I18n;
use Symfony\Component\HttpFoundation\Response;
class Template
class Template extends Response
{
/**
* The data associated with this template, accessible within the template itself.
......@@ -68,6 +69,14 @@ class Template
*/
private $module;
/**
* Whether to use the new user interface or not.
*
* @var string
*/
private $useNewUI;
/**
* A template controller, if any.
*
......@@ -84,10 +93,10 @@ class Template
* Whether we are using a non-default theme or not.
*
* If we are using a theme, this variable holds an array with two keys: "module" and "name", those being the name
* of the module and the name of the theme, respectively. If we are using the default theme, the variable defaults
* to false.
* 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 bool|array
* @var array
*/
private $theme;
......@@ -117,6 +126,9 @@ class Template
$this->translator = new \SimpleSAML\Locale\Translate($configuration, $defaultDictionary);
$this->localization = new \SimpleSAML\Locale\Localization($configuration);
// check if we are supposed to use the new UI
$this->useNewUI = $this->configuration->getBoolean('usenewui', false);
// check if we need to attach a theme controller
$controller = $this->configuration->getString('theme.controller', false);
if ($controller && class_exists($controller) &&
......@@ -126,6 +138,7 @@ class Template
}
$this->twig = $this->setupTwig();
parent::__construct();
}
......@@ -148,7 +161,11 @@ class Template
if ($tplpos) {
$templateName = substr($templateName, 0, $tplpos);
}
return $templateName.'.twig';
if ($this->useNewUI || $this->theme['module'] !== null) {
return $templateName.'.twig';
}
return $templateName;
}
......@@ -407,20 +424,51 @@ class Template
/**
* Show the template to the user.
* 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.
*/
public function show()
protected function getContents()
{
if ($this->twig !== false) {
$this->twigDefaultContext();
if ($this->controller) {
$this->controller->display($this->data);
}
echo $this->twig->render($this->twig_template, $this->data);
$content = $this->twig->render($this->twig_template, $this->data);
} else {
$filename = $this->findTemplatePath($this->template);
require($filename);
$content = require($this->findTemplatePath($this->template));
}
return $content;
}
/**
* 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.
*
* @deprecated Do not use this method, use send() instead.
*/
public function show()
{
$this->send();
echo $this->getContents();
}
......
<?php
namespace SimpleSAML\Module\core;
use SimpleSAML\Error\Exception;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller class for the core module.
*
* This class serves the different views available in the module.
*
* @package SimpleSAML\Module\core
*/
class Controller
{
/** @var \SimpleSAML\Configuration */
protected $config;
/** @var array */
protected $sources;
public function __construct()
{
$this->config = \SimpleSAML\Configuration::getInstance();
$this->sources = \SimpleSAML\Configuration::getOptionalConfig('authsources.php')->toArray();
}
/**
* Perform a login operation.
*
* This controller will either start a login operation (if that was requested, or if only one authentication
* source is available), or show a template allowing the user to choose which auth source to use.
*
* @param Request $request The request that lead to this login operation.
* @param string|null $as The name of the authentication source to use, if any. Optional.
*
* @return \SimpleSAML\XHTML\Template|RedirectResponse An HTML template or a redirect response.
*
* @throws Exception
* @throws \SimpleSAML\Error\CriticalConfigurationError
*/
public function login(Request $request, $as = null)
{
//delete admin
if (isset($this->sources['admin'])) {
unset($this->sources['admin']);
}
if (count($this->sources) === 1) { // we only have one source available
$as = key($this->sources);
}
if ($as === null) { // no authentication source specified
$t = new \SimpleSAML\XHTML\Template($this->config, 'core:login.twig');
$t->data['loginurl'] = \SimpleSAML\Utils\Auth::getAdminLoginURL();
$t->data['sources'] = $this->sources;
return $t;
}
// auth source defined, check if valid
if (!array_key_exists($as, $this->sources)) {
throw new Exception('Invalid authentication source');
}
// at this point, we have a valid auth source selected, start auth
$auth = new \SimpleSAML\Auth\Simple($as);
$as = urlencode($as);
if ($request->get('logout', false) !== false) {
$auth->logout($this->config->getBasePath().'logout.php');
}
if ($request->get(\SimpleSAML\Auth\State::EXCEPTION_PARAM, false) !== false) {
// This is just a simple example of an error
$state = \SimpleSAML\Auth\State::loadExceptionState();
assert(array_key_exists(\SimpleSAML\Auth\State::EXCEPTION_DATA, $state));
$e = $state[\SimpleSAML\Auth\State::EXCEPTION_DATA];
throw $e;
}
if ($auth->isAuthenticated()) {
return new RedirectResponse(\SimpleSAML\Module::getModuleURL('core/account/'.$as));
}
// we're not logged in, start auth
$url = \SimpleSAML\Module::getModuleURL('core/login/'.$as);
$params = array(
'ErrorURL' => $url,
'ReturnTo' => $url,
);
$auth->login($params);
}
/**
* Show account information for a given authentication source.
*
* @param string $as The identifier of the authentication source.
*
* @return \SimpleSAML\XHTML\Template|RedirectResponse An HTML template or a redirection if we are not
* authenticated.
*
* @throws Exception An exception in case the auth source specified is invalid.
*/
public function account($as)
{
if (!array_key_exists($as, $this->sources)) {
throw new Exception('Invalid authentication source');
}
$auth = new \SimpleSAML\Auth\Simple($as);
if (!$auth->isAuthenticated()) {
// not authenticated, start auth with specified source
return new RedirectResponse(\SimpleSAML\Module::getModuleURL('core/login/'.urlencode($as)));
}
$attributes = $auth->getAttributes();
$session = \SimpleSAML\Session::getSessionFromRequest();
$t = new \SimpleSAML\XHTML\Template($this->config, 'auth_status.php', 'attributes');
$t->data['header'] = '{status:header_saml20_sp}';
$t->data['attributes'] = $attributes;
$t->data['nameid'] = !is_null($auth->getAuthData('saml:sp:NameID'))
? $auth->getAuthData('saml:sp:NameID')
: false;
$t->data['logouturl'] = \SimpleSAML\Module::getModuleURL('core/logout/'.urlencode($as));
$t->data['remaining'] = $session->getAuthData($as, 'Expire') - time();
$t->setStatusCode(200);
return $t;
}
}
core-login:
path: /login/{as}
defaults: { _controller: '\SimpleSAML\Module\core\Controller::login', as: null }
core-account:
path: /account/{as}
defaults: { _controller: '\SimpleSAML\Module\core\Controller::account' }
......@@ -12,7 +12,11 @@
<div class="pure-menu custom-restricted-width">
<ul class="pure-menu-list auth_methods">
{% for id, config in sources -%}
<li class="pure-menu-item"><a href="?as={{ id|url_encode }}" class="pure-menu-link">{{ config.name|translateFromArray|default(id) }}</a></li>
<li class="pure-menu-item">
<a href="{{ baseurlpath }}/module.php/core/login/{{ id|url_encode }}" class="pure-menu-link">
{{ config.name|translateFromArray|default(id) }}
</a>
</li>
{% endfor -%}
</ul>
</div>
......
......@@ -19,7 +19,7 @@ $links_auth = array();
$links_federation = array();
$links_auth[] = array(
'href' => 'login.php',
'href' => 'authenticate.php',
'text' => '{core:frontpage:authtest}',
);
......
......@@ -5,7 +5,7 @@ require_once('_include.php');
$config = \SimpleSAML\Configuration::getInstance();
if ($config->getBoolean('usenewui', false)) {
\SimpleSAML\Utils\HTTP::redirectTrustedURL(SimpleSAML\Module::getModuleURL('core/login.php'));
\SimpleSAML\Utils\HTTP::redirectTrustedURL(SimpleSAML\Module::getModuleURL('core/login'));
}
\SimpleSAML\Utils\HTTP::redirectTrustedURL(SimpleSAML\Module::getModuleURL('core/frontpage_welcome.php'));
<?php
/**
* Handler for module requests.
*
* This web page receives requests for web-pages hosted by modules, and directs them to
* the RequestHandler in the module.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
* the process() handler in the Module class.
*/
require_once('_include.php');
// index pages - file names to attempt when accessing directories
$indexFiles = array('index.php', 'index.html', 'index.htm', 'index.txt');
// MIME types - key is file extension, value is MIME type
$mimeTypes = array(
'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',
);
if (empty($_SERVER['PATH_INFO'])) {
throw new \SimpleSAML\Error\NotFound('No PATH_INFO to module.php');
}
$url = $_SERVER['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 \SimpleSAML\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 (!SimpleSAML\Module::isModuleEnabled($module)) {
throw new \SimpleSAML\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 SimpleSAML\Error\BadRequest('Requested URL contained a backslash.');
} elseif (strpos($url, './') !== false) {
throw new \SimpleSAML\Error\BadRequest('Requested URL contained \'./\'.');
}
$moduleDir = SimpleSAML\Module::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;
$_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 ($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 \SimpleSAML\Error\NotFound('Directory listing not available.');
}
if (!file_exists($path)) {
// file not found
SimpleSAML\Logger::info('Could not find file \''.$path.'\'.');
throw new \SimpleSAML\Error\NotFound('The URL wasn\'t found in the module.');
}
if (preg_match('#\.php$#D', $path)) {
// 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 (stripos($_SERVER['SCRIPT_NAME'], $script) === false) {
$_SERVER['SCRIPT_NAME'] .= '/'.$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, $mimeTypes)) {
$contentType = $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
SimpleSAML\Logger::warning('Unable to determine mime content type of file: '.$path);
$contentType = 'application/octet-stream';
}
}
$contentLength = sprintf('%u', filesize($path)); // force filesize to an unsigned number
header('Content-Type: '.$contentType);
header('Content-Length: '.$contentLength);
header('Cache-Control: public,max-age=86400');
header('Expires: '.gmdate('D, j M Y H:i:s \G\M\T', time() + 10 * 60));
header('Last-Modified: '.gmdate('D, j M Y H:i:s \G\M\T', filemtime($path)));
readfile($path);
\SimpleSAML\Module::process()->send();
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment