aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
authorArthurHoaro <arthur.hoareau@wizacha.com>2020-07-07 10:15:56 +0200
committerArthurHoaro <arthur@hoa.ro>2020-07-23 21:19:21 +0200
commitc4ad3d4f061d05a01db25aa54dda830ba776792d (patch)
tree691d91a5b0bbac62cee41f7b95ad1daa38d610b3 /application
parent1a8ac737e52cb25a5c346232ee398f5908cee7d7 (diff)
downloadShaarli-c4ad3d4f061d05a01db25aa54dda830ba776792d.tar.gz
Shaarli-c4ad3d4f061d05a01db25aa54dda830ba776792d.tar.zst
Shaarli-c4ad3d4f061d05a01db25aa54dda830ba776792d.zip
Process Shaarli install through Slim controller
Diffstat (limited to 'application')
-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
14 files changed, 340 insertions, 33 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}