diff --git a/src/mibew/libs/classes/Mibew/AccessControl/Check/CheckResolver.php b/src/mibew/libs/classes/Mibew/AccessControl/Check/CheckResolver.php new file mode 100644 index 00000000..3d0c6000 --- /dev/null +++ b/src/mibew/libs/classes/Mibew/AccessControl/Check/CheckResolver.php @@ -0,0 +1,95 @@ +attributes->get('_access_check'); + if (!$access_check) { + // By default we do not need to restrict access + return function () { + return true; + }; + } + + // Check if specified access check is something that can be called + // directly + if (strpos($access_check, ':') === false) { + if (method_exists($access_check, '__invoke')) { + return new $access_check(); + } elseif (function_exists($access_check)) { + return $access_check; + } else { + throw new \InvalidArgumentException(sprintf( + 'Unable to find access check "%s".', + $access_check + )); + } + } + + // Build callable for specified access check + $callable = $this->createCheck($access_check); + + if (!is_callable($callable)) { + throw new \InvalidArgumentException(sprintf( + 'Access check "%s" for URI "%s" is not callable.', + $access_check, + $request->getPathInfo() + )); + } + + return $callable; + } + + /** + * Builds access check callable by its full name. + * + * @param string $access_check Full access check name in "::" + * format. + * @return callable Access check callable + * @throws \InvalidArgumentException + */ + protected function createCheck($access_check) + { + if (strpos($access_check, '::') === false) { + throw new \InvalidArgumentException(sprintf( + 'Unable to find access check "%s".', + $access_check + )); + } + + list($class, $method) = explode('::', $access_check, 2); + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); + } + + return array(new $class(), $method); + } +} diff --git a/src/mibew/libs/classes/Mibew/Application.php b/src/mibew/libs/classes/Mibew/Application.php index 8166dea2..5e0f79ac 100644 --- a/src/mibew/libs/classes/Mibew/Application.php +++ b/src/mibew/libs/classes/Mibew/Application.php @@ -17,12 +17,15 @@ namespace Mibew; +use Mibew\EventDispatcher; use Mibew\Routing\Router; use Mibew\Routing\RouteCollectionLoader; use Mibew\Routing\Exception\AccessDeniedException; use Mibew\Controller\ControllerResolver; +use Mibew\AccessControl\Check\CheckResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Exception\MethodNotAllowedException; @@ -56,6 +59,7 @@ class Application $this->fileLocator = new FileLocator(array(MIBEW_FS_ROOT)); $this->router = new Router(new RouteCollectionLoader($this->fileLocator)); $this->controllerResolver = new ControllerResolver($this->router); + $this->accessCheckResolver = new CheckResolver(); } /** @@ -72,17 +76,22 @@ class Application $this->router->setContext($context); try { - // Try to match route + // Try to match a route and add extra data to the request. $parameters = $this->router->matchRequest($request); $request->attributes->add($parameters); + $request->attributes->set('_operator', $this->extractOperator($request)); - // Get controller + // Check if the user can access the page + $access_check = $this->accessCheckResolver->getCheck($request); + if (!call_user_func($access_check, $request)) { + throw new AccessDeniedException(); + } + + // Get controller and perform its action to get a response. $controller = $this->controllerResolver->getController($request); - - // Execute the controller's action and get response. $response = call_user_func($controller, $request); } catch (AccessDeniedException $e) { - return new Response('Forbidden', 403); + return $this->buildAccessDeniedResponse($request); } catch (ResourceNotFoundException $e) { return new Response('Not Found', 404); } catch (MethodNotAllowedException $e) { @@ -99,4 +108,83 @@ class Application return new Response((string)$response); } } + + /** + * Extracts operator's data from the passed in request object. + * + * @param Request $request A request to extract operator from. + * @return array|bool Associative array with operator's data or boolean + * false if there is no operator related with the request. + * + * @todo Remove this method when Object Oriented wrapper for an operator + * will be created. + */ + protected function extractOperator(Request $request) + { + // Try to get operator from session. + if (isset($_SESSION[SESSION_PREFIX . "operator"])) { + return $_SESSION[SESSION_PREFIX . "operator"]; + } + + // Check if operator had used "remember me" feature. + if ($request->cookies->has(REMEMBER_OPERATOR_COOKIE_NAME)) { + $cookie_value = $request->cookies->get(REMEMBER_OPERATOR_COOKIE_NAME); + list($login, $pwd) = preg_split('/\x0/', base64_decode($cookie_value), 2); + $op = operator_by_login($login); + $can_login = $op + && isset($pwd) + && isset($op['vcpassword']) + && calculate_password_hash($op['vclogin'], $op['vcpassword']) == $pwd + && !operator_is_disabled($op); + if ($can_login) { + $_SESSION[SESSION_PREFIX . "operator"] = $op; + + return $op; + } + } + + // Operator's data cannot be extracted from the request. + return false; + } + + /** + * Builds response for pages with denied access + * + * Triggers "accessDenied' event to provide an ability for plugins to set custom response. + * an associative array with folloing keys is passed to event listeners: + * - 'request': {@link Symfony\Component\HttpFoundation\Request} object. + * + * An event listener can attach custom response to the arguments array + * (using "response" key) to send it to the client. + * + * @param Request $request Incoming request + * @return Response + */ + protected function buildAccessDeniedResponse(Request $request) + { + // Trigger fail + $args = array( + 'request' => $request, + 'response' => false, + ); + $dispatcher = EventDispatcher::getInstance(); + $dispatcher->triggerEvent('accessDenied', $args); + + if ($args['response'] && ($args['response'] instanceof Response)) { + // If one of event listeners returned the response object send it + // to the client. + return $args['response']; + } + + if ($request->attributes->get('_operator')) { + // If the operator already logged in, display 403 page. + return new Response('Forbidden', 403); + } + + // Operator is not logged in. Redirect him to the login page. + $_SESSION['backpath'] = $request->getUri(); + $response = new RedirectResponse($request->getUriForPath('/operator/login.php')); + + return $response; + } }