From c4ad3d4f061d05a01db25aa54dda830ba776792d Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Tue, 7 Jul 2020 10:15:56 +0200 Subject: [PATCH] Process Shaarli install through Slim controller --- application/bookmark/BookmarkFileService.php | 26 +- application/bookmark/BookmarkInitializer.php | 12 +- .../bookmark/BookmarkServiceInterface.php | 13 + application/container/ContainerBuilder.php | 7 + application/container/ShaarliContainer.php | 3 + application/front/ShaarliMiddleware.php | 6 + .../controller/admin/LogoutController.php | 10 +- .../controller/visitor/InstallController.php | 173 ++++++++++++ .../exceptions/AlreadyInstalledException.php | 15 + .../ResourcePermissionException.php | 13 + application/security/CookieManager.php | 33 +++ application/security/LoginManager.php | 16 +- application/security/SessionManager.php | 16 +- application/updater/Updater.php | 30 +- index.php | 153 +--------- tests/bootstrap.php | 11 +- tests/container/ContainerBuilderTest.php | 11 + tests/front/ShaarliMiddlewareTest.php | 12 + .../controller/admin/LogoutControllerTest.php | 18 +- .../visitor/InstallControllerTest.php | 264 ++++++++++++++++++ tests/render/PageCacheManagerTest.php | 5 +- tests/security/LoginManagerTest.php | 30 +- tests/security/SessionManagerTest.php | 14 +- tests/updater/UpdaterTest.php | 42 ++- tpl/default/error.html | 2 +- tpl/default/install.html | 2 +- tpl/default/page.header.html | 6 +- 27 files changed, 722 insertions(+), 221 deletions(-) create mode 100644 application/front/controller/visitor/InstallController.php create mode 100644 application/front/exceptions/AlreadyInstalledException.php create mode 100644 application/front/exceptions/ResourcePermissionException.php create mode 100644 application/security/CookieManager.php create mode 100644 tests/front/controller/visitor/InstallControllerTest.php diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 3d15d4c9..6e04f3b7 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -46,6 +46,9 @@ class BookmarkFileService implements BookmarkServiceInterface /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ protected $isLoggedIn; + /** @var bool Allow datastore alteration from not logged in users. */ + protected $anonymousPermission = false; + /** * @inheritDoc */ @@ -64,7 +67,7 @@ public function __construct(ConfigManager $conf, History $history, $isLoggedIn) $this->bookmarks = $this->bookmarksIO->read(); } catch (EmptyDataStoreException $e) { $this->bookmarks = new BookmarkArray(); - if ($isLoggedIn) { + if ($this->isLoggedIn) { $this->save(); } } @@ -154,7 +157,7 @@ public function get($id, $visibility = null) */ public function set($bookmark, $save = true) { - if ($this->isLoggedIn !== true) { + if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -179,7 +182,7 @@ public function set($bookmark, $save = true) */ public function add($bookmark, $save = true) { - if ($this->isLoggedIn !== true) { + if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -204,7 +207,7 @@ public function add($bookmark, $save = true) */ public function addOrSet($bookmark, $save = true) { - if ($this->isLoggedIn !== true) { + if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -221,7 +224,7 @@ public function addOrSet($bookmark, $save = true) */ public function remove($bookmark, $save = true) { - if ($this->isLoggedIn !== true) { + if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { throw new Exception(t('You\'re not authorized to alter the datastore')); } if (! $bookmark instanceof Bookmark) { @@ -274,10 +277,11 @@ public function count($visibility = null) */ public function save() { - if (!$this->isLoggedIn) { + if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { // TODO: raise an Exception instead die('You are not authorized to change the database.'); } + $this->bookmarks->reorder(); $this->bookmarksIO->write($this->bookmarks); $this->pageCacheManager->invalidateCaches(); @@ -357,6 +361,16 @@ public function initialize() $initializer->initialize(); } + public function enableAnonymousPermission(): void + { + $this->anonymousPermission = true; + } + + public function disableAnonymousPermission(): void + { + $this->anonymousPermission = false; + } + /** * Handles migration to the new database format (BookmarksArray). */ diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php index 9eee9a35..479ee9a9 100644 --- a/application/bookmark/BookmarkInitializer.php +++ b/application/bookmark/BookmarkInitializer.php @@ -34,13 +34,15 @@ public function __construct($bookmarkService) */ public function initialize() { + $this->bookmarkService->enableAnonymousPermission(); + $bookmark = new Bookmark(); $bookmark->setTitle(t('My secret stuff... - Pastebin.com')); - $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []); + $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8='); $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.')); $bookmark->setTagsString('secretstuff'); $bookmark->setPrivate(true); - $this->bookmarkService->add($bookmark); + $this->bookmarkService->add($bookmark, false); $bookmark = new Bookmark(); $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service')); @@ -54,6 +56,10 @@ public function initialize() You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' )); $bookmark->setTagsString('opensource software'); - $this->bookmarkService->add($bookmark); + $this->bookmarkService->add($bookmark, false); + + $this->bookmarkService->save(); + + $this->bookmarkService->disableAnonymousPermission(); } } diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 7b7a4f09..37fbda89 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -177,4 +177,17 @@ public function filterDay($request); * Creates the default database after a fresh install. */ public function initialize(); + + /** + * Allow to write the datastore from anonymous session (not logged in). + * + * This covers a few specific use cases, such as datastore initialization, + * but it should be used carefully as it can lead to security issues. + */ + public function enableAnonymousPermission(); + + /** + * Disable anonymous permission. + */ + public function disableAnonymousPermission(); } diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index ccb87c3a..593aafb7 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -15,6 +15,7 @@ use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; use Shaarli\Render\PageCacheManager; +use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Shaarli\Thumbnailer; @@ -38,6 +39,9 @@ class ContainerBuilder /** @var SessionManager */ protected $session; + /** @var CookieManager */ + protected $cookieManager; + /** @var LoginManager */ protected $login; @@ -47,11 +51,13 @@ class ContainerBuilder public function __construct( ConfigManager $conf, SessionManager $session, + CookieManager $cookieManager, LoginManager $login ) { $this->conf = $conf; $this->session = $session; $this->login = $login; + $this->cookieManager = $cookieManager; } public function build(): ShaarliContainer @@ -60,6 +66,7 @@ public function build(): ShaarliContainer $container['conf'] = $this->conf; $container['sessionManager'] = $this->session; + $container['cookieManager'] = $this->cookieManager; $container['loginManager'] = $this->login; $container['basePath'] = $this->basePath; diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php index 09e7d5b1..c4fe753e 100644 --- a/application/container/ShaarliContainer.php +++ b/application/container/ShaarliContainer.php @@ -4,6 +4,7 @@ namespace Shaarli\Container; +use http\Cookie; use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Config\ConfigManager; use Shaarli\Feed\FeedBuilder; @@ -14,6 +15,7 @@ use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; use Shaarli\Render\PageCacheManager; +use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Shaarli\Thumbnailer; @@ -25,6 +27,7 @@ * * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`) * @property BookmarkServiceInterface $bookmarkService + * @property CookieManager $cookieManager * @property ConfigManager $conf * @property mixed[] $environment $_SERVER automatically injected by Slim * @property callable $errorHandler Overrides default Slim error display diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php index baea6ef2..595182ac 100644 --- a/application/front/ShaarliMiddleware.php +++ b/application/front/ShaarliMiddleware.php @@ -43,6 +43,12 @@ public function __invoke(Request $request, Response $response, callable $next): $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); try { + if (!is_file($this->container->conf->getConfigFileExt()) + && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true) + ) { + return $response->withRedirect($this->container->basePath . '/install'); + } + $this->runUpdates(); $this->checkOpenShaarli($request, $response, $next); diff --git a/application/front/controller/admin/LogoutController.php b/application/front/controller/admin/LogoutController.php index c5984814..28165129 100644 --- a/application/front/controller/admin/LogoutController.php +++ b/application/front/controller/admin/LogoutController.php @@ -4,6 +4,7 @@ namespace Shaarli\Front\Controller\Admin; +use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Slim\Http\Request; use Slim\Http\Response; @@ -20,9 +21,12 @@ public function index(Request $request, Response $response): Response { $this->container->pageCacheManager->invalidateCaches(); $this->container->sessionManager->logout(); - - // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. - setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->basePath . '/'); + $this->container->cookieManager->setCookieParameter( + CookieManager::STAY_SIGNED_IN, + 'false', + 0, + $this->container->basePath . '/' + ); return $this->redirect($response, '/'); } diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php new file mode 100644 index 00000000..aa032860 --- /dev/null +++ b/application/front/controller/visitor/InstallController.php @@ -0,0 +1,173 @@ +container->conf->getConfigFileExt())) { + throw new AlreadyInstalledException(); + } + } + + /** + * Display the install template page. + * Also test file permissions and sessions beforehand. + */ + public function index(Request $request, Response $response): Response + { + // Before installation, we'll make sure that permissions are set properly, and sessions are working. + $this->checkPermissions(); + + if (static::SESSION_TEST_VALUE + !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) + ) { + $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE); + + return $this->redirect($response, '/install/session-test'); + } + + [$continents, $cities] = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); + + $this->assignView('continents', $continents); + $this->assignView('cities', $cities); + $this->assignView('languages', Languages::getAvailableLanguages()); + + return $response->write($this->render('install')); + } + + /** + * Route checking that the session parameter has been properly saved between two distinct requests. + * If the session parameter is preserved, redirect to install template page, otherwise displays error. + */ + public function sessionTest(Request $request, Response $response): Response + { + // This part makes sure sessions works correctly. + // (Because on some hosts, session.save_path may not be set correctly, + // or we may not have write access to it.) + if (static::SESSION_TEST_VALUE + !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) + ) { + // Step 2: Check if data in session is correct. + $msg = t( + '
Sessions do not seem to work correctly on your server.
'. + 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. + 'and that you have write access to it.
'. + 'It currently points to %s.
'. + 'On some browsers, accessing your server via a hostname like \'localhost\' '. + 'or any custom hostname without a dot causes cookie storage to fail. '. + 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.
' + ); + $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); + + $this->assignView('message', $msg); + + return $response->write($this->render('error')); + } + + return $this->redirect($response, '/install'); + } + + /** + * Save installation form and initialize config file and datastore if necessary. + */ + public function save(Request $request, Response $response): Response + { + $timezone = 'UTC'; + if (!empty($request->getParam('continent')) + && !empty($request->getParam('city')) + && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) + ) { + $timezone = $request->getParam('continent') . '/' . $request->getParam('city'); + } + $this->container->conf->set('general.timezone', $timezone); + + $login = $request->getParam('setlogin'); + $this->container->conf->set('credentials.login', $login); + $salt = sha1(uniqid('', true) .'_'. mt_rand()); + $this->container->conf->set('credentials.salt', $salt); + $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); + + if (!empty($request->getParam('title'))) { + $this->container->conf->set('general.title', escape($request->getParam('title'))); + } else { + $this->container->conf->set( + 'general.title', + 'Shared bookmarks on '.escape(index_url($this->container->environment)) + ); + } + + $this->container->conf->set('translation.language', escape($request->getParam('language'))); + $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck'))); + $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi'))); + $this->container->conf->set( + 'api.secret', + generate_api_secret( + $this->container->conf->get('credentials.login'), + $this->container->conf->get('credentials.salt') + ) + ); + + try { + // Everything is ok, let's create config file. + $this->container->conf->write($this->container->loginManager->isLoggedIn()); + } catch (\Exception $e) { + $this->assignView('message', $e->getMessage()); + $this->assignView('stacktrace', $e->getTraceAsString()); + + return $response->write($this->render('error')); + } + + if ($this->container->bookmarkService->count(BookmarkFilter::$ALL) === 0) { + $this->container->bookmarkService->initialize(); + } + + $this->container->sessionManager->setSessionParameter( + SessionManager::KEY_SUCCESS_MESSAGES, + [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')] + ); + + return $this->redirect($response, '/'); + } + + protected function checkPermissions(): bool + { + // Ensure Shaarli has proper access to its resources + $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); + + if (empty($errors)) { + return true; + } + + // FIXME! Do not insert HTML here. + $message = '

'. t('Insufficient permissions:') .'

'; + + throw new ResourcePermissionException($message); + } +} diff --git a/application/front/exceptions/AlreadyInstalledException.php b/application/front/exceptions/AlreadyInstalledException.php new file mode 100644 index 00000000..4add86cf --- /dev/null +++ b/application/front/exceptions/AlreadyInstalledException.php @@ -0,0 +1,15 @@ +cookies = $cookies; + } + + public function setCookieParameter(string $key, string $value, int $expires, string $path): self + { + $this->cookies[$key] = $value; + + setcookie($key, $value, $expires, $path); + + return $this; + } + + public function getCookieParameter(string $key, string $default = null): ?string + { + return $this->cookies[$key] ?? $default; + } +} diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index 39ec9b2e..d74c3118 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php @@ -9,9 +9,6 @@ */ class LoginManager { - /** @var string Name of the cookie set after logging in **/ - public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn'; - /** @var array A reference to the $_GLOBALS array */ protected $globals = []; @@ -32,17 +29,21 @@ class LoginManager /** @var string User sign-in token depending on remote IP and credentials */ protected $staySignedInToken = ''; + /** @var CookieManager */ + protected $cookieManager; /** * Constructor * * @param ConfigManager $configManager Configuration Manager instance * @param SessionManager $sessionManager SessionManager instance + * @param CookieManager $cookieManager CookieManager instance */ - public function __construct($configManager, $sessionManager) + public function __construct($configManager, $sessionManager, $cookieManager) { $this->configManager = $configManager; $this->sessionManager = $sessionManager; + $this->cookieManager = $cookieManager; $this->banManager = new BanManager( $this->configManager->get('security.trusted_proxies', []), $this->configManager->get('security.ban_after'), @@ -86,10 +87,9 @@ public function getStaySignedInToken() /** * Check user session state and validity (expiration) * - * @param array $cookie The $_COOKIE array * @param string $clientIpId Client IP address identifier */ - public function checkLoginState($cookie, $clientIpId) + public function checkLoginState($clientIpId) { if (! $this->configManager->exists('credentials.login')) { // Shaarli is not configured yet @@ -97,9 +97,7 @@ public function checkLoginState($cookie, $clientIpId) return; } - if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) - && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken - ) { + if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) { // The user client has a valid stay-signed-in cookie // Session information is updated with the current client information $this->sessionManager->storeLoginInfo($clientIpId); diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 0ac17d9a..82771c24 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php @@ -31,16 +31,21 @@ class SessionManager /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ protected $staySignedIn = false; + /** @var string */ + protected $savePath; + /** * Constructor * - * @param array $session The $_SESSION array (reference) - * @param ConfigManager $conf ConfigManager instance + * @param array $session The $_SESSION array (reference) + * @param ConfigManager $conf ConfigManager instance + * @param string $savePath Session save path returned by builtin function session_save_path() */ - public function __construct(& $session, $conf) + public function __construct(&$session, $conf, string $savePath) { $this->session = &$session; $this->conf = $conf; + $this->savePath = $savePath; } /** @@ -249,4 +254,9 @@ public function deleteSessionParameter(string $key): self return $this; } + + public function getSavePath(): string + { + return $this->savePath; + } } diff --git a/application/updater/Updater.php b/application/updater/Updater.php index f73a7452..4c578528 100644 --- a/application/updater/Updater.php +++ b/application/updater/Updater.php @@ -38,6 +38,11 @@ class Updater */ protected $methods; + /** + * @var string $basePath Shaarli root directory (from HTTP Request) + */ + protected $basePath = null; + /** * Object constructor. * @@ -62,11 +67,13 @@ public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn) * Run all new updates. * Update methods have to start with 'updateMethod' and return true (on success). * + * @param string $basePath Shaarli root directory (from HTTP Request) + * * @return array An array containing ran updates. * * @throws UpdaterException If something went wrong. */ - public function update() + public function update(string $basePath = null) { $updatesRan = []; @@ -123,16 +130,14 @@ public function writeUpdates(string $updatesFilepath, array $updates): void } /** - * With the Slim routing system, default header link should be `./` instead of `?`. - * Otherwise you can not go back to the home page. Example: `/picture-wall` -> `/picture-wall?` instead of `/`. + * With the Slim routing system, default header link should be `/subfolder/` instead of `?`. + * Otherwise you can not go back to the home page. + * Example: `/subfolder/picture-wall` -> `/subfolder/picture-wall?` instead of `/subfolder/`. */ public function updateMethodRelativeHomeLink(): bool { - $link = trim($this->conf->get('general.header_link')); - if ($link[0] === '?') { - $link = './'. ltrim($link, '?'); - - $this->conf->set('general.header_link', $link, true, true); + if ('?' === trim($this->conf->get('general.header_link'))) { + $this->conf->set('general.header_link', $this->basePath . '/', true, true); } return true; @@ -152,7 +157,7 @@ public function updateMethodMigrateExistingNotesUrl(): bool && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) ) { $updated = true; - $bookmark = $bookmark->setUrl('/shaare/' . $match[1]); + $bookmark = $bookmark->setUrl($this->basePath . '/shaare/' . $match[1]); $this->bookmarkService->set($bookmark, false); } @@ -164,4 +169,11 @@ public function updateMethodMigrateExistingNotesUrl(): bool return true; } + + public function setBasePath(string $basePath): self + { + $this->basePath = $basePath; + + return $this; + } } diff --git a/index.php b/index.php index 2737c22c..4627438e 100644 --- a/index.php +++ b/index.php @@ -61,13 +61,11 @@ require_once 'application/Utils.php'; use Shaarli\ApplicationUtils; -use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Config\ConfigManager; use Shaarli\Container\ContainerBuilder; -use Shaarli\History; use Shaarli\Languages; use Shaarli\Plugin\PluginManager; -use Shaarli\Render\PageBuilder; +use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Slim\App; @@ -118,13 +116,14 @@ // See all errors (for debugging only) error_reporting(-1); - set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) { + set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext) { throw new ErrorException($errstr, 0, $errno, $errfile, $errline); }); } -$sessionManager = new SessionManager($_SESSION, $conf); -$loginManager = new LoginManager($conf, $sessionManager); +$sessionManager = new SessionManager($_SESSION, $conf, session_save_path()); +$cookieManager = new CookieManager($_COOKIE); +$loginManager = new LoginManager($conf, $sessionManager, $cookieManager); $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); $clientIpId = client_ip_id($_SERVER); @@ -158,28 +157,7 @@ header("Cache-Control: post-check=0, pre-check=0", false); header("Pragma: no-cache"); -if (! is_file($conf->getConfigFileExt())) { - // Ensure Shaarli has proper access to its resources - $errors = ApplicationUtils::checkResourcePermissions($conf); - - if ($errors != array()) { - $message = '

'. t('Insufficient permissions:') .'

'; - - header('Content-Type: text/html; charset=utf-8'); - echo $message; - exit; - } - - // Display the installation form if no existing config is found - install($conf, $sessionManager, $loginManager); -} - -$loginManager->checkLoginState($_COOKIE, $clientIpId); +$loginManager->checkLoginState($clientIpId); // ------------------------------------------------------------------------------------------ // Process login form: Check if login/password is correct. @@ -205,7 +183,7 @@ $expirationTime = $sessionManager->extendSession(); setcookie( - $loginManager::$STAY_SIGNED_IN_COOKIE, + CookieManager::STAY_SIGNED_IN, $loginManager->getStaySignedInToken(), $expirationTime, WEB_PATH @@ -271,122 +249,11 @@ $_SESSION['tokens']=array(); // Token are attached to the session. } -/** - * Installation - * This function should NEVER be called if the file data/config.php exists. - * - * @param ConfigManager $conf Configuration Manager instance. - * @param SessionManager $sessionManager SessionManager instance - * @param LoginManager $loginManager LoginManager instance - */ -function install($conf, $sessionManager, $loginManager) -{ - // On free.fr host, make sure the /sessions directory exists, otherwise login will not work. - if (endsWith($_SERVER['HTTP_HOST'], '.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) { - mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions', 0705); - } - - - // This part makes sure sessions works correctly. - // (Because on some hosts, session.save_path may not be set correctly, - // or we may not have write access to it.) - if (isset($_GET['test_session']) - && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) { - // Step 2: Check if data in session is correct. - $msg = t( - '
Sessions do not seem to work correctly on your server.
'. - 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. - 'and that you have write access to it.
'. - 'It currently points to %s.
'. - 'On some browsers, accessing your server via a hostname like \'localhost\' '. - 'or any custom hostname without a dot causes cookie storage to fail. '. - 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.
' - ); - $msg = sprintf($msg, session_save_path()); - echo $msg; - echo '
'. t('Click to try again.') .'
'; - die; - } - if (!isset($_SESSION['session_tested'])) { - // Step 1 : Try to store data in session and reload page. - $_SESSION['session_tested'] = 'Working'; // Try to set a variable in session. - header('Location: '.index_url($_SERVER).'?test_session'); // Redirect to check stored data. - } - if (isset($_GET['test_session'])) { - // Step 3: Sessions are OK. Remove test parameter from URL. - header('Location: '.index_url($_SERVER)); - } - - - if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) { - $tz = 'UTC'; - if (!empty($_POST['continent']) && !empty($_POST['city']) - && isTimeZoneValid($_POST['continent'], $_POST['city']) - ) { - $tz = $_POST['continent'].'/'.$_POST['city']; - } - $conf->set('general.timezone', $tz); - $login = $_POST['setlogin']; - $conf->set('credentials.login', $login); - $salt = sha1(uniqid('', true) .'_'. mt_rand()); - $conf->set('credentials.salt', $salt); - $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt)); - if (!empty($_POST['title'])) { - $conf->set('general.title', escape($_POST['title'])); - } else { - $conf->set('general.title', 'Shared bookmarks on '.escape(index_url($_SERVER))); - } - $conf->set('translation.language', escape($_POST['language'])); - $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); - $conf->set('api.enabled', !empty($_POST['enableApi'])); - $conf->set( - 'api.secret', - generate_api_secret( - $conf->get('credentials.login'), - $conf->get('credentials.salt') - ) - ); - try { - // Everything is ok, let's create config file. - $conf->write($loginManager->isLoggedIn()); - } catch (Exception $e) { - error_log( - 'ERROR while writing config file after installation.' . PHP_EOL . - $e->getMessage() - ); - - // TODO: do not handle exceptions/errors in JS. - echo ''; - exit; - } - - $history = new History($conf->get('resource.history')); - $bookmarkService = new BookmarkFileService($conf, $history, true); - if ($bookmarkService->count() === 0) { - $bookmarkService->initialize(); - } - - echo ''; - exit; - } - - $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken()); - list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); - $PAGE->assign('continents', $continents); - $PAGE->assign('cities', $cities); - $PAGE->assign('languages', Languages::getAvailableLanguages()); - $PAGE->renderPage('install'); - exit; -} - if (!isset($_SESSION['LINKS_PER_PAGE'])) { $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); } -$containerBuilder = new ContainerBuilder($conf, $sessionManager, $loginManager); +$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager); $container = $containerBuilder->build(); $app = new App($container); @@ -408,6 +275,10 @@ function install($conf, $sessionManager, $loginManager) })->add('\Shaarli\Api\ApiMiddleware'); $app->group('', function () { + $this->get('/install', '\Shaarli\Front\Controller\Visitor\InstallController:index')->setName('displayInstall'); + $this->get('/install/session-test', '\Shaarli\Front\Controller\Visitor\InstallController:sessionTest'); + $this->post('/install', '\Shaarli\Front\Controller\Visitor\InstallController:save')->setName('saveInstall'); + /* -- PUBLIC --*/ $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index'); $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink'); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 511698ff..d4ddedd5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -18,9 +18,14 @@ function is_iterable($var) require_once 'application/Utils.php'; require_once 'application/http/UrlUtils.php'; require_once 'application/http/HttpUtils.php'; -require_once 'tests/utils/ReferenceLinkDB.php'; -require_once 'tests/utils/ReferenceHistory.php'; -require_once 'tests/utils/FakeBookmarkService.php'; require_once 'tests/container/ShaarliTestContainer.php'; require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php'; require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php'; +require_once 'tests/updater/DummyUpdater.php'; +require_once 'tests/utils/FakeBookmarkService.php'; +require_once 'tests/utils/FakeConfigManager.php'; +require_once 'tests/utils/ReferenceHistory.php'; +require_once 'tests/utils/ReferenceLinkDB.php'; +require_once 'tests/utils/ReferenceSessionIdHashes.php'; + +\ReferenceSessionIdHashes::genAllHashes(); diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php index db533f37..fa77bf31 100644 --- a/tests/container/ContainerBuilderTest.php +++ b/tests/container/ContainerBuilderTest.php @@ -11,12 +11,15 @@ use Shaarli\Formatter\FormatterFactory; use Shaarli\History; use Shaarli\Http\HttpAccess; +use Shaarli\Netscape\NetscapeBookmarkUtils; use Shaarli\Plugin\PluginManager; use Shaarli\Render\PageBuilder; use Shaarli\Render\PageCacheManager; +use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Shaarli\Thumbnailer; +use Shaarli\Updater\Updater; class ContainerBuilderTest extends TestCase { @@ -32,10 +35,14 @@ class ContainerBuilderTest extends TestCase /** @var ContainerBuilder */ protected $containerBuilder; + /** @var CookieManager */ + protected $cookieManager; + public function setUp(): void { $this->conf = new ConfigManager('tests/utils/config/configJson'); $this->sessionManager = $this->createMock(SessionManager::class); + $this->cookieManager = $this->createMock(CookieManager::class); $this->loginManager = $this->createMock(LoginManager::class); $this->loginManager->method('isLoggedIn')->willReturn(true); @@ -43,6 +50,7 @@ public function setUp(): void $this->containerBuilder = new ContainerBuilder( $this->conf, $this->sessionManager, + $this->cookieManager, $this->loginManager ); } @@ -53,6 +61,7 @@ public function testBuildContainer(): void static::assertInstanceOf(ConfigManager::class, $container->conf); static::assertInstanceOf(SessionManager::class, $container->sessionManager); + static::assertInstanceOf(CookieManager::class, $container->cookieManager); static::assertInstanceOf(LoginManager::class, $container->loginManager); static::assertInstanceOf(History::class, $container->history); static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService); @@ -63,6 +72,8 @@ public function testBuildContainer(): void static::assertInstanceOf(FeedBuilder::class, $container->feedBuilder); static::assertInstanceOf(Thumbnailer::class, $container->thumbnailer); static::assertInstanceOf(HttpAccess::class, $container->httpAccess); + static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils); + static::assertInstanceOf(Updater::class, $container->updater); // Set by the middleware static::assertNull($container->basePath); diff --git a/tests/front/ShaarliMiddlewareTest.php b/tests/front/ShaarliMiddlewareTest.php index 81ea1344..20090d8b 100644 --- a/tests/front/ShaarliMiddlewareTest.php +++ b/tests/front/ShaarliMiddlewareTest.php @@ -19,6 +19,8 @@ class ShaarliMiddlewareTest extends TestCase { + protected const TMP_MOCK_FILE = '.tmp'; + /** @var ShaarliContainer */ protected $container; @@ -29,12 +31,21 @@ public function setUp(): void { $this->container = $this->createMock(ShaarliContainer::class); + touch(static::TMP_MOCK_FILE); + $this->container->conf = $this->createMock(ConfigManager::class); + $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE); + $this->container->loginManager = $this->createMock(LoginManager::class); $this->middleware = new ShaarliMiddleware($this->container); } + public function tearDown() + { + unlink(static::TMP_MOCK_FILE); + } + /** * Test middleware execution with valid controller call */ @@ -179,6 +190,7 @@ public function testMiddlewareExecutionWithUpdates(): void $this->container->conf->method('get')->willReturnCallback(function (string $key): string { return $key; }); + $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE); $this->container->pageCacheManager = $this->createMock(PageCacheManager::class); $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches'); diff --git a/tests/front/controller/admin/LogoutControllerTest.php b/tests/front/controller/admin/LogoutControllerTest.php index ca177085..45e84dc0 100644 --- a/tests/front/controller/admin/LogoutControllerTest.php +++ b/tests/front/controller/admin/LogoutControllerTest.php @@ -4,14 +4,8 @@ namespace Shaarli\Front\Controller\Admin; -/** Override PHP builtin setcookie function in the local namespace to mock it... more or less */ -if (!function_exists('Shaarli\Front\Controller\Admin\setcookie')) { - function setcookie(string $name, string $value): void { - $_COOKIE[$name] = $value; - } -} - use PHPUnit\Framework\TestCase; +use Shaarli\Security\CookieManager; use Shaarli\Security\LoginManager; use Shaarli\Security\SessionManager; use Slim\Http\Request; @@ -29,8 +23,6 @@ public function setUp(): void $this->createContainer(); $this->controller = new LogoutController($this->container); - - setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, $cookie = 'hi there'); } public function testValidControllerInvoke(): void @@ -43,13 +35,17 @@ public function testValidControllerInvoke(): void $this->container->sessionManager = $this->createMock(SessionManager::class); $this->container->sessionManager->expects(static::once())->method('logout'); - static::assertSame('hi there', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]); + $this->container->cookieManager = $this->createMock(CookieManager::class); + $this->container->cookieManager + ->expects(static::once()) + ->method('setCookieParameter') + ->with(CookieManager::STAY_SIGNED_IN, 'false', 0, '/subfolder/') + ; $result = $this->controller->index($request, $response); static::assertInstanceOf(Response::class, $result); static::assertSame(302, $result->getStatusCode()); static::assertSame(['/subfolder/'], $result->getHeader('location')); - static::assertSame('false', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]); } } diff --git a/tests/front/controller/visitor/InstallControllerTest.php b/tests/front/controller/visitor/InstallControllerTest.php new file mode 100644 index 00000000..6871fdd9 --- /dev/null +++ b/tests/front/controller/visitor/InstallControllerTest.php @@ -0,0 +1,264 @@ +createContainer(); + + $this->container->conf = $this->createMock(ConfigManager::class); + $this->container->conf->method('getConfigFileExt')->willReturn(static::MOCK_FILE); + $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { + if ($key === 'resource.raintpl_tpl') { + return '.'; + } + + return $default ?? $key; + }); + + $this->controller = new InstallController($this->container); + } + + protected function tearDown(): void + { + if (file_exists(static::MOCK_FILE)) { + unlink(static::MOCK_FILE); + } + } + + /** + * Test displaying install page with valid session. + */ + public function testInstallIndexWithValidSession(): void + { + $assignedVariables = []; + $this->assignTemplateVars($assignedVariables); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->sessionManager = $this->createMock(SessionManager::class); + $this->container->sessionManager + ->method('getSessionParameter') + ->willReturnCallback(function (string $key, $default) { + return $key === 'session_tested' ? 'Working' : $default; + }) + ; + + $result = $this->controller->index($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('install', (string) $result->getBody()); + + static::assertIsArray($assignedVariables['continents']); + static::assertSame('Africa', $assignedVariables['continents'][0]); + static::assertSame('UTC', $assignedVariables['continents']['selected']); + + static::assertIsArray($assignedVariables['cities']); + static::assertSame(['continent' => 'Africa', 'city' => 'Abidjan'], $assignedVariables['cities'][0]); + static::assertSame('UTC', $assignedVariables['continents']['selected']); + + static::assertIsArray($assignedVariables['languages']); + static::assertSame('Automatic', $assignedVariables['languages']['auto']); + static::assertSame('French', $assignedVariables['languages']['fr']); + } + + /** + * Instantiate the install controller with an existing config file: exception. + */ + public function testInstallWithExistingConfigFile(): void + { + $this->expectException(AlreadyInstalledException::class); + + touch(static::MOCK_FILE); + + $this->controller = new InstallController($this->container); + } + + /** + * Call controller without session yet defined, redirect to test session install page. + */ + public function testInstallRedirectToSessionTest(): void + { + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->sessionManager = $this->createMock(SessionManager::class); + $this->container->sessionManager + ->expects(static::once()) + ->method('setSessionParameter') + ->with(InstallController::SESSION_TEST_KEY, InstallController::SESSION_TEST_VALUE) + ; + + $result = $this->controller->index($request, $response); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame('/subfolder/install/session-test', $result->getHeader('location')[0]); + } + + /** + * Call controller in session test mode: valid session then redirect to install page. + */ + public function testInstallSessionTestValid(): void + { + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->sessionManager = $this->createMock(SessionManager::class); + $this->container->sessionManager + ->method('getSessionParameter') + ->with(InstallController::SESSION_TEST_KEY) + ->willReturn(InstallController::SESSION_TEST_VALUE) + ; + + $result = $this->controller->sessionTest($request, $response); + + static::assertSame(302, $result->getStatusCode()); + static::assertSame('/subfolder/install', $result->getHeader('location')[0]); + } + + /** + * Call controller in session test mode: invalid session then redirect to error page. + */ + public function testInstallSessionTestError(): void + { + $assignedVars = []; + $this->assignTemplateVars($assignedVars); + + $request = $this->createMock(Request::class); + $response = new Response(); + + $this->container->sessionManager = $this->createMock(SessionManager::class); + $this->container->sessionManager + ->method('getSessionParameter') + ->with(InstallController::SESSION_TEST_KEY) + ->willReturn('KO') + ; + + $result = $this->controller->sessionTest($request, $response); + + static::assertSame(200, $result->getStatusCode()); + static::assertSame('error', (string) $result->getBody()); + static::assertStringStartsWith( + '
Sessions do not seem to work correctly on your server',
+            $assignedVars['message']
+        );
+    }
+
+    /**
+     * Test saving valid data from install form. Also initialize datastore.
+     */
+    public function testSaveInstallValid(): void
+    {
+        $providedParameters = [
+            'continent' => 'Europe',
+            'city' => 'Berlin',
+            'setlogin' => 'bob',
+            'setpassword' => 'password',
+            'title' => 'Shaarli',
+            'language' => 'fr',
+            'updateCheck' => true,
+            'enableApi' => true,
+        ];
+
+        $expectedSettings = [
+            'general.timezone' => 'Europe/Berlin',
+            'credentials.login' => 'bob',
+            'credentials.salt' => '_NOT_EMPTY',
+            'credentials.hash' => '_NOT_EMPTY',
+            'general.title' => 'Shaarli',
+            'translation.language' => 'en',
+            'updates.check_updates' => true,
+            'api.enabled' => true,
+            'api.secret' => '_NOT_EMPTY',
+        ];
+
+        $request = $this->createMock(Request::class);
+        $request->method('getParam')->willReturnCallback(function (string $key) use ($providedParameters) {
+            return $providedParameters[$key] ?? null;
+        });
+        $response = new Response();
+
+        $this->container->conf = $this->createMock(ConfigManager::class);
+        $this->container->conf
+            ->method('get')
+            ->willReturnCallback(function (string $key, $value) {
+                if ($key === 'credentials.login') {
+                    return 'bob';
+                } elseif ($key === 'credentials.salt') {
+                    return 'salt';
+                }
+
+                return $value;
+            })
+        ;
+        $this->container->conf
+            ->expects(static::exactly(count($expectedSettings)))
+            ->method('set')
+            ->willReturnCallback(function (string $key, $value) use ($expectedSettings) {
+                if ($expectedSettings[$key] ?? null === '_NOT_EMPTY') {
+                    static::assertNotEmpty($value);
+                } else {
+                    static::assertSame($expectedSettings[$key], $value);
+                }
+            })
+        ;
+        $this->container->conf->expects(static::once())->method('write');
+
+        $this->container->bookmarkService->expects(static::once())->method('count')->willReturn(0);
+        $this->container->bookmarkService->expects(static::once())->method('initialize');
+
+        $this->container->sessionManager
+            ->expects(static::once())
+            ->method('setSessionParameter')
+            ->with(SessionManager::KEY_SUCCESS_MESSAGES)
+        ;
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame('/subfolder/', $result->getHeader('location')[0]);
+    }
+
+    /**
+     * Test default settings (timezone and title).
+     * Also check that bookmarks are not initialized if
+     */
+    public function testSaveInstallDefaultValues(): void
+    {
+        $confSettings = [];
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->conf->method('set')->willReturnCallback(function (string $key, $value) use (&$confSettings) {
+            $confSettings[$key] = $value;
+        });
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame('/subfolder/', $result->getHeader('location')[0]);
+
+        static::assertSame('UTC', $confSettings['general.timezone']);
+        static::assertSame('Shared bookmarks on http://shaarli', $confSettings['general.title']);
+    }
+}
diff --git a/tests/render/PageCacheManagerTest.php b/tests/render/PageCacheManagerTest.php
index b870e6eb..c258f45f 100644
--- a/tests/render/PageCacheManagerTest.php
+++ b/tests/render/PageCacheManagerTest.php
@@ -1,15 +1,14 @@
 cookie = [];
         $this->session = [];
 
-        $this->sessionManager = new SessionManager($this->session, $this->configManager);
-        $this->loginManager = new LoginManager($this->configManager, $this->sessionManager);
+        $this->cookieManager = $this->createMock(CookieManager::class);
+        $this->cookieManager->method('getCookieParameter')->willReturnCallback(function (string $key) {
+            return $this->cookie[$key] ?? null;
+        });
+        $this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path');
+        $this->loginManager = new LoginManager($this->configManager, $this->sessionManager, $this->cookieManager);
         $this->server['REMOTE_ADDR'] = $this->ipAddr;
     }
 
@@ -193,8 +199,8 @@ public function testCheckLoginStateNotConfigured()
         $configManager = new \FakeConfigManager([
             'resource.ban_file' => $this->banFile,
         ]);
-        $loginManager = new LoginManager($configManager, null);
-        $loginManager->checkLoginState([], '');
+        $loginManager = new LoginManager($configManager, null, $this->cookieManager);
+        $loginManager->checkLoginState('');
 
         $this->assertFalse($loginManager->isLoggedIn());
     }
@@ -210,9 +216,9 @@ public function testCheckLoginStateStaySignedInWithInvalidToken()
             'expires_on' => time() + 100,
         ];
         $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
-        $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope';
+        $this->cookie[CookieManager::STAY_SIGNED_IN] = 'nope';
 
-        $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+        $this->loginManager->checkLoginState($this->clientIpAddress);
 
         $this->assertTrue($this->loginManager->isLoggedIn());
         $this->assertTrue(empty($this->session['username']));
@@ -224,9 +230,9 @@ public function testCheckLoginStateStaySignedInWithInvalidToken()
     public function testCheckLoginStateStaySignedInWithValidToken()
     {
         $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
-        $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken();
+        $this->cookie[CookieManager::STAY_SIGNED_IN] = $this->loginManager->getStaySignedInToken();
 
-        $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+        $this->loginManager->checkLoginState($this->clientIpAddress);
 
         $this->assertTrue($this->loginManager->isLoggedIn());
         $this->assertEquals($this->login, $this->session['username']);
@@ -241,7 +247,7 @@ public function testCheckLoginStateSessionExpired()
         $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
         $this->session['expires_on'] = time() - 100;
 
-        $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+        $this->loginManager->checkLoginState($this->clientIpAddress);
 
         $this->assertFalse($this->loginManager->isLoggedIn());
     }
@@ -253,7 +259,7 @@ public function testCheckLoginStateClientIpChanged()
     {
         $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
 
-        $this->loginManager->checkLoginState($this->cookie, '10.7.157.98');
+        $this->loginManager->checkLoginState('10.7.157.98');
 
         $this->assertFalse($this->loginManager->isLoggedIn());
     }
diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php
index d9db775e..60695dcf 100644
--- a/tests/security/SessionManagerTest.php
+++ b/tests/security/SessionManagerTest.php
@@ -1,12 +1,8 @@
 conf = new FakeConfigManager([
+        $this->conf = new \FakeConfigManager([
             'credentials.login' => 'johndoe',
             'credentials.salt' => 'salt',
             'security.session_protection_disabled' => false,
         ]);
         $this->session = [];
-        $this->sessionManager = new SessionManager($this->session, $this->conf);
+        $this->sessionManager = new SessionManager($this->session, $this->conf, 'session_path');
     }
 
     /**
@@ -69,7 +65,7 @@ public function testCheckToken()
                 $token => 1,
             ],
         ];
-        $sessionManager = new SessionManager($session, $this->conf);
+        $sessionManager = new SessionManager($session, $this->conf, 'session_path');
 
         // check and destroy the token
         $this->assertTrue($sessionManager->checkToken($token));
diff --git a/tests/updater/UpdaterTest.php b/tests/updater/UpdaterTest.php
index afc35aec..c801d451 100644
--- a/tests/updater/UpdaterTest.php
+++ b/tests/updater/UpdaterTest.php
@@ -7,9 +7,6 @@
 use Shaarli\Config\ConfigManager;
 use Shaarli\History;
 
-require_once 'tests/updater/DummyUpdater.php';
-require_once 'tests/utils/ReferenceLinkDB.php';
-require_once 'inc/rain.tpl.class.php';
 
 /**
  * Class UpdaterTest.
@@ -35,6 +32,9 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
     /** @var BookmarkServiceInterface */
     protected $bookmarkService;
 
+    /** @var \ReferenceLinkDB */
+    protected $refDB;
+
     /** @var Updater */
     protected $updater;
 
@@ -43,6 +43,9 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
      */
     public function setUp()
     {
+        $this->refDB = new \ReferenceLinkDB();
+        $this->refDB->write(self::$testDatastore);
+
         copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
         $this->conf = new ConfigManager(self::$configFile);
         $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true);
@@ -181,9 +184,40 @@ public function testUpdateFailed()
 
     public function testUpdateMethodRelativeHomeLinkRename(): void
     {
+        $this->updater->setBasePath('/subfolder');
         $this->conf->set('general.header_link', '?');
+
         $this->updater->updateMethodRelativeHomeLink();
 
-        static::assertSame();
+        static::assertSame('/subfolder/', $this->conf->get('general.header_link'));
+    }
+
+    public function testUpdateMethodRelativeHomeLinkDoNotRename(): void
+    {
+        $this->updater->setBasePath('/subfolder');
+        $this->conf->set('general.header_link', '~/my-blog');
+
+        $this->updater->updateMethodRelativeHomeLink();
+
+        static::assertSame('~/my-blog', $this->conf->get('general.header_link'));
+    }
+
+    public function testUpdateMethodMigrateExistingNotesUrl(): void
+    {
+        $this->updater->setBasePath('/subfolder');
+
+        $this->updater->updateMethodMigrateExistingNotesUrl();
+
+        static::assertSame($this->refDB->getLinks()[0]->getUrl(), $this->bookmarkService->get(0)->getUrl());
+        static::assertSame($this->refDB->getLinks()[1]->getUrl(), $this->bookmarkService->get(1)->getUrl());
+        static::assertSame($this->refDB->getLinks()[4]->getUrl(), $this->bookmarkService->get(4)->getUrl());
+        static::assertSame($this->refDB->getLinks()[6]->getUrl(), $this->bookmarkService->get(6)->getUrl());
+        static::assertSame($this->refDB->getLinks()[7]->getUrl(), $this->bookmarkService->get(7)->getUrl());
+        static::assertSame($this->refDB->getLinks()[8]->getUrl(), $this->bookmarkService->get(8)->getUrl());
+        static::assertSame($this->refDB->getLinks()[9]->getUrl(), $this->bookmarkService->get(9)->getUrl());
+        static::assertSame('/subfolder/shaare/WDWyig', $this->bookmarkService->get(42)->getUrl());
+        static::assertSame('/subfolder/shaare/WDWyig', $this->bookmarkService->get(41)->getUrl());
+        static::assertSame('/subfolder/shaare/0gCTjQ', $this->bookmarkService->get(10)->getUrl());
+        static::assertSame('/subfolder/shaare/PCRizQ', $this->bookmarkService->get(11)->getUrl());
     }
 }
diff --git a/tpl/default/error.html b/tpl/default/error.html
index 4abac9ca..c3e0c3c1 100644
--- a/tpl/default/error.html
+++ b/tpl/default/error.html
@@ -15,7 +15,7 @@ 

{$message}

{/if} - + {include="page.footer"} diff --git a/tpl/default/install.html b/tpl/default/install.html index 6f96c019..a506a2eb 100644 --- a/tpl/default/install.html +++ b/tpl/default/install.html @@ -10,7 +10,7 @@ {$ratioLabelMobile='7-8'} {$ratioInputMobile='1-8'} -
+
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html index 4fe2b69e..e4c7d91a 100644 --- a/tpl/default/page.header.html +++ b/tpl/default/page.header.html @@ -184,7 +184,7 @@
{/if} -{if="!empty($global_errors) && $is_logged_in"} +{if="!empty($global_errors)"}
@@ -198,7 +198,7 @@
{/if} -{if="!empty($global_warnings) && $is_logged_in"} +{if="!empty($global_warnings)"}
@@ -212,7 +212,7 @@
{/if} -{if="!empty($global_successes) && $is_logged_in"} +{if="!empty($global_successes)"}