diff --git a/src/mibew/install/index.php b/src/mibew/install/index.php index 3e628456..f9b1b42d 100644 --- a/src/mibew/install/index.php +++ b/src/mibew/install/index.php @@ -372,7 +372,7 @@ function check_status() if (!check_admin($link)) { $page['nextstep'] = getlocal("installed.login_link"); $page['nextnotice'] = getlocal2("installed.notice", array(MIBEW_WEB_ROOT . "/install/")); - $page['nextstepurl'] = MIBEW_WEB_ROOT . "/operator/login.php?login=admin"; + $page['nextstepurl'] = MIBEW_WEB_ROOT . "/operator/login?login=admin"; } $page['show_small_login'] = true; diff --git a/src/mibew/libs/classes/Mibew/Application.php b/src/mibew/libs/classes/Mibew/Application.php index bf3b4215..143c119f 100644 --- a/src/mibew/libs/classes/Mibew/Application.php +++ b/src/mibew/libs/classes/Mibew/Application.php @@ -18,8 +18,10 @@ namespace Mibew; use Mibew\AccessControl\Check\CheckResolver; +use Mibew\Authentication\AuthenticationManager; use Mibew\Controller\ControllerResolver; use Mibew\EventDispatcher; +use Mibew\Http\CookieFactory; use Mibew\Http\Exception\AccessDeniedException as AccessDeniedHttpException; use Mibew\Http\Exception\HttpException; use Mibew\Http\Exception\MethodNotAllowedException as MethodNotAllowedHttpException; @@ -60,6 +62,11 @@ class Application */ protected $accessCheckResolver = null; + /** + * @var AuthenticationManager|null + */ + protected $authenticationManager = null; + /** * Class constructor. */ @@ -69,6 +76,7 @@ class Application $this->router = new Router(new RouteCollectionLoader($this->fileLocator)); $this->controllerResolver = new ControllerResolver($this->router); $this->accessCheckResolver = new CheckResolver(); + $this->authenticationManager = new AuthenticationManager(); } /** @@ -84,13 +92,20 @@ class Application $context->fromRequest($request); $this->router->setContext($context); + // Actualize cookie factory in the authentication manager. + $cookie_factory = CookieFactory::fromRequest($request); + $this->authenticationManager->setCookieFactory($cookie_factory); + try { // Try to match a route, check if the client can access it and add // extra data to the request. try { $parameters = $this->router->matchRequest($request); $request->attributes->add($parameters); - $request->attributes->set('_operator', $this->extractOperator($request)); + $request->attributes->set( + '_operator', + $this->authenticationManager->extractOperator($request) + ); // Check if the user can access the page $access_check = $this->accessCheckResolver->getCheck($request); @@ -112,63 +127,30 @@ class Application $controller = $this->controllerResolver->getController($request); $response = call_user_func($controller, $request); } catch (AccessDeniedHttpException $e) { - return $this->buildAccessDeniedResponse($request); + $response = $this->buildAccessDeniedResponse($request); } catch (HttpException $e) { // Build response based on status code which is stored in exception // instance. $http_status = $e->getStatusCode(); $content = Response::$statusTexts[$http_status]; - return new Response($content, $http_status); + $response = new Response($content, $http_status); } catch (\Exception $e) { - return new Response('Internal Server Error', 500); + $response = new Response('Internal Server Error', 500); } - if ($response instanceof Response) { - return $response; - } else { + if (!($response instanceof Response)) { // Convert all content returned by a controller's action to Response // instance. - 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"]; + $response = new Response((string)$response); } - // 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; + // Get modified operator from the request and attach authentication info + // to the response to distinguish him in the next requests. + $operator = $request->attributes->get('_operator'); + $this->authenticationManager->attachOperator($response, $operator); - return $op; - } - } - - // Operator's data cannot be extracted from the request. - return false; + return $response; } /** @@ -207,7 +189,7 @@ class Application // Operator is not logged in. Redirect him to the login page. $_SESSION['backpath'] = $request->getUri(); - $response = new RedirectResponse($request->getUriForPath('/operator/login.php')); + $response = new RedirectResponse($this->router->generate('login')); return $response; } diff --git a/src/mibew/libs/classes/Mibew/Authentication/AuthenticationManager.php b/src/mibew/libs/classes/Mibew/Authentication/AuthenticationManager.php new file mode 100644 index 00000000..d79c0af1 --- /dev/null +++ b/src/mibew/libs/classes/Mibew/Authentication/AuthenticationManager.php @@ -0,0 +1,160 @@ +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; + } + + /** + * Attaches operator's token to the response, thus is can be used to extract + * operator in the next request. + * + * @param Response $response The response object which will be sent to the + * client. + * @param array $operator Operator's data. + * @return Response Updated response. + */ + public function attachOperator(Response $response, $operator) + { + if ($operator) { + // Calculate password hashes for operator in the request and for the + // operator in session. If the hashes are different then operator's + // password or login was changed. + $password_hash = calculate_password_hash( + $operator['vclogin'], + $operator['vcpassword'] + ); + + if (isset($_SESSION[SESSION_PREFIX . 'operator'])) { + $old_operator = $_SESSION[SESSION_PREFIX . 'operator']; + $old_password_hash = calculate_password_hash( + $old_operator['vclogin'], + $old_operator['vcpassword'] + ); + $credentials_changed = $password_hash != $old_password_hash; + } else { + $credentials_changed = false; + } + + // Check if we need to remember the operator + if (isset($operator['remember_me'])) { + $remember = $operator['remember_me']; + unset($operator['remember_me']); + } else { + $remember = false; + } + + // Update operator in the session + $_SESSION[SESSION_PREFIX . 'operator'] = $operator; + + // Set or update remember me cookie if needed + if ($remember || $credentials_changed) { + $remember_cookie = $this->getCookieFactory()->createCookie( + REMEMBER_OPERATOR_COOKIE_NAME, + base64_encode($operator['vclogin'] . "\x0" . $password_hash), + time() + 60 * 60 * 24 * 1000, + true + ); + + $response->headers->setCookie($remember_cookie); + } + } else { + // Clean up session data + unset($_SESSION[SESSION_PREFIX . 'operator']); + unset($_SESSION['backpath']); + + // Clear remember cookie + $cookie_factory = $this->getCookieFactory(); + $response->headers->clearCookie( + REMEMBER_OPERATOR_COOKIE_NAME, + $cookie_factory->getPath(), + $cookie_factory->getDomain() + ); + } + } + + /** + * Updates instance of cookie factory related with the manager. + * + * @param CookieFactory $factory An instance of CookieFactory. + */ + public function setCookieFactory(CookieFactory $factory) + { + $this->cookieFactory = $factory; + } + + /** + * Returns an instance of cookie factory related with the manager. + * + * @return CookieFactory + */ + public function getCookieFactory() + { + if (is_null($this->cookieFactory)) { + $this->cookieFactory = new CookieFactory(); + } + + return $this->cookieFactory; + } +} diff --git a/src/mibew/libs/classes/Mibew/Controller/LoginController.php b/src/mibew/libs/classes/Mibew/Controller/LoginController.php new file mode 100644 index 00000000..0c46fe42 --- /dev/null +++ b/src/mibew/libs/classes/Mibew/Controller/LoginController.php @@ -0,0 +1,132 @@ +attributes->get('_operator')) { + // Redirect the operator to home page. + // TODO: Use a route for URI generation. + return $this->redirect($request->getUriForPath('/operator')); + } + + $page = array( + 'formisRemember' => true, + 'version' => MIBEW_VERSION, + // Use errors list stored in the request. We need to do so to have + // an ability to pass the request from the "submitForm" action. + 'errors' => $request->attributes->get('errors', array()), + ); + + // Try to get login from the request. + if ($request->request->has('login')) { + $page['formlogin'] = $request->request->get('login'); + } elseif ($request->query->has('login')) { + $login = $request->query->get('login'); + if (preg_match("/^(\w{1,15})$/", $login)) { + $page['formlogin'] = $login; + } + } + + $page['localeLinks'] = get_locale_links(); + $page['title'] = getlocal('page_login.title'); + $page['headertitle'] = getlocal('app.title'); + $page['show_small_login'] = false; + $page['fixedwrap'] = true; + + return $this->render('login', $page); + } + + /** + * Processes submitting of the form which is generated in + * {@link \Mibew\Controller\LoginController::showFormAction()} method. + * + * Triggers 'operatorLogin' event after operator logged in and pass to it an + * associative array with following items: + * - 'operator': array of the logged in operator info; + * - 'remember': boolean, indicates if system should remember operator. + * + * @param Request $request Incoming request. + * @return string Rendered page content. + */ + public function submitFormAction(Request $request) + { + $login = $request->request->get('login'); + $password = $request->request->get('password'); + $remember = $request->request->get('isRemember') == 'on'; + $errors = array(); + + $operator = operator_by_login($login); + $operator_can_login = $operator + && isset($operator['vcpassword']) + && check_password_hash($operator['vclogin'], $password, $operator['vcpassword']) + && !operator_is_disabled($operator); + + if ($operator_can_login) { + if ($remember) { + $operator['remember_me'] = true; + } + + // Update operator in the request. Doing so we tell the + // Authentication manager that operator should be associated with + // the session. + $request->attributes->set('_operator', $operator); + + // Redirect the current operator to the needed page. + $target = isset($_SESSION['backpath']) + ? $_SESSION['backpath'] + : $request->getUriForPath('/operator'); + + // Trigger login event + $args = array( + 'operator' => $operator, + 'remember' => $remember, + ); + $dispatcher = EventDispatcher::getInstance(); + $dispatcher->triggerEvent('operatorLogin', $args); + + return $this->redirect($target); + } else { + if (operator_is_disabled($operator)) { + $errors[] = getlocal('page_login.operator.disabled'); + } else { + $errors[] = getlocal("page_login.error"); + } + } + + // Rebuild login form + $request->attributes->set('errors', $errors); + + return $this->showFormAction($request); + } +} diff --git a/src/mibew/libs/classes/Mibew/Controller/LogoutController.php b/src/mibew/libs/classes/Mibew/Controller/LogoutController.php new file mode 100644 index 00000000..38cff10c --- /dev/null +++ b/src/mibew/libs/classes/Mibew/Controller/LogoutController.php @@ -0,0 +1,49 @@ +attributes->remove('_operator'); + + // Trigger logout event + $dispatcher = EventDispatcher::getInstance(); + $dispatcher->triggerEvent('operatorLogout'); + + // Redirect the current operator to the login page. + return $this->redirect($this->generateUrl('login')); + } +} diff --git a/src/mibew/libs/classes/Mibew/Controller/Operator/AvatarController.php b/src/mibew/libs/classes/Mibew/Controller/Operator/AvatarController.php index 7737b81a..f002d4ad 100644 --- a/src/mibew/libs/classes/Mibew/Controller/Operator/AvatarController.php +++ b/src/mibew/libs/classes/Mibew/Controller/Operator/AvatarController.php @@ -140,12 +140,10 @@ class AvatarController extends AbstractController // Update path to avatar in the database update_operator_avatar($op['operatorid'], $avatar); - // Operator's data are cached in the session thus we need to update them + // Operator's data are cached in the request thus we need to update them // manually. if ($avatar && $operator['operatorid'] == $op_id) { $operator['vcavatar'] = $avatar; - - $_SESSION[SESSION_PREFIX . 'operator'] = $operator; $request->attributes->set('_operator', $operator); } diff --git a/src/mibew/libs/classes/Mibew/Controller/Operator/PermissionsController.php b/src/mibew/libs/classes/Mibew/Controller/Operator/PermissionsController.php index 9f60fac8..cff93ba6 100644 --- a/src/mibew/libs/classes/Mibew/Controller/Operator/PermissionsController.php +++ b/src/mibew/libs/classes/Mibew/Controller/Operator/PermissionsController.php @@ -113,13 +113,12 @@ class PermissionsController extends AbstractController } } - // Update operator's permissions in the database and in cached session + // Update operator's permissions in the database and in cached request // data if it is needed. update_operator_permissions($op['operatorid'], $new_permissions); if ($operator['operatorid'] == $op_id) { $operator['iperm'] = $new_permissions; - $_SESSION[SESSION_PREFIX . 'operator'] = $operator; $request->attributes->set('_operator', $operator); } diff --git a/src/mibew/libs/classes/Mibew/Controller/Operator/ProfileController.php b/src/mibew/libs/classes/Mibew/Controller/Operator/ProfileController.php index 33f4d767..240df324 100644 --- a/src/mibew/libs/classes/Mibew/Controller/Operator/ProfileController.php +++ b/src/mibew/libs/classes/Mibew/Controller/Operator/ProfileController.php @@ -206,7 +206,7 @@ class ProfileController extends AbstractController // Update existing operator update_operator($op_id, $login, $email, $password, $local_name, $common_name, $code); - // Operator data are cached in the session, thus we need to manually + // Operator data are cached in the request, thus we need to manually // update them. if (!empty($password) && $op_id == $operator['operatorid']) { // Check if the admin has set his password for the first time. @@ -214,7 +214,6 @@ class ProfileController extends AbstractController // Update operator's password. $operator['vcpassword'] = calculate_password_hash($login, $password); - $_SESSION[SESSION_PREFIX . 'operator'] = $operator; $request->attributes->set('_operator', $operator); // Redirect the admin to the home page if needed. diff --git a/src/mibew/libs/operator.php b/src/mibew/libs/operator.php index 21069658..76d0239d 100644 --- a/src/mibew/libs/operator.php +++ b/src/mibew/libs/operator.php @@ -572,7 +572,7 @@ function check_login($redirect = true) // Redirect operator if need if ($redirect) { $_SESSION['backpath'] = $requested; - header("Location: " . MIBEW_WEB_ROOT . "/operator/login.php"); + header("Location: " . MIBEW_WEB_ROOT . "/operator/login"); exit; } else { return null; @@ -603,63 +603,6 @@ function get_logged_in() : false; } -/** - * Log in operator - * - * Triggers 'operatorLogin' event after operator logged in and pass to it an - * associative array with following items: - * - 'operator': array of the logged in operator info; - * - 'remember': boolean, indicates if system should remember operator. - * - * @param array $operator Operators info - * @param boolean $remember Indicates if system should remember operator - * @param boolean $https Indicates if cookie should be flagged as a secure one - */ -function login_operator($operator, $remember, $https = false) -{ - $_SESSION[SESSION_PREFIX . "operator"] = $operator; - if ($remember) { - $password_hash = calculate_password_hash($operator['vclogin'], $operator['vcpassword']); - setcookie( - REMEMBER_OPERATOR_COOKIE_NAME, - base64_encode($operator['vclogin'] . "\x0" . $password_hash), - time() + 60 * 60 * 24 * 1000, - MIBEW_WEB_ROOT . "/", - null, - $https, - true - ); - } elseif (isset($_COOKIE[REMEMBER_OPERATOR_COOKIE_NAME])) { - setcookie(REMEMBER_OPERATOR_COOKIE_NAME, '', time() - 3600, MIBEW_WEB_ROOT . "/"); - } - - // Trigger login event - $args = array( - 'operator' => $operator, - 'remember' => $remember, - ); - $dispatcher = EventDispatcher::getInstance(); - $dispatcher->triggerEvent('operatorLogin', $args); -} - -/** - * Log out current operator - * - * Triggers 'operatorLogout' event after operator logged out. - */ -function logout_operator() -{ - unset($_SESSION[SESSION_PREFIX . "operator"]); - unset($_SESSION['backpath']); - if (isset($_COOKIE[REMEMBER_OPERATOR_COOKIE_NAME])) { - setcookie(REMEMBER_OPERATOR_COOKIE_NAME, '', time() - 3600, MIBEW_WEB_ROOT . "/"); - } - - // Trigger logout event - $dispatcher = EventDispatcher::getInstance(); - $dispatcher->triggerEvent('operatorLogout'); -} - function setup_redirect_links($threadid, $operator, $token) { $result = array(); diff --git a/src/mibew/libs/routing.yml b/src/mibew/libs/routing.yml index aa7a522b..86ddf037 100644 --- a/src/mibew/libs/routing.yml +++ b/src/mibew/libs/routing.yml @@ -216,6 +216,26 @@ invite: _controller: Mibew\Controller\InvitationController::inviteAction _access_check: Mibew\AccessControl\Check\LoggedInCheck +## Log in +login: + path: /operator/login + defaults: + _controller: Mibew\Controller\LoginController::showFormAction + methods: [GET] + +login_submit: + path: /operator/login + defaults: + _controller: Mibew\Controller\LoginController::submitFormAction + methods: [POST] + +## Log out +logout: + path: /operator/logout + defaults: + _controller: Mibew\Controller\LogoutController::logoutAction + _access_check: Mibew\AccessControl\Check\LoggedInCheck + ## Operators operator_add: path: /operator/operator/add diff --git a/src/mibew/operator/login.php b/src/mibew/operator/login.php deleted file mode 100644 index 09ba1944..00000000 --- a/src/mibew/operator/login.php +++ /dev/null @@ -1,71 +0,0 @@ - true, - 'version' => MIBEW_VERSION, - 'errors' => array(), -); - -if (isset($_POST['login']) && isset($_POST['password'])) { - $login = get_param('login'); - $password = get_param('password'); - $remember = isset($_POST['isRemember']) && $_POST['isRemember'] == "on"; - - $operator = operator_by_login($login); - $operator_can_login = $operator - && isset($operator['vcpassword']) - && check_password_hash($operator['vclogin'], $password, $operator['vcpassword']) - && !operator_is_disabled($operator); - - if ($operator_can_login) { - $target = $password == '' - ? MIBEW_WEB_ROOT . "/operator/operator/" . intval($operator['operatorid']) . '/edit' - : (isset($_SESSION['backpath']) ? $_SESSION['backpath'] : MIBEW_WEB_ROOT . "/operator/index.php"); - - login_operator($operator, $remember, is_secure_request()); - header("Location: $target"); - exit; - } else { - if (operator_is_disabled($operator)) { - $page['errors'][] = getlocal('page_login.operator.disabled'); - } else { - $page['errors'][] = getlocal("page_login.error"); - } - $page['formlogin'] = $login; - } -} elseif (isset($_GET['login'])) { - $login = get_get_param('login'); - if (preg_match("/^(\w{1,15})$/", $login)) { - $page['formlogin'] = $login; - } -} - -$page['localeLinks'] = get_locale_links(); -$page['title'] = getlocal("page_login.title"); -$page['headertitle'] = getlocal("app.title"); -$page['show_small_login'] = false; -$page['fixedwrap'] = true; - -$page_style = new PageStyle(PageStyle::getCurrentStyle()); -$page_style->render('login', $page); diff --git a/src/mibew/operator/logout.php b/src/mibew/operator/logout.php deleted file mode 100644 index bb8bf50d..00000000 --- a/src/mibew/operator/logout.php +++ /dev/null @@ -1,22 +0,0 @@ - -