. * * @package phpservermon * @author Pepijn Over * @copyright Copyright (c) 2008-2017 Pepijn Over * @license http://www.gnu.org/licenses/gpl.txt GNU GPL v3 * @version Release: @package_version@ * @link http://www.phpservermonitor.org/ * @since phpservermon 3.0 **/ namespace psm; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; /** * The router class opens the controller and runs the module. * * It uses a so-called $mod param to determine which controller to load. * The $mod of run() has 2 components, separated by an underscore. The first part determines * the module to load, the second on the controller. It uses the keys defined in * the module config. If the controller part is absent, it will always try to load * the controller with the same name as the module. */ class Router { /** * Service container * @var \Symfony\Component\DependencyInjection\ContainerBuilder $container */ protected $container; public function __construct() { $this->container = $this->buildServiceContainer(); $mods = $this->container->getParameter('modules'); foreach ($mods as $mod) { $mod_loader = $this->container->get($mod); $mod_loader->load($this->container); } } /** * Run a module. * * The $mod param is in the format $module_$controller. * If the "_$controller" part is omitted, it will attempt to load * the controller with the same name as the module. * * @param string $mod * @throws \InvalidArgumentException * @throws \LogicException */ public function run($mod) { if (strpos($mod, '_') !== false) { list($mod, $controller) = explode('_', $mod); } else { $controller = $mod; } $this->buildTwigEnvironment(); $controller = $this->getController($mod, $controller); $action = null; try { $this->validateRequest($controller); } catch (\InvalidArgumentException $ex) { switch ($ex->getMessage()) { case 'login_required': $controller = $this->getController('user', 'login'); break; case 'invalid_csrf_token': case 'invalid_user_level': default: $controller = $this->getController('error'); $action = '401'; break; } } $response = $controller->run($action); if (!($response instanceof Response)) { throw new \LogicException('Controller did not return a Response object.'); } $response->send(); } /** * Get an instance of the requested controller. * @param string $module_id * @param string $controller_id if NULL, default controller will be used * @return \psm\Module\ControllerInterface * @throws \InvalidArgumentException */ public function getController($module_id, $controller_id = null) { if ($controller_id === null) { // by default, we use the controller with the same id as the module. $controller_id = $module_id; } $module = $this->container->get('module.' . $module_id); $controllers = $module->getControllers(); if (!isset($controllers[$controller_id]) || !class_exists($controllers[$controller_id])) { throw new \InvalidArgumentException('Controller "' . $controller_id . '" is not registered or does not exist.'); } $controller = new $controllers[$controller_id]( $this->container->get('db'), $this->container->get('twig') ); if (!$controller instanceof \psm\Module\ControllerInterface) { throw new \Exception('Controller does not implement ControllerInterface'); } $controller->setContainer($this->container); return $controller; } /** * Get service from container * @param string $id * @return mixed FALSE on failure, service otherwise * @throws \InvalidArgumentException */ public function getService($id) { return $this->container->get($id); } /** * Validate requets before heading to a controller * @param \psm\Module\ControllerInterface $controller * @throws \InvalidArgumentException */ protected function validateRequest(\psm\Module\ControllerInterface $controller) { $request = Request::createFromGlobals(); if ($request->getMethod() == 'POST') { // require CSRF token for all POST calls $session = $this->container->get('user')->getSession(); $token_in = $request->request->get('csrf', ''); $csrf_key = $controller->getCSRFKey(); if (empty($csrf_key)) { if (!hash_equals($session->get('csrf_token'), $token_in)) { throw new \InvalidArgumentException('invalid_csrf_token'); } } else { if ( !hash_equals( hash_hmac('sha256', $csrf_key, $session->get('csrf_token2')), $token_in ) ) { throw new \InvalidArgumentException('invalid_csrf_token'); } } } // get min required level for this controller and make sure the user matches $min_lvl = $controller->getMinUserLevelRequired(); if ($min_lvl < PSM_USER_ANONYMOUS) { // if user is not logged in, load login module if (!$this->container->get('user')->isUserLoggedIn()) { throw new \InvalidArgumentException('login_required'); } elseif ($this->container->get('user')->getUserLevel() > $min_lvl) { throw new \InvalidArgumentException('invalid_user_level'); } } } /** * Build a new service container * @return \Symfony\Component\DependencyInjection\ContainerBuilder * @throws \InvalidArgumentException */ protected function buildServiceContainer() { $builder = new ContainerBuilder(); $loader = new XmlFileLoader($builder, new FileLocator(PSM_PATH_CONFIG)); $loader->load('services.xml'); return $builder; } /** * Prepare twig environment * @return \Twig_Environment */ protected function buildTwigEnvironment() { $twig = $this->container->get('twig'); $session = $this->container->get('user')->getSession(); if (!$session->has('csrf_token')) { $session->set('csrf_token', bin2hex(random_bytes(32))); } if (!$session->has('csrf_token2')) { $session->set('csrf_token2', random_bytes(32)); } $twig->addFunction( new \Twig_SimpleFunction( 'csrf_token', function ($lock_to = null) use ($session) { if (empty($lock_to)) { return $session->get('csrf_token'); } return hash_hmac('sha256', $lock_to, $session->get('csrf_token2')); } ) ); $twig->addGlobal('direction_current', psm_get_lang('locale_dir')); $twig->addGlobal('language_current', psm_get_lang('locale_tag')); $twig->addGlobal('language', psm_get_lang('locale')[1]); return $twig; } }