aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/container/ContainerBuilder.php77
-rw-r--r--application/container/ShaarliContainer.php28
-rw-r--r--application/front/ShaarliMiddleware.php57
-rw-r--r--application/front/controllers/LoginController.php46
-rw-r--r--application/front/controllers/ShaarliController.php31
-rw-r--r--application/front/exceptions/LoginBannedException.php15
-rw-r--r--application/front/exceptions/ShaarliException.php23
-rw-r--r--application/render/PageBuilder.php17
-rw-r--r--application/security/SessionManager.php6
-rw-r--r--assets/default/scss/shaarli.scss13
-rw-r--r--composer.json4
-rw-r--r--index.php66
-rw-r--r--tests/container/ContainerBuilderTest.php49
-rw-r--r--tests/front/ShaarliMiddlewareTest.php70
-rw-r--r--tests/front/controller/LoginControllerTest.php173
-rw-r--r--tpl/default/404.html2
-rw-r--r--tpl/default/error.html22
-rw-r--r--tpl/default/loginform.html60
-rw-r--r--tpl/vintage/error.html25
-rw-r--r--tpl/vintage/loginform.html44
20 files changed, 728 insertions, 100 deletions
diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php
new file mode 100644
index 00000000..ff29825c
--- /dev/null
+++ b/application/container/ContainerBuilder.php
@@ -0,0 +1,77 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Container;
6
7use Shaarli\Bookmark\BookmarkFileService;
8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager;
10use Shaarli\History;
11use Shaarli\Plugin\PluginManager;
12use Shaarli\Render\PageBuilder;
13use Shaarli\Security\LoginManager;
14use Shaarli\Security\SessionManager;
15
16/**
17 * Class ContainerBuilder
18 *
19 * Helper used to build a Slim container instance with Shaarli's object dependencies.
20 * Note that most injected objects MUST be added as closures, to let the container instantiate
21 * only the objects it requires during the execution.
22 *
23 * @package Container
24 */
25class ContainerBuilder
26{
27 /** @var ConfigManager */
28 protected $conf;
29
30 /** @var SessionManager */
31 protected $session;
32
33 /** @var LoginManager */
34 protected $login;
35
36 public function __construct(ConfigManager $conf, SessionManager $session, LoginManager $login)
37 {
38 $this->conf = $conf;
39 $this->session = $session;
40 $this->login = $login;
41 }
42
43 public function build(): ShaarliContainer
44 {
45 $container = new ShaarliContainer();
46 $container['conf'] = $this->conf;
47 $container['sessionManager'] = $this->session;
48 $container['loginManager'] = $this->login;
49 $container['plugins'] = function (ShaarliContainer $container): PluginManager {
50 return new PluginManager($container->conf);
51 };
52
53 $container['history'] = function (ShaarliContainer $container): History {
54 return new History($container->conf->get('resource.history'));
55 };
56
57 $container['bookmarkService'] = function (ShaarliContainer $container): BookmarkServiceInterface {
58 return new BookmarkFileService(
59 $container->conf,
60 $container->history,
61 $container->loginManager->isLoggedIn()
62 );
63 };
64
65 $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
66 return new PageBuilder(
67 $container->conf,
68 $container->sessionManager->getSession(),
69 $container->bookmarkService,
70 $container->sessionManager->generateToken(),
71 $container->loginManager->isLoggedIn()
72 );
73 };
74
75 return $container;
76 }
77}
diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php
new file mode 100644
index 00000000..f5483d5e
--- /dev/null
+++ b/application/container/ShaarliContainer.php
@@ -0,0 +1,28 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Container;
6
7use Shaarli\Bookmark\BookmarkServiceInterface;
8use Shaarli\Config\ConfigManager;
9use Shaarli\History;
10use Shaarli\Render\PageBuilder;
11use Shaarli\Security\LoginManager;
12use Shaarli\Security\SessionManager;
13use Slim\Container;
14
15/**
16 * Extension of Slim container to document the injected objects.
17 *
18 * @property ConfigManager $conf
19 * @property SessionManager $sessionManager
20 * @property LoginManager $loginManager
21 * @property History $history
22 * @property BookmarkServiceInterface $bookmarkService
23 * @property PageBuilder $pageBuilder
24 */
25class ShaarliContainer extends Container
26{
27
28}
diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php
new file mode 100644
index 00000000..fa6c6467
--- /dev/null
+++ b/application/front/ShaarliMiddleware.php
@@ -0,0 +1,57 @@
1<?php
2
3namespace Shaarli\Front;
4
5use Shaarli\Container\ShaarliContainer;
6use Shaarli\Front\Exception\ShaarliException;
7use Slim\Http\Request;
8use Slim\Http\Response;
9
10/**
11 * Class ShaarliMiddleware
12 *
13 * This will be called before accessing any Shaarli controller.
14 */
15class ShaarliMiddleware
16{
17 /** @var ShaarliContainer contains all Shaarli DI */
18 protected $container;
19
20 public function __construct(ShaarliContainer $container)
21 {
22 $this->container = $container;
23 }
24
25 /**
26 * Middleware execution:
27 * - execute the controller
28 * - return the response
29 *
30 * In case of error, the error template will be displayed with the exception message.
31 *
32 * @param Request $request Slim request
33 * @param Response $response Slim response
34 * @param callable $next Next action
35 *
36 * @return Response response.
37 */
38 public function __invoke(Request $request, Response $response, callable $next)
39 {
40 try {
41 $response = $next($request, $response);
42 } catch (ShaarliException $e) {
43 $this->container->pageBuilder->assign('message', $e->getMessage());
44 if ($this->container->conf->get('dev.debug', false)) {
45 $this->container->pageBuilder->assign(
46 'stacktrace',
47 nl2br(get_class($this) .': '. $e->getTraceAsString())
48 );
49 }
50
51 $response = $response->withStatus($e->getCode());
52 $response = $response->write($this->container->pageBuilder->render('error'));
53 }
54
55 return $response;
56 }
57}
diff --git a/application/front/controllers/LoginController.php b/application/front/controllers/LoginController.php
new file mode 100644
index 00000000..47fa3ee3
--- /dev/null
+++ b/application/front/controllers/LoginController.php
@@ -0,0 +1,46 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use Shaarli\Front\Exception\LoginBannedException;
8use Slim\Http\Request;
9use Slim\Http\Response;
10
11/**
12 * Class LoginController
13 *
14 * Slim controller used to render the login page.
15 *
16 * The login page is not available if the user is banned
17 * or if open shaarli setting is enabled.
18 *
19 * @package Front\Controller
20 */
21class LoginController extends ShaarliController
22{
23 public function index(Request $request, Response $response): Response
24 {
25 if ($this->ci->loginManager->isLoggedIn() || $this->ci->conf->get('security.open_shaarli', false)) {
26 return $response->withRedirect('./');
27 }
28
29 $userCanLogin = $this->ci->loginManager->canLogin($request->getServerParams());
30 if ($userCanLogin !== true) {
31 throw new LoginBannedException();
32 }
33
34 if ($request->getParam('username') !== null) {
35 $this->assignView('username', escape($request->getParam('username')));
36 }
37
38 $this
39 ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER')))
40 ->assignView('remember_user_default', $this->ci->conf->get('privacy.remember_user_default', true))
41 ->assignView('pagetitle', t('Login') .' - '. $this->ci->conf->get('general.title', 'Shaarli'))
42 ;
43
44 return $response->write($this->ci->pageBuilder->render('loginform'));
45 }
46}
diff --git a/application/front/controllers/ShaarliController.php b/application/front/controllers/ShaarliController.php
new file mode 100644
index 00000000..2a166c3c
--- /dev/null
+++ b/application/front/controllers/ShaarliController.php
@@ -0,0 +1,31 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use Shaarli\Container\ShaarliContainer;
8
9abstract class ShaarliController
10{
11 /** @var ShaarliContainer */
12 protected $ci;
13
14 /** @param ShaarliContainer $ci Slim container (extended for attribute completion). */
15 public function __construct(ShaarliContainer $ci)
16 {
17 $this->ci = $ci;
18 }
19
20 /**
21 * Assign variables to RainTPL template through the PageBuilder.
22 *
23 * @param mixed $value Value to assign to the template
24 */
25 protected function assignView(string $name, $value): self
26 {
27 $this->ci->pageBuilder->assign($name, $value);
28
29 return $this;
30 }
31}
diff --git a/application/front/exceptions/LoginBannedException.php b/application/front/exceptions/LoginBannedException.php
new file mode 100644
index 00000000..b31a4a14
--- /dev/null
+++ b/application/front/exceptions/LoginBannedException.php
@@ -0,0 +1,15 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class LoginBannedException extends ShaarliException
8{
9 public function __construct()
10 {
11 $message = t('You have been banned after too many failed login attempts. Try again later.');
12
13 parent::__construct($message, 401);
14 }
15}
diff --git a/application/front/exceptions/ShaarliException.php b/application/front/exceptions/ShaarliException.php
new file mode 100644
index 00000000..800bfbec
--- /dev/null
+++ b/application/front/exceptions/ShaarliException.php
@@ -0,0 +1,23 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7use Throwable;
8
9/**
10 * Class ShaarliException
11 *
12 * Abstract exception class used to defined any custom exception thrown during front rendering.
13 *
14 * @package Front\Exception
15 */
16abstract class ShaarliException extends \Exception
17{
18 /** Override parent constructor to force $message and $httpCode parameters to be set. */
19 public function __construct(string $message, int $httpCode, Throwable $previous = null)
20 {
21 parent::__construct($message, $httpCode, $previous);
22 }
23}
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php
index 65e85aaf..f4fefda8 100644
--- a/application/render/PageBuilder.php
+++ b/application/render/PageBuilder.php
@@ -200,6 +200,23 @@ class PageBuilder
200 } 200 }
201 201
202 /** 202 /**
203 * Render a specific page as string (using a template file).
204 * e.g. $pb->render('picwall');
205 *
206 * @param string $page Template filename (without extension).
207 *
208 * @return string Processed template content
209 */
210 public function render(string $page): string
211 {
212 if ($this->tpl === false) {
213 $this->initialize();
214 }
215
216 return $this->tpl->draw($page, true);
217 }
218
219 /**
203 * Render a 404 page (uses the template : tpl/404.tpl) 220 * Render a 404 page (uses the template : tpl/404.tpl)
204 * usage: $PAGE->render404('The link was deleted') 221 * usage: $PAGE->render404('The link was deleted')
205 * 222 *
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
index b8b8ab8d..994fcbe5 100644
--- a/application/security/SessionManager.php
+++ b/application/security/SessionManager.php
@@ -196,4 +196,10 @@ class SessionManager
196 } 196 }
197 return true; 197 return true;
198 } 198 }
199
200 /** @return array Local reference to the global $_SESSION array */
201 public function getSession(): array
202 {
203 return $this->session;
204 }
199} 205}
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index cd5dd9e6..28acb4b4 100644
--- a/assets/default/scss/shaarli.scss
+++ b/assets/default/scss/shaarli.scss
@@ -1236,8 +1236,19 @@ form {
1236 color: $dark-grey; 1236 color: $dark-grey;
1237} 1237}
1238 1238
1239.page404-container { 1239.pageError-container {
1240 color: $dark-grey; 1240 color: $dark-grey;
1241
1242 h2 {
1243 margin: 70px 0 25px 0;
1244 }
1245
1246 pre {
1247 text-align: left;
1248 margin: 0 20%;
1249 padding: 20px 0;
1250 line-height: 0.7em;
1251 }
1241} 1252}
1242 1253
1243// EDIT LINK 1254// EDIT LINK
diff --git a/composer.json b/composer.json
index ada06a74..6b670fa2 100644
--- a/composer.json
+++ b/composer.json
@@ -48,9 +48,13 @@
48 "Shaarli\\Bookmark\\Exception\\": "application/bookmark/exception", 48 "Shaarli\\Bookmark\\Exception\\": "application/bookmark/exception",
49 "Shaarli\\Config\\": "application/config/", 49 "Shaarli\\Config\\": "application/config/",
50 "Shaarli\\Config\\Exception\\": "application/config/exception", 50 "Shaarli\\Config\\Exception\\": "application/config/exception",
51 "Shaarli\\Container\\": "application/container",
51 "Shaarli\\Exceptions\\": "application/exceptions", 52 "Shaarli\\Exceptions\\": "application/exceptions",
52 "Shaarli\\Feed\\": "application/feed", 53 "Shaarli\\Feed\\": "application/feed",
53 "Shaarli\\Formatter\\": "application/formatter", 54 "Shaarli\\Formatter\\": "application/formatter",
55 "Shaarli\\Front\\": "application/front",
56 "Shaarli\\Front\\Controller\\": "application/front/controllers",
57 "Shaarli\\Front\\Exception\\": "application/front/exceptions",
54 "Shaarli\\Http\\": "application/http", 58 "Shaarli\\Http\\": "application/http",
55 "Shaarli\\Legacy\\": "application/legacy", 59 "Shaarli\\Legacy\\": "application/legacy",
56 "Shaarli\\Netscape\\": "application/netscape", 60 "Shaarli\\Netscape\\": "application/netscape",
diff --git a/index.php b/index.php
index 76ad3696..7da8c22f 100644
--- a/index.php
+++ b/index.php
@@ -61,29 +61,31 @@ require_once 'application/FileUtils.php';
61require_once 'application/TimeZone.php'; 61require_once 'application/TimeZone.php';
62require_once 'application/Utils.php'; 62require_once 'application/Utils.php';
63 63
64use \Shaarli\ApplicationUtils; 64use Shaarli\ApplicationUtils;
65use Shaarli\Bookmark\BookmarkServiceInterface;
66use \Shaarli\Bookmark\Exception\BookmarkNotFoundException;
67use Shaarli\Bookmark\Bookmark; 65use Shaarli\Bookmark\Bookmark;
68use Shaarli\Bookmark\BookmarkFilter;
69use Shaarli\Bookmark\BookmarkFileService; 66use Shaarli\Bookmark\BookmarkFileService;
70use \Shaarli\Config\ConfigManager; 67use Shaarli\Bookmark\BookmarkFilter;
71use \Shaarli\Feed\CachedPage; 68use Shaarli\Bookmark\BookmarkServiceInterface;
72use \Shaarli\Feed\FeedBuilder; 69use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
70use Shaarli\Config\ConfigManager;
71use Shaarli\Container\ContainerBuilder;
72use Shaarli\Feed\CachedPage;
73use Shaarli\Feed\FeedBuilder;
73use Shaarli\Formatter\BookmarkMarkdownFormatter; 74use Shaarli\Formatter\BookmarkMarkdownFormatter;
74use Shaarli\Formatter\FormatterFactory; 75use Shaarli\Formatter\FormatterFactory;
75use \Shaarli\History; 76use Shaarli\History;
76use \Shaarli\Languages; 77use Shaarli\Languages;
77use \Shaarli\Netscape\NetscapeBookmarkUtils; 78use Shaarli\Netscape\NetscapeBookmarkUtils;
78use \Shaarli\Plugin\PluginManager; 79use Shaarli\Plugin\PluginManager;
79use \Shaarli\Render\PageBuilder; 80use Shaarli\Render\PageBuilder;
80use \Shaarli\Render\ThemeUtils; 81use Shaarli\Render\ThemeUtils;
81use \Shaarli\Router; 82use Shaarli\Router;
82use \Shaarli\Security\LoginManager; 83use Shaarli\Security\LoginManager;
83use \Shaarli\Security\SessionManager; 84use Shaarli\Security\SessionManager;
84use \Shaarli\Thumbnailer; 85use Shaarli\Thumbnailer;
85use \Shaarli\Updater\Updater; 86use Shaarli\Updater\Updater;
86use \Shaarli\Updater\UpdaterUtils; 87use Shaarli\Updater\UpdaterUtils;
88use Slim\App;
87 89
88// Ensure the PHP version is supported 90// Ensure the PHP version is supported
89try { 91try {
@@ -594,19 +596,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
594 596
595 // -------- Display login form. 597 // -------- Display login form.
596 if ($targetPage == Router::$PAGE_LOGIN) { 598 if ($targetPage == Router::$PAGE_LOGIN) {
597 if ($conf->get('security.open_shaarli')) { 599 header('Location: ./login');
598 header('Location: ?');
599 exit;
600 } // No need to login for open Shaarli
601 if (isset($_GET['username'])) {
602 $PAGE->assign('username', escape($_GET['username']));
603 }
604 $PAGE->assign('returnurl', (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):''));
605 // add default state of the 'remember me' checkbox
606 $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default'));
607 $PAGE->assign('user_can_login', $loginManager->canLogin($_SERVER));
608 $PAGE->assign('pagetitle', t('Login') .' - '. $conf->get('general.title', 'Shaarli'));
609 $PAGE->renderPage('loginform');
610 exit; 600 exit;
611 } 601 }
612 // -------- User wants to logout. 602 // -------- User wants to logout.
@@ -1930,11 +1920,9 @@ if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=
1930 exit; 1920 exit;
1931} 1921}
1932 1922
1933$container = new \Slim\Container(); 1923$containerBuilder = new ContainerBuilder($conf, $sessionManager, $loginManager);
1934$container['conf'] = $conf; 1924$container = $containerBuilder->build();
1935$container['plugins'] = $pluginManager; 1925$app = new App($container);
1936$container['history'] = $history;
1937$app = new \Slim\App($container);
1938 1926
1939// REST API routes 1927// REST API routes
1940$app->group('/api/v1', function () { 1928$app->group('/api/v1', function () {
@@ -1953,6 +1941,10 @@ $app->group('/api/v1', function () {
1953 $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory'); 1941 $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory');
1954})->add('\Shaarli\Api\ApiMiddleware'); 1942})->add('\Shaarli\Api\ApiMiddleware');
1955 1943
1944$app->group('', function () {
1945 $this->get('/login', '\Shaarli\Front\Controller\LoginController:index')->setName('login');
1946})->add('\Shaarli\Front\ShaarliMiddleware');
1947
1956$response = $app->run(true); 1948$response = $app->run(true);
1957 1949
1958// Hack to make Slim and Shaarli router work together: 1950// Hack to make Slim and Shaarli router work together:
diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php
new file mode 100644
index 00000000..9b97ed6d
--- /dev/null
+++ b/tests/container/ContainerBuilderTest.php
@@ -0,0 +1,49 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Container;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager;
10use Shaarli\History;
11use Shaarli\Render\PageBuilder;
12use Shaarli\Security\LoginManager;
13use Shaarli\Security\SessionManager;
14
15class ContainerBuilderTest extends TestCase
16{
17 /** @var ConfigManager */
18 protected $conf;
19
20 /** @var SessionManager */
21 protected $sessionManager;
22
23 /** @var LoginManager */
24 protected $loginManager;
25
26 /** @var ContainerBuilder */
27 protected $containerBuilder;
28
29 public function setUp(): void
30 {
31 $this->conf = new ConfigManager('tests/utils/config/configJson');
32 $this->sessionManager = $this->createMock(SessionManager::class);
33 $this->loginManager = $this->createMock(LoginManager::class);
34
35 $this->containerBuilder = new ContainerBuilder($this->conf, $this->sessionManager, $this->loginManager);
36 }
37
38 public function testBuildContainer(): void
39 {
40 $container = $this->containerBuilder->build();
41
42 static::assertInstanceOf(ConfigManager::class, $container->conf);
43 static::assertInstanceOf(SessionManager::class, $container->sessionManager);
44 static::assertInstanceOf(LoginManager::class, $container->loginManager);
45 static::assertInstanceOf(History::class, $container->history);
46 static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService);
47 static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
48 }
49}
diff --git a/tests/front/ShaarliMiddlewareTest.php b/tests/front/ShaarliMiddlewareTest.php
new file mode 100644
index 00000000..80974f37
--- /dev/null
+++ b/tests/front/ShaarliMiddlewareTest.php
@@ -0,0 +1,70 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Container\ShaarliContainer;
10use Shaarli\Front\Exception\LoginBannedException;
11use Shaarli\Render\PageBuilder;
12use Slim\Http\Request;
13use Slim\Http\Response;
14
15class ShaarliMiddlewareTest extends TestCase
16{
17 /** @var ShaarliContainer */
18 protected $container;
19
20 /** @var ShaarliMiddleware */
21 protected $middleware;
22
23 public function setUp(): void
24 {
25 $this->container = $this->createMock(ShaarliContainer::class);
26 $this->middleware = new ShaarliMiddleware($this->container);
27 }
28
29 public function testMiddlewareExecution(): void
30 {
31 $request = $this->createMock(Request::class);
32 $response = new Response();
33 $controller = function (Request $request, Response $response): Response {
34 return $response->withStatus(418); // I'm a tea pot
35 };
36
37 /** @var Response $result */
38 $result = $this->middleware->__invoke($request, $response, $controller);
39
40 static::assertInstanceOf(Response::class, $result);
41 static::assertSame(418, $result->getStatusCode());
42 }
43
44 public function testMiddlewareExecutionWithException(): void
45 {
46 $request = $this->createMock(Request::class);
47 $response = new Response();
48 $controller = function (): void {
49 $exception = new LoginBannedException();
50
51 throw new $exception;
52 };
53
54 $pageBuilder = $this->createMock(PageBuilder::class);
55 $pageBuilder->method('render')->willReturnCallback(function (string $message): string {
56 return $message;
57 });
58 $this->container->pageBuilder = $pageBuilder;
59
60 $conf = $this->createMock(ConfigManager::class);
61 $this->container->conf = $conf;
62
63 /** @var Response $result */
64 $result = $this->middleware->__invoke($request, $response, $controller);
65
66 static::assertInstanceOf(Response::class, $result);
67 static::assertSame(401, $result->getStatusCode());
68 static::assertContains('error', (string) $result->getBody());
69 }
70}
diff --git a/tests/front/controller/LoginControllerTest.php b/tests/front/controller/LoginControllerTest.php
new file mode 100644
index 00000000..ddcfe154
--- /dev/null
+++ b/tests/front/controller/LoginControllerTest.php
@@ -0,0 +1,173 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Container\ShaarliContainer;
10use Shaarli\Front\Exception\LoginBannedException;
11use Shaarli\Render\PageBuilder;
12use Shaarli\Security\LoginManager;
13use Slim\Http\Request;
14use Slim\Http\Response;
15
16class LoginControllerTest extends TestCase
17{
18 /** @var ShaarliContainer */
19 protected $container;
20
21 /** @var LoginController */
22 protected $controller;
23
24 public function setUp(): void
25 {
26 $this->container = $this->createMock(ShaarliContainer::class);
27 $this->controller = new LoginController($this->container);
28 }
29
30 public function testValidControllerInvoke(): void
31 {
32 $this->createValidContainerMockSet();
33
34 $request = $this->createMock(Request::class);
35 $request->expects(static::once())->method('getServerParam')->willReturn('> referer');
36 $response = new Response();
37
38 $assignedVariables = [];
39 $this->container->pageBuilder
40 ->expects(static::exactly(3))
41 ->method('assign')
42 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
43 $assignedVariables[$key] = $value;
44
45 return $this;
46 })
47 ;
48
49 $result = $this->controller->index($request, $response);
50
51 static::assertInstanceOf(Response::class, $result);
52 static::assertSame(200, $result->getStatusCode());
53 static::assertSame('loginform', (string) $result->getBody());
54
55 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
56 static::assertSame(true, $assignedVariables['remember_user_default']);
57 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
58 }
59
60 public function testValidControllerInvokeWithUserName(): void
61 {
62 $this->createValidContainerMockSet();
63
64 $request = $this->createMock(Request::class);
65 $request->expects(static::once())->method('getServerParam')->willReturn('> referer');
66 $request->expects(static::exactly(2))->method('getParam')->willReturn('myUser>');
67 $response = new Response();
68
69 $assignedVariables = [];
70 $this->container->pageBuilder
71 ->expects(static::exactly(4))
72 ->method('assign')
73 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
74 $assignedVariables[$key] = $value;
75
76 return $this;
77 })
78 ;
79
80 $result = $this->controller->index($request, $response);
81
82 static::assertInstanceOf(Response::class, $result);
83 static::assertSame(200, $result->getStatusCode());
84 static::assertSame('loginform', (string) $result->getBody());
85
86 static::assertSame('myUser&gt;', $assignedVariables['username']);
87 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
88 static::assertSame(true, $assignedVariables['remember_user_default']);
89 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
90 }
91
92 public function testLoginControllerWhileLoggedIn(): void
93 {
94 $request = $this->createMock(Request::class);
95 $response = new Response();
96
97 $loginManager = $this->createMock(LoginManager::class);
98 $loginManager->expects(static::once())->method('isLoggedIn')->willReturn(true);
99 $this->container->loginManager = $loginManager;
100
101 $result = $this->controller->index($request, $response);
102
103 static::assertInstanceOf(Response::class, $result);
104 static::assertSame(302, $result->getStatusCode());
105 static::assertSame(['./'], $result->getHeader('Location'));
106 }
107
108 public function testLoginControllerOpenShaarli(): void
109 {
110 $this->createValidContainerMockSet();
111
112 $request = $this->createMock(Request::class);
113 $response = new Response();
114
115 $conf = $this->createMock(ConfigManager::class);
116 $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
117 if ($parameter === 'security.open_shaarli') {
118 return true;
119 }
120 return $default;
121 });
122 $this->container->conf = $conf;
123
124 $result = $this->controller->index($request, $response);
125
126 static::assertInstanceOf(Response::class, $result);
127 static::assertSame(302, $result->getStatusCode());
128 static::assertSame(['./'], $result->getHeader('Location'));
129 }
130
131 public function testLoginControllerWhileBanned(): void
132 {
133 $this->createValidContainerMockSet();
134
135 $request = $this->createMock(Request::class);
136 $response = new Response();
137
138 $loginManager = $this->createMock(LoginManager::class);
139 $loginManager->method('isLoggedIn')->willReturn(false);
140 $loginManager->method('canLogin')->willReturn(false);
141 $this->container->loginManager = $loginManager;
142
143 $this->expectException(LoginBannedException::class);
144
145 $this->controller->index($request, $response);
146 }
147
148 protected function createValidContainerMockSet(): void
149 {
150 // User logged out
151 $loginManager = $this->createMock(LoginManager::class);
152 $loginManager->method('isLoggedIn')->willReturn(false);
153 $loginManager->method('canLogin')->willReturn(true);
154 $this->container->loginManager = $loginManager;
155
156 // Config
157 $conf = $this->createMock(ConfigManager::class);
158 $conf->method('get')->willReturnCallback(function (string $parameter, $default) {
159 return $default;
160 });
161 $this->container->conf = $conf;
162
163 // PageBuilder
164 $pageBuilder = $this->createMock(PageBuilder::class);
165 $pageBuilder
166 ->method('render')
167 ->willReturnCallback(function (string $template): string {
168 return $template;
169 })
170 ;
171 $this->container->pageBuilder = $pageBuilder;
172 }
173}
diff --git a/tpl/default/404.html b/tpl/default/404.html
index 472566a6..1bc46c63 100644
--- a/tpl/default/404.html
+++ b/tpl/default/404.html
@@ -6,7 +6,7 @@
6<body> 6<body>
7<div id="pageheader"> 7<div id="pageheader">
8 {include="page.header"} 8 {include="page.header"}
9<div class="center" id="page404" class="page404-container"> 9<div id="pageError" class="pageError-container center">
10 <h2>{'Sorry, nothing to see here.'|t}</h2> 10 <h2>{'Sorry, nothing to see here.'|t}</h2>
11 <img src="img/sad_star.png" alt=""> 11 <img src="img/sad_star.png" alt="">
12 <p>{$error_message}</p> 12 <p>{$error_message}</p>
diff --git a/tpl/default/error.html b/tpl/default/error.html
new file mode 100644
index 00000000..8f357ce5
--- /dev/null
+++ b/tpl/default/error.html
@@ -0,0 +1,22 @@
1<!DOCTYPE html>
2<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
3<head>
4 {include="includes"}
5</head>
6<body>
7<div id="pageheader">
8 {include="page.header"}
9<div id="pageError" class="pageError-container center">
10 <h2>{$message}</h2>
11
12 {if="!empty($stacktrace)"}
13 <pre>
14 {$stacktrace}
15 </pre>
16 {/if}
17
18 <img src="img/sad_star.png" alt="">
19</div>
20{include="page.footer"}
21</body>
22</html>
diff --git a/tpl/default/loginform.html b/tpl/default/loginform.html
index 761aec0c..90c2b2b6 100644
--- a/tpl/default/loginform.html
+++ b/tpl/default/loginform.html
@@ -5,44 +5,32 @@
5</head> 5</head>
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8{if="!$user_can_login"} 8<div class="pure-g">
9<div class="pure-g pure-alert pure-alert-error pure-alert-closable center"> 9 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
10 <div class="pure-u-2-24"></div> 10 <div id="login-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24 login-form-container">
11 <div class="pure-u-20-24"> 11 <form method="post" name="loginform">
12 <p>{'You have been banned after too many failed login attempts. Try again later.'|t}</p> 12 <h2 class="window-title">{'Login'|t}</h2>
13 </div> 13 <div>
14 <div class="pure-u-2-24"> 14 <input type="text" name="login" aria-label="{'Username'|t}" placeholder="{'Username'|t}"
15 <i class="fa fa-times pure-alert-close"></i> 15 {if="!empty($username)"}value="{$username}"{/if} class="autofocus">
16 </div>
17 <div>
18 <input type="password" name="password" aria-label="{'Password'|t}" placeholder="{'Password'|t}" class="autofocus">
19 </div>
20 <div class="remember-me">
21 <input type="checkbox" name="longlastingsession" id="longlastingsessionform"
22 {if="$remember_user_default"}checked="checked"{/if}>
23 <label for="longlastingsessionform">{'Remember me'|t}</label>
24 </div>
25 <div>
26 <input type="submit" value="{'Login'|t}" class="bigbutton">
27 </div>
28 <input type="hidden" name="token" value="{$token}">
29 {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
30 </form>
16 </div> 31 </div>
32 <div class="pure-u-lg-1-3 pure-u-1-8"></div>
17</div> 33</div>
18{else}
19 <div class="pure-g">
20 <div class="pure-u-lg-1-3 pure-u-1-24"></div>
21 <div id="login-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24 login-form-container">
22 <form method="post" name="loginform">
23 <h2 class="window-title">{'Login'|t}</h2>
24 <div>
25 <input type="text" name="login" aria-label="{'Username'|t}" placeholder="{'Username'|t}"
26 {if="!empty($username)"}value="{$username}"{/if} class="autofocus">
27 </div>
28 <div>
29 <input type="password" name="password" aria-label="{'Password'|t}" placeholder="{'Password'|t}" class="autofocus">
30 </div>
31 <div class="remember-me">
32 <input type="checkbox" name="longlastingsession" id="longlastingsessionform"
33 {if="$remember_user_default"}checked="checked"{/if}>
34 <label for="longlastingsessionform">{'Remember me'|t}</label>
35 </div>
36 <div>
37 <input type="submit" value="{'Login'|t}" class="bigbutton">
38 </div>
39 <input type="hidden" name="token" value="{$token}">
40 {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
41 </form>
42 </div>
43 <div class="pure-u-lg-1-3 pure-u-1-8"></div>
44 </div>
45{/if}
46 34
47{include="page.footer"} 35{include="page.footer"}
48</body> 36</body>
diff --git a/tpl/vintage/error.html b/tpl/vintage/error.html
new file mode 100644
index 00000000..b6e62be0
--- /dev/null
+++ b/tpl/vintage/error.html
@@ -0,0 +1,25 @@
1<!DOCTYPE html>
2<html>
3<head>
4 {include="includes"}
5</head>
6<body>
7<div id="pageheader">
8 {include="page.header"}
9</div>
10<div class="error-container">
11 <h1>Error</h1>
12 <p>{$message}</p>
13
14 {if="!empty($stacktrace)"}
15 <br>
16 <pre>
17 {$stacktrace}
18 </pre>
19 {/if}
20
21 <p>Would you mind <a href="?">clicking here</a>?</p>
22</div>
23{include="page.footer"}
24</body>
25</html>
diff --git a/tpl/vintage/loginform.html b/tpl/vintage/loginform.html
index 0f7d6387..a3792066 100644
--- a/tpl/vintage/loginform.html
+++ b/tpl/vintage/loginform.html
@@ -2,36 +2,30 @@
2<html> 2<html>
3<head>{include="includes"}</head> 3<head>{include="includes"}</head>
4<body 4<body
5{if="$user_can_login"} 5{if="empty($username)"}
6 {if="empty($username)"} 6 onload="document.loginform.login.focus();"
7 onload="document.loginform.login.focus();" 7{else}
8 {else} 8 onload="document.loginform.password.focus();"
9 onload="document.loginform.password.focus();"
10 {/if}
11{/if}> 9{/if}>
12<div id="pageheader"> 10<div id="pageheader">
13 {include="page.header"} 11 {include="page.header"}
14 12
15 <div id="headerform"> 13 <div id="headerform">
16 {if="!$user_can_login"} 14 <form method="post" name="loginform">
17 You have been banned from login after too many failed attempts. Try later. 15 <label for="login">Login: <input type="text" id="login" name="login" tabindex="1"
18 {else} 16 {if="!empty($username)"}value="{$username}"{/if}>
19 <form method="post" name="loginform"> 17 </label>
20 <label for="login">Login: <input type="text" id="login" name="login" tabindex="1" 18 <label for="password">Password: <input type="password" id="password" name="password" tabindex="2">
21 {if="!empty($username)"}value="{$username}"{/if}> 19 </label>
22 </label> 20 <input type="submit" value="Login" class="bigbutton" tabindex="4">
23 <label for="password">Password: <input type="password" id="password" name="password" tabindex="2"> 21 <label for="longlastingsession">
24 </label> 22 <input type="checkbox" name="longlastingsession"
25 <input type="submit" value="Login" class="bigbutton" tabindex="4"> 23 id="longlastingsession" tabindex="3"
26 <label for="longlastingsession"> 24 {if="$remember_user_default"}checked="checked"{/if}>
27 <input type="checkbox" name="longlastingsession" 25 Stay signed in (Do not check on public computers)</label>
28 id="longlastingsession" tabindex="3" 26 <input type="hidden" name="token" value="{$token}">
29 {if="$remember_user_default"}checked="checked"{/if}> 27 {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
30 Stay signed in (Do not check on public computers)</label> 28 </form>
31 <input type="hidden" name="token" value="{$token}">
32 {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
33 </form>
34 {/if}
35 </div> 29 </div>
36</div> 30</div>
37 31