aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/bookmark/BookmarkFileService.php26
-rw-r--r--application/bookmark/BookmarkInitializer.php12
-rw-r--r--application/bookmark/BookmarkServiceInterface.php13
-rw-r--r--application/container/ContainerBuilder.php7
-rw-r--r--application/container/ShaarliContainer.php3
-rw-r--r--application/front/ShaarliMiddleware.php6
-rw-r--r--application/front/controller/admin/LogoutController.php10
-rw-r--r--application/front/controller/visitor/InstallController.php173
-rw-r--r--application/front/exceptions/AlreadyInstalledException.php15
-rw-r--r--application/front/exceptions/ResourcePermissionException.php13
-rw-r--r--application/security/CookieManager.php33
-rw-r--r--application/security/LoginManager.php16
-rw-r--r--application/security/SessionManager.php16
-rw-r--r--application/updater/Updater.php30
-rw-r--r--index.php153
-rw-r--r--tests/bootstrap.php11
-rw-r--r--tests/container/ContainerBuilderTest.php11
-rw-r--r--tests/front/ShaarliMiddlewareTest.php12
-rw-r--r--tests/front/controller/admin/LogoutControllerTest.php18
-rw-r--r--tests/front/controller/visitor/InstallControllerTest.php264
-rw-r--r--tests/render/PageCacheManagerTest.php5
-rw-r--r--tests/security/LoginManagerTest.php30
-rw-r--r--tests/security/SessionManagerTest.php14
-rw-r--r--tests/updater/UpdaterTest.php42
-rw-r--r--tpl/default/error.html2
-rw-r--r--tpl/default/install.html2
-rw-r--r--tpl/default/page.header.html6
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
54You use the community supported version of the original Shaarli project, by Sebastien Sauvage.' 56You 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;
15use Shaarli\Plugin\PluginManager; 15use Shaarli\Plugin\PluginManager;
16use Shaarli\Render\PageBuilder; 16use Shaarli\Render\PageBuilder;
17use Shaarli\Render\PageCacheManager; 17use Shaarli\Render\PageCacheManager;
18use Shaarli\Security\CookieManager;
18use Shaarli\Security\LoginManager; 19use Shaarli\Security\LoginManager;
19use Shaarli\Security\SessionManager; 20use Shaarli\Security\SessionManager;
20use Shaarli\Thumbnailer; 21use 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
5namespace Shaarli\Container; 5namespace Shaarli\Container;
6 6
7use http\Cookie;
7use Shaarli\Bookmark\BookmarkServiceInterface; 8use Shaarli\Bookmark\BookmarkServiceInterface;
8use Shaarli\Config\ConfigManager; 9use Shaarli\Config\ConfigManager;
9use Shaarli\Feed\FeedBuilder; 10use Shaarli\Feed\FeedBuilder;
@@ -14,6 +15,7 @@ use Shaarli\Netscape\NetscapeBookmarkUtils;
14use Shaarli\Plugin\PluginManager; 15use Shaarli\Plugin\PluginManager;
15use Shaarli\Render\PageBuilder; 16use Shaarli\Render\PageBuilder;
16use Shaarli\Render\PageCacheManager; 17use Shaarli\Render\PageCacheManager;
18use Shaarli\Security\CookieManager;
17use Shaarli\Security\LoginManager; 19use Shaarli\Security\LoginManager;
18use Shaarli\Security\SessionManager; 20use Shaarli\Security\SessionManager;
19use Shaarli\Thumbnailer; 21use 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
5namespace Shaarli\Front\Controller\Admin; 5namespace Shaarli\Front\Controller\Admin;
6 6
7use Shaarli\Security\CookieManager;
7use Shaarli\Security\LoginManager; 8use Shaarli\Security\LoginManager;
8use Slim\Http\Request; 9use Slim\Http\Request;
9use Slim\Http\Response; 10use 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use Shaarli\ApplicationUtils;
8use Shaarli\Bookmark\BookmarkFilter;
9use Shaarli\Container\ShaarliContainer;
10use Shaarli\Front\Exception\AlreadyInstalledException;
11use Shaarli\Front\Exception\ResourcePermissionException;
12use Shaarli\Languages;
13use Shaarli\Security\SessionManager;
14use Slim\Http\Request;
15use Slim\Http\Response;
16
17/**
18 * Slim controller used to render install page, and create initial configuration file.
19 */
20class 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Exception;
6
7class 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
3declare(strict_types=1);
4
5namespace Shaarli\Security;
6
7class 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 */
10class LoginManager 10class 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}
diff --git a/index.php b/index.php
index 2737c22c..4627438e 100644
--- a/index.php
+++ b/index.php
@@ -61,13 +61,11 @@ require_once 'application/TimeZone.php';
61require_once 'application/Utils.php'; 61require_once 'application/Utils.php';
62 62
63use Shaarli\ApplicationUtils; 63use Shaarli\ApplicationUtils;
64use Shaarli\Bookmark\BookmarkFileService;
65use Shaarli\Config\ConfigManager; 64use Shaarli\Config\ConfigManager;
66use Shaarli\Container\ContainerBuilder; 65use Shaarli\Container\ContainerBuilder;
67use Shaarli\History;
68use Shaarli\Languages; 66use Shaarli\Languages;
69use Shaarli\Plugin\PluginManager; 67use Shaarli\Plugin\PluginManager;
70use Shaarli\Render\PageBuilder; 68use Shaarli\Security\CookieManager;
71use Shaarli\Security\LoginManager; 69use Shaarli\Security\LoginManager;
72use Shaarli\Security\SessionManager; 70use Shaarli\Security\SessionManager;
73use Slim\App; 71use 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");
158header("Cache-Control: post-check=0, pre-check=0", false); 157header("Cache-Control: post-check=0, pre-check=0", false);
159header("Pragma: no-cache"); 158header("Pragma: no-cache");
160 159
161if (! 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 */
282function 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
385if (!isset($_SESSION['LINKS_PER_PAGE'])) { 252if (!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';
18require_once 'application/Utils.php'; 18require_once 'application/Utils.php';
19require_once 'application/http/UrlUtils.php'; 19require_once 'application/http/UrlUtils.php';
20require_once 'application/http/HttpUtils.php'; 20require_once 'application/http/HttpUtils.php';
21require_once 'tests/utils/ReferenceLinkDB.php';
22require_once 'tests/utils/ReferenceHistory.php';
23require_once 'tests/utils/FakeBookmarkService.php';
24require_once 'tests/container/ShaarliTestContainer.php'; 21require_once 'tests/container/ShaarliTestContainer.php';
25require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php'; 22require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php';
26require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php'; 23require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php';
24require_once 'tests/updater/DummyUpdater.php';
25require_once 'tests/utils/FakeBookmarkService.php';
26require_once 'tests/utils/FakeConfigManager.php';
27require_once 'tests/utils/ReferenceHistory.php';
28require_once 'tests/utils/ReferenceLinkDB.php';
29require_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;
11use Shaarli\Formatter\FormatterFactory; 11use Shaarli\Formatter\FormatterFactory;
12use Shaarli\History; 12use Shaarli\History;
13use Shaarli\Http\HttpAccess; 13use Shaarli\Http\HttpAccess;
14use Shaarli\Netscape\NetscapeBookmarkUtils;
14use Shaarli\Plugin\PluginManager; 15use Shaarli\Plugin\PluginManager;
15use Shaarli\Render\PageBuilder; 16use Shaarli\Render\PageBuilder;
16use Shaarli\Render\PageCacheManager; 17use Shaarli\Render\PageCacheManager;
18use Shaarli\Security\CookieManager;
17use Shaarli\Security\LoginManager; 19use Shaarli\Security\LoginManager;
18use Shaarli\Security\SessionManager; 20use Shaarli\Security\SessionManager;
19use Shaarli\Thumbnailer; 21use Shaarli\Thumbnailer;
22use Shaarli\Updater\Updater;
20 23
21class ContainerBuilderTest extends TestCase 24class 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
20class ShaarliMiddlewareTest extends TestCase 20class 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
5namespace Shaarli\Front\Controller\Admin; 5namespace Shaarli\Front\Controller\Admin;
6 6
7/** Override PHP builtin setcookie function in the local namespace to mock it... more or less */
8if (!function_exists('Shaarli\Front\Controller\Admin\setcookie')) {
9 function setcookie(string $name, string $value): void {
10 $_COOKIE[$name] = $value;
11 }
12}
13
14use PHPUnit\Framework\TestCase; 7use PHPUnit\Framework\TestCase;
8use Shaarli\Security\CookieManager;
15use Shaarli\Security\LoginManager; 9use Shaarli\Security\LoginManager;
16use Shaarli\Security\SessionManager; 10use Shaarli\Security\SessionManager;
17use Slim\Http\Request; 11use 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
3declare(strict_types=1);
4
5namespace Shaarli\Front\Controller\Visitor;
6
7use PHPUnit\Framework\TestCase;
8use Shaarli\Config\ConfigManager;
9use Shaarli\Front\Exception\AlreadyInstalledException;
10use Shaarli\Security\SessionManager;
11use Slim\Http\Request;
12use Slim\Http\Response;
13
14class 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
5namespace Shaarli\Render; 7namespace Shaarli\Render;
6 8
7use PHPUnit\Framework\TestCase; 9use PHPUnit\Framework\TestCase;
8use Shaarli\Security\SessionManager; 10use Shaarli\Security\SessionManager;
9 11
10// required to access $_SESSION array
11session_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
2namespace Shaarli\Security;
3 2
4require_once 'tests/utils/FakeConfigManager.php'; 3namespace Shaarli\Security;
5 4
6use PHPUnit\Framework\TestCase; 5use 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
2require_once 'tests/utils/FakeConfigManager.php';
3 2
4// Initialize reference data _before_ PHPUnit starts a session 3namespace Shaarli\Security;
5require_once 'tests/utils/ReferenceSessionIdHashes.php';
6ReferenceSessionIdHashes::genAllHashes();
7 4
8use PHPUnit\Framework\TestCase; 5use PHPUnit\Framework\TestCase;
9use 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;
7use Shaarli\Config\ConfigManager; 7use Shaarli\Config\ConfigManager;
8use Shaarli\History; 8use Shaarli\History;
9 9
10require_once 'tests/updater/DummyUpdater.php';
11require_once 'tests/utils/ReferenceLinkDB.php';
12require_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">