Process Shaarli install through Slim controller

This commit is contained in:
ArthurHoaro 2020-07-07 10:15:56 +02:00 committed by ArthurHoaro
parent 1a8ac737e5
commit c4ad3d4f06
27 changed files with 722 additions and 221 deletions

View file

@ -46,6 +46,9 @@ class BookmarkFileService implements BookmarkServiceInterface
/** @var bool true for logged in users. Default value to retrieve private bookmarks. */ /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
protected $isLoggedIn; protected $isLoggedIn;
/** @var bool Allow datastore alteration from not logged in users. */
protected $anonymousPermission = false;
/** /**
* @inheritDoc * @inheritDoc
*/ */
@ -64,7 +67,7 @@ public function __construct(ConfigManager $conf, History $history, $isLoggedIn)
$this->bookmarks = $this->bookmarksIO->read(); $this->bookmarks = $this->bookmarksIO->read();
} catch (EmptyDataStoreException $e) { } catch (EmptyDataStoreException $e) {
$this->bookmarks = new BookmarkArray(); $this->bookmarks = new BookmarkArray();
if ($isLoggedIn) { if ($this->isLoggedIn) {
$this->save(); $this->save();
} }
} }
@ -154,7 +157,7 @@ public function get($id, $visibility = null)
*/ */
public function set($bookmark, $save = true) 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')); throw new Exception(t('You\'re not authorized to alter the datastore'));
} }
if (! $bookmark instanceof Bookmark) { if (! $bookmark instanceof Bookmark) {
@ -179,7 +182,7 @@ public function set($bookmark, $save = true)
*/ */
public function add($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')); throw new Exception(t('You\'re not authorized to alter the datastore'));
} }
if (! $bookmark instanceof Bookmark) { if (! $bookmark instanceof Bookmark) {
@ -204,7 +207,7 @@ public function add($bookmark, $save = true)
*/ */
public function addOrSet($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')); throw new Exception(t('You\'re not authorized to alter the datastore'));
} }
if (! $bookmark instanceof Bookmark) { if (! $bookmark instanceof Bookmark) {
@ -221,7 +224,7 @@ public function addOrSet($bookmark, $save = true)
*/ */
public function remove($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')); throw new Exception(t('You\'re not authorized to alter the datastore'));
} }
if (! $bookmark instanceof Bookmark) { if (! $bookmark instanceof Bookmark) {
@ -274,10 +277,11 @@ public function count($visibility = null)
*/ */
public function save() public function save()
{ {
if (!$this->isLoggedIn) { if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) {
// TODO: raise an Exception instead // TODO: raise an Exception instead
die('You are not authorized to change the database.'); die('You are not authorized to change the database.');
} }
$this->bookmarks->reorder(); $this->bookmarks->reorder();
$this->bookmarksIO->write($this->bookmarks); $this->bookmarksIO->write($this->bookmarks);
$this->pageCacheManager->invalidateCaches(); $this->pageCacheManager->invalidateCaches();
@ -357,6 +361,16 @@ public function initialize()
$initializer->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). * Handles migration to the new database format (BookmarksArray).
*/ */

View file

@ -34,13 +34,15 @@ public function __construct($bookmarkService)
*/ */
public function initialize() public function initialize()
{ {
$this->bookmarkService->enableAnonymousPermission();
$bookmark = new Bookmark(); $bookmark = new Bookmark();
$bookmark->setTitle(t('My secret stuff... - Pastebin.com')); $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->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'));
$bookmark->setTagsString('secretstuff'); $bookmark->setTagsString('secretstuff');
$bookmark->setPrivate(true); $bookmark->setPrivate(true);
$this->bookmarkService->add($bookmark); $this->bookmarkService->add($bookmark, false);
$bookmark = new Bookmark(); $bookmark = new Bookmark();
$bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service')); $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.' You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
)); ));
$bookmark->setTagsString('opensource software'); $bookmark->setTagsString('opensource software');
$this->bookmarkService->add($bookmark); $this->bookmarkService->add($bookmark, false);
$this->bookmarkService->save();
$this->bookmarkService->disableAnonymousPermission();
} }
} }

View file

@ -177,4 +177,17 @@ public function filterDay($request);
* Creates the default database after a fresh install. * Creates the default database after a fresh install.
*/ */
public function initialize(); 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();
} }

View file

@ -15,6 +15,7 @@
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
use Shaarli\Render\PageCacheManager; use Shaarli\Render\PageCacheManager;
use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer; use Shaarli\Thumbnailer;
@ -38,6 +39,9 @@ class ContainerBuilder
/** @var SessionManager */ /** @var SessionManager */
protected $session; protected $session;
/** @var CookieManager */
protected $cookieManager;
/** @var LoginManager */ /** @var LoginManager */
protected $login; protected $login;
@ -47,11 +51,13 @@ class ContainerBuilder
public function __construct( public function __construct(
ConfigManager $conf, ConfigManager $conf,
SessionManager $session, SessionManager $session,
CookieManager $cookieManager,
LoginManager $login LoginManager $login
) { ) {
$this->conf = $conf; $this->conf = $conf;
$this->session = $session; $this->session = $session;
$this->login = $login; $this->login = $login;
$this->cookieManager = $cookieManager;
} }
public function build(): ShaarliContainer public function build(): ShaarliContainer
@ -60,6 +66,7 @@ public function build(): ShaarliContainer
$container['conf'] = $this->conf; $container['conf'] = $this->conf;
$container['sessionManager'] = $this->session; $container['sessionManager'] = $this->session;
$container['cookieManager'] = $this->cookieManager;
$container['loginManager'] = $this->login; $container['loginManager'] = $this->login;
$container['basePath'] = $this->basePath; $container['basePath'] = $this->basePath;

View file

@ -4,6 +4,7 @@
namespace Shaarli\Container; namespace Shaarli\Container;
use http\Cookie;
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Feed\FeedBuilder; use Shaarli\Feed\FeedBuilder;
@ -14,6 +15,7 @@
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
use Shaarli\Render\PageCacheManager; use Shaarli\Render\PageCacheManager;
use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer; use Shaarli\Thumbnailer;
@ -25,6 +27,7 @@
* *
* @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`) * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`)
* @property BookmarkServiceInterface $bookmarkService * @property BookmarkServiceInterface $bookmarkService
* @property CookieManager $cookieManager
* @property ConfigManager $conf * @property ConfigManager $conf
* @property mixed[] $environment $_SERVER automatically injected by Slim * @property mixed[] $environment $_SERVER automatically injected by Slim
* @property callable $errorHandler Overrides default Slim error display * @property callable $errorHandler Overrides default Slim error display

View file

@ -43,6 +43,12 @@ public function __invoke(Request $request, Response $response, callable $next):
$this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/');
try { 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->runUpdates();
$this->checkOpenShaarli($request, $response, $next); $this->checkOpenShaarli($request, $response, $next);

View file

@ -4,6 +4,7 @@
namespace Shaarli\Front\Controller\Admin; namespace Shaarli\Front\Controller\Admin;
use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Slim\Http\Request; use Slim\Http\Request;
use Slim\Http\Response; use Slim\Http\Response;
@ -20,9 +21,12 @@ public function index(Request $request, Response $response): Response
{ {
$this->container->pageCacheManager->invalidateCaches(); $this->container->pageCacheManager->invalidateCaches();
$this->container->sessionManager->logout(); $this->container->sessionManager->logout();
$this->container->cookieManager->setCookieParameter(
// TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. CookieManager::STAY_SIGNED_IN,
setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->basePath . '/'); 'false',
0,
$this->container->basePath . '/'
);
return $this->redirect($response, '/'); return $this->redirect($response, '/');
} }

View file

@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller\Visitor;
use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Container\ShaarliContainer;
use Shaarli\Front\Exception\AlreadyInstalledException;
use Shaarli\Front\Exception\ResourcePermissionException;
use Shaarli\Languages;
use Shaarli\Security\SessionManager;
use Slim\Http\Request;
use Slim\Http\Response;
/**
* Slim controller used to render install page, and create initial configuration file.
*/
class InstallController extends ShaarliVisitorController
{
public const SESSION_TEST_KEY = 'session_tested';
public const SESSION_TEST_VALUE = 'Working';
public function __construct(ShaarliContainer $container)
{
parent::__construct($container);
if (is_file($this->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(
'<pre>Sessions do not seem to work correctly on your server.<br>'.
'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
'and that you have write access to it.<br>'.
'It currently points to %s.<br>'.
'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.<br>'
);
$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 = '<p>'. t('Insufficient permissions:') .'</p><ul>';
foreach ($errors as $error) {
$message .= '<li>'.$error.'</li>';
}
$message .= '</ul>';
throw new ResourcePermissionException($message);
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Exception;
class AlreadyInstalledException extends ShaarliFrontException
{
public function __construct()
{
$message = t('Shaarli has already been installed. Login to edit the configuration.');
parent::__construct($message, 401);
}
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Exception;
class ResourcePermissionException extends ShaarliFrontException
{
public function __construct(string $message)
{
parent::__construct($message, 500);
}
}

View file

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Shaarli\Security;
class CookieManager
{
/** @var string Name of the cookie set after logging in **/
public const STAY_SIGNED_IN = 'shaarli_staySignedIn';
/** @var mixed $_COOKIE set by reference */
protected $cookies;
public function __construct(array &$cookies)
{
$this->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;
}
}

View file

@ -9,9 +9,6 @@
*/ */
class LoginManager 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 */ /** @var array A reference to the $_GLOBALS array */
protected $globals = []; protected $globals = [];
@ -32,17 +29,21 @@ class LoginManager
/** @var string User sign-in token depending on remote IP and credentials */ /** @var string User sign-in token depending on remote IP and credentials */
protected $staySignedInToken = ''; protected $staySignedInToken = '';
/** @var CookieManager */
protected $cookieManager;
/** /**
* Constructor * Constructor
* *
* @param ConfigManager $configManager Configuration Manager instance * @param ConfigManager $configManager Configuration Manager instance
* @param SessionManager $sessionManager SessionManager 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->configManager = $configManager;
$this->sessionManager = $sessionManager; $this->sessionManager = $sessionManager;
$this->cookieManager = $cookieManager;
$this->banManager = new BanManager( $this->banManager = new BanManager(
$this->configManager->get('security.trusted_proxies', []), $this->configManager->get('security.trusted_proxies', []),
$this->configManager->get('security.ban_after'), $this->configManager->get('security.ban_after'),
@ -86,10 +87,9 @@ public function getStaySignedInToken()
/** /**
* Check user session state and validity (expiration) * Check user session state and validity (expiration)
* *
* @param array $cookie The $_COOKIE array
* @param string $clientIpId Client IP address identifier * @param string $clientIpId Client IP address identifier
*/ */
public function checkLoginState($cookie, $clientIpId) public function checkLoginState($clientIpId)
{ {
if (! $this->configManager->exists('credentials.login')) { if (! $this->configManager->exists('credentials.login')) {
// Shaarli is not configured yet // Shaarli is not configured yet
@ -97,9 +97,7 @@ public function checkLoginState($cookie, $clientIpId)
return; return;
} }
if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) {
&& $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
) {
// The user client has a valid stay-signed-in cookie // The user client has a valid stay-signed-in cookie
// Session information is updated with the current client information // Session information is updated with the current client information
$this->sessionManager->storeLoginInfo($clientIpId); $this->sessionManager->storeLoginInfo($clientIpId);

View file

@ -31,16 +31,21 @@ class SessionManager
/** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
protected $staySignedIn = false; protected $staySignedIn = false;
/** @var string */
protected $savePath;
/** /**
* Constructor * Constructor
* *
* @param array $session The $_SESSION array (reference) * @param array $session The $_SESSION array (reference)
* @param ConfigManager $conf ConfigManager instance * @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->session = &$session;
$this->conf = $conf; $this->conf = $conf;
$this->savePath = $savePath;
} }
/** /**
@ -249,4 +254,9 @@ public function deleteSessionParameter(string $key): self
return $this; return $this;
} }
public function getSavePath(): string
{
return $this->savePath;
}
} }

View file

@ -38,6 +38,11 @@ class Updater
*/ */
protected $methods; protected $methods;
/**
* @var string $basePath Shaarli root directory (from HTTP Request)
*/
protected $basePath = null;
/** /**
* Object constructor. * Object constructor.
* *
@ -62,11 +67,13 @@ public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
* Run all new updates. * Run all new updates.
* Update methods have to start with 'updateMethod' and return true (on success). * 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. * @return array An array containing ran updates.
* *
* @throws UpdaterException If something went wrong. * @throws UpdaterException If something went wrong.
*/ */
public function update() public function update(string $basePath = null)
{ {
$updatesRan = []; $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 `?`. * With the Slim routing system, default header link should be `/subfolder/` instead of `?`.
* Otherwise you can not go back to the home page. Example: `/picture-wall` -> `/picture-wall?` 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 public function updateMethodRelativeHomeLink(): bool
{ {
$link = trim($this->conf->get('general.header_link')); if ('?' === trim($this->conf->get('general.header_link'))) {
if ($link[0] === '?') { $this->conf->set('general.header_link', $this->basePath . '/', true, true);
$link = './'. ltrim($link, '?');
$this->conf->set('general.header_link', $link, true, true);
} }
return true; return true;
@ -152,7 +157,7 @@ public function updateMethodMigrateExistingNotesUrl(): bool
&& 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match)
) { ) {
$updated = true; $updated = true;
$bookmark = $bookmark->setUrl('/shaare/' . $match[1]); $bookmark = $bookmark->setUrl($this->basePath . '/shaare/' . $match[1]);
$this->bookmarkService->set($bookmark, false); $this->bookmarkService->set($bookmark, false);
} }
@ -164,4 +169,11 @@ public function updateMethodMigrateExistingNotesUrl(): bool
return true; return true;
} }
public function setBasePath(string $basePath): self
{
$this->basePath = $basePath;
return $this;
}
} }

151
index.php
View file

@ -61,13 +61,11 @@
require_once 'application/Utils.php'; require_once 'application/Utils.php';
use Shaarli\ApplicationUtils; use Shaarli\ApplicationUtils;
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Container\ContainerBuilder; use Shaarli\Container\ContainerBuilder;
use Shaarli\History;
use Shaarli\Languages; use Shaarli\Languages;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
use Slim\App; use Slim\App;
@ -123,8 +121,9 @@
}); });
} }
$sessionManager = new SessionManager($_SESSION, $conf); $sessionManager = new SessionManager($_SESSION, $conf, session_save_path());
$loginManager = new LoginManager($conf, $sessionManager); $cookieManager = new CookieManager($_COOKIE);
$loginManager = new LoginManager($conf, $sessionManager, $cookieManager);
$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
$clientIpId = client_ip_id($_SERVER); $clientIpId = client_ip_id($_SERVER);
@ -158,28 +157,7 @@
header("Cache-Control: post-check=0, pre-check=0", false); header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache"); header("Pragma: no-cache");
if (! is_file($conf->getConfigFileExt())) { $loginManager->checkLoginState($clientIpId);
// Ensure Shaarli has proper access to its resources
$errors = ApplicationUtils::checkResourcePermissions($conf);
if ($errors != array()) {
$message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
foreach ($errors as $error) {
$message .= '<li>'.$error.'</li>';
}
$message .= '</ul>';
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);
// ------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------
// Process login form: Check if login/password is correct. // Process login form: Check if login/password is correct.
@ -205,7 +183,7 @@
$expirationTime = $sessionManager->extendSession(); $expirationTime = $sessionManager->extendSession();
setcookie( setcookie(
$loginManager::$STAY_SIGNED_IN_COOKIE, CookieManager::STAY_SIGNED_IN,
$loginManager->getStaySignedInToken(), $loginManager->getStaySignedInToken(),
$expirationTime, $expirationTime,
WEB_PATH WEB_PATH
@ -271,122 +249,11 @@
$_SESSION['tokens']=array(); // Token are attached to the session. $_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(
'<pre>Sessions do not seem to work correctly on your server.<br>'.
'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
'and that you have write access to it.<br>'.
'It currently points to %s.<br>'.
'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.<br>'
);
$msg = sprintf($msg, session_save_path());
echo $msg;
echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
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 '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>';
exit;
}
$history = new History($conf->get('resource.history'));
$bookmarkService = new BookmarkFileService($conf, $history, true);
if ($bookmarkService->count() === 0) {
$bookmarkService->initialize();
}
echo '<script>alert('
.'"Shaarli is now configured. '
.'Please enter your login/password and start shaaring your bookmarks!"'
.');document.location=\'./login\';</script>';
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'])) { if (!isset($_SESSION['LINKS_PER_PAGE'])) {
$_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); $_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(); $container = $containerBuilder->build();
$app = new App($container); $app = new App($container);
@ -408,6 +275,10 @@ function install($conf, $sessionManager, $loginManager)
})->add('\Shaarli\Api\ApiMiddleware'); })->add('\Shaarli\Api\ApiMiddleware');
$app->group('', function () { $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 --*/ /* -- PUBLIC --*/
$this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index'); $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index');
$this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink'); $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink');

View file

@ -18,9 +18,14 @@ function is_iterable($var)
require_once 'application/Utils.php'; require_once 'application/Utils.php';
require_once 'application/http/UrlUtils.php'; require_once 'application/http/UrlUtils.php';
require_once 'application/http/HttpUtils.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/container/ShaarliTestContainer.php';
require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php'; require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php';
require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.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();

View file

@ -11,12 +11,15 @@
use Shaarli\Formatter\FormatterFactory; use Shaarli\Formatter\FormatterFactory;
use Shaarli\History; use Shaarli\History;
use Shaarli\Http\HttpAccess; use Shaarli\Http\HttpAccess;
use Shaarli\Netscape\NetscapeBookmarkUtils;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageBuilder; use Shaarli\Render\PageBuilder;
use Shaarli\Render\PageCacheManager; use Shaarli\Render\PageCacheManager;
use Shaarli\Security\CookieManager;
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;
class ContainerBuilderTest extends TestCase class ContainerBuilderTest extends TestCase
{ {
@ -32,10 +35,14 @@ class ContainerBuilderTest extends TestCase
/** @var ContainerBuilder */ /** @var ContainerBuilder */
protected $containerBuilder; protected $containerBuilder;
/** @var CookieManager */
protected $cookieManager;
public function setUp(): void public function setUp(): void
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->sessionManager = $this->createMock(SessionManager::class); $this->sessionManager = $this->createMock(SessionManager::class);
$this->cookieManager = $this->createMock(CookieManager::class);
$this->loginManager = $this->createMock(LoginManager::class); $this->loginManager = $this->createMock(LoginManager::class);
$this->loginManager->method('isLoggedIn')->willReturn(true); $this->loginManager->method('isLoggedIn')->willReturn(true);
@ -43,6 +50,7 @@ public function setUp(): void
$this->containerBuilder = new ContainerBuilder( $this->containerBuilder = new ContainerBuilder(
$this->conf, $this->conf,
$this->sessionManager, $this->sessionManager,
$this->cookieManager,
$this->loginManager $this->loginManager
); );
} }
@ -53,6 +61,7 @@ public function testBuildContainer(): void
static::assertInstanceOf(ConfigManager::class, $container->conf); static::assertInstanceOf(ConfigManager::class, $container->conf);
static::assertInstanceOf(SessionManager::class, $container->sessionManager); static::assertInstanceOf(SessionManager::class, $container->sessionManager);
static::assertInstanceOf(CookieManager::class, $container->cookieManager);
static::assertInstanceOf(LoginManager::class, $container->loginManager); static::assertInstanceOf(LoginManager::class, $container->loginManager);
static::assertInstanceOf(History::class, $container->history); static::assertInstanceOf(History::class, $container->history);
static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService); static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService);
@ -63,6 +72,8 @@ public function testBuildContainer(): void
static::assertInstanceOf(FeedBuilder::class, $container->feedBuilder); static::assertInstanceOf(FeedBuilder::class, $container->feedBuilder);
static::assertInstanceOf(Thumbnailer::class, $container->thumbnailer); static::assertInstanceOf(Thumbnailer::class, $container->thumbnailer);
static::assertInstanceOf(HttpAccess::class, $container->httpAccess); static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
static::assertInstanceOf(Updater::class, $container->updater);
// Set by the middleware // Set by the middleware
static::assertNull($container->basePath); static::assertNull($container->basePath);

View file

@ -19,6 +19,8 @@
class ShaarliMiddlewareTest extends TestCase class ShaarliMiddlewareTest extends TestCase
{ {
protected const TMP_MOCK_FILE = '.tmp';
/** @var ShaarliContainer */ /** @var ShaarliContainer */
protected $container; protected $container;
@ -29,12 +31,21 @@ public function setUp(): void
{ {
$this->container = $this->createMock(ShaarliContainer::class); $this->container = $this->createMock(ShaarliContainer::class);
touch(static::TMP_MOCK_FILE);
$this->container->conf = $this->createMock(ConfigManager::class); $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->container->loginManager = $this->createMock(LoginManager::class);
$this->middleware = new ShaarliMiddleware($this->container); $this->middleware = new ShaarliMiddleware($this->container);
} }
public function tearDown()
{
unlink(static::TMP_MOCK_FILE);
}
/** /**
* Test middleware execution with valid controller call * 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 { $this->container->conf->method('get')->willReturnCallback(function (string $key): string {
return $key; return $key;
}); });
$this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
$this->container->pageCacheManager = $this->createMock(PageCacheManager::class); $this->container->pageCacheManager = $this->createMock(PageCacheManager::class);
$this->container->pageCacheManager->expects(static::once())->method('invalidateCaches'); $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches');

View file

@ -4,14 +4,8 @@
namespace Shaarli\Front\Controller\Admin; 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 PHPUnit\Framework\TestCase;
use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
use Slim\Http\Request; use Slim\Http\Request;
@ -29,8 +23,6 @@ public function setUp(): void
$this->createContainer(); $this->createContainer();
$this->controller = new LogoutController($this->container); $this->controller = new LogoutController($this->container);
setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, $cookie = 'hi there');
} }
public function testValidControllerInvoke(): void public function testValidControllerInvoke(): void
@ -43,13 +35,17 @@ public function testValidControllerInvoke(): void
$this->container->sessionManager = $this->createMock(SessionManager::class); $this->container->sessionManager = $this->createMock(SessionManager::class);
$this->container->sessionManager->expects(static::once())->method('logout'); $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); $result = $this->controller->index($request, $response);
static::assertInstanceOf(Response::class, $result); static::assertInstanceOf(Response::class, $result);
static::assertSame(302, $result->getStatusCode()); static::assertSame(302, $result->getStatusCode());
static::assertSame(['/subfolder/'], $result->getHeader('location')); static::assertSame(['/subfolder/'], $result->getHeader('location'));
static::assertSame('false', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]);
} }
} }

View file

@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
namespace Shaarli\Front\Controller\Visitor;
use PHPUnit\Framework\TestCase;
use Shaarli\Config\ConfigManager;
use Shaarli\Front\Exception\AlreadyInstalledException;
use Shaarli\Security\SessionManager;
use Slim\Http\Request;
use Slim\Http\Response;
class InstallControllerTest extends TestCase
{
use FrontControllerMockHelper;
const MOCK_FILE = '.tmp';
/** @var InstallController */
protected $controller;
public function setUp(): void
{
$this->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(
'<pre>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']);
}
}

View file

@ -1,15 +1,14 @@
<?php <?php
/** /**
* Cache tests * Cache tests
*/ */
namespace Shaarli\Render; namespace Shaarli\Render;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
// required to access $_SESSION array
session_start();
/** /**
* Unitary tests for cached pages * Unitary tests for cached pages
*/ */

View file

@ -1,7 +1,6 @@
<?php <?php
namespace Shaarli\Security;
require_once 'tests/utils/FakeConfigManager.php'; namespace Shaarli\Security;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -58,6 +57,9 @@ class LoginManagerTest extends TestCase
/** @var string Salt used by hash functions */ /** @var string Salt used by hash functions */
protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2'; protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2';
/** @var CookieManager */
protected $cookieManager;
/** /**
* Prepare or reset test resources * Prepare or reset test resources
*/ */
@ -84,8 +86,12 @@ public function setUp()
$this->cookie = []; $this->cookie = [];
$this->session = []; $this->session = [];
$this->sessionManager = new SessionManager($this->session, $this->configManager); $this->cookieManager = $this->createMock(CookieManager::class);
$this->loginManager = new LoginManager($this->configManager, $this->sessionManager); $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; $this->server['REMOTE_ADDR'] = $this->ipAddr;
} }
@ -193,8 +199,8 @@ public function testCheckLoginStateNotConfigured()
$configManager = new \FakeConfigManager([ $configManager = new \FakeConfigManager([
'resource.ban_file' => $this->banFile, 'resource.ban_file' => $this->banFile,
]); ]);
$loginManager = new LoginManager($configManager, null); $loginManager = new LoginManager($configManager, null, $this->cookieManager);
$loginManager->checkLoginState([], ''); $loginManager->checkLoginState('');
$this->assertFalse($loginManager->isLoggedIn()); $this->assertFalse($loginManager->isLoggedIn());
} }
@ -210,9 +216,9 @@ public function testCheckLoginStateStaySignedInWithInvalidToken()
'expires_on' => time() + 100, 'expires_on' => time() + 100,
]; ];
$this->loginManager->generateStaySignedInToken($this->clientIpAddress); $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($this->loginManager->isLoggedIn());
$this->assertTrue(empty($this->session['username'])); $this->assertTrue(empty($this->session['username']));
@ -224,9 +230,9 @@ public function testCheckLoginStateStaySignedInWithInvalidToken()
public function testCheckLoginStateStaySignedInWithValidToken() public function testCheckLoginStateStaySignedInWithValidToken()
{ {
$this->loginManager->generateStaySignedInToken($this->clientIpAddress); $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->assertTrue($this->loginManager->isLoggedIn());
$this->assertEquals($this->login, $this->session['username']); $this->assertEquals($this->login, $this->session['username']);
@ -241,7 +247,7 @@ public function testCheckLoginStateSessionExpired()
$this->loginManager->generateStaySignedInToken($this->clientIpAddress); $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
$this->session['expires_on'] = time() - 100; $this->session['expires_on'] = time() - 100;
$this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); $this->loginManager->checkLoginState($this->clientIpAddress);
$this->assertFalse($this->loginManager->isLoggedIn()); $this->assertFalse($this->loginManager->isLoggedIn());
} }
@ -253,7 +259,7 @@ public function testCheckLoginStateClientIpChanged()
{ {
$this->loginManager->generateStaySignedInToken($this->clientIpAddress); $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()); $this->assertFalse($this->loginManager->isLoggedIn());
} }

View file

@ -1,12 +1,8 @@
<?php <?php
require_once 'tests/utils/FakeConfigManager.php';
// Initialize reference data _before_ PHPUnit starts a session namespace Shaarli\Security;
require_once 'tests/utils/ReferenceSessionIdHashes.php';
ReferenceSessionIdHashes::genAllHashes();
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shaarli\Security\SessionManager;
/** /**
* Test coverage for SessionManager * Test coverage for SessionManager
@ -30,7 +26,7 @@ class SessionManagerTest extends TestCase
*/ */
public static function setUpBeforeClass() public static function setUpBeforeClass()
{ {
self::$sidHashes = ReferenceSessionIdHashes::getHashes(); self::$sidHashes = \ReferenceSessionIdHashes::getHashes();
} }
/** /**
@ -38,13 +34,13 @@ public static function setUpBeforeClass()
*/ */
public function setUp() public function setUp()
{ {
$this->conf = new FakeConfigManager([ $this->conf = new \FakeConfigManager([
'credentials.login' => 'johndoe', 'credentials.login' => 'johndoe',
'credentials.salt' => 'salt', 'credentials.salt' => 'salt',
'security.session_protection_disabled' => false, 'security.session_protection_disabled' => false,
]); ]);
$this->session = []; $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, $token => 1,
], ],
]; ];
$sessionManager = new SessionManager($session, $this->conf); $sessionManager = new SessionManager($session, $this->conf, 'session_path');
// check and destroy the token // check and destroy the token
$this->assertTrue($sessionManager->checkToken($token)); $this->assertTrue($sessionManager->checkToken($token));

View file

@ -7,9 +7,6 @@
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
require_once 'tests/updater/DummyUpdater.php';
require_once 'tests/utils/ReferenceLinkDB.php';
require_once 'inc/rain.tpl.class.php';
/** /**
* Class UpdaterTest. * Class UpdaterTest.
@ -35,6 +32,9 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
/** @var BookmarkServiceInterface */ /** @var BookmarkServiceInterface */
protected $bookmarkService; protected $bookmarkService;
/** @var \ReferenceLinkDB */
protected $refDB;
/** @var Updater */ /** @var Updater */
protected $updater; protected $updater;
@ -43,6 +43,9 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
*/ */
public function setUp() public function setUp()
{ {
$this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore);
copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
$this->conf = new ConfigManager(self::$configFile); $this->conf = new ConfigManager(self::$configFile);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true); $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true);
@ -181,9 +184,40 @@ public function testUpdateFailed()
public function testUpdateMethodRelativeHomeLinkRename(): void public function testUpdateMethodRelativeHomeLinkRename(): void
{ {
$this->updater->setBasePath('/subfolder');
$this->conf->set('general.header_link', '?'); $this->conf->set('general.header_link', '?');
$this->updater->updateMethodRelativeHomeLink(); $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());
} }
} }

View file

@ -15,7 +15,7 @@ <h2>{$message}</h2>
</pre> </pre>
{/if} {/if}
<img src="{asset_path}/img/sad_star.png#" alt=""> <img src="{$asset_path}/img/sad_star.png#" alt="">
</div> </div>
{include="page.footer"} {include="page.footer"}
</body> </body>

View file

@ -10,7 +10,7 @@
{$ratioLabelMobile='7-8'} {$ratioLabelMobile='7-8'}
{$ratioInputMobile='1-8'} {$ratioInputMobile='1-8'}
<form method="POST" action="{$base_path}/?do=install" name="installform" id="installform"> <form method="POST" action="{$base_path}/install" name="installform" id="installform">
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-lg-1-6 pure-u-1-24"></div> <div class="pure-u-lg-1-6 pure-u-1-24"></div>
<div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete"> <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete">

View file

@ -184,7 +184,7 @@
</div> </div>
{/if} {/if}
{if="!empty($global_errors) && $is_logged_in"} {if="!empty($global_errors)"}
<div class="pure-g header-alert-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert"> <div class="pure-g header-alert-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
<div class="pure-u-2-24"></div> <div class="pure-u-2-24"></div>
<div class="pure-u-20-24"> <div class="pure-u-20-24">
@ -198,7 +198,7 @@
</div> </div>
{/if} {/if}
{if="!empty($global_warnings) && $is_logged_in"} {if="!empty($global_warnings)"}
<div class="pure-g header-alert-message pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert"> <div class="pure-g header-alert-message pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
<div class="pure-u-2-24"></div> <div class="pure-u-2-24"></div>
<div class="pure-u-20-24"> <div class="pure-u-20-24">
@ -212,7 +212,7 @@
</div> </div>
{/if} {/if}
{if="!empty($global_successes) && $is_logged_in"} {if="!empty($global_successes)"}
<div class="pure-g header-alert-message new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert"> <div class="pure-g header-alert-message new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert">
<div class="pure-u-2-24"></div> <div class="pure-u-2-24"></div>
<div class="pure-u-20-24"> <div class="pure-u-20-24">