diff options
-rw-r--r-- | application/front/ShaarliMiddleware.php | 4 | ||||
-rw-r--r-- | application/front/controller/visitor/LoginController.php | 127 | ||||
-rw-r--r-- | application/front/exceptions/CantLoginException.php | 10 | ||||
-rw-r--r-- | application/security/SessionManager.php | 30 | ||||
-rw-r--r-- | index.php | 84 | ||||
-rw-r--r-- | tests/front/ShaarliMiddlewareTest.php | 7 | ||||
-rw-r--r-- | tests/front/controller/visitor/FrontControllerMockHelper.php | 1 | ||||
-rw-r--r-- | tests/front/controller/visitor/LoginControllerTest.php | 278 |
8 files changed, 442 insertions, 99 deletions
diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index 595182ac..e9f5552d 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php | |||
@@ -62,7 +62,9 @@ class ShaarliMiddleware | |||
62 | 62 | ||
63 | return $response->write($this->container->pageBuilder->render('error')); | 63 | return $response->write($this->container->pageBuilder->render('error')); |
64 | } catch (UnauthorizedException $e) { | 64 | } catch (UnauthorizedException $e) { |
65 | return $response->withRedirect($this->container->basePath . '/login'); | 65 | $returnUrl = urlencode($this->container->environment['REQUEST_URI']); |
66 | |||
67 | return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl); | ||
66 | } catch (\Throwable $e) { | 68 | } catch (\Throwable $e) { |
67 | // Unknown error encountered | 69 | // Unknown error encountered |
68 | $this->container->pageBuilder->reset(); | 70 | $this->container->pageBuilder->reset(); |
diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php index a257766f..c40b8cc4 100644 --- a/application/front/controller/visitor/LoginController.php +++ b/application/front/controller/visitor/LoginController.php | |||
@@ -4,8 +4,12 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Visitor; | 5 | namespace Shaarli\Front\Controller\Visitor; |
6 | 6 | ||
7 | use Shaarli\Front\Exception\CantLoginException; | ||
7 | use Shaarli\Front\Exception\LoginBannedException; | 8 | use Shaarli\Front\Exception\LoginBannedException; |
9 | use Shaarli\Front\Exception\WrongTokenException; | ||
8 | use Shaarli\Render\TemplatePage; | 10 | use Shaarli\Render\TemplatePage; |
11 | use Shaarli\Security\CookieManager; | ||
12 | use Shaarli\Security\SessionManager; | ||
9 | use Slim\Http\Request; | 13 | use Slim\Http\Request; |
10 | use Slim\Http\Response; | 14 | use Slim\Http\Response; |
11 | 15 | ||
@@ -19,29 +23,132 @@ use Slim\Http\Response; | |||
19 | */ | 23 | */ |
20 | class LoginController extends ShaarliVisitorController | 24 | class LoginController extends ShaarliVisitorController |
21 | { | 25 | { |
26 | /** | ||
27 | * GET /login - Display the login page. | ||
28 | */ | ||
22 | public function index(Request $request, Response $response): Response | 29 | public function index(Request $request, Response $response): Response |
23 | { | 30 | { |
24 | if ($this->container->loginManager->isLoggedIn() | 31 | try { |
25 | || $this->container->conf->get('security.open_shaarli', false) | 32 | $this->checkLoginState(); |
26 | ) { | 33 | } catch (CantLoginException $e) { |
27 | return $this->redirect($response, '/'); | 34 | return $this->redirect($response, '/'); |
28 | } | 35 | } |
29 | 36 | ||
30 | $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams()); | 37 | if ($request->getParam('login') !== null) { |
31 | if ($userCanLogin !== true) { | 38 | $this->assignView('username', escape($request->getParam('login'))); |
32 | throw new LoginBannedException(); | ||
33 | } | 39 | } |
34 | 40 | ||
35 | if ($request->getParam('username') !== null) { | 41 | $returnUrl = $request->getParam('returnurl') ?? $this->container->environment['HTTP_REFERER'] ?? null; |
36 | $this->assignView('username', escape($request->getParam('username'))); | ||
37 | } | ||
38 | 42 | ||
39 | $this | 43 | $this |
40 | ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER'))) | 44 | ->assignView('returnurl', escape($returnUrl)) |
41 | ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) | 45 | ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true)) |
42 | ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) | 46 | ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli')) |
43 | ; | 47 | ; |
44 | 48 | ||
45 | return $response->write($this->render(TemplatePage::LOGIN)); | 49 | return $response->write($this->render(TemplatePage::LOGIN)); |
46 | } | 50 | } |
51 | |||
52 | /** | ||
53 | * POST /login - Process login | ||
54 | */ | ||
55 | public function login(Request $request, Response $response): Response | ||
56 | { | ||
57 | if (!$this->container->sessionManager->checkToken($request->getParam('token'))) { | ||
58 | throw new WrongTokenException(); | ||
59 | } | ||
60 | |||
61 | try { | ||
62 | $this->checkLoginState(); | ||
63 | } catch (CantLoginException $e) { | ||
64 | return $this->redirect($response, '/'); | ||
65 | } | ||
66 | |||
67 | if (!$this->container->loginManager->checkCredentials( | ||
68 | $this->container->environment['REMOTE_ADDR'], | ||
69 | client_ip_id($this->container->environment), | ||
70 | $request->getParam('login'), | ||
71 | $request->getParam('password') | ||
72 | ) | ||
73 | ) { | ||
74 | $this->container->loginManager->handleFailedLogin($this->container->environment); | ||
75 | |||
76 | $this->container->sessionManager->setSessionParameter( | ||
77 | SessionManager::KEY_ERROR_MESSAGES, | ||
78 | [t('Wrong login/password.')] | ||
79 | ); | ||
80 | |||
81 | // Call controller directly instead of unnecessary redirection | ||
82 | return $this->index($request, $response); | ||
83 | } | ||
84 | |||
85 | $this->container->loginManager->handleSuccessfulLogin($this->container->environment); | ||
86 | |||
87 | $cookiePath = $this->container->basePath . '/'; | ||
88 | $expirationTime = $this->saveLongLastingSession($request, $cookiePath); | ||
89 | $this->renewUserSession($cookiePath, $expirationTime); | ||
90 | |||
91 | // Force referer from given return URL | ||
92 | $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl'); | ||
93 | |||
94 | return $this->redirectFromReferer($request, $response, ['login']); | ||
95 | } | ||
96 | |||
97 | /** | ||
98 | * Make sure that the user is allowed to login and/or displaying the login page: | ||
99 | * - not already logged in | ||
100 | * - not open shaarli | ||
101 | * - not banned | ||
102 | */ | ||
103 | protected function checkLoginState(): bool | ||
104 | { | ||
105 | if ($this->container->loginManager->isLoggedIn() | ||
106 | || $this->container->conf->get('security.open_shaarli', false) | ||
107 | ) { | ||
108 | throw new CantLoginException(); | ||
109 | } | ||
110 | |||
111 | if (true !== $this->container->loginManager->canLogin($this->container->environment)) { | ||
112 | throw new LoginBannedException(); | ||
113 | } | ||
114 | |||
115 | return true; | ||
116 | } | ||
117 | |||
118 | /** | ||
119 | * @return int Session duration in seconds | ||
120 | */ | ||
121 | protected function saveLongLastingSession(Request $request, string $cookiePath): int | ||
122 | { | ||
123 | if (empty($request->getParam('longlastingsession'))) { | ||
124 | // Standard session expiration (=when browser closes) | ||
125 | $expirationTime = 0; | ||
126 | } else { | ||
127 | // Keep the session cookie even after the browser closes | ||
128 | $this->container->sessionManager->setStaySignedIn(true); | ||
129 | $expirationTime = $this->container->sessionManager->extendSession(); | ||
130 | } | ||
131 | |||
132 | $this->container->cookieManager->setCookieParameter( | ||
133 | CookieManager::STAY_SIGNED_IN, | ||
134 | $this->container->loginManager->getStaySignedInToken(), | ||
135 | $expirationTime, | ||
136 | $cookiePath | ||
137 | ); | ||
138 | |||
139 | return $expirationTime; | ||
140 | } | ||
141 | |||
142 | protected function renewUserSession(string $cookiePath, int $expirationTime): void | ||
143 | { | ||
144 | // Send cookie with the new expiration date to the browser | ||
145 | $this->container->sessionManager->destroy(); | ||
146 | $this->container->sessionManager->cookieParameters( | ||
147 | $expirationTime, | ||
148 | $cookiePath, | ||
149 | $this->container->environment['SERVER_NAME'] | ||
150 | ); | ||
151 | $this->container->sessionManager->start(); | ||
152 | $this->container->sessionManager->regenerateId(true); | ||
153 | } | ||
47 | } | 154 | } |
diff --git a/application/front/exceptions/CantLoginException.php b/application/front/exceptions/CantLoginException.php new file mode 100644 index 00000000..cd16635d --- /dev/null +++ b/application/front/exceptions/CantLoginException.php | |||
@@ -0,0 +1,10 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Exception; | ||
6 | |||
7 | class CantLoginException extends \Exception | ||
8 | { | ||
9 | |||
10 | } | ||
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 82771c24..46219a3d 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php | |||
@@ -259,4 +259,34 @@ class SessionManager | |||
259 | { | 259 | { |
260 | return $this->savePath; | 260 | return $this->savePath; |
261 | } | 261 | } |
262 | |||
263 | /* | ||
264 | * Next public functions wrapping native PHP session API. | ||
265 | */ | ||
266 | |||
267 | public function destroy(): bool | ||
268 | { | ||
269 | $this->session = []; | ||
270 | |||
271 | return session_destroy(); | ||
272 | } | ||
273 | |||
274 | public function start(): bool | ||
275 | { | ||
276 | if (session_status() === PHP_SESSION_ACTIVE) { | ||
277 | $this->destroy(); | ||
278 | } | ||
279 | |||
280 | return session_start(); | ||
281 | } | ||
282 | |||
283 | public function cookieParameters(int $lifeTime, string $path, string $domain): bool | ||
284 | { | ||
285 | return session_set_cookie_params($lifeTime, $path, $domain); | ||
286 | } | ||
287 | |||
288 | public function regenerateId(bool $deleteOldSession = false): bool | ||
289 | { | ||
290 | return session_regenerate_id($deleteOldSession); | ||
291 | } | ||
262 | } | 292 | } |
@@ -160,89 +160,6 @@ header("Pragma: no-cache"); | |||
160 | $loginManager->checkLoginState($clientIpId); | 160 | $loginManager->checkLoginState($clientIpId); |
161 | 161 | ||
162 | // ------------------------------------------------------------------------------------------ | 162 | // ------------------------------------------------------------------------------------------ |
163 | // Process login form: Check if login/password is correct. | ||
164 | if (isset($_POST['login'])) { | ||
165 | if (! $loginManager->canLogin($_SERVER)) { | ||
166 | die(t('I said: NO. You are banned for the moment. Go away.')); | ||
167 | } | ||
168 | if (isset($_POST['password']) | ||
169 | && $sessionManager->checkToken($_POST['token']) | ||
170 | && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password']) | ||
171 | ) { | ||
172 | $loginManager->handleSuccessfulLogin($_SERVER); | ||
173 | |||
174 | $cookiedir = ''; | ||
175 | if (dirname($_SERVER['SCRIPT_NAME']) != '/') { | ||
176 | // Note: Never forget the trailing slash on the cookie path! | ||
177 | $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/'; | ||
178 | } | ||
179 | |||
180 | if (!empty($_POST['longlastingsession'])) { | ||
181 | // Keep the session cookie even after the browser closes | ||
182 | $sessionManager->setStaySignedIn(true); | ||
183 | $expirationTime = $sessionManager->extendSession(); | ||
184 | |||
185 | setcookie( | ||
186 | CookieManager::STAY_SIGNED_IN, | ||
187 | $loginManager->getStaySignedInToken(), | ||
188 | $expirationTime, | ||
189 | WEB_PATH | ||
190 | ); | ||
191 | } else { | ||
192 | // Standard session expiration (=when browser closes) | ||
193 | $expirationTime = 0; | ||
194 | } | ||
195 | |||
196 | // Send cookie with the new expiration date to the browser | ||
197 | session_destroy(); | ||
198 | session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']); | ||
199 | session_start(); | ||
200 | session_regenerate_id(true); | ||
201 | |||
202 | // Optional redirect after login: | ||
203 | if (isset($_GET['post'])) { | ||
204 | $uri = './?post='. urlencode($_GET['post']); | ||
205 | foreach (array('description', 'source', 'title', 'tags') as $param) { | ||
206 | if (!empty($_GET[$param])) { | ||
207 | $uri .= '&'.$param.'='.urlencode($_GET[$param]); | ||
208 | } | ||
209 | } | ||
210 | header('Location: '. $uri); | ||
211 | exit; | ||
212 | } | ||
213 | |||
214 | if (isset($_GET['edit_link'])) { | ||
215 | header('Location: ./?edit_link='. escape($_GET['edit_link'])); | ||
216 | exit; | ||
217 | } | ||
218 | |||
219 | if (isset($_POST['returnurl'])) { | ||
220 | // Prevent loops over login screen. | ||
221 | if (strpos($_POST['returnurl'], '/login') === false) { | ||
222 | header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST'])); | ||
223 | exit; | ||
224 | } | ||
225 | } | ||
226 | header('Location: ./?'); | ||
227 | exit; | ||
228 | } else { | ||
229 | $loginManager->handleFailedLogin($_SERVER); | ||
230 | $redir = '?username='. urlencode($_POST['login']); | ||
231 | if (isset($_GET['post'])) { | ||
232 | $redir .= '&post=' . urlencode($_GET['post']); | ||
233 | foreach (array('description', 'source', 'title', 'tags') as $param) { | ||
234 | if (!empty($_GET[$param])) { | ||
235 | $redir .= '&' . $param . '=' . urlencode($_GET[$param]); | ||
236 | } | ||
237 | } | ||
238 | } | ||
239 | // Redirect to login screen. | ||
240 | echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'./login'.$redir.'\';</script>'; | ||
241 | exit; | ||
242 | } | ||
243 | } | ||
244 | |||
245 | // ------------------------------------------------------------------------------------------ | ||
246 | // Token management for XSRF protection | 163 | // Token management for XSRF protection |
247 | // Token should be used in any form which acts on data (create,update,delete,import...). | 164 | // Token should be used in any form which acts on data (create,update,delete,import...). |
248 | if (!isset($_SESSION['tokens'])) { | 165 | if (!isset($_SESSION['tokens'])) { |
@@ -283,6 +200,7 @@ $app->group('', function () { | |||
283 | $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index'); | 200 | $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index'); |
284 | $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink'); | 201 | $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink'); |
285 | $this->get('/login', '\Shaarli\Front\Controller\Visitor\LoginController:index')->setName('login'); | 202 | $this->get('/login', '\Shaarli\Front\Controller\Visitor\LoginController:index')->setName('login'); |
203 | $this->post('/login', '\Shaarli\Front\Controller\Visitor\LoginController:login')->setName('processLogin'); | ||
286 | $this->get('/picture-wall', '\Shaarli\Front\Controller\Visitor\PictureWallController:index'); | 204 | $this->get('/picture-wall', '\Shaarli\Front\Controller\Visitor\PictureWallController:index'); |
287 | $this->get('/tags/cloud', '\Shaarli\Front\Controller\Visitor\TagCloudController:cloud'); | 205 | $this->get('/tags/cloud', '\Shaarli\Front\Controller\Visitor\TagCloudController:cloud'); |
288 | $this->get('/tags/list', '\Shaarli\Front\Controller\Visitor\TagCloudController:list'); | 206 | $this->get('/tags/list', '\Shaarli\Front\Controller\Visitor\TagCloudController:list'); |
diff --git a/tests/front/ShaarliMiddlewareTest.php b/tests/front/ShaarliMiddlewareTest.php index 20090d8b..09bebd04 100644 --- a/tests/front/ShaarliMiddlewareTest.php +++ b/tests/front/ShaarliMiddlewareTest.php | |||
@@ -38,6 +38,8 @@ class ShaarliMiddlewareTest extends TestCase | |||
38 | 38 | ||
39 | $this->container->loginManager = $this->createMock(LoginManager::class); | 39 | $this->container->loginManager = $this->createMock(LoginManager::class); |
40 | 40 | ||
41 | $this->container->environment = ['REQUEST_URI' => 'http://shaarli/subfolder/path']; | ||
42 | |||
41 | $this->middleware = new ShaarliMiddleware($this->container); | 43 | $this->middleware = new ShaarliMiddleware($this->container); |
42 | } | 44 | } |
43 | 45 | ||
@@ -127,7 +129,10 @@ class ShaarliMiddlewareTest extends TestCase | |||
127 | $result = $this->middleware->__invoke($request, $response, $controller); | 129 | $result = $this->middleware->__invoke($request, $response, $controller); |
128 | 130 | ||
129 | static::assertSame(302, $result->getStatusCode()); | 131 | static::assertSame(302, $result->getStatusCode()); |
130 | static::assertSame('/subfolder/login', $result->getHeader('location')[0]); | 132 | static::assertSame( |
133 | '/subfolder/login?returnurl=' . urlencode('http://shaarli/subfolder/path'), | ||
134 | $result->getHeader('location')[0] | ||
135 | ); | ||
131 | } | 136 | } |
132 | 137 | ||
133 | /** | 138 | /** |
diff --git a/tests/front/controller/visitor/FrontControllerMockHelper.php b/tests/front/controller/visitor/FrontControllerMockHelper.php index 7f560662..e0bd4ecf 100644 --- a/tests/front/controller/visitor/FrontControllerMockHelper.php +++ b/tests/front/controller/visitor/FrontControllerMockHelper.php | |||
@@ -80,6 +80,7 @@ trait FrontControllerMockHelper | |||
80 | 'SERVER_NAME' => 'shaarli', | 80 | 'SERVER_NAME' => 'shaarli', |
81 | 'SERVER_PORT' => '80', | 81 | 'SERVER_PORT' => '80', |
82 | 'REQUEST_URI' => '/daily-rss', | 82 | 'REQUEST_URI' => '/daily-rss', |
83 | 'REMOTE_ADDR' => '1.2.3.4', | ||
83 | ]; | 84 | ]; |
84 | 85 | ||
85 | $this->container->basePath = '/subfolder'; | 86 | $this->container->basePath = '/subfolder'; |
diff --git a/tests/front/controller/visitor/LoginControllerTest.php b/tests/front/controller/visitor/LoginControllerTest.php index e57f44b9..0a21f938 100644 --- a/tests/front/controller/visitor/LoginControllerTest.php +++ b/tests/front/controller/visitor/LoginControllerTest.php | |||
@@ -7,6 +7,10 @@ namespace Shaarli\Front\Controller\Visitor; | |||
7 | use PHPUnit\Framework\TestCase; | 7 | use PHPUnit\Framework\TestCase; |
8 | use Shaarli\Config\ConfigManager; | 8 | use Shaarli\Config\ConfigManager; |
9 | use Shaarli\Front\Exception\LoginBannedException; | 9 | use Shaarli\Front\Exception\LoginBannedException; |
10 | use Shaarli\Front\Exception\WrongTokenException; | ||
11 | use Shaarli\Render\TemplatePage; | ||
12 | use Shaarli\Security\CookieManager; | ||
13 | use Shaarli\Security\SessionManager; | ||
10 | use Slim\Http\Request; | 14 | use Slim\Http\Request; |
11 | use Slim\Http\Response; | 15 | use Slim\Http\Response; |
12 | 16 | ||
@@ -21,13 +25,25 @@ class LoginControllerTest extends TestCase | |||
21 | { | 25 | { |
22 | $this->createContainer(); | 26 | $this->createContainer(); |
23 | 27 | ||
28 | $this->container->cookieManager = $this->createMock(CookieManager::class); | ||
29 | $this->container->sessionManager->method('checkToken')->willReturn(true); | ||
30 | |||
24 | $this->controller = new LoginController($this->container); | 31 | $this->controller = new LoginController($this->container); |
25 | } | 32 | } |
26 | 33 | ||
34 | /** | ||
35 | * Test displaying login form with valid parameters. | ||
36 | */ | ||
27 | public function testValidControllerInvoke(): void | 37 | public function testValidControllerInvoke(): void |
28 | { | 38 | { |
29 | $request = $this->createMock(Request::class); | 39 | $request = $this->createMock(Request::class); |
30 | $request->expects(static::once())->method('getServerParam')->willReturn('> referer'); | 40 | $request |
41 | ->expects(static::atLeastOnce()) | ||
42 | ->method('getParam') | ||
43 | ->willReturnCallback(function (string $key) { | ||
44 | return 'returnurl' === $key ? '> referer' : null; | ||
45 | }) | ||
46 | ; | ||
31 | $response = new Response(); | 47 | $response = new Response(); |
32 | 48 | ||
33 | $assignedVariables = []; | 49 | $assignedVariables = []; |
@@ -46,18 +62,32 @@ class LoginControllerTest extends TestCase | |||
46 | 62 | ||
47 | static::assertInstanceOf(Response::class, $result); | 63 | static::assertInstanceOf(Response::class, $result); |
48 | static::assertSame(200, $result->getStatusCode()); | 64 | static::assertSame(200, $result->getStatusCode()); |
49 | static::assertSame('loginform', (string) $result->getBody()); | 65 | static::assertSame(TemplatePage::LOGIN, (string) $result->getBody()); |
50 | 66 | ||
51 | static::assertSame('> referer', $assignedVariables['returnurl']); | 67 | static::assertSame('> referer', $assignedVariables['returnurl']); |
52 | static::assertSame(true, $assignedVariables['remember_user_default']); | 68 | static::assertSame(true, $assignedVariables['remember_user_default']); |
53 | static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']); | 69 | static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']); |
54 | } | 70 | } |
55 | 71 | ||
72 | /** | ||
73 | * Test displaying login form with username defined in the request. | ||
74 | */ | ||
56 | public function testValidControllerInvokeWithUserName(): void | 75 | public function testValidControllerInvokeWithUserName(): void |
57 | { | 76 | { |
77 | $this->container->environment = ['HTTP_REFERER' => '> referer']; | ||
78 | |||
58 | $request = $this->createMock(Request::class); | 79 | $request = $this->createMock(Request::class); |
59 | $request->expects(static::once())->method('getServerParam')->willReturn('> referer'); | 80 | $request |
60 | $request->expects(static::exactly(2))->method('getParam')->willReturn('myUser>'); | 81 | ->expects(static::atLeastOnce()) |
82 | ->method('getParam') | ||
83 | ->willReturnCallback(function (string $key, $default) { | ||
84 | if ('login' === $key) { | ||
85 | return 'myUser>'; | ||
86 | } | ||
87 | |||
88 | return $default; | ||
89 | }) | ||
90 | ; | ||
61 | $response = new Response(); | 91 | $response = new Response(); |
62 | 92 | ||
63 | $assignedVariables = []; | 93 | $assignedVariables = []; |
@@ -84,6 +114,9 @@ class LoginControllerTest extends TestCase | |||
84 | static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']); | 114 | static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']); |
85 | } | 115 | } |
86 | 116 | ||
117 | /** | ||
118 | * Test displaying login page while being logged in. | ||
119 | */ | ||
87 | public function testLoginControllerWhileLoggedIn(): void | 120 | public function testLoginControllerWhileLoggedIn(): void |
88 | { | 121 | { |
89 | $request = $this->createMock(Request::class); | 122 | $request = $this->createMock(Request::class); |
@@ -98,6 +131,9 @@ class LoginControllerTest extends TestCase | |||
98 | static::assertSame(['/subfolder/'], $result->getHeader('Location')); | 131 | static::assertSame(['/subfolder/'], $result->getHeader('Location')); |
99 | } | 132 | } |
100 | 133 | ||
134 | /** | ||
135 | * Test displaying login page with open shaarli configured: redirect to homepage. | ||
136 | */ | ||
101 | public function testLoginControllerOpenShaarli(): void | 137 | public function testLoginControllerOpenShaarli(): void |
102 | { | 138 | { |
103 | $request = $this->createMock(Request::class); | 139 | $request = $this->createMock(Request::class); |
@@ -119,6 +155,9 @@ class LoginControllerTest extends TestCase | |||
119 | static::assertSame(['/subfolder/'], $result->getHeader('Location')); | 155 | static::assertSame(['/subfolder/'], $result->getHeader('Location')); |
120 | } | 156 | } |
121 | 157 | ||
158 | /** | ||
159 | * Test displaying login page while being banned. | ||
160 | */ | ||
122 | public function testLoginControllerWhileBanned(): void | 161 | public function testLoginControllerWhileBanned(): void |
123 | { | 162 | { |
124 | $request = $this->createMock(Request::class); | 163 | $request = $this->createMock(Request::class); |
@@ -131,4 +170,235 @@ class LoginControllerTest extends TestCase | |||
131 | 170 | ||
132 | $this->controller->index($request, $response); | 171 | $this->controller->index($request, $response); |
133 | } | 172 | } |
173 | |||
174 | /** | ||
175 | * Test processing login with valid parameters. | ||
176 | */ | ||
177 | public function testProcessLoginWithValidParameters(): void | ||
178 | { | ||
179 | $parameters = [ | ||
180 | 'login' => 'bob', | ||
181 | 'password' => 'pass', | ||
182 | ]; | ||
183 | $request = $this->createMock(Request::class); | ||
184 | $request | ||
185 | ->expects(static::atLeastOnce()) | ||
186 | ->method('getParam') | ||
187 | ->willReturnCallback(function (string $key) use ($parameters) { | ||
188 | return $parameters[$key] ?? null; | ||
189 | }) | ||
190 | ; | ||
191 | $response = new Response(); | ||
192 | |||
193 | $this->container->loginManager->method('canLogin')->willReturn(true); | ||
194 | $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin'); | ||
195 | $this->container->loginManager | ||
196 | ->expects(static::once()) | ||
197 | ->method('checkCredentials') | ||
198 | ->with('1.2.3.4', '1.2.3.4', 'bob', 'pass') | ||
199 | ->willReturn(true) | ||
200 | ; | ||
201 | $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8))); | ||
202 | |||
203 | $this->container->sessionManager->expects(static::never())->method('extendSession'); | ||
204 | $this->container->sessionManager->expects(static::once())->method('destroy'); | ||
205 | $this->container->sessionManager | ||
206 | ->expects(static::once()) | ||
207 | ->method('cookieParameters') | ||
208 | ->with(0, '/subfolder/', 'shaarli') | ||
209 | ; | ||
210 | $this->container->sessionManager->expects(static::once())->method('start'); | ||
211 | $this->container->sessionManager->expects(static::once())->method('regenerateId')->with(true); | ||
212 | |||
213 | $result = $this->controller->login($request, $response); | ||
214 | |||
215 | static::assertSame(302, $result->getStatusCode()); | ||
216 | static::assertSame('/subfolder/', $result->getHeader('location')[0]); | ||
217 | } | ||
218 | |||
219 | /** | ||
220 | * Test processing login with return URL. | ||
221 | */ | ||
222 | public function testProcessLoginWithReturnUrl(): void | ||
223 | { | ||
224 | $parameters = [ | ||
225 | 'returnurl' => 'http://shaarli/subfolder/admin/shaare', | ||
226 | ]; | ||
227 | $request = $this->createMock(Request::class); | ||
228 | $request | ||
229 | ->expects(static::atLeastOnce()) | ||
230 | ->method('getParam') | ||
231 | ->willReturnCallback(function (string $key) use ($parameters) { | ||
232 | return $parameters[$key] ?? null; | ||
233 | }) | ||
234 | ; | ||
235 | $response = new Response(); | ||
236 | |||
237 | $this->container->loginManager->method('canLogin')->willReturn(true); | ||
238 | $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin'); | ||
239 | $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(true); | ||
240 | $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8))); | ||
241 | |||
242 | $result = $this->controller->login($request, $response); | ||
243 | |||
244 | static::assertSame(302, $result->getStatusCode()); | ||
245 | static::assertSame('/subfolder/admin/shaare', $result->getHeader('location')[0]); | ||
246 | } | ||
247 | |||
248 | /** | ||
249 | * Test processing login with remember me session enabled. | ||
250 | */ | ||
251 | public function testProcessLoginLongLastingSession(): void | ||
252 | { | ||
253 | $parameters = [ | ||
254 | 'longlastingsession' => true, | ||
255 | ]; | ||
256 | $request = $this->createMock(Request::class); | ||
257 | $request | ||
258 | ->expects(static::atLeastOnce()) | ||
259 | ->method('getParam') | ||
260 | ->willReturnCallback(function (string $key) use ($parameters) { | ||
261 | return $parameters[$key] ?? null; | ||
262 | }) | ||
263 | ; | ||
264 | $response = new Response(); | ||
265 | |||
266 | $this->container->loginManager->method('canLogin')->willReturn(true); | ||
267 | $this->container->loginManager->expects(static::once())->method('handleSuccessfulLogin'); | ||
268 | $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(true); | ||
269 | $this->container->loginManager->method('getStaySignedInToken')->willReturn(bin2hex(random_bytes(8))); | ||
270 | |||
271 | $this->container->sessionManager->expects(static::once())->method('destroy'); | ||
272 | $this->container->sessionManager | ||
273 | ->expects(static::once()) | ||
274 | ->method('cookieParameters') | ||
275 | ->with(42, '/subfolder/', 'shaarli') | ||
276 | ; | ||
277 | $this->container->sessionManager->expects(static::once())->method('start'); | ||
278 | $this->container->sessionManager->expects(static::once())->method('regenerateId')->with(true); | ||
279 | $this->container->sessionManager->expects(static::once())->method('extendSession')->willReturn(42); | ||
280 | |||
281 | $this->container->cookieManager = $this->createMock(CookieManager::class); | ||
282 | $this->container->cookieManager | ||
283 | ->expects(static::once()) | ||
284 | ->method('setCookieParameter') | ||
285 | ->willReturnCallback(function (string $name): CookieManager { | ||
286 | static::assertSame(CookieManager::STAY_SIGNED_IN, $name); | ||
287 | |||
288 | return $this->container->cookieManager; | ||
289 | }) | ||
290 | ; | ||
291 | |||
292 | $result = $this->controller->login($request, $response); | ||
293 | |||
294 | static::assertSame(302, $result->getStatusCode()); | ||
295 | static::assertSame('/subfolder/', $result->getHeader('location')[0]); | ||
296 | } | ||
297 | |||
298 | /** | ||
299 | * Test processing login with invalid credentials | ||
300 | */ | ||
301 | public function testProcessLoginWrongCredentials(): void | ||
302 | { | ||
303 | $parameters = [ | ||
304 | 'returnurl' => 'http://shaarli/subfolder/admin/shaare', | ||
305 | ]; | ||
306 | $request = $this->createMock(Request::class); | ||
307 | $request | ||
308 | ->expects(static::atLeastOnce()) | ||
309 | ->method('getParam') | ||
310 | ->willReturnCallback(function (string $key) use ($parameters) { | ||
311 | return $parameters[$key] ?? null; | ||
312 | }) | ||
313 | ; | ||
314 | $response = new Response(); | ||
315 | |||
316 | $this->container->loginManager->method('canLogin')->willReturn(true); | ||
317 | $this->container->loginManager->expects(static::once())->method('handleFailedLogin'); | ||
318 | $this->container->loginManager->expects(static::once())->method('checkCredentials')->willReturn(false); | ||
319 | |||
320 | $this->container->sessionManager | ||
321 | ->expects(static::once()) | ||
322 | ->method('setSessionParameter') | ||
323 | ->with(SessionManager::KEY_ERROR_MESSAGES, ['Wrong login/password.']) | ||
324 | ; | ||
325 | |||
326 | $result = $this->controller->login($request, $response); | ||
327 | |||
328 | static::assertSame(200, $result->getStatusCode()); | ||
329 | static::assertSame(TemplatePage::LOGIN, (string) $result->getBody()); | ||
330 | } | ||
331 | |||
332 | /** | ||
333 | * Test processing login with wrong token | ||
334 | */ | ||
335 | public function testProcessLoginWrongToken(): void | ||
336 | { | ||
337 | $request = $this->createMock(Request::class); | ||
338 | $response = new Response(); | ||
339 | |||
340 | $this->container->sessionManager = $this->createMock(SessionManager::class); | ||
341 | $this->container->sessionManager->method('checkToken')->willReturn(false); | ||
342 | |||
343 | $this->expectException(WrongTokenException::class); | ||
344 | |||
345 | $this->controller->login($request, $response); | ||
346 | } | ||
347 | |||
348 | /** | ||
349 | * Test processing login with wrong token | ||
350 | */ | ||
351 | public function testProcessLoginAlreadyLoggedIn(): void | ||
352 | { | ||
353 | $request = $this->createMock(Request::class); | ||
354 | $response = new Response(); | ||
355 | |||
356 | $this->container->loginManager->method('isLoggedIn')->willReturn(true); | ||
357 | $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin'); | ||
358 | $this->container->loginManager->expects(static::never())->method('handleFailedLogin'); | ||
359 | |||
360 | $result = $this->controller->login($request, $response); | ||
361 | |||
362 | static::assertSame(302, $result->getStatusCode()); | ||
363 | static::assertSame('/subfolder/', $result->getHeader('location')[0]); | ||
364 | } | ||
365 | |||
366 | /** | ||
367 | * Test processing login with wrong token | ||
368 | */ | ||
369 | public function testProcessLoginInOpenShaarli(): void | ||
370 | { | ||
371 | $request = $this->createMock(Request::class); | ||
372 | $response = new Response(); | ||
373 | |||
374 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
375 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $value) { | ||
376 | return 'security.open_shaarli' === $key ? true : $value; | ||
377 | }); | ||
378 | |||
379 | $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin'); | ||
380 | $this->container->loginManager->expects(static::never())->method('handleFailedLogin'); | ||
381 | |||
382 | $result = $this->controller->login($request, $response); | ||
383 | |||
384 | static::assertSame(302, $result->getStatusCode()); | ||
385 | static::assertSame('/subfolder/', $result->getHeader('location')[0]); | ||
386 | } | ||
387 | |||
388 | /** | ||
389 | * Test processing login while being banned | ||
390 | */ | ||
391 | public function testProcessLoginWhileBanned(): void | ||
392 | { | ||
393 | $request = $this->createMock(Request::class); | ||
394 | $response = new Response(); | ||
395 | |||
396 | $this->container->loginManager->method('canLogin')->willReturn(false); | ||
397 | $this->container->loginManager->expects(static::never())->method('handleSuccessfulLogin'); | ||
398 | $this->container->loginManager->expects(static::never())->method('handleFailedLogin'); | ||
399 | |||
400 | $this->expectException(LoginBannedException::class); | ||
401 | |||
402 | $this->controller->login($request, $response); | ||
403 | } | ||
134 | } | 404 | } |