diff options
author | ArthurHoaro <arthur.hoareau@wizacha.com> | 2020-07-07 10:15:56 +0200 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2020-07-23 21:19:21 +0200 |
commit | c4ad3d4f061d05a01db25aa54dda830ba776792d (patch) | |
tree | 691d91a5b0bbac62cee41f7b95ad1daa38d610b3 | |
parent | 1a8ac737e52cb25a5c346232ee398f5908cee7d7 (diff) | |
download | Shaarli-c4ad3d4f061d05a01db25aa54dda830ba776792d.tar.gz Shaarli-c4ad3d4f061d05a01db25aa54dda830ba776792d.tar.zst Shaarli-c4ad3d4f061d05a01db25aa54dda830ba776792d.zip |
Process Shaarli install through Slim controller
27 files changed, 722 insertions, 221 deletions
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 | |||
46 | /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ | 46 | /** @var bool true for logged in users. Default value to retrieve private bookmarks. */ |
47 | protected $isLoggedIn; | 47 | protected $isLoggedIn; |
48 | 48 | ||
49 | /** @var bool Allow datastore alteration from not logged in users. */ | ||
50 | protected $anonymousPermission = false; | ||
51 | |||
49 | /** | 52 | /** |
50 | * @inheritDoc | 53 | * @inheritDoc |
51 | */ | 54 | */ |
@@ -64,7 +67,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
64 | $this->bookmarks = $this->bookmarksIO->read(); | 67 | $this->bookmarks = $this->bookmarksIO->read(); |
65 | } catch (EmptyDataStoreException $e) { | 68 | } catch (EmptyDataStoreException $e) { |
66 | $this->bookmarks = new BookmarkArray(); | 69 | $this->bookmarks = new BookmarkArray(); |
67 | if ($isLoggedIn) { | 70 | if ($this->isLoggedIn) { |
68 | $this->save(); | 71 | $this->save(); |
69 | } | 72 | } |
70 | } | 73 | } |
@@ -154,7 +157,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
154 | */ | 157 | */ |
155 | public function set($bookmark, $save = true) | 158 | public function set($bookmark, $save = true) |
156 | { | 159 | { |
157 | if ($this->isLoggedIn !== true) { | 160 | if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { |
158 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 161 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
159 | } | 162 | } |
160 | if (! $bookmark instanceof Bookmark) { | 163 | if (! $bookmark instanceof Bookmark) { |
@@ -179,7 +182,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
179 | */ | 182 | */ |
180 | public function add($bookmark, $save = true) | 183 | public function add($bookmark, $save = true) |
181 | { | 184 | { |
182 | if ($this->isLoggedIn !== true) { | 185 | if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { |
183 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 186 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
184 | } | 187 | } |
185 | if (! $bookmark instanceof Bookmark) { | 188 | if (! $bookmark instanceof Bookmark) { |
@@ -204,7 +207,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
204 | */ | 207 | */ |
205 | public function addOrSet($bookmark, $save = true) | 208 | public function addOrSet($bookmark, $save = true) |
206 | { | 209 | { |
207 | if ($this->isLoggedIn !== true) { | 210 | if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { |
208 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 211 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
209 | } | 212 | } |
210 | if (! $bookmark instanceof Bookmark) { | 213 | if (! $bookmark instanceof Bookmark) { |
@@ -221,7 +224,7 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
221 | */ | 224 | */ |
222 | public function remove($bookmark, $save = true) | 225 | public function remove($bookmark, $save = true) |
223 | { | 226 | { |
224 | if ($this->isLoggedIn !== true) { | 227 | if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { |
225 | throw new Exception(t('You\'re not authorized to alter the datastore')); | 228 | throw new Exception(t('You\'re not authorized to alter the datastore')); |
226 | } | 229 | } |
227 | if (! $bookmark instanceof Bookmark) { | 230 | if (! $bookmark instanceof Bookmark) { |
@@ -274,10 +277,11 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
274 | */ | 277 | */ |
275 | public function save() | 278 | public function save() |
276 | { | 279 | { |
277 | if (!$this->isLoggedIn) { | 280 | if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) { |
278 | // TODO: raise an Exception instead | 281 | // TODO: raise an Exception instead |
279 | die('You are not authorized to change the database.'); | 282 | die('You are not authorized to change the database.'); |
280 | } | 283 | } |
284 | |||
281 | $this->bookmarks->reorder(); | 285 | $this->bookmarks->reorder(); |
282 | $this->bookmarksIO->write($this->bookmarks); | 286 | $this->bookmarksIO->write($this->bookmarks); |
283 | $this->pageCacheManager->invalidateCaches(); | 287 | $this->pageCacheManager->invalidateCaches(); |
@@ -357,6 +361,16 @@ class BookmarkFileService implements BookmarkServiceInterface | |||
357 | $initializer->initialize(); | 361 | $initializer->initialize(); |
358 | } | 362 | } |
359 | 363 | ||
364 | public function enableAnonymousPermission(): void | ||
365 | { | ||
366 | $this->anonymousPermission = true; | ||
367 | } | ||
368 | |||
369 | public function disableAnonymousPermission(): void | ||
370 | { | ||
371 | $this->anonymousPermission = false; | ||
372 | } | ||
373 | |||
360 | /** | 374 | /** |
361 | * Handles migration to the new database format (BookmarksArray). | 375 | * Handles migration to the new database format (BookmarksArray). |
362 | */ | 376 | */ |
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 @@ class BookmarkInitializer | |||
34 | */ | 34 | */ |
35 | public function initialize() | 35 | public function initialize() |
36 | { | 36 | { |
37 | $this->bookmarkService->enableAnonymousPermission(); | ||
38 | |||
37 | $bookmark = new Bookmark(); | 39 | $bookmark = new Bookmark(); |
38 | $bookmark->setTitle(t('My secret stuff... - Pastebin.com')); | 40 | $bookmark->setTitle(t('My secret stuff... - Pastebin.com')); |
39 | $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []); | 41 | $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8='); |
40 | $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.')); | 42 | $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.')); |
41 | $bookmark->setTagsString('secretstuff'); | 43 | $bookmark->setTagsString('secretstuff'); |
42 | $bookmark->setPrivate(true); | 44 | $bookmark->setPrivate(true); |
43 | $this->bookmarkService->add($bookmark); | 45 | $this->bookmarkService->add($bookmark, false); |
44 | 46 | ||
45 | $bookmark = new Bookmark(); | 47 | $bookmark = new Bookmark(); |
46 | $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service')); | 48 | $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service')); |
@@ -54,6 +56,10 @@ To learn how to use Shaarli, consult the link "Documentation" at the bottom of t | |||
54 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' | 56 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' |
55 | )); | 57 | )); |
56 | $bookmark->setTagsString('opensource software'); | 58 | $bookmark->setTagsString('opensource software'); |
57 | $this->bookmarkService->add($bookmark); | 59 | $this->bookmarkService->add($bookmark, false); |
60 | |||
61 | $this->bookmarkService->save(); | ||
62 | |||
63 | $this->bookmarkService->disableAnonymousPermission(); | ||
58 | } | 64 | } |
59 | } | 65 | } |
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 @@ interface BookmarkServiceInterface | |||
177 | * Creates the default database after a fresh install. | 177 | * Creates the default database after a fresh install. |
178 | */ | 178 | */ |
179 | public function initialize(); | 179 | public function initialize(); |
180 | |||
181 | /** | ||
182 | * Allow to write the datastore from anonymous session (not logged in). | ||
183 | * | ||
184 | * This covers a few specific use cases, such as datastore initialization, | ||
185 | * but it should be used carefully as it can lead to security issues. | ||
186 | */ | ||
187 | public function enableAnonymousPermission(); | ||
188 | |||
189 | /** | ||
190 | * Disable anonymous permission. | ||
191 | */ | ||
192 | public function disableAnonymousPermission(); | ||
180 | } | 193 | } |
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\Netscape\NetscapeBookmarkUtils; | |||
15 | use Shaarli\Plugin\PluginManager; | 15 | use Shaarli\Plugin\PluginManager; |
16 | use Shaarli\Render\PageBuilder; | 16 | use Shaarli\Render\PageBuilder; |
17 | use Shaarli\Render\PageCacheManager; | 17 | use Shaarli\Render\PageCacheManager; |
18 | use Shaarli\Security\CookieManager; | ||
18 | use Shaarli\Security\LoginManager; | 19 | use Shaarli\Security\LoginManager; |
19 | use Shaarli\Security\SessionManager; | 20 | use Shaarli\Security\SessionManager; |
20 | use Shaarli\Thumbnailer; | 21 | use Shaarli\Thumbnailer; |
@@ -38,6 +39,9 @@ class ContainerBuilder | |||
38 | /** @var SessionManager */ | 39 | /** @var SessionManager */ |
39 | protected $session; | 40 | protected $session; |
40 | 41 | ||
42 | /** @var CookieManager */ | ||
43 | protected $cookieManager; | ||
44 | |||
41 | /** @var LoginManager */ | 45 | /** @var LoginManager */ |
42 | protected $login; | 46 | protected $login; |
43 | 47 | ||
@@ -47,11 +51,13 @@ class ContainerBuilder | |||
47 | public function __construct( | 51 | public function __construct( |
48 | ConfigManager $conf, | 52 | ConfigManager $conf, |
49 | SessionManager $session, | 53 | SessionManager $session, |
54 | CookieManager $cookieManager, | ||
50 | LoginManager $login | 55 | LoginManager $login |
51 | ) { | 56 | ) { |
52 | $this->conf = $conf; | 57 | $this->conf = $conf; |
53 | $this->session = $session; | 58 | $this->session = $session; |
54 | $this->login = $login; | 59 | $this->login = $login; |
60 | $this->cookieManager = $cookieManager; | ||
55 | } | 61 | } |
56 | 62 | ||
57 | public function build(): ShaarliContainer | 63 | public function build(): ShaarliContainer |
@@ -60,6 +66,7 @@ class ContainerBuilder | |||
60 | 66 | ||
61 | $container['conf'] = $this->conf; | 67 | $container['conf'] = $this->conf; |
62 | $container['sessionManager'] = $this->session; | 68 | $container['sessionManager'] = $this->session; |
69 | $container['cookieManager'] = $this->cookieManager; | ||
63 | $container['loginManager'] = $this->login; | 70 | $container['loginManager'] = $this->login; |
64 | $container['basePath'] = $this->basePath; | 71 | $container['basePath'] = $this->basePath; |
65 | 72 | ||
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 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Container; | 5 | namespace Shaarli\Container; |
6 | 6 | ||
7 | use http\Cookie; | ||
7 | use Shaarli\Bookmark\BookmarkServiceInterface; | 8 | use Shaarli\Bookmark\BookmarkServiceInterface; |
8 | use Shaarli\Config\ConfigManager; | 9 | use Shaarli\Config\ConfigManager; |
9 | use Shaarli\Feed\FeedBuilder; | 10 | use Shaarli\Feed\FeedBuilder; |
@@ -14,6 +15,7 @@ use Shaarli\Netscape\NetscapeBookmarkUtils; | |||
14 | use Shaarli\Plugin\PluginManager; | 15 | use Shaarli\Plugin\PluginManager; |
15 | use Shaarli\Render\PageBuilder; | 16 | use Shaarli\Render\PageBuilder; |
16 | use Shaarli\Render\PageCacheManager; | 17 | use Shaarli\Render\PageCacheManager; |
18 | use Shaarli\Security\CookieManager; | ||
17 | use Shaarli\Security\LoginManager; | 19 | use Shaarli\Security\LoginManager; |
18 | use Shaarli\Security\SessionManager; | 20 | use Shaarli\Security\SessionManager; |
19 | use Shaarli\Thumbnailer; | 21 | use Shaarli\Thumbnailer; |
@@ -25,6 +27,7 @@ use Slim\Container; | |||
25 | * | 27 | * |
26 | * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`) | 28 | * @property string $basePath Shaarli's instance base path (e.g. `/shaarli/`) |
27 | * @property BookmarkServiceInterface $bookmarkService | 29 | * @property BookmarkServiceInterface $bookmarkService |
30 | * @property CookieManager $cookieManager | ||
28 | * @property ConfigManager $conf | 31 | * @property ConfigManager $conf |
29 | * @property mixed[] $environment $_SERVER automatically injected by Slim | 32 | * @property mixed[] $environment $_SERVER automatically injected by Slim |
30 | * @property callable $errorHandler Overrides default Slim error display | 33 | * @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 @@ class ShaarliMiddleware | |||
43 | $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); | 43 | $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/'); |
44 | 44 | ||
45 | try { | 45 | try { |
46 | if (!is_file($this->container->conf->getConfigFileExt()) | ||
47 | && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true) | ||
48 | ) { | ||
49 | return $response->withRedirect($this->container->basePath . '/install'); | ||
50 | } | ||
51 | |||
46 | $this->runUpdates(); | 52 | $this->runUpdates(); |
47 | $this->checkOpenShaarli($request, $response, $next); | 53 | $this->checkOpenShaarli($request, $response, $next); |
48 | 54 | ||
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 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Admin; | 5 | namespace Shaarli\Front\Controller\Admin; |
6 | 6 | ||
7 | use Shaarli\Security\CookieManager; | ||
7 | use Shaarli\Security\LoginManager; | 8 | use Shaarli\Security\LoginManager; |
8 | use Slim\Http\Request; | 9 | use Slim\Http\Request; |
9 | use Slim\Http\Response; | 10 | use Slim\Http\Response; |
@@ -20,9 +21,12 @@ class LogoutController extends ShaarliAdminController | |||
20 | { | 21 | { |
21 | $this->container->pageCacheManager->invalidateCaches(); | 22 | $this->container->pageCacheManager->invalidateCaches(); |
22 | $this->container->sessionManager->logout(); | 23 | $this->container->sessionManager->logout(); |
23 | 24 | $this->container->cookieManager->setCookieParameter( | |
24 | // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks. | 25 | CookieManager::STAY_SIGNED_IN, |
25 | setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->basePath . '/'); | 26 | 'false', |
27 | 0, | ||
28 | $this->container->basePath . '/' | ||
29 | ); | ||
26 | 30 | ||
27 | return $this->redirect($response, '/'); | 31 | return $this->redirect($response, '/'); |
28 | } | 32 | } |
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Visitor; | ||
6 | |||
7 | use Shaarli\ApplicationUtils; | ||
8 | use Shaarli\Bookmark\BookmarkFilter; | ||
9 | use Shaarli\Container\ShaarliContainer; | ||
10 | use Shaarli\Front\Exception\AlreadyInstalledException; | ||
11 | use Shaarli\Front\Exception\ResourcePermissionException; | ||
12 | use Shaarli\Languages; | ||
13 | use Shaarli\Security\SessionManager; | ||
14 | use Slim\Http\Request; | ||
15 | use Slim\Http\Response; | ||
16 | |||
17 | /** | ||
18 | * Slim controller used to render install page, and create initial configuration file. | ||
19 | */ | ||
20 | class InstallController extends ShaarliVisitorController | ||
21 | { | ||
22 | public const SESSION_TEST_KEY = 'session_tested'; | ||
23 | public const SESSION_TEST_VALUE = 'Working'; | ||
24 | |||
25 | public function __construct(ShaarliContainer $container) | ||
26 | { | ||
27 | parent::__construct($container); | ||
28 | |||
29 | if (is_file($this->container->conf->getConfigFileExt())) { | ||
30 | throw new AlreadyInstalledException(); | ||
31 | } | ||
32 | } | ||
33 | |||
34 | /** | ||
35 | * Display the install template page. | ||
36 | * Also test file permissions and sessions beforehand. | ||
37 | */ | ||
38 | public function index(Request $request, Response $response): Response | ||
39 | { | ||
40 | // Before installation, we'll make sure that permissions are set properly, and sessions are working. | ||
41 | $this->checkPermissions(); | ||
42 | |||
43 | if (static::SESSION_TEST_VALUE | ||
44 | !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) | ||
45 | ) { | ||
46 | $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE); | ||
47 | |||
48 | return $this->redirect($response, '/install/session-test'); | ||
49 | } | ||
50 | |||
51 | [$continents, $cities] = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); | ||
52 | |||
53 | $this->assignView('continents', $continents); | ||
54 | $this->assignView('cities', $cities); | ||
55 | $this->assignView('languages', Languages::getAvailableLanguages()); | ||
56 | |||
57 | return $response->write($this->render('install')); | ||
58 | } | ||
59 | |||
60 | /** | ||
61 | * Route checking that the session parameter has been properly saved between two distinct requests. | ||
62 | * If the session parameter is preserved, redirect to install template page, otherwise displays error. | ||
63 | */ | ||
64 | public function sessionTest(Request $request, Response $response): Response | ||
65 | { | ||
66 | // This part makes sure sessions works correctly. | ||
67 | // (Because on some hosts, session.save_path may not be set correctly, | ||
68 | // or we may not have write access to it.) | ||
69 | if (static::SESSION_TEST_VALUE | ||
70 | !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY) | ||
71 | ) { | ||
72 | // Step 2: Check if data in session is correct. | ||
73 | $msg = t( | ||
74 | '<pre>Sessions do not seem to work correctly on your server.<br>'. | ||
75 | 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. | ||
76 | 'and that you have write access to it.<br>'. | ||
77 | 'It currently points to %s.<br>'. | ||
78 | 'On some browsers, accessing your server via a hostname like \'localhost\' '. | ||
79 | 'or any custom hostname without a dot causes cookie storage to fail. '. | ||
80 | 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>' | ||
81 | ); | ||
82 | $msg = sprintf($msg, $this->container->sessionManager->getSavePath()); | ||
83 | |||
84 | $this->assignView('message', $msg); | ||
85 | |||
86 | return $response->write($this->render('error')); | ||
87 | } | ||
88 | |||
89 | return $this->redirect($response, '/install'); | ||
90 | } | ||
91 | |||
92 | /** | ||
93 | * Save installation form and initialize config file and datastore if necessary. | ||
94 | */ | ||
95 | public function save(Request $request, Response $response): Response | ||
96 | { | ||
97 | $timezone = 'UTC'; | ||
98 | if (!empty($request->getParam('continent')) | ||
99 | && !empty($request->getParam('city')) | ||
100 | && isTimeZoneValid($request->getParam('continent'), $request->getParam('city')) | ||
101 | ) { | ||
102 | $timezone = $request->getParam('continent') . '/' . $request->getParam('city'); | ||
103 | } | ||
104 | $this->container->conf->set('general.timezone', $timezone); | ||
105 | |||
106 | $login = $request->getParam('setlogin'); | ||
107 | $this->container->conf->set('credentials.login', $login); | ||
108 | $salt = sha1(uniqid('', true) .'_'. mt_rand()); | ||
109 | $this->container->conf->set('credentials.salt', $salt); | ||
110 | $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt)); | ||
111 | |||
112 | if (!empty($request->getParam('title'))) { | ||
113 | $this->container->conf->set('general.title', escape($request->getParam('title'))); | ||
114 | } else { | ||
115 | $this->container->conf->set( | ||
116 | 'general.title', | ||
117 | 'Shared bookmarks on '.escape(index_url($this->container->environment)) | ||
118 | ); | ||
119 | } | ||
120 | |||
121 | $this->container->conf->set('translation.language', escape($request->getParam('language'))); | ||
122 | $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck'))); | ||
123 | $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi'))); | ||
124 | $this->container->conf->set( | ||
125 | 'api.secret', | ||
126 | generate_api_secret( | ||
127 | $this->container->conf->get('credentials.login'), | ||
128 | $this->container->conf->get('credentials.salt') | ||
129 | ) | ||
130 | ); | ||
131 | |||
132 | try { | ||
133 | // Everything is ok, let's create config file. | ||
134 | $this->container->conf->write($this->container->loginManager->isLoggedIn()); | ||
135 | } catch (\Exception $e) { | ||
136 | $this->assignView('message', $e->getMessage()); | ||
137 | $this->assignView('stacktrace', $e->getTraceAsString()); | ||
138 | |||
139 | return $response->write($this->render('error')); | ||
140 | } | ||
141 | |||
142 | if ($this->container->bookmarkService->count(BookmarkFilter::$ALL) === 0) { | ||
143 | $this->container->bookmarkService->initialize(); | ||
144 | } | ||
145 | |||
146 | $this->container->sessionManager->setSessionParameter( | ||
147 | SessionManager::KEY_SUCCESS_MESSAGES, | ||
148 | [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')] | ||
149 | ); | ||
150 | |||
151 | return $this->redirect($response, '/'); | ||
152 | } | ||
153 | |||
154 | protected function checkPermissions(): bool | ||
155 | { | ||
156 | // Ensure Shaarli has proper access to its resources | ||
157 | $errors = ApplicationUtils::checkResourcePermissions($this->container->conf); | ||
158 | |||
159 | if (empty($errors)) { | ||
160 | return true; | ||
161 | } | ||
162 | |||
163 | // FIXME! Do not insert HTML here. | ||
164 | $message = '<p>'. t('Insufficient permissions:') .'</p><ul>'; | ||
165 | |||
166 | foreach ($errors as $error) { | ||
167 | $message .= '<li>'.$error.'</li>'; | ||
168 | } | ||
169 | $message .= '</ul>'; | ||
170 | |||
171 | throw new ResourcePermissionException($message); | ||
172 | } | ||
173 | } | ||
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Exception; | ||
6 | |||
7 | class AlreadyInstalledException extends ShaarliFrontException | ||
8 | { | ||
9 | public function __construct() | ||
10 | { | ||
11 | $message = t('Shaarli has already been installed. Login to edit the configuration.'); | ||
12 | |||
13 | parent::__construct($message, 401); | ||
14 | } | ||
15 | } | ||
diff --git a/application/front/exceptions/ResourcePermissionException.php b/application/front/exceptions/ResourcePermissionException.php new file mode 100644 index 00000000..8fbf03b9 --- /dev/null +++ b/application/front/exceptions/ResourcePermissionException.php | |||
@@ -0,0 +1,13 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Exception; | ||
6 | |||
7 | class ResourcePermissionException extends ShaarliFrontException | ||
8 | { | ||
9 | public function __construct(string $message) | ||
10 | { | ||
11 | parent::__construct($message, 500); | ||
12 | } | ||
13 | } | ||
diff --git a/application/security/CookieManager.php b/application/security/CookieManager.php new file mode 100644 index 00000000..cde4746e --- /dev/null +++ b/application/security/CookieManager.php | |||
@@ -0,0 +1,33 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Security; | ||
6 | |||
7 | class CookieManager | ||
8 | { | ||
9 | /** @var string Name of the cookie set after logging in **/ | ||
10 | public const STAY_SIGNED_IN = 'shaarli_staySignedIn'; | ||
11 | |||
12 | /** @var mixed $_COOKIE set by reference */ | ||
13 | protected $cookies; | ||
14 | |||
15 | public function __construct(array &$cookies) | ||
16 | { | ||
17 | $this->cookies = $cookies; | ||
18 | } | ||
19 | |||
20 | public function setCookieParameter(string $key, string $value, int $expires, string $path): self | ||
21 | { | ||
22 | $this->cookies[$key] = $value; | ||
23 | |||
24 | setcookie($key, $value, $expires, $path); | ||
25 | |||
26 | return $this; | ||
27 | } | ||
28 | |||
29 | public function getCookieParameter(string $key, string $default = null): ?string | ||
30 | { | ||
31 | return $this->cookies[$key] ?? $default; | ||
32 | } | ||
33 | } | ||
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 @@ use Shaarli\Config\ConfigManager; | |||
9 | */ | 9 | */ |
10 | class LoginManager | 10 | class LoginManager |
11 | { | 11 | { |
12 | /** @var string Name of the cookie set after logging in **/ | ||
13 | public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn'; | ||
14 | |||
15 | /** @var array A reference to the $_GLOBALS array */ | 12 | /** @var array A reference to the $_GLOBALS array */ |
16 | protected $globals = []; | 13 | protected $globals = []; |
17 | 14 | ||
@@ -32,17 +29,21 @@ class LoginManager | |||
32 | 29 | ||
33 | /** @var string User sign-in token depending on remote IP and credentials */ | 30 | /** @var string User sign-in token depending on remote IP and credentials */ |
34 | protected $staySignedInToken = ''; | 31 | protected $staySignedInToken = ''; |
32 | /** @var CookieManager */ | ||
33 | protected $cookieManager; | ||
35 | 34 | ||
36 | /** | 35 | /** |
37 | * Constructor | 36 | * Constructor |
38 | * | 37 | * |
39 | * @param ConfigManager $configManager Configuration Manager instance | 38 | * @param ConfigManager $configManager Configuration Manager instance |
40 | * @param SessionManager $sessionManager SessionManager instance | 39 | * @param SessionManager $sessionManager SessionManager instance |
40 | * @param CookieManager $cookieManager CookieManager instance | ||
41 | */ | 41 | */ |
42 | public function __construct($configManager, $sessionManager) | 42 | public function __construct($configManager, $sessionManager, $cookieManager) |
43 | { | 43 | { |
44 | $this->configManager = $configManager; | 44 | $this->configManager = $configManager; |
45 | $this->sessionManager = $sessionManager; | 45 | $this->sessionManager = $sessionManager; |
46 | $this->cookieManager = $cookieManager; | ||
46 | $this->banManager = new BanManager( | 47 | $this->banManager = new BanManager( |
47 | $this->configManager->get('security.trusted_proxies', []), | 48 | $this->configManager->get('security.trusted_proxies', []), |
48 | $this->configManager->get('security.ban_after'), | 49 | $this->configManager->get('security.ban_after'), |
@@ -86,10 +87,9 @@ class LoginManager | |||
86 | /** | 87 | /** |
87 | * Check user session state and validity (expiration) | 88 | * Check user session state and validity (expiration) |
88 | * | 89 | * |
89 | * @param array $cookie The $_COOKIE array | ||
90 | * @param string $clientIpId Client IP address identifier | 90 | * @param string $clientIpId Client IP address identifier |
91 | */ | 91 | */ |
92 | public function checkLoginState($cookie, $clientIpId) | 92 | public function checkLoginState($clientIpId) |
93 | { | 93 | { |
94 | if (! $this->configManager->exists('credentials.login')) { | 94 | if (! $this->configManager->exists('credentials.login')) { |
95 | // Shaarli is not configured yet | 95 | // Shaarli is not configured yet |
@@ -97,9 +97,7 @@ class LoginManager | |||
97 | return; | 97 | return; |
98 | } | 98 | } |
99 | 99 | ||
100 | if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) | 100 | if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) { |
101 | && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken | ||
102 | ) { | ||
103 | // The user client has a valid stay-signed-in cookie | 101 | // The user client has a valid stay-signed-in cookie |
104 | // Session information is updated with the current client information | 102 | // Session information is updated with the current client information |
105 | $this->sessionManager->storeLoginInfo($clientIpId); | 103 | $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 | |||
31 | /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ | 31 | /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ |
32 | protected $staySignedIn = false; | 32 | protected $staySignedIn = false; |
33 | 33 | ||
34 | /** @var string */ | ||
35 | protected $savePath; | ||
36 | |||
34 | /** | 37 | /** |
35 | * Constructor | 38 | * Constructor |
36 | * | 39 | * |
37 | * @param array $session The $_SESSION array (reference) | 40 | * @param array $session The $_SESSION array (reference) |
38 | * @param ConfigManager $conf ConfigManager instance | 41 | * @param ConfigManager $conf ConfigManager instance |
42 | * @param string $savePath Session save path returned by builtin function session_save_path() | ||
39 | */ | 43 | */ |
40 | public function __construct(& $session, $conf) | 44 | public function __construct(&$session, $conf, string $savePath) |
41 | { | 45 | { |
42 | $this->session = &$session; | 46 | $this->session = &$session; |
43 | $this->conf = $conf; | 47 | $this->conf = $conf; |
48 | $this->savePath = $savePath; | ||
44 | } | 49 | } |
45 | 50 | ||
46 | /** | 51 | /** |
@@ -249,4 +254,9 @@ class SessionManager | |||
249 | 254 | ||
250 | return $this; | 255 | return $this; |
251 | } | 256 | } |
257 | |||
258 | public function getSavePath(): string | ||
259 | { | ||
260 | return $this->savePath; | ||
261 | } | ||
252 | } | 262 | } |
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 | |||
@@ -39,6 +39,11 @@ class Updater | |||
39 | protected $methods; | 39 | protected $methods; |
40 | 40 | ||
41 | /** | 41 | /** |
42 | * @var string $basePath Shaarli root directory (from HTTP Request) | ||
43 | */ | ||
44 | protected $basePath = null; | ||
45 | |||
46 | /** | ||
42 | * Object constructor. | 47 | * Object constructor. |
43 | * | 48 | * |
44 | * @param array $doneUpdates Updates which are already done. | 49 | * @param array $doneUpdates Updates which are already done. |
@@ -62,11 +67,13 @@ class Updater | |||
62 | * Run all new updates. | 67 | * Run all new updates. |
63 | * Update methods have to start with 'updateMethod' and return true (on success). | 68 | * Update methods have to start with 'updateMethod' and return true (on success). |
64 | * | 69 | * |
70 | * @param string $basePath Shaarli root directory (from HTTP Request) | ||
71 | * | ||
65 | * @return array An array containing ran updates. | 72 | * @return array An array containing ran updates. |
66 | * | 73 | * |
67 | * @throws UpdaterException If something went wrong. | 74 | * @throws UpdaterException If something went wrong. |
68 | */ | 75 | */ |
69 | public function update() | 76 | public function update(string $basePath = null) |
70 | { | 77 | { |
71 | $updatesRan = []; | 78 | $updatesRan = []; |
72 | 79 | ||
@@ -123,16 +130,14 @@ class Updater | |||
123 | } | 130 | } |
124 | 131 | ||
125 | /** | 132 | /** |
126 | * With the Slim routing system, default header link should be `./` instead of `?`. | 133 | * With the Slim routing system, default header link should be `/subfolder/` instead of `?`. |
127 | * Otherwise you can not go back to the home page. Example: `/picture-wall` -> `/picture-wall?` instead of `/`. | 134 | * Otherwise you can not go back to the home page. |
135 | * Example: `/subfolder/picture-wall` -> `/subfolder/picture-wall?` instead of `/subfolder/`. | ||
128 | */ | 136 | */ |
129 | public function updateMethodRelativeHomeLink(): bool | 137 | public function updateMethodRelativeHomeLink(): bool |
130 | { | 138 | { |
131 | $link = trim($this->conf->get('general.header_link')); | 139 | if ('?' === trim($this->conf->get('general.header_link'))) { |
132 | if ($link[0] === '?') { | 140 | $this->conf->set('general.header_link', $this->basePath . '/', true, true); |
133 | $link = './'. ltrim($link, '?'); | ||
134 | |||
135 | $this->conf->set('general.header_link', $link, true, true); | ||
136 | } | 141 | } |
137 | 142 | ||
138 | return true; | 143 | return true; |
@@ -152,7 +157,7 @@ class Updater | |||
152 | && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) | 157 | && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match) |
153 | ) { | 158 | ) { |
154 | $updated = true; | 159 | $updated = true; |
155 | $bookmark = $bookmark->setUrl('/shaare/' . $match[1]); | 160 | $bookmark = $bookmark->setUrl($this->basePath . '/shaare/' . $match[1]); |
156 | 161 | ||
157 | $this->bookmarkService->set($bookmark, false); | 162 | $this->bookmarkService->set($bookmark, false); |
158 | } | 163 | } |
@@ -164,4 +169,11 @@ class Updater | |||
164 | 169 | ||
165 | return true; | 170 | return true; |
166 | } | 171 | } |
172 | |||
173 | public function setBasePath(string $basePath): self | ||
174 | { | ||
175 | $this->basePath = $basePath; | ||
176 | |||
177 | return $this; | ||
178 | } | ||
167 | } | 179 | } |
@@ -61,13 +61,11 @@ require_once 'application/TimeZone.php'; | |||
61 | require_once 'application/Utils.php'; | 61 | require_once 'application/Utils.php'; |
62 | 62 | ||
63 | use Shaarli\ApplicationUtils; | 63 | use Shaarli\ApplicationUtils; |
64 | use Shaarli\Bookmark\BookmarkFileService; | ||
65 | use Shaarli\Config\ConfigManager; | 64 | use Shaarli\Config\ConfigManager; |
66 | use Shaarli\Container\ContainerBuilder; | 65 | use Shaarli\Container\ContainerBuilder; |
67 | use Shaarli\History; | ||
68 | use Shaarli\Languages; | 66 | use Shaarli\Languages; |
69 | use Shaarli\Plugin\PluginManager; | 67 | use Shaarli\Plugin\PluginManager; |
70 | use Shaarli\Render\PageBuilder; | 68 | use Shaarli\Security\CookieManager; |
71 | use Shaarli\Security\LoginManager; | 69 | use Shaarli\Security\LoginManager; |
72 | use Shaarli\Security\SessionManager; | 70 | use Shaarli\Security\SessionManager; |
73 | use Slim\App; | 71 | use Slim\App; |
@@ -118,13 +116,14 @@ if ($conf->get('dev.debug', false)) { | |||
118 | // See all errors (for debugging only) | 116 | // See all errors (for debugging only) |
119 | error_reporting(-1); | 117 | error_reporting(-1); |
120 | 118 | ||
121 | set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) { | 119 | set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext) { |
122 | throw new ErrorException($errstr, 0, $errno, $errfile, $errline); | 120 | throw new ErrorException($errstr, 0, $errno, $errfile, $errline); |
123 | }); | 121 | }); |
124 | } | 122 | } |
125 | 123 | ||
126 | $sessionManager = new SessionManager($_SESSION, $conf); | 124 | $sessionManager = new SessionManager($_SESSION, $conf, session_save_path()); |
127 | $loginManager = new LoginManager($conf, $sessionManager); | 125 | $cookieManager = new CookieManager($_COOKIE); |
126 | $loginManager = new LoginManager($conf, $sessionManager, $cookieManager); | ||
128 | $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); | 127 | $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); |
129 | $clientIpId = client_ip_id($_SERVER); | 128 | $clientIpId = client_ip_id($_SERVER); |
130 | 129 | ||
@@ -158,28 +157,7 @@ header("Cache-Control: no-store, no-cache, must-revalidate"); | |||
158 | header("Cache-Control: post-check=0, pre-check=0", false); | 157 | header("Cache-Control: post-check=0, pre-check=0", false); |
159 | header("Pragma: no-cache"); | 158 | header("Pragma: no-cache"); |
160 | 159 | ||
161 | if (! is_file($conf->getConfigFileExt())) { | 160 | $loginManager->checkLoginState($clientIpId); |
162 | // Ensure Shaarli has proper access to its resources | ||
163 | $errors = ApplicationUtils::checkResourcePermissions($conf); | ||
164 | |||
165 | if ($errors != array()) { | ||
166 | $message = '<p>'. t('Insufficient permissions:') .'</p><ul>'; | ||
167 | |||
168 | foreach ($errors as $error) { | ||
169 | $message .= '<li>'.$error.'</li>'; | ||
170 | } | ||
171 | $message .= '</ul>'; | ||
172 | |||
173 | header('Content-Type: text/html; charset=utf-8'); | ||
174 | echo $message; | ||
175 | exit; | ||
176 | } | ||
177 | |||
178 | // Display the installation form if no existing config is found | ||
179 | install($conf, $sessionManager, $loginManager); | ||
180 | } | ||
181 | |||
182 | $loginManager->checkLoginState($_COOKIE, $clientIpId); | ||
183 | 161 | ||
184 | // ------------------------------------------------------------------------------------------ | 162 | // ------------------------------------------------------------------------------------------ |
185 | // Process login form: Check if login/password is correct. | 163 | // Process login form: Check if login/password is correct. |
@@ -205,7 +183,7 @@ if (isset($_POST['login'])) { | |||
205 | $expirationTime = $sessionManager->extendSession(); | 183 | $expirationTime = $sessionManager->extendSession(); |
206 | 184 | ||
207 | setcookie( | 185 | setcookie( |
208 | $loginManager::$STAY_SIGNED_IN_COOKIE, | 186 | CookieManager::STAY_SIGNED_IN, |
209 | $loginManager->getStaySignedInToken(), | 187 | $loginManager->getStaySignedInToken(), |
210 | $expirationTime, | 188 | $expirationTime, |
211 | WEB_PATH | 189 | WEB_PATH |
@@ -271,122 +249,11 @@ if (!isset($_SESSION['tokens'])) { | |||
271 | $_SESSION['tokens']=array(); // Token are attached to the session. | 249 | $_SESSION['tokens']=array(); // Token are attached to the session. |
272 | } | 250 | } |
273 | 251 | ||
274 | /** | ||
275 | * Installation | ||
276 | * This function should NEVER be called if the file data/config.php exists. | ||
277 | * | ||
278 | * @param ConfigManager $conf Configuration Manager instance. | ||
279 | * @param SessionManager $sessionManager SessionManager instance | ||
280 | * @param LoginManager $loginManager LoginManager instance | ||
281 | */ | ||
282 | function install($conf, $sessionManager, $loginManager) | ||
283 | { | ||
284 | // On free.fr host, make sure the /sessions directory exists, otherwise login will not work. | ||
285 | if (endsWith($_SERVER['HTTP_HOST'], '.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) { | ||
286 | mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions', 0705); | ||
287 | } | ||
288 | |||
289 | |||
290 | // This part makes sure sessions works correctly. | ||
291 | // (Because on some hosts, session.save_path may not be set correctly, | ||
292 | // or we may not have write access to it.) | ||
293 | if (isset($_GET['test_session']) | ||
294 | && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) { | ||
295 | // Step 2: Check if data in session is correct. | ||
296 | $msg = t( | ||
297 | '<pre>Sessions do not seem to work correctly on your server.<br>'. | ||
298 | 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. | ||
299 | 'and that you have write access to it.<br>'. | ||
300 | 'It currently points to %s.<br>'. | ||
301 | 'On some browsers, accessing your server via a hostname like \'localhost\' '. | ||
302 | 'or any custom hostname without a dot causes cookie storage to fail. '. | ||
303 | 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>' | ||
304 | ); | ||
305 | $msg = sprintf($msg, session_save_path()); | ||
306 | echo $msg; | ||
307 | echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>'; | ||
308 | die; | ||
309 | } | ||
310 | if (!isset($_SESSION['session_tested'])) { | ||
311 | // Step 1 : Try to store data in session and reload page. | ||
312 | $_SESSION['session_tested'] = 'Working'; // Try to set a variable in session. | ||
313 | header('Location: '.index_url($_SERVER).'?test_session'); // Redirect to check stored data. | ||
314 | } | ||
315 | if (isset($_GET['test_session'])) { | ||
316 | // Step 3: Sessions are OK. Remove test parameter from URL. | ||
317 | header('Location: '.index_url($_SERVER)); | ||
318 | } | ||
319 | |||
320 | |||
321 | if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) { | ||
322 | $tz = 'UTC'; | ||
323 | if (!empty($_POST['continent']) && !empty($_POST['city']) | ||
324 | && isTimeZoneValid($_POST['continent'], $_POST['city']) | ||
325 | ) { | ||
326 | $tz = $_POST['continent'].'/'.$_POST['city']; | ||
327 | } | ||
328 | $conf->set('general.timezone', $tz); | ||
329 | $login = $_POST['setlogin']; | ||
330 | $conf->set('credentials.login', $login); | ||
331 | $salt = sha1(uniqid('', true) .'_'. mt_rand()); | ||
332 | $conf->set('credentials.salt', $salt); | ||
333 | $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt)); | ||
334 | if (!empty($_POST['title'])) { | ||
335 | $conf->set('general.title', escape($_POST['title'])); | ||
336 | } else { | ||
337 | $conf->set('general.title', 'Shared bookmarks on '.escape(index_url($_SERVER))); | ||
338 | } | ||
339 | $conf->set('translation.language', escape($_POST['language'])); | ||
340 | $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); | ||
341 | $conf->set('api.enabled', !empty($_POST['enableApi'])); | ||
342 | $conf->set( | ||
343 | 'api.secret', | ||
344 | generate_api_secret( | ||
345 | $conf->get('credentials.login'), | ||
346 | $conf->get('credentials.salt') | ||
347 | ) | ||
348 | ); | ||
349 | try { | ||
350 | // Everything is ok, let's create config file. | ||
351 | $conf->write($loginManager->isLoggedIn()); | ||
352 | } catch (Exception $e) { | ||
353 | error_log( | ||
354 | 'ERROR while writing config file after installation.' . PHP_EOL . | ||
355 | $e->getMessage() | ||
356 | ); | ||
357 | |||
358 | // TODO: do not handle exceptions/errors in JS. | ||
359 | echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>'; | ||
360 | exit; | ||
361 | } | ||
362 | |||
363 | $history = new History($conf->get('resource.history')); | ||
364 | $bookmarkService = new BookmarkFileService($conf, $history, true); | ||
365 | if ($bookmarkService->count() === 0) { | ||
366 | $bookmarkService->initialize(); | ||
367 | } | ||
368 | |||
369 | echo '<script>alert(' | ||
370 | .'"Shaarli is now configured. ' | ||
371 | .'Please enter your login/password and start shaaring your bookmarks!"' | ||
372 | .');document.location=\'./login\';</script>'; | ||
373 | exit; | ||
374 | } | ||
375 | |||
376 | $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken()); | ||
377 | list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); | ||
378 | $PAGE->assign('continents', $continents); | ||
379 | $PAGE->assign('cities', $cities); | ||
380 | $PAGE->assign('languages', Languages::getAvailableLanguages()); | ||
381 | $PAGE->renderPage('install'); | ||
382 | exit; | ||
383 | } | ||
384 | |||
385 | if (!isset($_SESSION['LINKS_PER_PAGE'])) { | 252 | if (!isset($_SESSION['LINKS_PER_PAGE'])) { |
386 | $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); | 253 | $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); |
387 | } | 254 | } |
388 | 255 | ||
389 | $containerBuilder = new ContainerBuilder($conf, $sessionManager, $loginManager); | 256 | $containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager); |
390 | $container = $containerBuilder->build(); | 257 | $container = $containerBuilder->build(); |
391 | $app = new App($container); | 258 | $app = new App($container); |
392 | 259 | ||
@@ -408,6 +275,10 @@ $app->group('/api/v1', function () { | |||
408 | })->add('\Shaarli\Api\ApiMiddleware'); | 275 | })->add('\Shaarli\Api\ApiMiddleware'); |
409 | 276 | ||
410 | $app->group('', function () { | 277 | $app->group('', function () { |
278 | $this->get('/install', '\Shaarli\Front\Controller\Visitor\InstallController:index')->setName('displayInstall'); | ||
279 | $this->get('/install/session-test', '\Shaarli\Front\Controller\Visitor\InstallController:sessionTest'); | ||
280 | $this->post('/install', '\Shaarli\Front\Controller\Visitor\InstallController:save')->setName('saveInstall'); | ||
281 | |||
411 | /* -- PUBLIC --*/ | 282 | /* -- PUBLIC --*/ |
412 | $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index'); | 283 | $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index'); |
413 | $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink'); | 284 | $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 @@ require_once 'application/bookmark/LinkUtils.php'; | |||
18 | require_once 'application/Utils.php'; | 18 | require_once 'application/Utils.php'; |
19 | require_once 'application/http/UrlUtils.php'; | 19 | require_once 'application/http/UrlUtils.php'; |
20 | require_once 'application/http/HttpUtils.php'; | 20 | require_once 'application/http/HttpUtils.php'; |
21 | require_once 'tests/utils/ReferenceLinkDB.php'; | ||
22 | require_once 'tests/utils/ReferenceHistory.php'; | ||
23 | require_once 'tests/utils/FakeBookmarkService.php'; | ||
24 | require_once 'tests/container/ShaarliTestContainer.php'; | 21 | require_once 'tests/container/ShaarliTestContainer.php'; |
25 | require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php'; | 22 | require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php'; |
26 | require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php'; | 23 | require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php'; |
24 | require_once 'tests/updater/DummyUpdater.php'; | ||
25 | require_once 'tests/utils/FakeBookmarkService.php'; | ||
26 | require_once 'tests/utils/FakeConfigManager.php'; | ||
27 | require_once 'tests/utils/ReferenceHistory.php'; | ||
28 | require_once 'tests/utils/ReferenceLinkDB.php'; | ||
29 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; | ||
30 | |||
31 | \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\Feed\FeedBuilder; | |||
11 | use Shaarli\Formatter\FormatterFactory; | 11 | use Shaarli\Formatter\FormatterFactory; |
12 | use Shaarli\History; | 12 | use Shaarli\History; |
13 | use Shaarli\Http\HttpAccess; | 13 | use Shaarli\Http\HttpAccess; |
14 | use Shaarli\Netscape\NetscapeBookmarkUtils; | ||
14 | use Shaarli\Plugin\PluginManager; | 15 | use Shaarli\Plugin\PluginManager; |
15 | use Shaarli\Render\PageBuilder; | 16 | use Shaarli\Render\PageBuilder; |
16 | use Shaarli\Render\PageCacheManager; | 17 | use Shaarli\Render\PageCacheManager; |
18 | use Shaarli\Security\CookieManager; | ||
17 | use Shaarli\Security\LoginManager; | 19 | use Shaarli\Security\LoginManager; |
18 | use Shaarli\Security\SessionManager; | 20 | use Shaarli\Security\SessionManager; |
19 | use Shaarli\Thumbnailer; | 21 | use Shaarli\Thumbnailer; |
22 | use Shaarli\Updater\Updater; | ||
20 | 23 | ||
21 | class ContainerBuilderTest extends TestCase | 24 | class ContainerBuilderTest extends TestCase |
22 | { | 25 | { |
@@ -32,10 +35,14 @@ class ContainerBuilderTest extends TestCase | |||
32 | /** @var ContainerBuilder */ | 35 | /** @var ContainerBuilder */ |
33 | protected $containerBuilder; | 36 | protected $containerBuilder; |
34 | 37 | ||
38 | /** @var CookieManager */ | ||
39 | protected $cookieManager; | ||
40 | |||
35 | public function setUp(): void | 41 | public function setUp(): void |
36 | { | 42 | { |
37 | $this->conf = new ConfigManager('tests/utils/config/configJson'); | 43 | $this->conf = new ConfigManager('tests/utils/config/configJson'); |
38 | $this->sessionManager = $this->createMock(SessionManager::class); | 44 | $this->sessionManager = $this->createMock(SessionManager::class); |
45 | $this->cookieManager = $this->createMock(CookieManager::class); | ||
39 | 46 | ||
40 | $this->loginManager = $this->createMock(LoginManager::class); | 47 | $this->loginManager = $this->createMock(LoginManager::class); |
41 | $this->loginManager->method('isLoggedIn')->willReturn(true); | 48 | $this->loginManager->method('isLoggedIn')->willReturn(true); |
@@ -43,6 +50,7 @@ class ContainerBuilderTest extends TestCase | |||
43 | $this->containerBuilder = new ContainerBuilder( | 50 | $this->containerBuilder = new ContainerBuilder( |
44 | $this->conf, | 51 | $this->conf, |
45 | $this->sessionManager, | 52 | $this->sessionManager, |
53 | $this->cookieManager, | ||
46 | $this->loginManager | 54 | $this->loginManager |
47 | ); | 55 | ); |
48 | } | 56 | } |
@@ -53,6 +61,7 @@ class ContainerBuilderTest extends TestCase | |||
53 | 61 | ||
54 | static::assertInstanceOf(ConfigManager::class, $container->conf); | 62 | static::assertInstanceOf(ConfigManager::class, $container->conf); |
55 | static::assertInstanceOf(SessionManager::class, $container->sessionManager); | 63 | static::assertInstanceOf(SessionManager::class, $container->sessionManager); |
64 | static::assertInstanceOf(CookieManager::class, $container->cookieManager); | ||
56 | static::assertInstanceOf(LoginManager::class, $container->loginManager); | 65 | static::assertInstanceOf(LoginManager::class, $container->loginManager); |
57 | static::assertInstanceOf(History::class, $container->history); | 66 | static::assertInstanceOf(History::class, $container->history); |
58 | static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService); | 67 | static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService); |
@@ -63,6 +72,8 @@ class ContainerBuilderTest extends TestCase | |||
63 | static::assertInstanceOf(FeedBuilder::class, $container->feedBuilder); | 72 | static::assertInstanceOf(FeedBuilder::class, $container->feedBuilder); |
64 | static::assertInstanceOf(Thumbnailer::class, $container->thumbnailer); | 73 | static::assertInstanceOf(Thumbnailer::class, $container->thumbnailer); |
65 | static::assertInstanceOf(HttpAccess::class, $container->httpAccess); | 74 | static::assertInstanceOf(HttpAccess::class, $container->httpAccess); |
75 | static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils); | ||
76 | static::assertInstanceOf(Updater::class, $container->updater); | ||
66 | 77 | ||
67 | // Set by the middleware | 78 | // Set by the middleware |
68 | static::assertNull($container->basePath); | 79 | 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 @@ use Slim\Http\Uri; | |||
19 | 19 | ||
20 | class ShaarliMiddlewareTest extends TestCase | 20 | class ShaarliMiddlewareTest extends TestCase |
21 | { | 21 | { |
22 | protected const TMP_MOCK_FILE = '.tmp'; | ||
23 | |||
22 | /** @var ShaarliContainer */ | 24 | /** @var ShaarliContainer */ |
23 | protected $container; | 25 | protected $container; |
24 | 26 | ||
@@ -29,12 +31,21 @@ class ShaarliMiddlewareTest extends TestCase | |||
29 | { | 31 | { |
30 | $this->container = $this->createMock(ShaarliContainer::class); | 32 | $this->container = $this->createMock(ShaarliContainer::class); |
31 | 33 | ||
34 | touch(static::TMP_MOCK_FILE); | ||
35 | |||
32 | $this->container->conf = $this->createMock(ConfigManager::class); | 36 | $this->container->conf = $this->createMock(ConfigManager::class); |
37 | $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE); | ||
38 | |||
33 | $this->container->loginManager = $this->createMock(LoginManager::class); | 39 | $this->container->loginManager = $this->createMock(LoginManager::class); |
34 | 40 | ||
35 | $this->middleware = new ShaarliMiddleware($this->container); | 41 | $this->middleware = new ShaarliMiddleware($this->container); |
36 | } | 42 | } |
37 | 43 | ||
44 | public function tearDown() | ||
45 | { | ||
46 | unlink(static::TMP_MOCK_FILE); | ||
47 | } | ||
48 | |||
38 | /** | 49 | /** |
39 | * Test middleware execution with valid controller call | 50 | * Test middleware execution with valid controller call |
40 | */ | 51 | */ |
@@ -179,6 +190,7 @@ class ShaarliMiddlewareTest extends TestCase | |||
179 | $this->container->conf->method('get')->willReturnCallback(function (string $key): string { | 190 | $this->container->conf->method('get')->willReturnCallback(function (string $key): string { |
180 | return $key; | 191 | return $key; |
181 | }); | 192 | }); |
193 | $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE); | ||
182 | 194 | ||
183 | $this->container->pageCacheManager = $this->createMock(PageCacheManager::class); | 195 | $this->container->pageCacheManager = $this->createMock(PageCacheManager::class); |
184 | $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches'); | 196 | $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 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Front\Controller\Admin; | 5 | namespace Shaarli\Front\Controller\Admin; |
6 | 6 | ||
7 | /** Override PHP builtin setcookie function in the local namespace to mock it... more or less */ | ||
8 | if (!function_exists('Shaarli\Front\Controller\Admin\setcookie')) { | ||
9 | function setcookie(string $name, string $value): void { | ||
10 | $_COOKIE[$name] = $value; | ||
11 | } | ||
12 | } | ||
13 | |||
14 | use PHPUnit\Framework\TestCase; | 7 | use PHPUnit\Framework\TestCase; |
8 | use Shaarli\Security\CookieManager; | ||
15 | use Shaarli\Security\LoginManager; | 9 | use Shaarli\Security\LoginManager; |
16 | use Shaarli\Security\SessionManager; | 10 | use Shaarli\Security\SessionManager; |
17 | use Slim\Http\Request; | 11 | use Slim\Http\Request; |
@@ -29,8 +23,6 @@ class LogoutControllerTest extends TestCase | |||
29 | $this->createContainer(); | 23 | $this->createContainer(); |
30 | 24 | ||
31 | $this->controller = new LogoutController($this->container); | 25 | $this->controller = new LogoutController($this->container); |
32 | |||
33 | setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, $cookie = 'hi there'); | ||
34 | } | 26 | } |
35 | 27 | ||
36 | public function testValidControllerInvoke(): void | 28 | public function testValidControllerInvoke(): void |
@@ -43,13 +35,17 @@ class LogoutControllerTest extends TestCase | |||
43 | $this->container->sessionManager = $this->createMock(SessionManager::class); | 35 | $this->container->sessionManager = $this->createMock(SessionManager::class); |
44 | $this->container->sessionManager->expects(static::once())->method('logout'); | 36 | $this->container->sessionManager->expects(static::once())->method('logout'); |
45 | 37 | ||
46 | static::assertSame('hi there', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]); | 38 | $this->container->cookieManager = $this->createMock(CookieManager::class); |
39 | $this->container->cookieManager | ||
40 | ->expects(static::once()) | ||
41 | ->method('setCookieParameter') | ||
42 | ->with(CookieManager::STAY_SIGNED_IN, 'false', 0, '/subfolder/') | ||
43 | ; | ||
47 | 44 | ||
48 | $result = $this->controller->index($request, $response); | 45 | $result = $this->controller->index($request, $response); |
49 | 46 | ||
50 | static::assertInstanceOf(Response::class, $result); | 47 | static::assertInstanceOf(Response::class, $result); |
51 | static::assertSame(302, $result->getStatusCode()); | 48 | static::assertSame(302, $result->getStatusCode()); |
52 | static::assertSame(['/subfolder/'], $result->getHeader('location')); | 49 | static::assertSame(['/subfolder/'], $result->getHeader('location')); |
53 | static::assertSame('false', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]); | ||
54 | } | 50 | } |
55 | } | 51 | } |
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Front\Controller\Visitor; | ||
6 | |||
7 | use PHPUnit\Framework\TestCase; | ||
8 | use Shaarli\Config\ConfigManager; | ||
9 | use Shaarli\Front\Exception\AlreadyInstalledException; | ||
10 | use Shaarli\Security\SessionManager; | ||
11 | use Slim\Http\Request; | ||
12 | use Slim\Http\Response; | ||
13 | |||
14 | class InstallControllerTest extends TestCase | ||
15 | { | ||
16 | use FrontControllerMockHelper; | ||
17 | |||
18 | const MOCK_FILE = '.tmp'; | ||
19 | |||
20 | /** @var InstallController */ | ||
21 | protected $controller; | ||
22 | |||
23 | public function setUp(): void | ||
24 | { | ||
25 | $this->createContainer(); | ||
26 | |||
27 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
28 | $this->container->conf->method('getConfigFileExt')->willReturn(static::MOCK_FILE); | ||
29 | $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) { | ||
30 | if ($key === 'resource.raintpl_tpl') { | ||
31 | return '.'; | ||
32 | } | ||
33 | |||
34 | return $default ?? $key; | ||
35 | }); | ||
36 | |||
37 | $this->controller = new InstallController($this->container); | ||
38 | } | ||
39 | |||
40 | protected function tearDown(): void | ||
41 | { | ||
42 | if (file_exists(static::MOCK_FILE)) { | ||
43 | unlink(static::MOCK_FILE); | ||
44 | } | ||
45 | } | ||
46 | |||
47 | /** | ||
48 | * Test displaying install page with valid session. | ||
49 | */ | ||
50 | public function testInstallIndexWithValidSession(): void | ||
51 | { | ||
52 | $assignedVariables = []; | ||
53 | $this->assignTemplateVars($assignedVariables); | ||
54 | |||
55 | $request = $this->createMock(Request::class); | ||
56 | $response = new Response(); | ||
57 | |||
58 | $this->container->sessionManager = $this->createMock(SessionManager::class); | ||
59 | $this->container->sessionManager | ||
60 | ->method('getSessionParameter') | ||
61 | ->willReturnCallback(function (string $key, $default) { | ||
62 | return $key === 'session_tested' ? 'Working' : $default; | ||
63 | }) | ||
64 | ; | ||
65 | |||
66 | $result = $this->controller->index($request, $response); | ||
67 | |||
68 | static::assertSame(200, $result->getStatusCode()); | ||
69 | static::assertSame('install', (string) $result->getBody()); | ||
70 | |||
71 | static::assertIsArray($assignedVariables['continents']); | ||
72 | static::assertSame('Africa', $assignedVariables['continents'][0]); | ||
73 | static::assertSame('UTC', $assignedVariables['continents']['selected']); | ||
74 | |||
75 | static::assertIsArray($assignedVariables['cities']); | ||
76 | static::assertSame(['continent' => 'Africa', 'city' => 'Abidjan'], $assignedVariables['cities'][0]); | ||
77 | static::assertSame('UTC', $assignedVariables['continents']['selected']); | ||
78 | |||
79 | static::assertIsArray($assignedVariables['languages']); | ||
80 | static::assertSame('Automatic', $assignedVariables['languages']['auto']); | ||
81 | static::assertSame('French', $assignedVariables['languages']['fr']); | ||
82 | } | ||
83 | |||
84 | /** | ||
85 | * Instantiate the install controller with an existing config file: exception. | ||
86 | */ | ||
87 | public function testInstallWithExistingConfigFile(): void | ||
88 | { | ||
89 | $this->expectException(AlreadyInstalledException::class); | ||
90 | |||
91 | touch(static::MOCK_FILE); | ||
92 | |||
93 | $this->controller = new InstallController($this->container); | ||
94 | } | ||
95 | |||
96 | /** | ||
97 | * Call controller without session yet defined, redirect to test session install page. | ||
98 | */ | ||
99 | public function testInstallRedirectToSessionTest(): void | ||
100 | { | ||
101 | $request = $this->createMock(Request::class); | ||
102 | $response = new Response(); | ||
103 | |||
104 | $this->container->sessionManager = $this->createMock(SessionManager::class); | ||
105 | $this->container->sessionManager | ||
106 | ->expects(static::once()) | ||
107 | ->method('setSessionParameter') | ||
108 | ->with(InstallController::SESSION_TEST_KEY, InstallController::SESSION_TEST_VALUE) | ||
109 | ; | ||
110 | |||
111 | $result = $this->controller->index($request, $response); | ||
112 | |||
113 | static::assertSame(302, $result->getStatusCode()); | ||
114 | static::assertSame('/subfolder/install/session-test', $result->getHeader('location')[0]); | ||
115 | } | ||
116 | |||
117 | /** | ||
118 | * Call controller in session test mode: valid session then redirect to install page. | ||
119 | */ | ||
120 | public function testInstallSessionTestValid(): void | ||
121 | { | ||
122 | $request = $this->createMock(Request::class); | ||
123 | $response = new Response(); | ||
124 | |||
125 | $this->container->sessionManager = $this->createMock(SessionManager::class); | ||
126 | $this->container->sessionManager | ||
127 | ->method('getSessionParameter') | ||
128 | ->with(InstallController::SESSION_TEST_KEY) | ||
129 | ->willReturn(InstallController::SESSION_TEST_VALUE) | ||
130 | ; | ||
131 | |||
132 | $result = $this->controller->sessionTest($request, $response); | ||
133 | |||
134 | static::assertSame(302, $result->getStatusCode()); | ||
135 | static::assertSame('/subfolder/install', $result->getHeader('location')[0]); | ||
136 | } | ||
137 | |||
138 | /** | ||
139 | * Call controller in session test mode: invalid session then redirect to error page. | ||
140 | */ | ||
141 | public function testInstallSessionTestError(): void | ||
142 | { | ||
143 | $assignedVars = []; | ||
144 | $this->assignTemplateVars($assignedVars); | ||
145 | |||
146 | $request = $this->createMock(Request::class); | ||
147 | $response = new Response(); | ||
148 | |||
149 | $this->container->sessionManager = $this->createMock(SessionManager::class); | ||
150 | $this->container->sessionManager | ||
151 | ->method('getSessionParameter') | ||
152 | ->with(InstallController::SESSION_TEST_KEY) | ||
153 | ->willReturn('KO') | ||
154 | ; | ||
155 | |||
156 | $result = $this->controller->sessionTest($request, $response); | ||
157 | |||
158 | static::assertSame(200, $result->getStatusCode()); | ||
159 | static::assertSame('error', (string) $result->getBody()); | ||
160 | static::assertStringStartsWith( | ||
161 | '<pre>Sessions do not seem to work correctly on your server', | ||
162 | $assignedVars['message'] | ||
163 | ); | ||
164 | } | ||
165 | |||
166 | /** | ||
167 | * Test saving valid data from install form. Also initialize datastore. | ||
168 | */ | ||
169 | public function testSaveInstallValid(): void | ||
170 | { | ||
171 | $providedParameters = [ | ||
172 | 'continent' => 'Europe', | ||
173 | 'city' => 'Berlin', | ||
174 | 'setlogin' => 'bob', | ||
175 | 'setpassword' => 'password', | ||
176 | 'title' => 'Shaarli', | ||
177 | 'language' => 'fr', | ||
178 | 'updateCheck' => true, | ||
179 | 'enableApi' => true, | ||
180 | ]; | ||
181 | |||
182 | $expectedSettings = [ | ||
183 | 'general.timezone' => 'Europe/Berlin', | ||
184 | 'credentials.login' => 'bob', | ||
185 | 'credentials.salt' => '_NOT_EMPTY', | ||
186 | 'credentials.hash' => '_NOT_EMPTY', | ||
187 | 'general.title' => 'Shaarli', | ||
188 | 'translation.language' => 'en', | ||
189 | 'updates.check_updates' => true, | ||
190 | 'api.enabled' => true, | ||
191 | 'api.secret' => '_NOT_EMPTY', | ||
192 | ]; | ||
193 | |||
194 | $request = $this->createMock(Request::class); | ||
195 | $request->method('getParam')->willReturnCallback(function (string $key) use ($providedParameters) { | ||
196 | return $providedParameters[$key] ?? null; | ||
197 | }); | ||
198 | $response = new Response(); | ||
199 | |||
200 | $this->container->conf = $this->createMock(ConfigManager::class); | ||
201 | $this->container->conf | ||
202 | ->method('get') | ||
203 | ->willReturnCallback(function (string $key, $value) { | ||
204 | if ($key === 'credentials.login') { | ||
205 | return 'bob'; | ||
206 | } elseif ($key === 'credentials.salt') { | ||
207 | return 'salt'; | ||
208 | } | ||
209 | |||
210 | return $value; | ||
211 | }) | ||
212 | ; | ||
213 | $this->container->conf | ||
214 | ->expects(static::exactly(count($expectedSettings))) | ||
215 | ->method('set') | ||
216 | ->willReturnCallback(function (string $key, $value) use ($expectedSettings) { | ||
217 | if ($expectedSettings[$key] ?? null === '_NOT_EMPTY') { | ||
218 | static::assertNotEmpty($value); | ||
219 | } else { | ||
220 | static::assertSame($expectedSettings[$key], $value); | ||
221 | } | ||
222 | }) | ||
223 | ; | ||
224 | $this->container->conf->expects(static::once())->method('write'); | ||
225 | |||
226 | $this->container->bookmarkService->expects(static::once())->method('count')->willReturn(0); | ||
227 | $this->container->bookmarkService->expects(static::once())->method('initialize'); | ||
228 | |||
229 | $this->container->sessionManager | ||
230 | ->expects(static::once()) | ||
231 | ->method('setSessionParameter') | ||
232 | ->with(SessionManager::KEY_SUCCESS_MESSAGES) | ||
233 | ; | ||
234 | |||
235 | $result = $this->controller->save($request, $response); | ||
236 | |||
237 | static::assertSame(302, $result->getStatusCode()); | ||
238 | static::assertSame('/subfolder/', $result->getHeader('location')[0]); | ||
239 | } | ||
240 | |||
241 | /** | ||
242 | * Test default settings (timezone and title). | ||
243 | * Also check that bookmarks are not initialized if | ||
244 | */ | ||
245 | public function testSaveInstallDefaultValues(): void | ||
246 | { | ||
247 | $confSettings = []; | ||
248 | |||
249 | $request = $this->createMock(Request::class); | ||
250 | $response = new Response(); | ||
251 | |||
252 | $this->container->conf->method('set')->willReturnCallback(function (string $key, $value) use (&$confSettings) { | ||
253 | $confSettings[$key] = $value; | ||
254 | }); | ||
255 | |||
256 | $result = $this->controller->save($request, $response); | ||
257 | |||
258 | static::assertSame(302, $result->getStatusCode()); | ||
259 | static::assertSame('/subfolder/', $result->getHeader('location')[0]); | ||
260 | |||
261 | static::assertSame('UTC', $confSettings['general.timezone']); | ||
262 | static::assertSame('Shared bookmarks on http://shaarli', $confSettings['general.title']); | ||
263 | } | ||
264 | } | ||
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 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
2 | /** | 3 | /** |
3 | * Cache tests | 4 | * Cache tests |
4 | */ | 5 | */ |
6 | |||
5 | namespace Shaarli\Render; | 7 | namespace Shaarli\Render; |
6 | 8 | ||
7 | use PHPUnit\Framework\TestCase; | 9 | use PHPUnit\Framework\TestCase; |
8 | use Shaarli\Security\SessionManager; | 10 | use Shaarli\Security\SessionManager; |
9 | 11 | ||
10 | // required to access $_SESSION array | ||
11 | session_start(); | ||
12 | |||
13 | /** | 12 | /** |
14 | * Unitary tests for cached pages | 13 | * Unitary tests for cached pages |
15 | */ | 14 | */ |
diff --git a/tests/security/LoginManagerTest.php b/tests/security/LoginManagerTest.php index 8fd1698c..f242be09 100644 --- a/tests/security/LoginManagerTest.php +++ b/tests/security/LoginManagerTest.php | |||
@@ -1,7 +1,6 @@ | |||
1 | <?php | 1 | <?php |
2 | namespace Shaarli\Security; | ||
3 | 2 | ||
4 | require_once 'tests/utils/FakeConfigManager.php'; | 3 | namespace Shaarli\Security; |
5 | 4 | ||
6 | use PHPUnit\Framework\TestCase; | 5 | use PHPUnit\Framework\TestCase; |
7 | 6 | ||
@@ -58,6 +57,9 @@ class LoginManagerTest extends TestCase | |||
58 | /** @var string Salt used by hash functions */ | 57 | /** @var string Salt used by hash functions */ |
59 | protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2'; | 58 | protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2'; |
60 | 59 | ||
60 | /** @var CookieManager */ | ||
61 | protected $cookieManager; | ||
62 | |||
61 | /** | 63 | /** |
62 | * Prepare or reset test resources | 64 | * Prepare or reset test resources |
63 | */ | 65 | */ |
@@ -84,8 +86,12 @@ class LoginManagerTest extends TestCase | |||
84 | $this->cookie = []; | 86 | $this->cookie = []; |
85 | $this->session = []; | 87 | $this->session = []; |
86 | 88 | ||
87 | $this->sessionManager = new SessionManager($this->session, $this->configManager); | 89 | $this->cookieManager = $this->createMock(CookieManager::class); |
88 | $this->loginManager = new LoginManager($this->configManager, $this->sessionManager); | 90 | $this->cookieManager->method('getCookieParameter')->willReturnCallback(function (string $key) { |
91 | return $this->cookie[$key] ?? null; | ||
92 | }); | ||
93 | $this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path'); | ||
94 | $this->loginManager = new LoginManager($this->configManager, $this->sessionManager, $this->cookieManager); | ||
89 | $this->server['REMOTE_ADDR'] = $this->ipAddr; | 95 | $this->server['REMOTE_ADDR'] = $this->ipAddr; |
90 | } | 96 | } |
91 | 97 | ||
@@ -193,8 +199,8 @@ class LoginManagerTest extends TestCase | |||
193 | $configManager = new \FakeConfigManager([ | 199 | $configManager = new \FakeConfigManager([ |
194 | 'resource.ban_file' => $this->banFile, | 200 | 'resource.ban_file' => $this->banFile, |
195 | ]); | 201 | ]); |
196 | $loginManager = new LoginManager($configManager, null); | 202 | $loginManager = new LoginManager($configManager, null, $this->cookieManager); |
197 | $loginManager->checkLoginState([], ''); | 203 | $loginManager->checkLoginState(''); |
198 | 204 | ||
199 | $this->assertFalse($loginManager->isLoggedIn()); | 205 | $this->assertFalse($loginManager->isLoggedIn()); |
200 | } | 206 | } |
@@ -210,9 +216,9 @@ class LoginManagerTest extends TestCase | |||
210 | 'expires_on' => time() + 100, | 216 | 'expires_on' => time() + 100, |
211 | ]; | 217 | ]; |
212 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); | 218 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); |
213 | $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope'; | 219 | $this->cookie[CookieManager::STAY_SIGNED_IN] = 'nope'; |
214 | 220 | ||
215 | $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); | 221 | $this->loginManager->checkLoginState($this->clientIpAddress); |
216 | 222 | ||
217 | $this->assertTrue($this->loginManager->isLoggedIn()); | 223 | $this->assertTrue($this->loginManager->isLoggedIn()); |
218 | $this->assertTrue(empty($this->session['username'])); | 224 | $this->assertTrue(empty($this->session['username'])); |
@@ -224,9 +230,9 @@ class LoginManagerTest extends TestCase | |||
224 | public function testCheckLoginStateStaySignedInWithValidToken() | 230 | public function testCheckLoginStateStaySignedInWithValidToken() |
225 | { | 231 | { |
226 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); | 232 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); |
227 | $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken(); | 233 | $this->cookie[CookieManager::STAY_SIGNED_IN] = $this->loginManager->getStaySignedInToken(); |
228 | 234 | ||
229 | $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); | 235 | $this->loginManager->checkLoginState($this->clientIpAddress); |
230 | 236 | ||
231 | $this->assertTrue($this->loginManager->isLoggedIn()); | 237 | $this->assertTrue($this->loginManager->isLoggedIn()); |
232 | $this->assertEquals($this->login, $this->session['username']); | 238 | $this->assertEquals($this->login, $this->session['username']); |
@@ -241,7 +247,7 @@ class LoginManagerTest extends TestCase | |||
241 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); | 247 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); |
242 | $this->session['expires_on'] = time() - 100; | 248 | $this->session['expires_on'] = time() - 100; |
243 | 249 | ||
244 | $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); | 250 | $this->loginManager->checkLoginState($this->clientIpAddress); |
245 | 251 | ||
246 | $this->assertFalse($this->loginManager->isLoggedIn()); | 252 | $this->assertFalse($this->loginManager->isLoggedIn()); |
247 | } | 253 | } |
@@ -253,7 +259,7 @@ class LoginManagerTest extends TestCase | |||
253 | { | 259 | { |
254 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); | 260 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); |
255 | 261 | ||
256 | $this->loginManager->checkLoginState($this->cookie, '10.7.157.98'); | 262 | $this->loginManager->checkLoginState('10.7.157.98'); |
257 | 263 | ||
258 | $this->assertFalse($this->loginManager->isLoggedIn()); | 264 | $this->assertFalse($this->loginManager->isLoggedIn()); |
259 | } | 265 | } |
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 @@ | |||
1 | <?php | 1 | <?php |
2 | require_once 'tests/utils/FakeConfigManager.php'; | ||
3 | 2 | ||
4 | // Initialize reference data _before_ PHPUnit starts a session | 3 | namespace Shaarli\Security; |
5 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; | ||
6 | ReferenceSessionIdHashes::genAllHashes(); | ||
7 | 4 | ||
8 | use PHPUnit\Framework\TestCase; | 5 | use PHPUnit\Framework\TestCase; |
9 | use Shaarli\Security\SessionManager; | ||
10 | 6 | ||
11 | /** | 7 | /** |
12 | * Test coverage for SessionManager | 8 | * Test coverage for SessionManager |
@@ -30,7 +26,7 @@ class SessionManagerTest extends TestCase | |||
30 | */ | 26 | */ |
31 | public static function setUpBeforeClass() | 27 | public static function setUpBeforeClass() |
32 | { | 28 | { |
33 | self::$sidHashes = ReferenceSessionIdHashes::getHashes(); | 29 | self::$sidHashes = \ReferenceSessionIdHashes::getHashes(); |
34 | } | 30 | } |
35 | 31 | ||
36 | /** | 32 | /** |
@@ -38,13 +34,13 @@ class SessionManagerTest extends TestCase | |||
38 | */ | 34 | */ |
39 | public function setUp() | 35 | public function setUp() |
40 | { | 36 | { |
41 | $this->conf = new FakeConfigManager([ | 37 | $this->conf = new \FakeConfigManager([ |
42 | 'credentials.login' => 'johndoe', | 38 | 'credentials.login' => 'johndoe', |
43 | 'credentials.salt' => 'salt', | 39 | 'credentials.salt' => 'salt', |
44 | 'security.session_protection_disabled' => false, | 40 | 'security.session_protection_disabled' => false, |
45 | ]); | 41 | ]); |
46 | $this->session = []; | 42 | $this->session = []; |
47 | $this->sessionManager = new SessionManager($this->session, $this->conf); | 43 | $this->sessionManager = new SessionManager($this->session, $this->conf, 'session_path'); |
48 | } | 44 | } |
49 | 45 | ||
50 | /** | 46 | /** |
@@ -69,7 +65,7 @@ class SessionManagerTest extends TestCase | |||
69 | $token => 1, | 65 | $token => 1, |
70 | ], | 66 | ], |
71 | ]; | 67 | ]; |
72 | $sessionManager = new SessionManager($session, $this->conf); | 68 | $sessionManager = new SessionManager($session, $this->conf, 'session_path'); |
73 | 69 | ||
74 | // check and destroy the token | 70 | // check and destroy the token |
75 | $this->assertTrue($sessionManager->checkToken($token)); | 71 | $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\Bookmark\BookmarkServiceInterface; | |||
7 | use Shaarli\Config\ConfigManager; | 7 | use Shaarli\Config\ConfigManager; |
8 | use Shaarli\History; | 8 | use Shaarli\History; |
9 | 9 | ||
10 | require_once 'tests/updater/DummyUpdater.php'; | ||
11 | require_once 'tests/utils/ReferenceLinkDB.php'; | ||
12 | require_once 'inc/rain.tpl.class.php'; | ||
13 | 10 | ||
14 | /** | 11 | /** |
15 | * Class UpdaterTest. | 12 | * Class UpdaterTest. |
@@ -35,6 +32,9 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase | |||
35 | /** @var BookmarkServiceInterface */ | 32 | /** @var BookmarkServiceInterface */ |
36 | protected $bookmarkService; | 33 | protected $bookmarkService; |
37 | 34 | ||
35 | /** @var \ReferenceLinkDB */ | ||
36 | protected $refDB; | ||
37 | |||
38 | /** @var Updater */ | 38 | /** @var Updater */ |
39 | protected $updater; | 39 | protected $updater; |
40 | 40 | ||
@@ -43,6 +43,9 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase | |||
43 | */ | 43 | */ |
44 | public function setUp() | 44 | public function setUp() |
45 | { | 45 | { |
46 | $this->refDB = new \ReferenceLinkDB(); | ||
47 | $this->refDB->write(self::$testDatastore); | ||
48 | |||
46 | copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); | 49 | copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); |
47 | $this->conf = new ConfigManager(self::$configFile); | 50 | $this->conf = new ConfigManager(self::$configFile); |
48 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true); | 51 | $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true); |
@@ -181,9 +184,40 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase | |||
181 | 184 | ||
182 | public function testUpdateMethodRelativeHomeLinkRename(): void | 185 | public function testUpdateMethodRelativeHomeLinkRename(): void |
183 | { | 186 | { |
187 | $this->updater->setBasePath('/subfolder'); | ||
184 | $this->conf->set('general.header_link', '?'); | 188 | $this->conf->set('general.header_link', '?'); |
189 | |||
190 | $this->updater->updateMethodRelativeHomeLink(); | ||
191 | |||
192 | static::assertSame('/subfolder/', $this->conf->get('general.header_link')); | ||
193 | } | ||
194 | |||
195 | public function testUpdateMethodRelativeHomeLinkDoNotRename(): void | ||
196 | { | ||
197 | $this->updater->setBasePath('/subfolder'); | ||
198 | $this->conf->set('general.header_link', '~/my-blog'); | ||
199 | |||
185 | $this->updater->updateMethodRelativeHomeLink(); | 200 | $this->updater->updateMethodRelativeHomeLink(); |
186 | 201 | ||
187 | static::assertSame(); | 202 | static::assertSame('~/my-blog', $this->conf->get('general.header_link')); |
203 | } | ||
204 | |||
205 | public function testUpdateMethodMigrateExistingNotesUrl(): void | ||
206 | { | ||
207 | $this->updater->setBasePath('/subfolder'); | ||
208 | |||
209 | $this->updater->updateMethodMigrateExistingNotesUrl(); | ||
210 | |||
211 | static::assertSame($this->refDB->getLinks()[0]->getUrl(), $this->bookmarkService->get(0)->getUrl()); | ||
212 | static::assertSame($this->refDB->getLinks()[1]->getUrl(), $this->bookmarkService->get(1)->getUrl()); | ||
213 | static::assertSame($this->refDB->getLinks()[4]->getUrl(), $this->bookmarkService->get(4)->getUrl()); | ||
214 | static::assertSame($this->refDB->getLinks()[6]->getUrl(), $this->bookmarkService->get(6)->getUrl()); | ||
215 | static::assertSame($this->refDB->getLinks()[7]->getUrl(), $this->bookmarkService->get(7)->getUrl()); | ||
216 | static::assertSame($this->refDB->getLinks()[8]->getUrl(), $this->bookmarkService->get(8)->getUrl()); | ||
217 | static::assertSame($this->refDB->getLinks()[9]->getUrl(), $this->bookmarkService->get(9)->getUrl()); | ||
218 | static::assertSame('/subfolder/shaare/WDWyig', $this->bookmarkService->get(42)->getUrl()); | ||
219 | static::assertSame('/subfolder/shaare/WDWyig', $this->bookmarkService->get(41)->getUrl()); | ||
220 | static::assertSame('/subfolder/shaare/0gCTjQ', $this->bookmarkService->get(10)->getUrl()); | ||
221 | static::assertSame('/subfolder/shaare/PCRizQ', $this->bookmarkService->get(11)->getUrl()); | ||
188 | } | 222 | } |
189 | } | 223 | } |
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 @@ | |||
15 | </pre> | 15 | </pre> |
16 | {/if} | 16 | {/if} |
17 | 17 | ||
18 | <img src="{asset_path}/img/sad_star.png#" alt=""> | 18 | <img src="{$asset_path}/img/sad_star.png#" alt=""> |
19 | </div> | 19 | </div> |
20 | {include="page.footer"} | 20 | {include="page.footer"} |
21 | </body> | 21 | </body> |
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 @@ | |||
10 | {$ratioLabelMobile='7-8'} | 10 | {$ratioLabelMobile='7-8'} |
11 | {$ratioInputMobile='1-8'} | 11 | {$ratioInputMobile='1-8'} |
12 | 12 | ||
13 | <form method="POST" action="{$base_path}/?do=install" name="installform" id="installform"> | 13 | <form method="POST" action="{$base_path}/install" name="installform" id="installform"> |
14 | <div class="pure-g"> | 14 | <div class="pure-g"> |
15 | <div class="pure-u-lg-1-6 pure-u-1-24"></div> | 15 | <div class="pure-u-lg-1-6 pure-u-1-24"></div> |
16 | <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete"> | 16 | <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete"> |
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 @@ | |||
184 | </div> | 184 | </div> |
185 | {/if} | 185 | {/if} |
186 | 186 | ||
187 | {if="!empty($global_errors) && $is_logged_in"} | 187 | {if="!empty($global_errors)"} |
188 | <div class="pure-g header-alert-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert"> | 188 | <div class="pure-g header-alert-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert"> |
189 | <div class="pure-u-2-24"></div> | 189 | <div class="pure-u-2-24"></div> |
190 | <div class="pure-u-20-24"> | 190 | <div class="pure-u-20-24"> |
@@ -198,7 +198,7 @@ | |||
198 | </div> | 198 | </div> |
199 | {/if} | 199 | {/if} |
200 | 200 | ||
201 | {if="!empty($global_warnings) && $is_logged_in"} | 201 | {if="!empty($global_warnings)"} |
202 | <div class="pure-g header-alert-message pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert"> | 202 | <div class="pure-g header-alert-message pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert"> |
203 | <div class="pure-u-2-24"></div> | 203 | <div class="pure-u-2-24"></div> |
204 | <div class="pure-u-20-24"> | 204 | <div class="pure-u-20-24"> |
@@ -212,7 +212,7 @@ | |||
212 | </div> | 212 | </div> |
213 | {/if} | 213 | {/if} |
214 | 214 | ||
215 | {if="!empty($global_successes) && $is_logged_in"} | 215 | {if="!empty($global_successes)"} |
216 | <div class="pure-g header-alert-message new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert"> | 216 | <div class="pure-g header-alert-message new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert"> |
217 | <div class="pure-u-2-24"></div> | 217 | <div class="pure-u-2-24"></div> |
218 | <div class="pure-u-20-24"> | 218 | <div class="pure-u-20-24"> |