From 2ae9f3b17e8478d3d8ad4909fe8ca4bcbff4fe87 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jaime=20Pe=CC=81rez=20Crespo?= <jaime.perez@uninett.no>
Date: Mon, 8 Oct 2018 14:14:04 +0200
Subject: [PATCH] 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.
---
 lib/SimpleSAML/Module.php                   | 227 ++++++++++++++++++++
 lib/SimpleSAML/ModuleControllerResolver.php | 154 +++++++++++++
 lib/SimpleSAML/Router.php                   |  81 +++++++
 lib/SimpleSAML/XHTML/Template.php           |  68 +++++-
 modules/core/lib/Controller.php             | 139 ++++++++++++
 modules/core/routes.yaml                    |   7 +
 modules/core/templates/login.twig           |   6 +-
 modules/core/www/frontpage_auth.php         |   2 +-
 www/index.php                               |   2 +-
 www/module.php                              | 167 +-------------
 10 files changed, 675 insertions(+), 178 deletions(-)
 create mode 100644 lib/SimpleSAML/ModuleControllerResolver.php
 create mode 100644 lib/SimpleSAML/Router.php
 create mode 100644 modules/core/lib/Controller.php
 create mode 100644 modules/core/routes.yaml

diff --git a/lib/SimpleSAML/Module.php b/lib/SimpleSAML/Module.php
index e3e018c7b..744dea83f 100644
--- a/lib/SimpleSAML/Module.php
+++ b/lib/SimpleSAML/Module.php
@@ -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);
+    }
 }
diff --git a/lib/SimpleSAML/ModuleControllerResolver.php b/lib/SimpleSAML/ModuleControllerResolver.php
new file mode 100644
index 000000000..97ebc9798
--- /dev/null
+++ b/lib/SimpleSAML/ModuleControllerResolver.php
@@ -0,0 +1,154 @@
+<?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;
+    }
+}
diff --git a/lib/SimpleSAML/Router.php b/lib/SimpleSAML/Router.php
new file mode 100644
index 000000000..fa9346896
--- /dev/null
+++ b/lib/SimpleSAML/Router.php
@@ -0,0 +1,81 @@
+<?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();
+    }
+}
diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php
index b20cac571..9d9fe35eb 100644
--- a/lib/SimpleSAML/XHTML/Template.php
+++ b/lib/SimpleSAML/XHTML/Template.php
@@ -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();
     }
 
 
diff --git a/modules/core/lib/Controller.php b/modules/core/lib/Controller.php
new file mode 100644
index 000000000..afdd0b5d3
--- /dev/null
+++ b/modules/core/lib/Controller.php
@@ -0,0 +1,139 @@
+<?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;
+    }
+}
diff --git a/modules/core/routes.yaml b/modules/core/routes.yaml
new file mode 100644
index 000000000..2eea68a23
--- /dev/null
+++ b/modules/core/routes.yaml
@@ -0,0 +1,7 @@
+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' }
+
diff --git a/modules/core/templates/login.twig b/modules/core/templates/login.twig
index e1b342cc0..03297b971 100644
--- a/modules/core/templates/login.twig
+++ b/modules/core/templates/login.twig
@@ -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>
diff --git a/modules/core/www/frontpage_auth.php b/modules/core/www/frontpage_auth.php
index 804f3cd6f..0b36c9231 100644
--- a/modules/core/www/frontpage_auth.php
+++ b/modules/core/www/frontpage_auth.php
@@ -19,7 +19,7 @@ $links_auth = array();
 $links_federation = array();
 
 $links_auth[] = array(
-    'href' => 'login.php',
+    'href' => 'authenticate.php',
     'text' => '{core:frontpage:authtest}',
 );
 
diff --git a/www/index.php b/www/index.php
index b5b456c7a..d080b8af9 100644
--- a/www/index.php
+++ b/www/index.php
@@ -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'));
diff --git a/www/module.php b/www/module.php
index d8a72013d..83ee0ff38 100644
--- a/www/module.php
+++ b/www/module.php
@@ -1,172 +1,9 @@
 <?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();
-- 
GitLab