aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/front/ShaarliMiddleware.php4
-rw-r--r--application/front/controller/visitor/LoginController.php127
-rw-r--r--application/front/exceptions/CantLoginException.php10
-rw-r--r--application/security/SessionManager.php30
-rw-r--r--index.php84
-rw-r--r--tests/front/ShaarliMiddlewareTest.php7
-rw-r--r--tests/front/controller/visitor/FrontControllerMockHelper.php1
-rw-r--r--tests/front/controller/visitor/LoginControllerTest.php278
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
5namespace Shaarli\Front\Controller\Visitor; 5namespace Shaarli\Front\Controller\Visitor;
6 6
7use Shaarli\Front\Exception\CantLoginException;
7use Shaarli\Front\Exception\LoginBannedException; 8use Shaarli\Front\Exception\LoginBannedException;
9use Shaarli\Front\Exception\WrongTokenException;
8use Shaarli\Render\TemplatePage; 10use Shaarli\Render\TemplatePage;
11use Shaarli\Security\CookieManager;
12use Shaarli\Security\SessionManager;
9use Slim\Http\Request; 13use Slim\Http\Request;
10use Slim\Http\Response; 14use Slim\Http\Response;
11 15
@@ -19,29 +23,132 @@ use Slim\Http\Response;
19 */ 23 */
20class LoginController extends ShaarliVisitorController 24class 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class 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}
diff --git a/index.php b/index.php
index 4627438e..1a121f37 100644
--- a/index.php
+++ b/index.php
@@ -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.
164if (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...).
248if (!isset($_SESSION['tokens'])) { 165if (!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;
7use PHPUnit\Framework\TestCase; 7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Exception\LoginBannedException; 9use Shaarli\Front\Exception\LoginBannedException;
10use Shaarli\Front\Exception\WrongTokenException;
11use Shaarli\Render\TemplatePage;
12use Shaarli\Security\CookieManager;
13use Shaarli\Security\SessionManager;
10use Slim\Http\Request; 14use Slim\Http\Request;
11use Slim\Http\Response; 15use 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('&gt; referer', $assignedVariables['returnurl']); 67 static::assertSame('&gt; 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}