aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-01-26 11:41:10 +0100
committerGitHub <noreply@github.com>2020-01-26 11:41:10 +0100
commitc653ae3bfb11f663a52f55817e6d02a66d0852c8 (patch)
treed5a03a3e425180f1fafb646c8ec68a4b8458d0b3
parent1410dce2db310e71b5e683b0871c2f28d8807844 (diff)
parent27ceea2aeeed69b43fef4ebff35ec8004fcc2e45 (diff)
downloadShaarli-c653ae3bfb11f663a52f55817e6d02a66d0852c8.tar.gz
Shaarli-c653ae3bfb11f663a52f55817e6d02a66d0852c8.tar.zst
Shaarli-c653ae3bfb11f663a52f55817e6d02a66d0852c8.zip
Render login page through Slim controller (#1401)
Render login page through Slim controller
-rw-r--r--application/Utils.php2
-rw-r--r--application/container/ContainerBuilder.php81
-rw-r--r--application/container/ShaarliContainer.php30
-rw-r--r--application/front/ShaarliMiddleware.php57
-rw-r--r--application/front/controllers/LoginController.php48
-rw-r--r--application/front/controllers/ShaarliController.php69
-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--doc/md/Translations.md32
-rw-r--r--index.php84
-rw-r--r--tests/UtilsTest.php4
-rw-r--r--tests/container/ContainerBuilderTest.php49
-rw-r--r--tests/front/ShaarliMiddlewareTest.php70
-rw-r--r--tests/front/controller/LoginControllerTest.php178
-rw-r--r--tests/front/controller/ShaarliControllerTest.php116
-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/default/page.header.html4
-rw-r--r--tpl/vintage/error.html25
-rw-r--r--tpl/vintage/loginform.html44
-rw-r--r--tpl/vintage/page.header.html2
26 files changed, 926 insertions, 131 deletions
diff --git a/application/Utils.php b/application/Utils.php
index 56f5b9a2..4b7fc546 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -159,7 +159,7 @@ function checkDateFormat($format, $string)
159 */ 159 */
160function generateLocation($referer, $host, $loopTerms = array()) 160function generateLocation($referer, $host, $loopTerms = array())
161{ 161{
162 $finalReferer = '?'; 162 $finalReferer = './?';
163 163
164 // No referer if it contains any value in $loopCriteria. 164 // No referer if it contains any value in $loopCriteria.
165 foreach (array_filter($loopTerms) as $value) { 165 foreach (array_filter($loopTerms) as $value) {
diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php
new file mode 100644
index 00000000..e2c78ccc
--- /dev/null
+++ b/application/container/ContainerBuilder.php
@@ -0,0 +1,81 @@
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 $container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
76 return new PluginManager($container->conf);
77 };
78
79 return $container;
80 }
81}
diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php
new file mode 100644
index 00000000..3fa9116e
--- /dev/null
+++ b/application/container/ShaarliContainer.php
@@ -0,0 +1,30 @@
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\Plugin\PluginManager;
11use Shaarli\Render\PageBuilder;
12use Shaarli\Security\LoginManager;
13use Shaarli\Security\SessionManager;
14use Slim\Container;
15
16/**
17 * Extension of Slim container to document the injected objects.
18 *
19 * @property ConfigManager $conf
20 * @property SessionManager $sessionManager
21 * @property LoginManager $loginManager
22 * @property History $history
23 * @property BookmarkServiceInterface $bookmarkService
24 * @property PageBuilder $pageBuilder
25 * @property PluginManager $pluginManager
26 */
27class ShaarliContainer extends Container
28{
29
30}
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..ae3599e0
--- /dev/null
+++ b/application/front/controllers/LoginController.php
@@ -0,0 +1,48 @@
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->container->loginManager->isLoggedIn()
26 || $this->container->conf->get('security.open_shaarli', false)
27 ) {
28 return $response->withRedirect('./');
29 }
30
31 $userCanLogin = $this->container->loginManager->canLogin($request->getServerParams());
32 if ($userCanLogin !== true) {
33 throw new LoginBannedException();
34 }
35
36 if ($request->getParam('username') !== null) {
37 $this->assignView('username', escape($request->getParam('username')));
38 }
39
40 $this
41 ->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER')))
42 ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
43 ->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli'))
44 ;
45
46 return $response->write($this->render('loginform'));
47 }
48}
diff --git a/application/front/controllers/ShaarliController.php b/application/front/controllers/ShaarliController.php
new file mode 100644
index 00000000..2b828588
--- /dev/null
+++ b/application/front/controllers/ShaarliController.php
@@ -0,0 +1,69 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use Shaarli\Bookmark\BookmarkFilter;
8use Shaarli\Container\ShaarliContainer;
9
10abstract class ShaarliController
11{
12 /** @var ShaarliContainer */
13 protected $container;
14
15 /** @param ShaarliContainer $container Slim container (extended for attribute completion). */
16 public function __construct(ShaarliContainer $container)
17 {
18 $this->container = $container;
19 }
20
21 /**
22 * Assign variables to RainTPL template through the PageBuilder.
23 *
24 * @param mixed $value Value to assign to the template
25 */
26 protected function assignView(string $name, $value): self
27 {
28 $this->container->pageBuilder->assign($name, $value);
29
30 return $this;
31 }
32
33 protected function render(string $template): string
34 {
35 $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL));
36 $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE));
37 $this->assignView('plugin_errors', $this->container->pluginManager->getErrors());
38
39 $this->executeDefaultHooks($template);
40
41 return $this->container->pageBuilder->render($template);
42 }
43
44 /**
45 * Call plugin hooks for header, footer and includes, specifying which page will be rendered.
46 * Then assign generated data to RainTPL.
47 */
48 protected function executeDefaultHooks(string $template): void
49 {
50 $common_hooks = [
51 'includes',
52 'header',
53 'footer',
54 ];
55
56 foreach ($common_hooks as $name) {
57 $plugin_data = [];
58 $this->container->pluginManager->executeHooks(
59 'render_' . $name,
60 $plugin_data,
61 [
62 'target' => $template,
63 'loggedin' => $this->container->loginManager->isLoggedIn()
64 ]
65 );
66 $this->assignView('plugins_' . $name, $plugin_data);
67 }
68 }
69}
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..243ab1b2 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.page-error-container {
1240 color: $dark-grey; 1240 color: $dark-grey;
1241
1242 h2 {
1243 margin: 70px 0 25px;
1244 }
1245
1246 pre {
1247 margin: 0 20%;
1248 padding: 20px 0;
1249 text-align: left;
1250 line-height: .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/doc/md/Translations.md b/doc/md/Translations.md
index c7d33855..58b92da3 100644
--- a/doc/md/Translations.md
+++ b/doc/md/Translations.md
@@ -7,8 +7,8 @@ Note that only the `default` theme supports translations.
7 7
8### Contributing 8### Contributing
9 9
10We encourage the community to contribute to Shaarli's translation either by improving existing 10We encourage the community to contribute to Shaarli's translation either by improving existing
11translations or submitting a new language. 11translations or submitting a new language.
12 12
13Contributing to the translation does not require development skill. 13Contributing to the translation does not require development skill.
14 14
@@ -21,8 +21,8 @@ First, install [Poedit](https://poedit.net/) tool.
21 21
22Poedit will extract strings to translate from the PHP source code. 22Poedit will extract strings to translate from the PHP source code.
23 23
24**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract 24**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract
25every translatable string. 25every translatable string.
26 26
27You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended) 27You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended)
28or visit every template page in your browser to generate cache files, while logged in. 28or visit every template page in your browser to generate cache files, while logged in.
@@ -41,7 +41,7 @@ http://<replace_domain>/?do=daily
41http://<replace_domain>/?post 41http://<replace_domain>/?post
42http://<replace_domain>/?do=export 42http://<replace_domain>/?do=export
43http://<replace_domain>/?do=import 43http://<replace_domain>/?do=import
44http://<replace_domain>/?do=login 44http://<replace_domain>/login
45http://<replace_domain>/?do=picwall 45http://<replace_domain>/?do=picwall
46http://<replace_domain>/?do=pluginadmin 46http://<replace_domain>/?do=pluginadmin
47http://<replace_domain>/?do=tagcloud 47http://<replace_domain>/?do=tagcloud
@@ -50,8 +50,8 @@ http://<replace_domain>/?do=taglist
50 50
51#### Improve existing translation 51#### Improve existing translation
52 52
53In Poedit, click on "Edit a Translation", and from Shaarli's directory open 53In Poedit, click on "Edit a Translation", and from Shaarli's directory open
54`inc/languages/<lang>/LC_MESSAGES/shaarli.po`. 54`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
55 55
56The existing list of translatable strings should have been loaded, then click on the "Update" button. 56The existing list of translatable strings should have been loaded, then click on the "Update" button.
57 57
@@ -63,20 +63,20 @@ Save when you're done, then you can submit a pull request containing the updated
63 63
64#### Add a new language 64#### Add a new language
65 65
66Open Poedit and select "Create New Translation", then from Shaarli's directory open 66Open Poedit and select "Create New Translation", then from Shaarli's directory open
67`inc/languages/<lang>/LC_MESSAGES/shaarli.po`. 67`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
68 68
69Then select the language you want to create. 69Then select the language you want to create.
70 70
71Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`. 71Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`.
72`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2) 72`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2)
73format in lowercase (e.g. `de` for German). 73format in lowercase (e.g. `de` for German).
74 74
75Then click on the "Update" button, and you can start to translate every available string. 75Then click on the "Update" button, and you can start to translate every available string.
76 76
77Save when you're done, then you can submit a pull request containing the new `shaarli.po`. 77Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
78 78
79### Theme translations 79### Theme translations
80 80
81Theme translation extensions are loaded automatically if they're present. 81Theme translation extensions are loaded automatically if they're present.
82 82
@@ -85,7 +85,7 @@ As a theme developer, all you have to do is to add the `.po` and `.mo` compiled
85 tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.po 85 tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.po
86 tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.mo 86 tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.mo
87 87
88Where `<lang>` is the ISO 3166-1 alpha-2 language code. 88Where `<lang>` is the ISO 3166-1 alpha-2 language code.
89Read the following section "Extend Shaarli's translation" to learn how to generate those files. 89Read the following section "Extend Shaarli's translation" to learn how to generate those files.
90 90
91### Extend Shaarli's translation 91### Extend Shaarli's translation
@@ -106,7 +106,7 @@ First, create your translation files tree directory:
106Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be 106Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be
107`my_theme.po`. 107`my_theme.po`.
108 108
109Users have to register your extension in their configuration with the parameter 109Users have to register your extension in their configuration with the parameter
110`translation.extensions.<domain>: <translation files path>`. 110`translation.extensions.<domain>: <translation files path>`.
111 111
112Example: 112Example:
@@ -151,11 +151,11 @@ When you're done, open Poedit and load translation strings from sources:
151 1. `File > New` 151 1. `File > New`
152 2. Choose your language 152 2. Choose your language
153 3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`. 153 3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`.
154 4. Go to `Catalog > Properties...` 154 4. Go to `Catalog > Properties...`
155 5. Fill the `Translation Properties` tab 155 5. Fill the `Translation Properties` tab
156 6. Add your source path in the `Sources Paths` tab 156 6. Add your source path in the `Sources Paths` tab
157 7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines: 157 7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines:
158 158
159``` 159```
160my_theme_t 160my_theme_t
161my_theme_t:1,2 161my_theme_t:1,2
diff --git a/index.php b/index.php
index 76ad3696..474d9af5 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 {
@@ -250,7 +252,7 @@ if (isset($_POST['login'])) {
250 252
251 // Optional redirect after login: 253 // Optional redirect after login:
252 if (isset($_GET['post'])) { 254 if (isset($_GET['post'])) {
253 $uri = '?post='. urlencode($_GET['post']); 255 $uri = './?post='. urlencode($_GET['post']);
254 foreach (array('description', 'source', 'title', 'tags') as $param) { 256 foreach (array('description', 'source', 'title', 'tags') as $param) {
255 if (!empty($_GET[$param])) { 257 if (!empty($_GET[$param])) {
256 $uri .= '&'.$param.'='.urlencode($_GET[$param]); 258 $uri .= '&'.$param.'='.urlencode($_GET[$param]);
@@ -261,22 +263,22 @@ if (isset($_POST['login'])) {
261 } 263 }
262 264
263 if (isset($_GET['edit_link'])) { 265 if (isset($_GET['edit_link'])) {
264 header('Location: ?edit_link='. escape($_GET['edit_link'])); 266 header('Location: ./?edit_link='. escape($_GET['edit_link']));
265 exit; 267 exit;
266 } 268 }
267 269
268 if (isset($_POST['returnurl'])) { 270 if (isset($_POST['returnurl'])) {
269 // Prevent loops over login screen. 271 // Prevent loops over login screen.
270 if (strpos($_POST['returnurl'], 'do=login') === false) { 272 if (strpos($_POST['returnurl'], '/login') === false) {
271 header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST'])); 273 header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
272 exit; 274 exit;
273 } 275 }
274 } 276 }
275 header('Location: ?'); 277 header('Location: ./?');
276 exit; 278 exit;
277 } else { 279 } else {
278 $loginManager->handleFailedLogin($_SERVER); 280 $loginManager->handleFailedLogin($_SERVER);
279 $redir = '&username='. urlencode($_POST['login']); 281 $redir = '?username='. urlencode($_POST['login']);
280 if (isset($_GET['post'])) { 282 if (isset($_GET['post'])) {
281 $redir .= '&post=' . urlencode($_GET['post']); 283 $redir .= '&post=' . urlencode($_GET['post']);
282 foreach (array('description', 'source', 'title', 'tags') as $param) { 284 foreach (array('description', 'source', 'title', 'tags') as $param) {
@@ -286,7 +288,7 @@ if (isset($_POST['login'])) {
286 } 288 }
287 } 289 }
288 // Redirect to login screen. 290 // Redirect to login screen.
289 echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>'; 291 echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'./login'.$redir.'\';</script>';
290 exit; 292 exit;
291 } 293 }
292} 294}
@@ -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.
@@ -933,7 +923,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
933 // Show login screen, then redirect to ?post=... 923 // Show login screen, then redirect to ?post=...
934 if (isset($_GET['post'])) { 924 if (isset($_GET['post'])) {
935 header( // Redirect to login page, then back to post link. 925 header( // Redirect to login page, then back to post link.
936 'Location: ?do=login&post='.urlencode($_GET['post']). 926 'Location: /login?post='.urlencode($_GET['post']).
937 (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):''). 927 (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
938 (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):''). 928 (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
939 (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):''). 929 (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
@@ -944,7 +934,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
944 934
945 showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager); 935 showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
946 if (isset($_GET['edit_link'])) { 936 if (isset($_GET['edit_link'])) {
947 header('Location: ?do=login&edit_link='. escape($_GET['edit_link'])); 937 header('Location: /login?edit_link='. escape($_GET['edit_link']));
948 exit; 938 exit;
949 } 939 }
950 940
@@ -1900,7 +1890,7 @@ function install($conf, $sessionManager, $loginManager)
1900 echo '<script>alert(' 1890 echo '<script>alert('
1901 .'"Shaarli is now configured. ' 1891 .'"Shaarli is now configured. '
1902 .'Please enter your login/password and start shaaring your bookmarks!"' 1892 .'Please enter your login/password and start shaaring your bookmarks!"'
1903 .');document.location=\'?do=login\';</script>'; 1893 .');document.location=\'./login\';</script>';
1904 exit; 1894 exit;
1905 } 1895 }
1906 1896
@@ -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/UtilsTest.php b/tests/UtilsTest.php
index 8225d95a..26d2a6b8 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -203,7 +203,7 @@ class UtilsTest extends PHPUnit\Framework\TestCase
203 public function testGenerateLocationLoop() 203 public function testGenerateLocationLoop()
204 { 204 {
205 $ref = 'http://localhost/?test'; 205 $ref = 'http://localhost/?test';
206 $this->assertEquals('?', generateLocation($ref, 'localhost', array('test'))); 206 $this->assertEquals('./?', generateLocation($ref, 'localhost', array('test')));
207 } 207 }
208 208
209 /** 209 /**
@@ -212,7 +212,7 @@ class UtilsTest extends PHPUnit\Framework\TestCase
212 public function testGenerateLocationOut() 212 public function testGenerateLocationOut()
213 { 213 {
214 $ref = 'http://somewebsite.com/?test'; 214 $ref = 'http://somewebsite.com/?test';
215 $this->assertEquals('?', generateLocation($ref, 'localhost')); 215 $this->assertEquals('./?', generateLocation($ref, 'localhost'));
216 } 216 }
217 217
218 218
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..8cf8ece7
--- /dev/null
+++ b/tests/front/controller/LoginControllerTest.php
@@ -0,0 +1,178 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkServiceInterface;
9use Shaarli\Config\ConfigManager;
10use Shaarli\Container\ShaarliContainer;
11use Shaarli\Front\Exception\LoginBannedException;
12use Shaarli\Plugin\PluginManager;
13use Shaarli\Render\PageBuilder;
14use Shaarli\Security\LoginManager;
15use Slim\Http\Request;
16use Slim\Http\Response;
17
18class LoginControllerTest extends TestCase
19{
20 /** @var ShaarliContainer */
21 protected $container;
22
23 /** @var LoginController */
24 protected $controller;
25
26 public function setUp(): void
27 {
28 $this->container = $this->createMock(ShaarliContainer::class);
29 $this->controller = new LoginController($this->container);
30 }
31
32 public function testValidControllerInvoke(): void
33 {
34 $this->createValidContainerMockSet();
35
36 $request = $this->createMock(Request::class);
37 $request->expects(static::once())->method('getServerParam')->willReturn('> referer');
38 $response = new Response();
39
40 $assignedVariables = [];
41 $this->container->pageBuilder
42 ->method('assign')
43 ->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
44 $assignedVariables[$key] = $value;
45
46 return $this;
47 })
48 ;
49
50 $result = $this->controller->index($request, $response);
51
52 static::assertInstanceOf(Response::class, $result);
53 static::assertSame(200, $result->getStatusCode());
54 static::assertSame('loginform', (string) $result->getBody());
55
56 static::assertSame('&gt; referer', $assignedVariables['returnurl']);
57 static::assertSame(true, $assignedVariables['remember_user_default']);
58 static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
59 }
60
61 public function testValidControllerInvokeWithUserName(): void
62 {
63 $this->createValidContainerMockSet();
64
65 $request = $this->createMock(Request::class);
66 $request->expects(static::once())->method('getServerParam')->willReturn('> referer');
67 $request->expects(static::exactly(2))->method('getParam')->willReturn('myUser>');
68 $response = new Response();
69
70 $assignedVariables = [];
71 $this->container->pageBuilder
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 $pluginManager = $this->createMock(PluginManager::class);
174 $this->container->pluginManager = $pluginManager;
175 $bookmarkService = $this->createMock(BookmarkServiceInterface::class);
176 $this->container->bookmarkService = $bookmarkService;
177 }
178}
diff --git a/tests/front/controller/ShaarliControllerTest.php b/tests/front/controller/ShaarliControllerTest.php
new file mode 100644
index 00000000..6fa3feb9
--- /dev/null
+++ b/tests/front/controller/ShaarliControllerTest.php
@@ -0,0 +1,116 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Bookmark\BookmarkFilter;
9use Shaarli\Bookmark\BookmarkServiceInterface;
10use Shaarli\Container\ShaarliContainer;
11use Shaarli\Plugin\PluginManager;
12use Shaarli\Render\PageBuilder;
13use Shaarli\Security\LoginManager;
14
15/**
16 * Class ShaarliControllerTest
17 *
18 * This class is used to test default behavior of ShaarliController abstract class.
19 * It uses a dummy non abstract controller.
20 */
21class ShaarliControllerTest extends TestCase
22{
23 /** @var ShaarliContainer */
24 protected $container;
25
26 /** @var LoginController */
27 protected $controller;
28
29 /** @var mixed[] List of variable assigned to the template */
30 protected $assignedValues;
31
32 public function setUp(): void
33 {
34 $this->container = $this->createMock(ShaarliContainer::class);
35 $this->controller = new class($this->container) extends ShaarliController
36 {
37 public function assignView(string $key, $value): ShaarliController
38 {
39 return parent::assignView($key, $value);
40 }
41
42 public function render(string $template): string
43 {
44 return parent::render($template);
45 }
46 };
47 $this->assignedValues = [];
48 }
49
50 public function testAssignView(): void
51 {
52 $this->createValidContainerMockSet();
53
54 $self = $this->controller->assignView('variableName', 'variableValue');
55
56 static::assertInstanceOf(ShaarliController::class, $self);
57 static::assertSame('variableValue', $this->assignedValues['variableName']);
58 }
59
60 public function testRender(): void
61 {
62 $this->createValidContainerMockSet();
63
64 $render = $this->controller->render('templateName');
65
66 static::assertSame('templateName', $render);
67
68 static::assertSame(10, $this->assignedValues['linkcount']);
69 static::assertSame(5, $this->assignedValues['privateLinkcount']);
70 static::assertSame(['error'], $this->assignedValues['plugin_errors']);
71
72 static::assertSame('templateName', $this->assignedValues['plugins_includes']['render_includes']['target']);
73 static::assertTrue($this->assignedValues['plugins_includes']['render_includes']['loggedin']);
74 static::assertSame('templateName', $this->assignedValues['plugins_header']['render_header']['target']);
75 static::assertTrue($this->assignedValues['plugins_header']['render_header']['loggedin']);
76 static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']);
77 static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']);
78 }
79
80 protected function createValidContainerMockSet(): void
81 {
82 $pageBuilder = $this->createMock(PageBuilder::class);
83 $pageBuilder
84 ->method('assign')
85 ->willReturnCallback(function (string $key, $value): void {
86 $this->assignedValues[$key] = $value;
87 });
88 $pageBuilder
89 ->method('render')
90 ->willReturnCallback(function (string $template): string {
91 return $template;
92 });
93 $this->container->pageBuilder = $pageBuilder;
94
95 $bookmarkService = $this->createMock(BookmarkServiceInterface::class);
96 $bookmarkService
97 ->method('count')
98 ->willReturnCallback(function (string $visibility): int {
99 return $visibility === BookmarkFilter::$PRIVATE ? 5 : 10;
100 });
101 $this->container->bookmarkService = $bookmarkService;
102
103 $pluginManager = $this->createMock(PluginManager::class);
104 $pluginManager
105 ->method('executeHooks')
106 ->willReturnCallback(function (string $hook, array &$data, array $params): array {
107 return $data[$hook] = $params;
108 });
109 $pluginManager->method('getErrors')->willReturn(['error']);
110 $this->container->pluginManager = $pluginManager;
111
112 $loginManager = $this->createMock(LoginManager::class);
113 $loginManager->method('isLoggedIn')->willReturn(true);
114 $this->container->loginManager = $loginManager;
115 }
116}
diff --git a/tpl/default/404.html b/tpl/default/404.html
index 472566a6..09737b4b 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="page-error-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..ef1dfd73
--- /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="page-error-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/default/page.header.html b/tpl/default/page.header.html
index 4f063dc3..82f8ebf1 100644
--- a/tpl/default/page.header.html
+++ b/tpl/default/page.header.html
@@ -60,7 +60,7 @@
60 </li> 60 </li>
61 {else} 61 {else}
62 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login"> 62 <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login">
63 <a href="?do=login" class="pure-menu-link">{'Login'|t}</a> 63 <a href="/login" class="pure-menu-link">{'Login'|t}</a>
64 </li> 64 </li>
65 {/if} 65 {/if}
66 </ul> 66 </ul>
@@ -80,7 +80,7 @@
80 </li> 80 </li>
81 {if="!$is_logged_in"} 81 {if="!$is_logged_in"}
82 <li class="pure-menu-item" id="shaarli-menu-desktop-login"> 82 <li class="pure-menu-item" id="shaarli-menu-desktop-login">
83 <a href="?do=login" class="pure-menu-link" 83 <a href="/login" class="pure-menu-link"
84 data-open-id="header-login-form" 84 data-open-id="header-login-form"
85 id="login-button" aria-label="{'Login'|t}" title="{'Login'|t}"> 85 id="login-button" aria-label="{'Login'|t}" title="{'Login'|t}">
86 <i class="fa fa-user" aria-hidden="true"></i> 86 <i class="fa fa-user" aria-hidden="true"></i>
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
diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html
index 40c53e5b..a37926d2 100644
--- a/tpl/vintage/page.header.html
+++ b/tpl/vintage/page.header.html
@@ -25,7 +25,7 @@
25 <li><a href="?do=tools">Tools</a></li> 25 <li><a href="?do=tools">Tools</a></li>
26 <li><a href="?do=addlink">Add link</a></li> 26 <li><a href="?do=addlink">Add link</a></li>
27 {else} 27 {else}
28 <li><a href="?do=login">Login</a></li> 28 <li><a href="/login">Login</a></li>
29 {/if} 29 {/if}
30 <li><a href="{$feedurl}?do=rss{$searchcrits}" class="nomobile">RSS Feed</a></li> 30 <li><a href="{$feedurl}?do=rss{$searchcrits}" class="nomobile">RSS Feed</a></li>
31 {if="$showatom"} 31 {if="$showatom"}