diff options
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 | */ |
160 | function generateLocation($referer, $host, $loopTerms = array()) | 160 | function 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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Container; | ||
6 | |||
7 | use Shaarli\Bookmark\BookmarkFileService; | ||
8 | use Shaarli\Bookmark\BookmarkServiceInterface; | ||
9 | use Shaarli\Config\ConfigManager; | ||
10 | use Shaarli\History; | ||
11 | use Shaarli\Plugin\PluginManager; | ||
12 | use Shaarli\Render\PageBuilder; | ||
13 | use Shaarli\Security\LoginManager; | ||
14 | use 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 | */ | ||
25 | class 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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Container; | ||
6 | |||
7 | use Shaarli\Bookmark\BookmarkServiceInterface; | ||
8 | use Shaarli\Config\ConfigManager; | ||
9 | use Shaarli\History; | ||
10 | use Shaarli\Plugin\PluginManager; | ||
11 | use Shaarli\Render\PageBuilder; | ||
12 | use Shaarli\Security\LoginManager; | ||
13 | use Shaarli\Security\SessionManager; | ||
14 | use 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 | */ | ||
27 | class 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 | |||
3 | namespace Shaarli\Front; | ||
4 | |||
5 | use Shaarli\Container\ShaarliContainer; | ||
6 | use Shaarli\Front\Exception\ShaarliException; | ||
7 | use Slim\Http\Request; | ||
8 | use Slim\Http\Response; | ||
9 | |||
10 | /** | ||
11 | * Class ShaarliMiddleware | ||
12 | * | ||
13 | * This will be called before accessing any Shaarli controller. | ||
14 | */ | ||
15 | class 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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller; | ||
6 | |||
7 | use Shaarli\Front\Exception\LoginBannedException; | ||
8 | use Slim\Http\Request; | ||
9 | use 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 | */ | ||
21 | class 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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller; | ||
6 | |||
7 | use Shaarli\Bookmark\BookmarkFilter; | ||
8 | use Shaarli\Container\ShaarliContainer; | ||
9 | |||
10 | abstract 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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Exception; | ||
6 | |||
7 | class 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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Exception; | ||
6 | |||
7 | use 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 | */ | ||
16 | abstract 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 | ||
10 | We encourage the community to contribute to Shaarli's translation either by improving existing | 10 | We encourage the community to contribute to Shaarli's translation either by improving existing |
11 | translations or submitting a new language. | 11 | translations or submitting a new language. |
12 | 12 | ||
13 | Contributing to the translation does not require development skill. | 13 | Contributing to the translation does not require development skill. |
14 | 14 | ||
@@ -21,8 +21,8 @@ First, install [Poedit](https://poedit.net/) tool. | |||
21 | 21 | ||
22 | Poedit will extract strings to translate from the PHP source code. | 22 | Poedit 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 |
25 | every translatable string. | 25 | every translatable string. |
26 | 26 | ||
27 | You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended) | 27 | You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended) |
28 | or visit every template page in your browser to generate cache files, while logged in. | 28 | or visit every template page in your browser to generate cache files, while logged in. |
@@ -41,7 +41,7 @@ http://<replace_domain>/?do=daily | |||
41 | http://<replace_domain>/?post | 41 | http://<replace_domain>/?post |
42 | http://<replace_domain>/?do=export | 42 | http://<replace_domain>/?do=export |
43 | http://<replace_domain>/?do=import | 43 | http://<replace_domain>/?do=import |
44 | http://<replace_domain>/?do=login | 44 | http://<replace_domain>/login |
45 | http://<replace_domain>/?do=picwall | 45 | http://<replace_domain>/?do=picwall |
46 | http://<replace_domain>/?do=pluginadmin | 46 | http://<replace_domain>/?do=pluginadmin |
47 | http://<replace_domain>/?do=tagcloud | 47 | http://<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 | ||
53 | In Poedit, click on "Edit a Translation", and from Shaarli's directory open | 53 | In 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 | ||
56 | The existing list of translatable strings should have been loaded, then click on the "Update" button. | 56 | The 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 | ||
66 | Open Poedit and select "Create New Translation", then from Shaarli's directory open | 66 | Open 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 | ||
69 | Then select the language you want to create. | 69 | Then select the language you want to create. |
70 | 70 | ||
71 | Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`. | 71 | Click 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) |
73 | format in lowercase (e.g. `de` for German). | 73 | format in lowercase (e.g. `de` for German). |
74 | 74 | ||
75 | Then click on the "Update" button, and you can start to translate every available string. | 75 | Then click on the "Update" button, and you can start to translate every available string. |
76 | 76 | ||
77 | Save when you're done, then you can submit a pull request containing the new `shaarli.po`. | 77 | Save 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 | ||
81 | Theme translation extensions are loaded automatically if they're present. | 81 | Theme 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 | ||
88 | Where `<lang>` is the ISO 3166-1 alpha-2 language code. | 88 | Where `<lang>` is the ISO 3166-1 alpha-2 language code. |
89 | Read the following section "Extend Shaarli's translation" to learn how to generate those files. | 89 | Read 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: | |||
106 | Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be | 106 | Your `.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 | ||
109 | Users have to register your extension in their configuration with the parameter | 109 | Users 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 | ||
112 | Example: | 112 | Example: |
@@ -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 | ``` |
160 | my_theme_t | 160 | my_theme_t |
161 | my_theme_t:1,2 | 161 | my_theme_t:1,2 |
@@ -61,29 +61,31 @@ require_once 'application/FileUtils.php'; | |||
61 | require_once 'application/TimeZone.php'; | 61 | require_once 'application/TimeZone.php'; |
62 | require_once 'application/Utils.php'; | 62 | require_once 'application/Utils.php'; |
63 | 63 | ||
64 | use \Shaarli\ApplicationUtils; | 64 | use Shaarli\ApplicationUtils; |
65 | use Shaarli\Bookmark\BookmarkServiceInterface; | ||
66 | use \Shaarli\Bookmark\Exception\BookmarkNotFoundException; | ||
67 | use Shaarli\Bookmark\Bookmark; | 65 | use Shaarli\Bookmark\Bookmark; |
68 | use Shaarli\Bookmark\BookmarkFilter; | ||
69 | use Shaarli\Bookmark\BookmarkFileService; | 66 | use Shaarli\Bookmark\BookmarkFileService; |
70 | use \Shaarli\Config\ConfigManager; | 67 | use Shaarli\Bookmark\BookmarkFilter; |
71 | use \Shaarli\Feed\CachedPage; | 68 | use Shaarli\Bookmark\BookmarkServiceInterface; |
72 | use \Shaarli\Feed\FeedBuilder; | 69 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
70 | use Shaarli\Config\ConfigManager; | ||
71 | use Shaarli\Container\ContainerBuilder; | ||
72 | use Shaarli\Feed\CachedPage; | ||
73 | use Shaarli\Feed\FeedBuilder; | ||
73 | use Shaarli\Formatter\BookmarkMarkdownFormatter; | 74 | use Shaarli\Formatter\BookmarkMarkdownFormatter; |
74 | use Shaarli\Formatter\FormatterFactory; | 75 | use Shaarli\Formatter\FormatterFactory; |
75 | use \Shaarli\History; | 76 | use Shaarli\History; |
76 | use \Shaarli\Languages; | 77 | use Shaarli\Languages; |
77 | use \Shaarli\Netscape\NetscapeBookmarkUtils; | 78 | use Shaarli\Netscape\NetscapeBookmarkUtils; |
78 | use \Shaarli\Plugin\PluginManager; | 79 | use Shaarli\Plugin\PluginManager; |
79 | use \Shaarli\Render\PageBuilder; | 80 | use Shaarli\Render\PageBuilder; |
80 | use \Shaarli\Render\ThemeUtils; | 81 | use Shaarli\Render\ThemeUtils; |
81 | use \Shaarli\Router; | 82 | use Shaarli\Router; |
82 | use \Shaarli\Security\LoginManager; | 83 | use Shaarli\Security\LoginManager; |
83 | use \Shaarli\Security\SessionManager; | 84 | use Shaarli\Security\SessionManager; |
84 | use \Shaarli\Thumbnailer; | 85 | use Shaarli\Thumbnailer; |
85 | use \Shaarli\Updater\Updater; | 86 | use Shaarli\Updater\Updater; |
86 | use \Shaarli\Updater\UpdaterUtils; | 87 | use Shaarli\Updater\UpdaterUtils; |
88 | use Slim\App; | ||
87 | 89 | ||
88 | // Ensure the PHP version is supported | 90 | // Ensure the PHP version is supported |
89 | try { | 91 | try { |
@@ -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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Container; | ||
6 | |||
7 | use PHPUnit\Framework\TestCase; | ||
8 | use Shaarli\Bookmark\BookmarkServiceInterface; | ||
9 | use Shaarli\Config\ConfigManager; | ||
10 | use Shaarli\History; | ||
11 | use Shaarli\Render\PageBuilder; | ||
12 | use Shaarli\Security\LoginManager; | ||
13 | use Shaarli\Security\SessionManager; | ||
14 | |||
15 | class 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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front; | ||
6 | |||
7 | use PHPUnit\Framework\TestCase; | ||
8 | use Shaarli\Config\ConfigManager; | ||
9 | use Shaarli\Container\ShaarliContainer; | ||
10 | use Shaarli\Front\Exception\LoginBannedException; | ||
11 | use Shaarli\Render\PageBuilder; | ||
12 | use Slim\Http\Request; | ||
13 | use Slim\Http\Response; | ||
14 | |||
15 | class 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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller; | ||
6 | |||
7 | use PHPUnit\Framework\TestCase; | ||
8 | use Shaarli\Bookmark\BookmarkServiceInterface; | ||
9 | use Shaarli\Config\ConfigManager; | ||
10 | use Shaarli\Container\ShaarliContainer; | ||
11 | use Shaarli\Front\Exception\LoginBannedException; | ||
12 | use Shaarli\Plugin\PluginManager; | ||
13 | use Shaarli\Render\PageBuilder; | ||
14 | use Shaarli\Security\LoginManager; | ||
15 | use Slim\Http\Request; | ||
16 | use Slim\Http\Response; | ||
17 | |||
18 | class 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('> 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>', $assignedVariables['username']); | ||
87 | static::assertSame('> 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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller; | ||
6 | |||
7 | use PHPUnit\Framework\TestCase; | ||
8 | use Shaarli\Bookmark\BookmarkFilter; | ||
9 | use Shaarli\Bookmark\BookmarkServiceInterface; | ||
10 | use Shaarli\Container\ShaarliContainer; | ||
11 | use Shaarli\Plugin\PluginManager; | ||
12 | use Shaarli\Render\PageBuilder; | ||
13 | use 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 | */ | ||
21 | class 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"} |