]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Merge pull request #1086 from virtualtam/refactor/login
authorVirtualTam <virtualtam+github@flibidi.net>
Sun, 3 Jun 2018 16:26:32 +0000 (18:26 +0200)
committerGitHub <noreply@github.com>
Sun, 3 Jun 2018 16:26:32 +0000 (18:26 +0200)
Refactor user login and session management

23 files changed:
application/HttpUtils.php
application/LoginManager.php [deleted file]
application/PageBuilder.php
application/SessionManager.php [deleted file]
application/security/LoginManager.php [new file with mode: 0644]
application/security/SessionManager.php [new file with mode: 0644]
composer.json
index.php
tests/HttpUtils/ClientIpIdTest.php [new file with mode: 0644]
tests/SessionManagerTest.php [deleted file]
tests/security/LoginManagerTest.php [moved from tests/LoginManagerTest.php with 54% similarity]
tests/security/SessionManagerTest.php [new file with mode: 0644]
tests/utils/FakeConfigManager.php
tpl/default/linklist.html
tpl/default/linklist.paging.html
tpl/default/page.footer.html
tpl/default/page.header.html
tpl/default/tag.list.html
tpl/vintage/daily.html
tpl/vintage/linklist.html
tpl/vintage/linklist.paging.html
tpl/vintage/page.footer.html
tpl/vintage/page.header.html

index 83a4c5e28699eff2472e4b7c132cef1e8bc53e8b..e9282506188f4d19969e9a49ac27664171c79938 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /**
  * GET an HTTP URL to retrieve its content
- * Uses the cURL library or a fallback method 
+ * Uses the cURL library or a fallback method
  *
  * @param string          $url               URL to get (http://...)
  * @param int             $timeout           network timeout (in seconds)
@@ -415,6 +415,37 @@ function getIpAddressFromProxy($server, $trustedIps)
     return array_pop($ips);
 }
 
+
+/**
+ * Return an identifier based on the advertised client IP address(es)
+ *
+ * This aims at preventing session hijacking from users behind the same proxy
+ * by relying on HTTP headers.
+ *
+ * See:
+ * - https://secure.php.net/manual/en/reserved.variables.server.php
+ * - https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php
+ * - https://stackoverflow.com/questions/12233406/preventing-session-hijacking
+ * - https://stackoverflow.com/questions/21354859/trusting-x-forwarded-for-to-identify-a-visitor
+ *
+ * @param array $server The $_SERVER array
+ *
+ * @return string An identifier based on client IP address information
+ */
+function client_ip_id($server)
+{
+    $ip = $server['REMOTE_ADDR'];
+
+    if (isset($server['HTTP_X_FORWARDED_FOR'])) {
+        $ip = $ip . '_' . $server['HTTP_X_FORWARDED_FOR'];
+    }
+    if (isset($server['HTTP_CLIENT_IP'])) {
+        $ip = $ip . '_' . $server['HTTP_CLIENT_IP'];
+    }
+    return $ip;
+}
+
+
 /**
  * Returns true if Shaarli's currently browsed in HTTPS.
  * Supports reverse proxies (if the headers are correctly set).
diff --git a/application/LoginManager.php b/application/LoginManager.php
deleted file mode 100644 (file)
index 397bc6e..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-<?php
-namespace Shaarli;
-
-/**
- * User login management
- */
-class LoginManager
-{
-    protected $globals = [];
-    protected $configManager = null;
-    protected $banFile = '';
-
-    /**
-     * Constructor
-     *
-     * @param array         $globals       The $GLOBALS array (reference)
-     * @param ConfigManager $configManager Configuration Manager instance.
-     */
-    public function __construct(& $globals, $configManager)
-    {
-        $this->globals = &$globals;
-        $this->configManager = $configManager;
-        $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php');
-        $this->readBanFile();
-    }
-
-    /**
-     * Read a file containing banned IPs
-     */
-    protected function readBanFile()
-    {
-        if (! file_exists($this->banFile)) {
-            return;
-        }
-        include $this->banFile;
-    }
-
-    /**
-     * Write the banned IPs to a file
-     */
-    protected function writeBanFile()
-    {
-        if (! array_key_exists('IPBANS', $this->globals)) {
-            return;
-        }
-        file_put_contents(
-            $this->banFile,
-            "<?php\n\$GLOBALS['IPBANS']=" . var_export($this->globals['IPBANS'], true) . ";\n?>"
-        );
-    }
-
-    /**
-     * Handle a failed login and ban the IP after too many failed attempts
-     *
-     * @param array $server The $_SERVER array
-     */
-    public function handleFailedLogin($server)
-    {
-        $ip = $server['REMOTE_ADDR'];
-        $trusted = $this->configManager->get('security.trusted_proxies', []);
-
-        if (in_array($ip, $trusted)) {
-            $ip = getIpAddressFromProxy($server, $trusted);
-            if (! $ip) {
-                // the IP is behind a trusted forward proxy, but is not forwarded
-                // in the HTTP headers, so we do nothing
-                return;
-            }
-        }
-
-        // increment the fail count for this IP
-        if (isset($this->globals['IPBANS']['FAILURES'][$ip])) {
-            $this->globals['IPBANS']['FAILURES'][$ip]++;
-        } else {
-            $this->globals['IPBANS']['FAILURES'][$ip] = 1;
-        }
-
-        if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) {
-            $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800);
-            logm(
-                $this->configManager->get('resource.log'),
-                $server['REMOTE_ADDR'],
-                'IP address banned from login'
-            );
-        }
-        $this->writeBanFile();
-    }
-
-    /**
-     * Handle a successful login
-     *
-     * @param array $server The $_SERVER array
-     */
-    public function handleSuccessfulLogin($server)
-    {
-        $ip = $server['REMOTE_ADDR'];
-        // FIXME unban when behind a trusted proxy?
-
-        unset($this->globals['IPBANS']['FAILURES'][$ip]);
-        unset($this->globals['IPBANS']['BANS'][$ip]);
-
-        $this->writeBanFile();
-    }
-
-    /**
-     * Check if the user can login from this IP
-     *
-     * @param array $server The $_SERVER array
-     *
-     * @return bool true if the user is allowed to login
-     */
-    public function canLogin($server)
-    {
-        $ip = $server['REMOTE_ADDR'];
-
-        if (! isset($this->globals['IPBANS']['BANS'][$ip])) {
-            // the user is not banned
-            return true;
-        }
-
-        if ($this->globals['IPBANS']['BANS'][$ip] > time()) {
-            // the user is still banned
-            return false;
-        }
-
-        // the ban has expired, the user can attempt to log in again
-        logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.');
-        unset($this->globals['IPBANS']['FAILURES'][$ip]);
-        unset($this->globals['IPBANS']['BANS'][$ip]);
-
-        $this->writeBanFile();
-        return true;
-    }
-}
index 3233d6b64562f78790f08513d8a519be1640e3e3..a4483870497961a210f82fa619f730f207c7d601 100644 (file)
@@ -25,6 +25,9 @@ class PageBuilder
      * @var LinkDB $linkDB instance.
      */
     protected $linkDB;
+    
+    /** @var bool $isLoggedIn Whether the user is logged in **/
+    protected $isLoggedIn = false;
 
     /**
      * PageBuilder constructor.
@@ -34,12 +37,13 @@ class PageBuilder
      * @param LinkDB        $linkDB instance.
      * @param string        $token  Session token
      */
-    public function __construct(&$conf, $linkDB = null, $token = null)
+    public function __construct(&$conf, $linkDB = null, $token = null, $isLoggedIn = false)
     {
         $this->tpl = false;
         $this->conf = $conf;
         $this->linkDB = $linkDB;
         $this->token = $token;
+        $this->isLoggedIn = $isLoggedIn;
     }
 
     /**
@@ -55,7 +59,7 @@ class PageBuilder
                 $this->conf->get('resource.update_check'),
                 $this->conf->get('updates.check_updates_interval'),
                 $this->conf->get('updates.check_updates'),
-                isLoggedIn(),
+                $this->isLoggedIn,
                 $this->conf->get('updates.check_updates_branch')
             );
             $this->tpl->assign('newVersion', escape($version));
@@ -67,6 +71,7 @@ class PageBuilder
             $this->tpl->assign('versionError', escape($exc->getMessage()));
         }
 
+        $this->tpl->assign('is_logged_in', $this->isLoggedIn);
         $this->tpl->assign('feedurl', escape(index_url($_SERVER)));
         $searchcrits = ''; // Search criteria
         if (!empty($_GET['searchtags'])) {
diff --git a/application/SessionManager.php b/application/SessionManager.php
deleted file mode 100644 (file)
index 71f0b38..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-namespace Shaarli;
-
-/**
- * Manages the server-side session
- */
-class SessionManager
-{
-    protected $session = [];
-
-    /**
-     * Constructor
-     *
-     * @param array         $session The $_SESSION array (reference)
-     * @param ConfigManager $conf    ConfigManager instance
-     */
-    public function __construct(& $session, $conf)
-    {
-        $this->session = &$session;
-        $this->conf = $conf;
-    }
-
-    /**
-     * Generates a session token
-     *
-     * @return string token
-     */
-    public function generateToken()
-    {
-        $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
-        $this->session['tokens'][$token] = 1;
-        return $token;
-    }
-
-    /**
-     * Checks the validity of a session token, and destroys it afterwards
-     *
-     * @param string $token The token to check
-     *
-     * @return bool true if the token is valid, else false
-     */
-    public function checkToken($token)
-    {
-        if (! isset($this->session['tokens'][$token])) {
-            // the token is wrong, or has already been used
-            return false;
-        }
-
-        // destroy the token to prevent future use
-        unset($this->session['tokens'][$token]);
-        return true;
-    }
-
-    /**
-     * Validate session ID to prevent Full Path Disclosure.
-     *
-     * See #298.
-     * The session ID's format depends on the hash algorithm set in PHP settings
-     *
-     * @param string $sessionId Session ID
-     *
-     * @return true if valid, false otherwise.
-     *
-     * @see http://php.net/manual/en/function.hash-algos.php
-     * @see http://php.net/manual/en/session.configuration.php
-     */
-    public static function checkId($sessionId)
-    {
-        if (empty($sessionId)) {
-            return false;
-        }
-
-        if (!$sessionId) {
-            return false;
-        }
-
-        if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
-            return false;
-        }
-
-        return true;
-    }
-}
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php
new file mode 100644 (file)
index 0000000..d6784d6
--- /dev/null
@@ -0,0 +1,265 @@
+<?php
+namespace Shaarli\Security;
+
+use Shaarli\Config\ConfigManager;
+
+/**
+ * User login management
+ */
+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 = [];
+
+    /** @var ConfigManager Configuration Manager instance **/
+    protected $configManager = null;
+
+    /** @var SessionManager Session Manager instance **/
+    protected $sessionManager = null;
+
+    /** @var string Path to the file containing IP bans */
+    protected $banFile = '';
+
+    /** @var bool Whether the user is logged in **/
+    protected $isLoggedIn = false;
+
+    /** @var bool Whether the Shaarli instance is open to public edition **/
+    protected $openShaarli = false;
+
+    /** @var string User sign-in token depending on remote IP and credentials */
+    protected $staySignedInToken = '';
+
+    /**
+     * Constructor
+     *
+     * @param array          $globals        The $GLOBALS array (reference)
+     * @param ConfigManager  $configManager  Configuration Manager instance
+     * @param SessionManager $sessionManager SessionManager instance
+     */
+    public function __construct(& $globals, $configManager, $sessionManager)
+    {
+        $this->globals = &$globals;
+        $this->configManager = $configManager;
+        $this->sessionManager = $sessionManager;
+        $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php');
+        $this->readBanFile();
+        if ($this->configManager->get('security.open_shaarli') === true) {
+            $this->openShaarli = true;
+        }
+    }
+
+    /**
+     * Generate a token depending on deployment salt, user password and client IP
+     *
+     * @param string $clientIpAddress The remote client IP address
+     */
+    public function generateStaySignedInToken($clientIpAddress)
+    {
+        $this->staySignedInToken = sha1(
+            $this->configManager->get('credentials.hash')
+            . $clientIpAddress
+            . $this->configManager->get('credentials.salt')
+        );
+    }
+
+    /**
+     * Return the user's client stay-signed-in token
+     *
+     * @return string User's client stay-signed-in token
+     */
+    public function getStaySignedInToken()
+    {
+        return $this->staySignedInToken;
+    }
+
+    /**
+     * 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)
+    {
+        if (! $this->configManager->exists('credentials.login')) {
+            // Shaarli is not configured yet
+            $this->isLoggedIn = false;
+            return;
+        }
+
+        if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE])
+            && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
+        ) {
+            // The user client has a valid stay-signed-in cookie
+            // Session information is updated with the current client information
+            $this->sessionManager->storeLoginInfo($clientIpId);
+
+        } elseif ($this->sessionManager->hasSessionExpired()
+            || $this->sessionManager->hasClientIpChanged($clientIpId)
+        ) {
+            $this->sessionManager->logout();
+            $this->isLoggedIn = false;
+            return;
+        }
+
+        $this->isLoggedIn = true;
+        $this->sessionManager->extendSession();
+    }
+
+    /**
+     * Return whether the user is currently logged in
+     *
+     * @return true when the user is logged in, false otherwise
+     */
+    public function isLoggedIn()
+    {
+        if ($this->openShaarli) {
+            return true;
+        }
+        return $this->isLoggedIn;
+    }
+
+    /**
+     * Check user credentials are valid
+     *
+     * @param string $remoteIp   Remote client IP address
+     * @param string $clientIpId Client IP address identifier
+     * @param string $login      Username
+     * @param string $password   Password
+     *
+     * @return bool true if the provided credentials are valid, false otherwise
+     */
+    public function checkCredentials($remoteIp, $clientIpId, $login, $password)
+    {
+        $hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
+
+        if ($login != $this->configManager->get('credentials.login')
+            || $hash != $this->configManager->get('credentials.hash')
+        ) {
+            logm(
+                $this->configManager->get('resource.log'),
+                $remoteIp,
+                'Login failed for user ' . $login
+            );
+            return false;
+        }
+
+        $this->sessionManager->storeLoginInfo($clientIpId);
+        logm(
+            $this->configManager->get('resource.log'),
+            $remoteIp,
+            'Login successful'
+        );
+        return true;
+    }
+
+    /**
+     * Read a file containing banned IPs
+     */
+    protected function readBanFile()
+    {
+        if (! file_exists($this->banFile)) {
+            return;
+        }
+        include $this->banFile;
+    }
+
+    /**
+     * Write the banned IPs to a file
+     */
+    protected function writeBanFile()
+    {
+        if (! array_key_exists('IPBANS', $this->globals)) {
+            return;
+        }
+        file_put_contents(
+            $this->banFile,
+            "<?php\n\$GLOBALS['IPBANS']=" . var_export($this->globals['IPBANS'], true) . ";\n?>"
+        );
+    }
+
+    /**
+     * Handle a failed login and ban the IP after too many failed attempts
+     *
+     * @param array $server The $_SERVER array
+     */
+    public function handleFailedLogin($server)
+    {
+        $ip = $server['REMOTE_ADDR'];
+        $trusted = $this->configManager->get('security.trusted_proxies', []);
+
+        if (in_array($ip, $trusted)) {
+            $ip = getIpAddressFromProxy($server, $trusted);
+            if (! $ip) {
+                // the IP is behind a trusted forward proxy, but is not forwarded
+                // in the HTTP headers, so we do nothing
+                return;
+            }
+        }
+
+        // increment the fail count for this IP
+        if (isset($this->globals['IPBANS']['FAILURES'][$ip])) {
+            $this->globals['IPBANS']['FAILURES'][$ip]++;
+        } else {
+            $this->globals['IPBANS']['FAILURES'][$ip] = 1;
+        }
+
+        if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) {
+            $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800);
+            logm(
+                $this->configManager->get('resource.log'),
+                $server['REMOTE_ADDR'],
+                'IP address banned from login'
+            );
+        }
+        $this->writeBanFile();
+    }
+
+    /**
+     * Handle a successful login
+     *
+     * @param array $server The $_SERVER array
+     */
+    public function handleSuccessfulLogin($server)
+    {
+        $ip = $server['REMOTE_ADDR'];
+        // FIXME unban when behind a trusted proxy?
+
+        unset($this->globals['IPBANS']['FAILURES'][$ip]);
+        unset($this->globals['IPBANS']['BANS'][$ip]);
+
+        $this->writeBanFile();
+    }
+
+    /**
+     * Check if the user can login from this IP
+     *
+     * @param array $server The $_SERVER array
+     *
+     * @return bool true if the user is allowed to login
+     */
+    public function canLogin($server)
+    {
+        $ip = $server['REMOTE_ADDR'];
+
+        if (! isset($this->globals['IPBANS']['BANS'][$ip])) {
+            // the user is not banned
+            return true;
+        }
+
+        if ($this->globals['IPBANS']['BANS'][$ip] > time()) {
+            // the user is still banned
+            return false;
+        }
+
+        // the ban has expired, the user can attempt to log in again
+        logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.');
+        unset($this->globals['IPBANS']['FAILURES'][$ip]);
+        unset($this->globals['IPBANS']['BANS'][$ip]);
+
+        $this->writeBanFile();
+        return true;
+    }
+}
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
new file mode 100644 (file)
index 0000000..b8b8ab8
--- /dev/null
@@ -0,0 +1,199 @@
+<?php
+namespace Shaarli\Security;
+
+use Shaarli\Config\ConfigManager;
+
+/**
+ * Manages the server-side session
+ */
+class SessionManager
+{
+    /** @var int Session expiration timeout, in seconds */
+    public static $SHORT_TIMEOUT = 3600;    // 1 hour
+
+    /** @var int Session expiration timeout, in seconds */
+    public static $LONG_TIMEOUT = 31536000; // 1 year
+
+    /** @var array Local reference to the global $_SESSION array */
+    protected $session = [];
+
+    /** @var ConfigManager Configuration Manager instance **/
+    protected $conf = null;
+
+    /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */
+    protected $staySignedIn = false;
+
+    /**
+     * Constructor
+     *
+     * @param array         $session The $_SESSION array (reference)
+     * @param ConfigManager $conf    ConfigManager instance
+     */
+    public function __construct(& $session, $conf)
+    {
+        $this->session = &$session;
+        $this->conf = $conf;
+    }
+
+    /**
+     * Define whether the user should stay signed in across browser sessions
+     *
+     * @param bool $staySignedIn Keep the user signed in
+     */
+    public function setStaySignedIn($staySignedIn)
+    {
+        $this->staySignedIn = $staySignedIn;
+    }
+
+    /**
+     * Generates a session token
+     *
+     * @return string token
+     */
+    public function generateToken()
+    {
+        $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
+        $this->session['tokens'][$token] = 1;
+        return $token;
+    }
+
+    /**
+     * Checks the validity of a session token, and destroys it afterwards
+     *
+     * @param string $token The token to check
+     *
+     * @return bool true if the token is valid, else false
+     */
+    public function checkToken($token)
+    {
+        if (! isset($this->session['tokens'][$token])) {
+            // the token is wrong, or has already been used
+            return false;
+        }
+
+        // destroy the token to prevent future use
+        unset($this->session['tokens'][$token]);
+        return true;
+    }
+
+    /**
+     * Validate session ID to prevent Full Path Disclosure.
+     *
+     * See #298.
+     * The session ID's format depends on the hash algorithm set in PHP settings
+     *
+     * @param string $sessionId Session ID
+     *
+     * @return true if valid, false otherwise.
+     *
+     * @see http://php.net/manual/en/function.hash-algos.php
+     * @see http://php.net/manual/en/session.configuration.php
+     */
+    public static function checkId($sessionId)
+    {
+        if (empty($sessionId)) {
+            return false;
+        }
+
+        if (!$sessionId) {
+            return false;
+        }
+
+        if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Store user login information after a successful login
+     *
+     * @param string $clientIpId Client IP address identifier
+     */
+    public function storeLoginInfo($clientIpId)
+    {
+        $this->session['ip'] = $clientIpId;
+        $this->session['username'] = $this->conf->get('credentials.login');
+        $this->extendTimeValidityBy(self::$SHORT_TIMEOUT);
+    }
+
+    /**
+     * Extend session validity
+     */
+    public function extendSession()
+    {
+        if ($this->staySignedIn) {
+            return $this->extendTimeValidityBy(self::$LONG_TIMEOUT);
+        }
+        return $this->extendTimeValidityBy(self::$SHORT_TIMEOUT);
+    }
+
+    /**
+     * Extend expiration time
+     *
+     * @param int $duration Expiration time extension (seconds)
+     *
+     * @return int New session expiration time
+     */
+    protected function extendTimeValidityBy($duration)
+    {
+        $expirationTime = time() + $duration;
+        $this->session['expires_on'] = $expirationTime;
+        return $expirationTime;
+    }
+
+    /**
+     * Logout a user by unsetting all login information
+     *
+     * See:
+     * - https://secure.php.net/manual/en/function.setcookie.php
+     */
+    public function logout()
+    {
+        if (isset($this->session)) {
+            unset($this->session['ip']);
+            unset($this->session['expires_on']);
+            unset($this->session['username']);
+            unset($this->session['visibility']);
+            unset($this->session['untaggedonly']);
+        }
+    }
+
+    /**
+     * Check whether the session has expired
+     *
+     * @param string $clientIpId Client IP address identifier
+     *
+     * @return bool true if the session has expired, false otherwise
+     */
+    public function hasSessionExpired()
+    {
+        if (empty($this->session['expires_on'])) {
+            return true;
+        }
+        if (time() >= $this->session['expires_on']) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Check whether the client IP address has changed
+     *
+     * @param string $clientIpId Client IP address identifier
+     *
+     * @return bool true if the IP has changed, false if it has not, or
+     *              if session protection has been disabled
+     */
+    public function hasClientIpChanged($clientIpId)
+    {
+        if ($this->conf->get('security.session_protection_disabled') === true) {
+            return false;
+        }
+        if (isset($this->session['ip']) && $this->session['ip'] === $clientIpId) {
+            return false;
+        }
+        return true;
+    }
+}
index 15e082f8195159b92ef4e36091678718b779f0c0..0d4c623c8b181bc2aa5a7433de4a40290b6b3420 100644 (file)
@@ -36,7 +36,8 @@
             "Shaarli\\Api\\Controllers\\": "application/api/controllers",
             "Shaarli\\Api\\Exceptions\\": "application/api/exceptions",
             "Shaarli\\Config\\": "application/config/",
-            "Shaarli\\Config\\Exception\\": "application/config/exception"
+            "Shaarli\\Config\\Exception\\": "application/config/exception",
+            "Shaarli\\Security\\": "application/security"
         }
     }
 }
index 2fe3f8215f659683cf96ed1e403089a1775aad62..c34434ddcb0016b896330d63d19c7cdfc16fdd78 100644 (file)
--- a/index.php
+++ b/index.php
@@ -78,8 +78,8 @@ require_once 'application/Updater.php';
 use \Shaarli\Languages;
 use \Shaarli\ThemeUtils;
 use \Shaarli\Config\ConfigManager;
-use \Shaarli\LoginManager;
-use \Shaarli\SessionManager;
+use \Shaarli\Security\LoginManager;
+use \Shaarli\Security\SessionManager;
 
 // Ensure the PHP version is supported
 try {
@@ -101,8 +101,6 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
 // Set default cookie expiration and path.
 session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
 // Set session parameters on server side.
-// If the user does not access any page within this time, his/her session is considered expired.
-define('INACTIVITY_TIMEOUT', 3600); // in seconds.
 // Use cookies to store session.
 ini_set('session.use_cookies', 1);
 // Force cookies for session (phpsessionID forbidden in URL).
@@ -123,8 +121,10 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli']))
 }
 
 $conf = new ConfigManager();
-$loginManager = new LoginManager($GLOBALS, $conf);
 $sessionManager = new SessionManager($_SESSION, $conf);
+$loginManager = new LoginManager($GLOBALS, $conf, $sessionManager);
+$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
+$clientIpId = client_ip_id($_SERVER);
 
 // LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
 if (! defined('LC_MESSAGES')) {
@@ -177,157 +177,61 @@ if (! is_file($conf->getConfigFileExt())) {
     install($conf, $sessionManager);
 }
 
-// a token depending of deployment salt, user password, and the current ip
-define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
+$loginManager->checkLoginState($_COOKIE, $clientIpId);
 
 /**
- * Checking session state (i.e. is the user still logged in)
+ * Adapter function to ensure compatibility with third-party templates
  *
- * @param ConfigManager $conf The configuration manager.
+ * @see https://github.com/shaarli/Shaarli/pull/1086
  *
- * @return bool: true if the user is logged in, false otherwise.
+ * @return bool true when the user is logged in, false otherwise
  */
-function setup_login_state($conf)
-{
-    if ($conf->get('security.open_shaarli')) {
-        return true;
-    }
-    $userIsLoggedIn = false; // By default, we do not consider the user as logged in;
-    $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met.
-    if (! $conf->exists('credentials.login')) {
-        $userIsLoggedIn = false;  // Shaarli is not configured yet.
-        $loginFailure = true;
-    }
-    if (isset($_COOKIE['shaarli_staySignedIn']) &&
-        $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN &&
-        !$loginFailure)
-    {
-        fillSessionInfo($conf);
-        $userIsLoggedIn = true;
-    }
-    // If session does not exist on server side, or IP address has changed, or session has expired, logout.
-    if (empty($_SESSION['uid'])
-        || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs())
-        || time() >= $_SESSION['expires_on'])
-    {
-        logout();
-        $userIsLoggedIn = false;
-        $loginFailure = true;
-    }
-    if (!empty($_SESSION['longlastingsession'])) {
-        $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked.
-    }
-    else {
-        $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date.
-    }
-    if (!$loginFailure) {
-        $userIsLoggedIn = true;
-    }
-
-    return $userIsLoggedIn;
-}
-$userIsLoggedIn = setup_login_state($conf);
-
-// ------------------------------------------------------------------------------------------
-// Session management
-
-// Returns the IP address of the client (Used to prevent session cookie hijacking.)
-function allIPs()
-{
-    $ip = $_SERVER['REMOTE_ADDR'];
-    // Then we use more HTTP headers to prevent session hijacking from users behind the same proxy.
-    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; }
-    if (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip=$ip.'_'.$_SERVER['HTTP_CLIENT_IP']; }
-    return $ip;
-}
-
-/**
- * Load user session.
- *
- * @param ConfigManager $conf Configuration Manager instance.
- */
-function fillSessionInfo($conf)
-{
-    $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid)
-    $_SESSION['ip']=allIPs();                // We store IP address(es) of the client to make sure session is not hijacked.
-    $_SESSION['username']= $conf->get('credentials.login');
-    $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT;  // Set session expiration.
-}
-
-/**
- * Check that user/password is correct.
- *
- * @param string        $login    Username
- * @param string        $password User password
- * @param ConfigManager $conf     Configuration Manager instance.
- *
- * @return bool: authentication successful or not.
- */
-function check_auth($login, $password, $conf)
-{
-    $hash = sha1($password . $login . $conf->get('credentials.salt'));
-    if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash'))
-    {   // Login/password is correct.
-        fillSessionInfo($conf);
-        logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful');
-        return true;
-    }
-    logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login);
-    return false;
-}
-
-// Returns true if the user is logged in.
 function isLoggedIn()
 {
-    global $userIsLoggedIn;
-    return $userIsLoggedIn;
+    global $loginManager;
+    return $loginManager->isLoggedIn();
 }
 
-// Force logout.
-function logout() {
-    if (isset($_SESSION)) {
-        unset($_SESSION['uid']);
-        unset($_SESSION['ip']);
-        unset($_SESSION['username']);
-        unset($_SESSION['visibility']);
-        unset($_SESSION['untaggedonly']);
-    }
-    setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH);
-}
 
 // ------------------------------------------------------------------------------------------
 // Process login form: Check if login/password is correct.
-if (isset($_POST['login']))
-{
+if (isset($_POST['login'])) {
     if (! $loginManager->canLogin($_SERVER)) {
         die(t('I said: NO. You are banned for the moment. Go away.'));
     }
     if (isset($_POST['password'])
         && $sessionManager->checkToken($_POST['token'])
-        && (check_auth($_POST['login'], $_POST['password'], $conf))
+        && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
     ) {
-        // Login/password is OK.
         $loginManager->handleSuccessfulLogin($_SERVER);
 
-        // If user wants to keep the session cookie even after the browser closes:
-        if (!empty($_POST['longlastingsession'])) {
-            $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year)
-            $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now)
-            setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH);
-            $_SESSION['expires_on'] = $expiration;  // Set session expiration on server-side.
-
-            $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
-            session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side
+        $cookiedir = '';
+        if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
             // Note: Never forget the trailing slash on the cookie path!
-            session_regenerate_id(true);  // Send cookie with new expiration date to browser.
+            $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
         }
-        else // Standard session expiration (=when browser closes)
-        {
-            $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
-            session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes"
-            session_regenerate_id(true);
+
+        if (!empty($_POST['longlastingsession'])) {
+            // Keep the session cookie even after the browser closes
+            $sessionManager->setStaySignedIn(true);
+            $expirationTime = $sessionManager->extendSession();
+
+            setcookie(
+                $loginManager::$STAY_SIGNED_IN_COOKIE,
+                $loginManager->getStaySignedInToken(),
+                $expirationTime,
+                WEB_PATH
+            );
+
+        } else {
+            // Standard session expiration (=when browser closes)
+            $expirationTime = 0;
         }
 
+        // Send cookie with the new expiration date to the browser
+        session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
+        session_regenerate_id(true);
+
         // Optional redirect after login:
         if (isset($_GET['post'])) {
             $uri = '?post='. urlencode($_GET['post']);
@@ -380,15 +284,16 @@ if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array();  // Token are atta
  * Gives the last 7 days (which have links).
  * This RSS feed cannot be filtered.
  *
- * @param ConfigManager $conf Configuration Manager instance.
+ * @param ConfigManager $conf         Configuration Manager instance
+ * @param LoginManager  $loginManager LoginManager instance
  */
-function showDailyRSS($conf) {
+function showDailyRSS($conf, $loginManager) {
     // Cache system
     $query = $_SERVER['QUERY_STRING'];
     $cache = new CachedPage(
         $conf->get('config.PAGE_CACHE'),
         page_url($_SERVER),
-        startsWith($query,'do=dailyrss') && !isLoggedIn()
+        startsWith($query,'do=dailyrss') && !$loginManager->isLoggedIn()
     );
     $cached = $cache->cachedVersion();
     if (!empty($cached)) {
@@ -400,7 +305,7 @@ function showDailyRSS($conf) {
     // Read links from database (and filter private links if used it not logged in).
     $LINKSDB = new LinkDB(
         $conf->get('resource.datastore'),
-        isLoggedIn(),
+        $loginManager->isLoggedIn(),
         $conf->get('privacy.hide_public_links'),
         $conf->get('redirector.url'),
         $conf->get('redirector.encode_url')
@@ -482,9 +387,10 @@ function showDailyRSS($conf) {
  * @param PageBuilder   $pageBuilder   Template engine wrapper.
  * @param LinkDB        $LINKSDB       LinkDB instance.
  * @param ConfigManager $conf          Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instane.
+ * @param PluginManager $pluginManager Plugin Manager instance.
+ * @param LoginManager  $loginManager  Login Manager instance
  */
-function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
+function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
 {
     $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
     if (isset($_GET['day'])) {
@@ -542,7 +448,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
 
     /* Hook is called before column construction so that plugins don't have
        to deal with columns. */
-    $pluginManager->executeHooks('render_daily', $data, array('loggedin' => isLoggedIn()));
+    $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
 
     /* We need to spread the articles on 3 columns.
        I did not want to use a JavaScript lib like http://masonry.desandro.com/
@@ -586,8 +492,8 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
  * @param ConfigManager $conf    Configuration Manager instance.
  * @param PluginManager $pluginManager Plugin Manager instance.
  */
-function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
-    buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager); // Compute list of links to display
+function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) {
+    buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager, $loginManager);
     $PAGE->renderPage('linklist');
 }
 
@@ -607,7 +513,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         read_updates_file($conf->get('resource.updates')),
         $LINKSDB,
         $conf,
-        isLoggedIn()
+        $loginManager->isLoggedIn()
     );
     try {
         $newUpdates = $updater->update();
@@ -622,18 +528,18 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         die($e->getMessage());
     }
 
-    $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken());
+    $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
     $PAGE->assign('linkcount', count($LINKSDB));
     $PAGE->assign('privateLinkcount', count_private($LINKSDB));
     $PAGE->assign('plugin_errors', $pluginManager->getErrors());
 
     // Determine which page will be rendered.
     $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
-    $targetPage = Router::findPage($query, $_GET, isLoggedIn());
+    $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
 
     if (
         // if the user isn't logged in
-        !isLoggedIn() &&
+        !$loginManager->isLoggedIn() &&
         // and Shaarli doesn't have public content...
         $conf->get('privacy.hide_public_links') &&
         // and is configured to enforce the login
@@ -661,7 +567,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         $pluginManager->executeHooks('render_' . $name, $plugin_data,
             array(
                 'target' => $targetPage,
-                'loggedin' => isLoggedIn()
+                'loggedin' => $loginManager->isLoggedIn()
             )
         );
         $PAGE->assign('plugins_' . $name, $plugin_data);
@@ -686,7 +592,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
     if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout'))
     {
         invalidateCaches($conf->get('resource.page_cache'));
-        logout();
+        $sessionManager->logout();
+        setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
         header('Location: ?');
         exit;
     }
@@ -713,7 +620,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         $data = array(
             'linksToDisplay' => $linksToDisplay,
         );
-        $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => isLoggedIn()));
+        $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn()));
 
         foreach ($data as $key => $value) {
             $PAGE->assign($key, $value);
@@ -760,7 +667,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             'search_tags' => $searchTags,
             'tags' => $tagList,
         );
-        $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn()));
+        $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
 
         foreach ($data as $key => $value) {
             $PAGE->assign($key, $value);
@@ -793,7 +700,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             'search_tags' => $searchTags,
             'tags' => $tags,
         ];
-        $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
+        $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
 
         foreach ($data as $key => $value) {
             $PAGE->assign($key, $value);
@@ -807,7 +714,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
 
     // Daily page.
     if ($targetPage == Router::$PAGE_DAILY) {
-        showDaily($PAGE, $LINKSDB, $conf, $pluginManager);
+        showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
     }
 
     // ATOM and RSS feed.
@@ -820,7 +727,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         $cache = new CachedPage(
             $conf->get('resource.page_cache'),
             page_url($_SERVER),
-            startsWith($query,'do='. $targetPage) && !isLoggedIn()
+            startsWith($query,'do='. $targetPage) && !$loginManager->isLoggedIn()
         );
         $cached = $cache->cachedVersion();
         if (!empty($cached)) {
@@ -829,15 +736,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
         }
 
         // Generate data.
-        $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, isLoggedIn());
+        $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn());
         $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
-        $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn());
+        $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
         $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
         $data = $feedGenerator->buildData();
 
         // Process plugin hook.
         $pluginManager->executeHooks('render_feed', $data, array(
-            'loggedin' => isLoggedIn(),
+            'loggedin' => $loginManager->isLoggedIn(),
             'target' => $targetPage,
         ));
 
@@ -985,7 +892,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
     }
 
     // -------- Handle other actions allowed for non-logged in users:
-    if (!isLoggedIn())
+    if (!$loginManager->isLoggedIn())
     {
         // User tries to post new link but is not logged in:
         // Show login screen, then redirect to ?post=...
@@ -1001,7 +908,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             exit;
         }
 
-        showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
+        showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
         if (isset($_GET['edit_link'])) {
             header('Location: ?do=login&edit_link='. escape($_GET['edit_link']));
             exit;
@@ -1052,7 +959,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
             $conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt')));
             try {
-                $conf->write(isLoggedIn());
+                $conf->write($loginManager->isLoggedIn());
             }
             catch(Exception $e) {
                 error_log(
@@ -1103,7 +1010,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             $conf->set('translation.language', escape($_POST['language']));
 
             try {
-                $conf->write(isLoggedIn());
+                $conf->write($loginManager->isLoggedIn());
                 $history->updateSettings();
                 invalidateCaches($conf->get('resource.page_cache'));
             }
@@ -1555,7 +1462,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
             else {
                 $conf->set('general.enabled_plugins', save_plugin_config($_POST));
             }
-            $conf->write(isLoggedIn());
+            $conf->write($loginManager->isLoggedIn());
             $history->updateSettings();
         }
         catch (Exception $e) {
@@ -1580,7 +1487,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
     }
 
     // -------- Otherwise, simply display search form and links:
-    showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
+    showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
     exit;
 }
 
@@ -1592,8 +1499,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager,
  * @param LinkDB        $LINKSDB       LinkDB instance.
  * @param ConfigManager $conf          Configuration Manager instance.
  * @param PluginManager $pluginManager Plugin Manager instance.
+ * @param LoginManager  $loginManager  LoginManager instance
  */
-function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
+function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
 {
     // Used in templates
     if (isset($_GET['searchtags'])) {
@@ -1632,8 +1540,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
         $keys[] = $key;
     }
 
-
-
     // Select articles according to paging.
     $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
     $pagecount = $pagecount == 0 ? 1 : $pagecount;
@@ -1714,7 +1620,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
         $data['pagetitle'] .= '- '. $conf->get('general.title');
     }
 
-    $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn()));
+    $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
 
     foreach ($data as $key => $value) {
         $PAGE->assign($key, $value);
@@ -1985,7 +1891,7 @@ function install($conf, $sessionManager) {
         );
         try {
             // Everything is ok, let's create config file.
-            $conf->write(isLoggedIn());
+            $conf->write($loginManager->isLoggedIn());
         }
         catch(Exception $e) {
             error_log(
@@ -2249,7 +2155,7 @@ try {
 
 $linkDb = new LinkDB(
     $conf->get('resource.datastore'),
-    isLoggedIn(),
+    $loginManager->isLoggedIn(),
     $conf->get('privacy.hide_public_links'),
     $conf->get('redirector.url'),
     $conf->get('redirector.encode_url')
diff --git a/tests/HttpUtils/ClientIpIdTest.php b/tests/HttpUtils/ClientIpIdTest.php
new file mode 100644 (file)
index 0000000..c15ac5c
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+/**
+ * HttpUtils' tests
+ */
+
+require_once 'application/HttpUtils.php';
+
+/**
+ * Unitary tests for client_ip_id()
+ */
+class ClientIpIdTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * Get a remote client ID based on its IP
+     */
+    public function testClientIpIdRemote()
+    {
+        $this->assertEquals(
+            '10.1.167.42',
+            client_ip_id(['REMOTE_ADDR' => '10.1.167.42'])
+        );
+    }
+
+    /**
+     * Get a remote client ID based on its IP and proxy information (1)
+     */
+    public function testClientIpIdRemoteForwarded()
+    {
+        $this->assertEquals(
+            '10.1.167.42_127.0.1.47',
+            client_ip_id([
+                'REMOTE_ADDR' => '10.1.167.42',
+                'HTTP_X_FORWARDED_FOR' => '127.0.1.47'
+            ])
+        );
+    }
+
+    /**
+     * Get a remote client ID based on its IP and proxy information (2)
+     */
+    public function testClientIpIdRemoteForwardedClient()
+    {
+        $this->assertEquals(
+            '10.1.167.42_10.1.167.56_127.0.1.47',
+            client_ip_id([
+                'REMOTE_ADDR' => '10.1.167.42',
+                'HTTP_X_FORWARDED_FOR' => '10.1.167.56',
+                'HTTP_CLIENT_IP' => '127.0.1.47'
+            ])
+        );
+    }
+}
diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php
deleted file mode 100644 (file)
index aa75962..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-<?php
-require_once 'tests/utils/FakeConfigManager.php';
-
-// Initialize reference data _before_ PHPUnit starts a session
-require_once 'tests/utils/ReferenceSessionIdHashes.php';
-ReferenceSessionIdHashes::genAllHashes();
-
-use \Shaarli\SessionManager;
-use \PHPUnit\Framework\TestCase;
-
-
-/**
- * Test coverage for SessionManager
- */
-class SessionManagerTest extends TestCase
-{
-    // Session ID hashes
-    protected static $sidHashes = null;
-
-    // Fake ConfigManager
-    protected static $conf = null;
-
-    /**
-     * Assign reference data
-     */
-    public static function setUpBeforeClass()
-    {
-        self::$sidHashes = ReferenceSessionIdHashes::getHashes();
-        self::$conf = new FakeConfigManager();
-    }
-
-    /**
-     * Generate a session token
-     */
-    public function testGenerateToken()
-    {
-        $session = [];
-        $sessionManager = new SessionManager($session, self::$conf);
-
-        $token = $sessionManager->generateToken();
-
-        $this->assertEquals(1, $session['tokens'][$token]);
-        $this->assertEquals(40, strlen($token));
-    }
-
-    /**
-     * Check a session token
-     */
-    public function testCheckToken()
-    {
-        $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b';
-        $session = [
-            'tokens' => [
-                $token => 1,
-            ],
-        ];
-        $sessionManager = new SessionManager($session, self::$conf);
-
-        // check and destroy the token
-        $this->assertTrue($sessionManager->checkToken($token));
-        $this->assertFalse(isset($session['tokens'][$token]));
-
-        // ensure the token has been destroyed
-        $this->assertFalse($sessionManager->checkToken($token));
-    }
-
-    /**
-     * Generate and check a session token
-     */
-    public function testGenerateAndCheckToken()
-    {
-        $session = [];
-        $sessionManager = new SessionManager($session, self::$conf);
-
-        $token = $sessionManager->generateToken();
-
-        // ensure a token has been generated
-        $this->assertEquals(1, $session['tokens'][$token]);
-        $this->assertEquals(40, strlen($token));
-
-        // check and destroy the token
-        $this->assertTrue($sessionManager->checkToken($token));
-        $this->assertFalse(isset($session['tokens'][$token]));
-
-        // ensure the token has been destroyed
-        $this->assertFalse($sessionManager->checkToken($token));
-    }
-
-    /**
-     * Check an invalid session token
-     */
-    public function testCheckInvalidToken()
-    {
-        $session = [];
-        $sessionManager = new SessionManager($session, self::$conf);
-
-        $this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'));
-    }
-
-    /**
-     * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES!
-     *
-     * This tests extensively covers all hash algorithms / bit representations
-     */
-    public function testIsAnyHashSessionIdValid()
-    {
-        foreach (self::$sidHashes as $algo => $bpcs) {
-            foreach ($bpcs as $bpc => $hash) {
-                $this->assertTrue(SessionManager::checkId($hash));
-            }
-        }
-    }
-
-    /**
-     * Test checkId with a valid ID - SHA-1 hashes
-     */
-    public function testIsSha1SessionIdValid()
-    {
-        $this->assertTrue(SessionManager::checkId(sha1('shaarli')));
-    }
-
-    /**
-     * Test checkId with a valid ID - SHA-256 hashes
-     */
-    public function testIsSha256SessionIdValid()
-    {
-        $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli')));
-    }
-
-    /**
-     * Test checkId with a valid ID - SHA-512 hashes
-     */
-    public function testIsSha512SessionIdValid()
-    {
-        $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli')));
-    }
-
-    /**
-     * Test checkId with invalid IDs.
-     */
-    public function testIsSessionIdInvalid()
-    {
-        $this->assertFalse(SessionManager::checkId(''));
-        $this->assertFalse(SessionManager::checkId([]));
-        $this->assertFalse(
-            SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
-        );
-    }
-}
similarity index 54%
rename from tests/LoginManagerTest.php
rename to tests/security/LoginManagerTest.php
index 4159038ef0c77499a7e0cd48a0c4d5d74ec28020..f26cd1eb8635c0bd21f8f4ab68043b7569562ddf 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-namespace Shaarli;
+namespace Shaarli\Security;
 
 require_once 'tests/utils/FakeConfigManager.php';
 use \PHPUnit\Framework\TestCase;
@@ -9,15 +9,54 @@ use \PHPUnit\Framework\TestCase;
  */
 class LoginManagerTest extends TestCase
 {
+    /** @var \FakeConfigManager Configuration Manager instance */
     protected $configManager = null;
+
+    /** @var LoginManager Login Manager instance */
     protected $loginManager = null;
+
+    /** @var SessionManager Session Manager instance */
+    protected $sessionManager = null;
+
+    /** @var string Banned IP filename */
     protected $banFile = 'sandbox/ipbans.php';
+
+    /** @var string Log filename */
     protected $logFile = 'sandbox/shaarli.log';
+
+    /** @var array Simulates the $_COOKIE array */
+    protected $cookie = [];
+
+    /** @var array Simulates the $GLOBALS array */
     protected $globals = [];
-    protected $ipAddr = '127.0.0.1';
+
+    /** @var array Simulates the $_SERVER array */
     protected $server = [];
+
+    /** @var array Simulates the $_SESSION array */
+    protected $session = [];
+
+    /** @var string Advertised client IP address */
+    protected $clientIpAddress = '10.1.47.179';
+
+    /** @var string Local client IP address */
+    protected $ipAddr = '127.0.0.1';
+
+    /** @var string Trusted proxy IP address */
     protected $trustedProxy = '10.1.1.100';
 
+    /** @var string User login */
+    protected $login = 'johndoe';
+
+    /** @var string User password */
+    protected $password = 'IC4nHazL0g1n?';
+
+    /** @var string Hash of the salted user password */
+    protected $passwordHash = '';
+
+    /** @var string Salt used by hash functions */
+    protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2';
+
     /**
      * Prepare or reset test resources
      */
@@ -27,7 +66,12 @@ class LoginManagerTest extends TestCase
             unlink($this->banFile);
         }
 
+        $this->passwordHash = sha1($this->password . $this->login . $this->salt);
+
         $this->configManager = new \FakeConfigManager([
+            'credentials.login' => $this->login,
+            'credentials.hash' => $this->passwordHash,
+            'credentials.salt' => $this->salt,
             'resource.ban_file' => $this->banFile,
             'resource.log' => $this->logFile,
             'security.ban_after' => 4,
@@ -35,10 +79,15 @@ class LoginManagerTest extends TestCase
             'security.trusted_proxies' => [$this->trustedProxy],
         ]);
 
+        $this->cookie = [];
+
         $this->globals = &$GLOBALS;
         unset($this->globals['IPBANS']);
 
-        $this->loginManager = new LoginManager($this->globals, $this->configManager);
+        $this->session = [];
+
+        $this->sessionManager = new SessionManager($this->session, $this->configManager);
+        $this->loginManager = new LoginManager($this->globals, $this->configManager, $this->sessionManager);
         $this->server['REMOTE_ADDR'] = $this->ipAddr;
     }
 
@@ -59,7 +108,7 @@ class LoginManagerTest extends TestCase
             $this->banFile,
             "<?php\n\$GLOBALS['IPBANS']=array('FAILURES' => array('127.0.0.1' => 99));\n?>"
         );
-        new LoginManager($this->globals, $this->configManager);
+        new LoginManager($this->globals, $this->configManager, null);
         $this->assertEquals(99, $this->globals['IPBANS']['FAILURES']['127.0.0.1']);
     }
 
@@ -196,4 +245,130 @@ class LoginManagerTest extends TestCase
         $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600;
         $this->assertTrue($this->loginManager->canLogin($this->server));
     }
+
+    /**
+     * Generate a token depending on the user credentials and client IP
+     */
+    public function testGenerateStaySignedInToken()
+    {
+        $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
+
+        $this->assertEquals(
+            sha1($this->passwordHash . $this->clientIpAddress . $this->salt),
+            $this->loginManager->getStaySignedInToken()
+        );
+    }
+
+    /**
+     * Check user login - Shaarli has not yet been configured
+     */
+    public function testCheckLoginStateNotConfigured()
+    {
+        $configManager = new \FakeConfigManager([
+            'resource.ban_file' => $this->banFile,
+        ]);
+        $loginManager = new LoginManager($this->globals, $configManager, null);
+        $loginManager->checkLoginState([], '');
+
+        $this->assertFalse($loginManager->isLoggedIn());
+    }
+
+    /**
+     * Check user login - the client cookie does not match the server token
+     */
+    public function testCheckLoginStateStaySignedInWithInvalidToken()
+    {
+        // simulate a previous login
+        $this->session = [
+            'ip' => $this->clientIpAddress,
+            'expires_on' => time() + 100,
+        ];
+        $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
+        $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope';
+
+        $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+
+        $this->assertTrue($this->loginManager->isLoggedIn());
+        $this->assertTrue(empty($this->session['username']));
+    }
+
+    /**
+     * Check user login - the client cookie matches the server token
+     */
+    public function testCheckLoginStateStaySignedInWithValidToken()
+    {
+        $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
+        $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken();
+
+        $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+
+        $this->assertTrue($this->loginManager->isLoggedIn());
+        $this->assertEquals($this->login, $this->session['username']);
+        $this->assertEquals($this->clientIpAddress, $this->session['ip']);
+    }
+
+    /**
+     * Check user login - the session has expired
+     */
+    public function testCheckLoginStateSessionExpired()
+    {
+        $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
+        $this->session['expires_on'] = time() - 100;
+
+        $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress);
+
+        $this->assertFalse($this->loginManager->isLoggedIn());
+    }
+
+    /**
+     * Check user login - the remote client IP has changed
+     */
+    public function testCheckLoginStateClientIpChanged()
+    {
+        $this->loginManager->generateStaySignedInToken($this->clientIpAddress);
+
+        $this->loginManager->checkLoginState($this->cookie, '10.7.157.98');
+
+        $this->assertFalse($this->loginManager->isLoggedIn());
+    }
+
+    /**
+     * Check user credentials - wrong login supplied
+     */
+    public function testCheckCredentialsWrongLogin()
+    {
+        $this->assertFalse(
+            $this->loginManager->checkCredentials('', '', 'b4dl0g1n', $this->password)
+        );
+    }
+
+    /**
+     * Check user credentials - wrong password supplied
+     */
+    public function testCheckCredentialsWrongPassword()
+    {
+        $this->assertFalse(
+            $this->loginManager->checkCredentials('', '', $this->login, 'b4dp455wd')
+        );
+    }
+
+    /**
+     * Check user credentials - wrong login and password supplied
+     */
+    public function testCheckCredentialsWrongLoginAndPassword()
+    {
+        $this->assertFalse(
+            $this->loginManager->checkCredentials('', '', 'b4dl0g1n', 'b4dp455wd')
+        );
+    }
+
+    /**
+     * Check user credentials - correct login and password supplied
+     */
+    public function testCheckCredentialsGoodLoginAndPassword()
+    {
+        $this->assertTrue(
+            $this->loginManager->checkCredentials('', '', $this->login, $this->password)
+        );
+    }
 }
diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php
new file mode 100644 (file)
index 0000000..9bd868f
--- /dev/null
@@ -0,0 +1,273 @@
+<?php
+require_once 'tests/utils/FakeConfigManager.php';
+
+// Initialize reference data _before_ PHPUnit starts a session
+require_once 'tests/utils/ReferenceSessionIdHashes.php';
+ReferenceSessionIdHashes::genAllHashes();
+
+use \Shaarli\Security\SessionManager;
+use \PHPUnit\Framework\TestCase;
+
+
+/**
+ * Test coverage for SessionManager
+ */
+class SessionManagerTest extends TestCase
+{
+    /** @var array Session ID hashes */
+    protected static $sidHashes = null;
+
+    /** @var \FakeConfigManager ConfigManager substitute for testing */
+    protected $conf = null;
+
+    /** @var array $_SESSION array for testing */
+    protected $session = [];
+
+    /** @var SessionManager Server-side session management abstraction */
+    protected $sessionManager = null;
+
+    /**
+     * Assign reference data
+     */
+    public static function setUpBeforeClass()
+    {
+        self::$sidHashes = ReferenceSessionIdHashes::getHashes();
+    }
+
+    /**
+     * Initialize or reset test resources
+     */
+    public function setUp()
+    {
+        $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);
+    }
+
+    /**
+     * Generate a session token
+     */
+    public function testGenerateToken()
+    {
+        $token = $this->sessionManager->generateToken();
+
+        $this->assertEquals(1, $this->session['tokens'][$token]);
+        $this->assertEquals(40, strlen($token));
+    }
+
+    /**
+     * Check a session token
+     */
+    public function testCheckToken()
+    {
+        $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b';
+        $session = [
+            'tokens' => [
+                $token => 1,
+            ],
+        ];
+        $sessionManager = new SessionManager($session, $this->conf);
+
+        // check and destroy the token
+        $this->assertTrue($sessionManager->checkToken($token));
+        $this->assertFalse(isset($session['tokens'][$token]));
+
+        // ensure the token has been destroyed
+        $this->assertFalse($sessionManager->checkToken($token));
+    }
+
+    /**
+     * Generate and check a session token
+     */
+    public function testGenerateAndCheckToken()
+    {
+        $token = $this->sessionManager->generateToken();
+
+        // ensure a token has been generated
+        $this->assertEquals(1, $this->session['tokens'][$token]);
+        $this->assertEquals(40, strlen($token));
+
+        // check and destroy the token
+        $this->assertTrue($this->sessionManager->checkToken($token));
+        $this->assertFalse(isset($this->session['tokens'][$token]));
+
+        // ensure the token has been destroyed
+        $this->assertFalse($this->sessionManager->checkToken($token));
+    }
+
+    /**
+     * Check an invalid session token
+     */
+    public function testCheckInvalidToken()
+    {
+        $this->assertFalse($this->sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'));
+    }
+
+    /**
+     * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES!
+     *
+     * This tests extensively covers all hash algorithms / bit representations
+     */
+    public function testIsAnyHashSessionIdValid()
+    {
+        foreach (self::$sidHashes as $algo => $bpcs) {
+            foreach ($bpcs as $bpc => $hash) {
+                $this->assertTrue(SessionManager::checkId($hash));
+            }
+        }
+    }
+
+    /**
+     * Test checkId with a valid ID - SHA-1 hashes
+     */
+    public function testIsSha1SessionIdValid()
+    {
+        $this->assertTrue(SessionManager::checkId(sha1('shaarli')));
+    }
+
+    /**
+     * Test checkId with a valid ID - SHA-256 hashes
+     */
+    public function testIsSha256SessionIdValid()
+    {
+        $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli')));
+    }
+
+    /**
+     * Test checkId with a valid ID - SHA-512 hashes
+     */
+    public function testIsSha512SessionIdValid()
+    {
+        $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli')));
+    }
+
+    /**
+     * Test checkId with invalid IDs.
+     */
+    public function testIsSessionIdInvalid()
+    {
+        $this->assertFalse(SessionManager::checkId(''));
+        $this->assertFalse(SessionManager::checkId([]));
+        $this->assertFalse(
+            SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
+        );
+    }
+
+    /**
+     * Store login information after a successful login
+     */
+    public function testStoreLoginInfo()
+    {
+        $this->sessionManager->storeLoginInfo('ip_id');
+
+        $this->assertGreaterThan(time(), $this->session['expires_on']);
+        $this->assertEquals('ip_id', $this->session['ip']);
+        $this->assertEquals('johndoe', $this->session['username']);
+    }
+
+    /**
+     * Extend a server-side session by SessionManager::$SHORT_TIMEOUT
+     */
+    public function testExtendSession()
+    {
+        $this->sessionManager->extendSession();
+
+        $this->assertGreaterThan(time(), $this->session['expires_on']);
+        $this->assertLessThanOrEqual(
+            time() + SessionManager::$SHORT_TIMEOUT,
+            $this->session['expires_on']
+        );
+    }
+
+    /**
+     * Extend a server-side session by SessionManager::$LONG_TIMEOUT
+     */
+    public function testExtendSessionStaySignedIn()
+    {
+        $this->sessionManager->setStaySignedIn(true);
+        $this->sessionManager->extendSession();
+
+        $this->assertGreaterThan(time(), $this->session['expires_on']);
+        $this->assertGreaterThan(
+            time() + SessionManager::$LONG_TIMEOUT - 10,
+            $this->session['expires_on']
+        );
+        $this->assertLessThanOrEqual(
+            time() + SessionManager::$LONG_TIMEOUT,
+            $this->session['expires_on']
+        );
+    }
+
+    /**
+     * Unset session variables after logging out
+     */
+    public function testLogout()
+    {
+        $this->session = [
+            'ip' => 'ip_id',
+            'expires_on' => time() + 1000,
+            'username' => 'johndoe',
+            'visibility' => 'public',
+            'untaggedonly' => false,
+        ];
+        $this->sessionManager->logout();
+
+        $this->assertFalse(isset($this->session['ip']));
+        $this->assertFalse(isset($this->session['expires_on']));
+        $this->assertFalse(isset($this->session['username']));
+        $this->assertFalse(isset($this->session['visibility']));
+        $this->assertFalse(isset($this->session['untaggedonly']));
+    }
+
+    /**
+     * The session is active and expiration time has been reached
+     */
+    public function testHasExpiredTimeElapsed()
+    {
+        $this->session['expires_on'] = time() - 10;
+
+        $this->assertTrue($this->sessionManager->hasSessionExpired());
+    }
+
+    /**
+     * The session is active and expiration time has not been reached
+     */
+    public function testHasNotExpired()
+    {
+        $this->session['expires_on'] = time() + 1000;
+
+        $this->assertFalse($this->sessionManager->hasSessionExpired());
+    }
+
+    /**
+     * Session hijacking protection is disabled, we assume the IP has not changed
+     */
+    public function testHasClientIpChangedNoSessionProtection()
+    {
+        $this->conf->set('security.session_protection_disabled', true);
+
+        $this->assertFalse($this->sessionManager->hasClientIpChanged(''));
+    }
+
+    /**
+     * The client IP identifier has not changed
+     */
+    public function testHasClientIpChangedNope()
+    {
+        $this->session['ip'] = 'ip_id';
+        $this->assertFalse($this->sessionManager->hasClientIpChanged('ip_id'));
+    }
+
+    /**
+     * The client IP identifier has changed
+     */
+    public function testHasClientIpChanged()
+    {
+        $this->session['ip'] = 'ip_id_one';
+        $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two'));
+    }
+}
index 85434de7b528861c436c430fff555013397dac95..360b34a981c91d797d29d769dd858bb1c032a36a 100644 (file)
@@ -42,4 +42,16 @@ class FakeConfigManager
         }
         return $key;
     }
+
+    /**
+     * Check if a setting exists
+     *
+     * @param string $setting Asked setting, keys separated with dots
+     *
+     * @return bool true if the setting exists, false otherwise
+     */
+    public function exists($setting)
+    {
+        return array_key_exists($setting, $this->values);
+    }
 }
index d546be0a576c0494461c36a253719113bef55edb..322cddd5341ec417cbfc9597bf199a629b7d6f9b 100644 (file)
               <div class="linklist-item-thumbnail">{$thumb}</div>
             {/if}
 
-            {if="isLoggedIn()"}
+            {if="$is_logged_in"}
               <div class="linklist-item-editbuttons">
                 {if="$value.private"}
                   <span class="label label-private">{$strPrivate}</span>
 
             <div class="linklist-item-infos-date-url-block pure-g">
               <div class="linklist-item-infos-dateblock pure-u-lg-7-12 pure-u-1">
-                {if="isLoggedIn()"}
+                {if="$is_logged_in"}
                   <div class="linklist-item-infos-controls-group pure-u-0 pure-u-lg-visible">
                     <span class="linklist-item-infos-controls-item ctrl-checkbox">
                       <input type="checkbox" class="delete-checkbox" value="{$value.id}">
                   </div>
                 {/if}
                 <a href="?{$value.shorturl}" title="{$strPermalink}">
-                  {if="!$hide_timestamps || isLoggedIn()"}
+                  {if="!$hide_timestamps || $is_logged_in"}
                     {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
                     <span class="linkdate" title="{$updated}">
                       <i class="fa fa-clock-o"></i>
                     {if="$link_plugin_counter - 1 != $counter"}&middot;{/if}
                   {/loop}
                 {/if}
-                {if="isLoggedIn()"}
+                {if="$is_logged_in"}
                   &middot;
                   <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}"
                      title="{$strDelete}" class="delete-link confirm-delete">
index 72bdd931a652eb6b8ca6d7fe243fde136b4e9614..5309e348a8c38ba236be2ee13be916279c84fa67 100644 (file)
@@ -1,11 +1,11 @@
 <div class="linklist-paging">
   <div class="paging pure-g">
     <div class="linklist-filters pure-u-1-3">
-      {if="isLoggedIn() or !empty($action_plugin)"}
+      {if="$is_logged_in or !empty($action_plugin)"}
         <span class="linklist-filters-text pure-u-0 pure-u-lg-visible">
           {'Filters'|t}
         </span>
-        {if="isLoggedIn()"}
+        {if="$is_logged_in"}
         <a href="?visibility=private" title="{'Only display private links'|t}"
            class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}"
         ><i class="fa fa-user-secret"></i></a>
index 34193743ab5a45de4e328e8e20e5e37b5251838c..5af39be7d5d7ec2c6fd5a448b47953d0a0752156 100644 (file)
@@ -4,7 +4,7 @@
   <div class="pure-u-2-24"></div>
   <div id="footer" class="pure-u-20-24 footer-container">
     <strong><a href="https://github.com/shaarli/Shaarli">Shaarli</a></strong>
-    {if="isLoggedIn()===true"}
+    {if="$is_logged_in===true"}
       {$version}
     {/if}
     &middot;
index 18aa77c82ea91d1706ffabb97a645d2698533cd0..82568d635ca0db797b2daf609b7d1852d1142bf0 100644 (file)
@@ -17,7 +17,7 @@
             {$shaarlititle}
           </a>
         </li>
-        {if="isLoggedIn() || $openshaarli"}
+        {if="$is_logged_in || $openshaarli"}
           <li class="pure-menu-item">
             <a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare">
               <i class="fa fa-plus" ></i> {'Shaare'|t}
@@ -50,7 +50,7 @@
         <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss">
             <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a>
         </li>
-        {if="isLoggedIn()"}
+        {if="$is_logged_in"}
           <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout">
             <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a>
           </li>
@@ -74,7 +74,7 @@
               <i class="fa fa-rss"></i>
             </a>
           </li>
-          {if="!isLoggedIn()"}
+          {if="!$is_logged_in"}
             <li class="pure-menu-item" id="shaarli-menu-desktop-login">
               <a href="?do=login" class="pure-menu-link"
                  data-open-id="header-login-form"
       </div>
     </div>
   </div>
-  {if="!isLoggedIn()"}
+  {if="!$is_logged_in"}
     <form method="post" name="loginform">
       <div class="subheader-form header-login-form" id="header-login-form">
         <input type="text" name="login" placeholder="{'Username'|t}" tabindex="3">
   </div>
 {/if}
 
-{if="!empty($plugin_errors) && isLoggedIn()"}
+{if="!empty($plugin_errors) && $is_logged_in"}
   <div class="pure-g new-version-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">
index 772d6ad3ee8598bcec28e9a2c634e68c6643f09c..bcddcd56337a745c5a06d87a0476572ada9201aa 100644 (file)
@@ -49,7 +49,7 @@
       {loop="tags"}
         <div class="tag-list-item pure-g" data-tag="{$key}">
           <div class="pure-u-1">
-            {if="isLoggedIn()===true"}
+            {if="$is_logged_in===true"}
               <a href="#" class="delete-tag"><i class="fa fa-trash"></i></a>&nbsp;&nbsp;
               <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag">
                 <i class="fa fa-pencil-square-o {$key}"></i>
@@ -63,7 +63,7 @@
               {$value}
             {/loop}
           </div>
-          {if="isLoggedIn()===true"}
+          {if="$is_logged_in===true"}
             <div class="rename-tag-form pure-u-1">
               <input type="text" name="{$key}" value="{$key}" class="rename-tag-input" />
               <a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a>
@@ -81,7 +81,7 @@
   </div>
 </div>
 
-{if="isLoggedIn()===true"}
+{if="$is_logged_in===true"}
   <input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}"
 {/if}
 
index 42db16a7f5315fb2275da5e91be99c9ab0bfc348..ede359106d30e107422c884b59bf494ae316092c 100644 (file)
@@ -53,7 +53,7 @@
                                 <img src="img/squiggle.png" width="25" height="26" title="permalink" alt="permalink">
                             </a>
                         </div>
-                        {if="!$hide_timestamps || isLoggedIn()"}
+                        {if="!$hide_timestamps || $is_logged_in"}
                             <div class="dailyEntryLinkdate">
                                 <a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a>
                             </div>
index e7137246d95ff9d6ef5ac88eaa74279b3194df3f..1ca51be3b962f4ac1d3965fa94b5ba231623924f 100644 (file)
@@ -82,7 +82,7 @@
             <a id="{$value.shorturl}"></a>
             <div class="thumbnail">{$value.url|thumbnail}</div>
             <div class="linkcontainer">
-                {if="isLoggedIn()"}
+                {if="$is_logged_in"}
                     <div class="linkeditbuttons">
                         <form method="GET" class="buttoneditform">
                             <input type="hidden" name="edit_link" value="{$value.id}">
                 </span>
                 <br>
                 {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if}
-                {if="!$hide_timestamps || isLoggedIn()"}
+                {if="!$hide_timestamps || $is_logged_in"}
                     {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
                     <span class="linkdate" title="Permalink">
                         <a href="?{$value.shorturl}">
index e3b88ee6498881684a5f17806ab24c95a259b73b..35149a6bfddda4c5b3633da3ca5ada947f855b9e 100644 (file)
@@ -1,5 +1,5 @@
 <div class="paging">
-{if="isLoggedIn()"}
+{if="$is_logged_in"}
     <div class="paging_privatelinks">
       <a href="?visibility=private">
                {if="$visibility=='private'"}
index 1485e1ce53f237308793dcf036c4fdeb3b55e59f..f409721e7271ecdd5be90181623276332513e80d 100644 (file)
@@ -25,7 +25,7 @@
 
 <script src="js/shaarli.min.js"></script>
 
-{if="isLoggedIn()"}
+{if="$is_logged_in"}
 <script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
 {/if}
 
index 8a58844ee5556472db98089393eeef948f82c760..40c53e5bdf9bec9c55f8e3799a8152dff7f0bdbd 100644 (file)
@@ -17,7 +17,7 @@
     {ignore} When called as a popup from bookmarklet, do not display menu. {/ignore}
 {else}
 <li><a href="{$titleLink}" class="nomobile">Home</a></li>
-    {if="isLoggedIn()"}
+    {if="$is_logged_in"}
     <li><a href="?do=logout">Logout</a></li>
     <li><a href="?do=tools">Tools</a></li>
     <li><a href="?do=addlink">Add link</a></li>
@@ -46,7 +46,7 @@
   </ul>
 </div>
 
-{if="!empty($plugin_errors) && isLoggedIn()"}
+{if="!empty($plugin_errors) && $is_logged_in"}
     <ul class="errors">
         {loop="$plugin_errors"}
             <li>{$value}</li>