Render login page through Slim controller (#1401)

Render login page through Slim controller
This commit is contained in:
ArthurHoaro 2020-01-26 11:41:10 +01:00 committed by GitHub
commit c653ae3bfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 926 additions and 131 deletions

View file

@ -159,7 +159,7 @@ function checkDateFormat($format, $string)
*/ */
function generateLocation($referer, $host, $loopTerms = array()) function generateLocation($referer, $host, $loopTerms = array())
{ {
$finalReferer = '?'; $finalReferer = './?';
// No referer if it contains any value in $loopCriteria. // No referer if it contains any value in $loopCriteria.
foreach (array_filter($loopTerms) as $value) { foreach (array_filter($loopTerms) as $value) {

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Shaarli\Container;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder;
use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager;
/**
* Class ContainerBuilder
*
* Helper used to build a Slim container instance with Shaarli's object dependencies.
* Note that most injected objects MUST be added as closures, to let the container instantiate
* only the objects it requires during the execution.
*
* @package Container
*/
class ContainerBuilder
{
/** @var ConfigManager */
protected $conf;
/** @var SessionManager */
protected $session;
/** @var LoginManager */
protected $login;
public function __construct(ConfigManager $conf, SessionManager $session, LoginManager $login)
{
$this->conf = $conf;
$this->session = $session;
$this->login = $login;
}
public function build(): ShaarliContainer
{
$container = new ShaarliContainer();
$container['conf'] = $this->conf;
$container['sessionManager'] = $this->session;
$container['loginManager'] = $this->login;
$container['plugins'] = function (ShaarliContainer $container): PluginManager {
return new PluginManager($container->conf);
};
$container['history'] = function (ShaarliContainer $container): History {
return new History($container->conf->get('resource.history'));
};
$container['bookmarkService'] = function (ShaarliContainer $container): BookmarkServiceInterface {
return new BookmarkFileService(
$container->conf,
$container->history,
$container->loginManager->isLoggedIn()
);
};
$container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
return new PageBuilder(
$container->conf,
$container->sessionManager->getSession(),
$container->bookmarkService,
$container->sessionManager->generateToken(),
$container->loginManager->isLoggedIn()
);
};
$container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
return new PluginManager($container->conf);
};
return $container;
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Shaarli\Container;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder;
use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager;
use Slim\Container;
/**
* Extension of Slim container to document the injected objects.
*
* @property ConfigManager $conf
* @property SessionManager $sessionManager
* @property LoginManager $loginManager
* @property History $history
* @property BookmarkServiceInterface $bookmarkService
* @property PageBuilder $pageBuilder
* @property PluginManager $pluginManager
*/
class ShaarliContainer extends Container
{
}

View file

@ -0,0 +1,57 @@
<?php
namespace Shaarli\Front;
use Shaarli\Container\ShaarliContainer;
use Shaarli\Front\Exception\ShaarliException;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Class ShaarliMiddleware
*
* This will be called before accessing any Shaarli controller.
*/
class ShaarliMiddleware
{
/** @var ShaarliContainer contains all Shaarli DI */
protected $container;
public function __construct(ShaarliContainer $container)
{
$this->container = $container;
}
/**
* Middleware execution:
* - execute the controller
* - return the response
*
* In case of error, the error template will be displayed with the exception message.
*
* @param Request $request Slim request
* @param Response $response Slim response
* @param callable $next Next action
*
* @return Response response.
*/
public function __invoke(Request $request, Response $response, callable $next)
{
try {
$response = $next($request, $response);
} catch (ShaarliException $e) {
$this->container->pageBuilder->assign('message', $e->getMessage());
if ($this->container->conf->get('dev.debug', false)) {
$this->container->pageBuilder->assign(
'stacktrace',
nl2br(get_class($this) .': '. $e->getTraceAsString())
);
}
$response = $response->withStatus($e->getCode());
$response = $response->write($this->container->pageBuilder->render('error'));
}
return $response;
}
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller;
use Shaarli\Front\Exception\LoginBannedException;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Class LoginController
*
* Slim controller used to render the login page.
*
* The login page is not available if the user is banned
* or if open shaarli setting is enabled.
*
* @package Front\Controller
*/
class LoginController extends ShaarliController
{
public function index(Request $request, Response $response): Response
{
if ($this->container->loginManager->isLoggedIn()
|| $this->container->conf->get('security.open_shaarli', false)
) {
return $response->withRedirect('./');
}
$userCanLogin = $this->container->loginManager->canLogin($request->getServerParams());
if ($userCanLogin !== true) {
throw new LoginBannedException();
}
if ($request->getParam('username') !== null) {
$this->assignView('username', escape($request->getParam('username')));
}
$this
->assignView('returnurl', escape($request->getServerParam('HTTP_REFERER')))
->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli'))
;
return $response->write($this->render('loginform'));
}
}

View file

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller;
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Container\ShaarliContainer;
abstract class ShaarliController
{
/** @var ShaarliContainer */
protected $container;
/** @param ShaarliContainer $container Slim container (extended for attribute completion). */
public function __construct(ShaarliContainer $container)
{
$this->container = $container;
}
/**
* Assign variables to RainTPL template through the PageBuilder.
*
* @param mixed $value Value to assign to the template
*/
protected function assignView(string $name, $value): self
{
$this->container->pageBuilder->assign($name, $value);
return $this;
}
protected function render(string $template): string
{
$this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL));
$this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE));
$this->assignView('plugin_errors', $this->container->pluginManager->getErrors());
$this->executeDefaultHooks($template);
return $this->container->pageBuilder->render($template);
}
/**
* Call plugin hooks for header, footer and includes, specifying which page will be rendered.
* Then assign generated data to RainTPL.
*/
protected function executeDefaultHooks(string $template): void
{
$common_hooks = [
'includes',
'header',
'footer',
];
foreach ($common_hooks as $name) {
$plugin_data = [];
$this->container->pluginManager->executeHooks(
'render_' . $name,
$plugin_data,
[
'target' => $template,
'loggedin' => $this->container->loginManager->isLoggedIn()
]
);
$this->assignView('plugins_' . $name, $plugin_data);
}
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Exception;
class LoginBannedException extends ShaarliException
{
public function __construct()
{
$message = t('You have been banned after too many failed login attempts. Try again later.');
parent::__construct($message, 401);
}
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Exception;
use Throwable;
/**
* Class ShaarliException
*
* Abstract exception class used to defined any custom exception thrown during front rendering.
*
* @package Front\Exception
*/
abstract class ShaarliException extends \Exception
{
/** Override parent constructor to force $message and $httpCode parameters to be set. */
public function __construct(string $message, int $httpCode, Throwable $previous = null)
{
parent::__construct($message, $httpCode, $previous);
}
}

View file

@ -199,6 +199,23 @@ public function renderPage($page)
$this->tpl->draw($page); $this->tpl->draw($page);
} }
/**
* Render a specific page as string (using a template file).
* e.g. $pb->render('picwall');
*
* @param string $page Template filename (without extension).
*
* @return string Processed template content
*/
public function render(string $page): string
{
if ($this->tpl === false) {
$this->initialize();
}
return $this->tpl->draw($page, true);
}
/** /**
* Render a 404 page (uses the template : tpl/404.tpl) * Render a 404 page (uses the template : tpl/404.tpl)
* usage: $PAGE->render404('The link was deleted') * usage: $PAGE->render404('The link was deleted')

View file

@ -196,4 +196,10 @@ public function hasClientIpChanged($clientIpId)
} }
return true; return true;
} }
/** @return array Local reference to the global $_SESSION array */
public function getSession(): array
{
return $this->session;
}
} }

View file

@ -1236,8 +1236,19 @@ form {
color: $dark-grey; color: $dark-grey;
} }
.page404-container { .page-error-container {
color: $dark-grey; color: $dark-grey;
h2 {
margin: 70px 0 25px;
}
pre {
margin: 0 20%;
padding: 20px 0;
text-align: left;
line-height: .7em;
}
} }
// EDIT LINK // EDIT LINK

View file

@ -48,9 +48,13 @@
"Shaarli\\Bookmark\\Exception\\": "application/bookmark/exception", "Shaarli\\Bookmark\\Exception\\": "application/bookmark/exception",
"Shaarli\\Config\\": "application/config/", "Shaarli\\Config\\": "application/config/",
"Shaarli\\Config\\Exception\\": "application/config/exception", "Shaarli\\Config\\Exception\\": "application/config/exception",
"Shaarli\\Container\\": "application/container",
"Shaarli\\Exceptions\\": "application/exceptions", "Shaarli\\Exceptions\\": "application/exceptions",
"Shaarli\\Feed\\": "application/feed", "Shaarli\\Feed\\": "application/feed",
"Shaarli\\Formatter\\": "application/formatter", "Shaarli\\Formatter\\": "application/formatter",
"Shaarli\\Front\\": "application/front",
"Shaarli\\Front\\Controller\\": "application/front/controllers",
"Shaarli\\Front\\Exception\\": "application/front/exceptions",
"Shaarli\\Http\\": "application/http", "Shaarli\\Http\\": "application/http",
"Shaarli\\Legacy\\": "application/legacy", "Shaarli\\Legacy\\": "application/legacy",
"Shaarli\\Netscape\\": "application/netscape", "Shaarli\\Netscape\\": "application/netscape",

View file

@ -7,8 +7,8 @@ Note that only the `default` theme supports translations.
### Contributing ### Contributing
We encourage the community to contribute to Shaarli's translation either by improving existing We encourage the community to contribute to Shaarli's translation either by improving existing
translations or submitting a new language. translations or submitting a new language.
Contributing to the translation does not require development skill. Contributing to the translation does not require development skill.
@ -21,8 +21,8 @@ First, install [Poedit](https://poedit.net/) tool.
Poedit will extract strings to translate from the PHP source code. Poedit will extract strings to translate from the PHP source code.
**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract **Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract
every translatable string. every translatable string.
You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended) You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended)
or visit every template page in your browser to generate cache files, while logged in. or visit every template page in your browser to generate cache files, while logged in.
@ -41,7 +41,7 @@ http://<replace_domain>/?do=daily
http://<replace_domain>/?post http://<replace_domain>/?post
http://<replace_domain>/?do=export http://<replace_domain>/?do=export
http://<replace_domain>/?do=import http://<replace_domain>/?do=import
http://<replace_domain>/?do=login http://<replace_domain>/login
http://<replace_domain>/?do=picwall http://<replace_domain>/?do=picwall
http://<replace_domain>/?do=pluginadmin http://<replace_domain>/?do=pluginadmin
http://<replace_domain>/?do=tagcloud http://<replace_domain>/?do=tagcloud
@ -50,8 +50,8 @@ http://<replace_domain>/?do=taglist
#### Improve existing translation #### Improve existing translation
In Poedit, click on "Edit a Translation", and from Shaarli's directory open In Poedit, click on "Edit a Translation", and from Shaarli's directory open
`inc/languages/<lang>/LC_MESSAGES/shaarli.po`. `inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
The existing list of translatable strings should have been loaded, then click on the "Update" button. The existing list of translatable strings should have been loaded, then click on the "Update" button.
@ -63,20 +63,20 @@ Save when you're done, then you can submit a pull request containing the updated
#### Add a new language #### Add a new language
Open Poedit and select "Create New Translation", then from Shaarli's directory open Open Poedit and select "Create New Translation", then from Shaarli's directory open
`inc/languages/<lang>/LC_MESSAGES/shaarli.po`. `inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
Then select the language you want to create. Then select the language you want to create.
Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`. Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`.
`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2) `<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2)
format in lowercase (e.g. `de` for German). format in lowercase (e.g. `de` for German).
Then click on the "Update" button, and you can start to translate every available string. Then click on the "Update" button, and you can start to translate every available string.
Save when you're done, then you can submit a pull request containing the new `shaarli.po`. Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
### Theme translations ### Theme translations
Theme translation extensions are loaded automatically if they're present. Theme translation extensions are loaded automatically if they're present.
@ -85,7 +85,7 @@ As a theme developer, all you have to do is to add the `.po` and `.mo` compiled
tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.po tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.po
tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.mo tpl/<theme name>/language/<lang>/LC_MESSAGES/<theme name>.mo
Where `<lang>` is the ISO 3166-1 alpha-2 language code. Where `<lang>` is the ISO 3166-1 alpha-2 language code.
Read the following section "Extend Shaarli's translation" to learn how to generate those files. Read the following section "Extend Shaarli's translation" to learn how to generate those files.
### Extend Shaarli's translation ### Extend Shaarli's translation
@ -106,7 +106,7 @@ First, create your translation files tree directory:
Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be
`my_theme.po`. `my_theme.po`.
Users have to register your extension in their configuration with the parameter Users have to register your extension in their configuration with the parameter
`translation.extensions.<domain>: <translation files path>`. `translation.extensions.<domain>: <translation files path>`.
Example: Example:
@ -151,11 +151,11 @@ When you're done, open Poedit and load translation strings from sources:
1. `File > New` 1. `File > New`
2. Choose your language 2. Choose your language
3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`. 3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`.
4. Go to `Catalog > Properties...` 4. Go to `Catalog > Properties...`
5. Fill the `Translation Properties` tab 5. Fill the `Translation Properties` tab
6. Add your source path in the `Sources Paths` tab 6. Add your source path in the `Sources Paths` tab
7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines: 7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines:
``` ```
my_theme_t my_theme_t
my_theme_t:1,2 my_theme_t:1,2

View file

@ -61,29 +61,31 @@
require_once 'application/TimeZone.php'; require_once 'application/TimeZone.php';
require_once 'application/Utils.php'; require_once 'application/Utils.php';
use \Shaarli\ApplicationUtils; use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\BookmarkServiceInterface;
use \Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkFileService;
use \Shaarli\Config\ConfigManager; use Shaarli\Bookmark\BookmarkFilter;
use \Shaarli\Feed\CachedPage; use Shaarli\Bookmark\BookmarkServiceInterface;
use \Shaarli\Feed\FeedBuilder; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Config\ConfigManager;
use Shaarli\Container\ContainerBuilder;
use Shaarli\Feed\CachedPage;
use Shaarli\Feed\FeedBuilder;
use Shaarli\Formatter\BookmarkMarkdownFormatter; use Shaarli\Formatter\BookmarkMarkdownFormatter;
use Shaarli\Formatter\FormatterFactory; use Shaarli\Formatter\FormatterFactory;
use \Shaarli\History; use Shaarli\History;
use \Shaarli\Languages; use Shaarli\Languages;
use \Shaarli\Netscape\NetscapeBookmarkUtils; use Shaarli\Netscape\NetscapeBookmarkUtils;
use \Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use \Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
use \Shaarli\Render\ThemeUtils; use Shaarli\Render\ThemeUtils;
use \Shaarli\Router; use Shaarli\Router;
use \Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use \Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
use \Shaarli\Thumbnailer; use Shaarli\Thumbnailer;
use \Shaarli\Updater\Updater; use Shaarli\Updater\Updater;
use \Shaarli\Updater\UpdaterUtils; use Shaarli\Updater\UpdaterUtils;
use Slim\App;
// Ensure the PHP version is supported // Ensure the PHP version is supported
try { try {
@ -250,7 +252,7 @@ function isLoggedIn()
// Optional redirect after login: // Optional redirect after login:
if (isset($_GET['post'])) { if (isset($_GET['post'])) {
$uri = '?post='. urlencode($_GET['post']); $uri = './?post='. urlencode($_GET['post']);
foreach (array('description', 'source', 'title', 'tags') as $param) { foreach (array('description', 'source', 'title', 'tags') as $param) {
if (!empty($_GET[$param])) { if (!empty($_GET[$param])) {
$uri .= '&'.$param.'='.urlencode($_GET[$param]); $uri .= '&'.$param.'='.urlencode($_GET[$param]);
@ -261,22 +263,22 @@ function isLoggedIn()
} }
if (isset($_GET['edit_link'])) { if (isset($_GET['edit_link'])) {
header('Location: ?edit_link='. escape($_GET['edit_link'])); header('Location: ./?edit_link='. escape($_GET['edit_link']));
exit; exit;
} }
if (isset($_POST['returnurl'])) { if (isset($_POST['returnurl'])) {
// Prevent loops over login screen. // Prevent loops over login screen.
if (strpos($_POST['returnurl'], 'do=login') === false) { if (strpos($_POST['returnurl'], '/login') === false) {
header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST'])); header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
exit; exit;
} }
} }
header('Location: ?'); header('Location: ./?');
exit; exit;
} else { } else {
$loginManager->handleFailedLogin($_SERVER); $loginManager->handleFailedLogin($_SERVER);
$redir = '&username='. urlencode($_POST['login']); $redir = '?username='. urlencode($_POST['login']);
if (isset($_GET['post'])) { if (isset($_GET['post'])) {
$redir .= '&post=' . urlencode($_GET['post']); $redir .= '&post=' . urlencode($_GET['post']);
foreach (array('description', 'source', 'title', 'tags') as $param) { foreach (array('description', 'source', 'title', 'tags') as $param) {
@ -286,7 +288,7 @@ function isLoggedIn()
} }
} }
// Redirect to login screen. // Redirect to login screen.
echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>'; echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'./login'.$redir.'\';</script>';
exit; exit;
} }
} }
@ -594,19 +596,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
// -------- Display login form. // -------- Display login form.
if ($targetPage == Router::$PAGE_LOGIN) { if ($targetPage == Router::$PAGE_LOGIN) {
if ($conf->get('security.open_shaarli')) { header('Location: ./login');
header('Location: ?');
exit;
} // No need to login for open Shaarli
if (isset($_GET['username'])) {
$PAGE->assign('username', escape($_GET['username']));
}
$PAGE->assign('returnurl', (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):''));
// add default state of the 'remember me' checkbox
$PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default'));
$PAGE->assign('user_can_login', $loginManager->canLogin($_SERVER));
$PAGE->assign('pagetitle', t('Login') .' - '. $conf->get('general.title', 'Shaarli'));
$PAGE->renderPage('loginform');
exit; exit;
} }
// -------- User wants to logout. // -------- User wants to logout.
@ -933,7 +923,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
// Show login screen, then redirect to ?post=... // Show login screen, then redirect to ?post=...
if (isset($_GET['post'])) { if (isset($_GET['post'])) {
header( // Redirect to login page, then back to post link. header( // Redirect to login page, then back to post link.
'Location: ?do=login&post='.urlencode($_GET['post']). 'Location: /login?post='.urlencode($_GET['post']).
(!empty($_GET['title'])?'&title='.urlencode($_GET['title']):''). (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
(!empty($_GET['description'])?'&description='.urlencode($_GET['description']):''). (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
(!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):''). (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
@ -944,7 +934,7 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager); showLinkList($PAGE, $bookmarkService, $conf, $pluginManager, $loginManager);
if (isset($_GET['edit_link'])) { if (isset($_GET['edit_link'])) {
header('Location: ?do=login&edit_link='. escape($_GET['edit_link'])); header('Location: /login?edit_link='. escape($_GET['edit_link']));
exit; exit;
} }
@ -1900,7 +1890,7 @@ function install($conf, $sessionManager, $loginManager)
echo '<script>alert(' echo '<script>alert('
.'"Shaarli is now configured. ' .'"Shaarli is now configured. '
.'Please enter your login/password and start shaaring your bookmarks!"' .'Please enter your login/password and start shaaring your bookmarks!"'
.');document.location=\'?do=login\';</script>'; .');document.location=\'./login\';</script>';
exit; exit;
} }
@ -1930,11 +1920,9 @@ function install($conf, $sessionManager, $loginManager)
exit; exit;
} }
$container = new \Slim\Container(); $containerBuilder = new ContainerBuilder($conf, $sessionManager, $loginManager);
$container['conf'] = $conf; $container = $containerBuilder->build();
$container['plugins'] = $pluginManager; $app = new App($container);
$container['history'] = $history;
$app = new \Slim\App($container);
// REST API routes // REST API routes
$app->group('/api/v1', function () { $app->group('/api/v1', function () {
@ -1953,6 +1941,10 @@ function install($conf, $sessionManager, $loginManager)
$this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory'); $this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory');
})->add('\Shaarli\Api\ApiMiddleware'); })->add('\Shaarli\Api\ApiMiddleware');
$app->group('', function () {
$this->get('/login', '\Shaarli\Front\Controller\LoginController:index')->setName('login');
})->add('\Shaarli\Front\ShaarliMiddleware');
$response = $app->run(true); $response = $app->run(true);
// Hack to make Slim and Shaarli router work together: // Hack to make Slim and Shaarli router work together:

View file

@ -203,7 +203,7 @@ public function testGenerateLocation()
public function testGenerateLocationLoop() public function testGenerateLocationLoop()
{ {
$ref = 'http://localhost/?test'; $ref = 'http://localhost/?test';
$this->assertEquals('?', generateLocation($ref, 'localhost', array('test'))); $this->assertEquals('./?', generateLocation($ref, 'localhost', array('test')));
} }
/** /**
@ -212,7 +212,7 @@ public function testGenerateLocationLoop()
public function testGenerateLocationOut() public function testGenerateLocationOut()
{ {
$ref = 'http://somewebsite.com/?test'; $ref = 'http://somewebsite.com/?test';
$this->assertEquals('?', generateLocation($ref, 'localhost')); $this->assertEquals('./?', generateLocation($ref, 'localhost'));
} }

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Shaarli\Container;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Render\PageBuilder;
use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager;
class ContainerBuilderTest extends TestCase
{
/** @var ConfigManager */
protected $conf;
/** @var SessionManager */
protected $sessionManager;
/** @var LoginManager */
protected $loginManager;
/** @var ContainerBuilder */
protected $containerBuilder;
public function setUp(): void
{
$this->conf = new ConfigManager('tests/utils/config/configJson');
$this->sessionManager = $this->createMock(SessionManager::class);
$this->loginManager = $this->createMock(LoginManager::class);
$this->containerBuilder = new ContainerBuilder($this->conf, $this->sessionManager, $this->loginManager);
}
public function testBuildContainer(): void
{
$container = $this->containerBuilder->build();
static::assertInstanceOf(ConfigManager::class, $container->conf);
static::assertInstanceOf(SessionManager::class, $container->sessionManager);
static::assertInstanceOf(LoginManager::class, $container->loginManager);
static::assertInstanceOf(History::class, $container->history);
static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService);
static::assertInstanceOf(PageBuilder::class, $container->pageBuilder);
}
}

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front;
use PHPUnit\Framework\TestCase;
use Shaarli\Config\ConfigManager;
use Shaarli\Container\ShaarliContainer;
use Shaarli\Front\Exception\LoginBannedException;
use Shaarli\Render\PageBuilder;
use Slim\Http\Request;
use Slim\Http\Response;
class ShaarliMiddlewareTest extends TestCase
{
/** @var ShaarliContainer */
protected $container;
/** @var ShaarliMiddleware */
protected $middleware;
public function setUp(): void
{
$this->container = $this->createMock(ShaarliContainer::class);
$this->middleware = new ShaarliMiddleware($this->container);
}
public function testMiddlewareExecution(): void
{
$request = $this->createMock(Request::class);
$response = new Response();
$controller = function (Request $request, Response $response): Response {
return $response->withStatus(418); // I'm a tea pot
};
/** @var Response $result */
$result = $this->middleware->__invoke($request, $response, $controller);
static::assertInstanceOf(Response::class, $result);
static::assertSame(418, $result->getStatusCode());
}
public function testMiddlewareExecutionWithException(): void
{
$request = $this->createMock(Request::class);
$response = new Response();
$controller = function (): void {
$exception = new LoginBannedException();
throw new $exception;
};
$pageBuilder = $this->createMock(PageBuilder::class);
$pageBuilder->method('render')->willReturnCallback(function (string $message): string {
return $message;
});
$this->container->pageBuilder = $pageBuilder;
$conf = $this->createMock(ConfigManager::class);
$this->container->conf = $conf;
/** @var Response $result */
$result = $this->middleware->__invoke($request, $response, $controller);
static::assertInstanceOf(Response::class, $result);
static::assertSame(401, $result->getStatusCode());
static::assertContains('error', (string) $result->getBody());
}
}

View file

@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\Container\ShaarliContainer;
use Shaarli\Front\Exception\LoginBannedException;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder;
use Shaarli\Security\LoginManager;
use Slim\Http\Request;
use Slim\Http\Response;
class LoginControllerTest extends TestCase
{
/** @var ShaarliContainer */
protected $container;
/** @var LoginController */
protected $controller;
public function setUp(): void
{
$this->container = $this->createMock(ShaarliContainer::class);
$this->controller = new LoginController($this->container);
}
public function testValidControllerInvoke(): void
{
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$request->expects(static::once())->method('getServerParam')->willReturn('> referer');
$response = new Response();
$assignedVariables = [];
$this->container->pageBuilder
->method('assign')
->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
$assignedVariables[$key] = $value;
return $this;
})
;
$result = $this->controller->index($request, $response);
static::assertInstanceOf(Response::class, $result);
static::assertSame(200, $result->getStatusCode());
static::assertSame('loginform', (string) $result->getBody());
static::assertSame('&gt; referer', $assignedVariables['returnurl']);
static::assertSame(true, $assignedVariables['remember_user_default']);
static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
}
public function testValidControllerInvokeWithUserName(): void
{
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$request->expects(static::once())->method('getServerParam')->willReturn('> referer');
$request->expects(static::exactly(2))->method('getParam')->willReturn('myUser>');
$response = new Response();
$assignedVariables = [];
$this->container->pageBuilder
->method('assign')
->willReturnCallback(function ($key, $value) use (&$assignedVariables) {
$assignedVariables[$key] = $value;
return $this;
})
;
$result = $this->controller->index($request, $response);
static::assertInstanceOf(Response::class, $result);
static::assertSame(200, $result->getStatusCode());
static::assertSame('loginform', (string) $result->getBody());
static::assertSame('myUser&gt;', $assignedVariables['username']);
static::assertSame('&gt; referer', $assignedVariables['returnurl']);
static::assertSame(true, $assignedVariables['remember_user_default']);
static::assertSame('Login - Shaarli', $assignedVariables['pagetitle']);
}
public function testLoginControllerWhileLoggedIn(): void
{
$request = $this->createMock(Request::class);
$response = new Response();
$loginManager = $this->createMock(LoginManager::class);
$loginManager->expects(static::once())->method('isLoggedIn')->willReturn(true);
$this->container->loginManager = $loginManager;
$result = $this->controller->index($request, $response);
static::assertInstanceOf(Response::class, $result);
static::assertSame(302, $result->getStatusCode());
static::assertSame(['./'], $result->getHeader('Location'));
}
public function testLoginControllerOpenShaarli(): void
{
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$response = new Response();
$conf = $this->createMock(ConfigManager::class);
$conf->method('get')->willReturnCallback(function (string $parameter, $default) {
if ($parameter === 'security.open_shaarli') {
return true;
}
return $default;
});
$this->container->conf = $conf;
$result = $this->controller->index($request, $response);
static::assertInstanceOf(Response::class, $result);
static::assertSame(302, $result->getStatusCode());
static::assertSame(['./'], $result->getHeader('Location'));
}
public function testLoginControllerWhileBanned(): void
{
$this->createValidContainerMockSet();
$request = $this->createMock(Request::class);
$response = new Response();
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('isLoggedIn')->willReturn(false);
$loginManager->method('canLogin')->willReturn(false);
$this->container->loginManager = $loginManager;
$this->expectException(LoginBannedException::class);
$this->controller->index($request, $response);
}
protected function createValidContainerMockSet(): void
{
// User logged out
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('isLoggedIn')->willReturn(false);
$loginManager->method('canLogin')->willReturn(true);
$this->container->loginManager = $loginManager;
// Config
$conf = $this->createMock(ConfigManager::class);
$conf->method('get')->willReturnCallback(function (string $parameter, $default) {
return $default;
});
$this->container->conf = $conf;
// PageBuilder
$pageBuilder = $this->createMock(PageBuilder::class);
$pageBuilder
->method('render')
->willReturnCallback(function (string $template): string {
return $template;
})
;
$this->container->pageBuilder = $pageBuilder;
$pluginManager = $this->createMock(PluginManager::class);
$this->container->pluginManager = $pluginManager;
$bookmarkService = $this->createMock(BookmarkServiceInterface::class);
$this->container->bookmarkService = $bookmarkService;
}
}

View file

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller;
use PHPUnit\Framework\TestCase;
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Container\ShaarliContainer;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder;
use Shaarli\Security\LoginManager;
/**
* Class ShaarliControllerTest
*
* This class is used to test default behavior of ShaarliController abstract class.
* It uses a dummy non abstract controller.
*/
class ShaarliControllerTest extends TestCase
{
/** @var ShaarliContainer */
protected $container;
/** @var LoginController */
protected $controller;
/** @var mixed[] List of variable assigned to the template */
protected $assignedValues;
public function setUp(): void
{
$this->container = $this->createMock(ShaarliContainer::class);
$this->controller = new class($this->container) extends ShaarliController
{
public function assignView(string $key, $value): ShaarliController
{
return parent::assignView($key, $value);
}
public function render(string $template): string
{
return parent::render($template);
}
};
$this->assignedValues = [];
}
public function testAssignView(): void
{
$this->createValidContainerMockSet();
$self = $this->controller->assignView('variableName', 'variableValue');
static::assertInstanceOf(ShaarliController::class, $self);
static::assertSame('variableValue', $this->assignedValues['variableName']);
}
public function testRender(): void
{
$this->createValidContainerMockSet();
$render = $this->controller->render('templateName');
static::assertSame('templateName', $render);
static::assertSame(10, $this->assignedValues['linkcount']);
static::assertSame(5, $this->assignedValues['privateLinkcount']);
static::assertSame(['error'], $this->assignedValues['plugin_errors']);
static::assertSame('templateName', $this->assignedValues['plugins_includes']['render_includes']['target']);
static::assertTrue($this->assignedValues['plugins_includes']['render_includes']['loggedin']);
static::assertSame('templateName', $this->assignedValues['plugins_header']['render_header']['target']);
static::assertTrue($this->assignedValues['plugins_header']['render_header']['loggedin']);
static::assertSame('templateName', $this->assignedValues['plugins_footer']['render_footer']['target']);
static::assertTrue($this->assignedValues['plugins_footer']['render_footer']['loggedin']);
}
protected function createValidContainerMockSet(): void
{
$pageBuilder = $this->createMock(PageBuilder::class);
$pageBuilder
->method('assign')
->willReturnCallback(function (string $key, $value): void {
$this->assignedValues[$key] = $value;
});
$pageBuilder
->method('render')
->willReturnCallback(function (string $template): string {
return $template;
});
$this->container->pageBuilder = $pageBuilder;
$bookmarkService = $this->createMock(BookmarkServiceInterface::class);
$bookmarkService
->method('count')
->willReturnCallback(function (string $visibility): int {
return $visibility === BookmarkFilter::$PRIVATE ? 5 : 10;
});
$this->container->bookmarkService = $bookmarkService;
$pluginManager = $this->createMock(PluginManager::class);
$pluginManager
->method('executeHooks')
->willReturnCallback(function (string $hook, array &$data, array $params): array {
return $data[$hook] = $params;
});
$pluginManager->method('getErrors')->willReturn(['error']);
$this->container->pluginManager = $pluginManager;
$loginManager = $this->createMock(LoginManager::class);
$loginManager->method('isLoggedIn')->willReturn(true);
$this->container->loginManager = $loginManager;
}
}

View file

@ -6,7 +6,7 @@
<body> <body>
<div id="pageheader"> <div id="pageheader">
{include="page.header"} {include="page.header"}
<div class="center" id="page404" class="page404-container"> <div id="pageError" class="page-error-container center">
<h2>{'Sorry, nothing to see here.'|t}</h2> <h2>{'Sorry, nothing to see here.'|t}</h2>
<img src="img/sad_star.png" alt=""> <img src="img/sad_star.png" alt="">
<p>{$error_message}</p> <p>{$error_message}</p>

22
tpl/default/error.html Normal file
View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
<head>
{include="includes"}
</head>
<body>
<div id="pageheader">
{include="page.header"}
<div id="pageError" class="page-error-container center">
<h2>{$message}</h2>
{if="!empty($stacktrace)"}
<pre>
{$stacktrace}
</pre>
{/if}
<img src="img/sad_star.png" alt="">
</div>
{include="page.footer"}
</body>
</html>

View file

@ -5,44 +5,32 @@
</head> </head>
<body> <body>
{include="page.header"} {include="page.header"}
{if="!$user_can_login"} <div class="pure-g">
<div class="pure-g pure-alert pure-alert-error pure-alert-closable center"> <div class="pure-u-lg-1-3 pure-u-1-24"></div>
<div class="pure-u-2-24"></div> <div id="login-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24 login-form-container">
<div class="pure-u-20-24"> <form method="post" name="loginform">
<p>{'You have been banned after too many failed login attempts. Try again later.'|t}</p> <h2 class="window-title">{'Login'|t}</h2>
</div> <div>
<div class="pure-u-2-24"> <input type="text" name="login" aria-label="{'Username'|t}" placeholder="{'Username'|t}"
<i class="fa fa-times pure-alert-close"></i> {if="!empty($username)"}value="{$username}"{/if} class="autofocus">
</div>
<div>
<input type="password" name="password" aria-label="{'Password'|t}" placeholder="{'Password'|t}" class="autofocus">
</div>
<div class="remember-me">
<input type="checkbox" name="longlastingsession" id="longlastingsessionform"
{if="$remember_user_default"}checked="checked"{/if}>
<label for="longlastingsessionform">{'Remember me'|t}</label>
</div>
<div>
<input type="submit" value="{'Login'|t}" class="bigbutton">
</div>
<input type="hidden" name="token" value="{$token}">
{if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
</form>
</div> </div>
<div class="pure-u-lg-1-3 pure-u-1-8"></div>
</div> </div>
{else}
<div class="pure-g">
<div class="pure-u-lg-1-3 pure-u-1-24"></div>
<div id="login-form" class="page-form page-form-light pure-u-lg-1-3 pure-u-22-24 login-form-container">
<form method="post" name="loginform">
<h2 class="window-title">{'Login'|t}</h2>
<div>
<input type="text" name="login" aria-label="{'Username'|t}" placeholder="{'Username'|t}"
{if="!empty($username)"}value="{$username}"{/if} class="autofocus">
</div>
<div>
<input type="password" name="password" aria-label="{'Password'|t}" placeholder="{'Password'|t}" class="autofocus">
</div>
<div class="remember-me">
<input type="checkbox" name="longlastingsession" id="longlastingsessionform"
{if="$remember_user_default"}checked="checked"{/if}>
<label for="longlastingsessionform">{'Remember me'|t}</label>
</div>
<div>
<input type="submit" value="{'Login'|t}" class="bigbutton">
</div>
<input type="hidden" name="token" value="{$token}">
{if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
</form>
</div>
<div class="pure-u-lg-1-3 pure-u-1-8"></div>
</div>
{/if}
{include="page.footer"} {include="page.footer"}
</body> </body>

View file

@ -60,7 +60,7 @@
</li> </li>
{else} {else}
<li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login"> <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-login">
<a href="?do=login" class="pure-menu-link">{'Login'|t}</a> <a href="/login" class="pure-menu-link">{'Login'|t}</a>
</li> </li>
{/if} {/if}
</ul> </ul>
@ -80,7 +80,7 @@
</li> </li>
{if="!$is_logged_in"} {if="!$is_logged_in"}
<li class="pure-menu-item" id="shaarli-menu-desktop-login"> <li class="pure-menu-item" id="shaarli-menu-desktop-login">
<a href="?do=login" class="pure-menu-link" <a href="/login" class="pure-menu-link"
data-open-id="header-login-form" data-open-id="header-login-form"
id="login-button" aria-label="{'Login'|t}" title="{'Login'|t}"> id="login-button" aria-label="{'Login'|t}" title="{'Login'|t}">
<i class="fa fa-user" aria-hidden="true"></i> <i class="fa fa-user" aria-hidden="true"></i>

25
tpl/vintage/error.html Normal file
View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
{include="includes"}
</head>
<body>
<div id="pageheader">
{include="page.header"}
</div>
<div class="error-container">
<h1>Error</h1>
<p>{$message}</p>
{if="!empty($stacktrace)"}
<br>
<pre>
{$stacktrace}
</pre>
{/if}
<p>Would you mind <a href="?">clicking here</a>?</p>
</div>
{include="page.footer"}
</body>
</html>

View file

@ -2,36 +2,30 @@
<html> <html>
<head>{include="includes"}</head> <head>{include="includes"}</head>
<body <body
{if="$user_can_login"} {if="empty($username)"}
{if="empty($username)"} onload="document.loginform.login.focus();"
onload="document.loginform.login.focus();" {else}
{else} onload="document.loginform.password.focus();"
onload="document.loginform.password.focus();"
{/if}
{/if}> {/if}>
<div id="pageheader"> <div id="pageheader">
{include="page.header"} {include="page.header"}
<div id="headerform"> <div id="headerform">
{if="!$user_can_login"} <form method="post" name="loginform">
You have been banned from login after too many failed attempts. Try later. <label for="login">Login: <input type="text" id="login" name="login" tabindex="1"
{else} {if="!empty($username)"}value="{$username}"{/if}>
<form method="post" name="loginform"> </label>
<label for="login">Login: <input type="text" id="login" name="login" tabindex="1" <label for="password">Password: <input type="password" id="password" name="password" tabindex="2">
{if="!empty($username)"}value="{$username}"{/if}> </label>
</label> <input type="submit" value="Login" class="bigbutton" tabindex="4">
<label for="password">Password: <input type="password" id="password" name="password" tabindex="2"> <label for="longlastingsession">
</label> <input type="checkbox" name="longlastingsession"
<input type="submit" value="Login" class="bigbutton" tabindex="4"> id="longlastingsession" tabindex="3"
<label for="longlastingsession"> {if="$remember_user_default"}checked="checked"{/if}>
<input type="checkbox" name="longlastingsession" Stay signed in (Do not check on public computers)</label>
id="longlastingsession" tabindex="3" <input type="hidden" name="token" value="{$token}">
{if="$remember_user_default"}checked="checked"{/if}> {if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
Stay signed in (Do not check on public computers)</label> </form>
<input type="hidden" name="token" value="{$token}">
{if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}
</form>
{/if}
</div> </div>
</div> </div>

View file

@ -25,7 +25,7 @@
<li><a href="?do=tools">Tools</a></li> <li><a href="?do=tools">Tools</a></li>
<li><a href="?do=addlink">Add link</a></li> <li><a href="?do=addlink">Add link</a></li>
{else} {else}
<li><a href="?do=login">Login</a></li> <li><a href="/login">Login</a></li>
{/if} {/if}
<li><a href="{$feedurl}?do=rss{$searchcrits}" class="nomobile">RSS Feed</a></li> <li><a href="{$feedurl}?do=rss{$searchcrits}" class="nomobile">RSS Feed</a></li>
{if="$showatom"} {if="$showatom"}