]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Process Shaarli install through Slim controller
authorArthurHoaro <arthur.hoareau@wizacha.com>
Tue, 7 Jul 2020 08:15:56 +0000 (10:15 +0200)
committerArthurHoaro <arthur@hoa.ro>
Thu, 23 Jul 2020 19:19:21 +0000 (21:19 +0200)
27 files changed:
application/bookmark/BookmarkFileService.php
application/bookmark/BookmarkInitializer.php
application/bookmark/BookmarkServiceInterface.php
application/container/ContainerBuilder.php
application/container/ShaarliContainer.php
application/front/ShaarliMiddleware.php
application/front/controller/admin/LogoutController.php
application/front/controller/visitor/InstallController.php [new file with mode: 0644]
application/front/exceptions/AlreadyInstalledException.php [new file with mode: 0644]
application/front/exceptions/ResourcePermissionException.php [new file with mode: 0644]
application/security/CookieManager.php [new file with mode: 0644]
application/security/LoginManager.php
application/security/SessionManager.php
application/updater/Updater.php
index.php
tests/bootstrap.php
tests/container/ContainerBuilderTest.php
tests/front/ShaarliMiddlewareTest.php
tests/front/controller/admin/LogoutControllerTest.php
tests/front/controller/visitor/InstallControllerTest.php [new file with mode: 0644]
tests/render/PageCacheManagerTest.php
tests/security/LoginManagerTest.php
tests/security/SessionManagerTest.php
tests/updater/UpdaterTest.php
tpl/default/error.html
tpl/default/install.html
tpl/default/page.header.html

index 3d15d4c921d71e7c3db2e174ce59d07e0a556b72..6e04f3b71cee32ea88720a16f6f6b99bad7b4e91 100644 (file)
@@ -46,6 +46,9 @@ class BookmarkFileService implements BookmarkServiceInterface
     /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
     protected $isLoggedIn;
 
+    /** @var bool Allow datastore alteration from not logged in users. */
+    protected $anonymousPermission = false;
+
     /**
      * @inheritDoc
      */
@@ -64,7 +67,7 @@ class BookmarkFileService implements BookmarkServiceInterface
                 $this->bookmarks = $this->bookmarksIO->read();
             } catch (EmptyDataStoreException $e) {
                 $this->bookmarks = new BookmarkArray();
-                if ($isLoggedIn) {
+                if ($this->isLoggedIn) {
                     $this->save();
                 }
             }
@@ -154,7 +157,7 @@ class BookmarkFileService implements BookmarkServiceInterface
      */
     public function set($bookmark, $save = true)
     {
-        if ($this->isLoggedIn !== true) {
+        if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) {
             throw new Exception(t('You\'re not authorized to alter the datastore'));
         }
         if (! $bookmark instanceof Bookmark) {
@@ -179,7 +182,7 @@ class BookmarkFileService implements BookmarkServiceInterface
      */
     public function add($bookmark, $save = true)
     {
-        if ($this->isLoggedIn !== true) {
+        if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) {
             throw new Exception(t('You\'re not authorized to alter the datastore'));
         }
         if (! $bookmark instanceof Bookmark) {
@@ -204,7 +207,7 @@ class BookmarkFileService implements BookmarkServiceInterface
      */
     public function addOrSet($bookmark, $save = true)
     {
-        if ($this->isLoggedIn !== true) {
+        if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) {
             throw new Exception(t('You\'re not authorized to alter the datastore'));
         }
         if (! $bookmark instanceof Bookmark) {
@@ -221,7 +224,7 @@ class BookmarkFileService implements BookmarkServiceInterface
      */
     public function remove($bookmark, $save = true)
     {
-        if ($this->isLoggedIn !== true) {
+        if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) {
             throw new Exception(t('You\'re not authorized to alter the datastore'));
         }
         if (! $bookmark instanceof Bookmark) {
@@ -274,10 +277,11 @@ class BookmarkFileService implements BookmarkServiceInterface
      */
     public function save()
     {
-        if (!$this->isLoggedIn) {
+        if (true !== $this->isLoggedIn && true !== $this->anonymousPermission) {
             // TODO: raise an Exception instead
             die('You are not authorized to change the database.');
         }
+
         $this->bookmarks->reorder();
         $this->bookmarksIO->write($this->bookmarks);
         $this->pageCacheManager->invalidateCaches();
@@ -357,6 +361,16 @@ class BookmarkFileService implements BookmarkServiceInterface
         $initializer->initialize();
     }
 
+    public function enableAnonymousPermission(): void
+    {
+        $this->anonymousPermission = true;
+    }
+
+    public function disableAnonymousPermission(): void
+    {
+        $this->anonymousPermission = false;
+    }
+
     /**
      * Handles migration to the new database format (BookmarksArray).
      */
index 9eee9a35bad09f3d9840d6bba035115dddf7075f..479ee9a9e749f195e0e7a9c89cbabbc45f736aa6 100644 (file)
@@ -34,13 +34,15 @@ class BookmarkInitializer
      */
     public function initialize()
     {
+        $this->bookmarkService->enableAnonymousPermission();
+
         $bookmark = new Bookmark();
         $bookmark->setTitle(t('My secret stuff... - Pastebin.com'));
-        $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', []);
+        $bookmark->setUrl('http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=');
         $bookmark->setDescription(t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'));
         $bookmark->setTagsString('secretstuff');
         $bookmark->setPrivate(true);
-        $this->bookmarkService->add($bookmark);
+        $this->bookmarkService->add($bookmark, false);
 
         $bookmark = new Bookmark();
         $bookmark->setTitle(t('The personal, minimalist, super-fast, database free, bookmarking service'));
@@ -54,6 +56,10 @@ To learn how to use Shaarli, consult the link "Documentation" at the bottom of t
 You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
         ));
         $bookmark->setTagsString('opensource software');
-        $this->bookmarkService->add($bookmark);
+        $this->bookmarkService->add($bookmark, false);
+
+        $this->bookmarkService->save();
+
+        $this->bookmarkService->disableAnonymousPermission();
     }
 }
index 7b7a4f09e131df45e0e3c92fffe1272f37cc4de1..37fbda890e2d607552d9b0620ac66f3b4a57cae0 100644 (file)
@@ -177,4 +177,17 @@ interface BookmarkServiceInterface
      * Creates the default database after a fresh install.
      */
     public function initialize();
+
+    /**
+     * Allow to write the datastore from anonymous session (not logged in).
+     *
+     * This covers a few specific use cases, such as datastore initialization,
+     * but it should be used carefully as it can lead to security issues.
+     */
+    public function enableAnonymousPermission();
+
+    /**
+     * Disable anonymous permission.
+     */
+    public function disableAnonymousPermission();
 }
index ccb87c3a22cc93060e1ec882385814be05c66f23..593aafb79fa7114ef63b0aa46567cb749595e577 100644 (file)
@@ -15,6 +15,7 @@ use Shaarli\Netscape\NetscapeBookmarkUtils;
 use Shaarli\Plugin\PluginManager;
 use Shaarli\Render\PageBuilder;
 use Shaarli\Render\PageCacheManager;
+use Shaarli\Security\CookieManager;
 use Shaarli\Security\LoginManager;
 use Shaarli\Security\SessionManager;
 use Shaarli\Thumbnailer;
@@ -38,6 +39,9 @@ class ContainerBuilder
     /** @var SessionManager */
     protected $session;
 
+    /** @var CookieManager */
+    protected $cookieManager;
+
     /** @var LoginManager */
     protected $login;
 
@@ -47,11 +51,13 @@ class ContainerBuilder
     public function __construct(
         ConfigManager $conf,
         SessionManager $session,
+        CookieManager $cookieManager,
         LoginManager $login
     ) {
         $this->conf = $conf;
         $this->session = $session;
         $this->login = $login;
+        $this->cookieManager = $cookieManager;
     }
 
     public function build(): ShaarliContainer
@@ -60,6 +66,7 @@ class ContainerBuilder
 
         $container['conf'] = $this->conf;
         $container['sessionManager'] = $this->session;
+        $container['cookieManager'] = $this->cookieManager;
         $container['loginManager'] = $this->login;
         $container['basePath'] = $this->basePath;
 
index 09e7d5b1ae78b029ea14f32d3bd568515d48484a..c4fe753e5eec3b015d360f67b7bf77c18ac6fcd0 100644 (file)
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace Shaarli\Container;
 
+use http\Cookie;
 use Shaarli\Bookmark\BookmarkServiceInterface;
 use Shaarli\Config\ConfigManager;
 use Shaarli\Feed\FeedBuilder;
@@ -14,6 +15,7 @@ use Shaarli\Netscape\NetscapeBookmarkUtils;
 use Shaarli\Plugin\PluginManager;
 use Shaarli\Render\PageBuilder;
 use Shaarli\Render\PageCacheManager;
+use Shaarli\Security\CookieManager;
 use Shaarli\Security\LoginManager;
 use Shaarli\Security\SessionManager;
 use Shaarli\Thumbnailer;
@@ -25,6 +27,7 @@ use Slim\Container;
  *
  * @property string                   $basePath             Shaarli's instance base path (e.g. `/shaarli/`)
  * @property BookmarkServiceInterface $bookmarkService
+ * @property CookieManager            $cookieManager
  * @property ConfigManager            $conf
  * @property mixed[]                  $environment          $_SERVER automatically injected by Slim
  * @property callable                 $errorHandler         Overrides default Slim error display
index baea6ef28396c7c517af1a8c4585c912538003c1..595182ac324d1f8a4cb699cbe71c1fcfa9b67039 100644 (file)
@@ -43,6 +43,12 @@ class ShaarliMiddleware
         $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/');
 
         try {
+            if (!is_file($this->container->conf->getConfigFileExt())
+                && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
+            ) {
+                return $response->withRedirect($this->container->basePath . '/install');
+            }
+
             $this->runUpdates();
             $this->checkOpenShaarli($request, $response, $next);
 
index c5984814738674c8601c1c35b4ccf5de971bb220..28165129f5596cc8aaf7b04da36e6271145bb7d7 100644 (file)
@@ -4,6 +4,7 @@ declare(strict_types=1);
 
 namespace Shaarli\Front\Controller\Admin;
 
+use Shaarli\Security\CookieManager;
 use Shaarli\Security\LoginManager;
 use Slim\Http\Request;
 use Slim\Http\Response;
@@ -20,9 +21,12 @@ class LogoutController extends ShaarliAdminController
     {
         $this->container->pageCacheManager->invalidateCaches();
         $this->container->sessionManager->logout();
-
-        // TODO: switch to a simple Cookie manager allowing to check the session, and create mocks.
-        setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, $this->container->basePath . '/');
+        $this->container->cookieManager->setCookieParameter(
+            CookieManager::STAY_SIGNED_IN,
+            'false',
+            0,
+            $this->container->basePath . '/'
+        );
 
         return $this->redirect($response, '/');
     }
diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php
new file mode 100644 (file)
index 0000000..aa03286
--- /dev/null
@@ -0,0 +1,173 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use Shaarli\ApplicationUtils;
+use Shaarli\Bookmark\BookmarkFilter;
+use Shaarli\Container\ShaarliContainer;
+use Shaarli\Front\Exception\AlreadyInstalledException;
+use Shaarli\Front\Exception\ResourcePermissionException;
+use Shaarli\Languages;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/**
+ * Slim controller used to render install page, and create initial configuration file.
+ */
+class InstallController extends ShaarliVisitorController
+{
+    public const SESSION_TEST_KEY = 'session_tested';
+    public const SESSION_TEST_VALUE = 'Working';
+
+    public function __construct(ShaarliContainer $container)
+    {
+        parent::__construct($container);
+
+        if (is_file($this->container->conf->getConfigFileExt())) {
+            throw new AlreadyInstalledException();
+        }
+    }
+
+    /**
+     * Display the install template page.
+     * Also test file permissions and sessions beforehand.
+     */
+    public function index(Request $request, Response $response): Response
+    {
+        // Before installation, we'll make sure that permissions are set properly, and sessions are working.
+        $this->checkPermissions();
+
+        if (static::SESSION_TEST_VALUE
+            !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
+        ) {
+            $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE);
+
+            return $this->redirect($response, '/install/session-test');
+        }
+
+        [$continents, $cities] = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
+
+        $this->assignView('continents', $continents);
+        $this->assignView('cities', $cities);
+        $this->assignView('languages', Languages::getAvailableLanguages());
+
+        return $response->write($this->render('install'));
+    }
+
+    /**
+     * Route checking that the session parameter has been properly saved between two distinct requests.
+     * If the session parameter is preserved, redirect to install template page, otherwise displays error.
+     */
+    public function sessionTest(Request $request, Response $response): Response
+    {
+        // This part makes sure sessions works correctly.
+        // (Because on some hosts, session.save_path may not be set correctly,
+        // or we may not have write access to it.)
+        if (static::SESSION_TEST_VALUE
+            !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
+        ) {
+            // Step 2: Check if data in session is correct.
+            $msg = t(
+                '<pre>Sessions do not seem to work correctly on your server.<br>'.
+                'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
+                'and that you have write access to it.<br>'.
+                'It currently points to %s.<br>'.
+                'On some browsers, accessing your server via a hostname like \'localhost\' '.
+                'or any custom hostname without a dot causes cookie storage to fail. '.
+                'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
+            );
+            $msg = sprintf($msg, $this->container->sessionManager->getSavePath());
+
+            $this->assignView('message', $msg);
+
+            return $response->write($this->render('error'));
+        }
+
+        return $this->redirect($response, '/install');
+    }
+
+    /**
+     * Save installation form and initialize config file and datastore if necessary.
+     */
+    public function save(Request $request, Response $response): Response
+    {
+        $timezone = 'UTC';
+        if (!empty($request->getParam('continent'))
+            && !empty($request->getParam('city'))
+            && isTimeZoneValid($request->getParam('continent'), $request->getParam('city'))
+        ) {
+            $timezone = $request->getParam('continent') . '/' . $request->getParam('city');
+        }
+        $this->container->conf->set('general.timezone', $timezone);
+
+        $login = $request->getParam('setlogin');
+        $this->container->conf->set('credentials.login', $login);
+        $salt = sha1(uniqid('', true) .'_'. mt_rand());
+        $this->container->conf->set('credentials.salt', $salt);
+        $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt));
+
+        if (!empty($request->getParam('title'))) {
+            $this->container->conf->set('general.title', escape($request->getParam('title')));
+        } else {
+            $this->container->conf->set(
+                'general.title',
+                'Shared bookmarks on '.escape(index_url($this->container->environment))
+            );
+        }
+
+        $this->container->conf->set('translation.language', escape($request->getParam('language')));
+        $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
+        $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
+        $this->container->conf->set(
+            'api.secret',
+            generate_api_secret(
+                $this->container->conf->get('credentials.login'),
+                $this->container->conf->get('credentials.salt')
+            )
+        );
+
+        try {
+            // Everything is ok, let's create config file.
+            $this->container->conf->write($this->container->loginManager->isLoggedIn());
+        } catch (\Exception $e) {
+            $this->assignView('message', $e->getMessage());
+            $this->assignView('stacktrace', $e->getTraceAsString());
+
+            return $response->write($this->render('error'));
+        }
+
+        if ($this->container->bookmarkService->count(BookmarkFilter::$ALL) === 0) {
+            $this->container->bookmarkService->initialize();
+        }
+
+        $this->container->sessionManager->setSessionParameter(
+            SessionManager::KEY_SUCCESS_MESSAGES,
+            [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')]
+        );
+
+        return $this->redirect($response, '/');
+    }
+
+    protected function checkPermissions(): bool
+    {
+        // Ensure Shaarli has proper access to its resources
+        $errors = ApplicationUtils::checkResourcePermissions($this->container->conf);
+
+        if (empty($errors)) {
+            return true;
+        }
+
+        // FIXME! Do not insert HTML here.
+        $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
+
+        foreach ($errors as $error) {
+            $message .= '<li>'.$error.'</li>';
+        }
+        $message .= '</ul>';
+
+        throw new ResourcePermissionException($message);
+    }
+}
diff --git a/application/front/exceptions/AlreadyInstalledException.php b/application/front/exceptions/AlreadyInstalledException.php
new file mode 100644 (file)
index 0000000..4add86c
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Exception;
+
+class AlreadyInstalledException extends ShaarliFrontException
+{
+    public function __construct()
+    {
+        $message = t('Shaarli has already been installed. Login to edit the configuration.');
+
+        parent::__construct($message, 401);
+    }
+}
diff --git a/application/front/exceptions/ResourcePermissionException.php b/application/front/exceptions/ResourcePermissionException.php
new file mode 100644 (file)
index 0000000..8fbf03b
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Exception;
+
+class ResourcePermissionException extends ShaarliFrontException
+{
+    public function __construct(string $message)
+    {
+        parent::__construct($message, 500);
+    }
+}
diff --git a/application/security/CookieManager.php b/application/security/CookieManager.php
new file mode 100644 (file)
index 0000000..cde4746
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Security;
+
+class CookieManager
+{
+    /** @var string Name of the cookie set after logging in **/
+    public const STAY_SIGNED_IN = 'shaarli_staySignedIn';
+
+    /** @var mixed $_COOKIE set by reference */
+    protected $cookies;
+
+    public function __construct(array &$cookies)
+    {
+        $this->cookies = $cookies;
+    }
+
+    public function setCookieParameter(string $key, string $value, int $expires, string $path): self
+    {
+        $this->cookies[$key] = $value;
+
+        setcookie($key, $value, $expires, $path);
+
+        return $this;
+    }
+
+    public function getCookieParameter(string $key, string $default = null): ?string
+    {
+        return $this->cookies[$key] ?? $default;
+    }
+}
index 39ec9b2e7fffa92688ab29dbc3e2a551a9b5967b..d74c3118c4eded1713a339ebc824d1978c83f93d 100644 (file)
@@ -9,9 +9,6 @@ use Shaarli\Config\ConfigManager;
  */
 class LoginManager
 {
-    /** @var string Name of the cookie set after logging in **/
-    public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn';
-
     /** @var array A reference to the $_GLOBALS array */
     protected $globals = [];
 
@@ -32,17 +29,21 @@ class LoginManager
 
     /** @var string User sign-in token depending on remote IP and credentials */
     protected $staySignedInToken = '';
+    /** @var CookieManager */
+    protected $cookieManager;
 
     /**
      * Constructor
      *
      * @param ConfigManager  $configManager  Configuration Manager instance
      * @param SessionManager $sessionManager SessionManager instance
+     * @param CookieManager  $cookieManager  CookieManager instance
      */
-    public function __construct($configManager, $sessionManager)
+    public function __construct($configManager, $sessionManager, $cookieManager)
     {
         $this->configManager = $configManager;
         $this->sessionManager = $sessionManager;
+        $this->cookieManager = $cookieManager;
         $this->banManager = new BanManager(
             $this->configManager->get('security.trusted_proxies', []),
             $this->configManager->get('security.ban_after'),
@@ -86,10 +87,9 @@ class LoginManager
     /**
      * Check user session state and validity (expiration)
      *
-     * @param array  $cookie     The $_COOKIE array
      * @param string $clientIpId Client IP address identifier
      */
-    public function checkLoginState($cookie, $clientIpId)
+    public function checkLoginState($clientIpId)
     {
         if (! $this->configManager->exists('credentials.login')) {
             // Shaarli is not configured yet
@@ -97,9 +97,7 @@ class LoginManager
             return;
         }
 
-        if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE])
-            && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
-        ) {
+        if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) {
             // The user client has a valid stay-signed-in cookie
             // Session information is updated with the current client information
             $this->sessionManager->storeLoginInfo($clientIpId);
index 0ac17d9ab23b6b0c0110033e9b8f2dc49e761db0..82771c248cddce08277baa26556e83ea2b835bb2 100644 (file)
@@ -31,16 +31,21 @@ class SessionManager
     /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
     protected $staySignedIn = false;
 
+    /** @var string */
+    protected $savePath;
+
     /**
      * Constructor
      *
-     * @param array         $session The $_SESSION array (reference)
-     * @param ConfigManager $conf    ConfigManager instance
+     * @param array         $session  The $_SESSION array (reference)
+     * @param ConfigManager $conf     ConfigManager instance
+     * @param string        $savePath Session save path returned by builtin function session_save_path()
      */
-    public function __construct(& $session, $conf)
+    public function __construct(&$session, $conf, string $savePath)
     {
         $this->session = &$session;
         $this->conf = $conf;
+        $this->savePath = $savePath;
     }
 
     /**
@@ -249,4 +254,9 @@ class SessionManager
 
         return $this;
     }
+
+    public function getSavePath(): string
+    {
+        return $this->savePath;
+    }
 }
index f73a7452f65ea97966a0d3e908066e8be79b02cd..4c5785281044c7a69ebd790846dca7caed597a1e 100644 (file)
@@ -38,6 +38,11 @@ class Updater
      */
     protected $methods;
 
+    /**
+     * @var string $basePath Shaarli root directory (from HTTP Request)
+     */
+    protected $basePath = null;
+
     /**
      * Object constructor.
      *
@@ -62,11 +67,13 @@ class Updater
      * Run all new updates.
      * Update methods have to start with 'updateMethod' and return true (on success).
      *
+     * @param string $basePath Shaarli root directory (from HTTP Request)
+     *
      * @return array An array containing ran updates.
      *
      * @throws UpdaterException If something went wrong.
      */
-    public function update()
+    public function update(string $basePath = null)
     {
         $updatesRan = [];
 
@@ -123,16 +130,14 @@ class Updater
     }
 
     /**
-     * With the Slim routing system, default header link should be `./` instead of `?`.
-     * Otherwise you can not go back to the home page. Example: `/picture-wall` -> `/picture-wall?` instead of `/`.
+     * With the Slim routing system, default header link should be `/subfolder/` instead of `?`.
+     * Otherwise you can not go back to the home page.
+     * Example: `/subfolder/picture-wall` -> `/subfolder/picture-wall?` instead of `/subfolder/`.
      */
     public function updateMethodRelativeHomeLink(): bool
     {
-        $link = trim($this->conf->get('general.header_link'));
-        if ($link[0] === '?') {
-            $link = './'. ltrim($link, '?');
-
-            $this->conf->set('general.header_link', $link, true, true);
+        if ('?' === trim($this->conf->get('general.header_link'))) {
+            $this->conf->set('general.header_link', $this->basePath . '/', true, true);
         }
 
         return true;
@@ -152,7 +157,7 @@ class Updater
                 && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match)
             ) {
                 $updated = true;
-                $bookmark = $bookmark->setUrl('/shaare/' . $match[1]);
+                $bookmark = $bookmark->setUrl($this->basePath . '/shaare/' . $match[1]);
 
                 $this->bookmarkService->set($bookmark, false);
             }
@@ -164,4 +169,11 @@ class Updater
 
         return true;
     }
+
+    public function setBasePath(string $basePath): self
+    {
+        $this->basePath = $basePath;
+
+        return $this;
+    }
 }
index 2737c22cdfea7a2f8a4bc802daae77c4a2e0f171..4627438e523bf16a68530ac1d34d0790a271e9c2 100644 (file)
--- a/index.php
+++ b/index.php
@@ -61,13 +61,11 @@ require_once 'application/TimeZone.php';
 require_once 'application/Utils.php';
 
 use Shaarli\ApplicationUtils;
-use Shaarli\Bookmark\BookmarkFileService;
 use Shaarli\Config\ConfigManager;
 use Shaarli\Container\ContainerBuilder;
-use Shaarli\History;
 use Shaarli\Languages;
 use Shaarli\Plugin\PluginManager;
-use Shaarli\Render\PageBuilder;
+use Shaarli\Security\CookieManager;
 use Shaarli\Security\LoginManager;
 use Shaarli\Security\SessionManager;
 use Slim\App;
@@ -118,13 +116,14 @@ if ($conf->get('dev.debug', false)) {
     // See all errors (for debugging only)
     error_reporting(-1);
 
-    set_error_handler(function($errno, $errstr, $errfile, $errline, array $errcontext) {
+    set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext) {
         throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
     });
 }
 
-$sessionManager = new SessionManager($_SESSION, $conf);
-$loginManager = new LoginManager($conf, $sessionManager);
+$sessionManager = new SessionManager($_SESSION, $conf, session_save_path());
+$cookieManager = new CookieManager($_COOKIE);
+$loginManager = new LoginManager($conf, $sessionManager, $cookieManager);
 $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
 $clientIpId = client_ip_id($_SERVER);
 
@@ -158,28 +157,7 @@ header("Cache-Control: no-store, no-cache, must-revalidate");
 header("Cache-Control: post-check=0, pre-check=0", false);
 header("Pragma: no-cache");
 
-if (! is_file($conf->getConfigFileExt())) {
-    // Ensure Shaarli has proper access to its resources
-    $errors = ApplicationUtils::checkResourcePermissions($conf);
-
-    if ($errors != array()) {
-        $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
-
-        foreach ($errors as $error) {
-            $message .= '<li>'.$error.'</li>';
-        }
-        $message .= '</ul>';
-
-        header('Content-Type: text/html; charset=utf-8');
-        echo $message;
-        exit;
-    }
-
-    // Display the installation form if no existing config is found
-    install($conf, $sessionManager, $loginManager);
-}
-
-$loginManager->checkLoginState($_COOKIE, $clientIpId);
+$loginManager->checkLoginState($clientIpId);
 
 // ------------------------------------------------------------------------------------------
 // Process login form: Check if login/password is correct.
@@ -205,7 +183,7 @@ if (isset($_POST['login'])) {
             $expirationTime = $sessionManager->extendSession();
 
             setcookie(
-                $loginManager::$STAY_SIGNED_IN_COOKIE,
+                CookieManager::STAY_SIGNED_IN,
                 $loginManager->getStaySignedInToken(),
                 $expirationTime,
                 WEB_PATH
@@ -271,122 +249,11 @@ if (!isset($_SESSION['tokens'])) {
     $_SESSION['tokens']=array();  // Token are attached to the session.
 }
 
-/**
- * Installation
- * This function should NEVER be called if the file data/config.php exists.
- *
- * @param ConfigManager  $conf           Configuration Manager instance.
- * @param SessionManager $sessionManager SessionManager instance
- * @param LoginManager   $loginManager   LoginManager instance
- */
-function install($conf, $sessionManager, $loginManager)
-{
-    // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
-    if (endsWith($_SERVER['HTTP_HOST'], '.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) {
-        mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions', 0705);
-    }
-
-
-    // This part makes sure sessions works correctly.
-    // (Because on some hosts, session.save_path may not be set correctly,
-    // or we may not have write access to it.)
-    if (isset($_GET['test_session'])
-        && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) {
-        // Step 2: Check if data in session is correct.
-        $msg = t(
-            '<pre>Sessions do not seem to work correctly on your server.<br>'.
-            'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
-            'and that you have write access to it.<br>'.
-            'It currently points to %s.<br>'.
-            'On some browsers, accessing your server via a hostname like \'localhost\' '.
-            'or any custom hostname without a dot causes cookie storage to fail. '.
-            'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
-        );
-        $msg = sprintf($msg, session_save_path());
-        echo $msg;
-        echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
-        die;
-    }
-    if (!isset($_SESSION['session_tested'])) {
-        // Step 1 : Try to store data in session and reload page.
-        $_SESSION['session_tested'] = 'Working';  // Try to set a variable in session.
-        header('Location: '.index_url($_SERVER).'?test_session');  // Redirect to check stored data.
-    }
-    if (isset($_GET['test_session'])) {
-        // Step 3: Sessions are OK. Remove test parameter from URL.
-        header('Location: '.index_url($_SERVER));
-    }
-
-
-    if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) {
-        $tz = 'UTC';
-        if (!empty($_POST['continent']) && !empty($_POST['city'])
-            && isTimeZoneValid($_POST['continent'], $_POST['city'])
-        ) {
-            $tz = $_POST['continent'].'/'.$_POST['city'];
-        }
-        $conf->set('general.timezone', $tz);
-        $login = $_POST['setlogin'];
-        $conf->set('credentials.login', $login);
-        $salt = sha1(uniqid('', true) .'_'. mt_rand());
-        $conf->set('credentials.salt', $salt);
-        $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt));
-        if (!empty($_POST['title'])) {
-            $conf->set('general.title', escape($_POST['title']));
-        } else {
-            $conf->set('general.title', 'Shared bookmarks on '.escape(index_url($_SERVER)));
-        }
-        $conf->set('translation.language', escape($_POST['language']));
-        $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
-        $conf->set('api.enabled', !empty($_POST['enableApi']));
-        $conf->set(
-            'api.secret',
-            generate_api_secret(
-                $conf->get('credentials.login'),
-                $conf->get('credentials.salt')
-            )
-        );
-        try {
-            // Everything is ok, let's create config file.
-            $conf->write($loginManager->isLoggedIn());
-        } catch (Exception $e) {
-            error_log(
-                'ERROR while writing config file after installation.' . PHP_EOL .
-                    $e->getMessage()
-            );
-
-            // TODO: do not handle exceptions/errors in JS.
-            echo '<script>alert("'. $e->getMessage() .'");document.location=\'?\';</script>';
-            exit;
-        }
-
-        $history = new History($conf->get('resource.history'));
-        $bookmarkService = new BookmarkFileService($conf, $history, true);
-        if ($bookmarkService->count() === 0) {
-            $bookmarkService->initialize();
-        }
-
-        echo '<script>alert('
-            .'"Shaarli is now configured. '
-            .'Please enter your login/password and start shaaring your bookmarks!"'
-            .');document.location=\'./login\';</script>';
-        exit;
-    }
-
-    $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
-    list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
-    $PAGE->assign('continents', $continents);
-    $PAGE->assign('cities', $cities);
-    $PAGE->assign('languages', Languages::getAvailableLanguages());
-    $PAGE->renderPage('install');
-    exit;
-}
-
 if (!isset($_SESSION['LINKS_PER_PAGE'])) {
     $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
 }
 
-$containerBuilder = new ContainerBuilder($conf, $sessionManager, $loginManager);
+$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager);
 $container = $containerBuilder->build();
 $app = new App($container);
 
@@ -408,6 +275,10 @@ $app->group('/api/v1', function () {
 })->add('\Shaarli\Api\ApiMiddleware');
 
 $app->group('', function () {
+    $this->get('/install', '\Shaarli\Front\Controller\Visitor\InstallController:index')->setName('displayInstall');
+    $this->get('/install/session-test', '\Shaarli\Front\Controller\Visitor\InstallController:sessionTest');
+    $this->post('/install', '\Shaarli\Front\Controller\Visitor\InstallController:save')->setName('saveInstall');
+
     /* -- PUBLIC --*/
     $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index');
     $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink');
index 511698ff085e27bab7e14092e01e7e2f7a6dced7..d4ddedd50a6f06d26d05a12121be7c29c6392a64 100644 (file)
@@ -18,9 +18,14 @@ require_once 'application/bookmark/LinkUtils.php';
 require_once 'application/Utils.php';
 require_once 'application/http/UrlUtils.php';
 require_once 'application/http/HttpUtils.php';
-require_once 'tests/utils/ReferenceLinkDB.php';
-require_once 'tests/utils/ReferenceHistory.php';
-require_once 'tests/utils/FakeBookmarkService.php';
 require_once 'tests/container/ShaarliTestContainer.php';
 require_once 'tests/front/controller/visitor/FrontControllerMockHelper.php';
 require_once 'tests/front/controller/admin/FrontAdminControllerMockHelper.php';
+require_once 'tests/updater/DummyUpdater.php';
+require_once 'tests/utils/FakeBookmarkService.php';
+require_once 'tests/utils/FakeConfigManager.php';
+require_once 'tests/utils/ReferenceHistory.php';
+require_once 'tests/utils/ReferenceLinkDB.php';
+require_once 'tests/utils/ReferenceSessionIdHashes.php';
+
+\ReferenceSessionIdHashes::genAllHashes();
index db533f378d11379236e350faa73cbe06eea8ec2a..fa77bf310c361dc363c9d476d8f41c1d4a254060 100644 (file)
@@ -11,12 +11,15 @@ use Shaarli\Feed\FeedBuilder;
 use Shaarli\Formatter\FormatterFactory;
 use Shaarli\History;
 use Shaarli\Http\HttpAccess;
+use Shaarli\Netscape\NetscapeBookmarkUtils;
 use Shaarli\Plugin\PluginManager;
 use Shaarli\Render\PageBuilder;
 use Shaarli\Render\PageCacheManager;
+use Shaarli\Security\CookieManager;
 use Shaarli\Security\LoginManager;
 use Shaarli\Security\SessionManager;
 use Shaarli\Thumbnailer;
+use Shaarli\Updater\Updater;
 
 class ContainerBuilderTest extends TestCase
 {
@@ -32,10 +35,14 @@ class ContainerBuilderTest extends TestCase
     /** @var ContainerBuilder */
     protected $containerBuilder;
 
+    /** @var CookieManager */
+    protected $cookieManager;
+
     public function setUp(): void
     {
         $this->conf = new ConfigManager('tests/utils/config/configJson');
         $this->sessionManager = $this->createMock(SessionManager::class);
+        $this->cookieManager = $this->createMock(CookieManager::class);
 
         $this->loginManager = $this->createMock(LoginManager::class);
         $this->loginManager->method('isLoggedIn')->willReturn(true);
@@ -43,6 +50,7 @@ class ContainerBuilderTest extends TestCase
         $this->containerBuilder = new ContainerBuilder(
             $this->conf,
             $this->sessionManager,
+            $this->cookieManager,
             $this->loginManager
         );
     }
@@ -53,6 +61,7 @@ class ContainerBuilderTest extends TestCase
 
         static::assertInstanceOf(ConfigManager::class, $container->conf);
         static::assertInstanceOf(SessionManager::class, $container->sessionManager);
+        static::assertInstanceOf(CookieManager::class, $container->cookieManager);
         static::assertInstanceOf(LoginManager::class, $container->loginManager);
         static::assertInstanceOf(History::class, $container->history);
         static::assertInstanceOf(BookmarkServiceInterface::class, $container->bookmarkService);
@@ -63,6 +72,8 @@ class ContainerBuilderTest extends TestCase
         static::assertInstanceOf(FeedBuilder::class, $container->feedBuilder);
         static::assertInstanceOf(Thumbnailer::class, $container->thumbnailer);
         static::assertInstanceOf(HttpAccess::class, $container->httpAccess);
+        static::assertInstanceOf(NetscapeBookmarkUtils::class, $container->netscapeBookmarkUtils);
+        static::assertInstanceOf(Updater::class, $container->updater);
 
         // Set by the middleware
         static::assertNull($container->basePath);
index 81ea134409ffdd8e9f03d4dff90da8c4f1bebde6..20090d8b0a9720c0e8c6e9f1c91905f3533959be 100644 (file)
@@ -19,6 +19,8 @@ use Slim\Http\Uri;
 
 class ShaarliMiddlewareTest extends TestCase
 {
+    protected const TMP_MOCK_FILE = '.tmp';
+
     /** @var ShaarliContainer */
     protected $container;
 
@@ -29,12 +31,21 @@ class ShaarliMiddlewareTest extends TestCase
     {
         $this->container = $this->createMock(ShaarliContainer::class);
 
+        touch(static::TMP_MOCK_FILE);
+
         $this->container->conf = $this->createMock(ConfigManager::class);
+        $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
+
         $this->container->loginManager = $this->createMock(LoginManager::class);
 
         $this->middleware = new ShaarliMiddleware($this->container);
     }
 
+    public function tearDown()
+    {
+        unlink(static::TMP_MOCK_FILE);
+    }
+
     /**
      * Test middleware execution with valid controller call
      */
@@ -179,6 +190,7 @@ class ShaarliMiddlewareTest extends TestCase
         $this->container->conf->method('get')->willReturnCallback(function (string $key): string {
             return $key;
         });
+        $this->container->conf->method('getConfigFileExt')->willReturn(static::TMP_MOCK_FILE);
 
         $this->container->pageCacheManager = $this->createMock(PageCacheManager::class);
         $this->container->pageCacheManager->expects(static::once())->method('invalidateCaches');
index ca177085e7734112fd68c090e50b0822e654b6bf..45e84dc035b65d89984377bb507bdcbc5d7d27d4 100644 (file)
@@ -4,14 +4,8 @@ declare(strict_types=1);
 
 namespace Shaarli\Front\Controller\Admin;
 
-/** Override PHP builtin setcookie function in the local namespace to mock it... more or less */
-if (!function_exists('Shaarli\Front\Controller\Admin\setcookie')) {
-    function setcookie(string $name, string $value): void {
-        $_COOKIE[$name] = $value;
-    }
-}
-
 use PHPUnit\Framework\TestCase;
+use Shaarli\Security\CookieManager;
 use Shaarli\Security\LoginManager;
 use Shaarli\Security\SessionManager;
 use Slim\Http\Request;
@@ -29,8 +23,6 @@ class LogoutControllerTest extends TestCase
         $this->createContainer();
 
         $this->controller = new LogoutController($this->container);
-
-        setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, $cookie = 'hi there');
     }
 
     public function testValidControllerInvoke(): void
@@ -43,13 +35,17 @@ class LogoutControllerTest extends TestCase
         $this->container->sessionManager = $this->createMock(SessionManager::class);
         $this->container->sessionManager->expects(static::once())->method('logout');
 
-        static::assertSame('hi there', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]);
+        $this->container->cookieManager = $this->createMock(CookieManager::class);
+        $this->container->cookieManager
+            ->expects(static::once())
+            ->method('setCookieParameter')
+            ->with(CookieManager::STAY_SIGNED_IN, 'false', 0, '/subfolder/')
+        ;
 
         $result = $this->controller->index($request, $response);
 
         static::assertInstanceOf(Response::class, $result);
         static::assertSame(302, $result->getStatusCode());
         static::assertSame(['/subfolder/'], $result->getHeader('location'));
-        static::assertSame('false', $_COOKIE[LoginManager::$STAY_SIGNED_IN_COOKIE]);
     }
 }
diff --git a/tests/front/controller/visitor/InstallControllerTest.php b/tests/front/controller/visitor/InstallControllerTest.php
new file mode 100644 (file)
index 0000000..6871fdd
--- /dev/null
@@ -0,0 +1,264 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Front\Controller\Visitor;
+
+use PHPUnit\Framework\TestCase;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Front\Exception\AlreadyInstalledException;
+use Shaarli\Security\SessionManager;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class InstallControllerTest extends TestCase
+{
+    use FrontControllerMockHelper;
+
+    const MOCK_FILE = '.tmp';
+
+    /** @var InstallController */
+    protected $controller;
+
+    public function setUp(): void
+    {
+        $this->createContainer();
+
+        $this->container->conf = $this->createMock(ConfigManager::class);
+        $this->container->conf->method('getConfigFileExt')->willReturn(static::MOCK_FILE);
+        $this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
+            if ($key === 'resource.raintpl_tpl') {
+                return '.';
+            }
+
+            return $default ?? $key;
+        });
+
+        $this->controller = new InstallController($this->container);
+    }
+
+    protected function tearDown(): void
+    {
+        if (file_exists(static::MOCK_FILE)) {
+            unlink(static::MOCK_FILE);
+        }
+    }
+
+    /**
+     * Test displaying install page with valid session.
+     */
+    public function testInstallIndexWithValidSession(): void
+    {
+        $assignedVariables = [];
+        $this->assignTemplateVars($assignedVariables);
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->sessionManager = $this->createMock(SessionManager::class);
+        $this->container->sessionManager
+            ->method('getSessionParameter')
+            ->willReturnCallback(function (string $key, $default) {
+                return $key === 'session_tested' ? 'Working' : $default;
+            })
+        ;
+
+        $result = $this->controller->index($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('install', (string) $result->getBody());
+
+        static::assertIsArray($assignedVariables['continents']);
+        static::assertSame('Africa', $assignedVariables['continents'][0]);
+        static::assertSame('UTC', $assignedVariables['continents']['selected']);
+
+        static::assertIsArray($assignedVariables['cities']);
+        static::assertSame(['continent' => 'Africa', 'city' => 'Abidjan'], $assignedVariables['cities'][0]);
+        static::assertSame('UTC', $assignedVariables['continents']['selected']);
+
+        static::assertIsArray($assignedVariables['languages']);
+        static::assertSame('Automatic', $assignedVariables['languages']['auto']);
+        static::assertSame('French', $assignedVariables['languages']['fr']);
+    }
+
+    /**
+     * Instantiate the install controller with an existing config file: exception.
+     */
+    public function testInstallWithExistingConfigFile(): void
+    {
+        $this->expectException(AlreadyInstalledException::class);
+
+        touch(static::MOCK_FILE);
+
+        $this->controller = new InstallController($this->container);
+    }
+
+    /**
+     * Call controller without session yet defined, redirect to test session install page.
+     */
+    public function testInstallRedirectToSessionTest(): void
+    {
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->sessionManager = $this->createMock(SessionManager::class);
+        $this->container->sessionManager
+            ->expects(static::once())
+            ->method('setSessionParameter')
+            ->with(InstallController::SESSION_TEST_KEY, InstallController::SESSION_TEST_VALUE)
+        ;
+
+        $result = $this->controller->index($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame('/subfolder/install/session-test', $result->getHeader('location')[0]);
+    }
+
+    /**
+     * Call controller in session test mode: valid session then redirect to install page.
+     */
+    public function testInstallSessionTestValid(): void
+    {
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->sessionManager = $this->createMock(SessionManager::class);
+        $this->container->sessionManager
+            ->method('getSessionParameter')
+            ->with(InstallController::SESSION_TEST_KEY)
+            ->willReturn(InstallController::SESSION_TEST_VALUE)
+        ;
+
+        $result = $this->controller->sessionTest($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame('/subfolder/install', $result->getHeader('location')[0]);
+    }
+
+    /**
+     * Call controller in session test mode: invalid session then redirect to error page.
+     */
+    public function testInstallSessionTestError(): void
+    {
+        $assignedVars = [];
+        $this->assignTemplateVars($assignedVars);
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->sessionManager = $this->createMock(SessionManager::class);
+        $this->container->sessionManager
+            ->method('getSessionParameter')
+            ->with(InstallController::SESSION_TEST_KEY)
+            ->willReturn('KO')
+        ;
+
+        $result = $this->controller->sessionTest($request, $response);
+
+        static::assertSame(200, $result->getStatusCode());
+        static::assertSame('error', (string) $result->getBody());
+        static::assertStringStartsWith(
+            '<pre>Sessions do not seem to work correctly on your server',
+            $assignedVars['message']
+        );
+    }
+
+    /**
+     * Test saving valid data from install form. Also initialize datastore.
+     */
+    public function testSaveInstallValid(): void
+    {
+        $providedParameters = [
+            'continent' => 'Europe',
+            'city' => 'Berlin',
+            'setlogin' => 'bob',
+            'setpassword' => 'password',
+            'title' => 'Shaarli',
+            'language' => 'fr',
+            'updateCheck' => true,
+            'enableApi' => true,
+        ];
+
+        $expectedSettings = [
+            'general.timezone' => 'Europe/Berlin',
+            'credentials.login' => 'bob',
+            'credentials.salt' => '_NOT_EMPTY',
+            'credentials.hash' => '_NOT_EMPTY',
+            'general.title' => 'Shaarli',
+            'translation.language' => 'en',
+            'updates.check_updates' => true,
+            'api.enabled' => true,
+            'api.secret' => '_NOT_EMPTY',
+        ];
+
+        $request = $this->createMock(Request::class);
+        $request->method('getParam')->willReturnCallback(function (string $key) use ($providedParameters) {
+            return $providedParameters[$key] ?? null;
+        });
+        $response = new Response();
+
+        $this->container->conf = $this->createMock(ConfigManager::class);
+        $this->container->conf
+            ->method('get')
+            ->willReturnCallback(function (string $key, $value) {
+                if ($key === 'credentials.login') {
+                    return 'bob';
+                } elseif ($key === 'credentials.salt') {
+                    return 'salt';
+                }
+
+                return $value;
+            })
+        ;
+        $this->container->conf
+            ->expects(static::exactly(count($expectedSettings)))
+            ->method('set')
+            ->willReturnCallback(function (string $key, $value) use ($expectedSettings) {
+                if ($expectedSettings[$key] ?? null === '_NOT_EMPTY') {
+                    static::assertNotEmpty($value);
+                } else {
+                    static::assertSame($expectedSettings[$key], $value);
+                }
+            })
+        ;
+        $this->container->conf->expects(static::once())->method('write');
+
+        $this->container->bookmarkService->expects(static::once())->method('count')->willReturn(0);
+        $this->container->bookmarkService->expects(static::once())->method('initialize');
+
+        $this->container->sessionManager
+            ->expects(static::once())
+            ->method('setSessionParameter')
+            ->with(SessionManager::KEY_SUCCESS_MESSAGES)
+        ;
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame('/subfolder/', $result->getHeader('location')[0]);
+    }
+
+    /**
+     * Test default settings (timezone and title).
+     * Also check that bookmarks are not initialized if
+     */
+    public function testSaveInstallDefaultValues(): void
+    {
+        $confSettings = [];
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->conf->method('set')->willReturnCallback(function (string $key, $value) use (&$confSettings) {
+            $confSettings[$key] = $value;
+        });
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame('/subfolder/', $result->getHeader('location')[0]);
+
+        static::assertSame('UTC', $confSettings['general.timezone']);
+        static::assertSame('Shared bookmarks on http://shaarli', $confSettings['general.title']);
+    }
+}
index b870e6eb4b314a80be742c86223d6bfb58838a19..c258f45f566cb12a321730fa6a9f71e112648181 100644 (file)
@@ -1,15 +1,14 @@
 <?php
+
 /**
  * Cache tests
  */
+
 namespace Shaarli\Render;
 
 use PHPUnit\Framework\TestCase;
 use Shaarli\Security\SessionManager;
 
-// required to access $_SESSION array
-session_start();
-
 /**
  * Unitary tests for cached pages
  */
index 8fd1698c1bf751043afa4ec437990242437bd2a6..f242be0919685d9d5231e1ae20b96309471a136f 100644 (file)
@@ -1,7 +1,6 @@
 <?php
-namespace Shaarli\Security;
 
-require_once 'tests/utils/FakeConfigManager.php';
+namespace Shaarli\Security;
 
 use PHPUnit\Framework\TestCase;
 
@@ -58,6 +57,9 @@ class LoginManagerTest extends TestCase
     /** @var string Salt used by hash functions */
     protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2';
 
+    /** @var CookieManager */
+    protected $cookieManager;
+
     /**
      * Prepare or reset test resources
      */
@@ -84,8 +86,12 @@ class LoginManagerTest extends TestCase
         $this->cookie = [];
         $this->session = [];
 
-        $this->sessionManager = new SessionManager($this->session, $this->configManager);
-        $this->loginManager = new LoginManager($this->configManager, $this->sessionManager);
+        $this->cookieManager = $this->createMock(CookieManager::class);
+        $this->cookieManager->method('getCookieParameter')->willReturnCallback(function (string $key) {
+            return $this->cookie[$key] ?? null;
+        });
+        $this->sessionManager = new SessionManager($this->session, $this->configManager, 'session_path');
+        $this->loginManager = new LoginManager($this->configManager, $this->sessionManager, $this->cookieManager);
         $this->server['REMOTE_ADDR'] = $this->ipAddr;
     }
 
@@ -193,8 +199,8 @@ class LoginManagerTest extends TestCase
         $configManager = new \FakeConfigManager([
             'resource.ban_file' => $this->banFile,
         ]);
-        $loginManager = new LoginManager($configManager, null);
-        $loginManager->checkLoginState([], '');
+        $loginManager = new LoginManager($configManager, null, $this->cookieManager);
+        $loginManager->checkLoginState('');
 
         $this->assertFalse($loginManager->isLoggedIn());
     }
@@ -210,9 +216,9 @@ class LoginManagerTest extends TestCase
             'expires_on' => time() + 100,
         ];
         $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
-        $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope';
+        $this->cookie[CookieManager::STAY_SIGNED_IN] = 'nope';
 
-        $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+        $this->loginManager->checkLoginState($this->clientIpAddress);
 
         $this->assertTrue($this->loginManager->isLoggedIn());
         $this->assertTrue(empty($this->session['username']));
@@ -224,9 +230,9 @@ class LoginManagerTest extends TestCase
     public function testCheckLoginStateStaySignedInWithValidToken()
     {
         $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
-        $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken();
+        $this->cookie[CookieManager::STAY_SIGNED_IN] = $this->loginManager->getStaySignedInToken();
 
-        $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+        $this->loginManager->checkLoginState($this->clientIpAddress);
 
         $this->assertTrue($this->loginManager->isLoggedIn());
         $this->assertEquals($this->login, $this->session['username']);
@@ -241,7 +247,7 @@ class LoginManagerTest extends TestCase
         $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
         $this->session['expires_on'] = time() - 100;
 
-        $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+        $this->loginManager->checkLoginState($this->clientIpAddress);
 
         $this->assertFalse($this->loginManager->isLoggedIn());
     }
@@ -253,7 +259,7 @@ class LoginManagerTest extends TestCase
     {
         $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
 
-        $this->loginManager->checkLoginState($this->cookie, '10.7.157.98');
+        $this->loginManager->checkLoginState('10.7.157.98');
 
         $this->assertFalse($this->loginManager->isLoggedIn());
     }
index d9db775efc59fd2a4b9b8e4accbba69788f6534e..60695dcf944de720ac8e16163ef7df346f434513 100644 (file)
@@ -1,12 +1,8 @@
 <?php
-require_once 'tests/utils/FakeConfigManager.php';
 
-// Initialize reference data _before_ PHPUnit starts a session
-require_once 'tests/utils/ReferenceSessionIdHashes.php';
-ReferenceSessionIdHashes::genAllHashes();
+namespace Shaarli\Security;
 
 use PHPUnit\Framework\TestCase;
-use Shaarli\Security\SessionManager;
 
 /**
  * Test coverage for SessionManager
@@ -30,7 +26,7 @@ class SessionManagerTest extends TestCase
      */
     public static function setUpBeforeClass()
     {
-        self::$sidHashes = ReferenceSessionIdHashes::getHashes();
+        self::$sidHashes = \ReferenceSessionIdHashes::getHashes();
     }
 
     /**
@@ -38,13 +34,13 @@ class SessionManagerTest extends TestCase
      */
     public function setUp()
     {
-        $this->conf = new FakeConfigManager([
+        $this->conf = new \FakeConfigManager([
             'credentials.login' => 'johndoe',
             'credentials.salt' => 'salt',
             'security.session_protection_disabled' => false,
         ]);
         $this->session = [];
-        $this->sessionManager = new SessionManager($this->session, $this->conf);
+        $this->sessionManager = new SessionManager($this->session, $this->conf, 'session_path');
     }
 
     /**
@@ -69,7 +65,7 @@ class SessionManagerTest extends TestCase
                 $token => 1,
             ],
         ];
-        $sessionManager = new SessionManager($session, $this->conf);
+        $sessionManager = new SessionManager($session, $this->conf, 'session_path');
 
         // check and destroy the token
         $this->assertTrue($sessionManager->checkToken($token));
index afc35aece2306fb4014fef30bb3b7543f3fcac0a..c801d451fecc526bc8de258e4701a54a26f5d605 100644 (file)
@@ -7,9 +7,6 @@ use Shaarli\Bookmark\BookmarkServiceInterface;
 use Shaarli\Config\ConfigManager;
 use Shaarli\History;
 
-require_once 'tests/updater/DummyUpdater.php';
-require_once 'tests/utils/ReferenceLinkDB.php';
-require_once 'inc/rain.tpl.class.php';
 
 /**
  * Class UpdaterTest.
@@ -35,6 +32,9 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
     /** @var BookmarkServiceInterface */
     protected $bookmarkService;
 
+    /** @var \ReferenceLinkDB */
+    protected $refDB;
+
     /** @var Updater */
     protected $updater;
 
@@ -43,6 +43,9 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
      */
     public function setUp()
     {
+        $this->refDB = new \ReferenceLinkDB();
+        $this->refDB->write(self::$testDatastore);
+
         copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
         $this->conf = new ConfigManager(self::$configFile);
         $this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), true);
@@ -181,9 +184,40 @@ class UpdaterTest extends \PHPUnit\Framework\TestCase
 
     public function testUpdateMethodRelativeHomeLinkRename(): void
     {
+        $this->updater->setBasePath('/subfolder');
         $this->conf->set('general.header_link', '?');
+
+        $this->updater->updateMethodRelativeHomeLink();
+
+        static::assertSame('/subfolder/', $this->conf->get('general.header_link'));
+    }
+
+    public function testUpdateMethodRelativeHomeLinkDoNotRename(): void
+    {
+        $this->updater->setBasePath('/subfolder');
+        $this->conf->set('general.header_link', '~/my-blog');
+
         $this->updater->updateMethodRelativeHomeLink();
 
-        static::assertSame();
+        static::assertSame('~/my-blog', $this->conf->get('general.header_link'));
+    }
+
+    public function testUpdateMethodMigrateExistingNotesUrl(): void
+    {
+        $this->updater->setBasePath('/subfolder');
+
+        $this->updater->updateMethodMigrateExistingNotesUrl();
+
+        static::assertSame($this->refDB->getLinks()[0]->getUrl(), $this->bookmarkService->get(0)->getUrl());
+        static::assertSame($this->refDB->getLinks()[1]->getUrl(), $this->bookmarkService->get(1)->getUrl());
+        static::assertSame($this->refDB->getLinks()[4]->getUrl(), $this->bookmarkService->get(4)->getUrl());
+        static::assertSame($this->refDB->getLinks()[6]->getUrl(), $this->bookmarkService->get(6)->getUrl());
+        static::assertSame($this->refDB->getLinks()[7]->getUrl(), $this->bookmarkService->get(7)->getUrl());
+        static::assertSame($this->refDB->getLinks()[8]->getUrl(), $this->bookmarkService->get(8)->getUrl());
+        static::assertSame($this->refDB->getLinks()[9]->getUrl(), $this->bookmarkService->get(9)->getUrl());
+        static::assertSame('/subfolder/shaare/WDWyig', $this->bookmarkService->get(42)->getUrl());
+        static::assertSame('/subfolder/shaare/WDWyig', $this->bookmarkService->get(41)->getUrl());
+        static::assertSame('/subfolder/shaare/0gCTjQ', $this->bookmarkService->get(10)->getUrl());
+        static::assertSame('/subfolder/shaare/PCRizQ', $this->bookmarkService->get(11)->getUrl());
     }
 }
index 4abac9cae02dfb841b4a45ddebb743d2e423fbfb..c3e0c3c1db3f914475fbf0d59fa6a3e349f684c1 100644 (file)
@@ -15,7 +15,7 @@
       </pre>
   {/if}
 
-  <img src="{asset_path}/img/sad_star.png#" alt="">
+  <img src="{$asset_path}/img/sad_star.png#" alt="">
 </div>
 {include="page.footer"}
 </body>
index 6f96c01915df69f53d80225e0b1328083e676edb..a506a2eb2543b76f7dbe5ee760de737938a0bce5 100644 (file)
@@ -10,7 +10,7 @@
 {$ratioLabelMobile='7-8'}
 {$ratioInputMobile='1-8'}
 
-<form method="POST" action="{$base_path}/?do=install" name="installform" id="installform">
+<form method="POST" action="{$base_path}/install" name="installform" id="installform">
 <div class="pure-g">
   <div class="pure-u-lg-1-6 pure-u-1-24"></div>
   <div class="pure-u-lg-2-3 pure-u-22-24 page-form page-form-complete">
index 4fe2b69e9609692b19f5dbf4455f7b491bdec8ba..e4c7d91a14fcbe995c3e620d7d38c06492d2a8d2 100644 (file)
   </div>
 {/if}
 
-{if="!empty($global_errors) && $is_logged_in"}
+{if="!empty($global_errors)"}
   <div class="pure-g header-alert-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
   <div class="pure-u-2-24"></div>
     <div class="pure-u-20-24">
   </div>
 {/if}
 
-{if="!empty($global_warnings) && $is_logged_in"}
+{if="!empty($global_warnings)"}
   <div class="pure-g header-alert-message pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
     <div class="pure-u-2-24"></div>
     <div class="pure-u-20-24">
   </div>
 {/if}
 
-{if="!empty($global_successes) && $is_logged_in"}
+{if="!empty($global_successes)"}
   <div class="pure-g header-alert-message new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert">
     <div class="pure-u-2-24"></div>
     <div class="pure-u-20-24">