diff options
author | VirtualTam <virtualtam+github@flibidi.net> | 2018-06-03 18:26:32 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-06-03 18:26:32 +0200 |
commit | d9cd27322a97e6ed3a8b11a380ef0080e3baf79c (patch) | |
tree | c4299a352b3f4c518f79eb7208f667f68f8e9388 | |
parent | 8f816d8ddfe9219e15580cef6e5c9037d1d4fd28 (diff) | |
parent | 8edd7f15886620b07064aa889aea05c5acbc0e58 (diff) | |
download | Shaarli-d9cd27322a97e6ed3a8b11a380ef0080e3baf79c.tar.gz Shaarli-d9cd27322a97e6ed3a8b11a380ef0080e3baf79c.tar.zst Shaarli-d9cd27322a97e6ed3a8b11a380ef0080e3baf79c.zip |
Merge pull request #1086 from virtualtam/refactor/login
Refactor user login and session management
-rw-r--r-- | application/HttpUtils.php | 33 | ||||
-rw-r--r-- | application/LoginManager.php | 134 | ||||
-rw-r--r-- | application/PageBuilder.php | 9 | ||||
-rw-r--r-- | application/SessionManager.php | 83 | ||||
-rw-r--r-- | application/security/LoginManager.php | 265 | ||||
-rw-r--r-- | application/security/SessionManager.php | 199 | ||||
-rw-r--r-- | composer.json | 3 | ||||
-rw-r--r-- | index.php | 240 | ||||
-rw-r--r-- | tests/HttpUtils/ClientIpIdTest.php | 52 | ||||
-rw-r--r-- | tests/SessionManagerTest.php | 149 | ||||
-rw-r--r-- | tests/security/LoginManagerTest.php (renamed from tests/LoginManagerTest.php) | 183 | ||||
-rw-r--r-- | tests/security/SessionManagerTest.php | 273 | ||||
-rw-r--r-- | tests/utils/FakeConfigManager.php | 12 | ||||
-rw-r--r-- | tpl/default/linklist.html | 8 | ||||
-rw-r--r-- | tpl/default/linklist.paging.html | 4 | ||||
-rw-r--r-- | tpl/default/page.footer.html | 2 | ||||
-rw-r--r-- | tpl/default/page.header.html | 10 | ||||
-rw-r--r-- | tpl/default/tag.list.html | 6 | ||||
-rw-r--r-- | tpl/vintage/daily.html | 2 | ||||
-rw-r--r-- | tpl/vintage/linklist.html | 4 | ||||
-rw-r--r-- | tpl/vintage/linklist.paging.html | 2 | ||||
-rw-r--r-- | tpl/vintage/page.footer.html | 2 | ||||
-rw-r--r-- | tpl/vintage/page.header.html | 4 |
23 files changed, 1116 insertions, 563 deletions
diff --git a/application/HttpUtils.php b/application/HttpUtils.php index 83a4c5e2..e9282506 100644 --- a/application/HttpUtils.php +++ b/application/HttpUtils.php | |||
@@ -1,7 +1,7 @@ | |||
1 | <?php | 1 | <?php |
2 | /** | 2 | /** |
3 | * GET an HTTP URL to retrieve its content | 3 | * GET an HTTP URL to retrieve its content |
4 | * Uses the cURL library or a fallback method | 4 | * Uses the cURL library or a fallback method |
5 | * | 5 | * |
6 | * @param string $url URL to get (http://...) | 6 | * @param string $url URL to get (http://...) |
7 | * @param int $timeout network timeout (in seconds) | 7 | * @param int $timeout network timeout (in seconds) |
@@ -415,6 +415,37 @@ function getIpAddressFromProxy($server, $trustedIps) | |||
415 | return array_pop($ips); | 415 | return array_pop($ips); |
416 | } | 416 | } |
417 | 417 | ||
418 | |||
419 | /** | ||
420 | * Return an identifier based on the advertised client IP address(es) | ||
421 | * | ||
422 | * This aims at preventing session hijacking from users behind the same proxy | ||
423 | * by relying on HTTP headers. | ||
424 | * | ||
425 | * See: | ||
426 | * - https://secure.php.net/manual/en/reserved.variables.server.php | ||
427 | * - https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php | ||
428 | * - https://stackoverflow.com/questions/12233406/preventing-session-hijacking | ||
429 | * - https://stackoverflow.com/questions/21354859/trusting-x-forwarded-for-to-identify-a-visitor | ||
430 | * | ||
431 | * @param array $server The $_SERVER array | ||
432 | * | ||
433 | * @return string An identifier based on client IP address information | ||
434 | */ | ||
435 | function client_ip_id($server) | ||
436 | { | ||
437 | $ip = $server['REMOTE_ADDR']; | ||
438 | |||
439 | if (isset($server['HTTP_X_FORWARDED_FOR'])) { | ||
440 | $ip = $ip . '_' . $server['HTTP_X_FORWARDED_FOR']; | ||
441 | } | ||
442 | if (isset($server['HTTP_CLIENT_IP'])) { | ||
443 | $ip = $ip . '_' . $server['HTTP_CLIENT_IP']; | ||
444 | } | ||
445 | return $ip; | ||
446 | } | ||
447 | |||
448 | |||
418 | /** | 449 | /** |
419 | * Returns true if Shaarli's currently browsed in HTTPS. | 450 | * Returns true if Shaarli's currently browsed in HTTPS. |
420 | * Supports reverse proxies (if the headers are correctly set). | 451 | * Supports reverse proxies (if the headers are correctly set). |
diff --git a/application/LoginManager.php b/application/LoginManager.php deleted file mode 100644 index 397bc6e3..00000000 --- a/application/LoginManager.php +++ /dev/null | |||
@@ -1,134 +0,0 @@ | |||
1 | <?php | ||
2 | namespace Shaarli; | ||
3 | |||
4 | /** | ||
5 | * User login management | ||
6 | */ | ||
7 | class LoginManager | ||
8 | { | ||
9 | protected $globals = []; | ||
10 | protected $configManager = null; | ||
11 | protected $banFile = ''; | ||
12 | |||
13 | /** | ||
14 | * Constructor | ||
15 | * | ||
16 | * @param array $globals The $GLOBALS array (reference) | ||
17 | * @param ConfigManager $configManager Configuration Manager instance. | ||
18 | */ | ||
19 | public function __construct(& $globals, $configManager) | ||
20 | { | ||
21 | $this->globals = &$globals; | ||
22 | $this->configManager = $configManager; | ||
23 | $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php'); | ||
24 | $this->readBanFile(); | ||
25 | } | ||
26 | |||
27 | /** | ||
28 | * Read a file containing banned IPs | ||
29 | */ | ||
30 | protected function readBanFile() | ||
31 | { | ||
32 | if (! file_exists($this->banFile)) { | ||
33 | return; | ||
34 | } | ||
35 | include $this->banFile; | ||
36 | } | ||
37 | |||
38 | /** | ||
39 | * Write the banned IPs to a file | ||
40 | */ | ||
41 | protected function writeBanFile() | ||
42 | { | ||
43 | if (! array_key_exists('IPBANS', $this->globals)) { | ||
44 | return; | ||
45 | } | ||
46 | file_put_contents( | ||
47 | $this->banFile, | ||
48 | "<?php\n\$GLOBALS['IPBANS']=" . var_export($this->globals['IPBANS'], true) . ";\n?>" | ||
49 | ); | ||
50 | } | ||
51 | |||
52 | /** | ||
53 | * Handle a failed login and ban the IP after too many failed attempts | ||
54 | * | ||
55 | * @param array $server The $_SERVER array | ||
56 | */ | ||
57 | public function handleFailedLogin($server) | ||
58 | { | ||
59 | $ip = $server['REMOTE_ADDR']; | ||
60 | $trusted = $this->configManager->get('security.trusted_proxies', []); | ||
61 | |||
62 | if (in_array($ip, $trusted)) { | ||
63 | $ip = getIpAddressFromProxy($server, $trusted); | ||
64 | if (! $ip) { | ||
65 | // the IP is behind a trusted forward proxy, but is not forwarded | ||
66 | // in the HTTP headers, so we do nothing | ||
67 | return; | ||
68 | } | ||
69 | } | ||
70 | |||
71 | // increment the fail count for this IP | ||
72 | if (isset($this->globals['IPBANS']['FAILURES'][$ip])) { | ||
73 | $this->globals['IPBANS']['FAILURES'][$ip]++; | ||
74 | } else { | ||
75 | $this->globals['IPBANS']['FAILURES'][$ip] = 1; | ||
76 | } | ||
77 | |||
78 | if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) { | ||
79 | $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800); | ||
80 | logm( | ||
81 | $this->configManager->get('resource.log'), | ||
82 | $server['REMOTE_ADDR'], | ||
83 | 'IP address banned from login' | ||
84 | ); | ||
85 | } | ||
86 | $this->writeBanFile(); | ||
87 | } | ||
88 | |||
89 | /** | ||
90 | * Handle a successful login | ||
91 | * | ||
92 | * @param array $server The $_SERVER array | ||
93 | */ | ||
94 | public function handleSuccessfulLogin($server) | ||
95 | { | ||
96 | $ip = $server['REMOTE_ADDR']; | ||
97 | // FIXME unban when behind a trusted proxy? | ||
98 | |||
99 | unset($this->globals['IPBANS']['FAILURES'][$ip]); | ||
100 | unset($this->globals['IPBANS']['BANS'][$ip]); | ||
101 | |||
102 | $this->writeBanFile(); | ||
103 | } | ||
104 | |||
105 | /** | ||
106 | * Check if the user can login from this IP | ||
107 | * | ||
108 | * @param array $server The $_SERVER array | ||
109 | * | ||
110 | * @return bool true if the user is allowed to login | ||
111 | */ | ||
112 | public function canLogin($server) | ||
113 | { | ||
114 | $ip = $server['REMOTE_ADDR']; | ||
115 | |||
116 | if (! isset($this->globals['IPBANS']['BANS'][$ip])) { | ||
117 | // the user is not banned | ||
118 | return true; | ||
119 | } | ||
120 | |||
121 | if ($this->globals['IPBANS']['BANS'][$ip] > time()) { | ||
122 | // the user is still banned | ||
123 | return false; | ||
124 | } | ||
125 | |||
126 | // the ban has expired, the user can attempt to log in again | ||
127 | logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.'); | ||
128 | unset($this->globals['IPBANS']['FAILURES'][$ip]); | ||
129 | unset($this->globals['IPBANS']['BANS'][$ip]); | ||
130 | |||
131 | $this->writeBanFile(); | ||
132 | return true; | ||
133 | } | ||
134 | } | ||
diff --git a/application/PageBuilder.php b/application/PageBuilder.php index 3233d6b6..a4483870 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php | |||
@@ -25,6 +25,9 @@ class PageBuilder | |||
25 | * @var LinkDB $linkDB instance. | 25 | * @var LinkDB $linkDB instance. |
26 | */ | 26 | */ |
27 | protected $linkDB; | 27 | protected $linkDB; |
28 | |||
29 | /** @var bool $isLoggedIn Whether the user is logged in **/ | ||
30 | protected $isLoggedIn = false; | ||
28 | 31 | ||
29 | /** | 32 | /** |
30 | * PageBuilder constructor. | 33 | * PageBuilder constructor. |
@@ -34,12 +37,13 @@ class PageBuilder | |||
34 | * @param LinkDB $linkDB instance. | 37 | * @param LinkDB $linkDB instance. |
35 | * @param string $token Session token | 38 | * @param string $token Session token |
36 | */ | 39 | */ |
37 | public function __construct(&$conf, $linkDB = null, $token = null) | 40 | public function __construct(&$conf, $linkDB = null, $token = null, $isLoggedIn = false) |
38 | { | 41 | { |
39 | $this->tpl = false; | 42 | $this->tpl = false; |
40 | $this->conf = $conf; | 43 | $this->conf = $conf; |
41 | $this->linkDB = $linkDB; | 44 | $this->linkDB = $linkDB; |
42 | $this->token = $token; | 45 | $this->token = $token; |
46 | $this->isLoggedIn = $isLoggedIn; | ||
43 | } | 47 | } |
44 | 48 | ||
45 | /** | 49 | /** |
@@ -55,7 +59,7 @@ class PageBuilder | |||
55 | $this->conf->get('resource.update_check'), | 59 | $this->conf->get('resource.update_check'), |
56 | $this->conf->get('updates.check_updates_interval'), | 60 | $this->conf->get('updates.check_updates_interval'), |
57 | $this->conf->get('updates.check_updates'), | 61 | $this->conf->get('updates.check_updates'), |
58 | isLoggedIn(), | 62 | $this->isLoggedIn, |
59 | $this->conf->get('updates.check_updates_branch') | 63 | $this->conf->get('updates.check_updates_branch') |
60 | ); | 64 | ); |
61 | $this->tpl->assign('newVersion', escape($version)); | 65 | $this->tpl->assign('newVersion', escape($version)); |
@@ -67,6 +71,7 @@ class PageBuilder | |||
67 | $this->tpl->assign('versionError', escape($exc->getMessage())); | 71 | $this->tpl->assign('versionError', escape($exc->getMessage())); |
68 | } | 72 | } |
69 | 73 | ||
74 | $this->tpl->assign('is_logged_in', $this->isLoggedIn); | ||
70 | $this->tpl->assign('feedurl', escape(index_url($_SERVER))); | 75 | $this->tpl->assign('feedurl', escape(index_url($_SERVER))); |
71 | $searchcrits = ''; // Search criteria | 76 | $searchcrits = ''; // Search criteria |
72 | if (!empty($_GET['searchtags'])) { | 77 | if (!empty($_GET['searchtags'])) { |
diff --git a/application/SessionManager.php b/application/SessionManager.php deleted file mode 100644 index 71f0b38d..00000000 --- a/application/SessionManager.php +++ /dev/null | |||
@@ -1,83 +0,0 @@ | |||
1 | <?php | ||
2 | namespace Shaarli; | ||
3 | |||
4 | /** | ||
5 | * Manages the server-side session | ||
6 | */ | ||
7 | class SessionManager | ||
8 | { | ||
9 | protected $session = []; | ||
10 | |||
11 | /** | ||
12 | * Constructor | ||
13 | * | ||
14 | * @param array $session The $_SESSION array (reference) | ||
15 | * @param ConfigManager $conf ConfigManager instance | ||
16 | */ | ||
17 | public function __construct(& $session, $conf) | ||
18 | { | ||
19 | $this->session = &$session; | ||
20 | $this->conf = $conf; | ||
21 | } | ||
22 | |||
23 | /** | ||
24 | * Generates a session token | ||
25 | * | ||
26 | * @return string token | ||
27 | */ | ||
28 | public function generateToken() | ||
29 | { | ||
30 | $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt')); | ||
31 | $this->session['tokens'][$token] = 1; | ||
32 | return $token; | ||
33 | } | ||
34 | |||
35 | /** | ||
36 | * Checks the validity of a session token, and destroys it afterwards | ||
37 | * | ||
38 | * @param string $token The token to check | ||
39 | * | ||
40 | * @return bool true if the token is valid, else false | ||
41 | */ | ||
42 | public function checkToken($token) | ||
43 | { | ||
44 | if (! isset($this->session['tokens'][$token])) { | ||
45 | // the token is wrong, or has already been used | ||
46 | return false; | ||
47 | } | ||
48 | |||
49 | // destroy the token to prevent future use | ||
50 | unset($this->session['tokens'][$token]); | ||
51 | return true; | ||
52 | } | ||
53 | |||
54 | /** | ||
55 | * Validate session ID to prevent Full Path Disclosure. | ||
56 | * | ||
57 | * See #298. | ||
58 | * The session ID's format depends on the hash algorithm set in PHP settings | ||
59 | * | ||
60 | * @param string $sessionId Session ID | ||
61 | * | ||
62 | * @return true if valid, false otherwise. | ||
63 | * | ||
64 | * @see http://php.net/manual/en/function.hash-algos.php | ||
65 | * @see http://php.net/manual/en/session.configuration.php | ||
66 | */ | ||
67 | public static function checkId($sessionId) | ||
68 | { | ||
69 | if (empty($sessionId)) { | ||
70 | return false; | ||
71 | } | ||
72 | |||
73 | if (!$sessionId) { | ||
74 | return false; | ||
75 | } | ||
76 | |||
77 | if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) { | ||
78 | return false; | ||
79 | } | ||
80 | |||
81 | return true; | ||
82 | } | ||
83 | } | ||
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php new file mode 100644 index 00000000..d6784d6d --- /dev/null +++ b/application/security/LoginManager.php | |||
@@ -0,0 +1,265 @@ | |||
1 | <?php | ||
2 | namespace Shaarli\Security; | ||
3 | |||
4 | use Shaarli\Config\ConfigManager; | ||
5 | |||
6 | /** | ||
7 | * User login management | ||
8 | */ | ||
9 | class LoginManager | ||
10 | { | ||
11 | /** @var string Name of the cookie set after logging in **/ | ||
12 | public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn'; | ||
13 | |||
14 | /** @var array A reference to the $_GLOBALS array */ | ||
15 | protected $globals = []; | ||
16 | |||
17 | /** @var ConfigManager Configuration Manager instance **/ | ||
18 | protected $configManager = null; | ||
19 | |||
20 | /** @var SessionManager Session Manager instance **/ | ||
21 | protected $sessionManager = null; | ||
22 | |||
23 | /** @var string Path to the file containing IP bans */ | ||
24 | protected $banFile = ''; | ||
25 | |||
26 | /** @var bool Whether the user is logged in **/ | ||
27 | protected $isLoggedIn = false; | ||
28 | |||
29 | /** @var bool Whether the Shaarli instance is open to public edition **/ | ||
30 | protected $openShaarli = false; | ||
31 | |||
32 | /** @var string User sign-in token depending on remote IP and credentials */ | ||
33 | protected $staySignedInToken = ''; | ||
34 | |||
35 | /** | ||
36 | * Constructor | ||
37 | * | ||
38 | * @param array $globals The $GLOBALS array (reference) | ||
39 | * @param ConfigManager $configManager Configuration Manager instance | ||
40 | * @param SessionManager $sessionManager SessionManager instance | ||
41 | */ | ||
42 | public function __construct(& $globals, $configManager, $sessionManager) | ||
43 | { | ||
44 | $this->globals = &$globals; | ||
45 | $this->configManager = $configManager; | ||
46 | $this->sessionManager = $sessionManager; | ||
47 | $this->banFile = $this->configManager->get('resource.ban_file', 'data/ipbans.php'); | ||
48 | $this->readBanFile(); | ||
49 | if ($this->configManager->get('security.open_shaarli') === true) { | ||
50 | $this->openShaarli = true; | ||
51 | } | ||
52 | } | ||
53 | |||
54 | /** | ||
55 | * Generate a token depending on deployment salt, user password and client IP | ||
56 | * | ||
57 | * @param string $clientIpAddress The remote client IP address | ||
58 | */ | ||
59 | public function generateStaySignedInToken($clientIpAddress) | ||
60 | { | ||
61 | $this->staySignedInToken = sha1( | ||
62 | $this->configManager->get('credentials.hash') | ||
63 | . $clientIpAddress | ||
64 | . $this->configManager->get('credentials.salt') | ||
65 | ); | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * Return the user's client stay-signed-in token | ||
70 | * | ||
71 | * @return string User's client stay-signed-in token | ||
72 | */ | ||
73 | public function getStaySignedInToken() | ||
74 | { | ||
75 | return $this->staySignedInToken; | ||
76 | } | ||
77 | |||
78 | /** | ||
79 | * Check user session state and validity (expiration) | ||
80 | * | ||
81 | * @param array $cookie The $_COOKIE array | ||
82 | * @param string $clientIpId Client IP address identifier | ||
83 | */ | ||
84 | public function checkLoginState($cookie, $clientIpId) | ||
85 | { | ||
86 | if (! $this->configManager->exists('credentials.login')) { | ||
87 | // Shaarli is not configured yet | ||
88 | $this->isLoggedIn = false; | ||
89 | return; | ||
90 | } | ||
91 | |||
92 | if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) | ||
93 | && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken | ||
94 | ) { | ||
95 | // The user client has a valid stay-signed-in cookie | ||
96 | // Session information is updated with the current client information | ||
97 | $this->sessionManager->storeLoginInfo($clientIpId); | ||
98 | |||
99 | } elseif ($this->sessionManager->hasSessionExpired() | ||
100 | || $this->sessionManager->hasClientIpChanged($clientIpId) | ||
101 | ) { | ||
102 | $this->sessionManager->logout(); | ||
103 | $this->isLoggedIn = false; | ||
104 | return; | ||
105 | } | ||
106 | |||
107 | $this->isLoggedIn = true; | ||
108 | $this->sessionManager->extendSession(); | ||
109 | } | ||
110 | |||
111 | /** | ||
112 | * Return whether the user is currently logged in | ||
113 | * | ||
114 | * @return true when the user is logged in, false otherwise | ||
115 | */ | ||
116 | public function isLoggedIn() | ||
117 | { | ||
118 | if ($this->openShaarli) { | ||
119 | return true; | ||
120 | } | ||
121 | return $this->isLoggedIn; | ||
122 | } | ||
123 | |||
124 | /** | ||
125 | * Check user credentials are valid | ||
126 | * | ||
127 | * @param string $remoteIp Remote client IP address | ||
128 | * @param string $clientIpId Client IP address identifier | ||
129 | * @param string $login Username | ||
130 | * @param string $password Password | ||
131 | * | ||
132 | * @return bool true if the provided credentials are valid, false otherwise | ||
133 | */ | ||
134 | public function checkCredentials($remoteIp, $clientIpId, $login, $password) | ||
135 | { | ||
136 | $hash = sha1($password . $login . $this->configManager->get('credentials.salt')); | ||
137 | |||
138 | if ($login != $this->configManager->get('credentials.login') | ||
139 | || $hash != $this->configManager->get('credentials.hash') | ||
140 | ) { | ||
141 | logm( | ||
142 | $this->configManager->get('resource.log'), | ||
143 | $remoteIp, | ||
144 | 'Login failed for user ' . $login | ||
145 | ); | ||
146 | return false; | ||
147 | } | ||
148 | |||
149 | $this->sessionManager->storeLoginInfo($clientIpId); | ||
150 | logm( | ||
151 | $this->configManager->get('resource.log'), | ||
152 | $remoteIp, | ||
153 | 'Login successful' | ||
154 | ); | ||
155 | return true; | ||
156 | } | ||
157 | |||
158 | /** | ||
159 | * Read a file containing banned IPs | ||
160 | */ | ||
161 | protected function readBanFile() | ||
162 | { | ||
163 | if (! file_exists($this->banFile)) { | ||
164 | return; | ||
165 | } | ||
166 | include $this->banFile; | ||
167 | } | ||
168 | |||
169 | /** | ||
170 | * Write the banned IPs to a file | ||
171 | */ | ||
172 | protected function writeBanFile() | ||
173 | { | ||
174 | if (! array_key_exists('IPBANS', $this->globals)) { | ||
175 | return; | ||
176 | } | ||
177 | file_put_contents( | ||
178 | $this->banFile, | ||
179 | "<?php\n\$GLOBALS['IPBANS']=" . var_export($this->globals['IPBANS'], true) . ";\n?>" | ||
180 | ); | ||
181 | } | ||
182 | |||
183 | /** | ||
184 | * Handle a failed login and ban the IP after too many failed attempts | ||
185 | * | ||
186 | * @param array $server The $_SERVER array | ||
187 | */ | ||
188 | public function handleFailedLogin($server) | ||
189 | { | ||
190 | $ip = $server['REMOTE_ADDR']; | ||
191 | $trusted = $this->configManager->get('security.trusted_proxies', []); | ||
192 | |||
193 | if (in_array($ip, $trusted)) { | ||
194 | $ip = getIpAddressFromProxy($server, $trusted); | ||
195 | if (! $ip) { | ||
196 | // the IP is behind a trusted forward proxy, but is not forwarded | ||
197 | // in the HTTP headers, so we do nothing | ||
198 | return; | ||
199 | } | ||
200 | } | ||
201 | |||
202 | // increment the fail count for this IP | ||
203 | if (isset($this->globals['IPBANS']['FAILURES'][$ip])) { | ||
204 | $this->globals['IPBANS']['FAILURES'][$ip]++; | ||
205 | } else { | ||
206 | $this->globals['IPBANS']['FAILURES'][$ip] = 1; | ||
207 | } | ||
208 | |||
209 | if ($this->globals['IPBANS']['FAILURES'][$ip] >= $this->configManager->get('security.ban_after')) { | ||
210 | $this->globals['IPBANS']['BANS'][$ip] = time() + $this->configManager->get('security.ban_duration', 1800); | ||
211 | logm( | ||
212 | $this->configManager->get('resource.log'), | ||
213 | $server['REMOTE_ADDR'], | ||
214 | 'IP address banned from login' | ||
215 | ); | ||
216 | } | ||
217 | $this->writeBanFile(); | ||
218 | } | ||
219 | |||
220 | /** | ||
221 | * Handle a successful login | ||
222 | * | ||
223 | * @param array $server The $_SERVER array | ||
224 | */ | ||
225 | public function handleSuccessfulLogin($server) | ||
226 | { | ||
227 | $ip = $server['REMOTE_ADDR']; | ||
228 | // FIXME unban when behind a trusted proxy? | ||
229 | |||
230 | unset($this->globals['IPBANS']['FAILURES'][$ip]); | ||
231 | unset($this->globals['IPBANS']['BANS'][$ip]); | ||
232 | |||
233 | $this->writeBanFile(); | ||
234 | } | ||
235 | |||
236 | /** | ||
237 | * Check if the user can login from this IP | ||
238 | * | ||
239 | * @param array $server The $_SERVER array | ||
240 | * | ||
241 | * @return bool true if the user is allowed to login | ||
242 | */ | ||
243 | public function canLogin($server) | ||
244 | { | ||
245 | $ip = $server['REMOTE_ADDR']; | ||
246 | |||
247 | if (! isset($this->globals['IPBANS']['BANS'][$ip])) { | ||
248 | // the user is not banned | ||
249 | return true; | ||
250 | } | ||
251 | |||
252 | if ($this->globals['IPBANS']['BANS'][$ip] > time()) { | ||
253 | // the user is still banned | ||
254 | return false; | ||
255 | } | ||
256 | |||
257 | // the ban has expired, the user can attempt to log in again | ||
258 | logm($this->configManager->get('resource.log'), $server['REMOTE_ADDR'], 'Ban lifted.'); | ||
259 | unset($this->globals['IPBANS']['FAILURES'][$ip]); | ||
260 | unset($this->globals['IPBANS']['BANS'][$ip]); | ||
261 | |||
262 | $this->writeBanFile(); | ||
263 | return true; | ||
264 | } | ||
265 | } | ||
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php new file mode 100644 index 00000000..b8b8ab8d --- /dev/null +++ b/application/security/SessionManager.php | |||
@@ -0,0 +1,199 @@ | |||
1 | <?php | ||
2 | namespace Shaarli\Security; | ||
3 | |||
4 | use Shaarli\Config\ConfigManager; | ||
5 | |||
6 | /** | ||
7 | * Manages the server-side session | ||
8 | */ | ||
9 | class SessionManager | ||
10 | { | ||
11 | /** @var int Session expiration timeout, in seconds */ | ||
12 | public static $SHORT_TIMEOUT = 3600; // 1 hour | ||
13 | |||
14 | /** @var int Session expiration timeout, in seconds */ | ||
15 | public static $LONG_TIMEOUT = 31536000; // 1 year | ||
16 | |||
17 | /** @var array Local reference to the global $_SESSION array */ | ||
18 | protected $session = []; | ||
19 | |||
20 | /** @var ConfigManager Configuration Manager instance **/ | ||
21 | protected $conf = null; | ||
22 | |||
23 | /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ | ||
24 | protected $staySignedIn = false; | ||
25 | |||
26 | /** | ||
27 | * Constructor | ||
28 | * | ||
29 | * @param array $session The $_SESSION array (reference) | ||
30 | * @param ConfigManager $conf ConfigManager instance | ||
31 | */ | ||
32 | public function __construct(& $session, $conf) | ||
33 | { | ||
34 | $this->session = &$session; | ||
35 | $this->conf = $conf; | ||
36 | } | ||
37 | |||
38 | /** | ||
39 | * Define whether the user should stay signed in across browser sessions | ||
40 | * | ||
41 | * @param bool $staySignedIn Keep the user signed in | ||
42 | */ | ||
43 | public function setStaySignedIn($staySignedIn) | ||
44 | { | ||
45 | $this->staySignedIn = $staySignedIn; | ||
46 | } | ||
47 | |||
48 | /** | ||
49 | * Generates a session token | ||
50 | * | ||
51 | * @return string token | ||
52 | */ | ||
53 | public function generateToken() | ||
54 | { | ||
55 | $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt')); | ||
56 | $this->session['tokens'][$token] = 1; | ||
57 | return $token; | ||
58 | } | ||
59 | |||
60 | /** | ||
61 | * Checks the validity of a session token, and destroys it afterwards | ||
62 | * | ||
63 | * @param string $token The token to check | ||
64 | * | ||
65 | * @return bool true if the token is valid, else false | ||
66 | */ | ||
67 | public function checkToken($token) | ||
68 | { | ||
69 | if (! isset($this->session['tokens'][$token])) { | ||
70 | // the token is wrong, or has already been used | ||
71 | return false; | ||
72 | } | ||
73 | |||
74 | // destroy the token to prevent future use | ||
75 | unset($this->session['tokens'][$token]); | ||
76 | return true; | ||
77 | } | ||
78 | |||
79 | /** | ||
80 | * Validate session ID to prevent Full Path Disclosure. | ||
81 | * | ||
82 | * See #298. | ||
83 | * The session ID's format depends on the hash algorithm set in PHP settings | ||
84 | * | ||
85 | * @param string $sessionId Session ID | ||
86 | * | ||
87 | * @return true if valid, false otherwise. | ||
88 | * | ||
89 | * @see http://php.net/manual/en/function.hash-algos.php | ||
90 | * @see http://php.net/manual/en/session.configuration.php | ||
91 | */ | ||
92 | public static function checkId($sessionId) | ||
93 | { | ||
94 | if (empty($sessionId)) { | ||
95 | return false; | ||
96 | } | ||
97 | |||
98 | if (!$sessionId) { | ||
99 | return false; | ||
100 | } | ||
101 | |||
102 | if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) { | ||
103 | return false; | ||
104 | } | ||
105 | |||
106 | return true; | ||
107 | } | ||
108 | |||
109 | /** | ||
110 | * Store user login information after a successful login | ||
111 | * | ||
112 | * @param string $clientIpId Client IP address identifier | ||
113 | */ | ||
114 | public function storeLoginInfo($clientIpId) | ||
115 | { | ||
116 | $this->session['ip'] = $clientIpId; | ||
117 | $this->session['username'] = $this->conf->get('credentials.login'); | ||
118 | $this->extendTimeValidityBy(self::$SHORT_TIMEOUT); | ||
119 | } | ||
120 | |||
121 | /** | ||
122 | * Extend session validity | ||
123 | */ | ||
124 | public function extendSession() | ||
125 | { | ||
126 | if ($this->staySignedIn) { | ||
127 | return $this->extendTimeValidityBy(self::$LONG_TIMEOUT); | ||
128 | } | ||
129 | return $this->extendTimeValidityBy(self::$SHORT_TIMEOUT); | ||
130 | } | ||
131 | |||
132 | /** | ||
133 | * Extend expiration time | ||
134 | * | ||
135 | * @param int $duration Expiration time extension (seconds) | ||
136 | * | ||
137 | * @return int New session expiration time | ||
138 | */ | ||
139 | protected function extendTimeValidityBy($duration) | ||
140 | { | ||
141 | $expirationTime = time() + $duration; | ||
142 | $this->session['expires_on'] = $expirationTime; | ||
143 | return $expirationTime; | ||
144 | } | ||
145 | |||
146 | /** | ||
147 | * Logout a user by unsetting all login information | ||
148 | * | ||
149 | * See: | ||
150 | * - https://secure.php.net/manual/en/function.setcookie.php | ||
151 | */ | ||
152 | public function logout() | ||
153 | { | ||
154 | if (isset($this->session)) { | ||
155 | unset($this->session['ip']); | ||
156 | unset($this->session['expires_on']); | ||
157 | unset($this->session['username']); | ||
158 | unset($this->session['visibility']); | ||
159 | unset($this->session['untaggedonly']); | ||
160 | } | ||
161 | } | ||
162 | |||
163 | /** | ||
164 | * Check whether the session has expired | ||
165 | * | ||
166 | * @param string $clientIpId Client IP address identifier | ||
167 | * | ||
168 | * @return bool true if the session has expired, false otherwise | ||
169 | */ | ||
170 | public function hasSessionExpired() | ||
171 | { | ||
172 | if (empty($this->session['expires_on'])) { | ||
173 | return true; | ||
174 | } | ||
175 | if (time() >= $this->session['expires_on']) { | ||
176 | return true; | ||
177 | } | ||
178 | return false; | ||
179 | } | ||
180 | |||
181 | /** | ||
182 | * Check whether the client IP address has changed | ||
183 | * | ||
184 | * @param string $clientIpId Client IP address identifier | ||
185 | * | ||
186 | * @return bool true if the IP has changed, false if it has not, or | ||
187 | * if session protection has been disabled | ||
188 | */ | ||
189 | public function hasClientIpChanged($clientIpId) | ||
190 | { | ||
191 | if ($this->conf->get('security.session_protection_disabled') === true) { | ||
192 | return false; | ||
193 | } | ||
194 | if (isset($this->session['ip']) && $this->session['ip'] === $clientIpId) { | ||
195 | return false; | ||
196 | } | ||
197 | return true; | ||
198 | } | ||
199 | } | ||
diff --git a/composer.json b/composer.json index 15e082f8..0d4c623c 100644 --- a/composer.json +++ b/composer.json | |||
@@ -36,7 +36,8 @@ | |||
36 | "Shaarli\\Api\\Controllers\\": "application/api/controllers", | 36 | "Shaarli\\Api\\Controllers\\": "application/api/controllers", |
37 | "Shaarli\\Api\\Exceptions\\": "application/api/exceptions", | 37 | "Shaarli\\Api\\Exceptions\\": "application/api/exceptions", |
38 | "Shaarli\\Config\\": "application/config/", | 38 | "Shaarli\\Config\\": "application/config/", |
39 | "Shaarli\\Config\\Exception\\": "application/config/exception" | 39 | "Shaarli\\Config\\Exception\\": "application/config/exception", |
40 | "Shaarli\\Security\\": "application/security" | ||
40 | } | 41 | } |
41 | } | 42 | } |
42 | } | 43 | } |
@@ -78,8 +78,8 @@ require_once 'application/Updater.php'; | |||
78 | use \Shaarli\Languages; | 78 | use \Shaarli\Languages; |
79 | use \Shaarli\ThemeUtils; | 79 | use \Shaarli\ThemeUtils; |
80 | use \Shaarli\Config\ConfigManager; | 80 | use \Shaarli\Config\ConfigManager; |
81 | use \Shaarli\LoginManager; | 81 | use \Shaarli\Security\LoginManager; |
82 | use \Shaarli\SessionManager; | 82 | use \Shaarli\Security\SessionManager; |
83 | 83 | ||
84 | // Ensure the PHP version is supported | 84 | // Ensure the PHP version is supported |
85 | try { | 85 | try { |
@@ -101,8 +101,6 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/') { | |||
101 | // Set default cookie expiration and path. | 101 | // Set default cookie expiration and path. |
102 | session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']); | 102 | session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']); |
103 | // Set session parameters on server side. | 103 | // Set session parameters on server side. |
104 | // If the user does not access any page within this time, his/her session is considered expired. | ||
105 | define('INACTIVITY_TIMEOUT', 3600); // in seconds. | ||
106 | // Use cookies to store session. | 104 | // Use cookies to store session. |
107 | ini_set('session.use_cookies', 1); | 105 | ini_set('session.use_cookies', 1); |
108 | // Force cookies for session (phpsessionID forbidden in URL). | 106 | // Force cookies for session (phpsessionID forbidden in URL). |
@@ -123,8 +121,10 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) | |||
123 | } | 121 | } |
124 | 122 | ||
125 | $conf = new ConfigManager(); | 123 | $conf = new ConfigManager(); |
126 | $loginManager = new LoginManager($GLOBALS, $conf); | ||
127 | $sessionManager = new SessionManager($_SESSION, $conf); | 124 | $sessionManager = new SessionManager($_SESSION, $conf); |
125 | $loginManager = new LoginManager($GLOBALS, $conf, $sessionManager); | ||
126 | $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']); | ||
127 | $clientIpId = client_ip_id($_SERVER); | ||
128 | 128 | ||
129 | // LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead. | 129 | // LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead. |
130 | if (! defined('LC_MESSAGES')) { | 130 | if (! defined('LC_MESSAGES')) { |
@@ -177,157 +177,61 @@ if (! is_file($conf->getConfigFileExt())) { | |||
177 | install($conf, $sessionManager); | 177 | install($conf, $sessionManager); |
178 | } | 178 | } |
179 | 179 | ||
180 | // a token depending of deployment salt, user password, and the current ip | 180 | $loginManager->checkLoginState($_COOKIE, $clientIpId); |
181 | define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); | ||
182 | 181 | ||
183 | /** | 182 | /** |
184 | * Checking session state (i.e. is the user still logged in) | 183 | * Adapter function to ensure compatibility with third-party templates |
185 | * | 184 | * |
186 | * @param ConfigManager $conf The configuration manager. | 185 | * @see https://github.com/shaarli/Shaarli/pull/1086 |
187 | * | 186 | * |
188 | * @return bool: true if the user is logged in, false otherwise. | 187 | * @return bool true when the user is logged in, false otherwise |
189 | */ | 188 | */ |
190 | function setup_login_state($conf) | ||
191 | { | ||
192 | if ($conf->get('security.open_shaarli')) { | ||
193 | return true; | ||
194 | } | ||
195 | $userIsLoggedIn = false; // By default, we do not consider the user as logged in; | ||
196 | $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met. | ||
197 | if (! $conf->exists('credentials.login')) { | ||
198 | $userIsLoggedIn = false; // Shaarli is not configured yet. | ||
199 | $loginFailure = true; | ||
200 | } | ||
201 | if (isset($_COOKIE['shaarli_staySignedIn']) && | ||
202 | $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN && | ||
203 | !$loginFailure) | ||
204 | { | ||
205 | fillSessionInfo($conf); | ||
206 | $userIsLoggedIn = true; | ||
207 | } | ||
208 | // If session does not exist on server side, or IP address has changed, or session has expired, logout. | ||
209 | if (empty($_SESSION['uid']) | ||
210 | || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs()) | ||
211 | || time() >= $_SESSION['expires_on']) | ||
212 | { | ||
213 | logout(); | ||
214 | $userIsLoggedIn = false; | ||
215 | $loginFailure = true; | ||
216 | } | ||
217 | if (!empty($_SESSION['longlastingsession'])) { | ||
218 | $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked. | ||
219 | } | ||
220 | else { | ||
221 | $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date. | ||
222 | } | ||
223 | if (!$loginFailure) { | ||
224 | $userIsLoggedIn = true; | ||
225 | } | ||
226 | |||
227 | return $userIsLoggedIn; | ||
228 | } | ||
229 | $userIsLoggedIn = setup_login_state($conf); | ||
230 | |||
231 | // ------------------------------------------------------------------------------------------ | ||
232 | // Session management | ||
233 | |||
234 | // Returns the IP address of the client (Used to prevent session cookie hijacking.) | ||
235 | function allIPs() | ||
236 | { | ||
237 | $ip = $_SERVER['REMOTE_ADDR']; | ||
238 | // Then we use more HTTP headers to prevent session hijacking from users behind the same proxy. | ||
239 | if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; } | ||
240 | if (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip=$ip.'_'.$_SERVER['HTTP_CLIENT_IP']; } | ||
241 | return $ip; | ||
242 | } | ||
243 | |||
244 | /** | ||
245 | * Load user session. | ||
246 | * | ||
247 | * @param ConfigManager $conf Configuration Manager instance. | ||
248 | */ | ||
249 | function fillSessionInfo($conf) | ||
250 | { | ||
251 | $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid) | ||
252 | $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked. | ||
253 | $_SESSION['username']= $conf->get('credentials.login'); | ||
254 | $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration. | ||
255 | } | ||
256 | |||
257 | /** | ||
258 | * Check that user/password is correct. | ||
259 | * | ||
260 | * @param string $login Username | ||
261 | * @param string $password User password | ||
262 | * @param ConfigManager $conf Configuration Manager instance. | ||
263 | * | ||
264 | * @return bool: authentication successful or not. | ||
265 | */ | ||
266 | function check_auth($login, $password, $conf) | ||
267 | { | ||
268 | $hash = sha1($password . $login . $conf->get('credentials.salt')); | ||
269 | if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash')) | ||
270 | { // Login/password is correct. | ||
271 | fillSessionInfo($conf); | ||
272 | logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful'); | ||
273 | return true; | ||
274 | } | ||
275 | logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login); | ||
276 | return false; | ||
277 | } | ||
278 | |||
279 | // Returns true if the user is logged in. | ||
280 | function isLoggedIn() | 189 | function isLoggedIn() |
281 | { | 190 | { |
282 | global $userIsLoggedIn; | 191 | global $loginManager; |
283 | return $userIsLoggedIn; | 192 | return $loginManager->isLoggedIn(); |
284 | } | 193 | } |
285 | 194 | ||
286 | // Force logout. | ||
287 | function logout() { | ||
288 | if (isset($_SESSION)) { | ||
289 | unset($_SESSION['uid']); | ||
290 | unset($_SESSION['ip']); | ||
291 | unset($_SESSION['username']); | ||
292 | unset($_SESSION['visibility']); | ||
293 | unset($_SESSION['untaggedonly']); | ||
294 | } | ||
295 | setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH); | ||
296 | } | ||
297 | 195 | ||
298 | // ------------------------------------------------------------------------------------------ | 196 | // ------------------------------------------------------------------------------------------ |
299 | // Process login form: Check if login/password is correct. | 197 | // Process login form: Check if login/password is correct. |
300 | if (isset($_POST['login'])) | 198 | if (isset($_POST['login'])) { |
301 | { | ||
302 | if (! $loginManager->canLogin($_SERVER)) { | 199 | if (! $loginManager->canLogin($_SERVER)) { |
303 | die(t('I said: NO. You are banned for the moment. Go away.')); | 200 | die(t('I said: NO. You are banned for the moment. Go away.')); |
304 | } | 201 | } |
305 | if (isset($_POST['password']) | 202 | if (isset($_POST['password']) |
306 | && $sessionManager->checkToken($_POST['token']) | 203 | && $sessionManager->checkToken($_POST['token']) |
307 | && (check_auth($_POST['login'], $_POST['password'], $conf)) | 204 | && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password']) |
308 | ) { | 205 | ) { |
309 | // Login/password is OK. | ||
310 | $loginManager->handleSuccessfulLogin($_SERVER); | 206 | $loginManager->handleSuccessfulLogin($_SERVER); |
311 | 207 | ||
312 | // If user wants to keep the session cookie even after the browser closes: | 208 | $cookiedir = ''; |
313 | if (!empty($_POST['longlastingsession'])) { | 209 | if (dirname($_SERVER['SCRIPT_NAME']) != '/') { |
314 | $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year) | ||
315 | $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now) | ||
316 | setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH); | ||
317 | $_SESSION['expires_on'] = $expiration; // Set session expiration on server-side. | ||
318 | |||
319 | $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; | ||
320 | session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side | ||
321 | // Note: Never forget the trailing slash on the cookie path! | 210 | // Note: Never forget the trailing slash on the cookie path! |
322 | session_regenerate_id(true); // Send cookie with new expiration date to browser. | 211 | $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/'; |
323 | } | 212 | } |
324 | else // Standard session expiration (=when browser closes) | 213 | |
325 | { | 214 | if (!empty($_POST['longlastingsession'])) { |
326 | $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; | 215 | // Keep the session cookie even after the browser closes |
327 | session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes" | 216 | $sessionManager->setStaySignedIn(true); |
328 | session_regenerate_id(true); | 217 | $expirationTime = $sessionManager->extendSession(); |
218 | |||
219 | setcookie( | ||
220 | $loginManager::$STAY_SIGNED_IN_COOKIE, | ||
221 | $loginManager->getStaySignedInToken(), | ||
222 | $expirationTime, | ||
223 | WEB_PATH | ||
224 | ); | ||
225 | |||
226 | } else { | ||
227 | // Standard session expiration (=when browser closes) | ||
228 | $expirationTime = 0; | ||
329 | } | 229 | } |
330 | 230 | ||
231 | // Send cookie with the new expiration date to the browser | ||
232 | session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']); | ||
233 | session_regenerate_id(true); | ||
234 | |||
331 | // Optional redirect after login: | 235 | // Optional redirect after login: |
332 | if (isset($_GET['post'])) { | 236 | if (isset($_GET['post'])) { |
333 | $uri = '?post='. urlencode($_GET['post']); | 237 | $uri = '?post='. urlencode($_GET['post']); |
@@ -380,15 +284,16 @@ if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are atta | |||
380 | * Gives the last 7 days (which have links). | 284 | * Gives the last 7 days (which have links). |
381 | * This RSS feed cannot be filtered. | 285 | * This RSS feed cannot be filtered. |
382 | * | 286 | * |
383 | * @param ConfigManager $conf Configuration Manager instance. | 287 | * @param ConfigManager $conf Configuration Manager instance |
288 | * @param LoginManager $loginManager LoginManager instance | ||
384 | */ | 289 | */ |
385 | function showDailyRSS($conf) { | 290 | function showDailyRSS($conf, $loginManager) { |
386 | // Cache system | 291 | // Cache system |
387 | $query = $_SERVER['QUERY_STRING']; | 292 | $query = $_SERVER['QUERY_STRING']; |
388 | $cache = new CachedPage( | 293 | $cache = new CachedPage( |
389 | $conf->get('config.PAGE_CACHE'), | 294 | $conf->get('config.PAGE_CACHE'), |
390 | page_url($_SERVER), | 295 | page_url($_SERVER), |
391 | startsWith($query,'do=dailyrss') && !isLoggedIn() | 296 | startsWith($query,'do=dailyrss') && !$loginManager->isLoggedIn() |
392 | ); | 297 | ); |
393 | $cached = $cache->cachedVersion(); | 298 | $cached = $cache->cachedVersion(); |
394 | if (!empty($cached)) { | 299 | if (!empty($cached)) { |
@@ -400,7 +305,7 @@ function showDailyRSS($conf) { | |||
400 | // Read links from database (and filter private links if used it not logged in). | 305 | // Read links from database (and filter private links if used it not logged in). |
401 | $LINKSDB = new LinkDB( | 306 | $LINKSDB = new LinkDB( |
402 | $conf->get('resource.datastore'), | 307 | $conf->get('resource.datastore'), |
403 | isLoggedIn(), | 308 | $loginManager->isLoggedIn(), |
404 | $conf->get('privacy.hide_public_links'), | 309 | $conf->get('privacy.hide_public_links'), |
405 | $conf->get('redirector.url'), | 310 | $conf->get('redirector.url'), |
406 | $conf->get('redirector.encode_url') | 311 | $conf->get('redirector.encode_url') |
@@ -482,9 +387,10 @@ function showDailyRSS($conf) { | |||
482 | * @param PageBuilder $pageBuilder Template engine wrapper. | 387 | * @param PageBuilder $pageBuilder Template engine wrapper. |
483 | * @param LinkDB $LINKSDB LinkDB instance. | 388 | * @param LinkDB $LINKSDB LinkDB instance. |
484 | * @param ConfigManager $conf Configuration Manager instance. | 389 | * @param ConfigManager $conf Configuration Manager instance. |
485 | * @param PluginManager $pluginManager Plugin Manager instane. | 390 | * @param PluginManager $pluginManager Plugin Manager instance. |
391 | * @param LoginManager $loginManager Login Manager instance | ||
486 | */ | 392 | */ |
487 | function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) | 393 | function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager) |
488 | { | 394 | { |
489 | $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD. | 395 | $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD. |
490 | if (isset($_GET['day'])) { | 396 | if (isset($_GET['day'])) { |
@@ -542,7 +448,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) | |||
542 | 448 | ||
543 | /* Hook is called before column construction so that plugins don't have | 449 | /* Hook is called before column construction so that plugins don't have |
544 | to deal with columns. */ | 450 | to deal with columns. */ |
545 | $pluginManager->executeHooks('render_daily', $data, array('loggedin' => isLoggedIn())); | 451 | $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn())); |
546 | 452 | ||
547 | /* We need to spread the articles on 3 columns. | 453 | /* We need to spread the articles on 3 columns. |
548 | I did not want to use a JavaScript lib like http://masonry.desandro.com/ | 454 | I did not want to use a JavaScript lib like http://masonry.desandro.com/ |
@@ -586,8 +492,8 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) | |||
586 | * @param ConfigManager $conf Configuration Manager instance. | 492 | * @param ConfigManager $conf Configuration Manager instance. |
587 | * @param PluginManager $pluginManager Plugin Manager instance. | 493 | * @param PluginManager $pluginManager Plugin Manager instance. |
588 | */ | 494 | */ |
589 | function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) { | 495 | function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) { |
590 | buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager); // Compute list of links to display | 496 | buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager, $loginManager); |
591 | $PAGE->renderPage('linklist'); | 497 | $PAGE->renderPage('linklist'); |
592 | } | 498 | } |
593 | 499 | ||
@@ -607,7 +513,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
607 | read_updates_file($conf->get('resource.updates')), | 513 | read_updates_file($conf->get('resource.updates')), |
608 | $LINKSDB, | 514 | $LINKSDB, |
609 | $conf, | 515 | $conf, |
610 | isLoggedIn() | 516 | $loginManager->isLoggedIn() |
611 | ); | 517 | ); |
612 | try { | 518 | try { |
613 | $newUpdates = $updater->update(); | 519 | $newUpdates = $updater->update(); |
@@ -622,18 +528,18 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
622 | die($e->getMessage()); | 528 | die($e->getMessage()); |
623 | } | 529 | } |
624 | 530 | ||
625 | $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken()); | 531 | $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn()); |
626 | $PAGE->assign('linkcount', count($LINKSDB)); | 532 | $PAGE->assign('linkcount', count($LINKSDB)); |
627 | $PAGE->assign('privateLinkcount', count_private($LINKSDB)); | 533 | $PAGE->assign('privateLinkcount', count_private($LINKSDB)); |
628 | $PAGE->assign('plugin_errors', $pluginManager->getErrors()); | 534 | $PAGE->assign('plugin_errors', $pluginManager->getErrors()); |
629 | 535 | ||
630 | // Determine which page will be rendered. | 536 | // Determine which page will be rendered. |
631 | $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; | 537 | $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; |
632 | $targetPage = Router::findPage($query, $_GET, isLoggedIn()); | 538 | $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn()); |
633 | 539 | ||
634 | if ( | 540 | if ( |
635 | // if the user isn't logged in | 541 | // if the user isn't logged in |
636 | !isLoggedIn() && | 542 | !$loginManager->isLoggedIn() && |
637 | // and Shaarli doesn't have public content... | 543 | // and Shaarli doesn't have public content... |
638 | $conf->get('privacy.hide_public_links') && | 544 | $conf->get('privacy.hide_public_links') && |
639 | // and is configured to enforce the login | 545 | // and is configured to enforce the login |
@@ -661,7 +567,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
661 | $pluginManager->executeHooks('render_' . $name, $plugin_data, | 567 | $pluginManager->executeHooks('render_' . $name, $plugin_data, |
662 | array( | 568 | array( |
663 | 'target' => $targetPage, | 569 | 'target' => $targetPage, |
664 | 'loggedin' => isLoggedIn() | 570 | 'loggedin' => $loginManager->isLoggedIn() |
665 | ) | 571 | ) |
666 | ); | 572 | ); |
667 | $PAGE->assign('plugins_' . $name, $plugin_data); | 573 | $PAGE->assign('plugins_' . $name, $plugin_data); |
@@ -686,7 +592,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
686 | if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) | 592 | if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) |
687 | { | 593 | { |
688 | invalidateCaches($conf->get('resource.page_cache')); | 594 | invalidateCaches($conf->get('resource.page_cache')); |
689 | logout(); | 595 | $sessionManager->logout(); |
596 | setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH); | ||
690 | header('Location: ?'); | 597 | header('Location: ?'); |
691 | exit; | 598 | exit; |
692 | } | 599 | } |
@@ -713,7 +620,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
713 | $data = array( | 620 | $data = array( |
714 | 'linksToDisplay' => $linksToDisplay, | 621 | 'linksToDisplay' => $linksToDisplay, |
715 | ); | 622 | ); |
716 | $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => isLoggedIn())); | 623 | $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn())); |
717 | 624 | ||
718 | foreach ($data as $key => $value) { | 625 | foreach ($data as $key => $value) { |
719 | $PAGE->assign($key, $value); | 626 | $PAGE->assign($key, $value); |
@@ -760,7 +667,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
760 | 'search_tags' => $searchTags, | 667 | 'search_tags' => $searchTags, |
761 | 'tags' => $tagList, | 668 | 'tags' => $tagList, |
762 | ); | 669 | ); |
763 | $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); | 670 | $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn())); |
764 | 671 | ||
765 | foreach ($data as $key => $value) { | 672 | foreach ($data as $key => $value) { |
766 | $PAGE->assign($key, $value); | 673 | $PAGE->assign($key, $value); |
@@ -793,7 +700,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
793 | 'search_tags' => $searchTags, | 700 | 'search_tags' => $searchTags, |
794 | 'tags' => $tags, | 701 | 'tags' => $tags, |
795 | ]; | 702 | ]; |
796 | $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); | 703 | $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]); |
797 | 704 | ||
798 | foreach ($data as $key => $value) { | 705 | foreach ($data as $key => $value) { |
799 | $PAGE->assign($key, $value); | 706 | $PAGE->assign($key, $value); |
@@ -807,7 +714,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
807 | 714 | ||
808 | // Daily page. | 715 | // Daily page. |
809 | if ($targetPage == Router::$PAGE_DAILY) { | 716 | if ($targetPage == Router::$PAGE_DAILY) { |
810 | showDaily($PAGE, $LINKSDB, $conf, $pluginManager); | 717 | showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); |
811 | } | 718 | } |
812 | 719 | ||
813 | // ATOM and RSS feed. | 720 | // ATOM and RSS feed. |
@@ -820,7 +727,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
820 | $cache = new CachedPage( | 727 | $cache = new CachedPage( |
821 | $conf->get('resource.page_cache'), | 728 | $conf->get('resource.page_cache'), |
822 | page_url($_SERVER), | 729 | page_url($_SERVER), |
823 | startsWith($query,'do='. $targetPage) && !isLoggedIn() | 730 | startsWith($query,'do='. $targetPage) && !$loginManager->isLoggedIn() |
824 | ); | 731 | ); |
825 | $cached = $cache->cachedVersion(); | 732 | $cached = $cache->cachedVersion(); |
826 | if (!empty($cached)) { | 733 | if (!empty($cached)) { |
@@ -829,15 +736,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
829 | } | 736 | } |
830 | 737 | ||
831 | // Generate data. | 738 | // Generate data. |
832 | $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, isLoggedIn()); | 739 | $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn()); |
833 | $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0))); | 740 | $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0))); |
834 | $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn()); | 741 | $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn()); |
835 | $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks')); | 742 | $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks')); |
836 | $data = $feedGenerator->buildData(); | 743 | $data = $feedGenerator->buildData(); |
837 | 744 | ||
838 | // Process plugin hook. | 745 | // Process plugin hook. |
839 | $pluginManager->executeHooks('render_feed', $data, array( | 746 | $pluginManager->executeHooks('render_feed', $data, array( |
840 | 'loggedin' => isLoggedIn(), | 747 | 'loggedin' => $loginManager->isLoggedIn(), |
841 | 'target' => $targetPage, | 748 | 'target' => $targetPage, |
842 | )); | 749 | )); |
843 | 750 | ||
@@ -985,7 +892,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
985 | } | 892 | } |
986 | 893 | ||
987 | // -------- Handle other actions allowed for non-logged in users: | 894 | // -------- Handle other actions allowed for non-logged in users: |
988 | if (!isLoggedIn()) | 895 | if (!$loginManager->isLoggedIn()) |
989 | { | 896 | { |
990 | // User tries to post new link but is not logged in: | 897 | // User tries to post new link but is not logged in: |
991 | // Show login screen, then redirect to ?post=... | 898 | // Show login screen, then redirect to ?post=... |
@@ -1001,7 +908,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
1001 | exit; | 908 | exit; |
1002 | } | 909 | } |
1003 | 910 | ||
1004 | showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); | 911 | showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); |
1005 | if (isset($_GET['edit_link'])) { | 912 | if (isset($_GET['edit_link'])) { |
1006 | header('Location: ?do=login&edit_link='. escape($_GET['edit_link'])); | 913 | header('Location: ?do=login&edit_link='. escape($_GET['edit_link'])); |
1007 | exit; | 914 | exit; |
@@ -1052,7 +959,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
1052 | $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); | 959 | $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); |
1053 | $conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt'))); | 960 | $conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt'))); |
1054 | try { | 961 | try { |
1055 | $conf->write(isLoggedIn()); | 962 | $conf->write($loginManager->isLoggedIn()); |
1056 | } | 963 | } |
1057 | catch(Exception $e) { | 964 | catch(Exception $e) { |
1058 | error_log( | 965 | error_log( |
@@ -1103,7 +1010,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
1103 | $conf->set('translation.language', escape($_POST['language'])); | 1010 | $conf->set('translation.language', escape($_POST['language'])); |
1104 | 1011 | ||
1105 | try { | 1012 | try { |
1106 | $conf->write(isLoggedIn()); | 1013 | $conf->write($loginManager->isLoggedIn()); |
1107 | $history->updateSettings(); | 1014 | $history->updateSettings(); |
1108 | invalidateCaches($conf->get('resource.page_cache')); | 1015 | invalidateCaches($conf->get('resource.page_cache')); |
1109 | } | 1016 | } |
@@ -1555,7 +1462,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
1555 | else { | 1462 | else { |
1556 | $conf->set('general.enabled_plugins', save_plugin_config($_POST)); | 1463 | $conf->set('general.enabled_plugins', save_plugin_config($_POST)); |
1557 | } | 1464 | } |
1558 | $conf->write(isLoggedIn()); | 1465 | $conf->write($loginManager->isLoggedIn()); |
1559 | $history->updateSettings(); | 1466 | $history->updateSettings(); |
1560 | } | 1467 | } |
1561 | catch (Exception $e) { | 1468 | catch (Exception $e) { |
@@ -1580,7 +1487,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
1580 | } | 1487 | } |
1581 | 1488 | ||
1582 | // -------- Otherwise, simply display search form and links: | 1489 | // -------- Otherwise, simply display search form and links: |
1583 | showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); | 1490 | showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager); |
1584 | exit; | 1491 | exit; |
1585 | } | 1492 | } |
1586 | 1493 | ||
@@ -1592,8 +1499,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, | |||
1592 | * @param LinkDB $LINKSDB LinkDB instance. | 1499 | * @param LinkDB $LINKSDB LinkDB instance. |
1593 | * @param ConfigManager $conf Configuration Manager instance. | 1500 | * @param ConfigManager $conf Configuration Manager instance. |
1594 | * @param PluginManager $pluginManager Plugin Manager instance. | 1501 | * @param PluginManager $pluginManager Plugin Manager instance. |
1502 | * @param LoginManager $loginManager LoginManager instance | ||
1595 | */ | 1503 | */ |
1596 | function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) | 1504 | function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) |
1597 | { | 1505 | { |
1598 | // Used in templates | 1506 | // Used in templates |
1599 | if (isset($_GET['searchtags'])) { | 1507 | if (isset($_GET['searchtags'])) { |
@@ -1632,8 +1540,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) | |||
1632 | $keys[] = $key; | 1540 | $keys[] = $key; |
1633 | } | 1541 | } |
1634 | 1542 | ||
1635 | |||
1636 | |||
1637 | // Select articles according to paging. | 1543 | // Select articles according to paging. |
1638 | $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']); | 1544 | $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']); |
1639 | $pagecount = $pagecount == 0 ? 1 : $pagecount; | 1545 | $pagecount = $pagecount == 0 ? 1 : $pagecount; |
@@ -1714,7 +1620,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) | |||
1714 | $data['pagetitle'] .= '- '. $conf->get('general.title'); | 1620 | $data['pagetitle'] .= '- '. $conf->get('general.title'); |
1715 | } | 1621 | } |
1716 | 1622 | ||
1717 | $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn())); | 1623 | $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn())); |
1718 | 1624 | ||
1719 | foreach ($data as $key => $value) { | 1625 | foreach ($data as $key => $value) { |
1720 | $PAGE->assign($key, $value); | 1626 | $PAGE->assign($key, $value); |
@@ -1985,7 +1891,7 @@ function install($conf, $sessionManager) { | |||
1985 | ); | 1891 | ); |
1986 | try { | 1892 | try { |
1987 | // Everything is ok, let's create config file. | 1893 | // Everything is ok, let's create config file. |
1988 | $conf->write(isLoggedIn()); | 1894 | $conf->write($loginManager->isLoggedIn()); |
1989 | } | 1895 | } |
1990 | catch(Exception $e) { | 1896 | catch(Exception $e) { |
1991 | error_log( | 1897 | error_log( |
@@ -2249,7 +2155,7 @@ try { | |||
2249 | 2155 | ||
2250 | $linkDb = new LinkDB( | 2156 | $linkDb = new LinkDB( |
2251 | $conf->get('resource.datastore'), | 2157 | $conf->get('resource.datastore'), |
2252 | isLoggedIn(), | 2158 | $loginManager->isLoggedIn(), |
2253 | $conf->get('privacy.hide_public_links'), | 2159 | $conf->get('privacy.hide_public_links'), |
2254 | $conf->get('redirector.url'), | 2160 | $conf->get('redirector.url'), |
2255 | $conf->get('redirector.encode_url') | 2161 | $conf->get('redirector.encode_url') |
diff --git a/tests/HttpUtils/ClientIpIdTest.php b/tests/HttpUtils/ClientIpIdTest.php new file mode 100644 index 00000000..c15ac5cc --- /dev/null +++ b/tests/HttpUtils/ClientIpIdTest.php | |||
@@ -0,0 +1,52 @@ | |||
1 | <?php | ||
2 | /** | ||
3 | * HttpUtils' tests | ||
4 | */ | ||
5 | |||
6 | require_once 'application/HttpUtils.php'; | ||
7 | |||
8 | /** | ||
9 | * Unitary tests for client_ip_id() | ||
10 | */ | ||
11 | class ClientIpIdTest extends PHPUnit_Framework_TestCase | ||
12 | { | ||
13 | /** | ||
14 | * Get a remote client ID based on its IP | ||
15 | */ | ||
16 | public function testClientIpIdRemote() | ||
17 | { | ||
18 | $this->assertEquals( | ||
19 | '10.1.167.42', | ||
20 | client_ip_id(['REMOTE_ADDR' => '10.1.167.42']) | ||
21 | ); | ||
22 | } | ||
23 | |||
24 | /** | ||
25 | * Get a remote client ID based on its IP and proxy information (1) | ||
26 | */ | ||
27 | public function testClientIpIdRemoteForwarded() | ||
28 | { | ||
29 | $this->assertEquals( | ||
30 | '10.1.167.42_127.0.1.47', | ||
31 | client_ip_id([ | ||
32 | 'REMOTE_ADDR' => '10.1.167.42', | ||
33 | 'HTTP_X_FORWARDED_FOR' => '127.0.1.47' | ||
34 | ]) | ||
35 | ); | ||
36 | } | ||
37 | |||
38 | /** | ||
39 | * Get a remote client ID based on its IP and proxy information (2) | ||
40 | */ | ||
41 | public function testClientIpIdRemoteForwardedClient() | ||
42 | { | ||
43 | $this->assertEquals( | ||
44 | '10.1.167.42_10.1.167.56_127.0.1.47', | ||
45 | client_ip_id([ | ||
46 | 'REMOTE_ADDR' => '10.1.167.42', | ||
47 | 'HTTP_X_FORWARDED_FOR' => '10.1.167.56', | ||
48 | 'HTTP_CLIENT_IP' => '127.0.1.47' | ||
49 | ]) | ||
50 | ); | ||
51 | } | ||
52 | } | ||
diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php deleted file mode 100644 index aa75962a..00000000 --- a/tests/SessionManagerTest.php +++ /dev/null | |||
@@ -1,149 +0,0 @@ | |||
1 | <?php | ||
2 | require_once 'tests/utils/FakeConfigManager.php'; | ||
3 | |||
4 | // Initialize reference data _before_ PHPUnit starts a session | ||
5 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; | ||
6 | ReferenceSessionIdHashes::genAllHashes(); | ||
7 | |||
8 | use \Shaarli\SessionManager; | ||
9 | use \PHPUnit\Framework\TestCase; | ||
10 | |||
11 | |||
12 | /** | ||
13 | * Test coverage for SessionManager | ||
14 | */ | ||
15 | class SessionManagerTest extends TestCase | ||
16 | { | ||
17 | // Session ID hashes | ||
18 | protected static $sidHashes = null; | ||
19 | |||
20 | // Fake ConfigManager | ||
21 | protected static $conf = null; | ||
22 | |||
23 | /** | ||
24 | * Assign reference data | ||
25 | */ | ||
26 | public static function setUpBeforeClass() | ||
27 | { | ||
28 | self::$sidHashes = ReferenceSessionIdHashes::getHashes(); | ||
29 | self::$conf = new FakeConfigManager(); | ||
30 | } | ||
31 | |||
32 | /** | ||
33 | * Generate a session token | ||
34 | */ | ||
35 | public function testGenerateToken() | ||
36 | { | ||
37 | $session = []; | ||
38 | $sessionManager = new SessionManager($session, self::$conf); | ||
39 | |||
40 | $token = $sessionManager->generateToken(); | ||
41 | |||
42 | $this->assertEquals(1, $session['tokens'][$token]); | ||
43 | $this->assertEquals(40, strlen($token)); | ||
44 | } | ||
45 | |||
46 | /** | ||
47 | * Check a session token | ||
48 | */ | ||
49 | public function testCheckToken() | ||
50 | { | ||
51 | $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'; | ||
52 | $session = [ | ||
53 | 'tokens' => [ | ||
54 | $token => 1, | ||
55 | ], | ||
56 | ]; | ||
57 | $sessionManager = new SessionManager($session, self::$conf); | ||
58 | |||
59 | // check and destroy the token | ||
60 | $this->assertTrue($sessionManager->checkToken($token)); | ||
61 | $this->assertFalse(isset($session['tokens'][$token])); | ||
62 | |||
63 | // ensure the token has been destroyed | ||
64 | $this->assertFalse($sessionManager->checkToken($token)); | ||
65 | } | ||
66 | |||
67 | /** | ||
68 | * Generate and check a session token | ||
69 | */ | ||
70 | public function testGenerateAndCheckToken() | ||
71 | { | ||
72 | $session = []; | ||
73 | $sessionManager = new SessionManager($session, self::$conf); | ||
74 | |||
75 | $token = $sessionManager->generateToken(); | ||
76 | |||
77 | // ensure a token has been generated | ||
78 | $this->assertEquals(1, $session['tokens'][$token]); | ||
79 | $this->assertEquals(40, strlen($token)); | ||
80 | |||
81 | // check and destroy the token | ||
82 | $this->assertTrue($sessionManager->checkToken($token)); | ||
83 | $this->assertFalse(isset($session['tokens'][$token])); | ||
84 | |||
85 | // ensure the token has been destroyed | ||
86 | $this->assertFalse($sessionManager->checkToken($token)); | ||
87 | } | ||
88 | |||
89 | /** | ||
90 | * Check an invalid session token | ||
91 | */ | ||
92 | public function testCheckInvalidToken() | ||
93 | { | ||
94 | $session = []; | ||
95 | $sessionManager = new SessionManager($session, self::$conf); | ||
96 | |||
97 | $this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b')); | ||
98 | } | ||
99 | |||
100 | /** | ||
101 | * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES! | ||
102 | * | ||
103 | * This tests extensively covers all hash algorithms / bit representations | ||
104 | */ | ||
105 | public function testIsAnyHashSessionIdValid() | ||
106 | { | ||
107 | foreach (self::$sidHashes as $algo => $bpcs) { | ||
108 | foreach ($bpcs as $bpc => $hash) { | ||
109 | $this->assertTrue(SessionManager::checkId($hash)); | ||
110 | } | ||
111 | } | ||
112 | } | ||
113 | |||
114 | /** | ||
115 | * Test checkId with a valid ID - SHA-1 hashes | ||
116 | */ | ||
117 | public function testIsSha1SessionIdValid() | ||
118 | { | ||
119 | $this->assertTrue(SessionManager::checkId(sha1('shaarli'))); | ||
120 | } | ||
121 | |||
122 | /** | ||
123 | * Test checkId with a valid ID - SHA-256 hashes | ||
124 | */ | ||
125 | public function testIsSha256SessionIdValid() | ||
126 | { | ||
127 | $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli'))); | ||
128 | } | ||
129 | |||
130 | /** | ||
131 | * Test checkId with a valid ID - SHA-512 hashes | ||
132 | */ | ||
133 | public function testIsSha512SessionIdValid() | ||
134 | { | ||
135 | $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli'))); | ||
136 | } | ||
137 | |||
138 | /** | ||
139 | * Test checkId with invalid IDs. | ||
140 | */ | ||
141 | public function testIsSessionIdInvalid() | ||
142 | { | ||
143 | $this->assertFalse(SessionManager::checkId('')); | ||
144 | $this->assertFalse(SessionManager::checkId([])); | ||
145 | $this->assertFalse( | ||
146 | SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=') | ||
147 | ); | ||
148 | } | ||
149 | } | ||
diff --git a/tests/LoginManagerTest.php b/tests/security/LoginManagerTest.php index 4159038e..f26cd1eb 100644 --- a/tests/LoginManagerTest.php +++ b/tests/security/LoginManagerTest.php | |||
@@ -1,5 +1,5 @@ | |||
1 | <?php | 1 | <?php |
2 | namespace Shaarli; | 2 | namespace Shaarli\Security; |
3 | 3 | ||
4 | require_once 'tests/utils/FakeConfigManager.php'; | 4 | require_once 'tests/utils/FakeConfigManager.php'; |
5 | use \PHPUnit\Framework\TestCase; | 5 | use \PHPUnit\Framework\TestCase; |
@@ -9,15 +9,54 @@ use \PHPUnit\Framework\TestCase; | |||
9 | */ | 9 | */ |
10 | class LoginManagerTest extends TestCase | 10 | class LoginManagerTest extends TestCase |
11 | { | 11 | { |
12 | /** @var \FakeConfigManager Configuration Manager instance */ | ||
12 | protected $configManager = null; | 13 | protected $configManager = null; |
14 | |||
15 | /** @var LoginManager Login Manager instance */ | ||
13 | protected $loginManager = null; | 16 | protected $loginManager = null; |
17 | |||
18 | /** @var SessionManager Session Manager instance */ | ||
19 | protected $sessionManager = null; | ||
20 | |||
21 | /** @var string Banned IP filename */ | ||
14 | protected $banFile = 'sandbox/ipbans.php'; | 22 | protected $banFile = 'sandbox/ipbans.php'; |
23 | |||
24 | /** @var string Log filename */ | ||
15 | protected $logFile = 'sandbox/shaarli.log'; | 25 | protected $logFile = 'sandbox/shaarli.log'; |
26 | |||
27 | /** @var array Simulates the $_COOKIE array */ | ||
28 | protected $cookie = []; | ||
29 | |||
30 | /** @var array Simulates the $GLOBALS array */ | ||
16 | protected $globals = []; | 31 | protected $globals = []; |
17 | protected $ipAddr = '127.0.0.1'; | 32 | |
33 | /** @var array Simulates the $_SERVER array */ | ||
18 | protected $server = []; | 34 | protected $server = []; |
35 | |||
36 | /** @var array Simulates the $_SESSION array */ | ||
37 | protected $session = []; | ||
38 | |||
39 | /** @var string Advertised client IP address */ | ||
40 | protected $clientIpAddress = '10.1.47.179'; | ||
41 | |||
42 | /** @var string Local client IP address */ | ||
43 | protected $ipAddr = '127.0.0.1'; | ||
44 | |||
45 | /** @var string Trusted proxy IP address */ | ||
19 | protected $trustedProxy = '10.1.1.100'; | 46 | protected $trustedProxy = '10.1.1.100'; |
20 | 47 | ||
48 | /** @var string User login */ | ||
49 | protected $login = 'johndoe'; | ||
50 | |||
51 | /** @var string User password */ | ||
52 | protected $password = 'IC4nHazL0g1n?'; | ||
53 | |||
54 | /** @var string Hash of the salted user password */ | ||
55 | protected $passwordHash = ''; | ||
56 | |||
57 | /** @var string Salt used by hash functions */ | ||
58 | protected $salt = '669e24fa9c5a59a613f98e8e38327384504a4af2'; | ||
59 | |||
21 | /** | 60 | /** |
22 | * Prepare or reset test resources | 61 | * Prepare or reset test resources |
23 | */ | 62 | */ |
@@ -27,7 +66,12 @@ class LoginManagerTest extends TestCase | |||
27 | unlink($this->banFile); | 66 | unlink($this->banFile); |
28 | } | 67 | } |
29 | 68 | ||
69 | $this->passwordHash = sha1($this->password . $this->login . $this->salt); | ||
70 | |||
30 | $this->configManager = new \FakeConfigManager([ | 71 | $this->configManager = new \FakeConfigManager([ |
72 | 'credentials.login' => $this->login, | ||
73 | 'credentials.hash' => $this->passwordHash, | ||
74 | 'credentials.salt' => $this->salt, | ||
31 | 'resource.ban_file' => $this->banFile, | 75 | 'resource.ban_file' => $this->banFile, |
32 | 'resource.log' => $this->logFile, | 76 | 'resource.log' => $this->logFile, |
33 | 'security.ban_after' => 4, | 77 | 'security.ban_after' => 4, |
@@ -35,10 +79,15 @@ class LoginManagerTest extends TestCase | |||
35 | 'security.trusted_proxies' => [$this->trustedProxy], | 79 | 'security.trusted_proxies' => [$this->trustedProxy], |
36 | ]); | 80 | ]); |
37 | 81 | ||
82 | $this->cookie = []; | ||
83 | |||
38 | $this->globals = &$GLOBALS; | 84 | $this->globals = &$GLOBALS; |
39 | unset($this->globals['IPBANS']); | 85 | unset($this->globals['IPBANS']); |
40 | 86 | ||
41 | $this->loginManager = new LoginManager($this->globals, $this->configManager); | 87 | $this->session = []; |
88 | |||
89 | $this->sessionManager = new SessionManager($this->session, $this->configManager); | ||
90 | $this->loginManager = new LoginManager($this->globals, $this->configManager, $this->sessionManager); | ||
42 | $this->server['REMOTE_ADDR'] = $this->ipAddr; | 91 | $this->server['REMOTE_ADDR'] = $this->ipAddr; |
43 | } | 92 | } |
44 | 93 | ||
@@ -59,7 +108,7 @@ class LoginManagerTest extends TestCase | |||
59 | $this->banFile, | 108 | $this->banFile, |
60 | "<?php\n\$GLOBALS['IPBANS']=array('FAILURES' => array('127.0.0.1' => 99));\n?>" | 109 | "<?php\n\$GLOBALS['IPBANS']=array('FAILURES' => array('127.0.0.1' => 99));\n?>" |
61 | ); | 110 | ); |
62 | new LoginManager($this->globals, $this->configManager); | 111 | new LoginManager($this->globals, $this->configManager, null); |
63 | $this->assertEquals(99, $this->globals['IPBANS']['FAILURES']['127.0.0.1']); | 112 | $this->assertEquals(99, $this->globals['IPBANS']['FAILURES']['127.0.0.1']); |
64 | } | 113 | } |
65 | 114 | ||
@@ -196,4 +245,130 @@ class LoginManagerTest extends TestCase | |||
196 | $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600; | 245 | $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600; |
197 | $this->assertTrue($this->loginManager->canLogin($this->server)); | 246 | $this->assertTrue($this->loginManager->canLogin($this->server)); |
198 | } | 247 | } |
248 | |||
249 | /** | ||
250 | * Generate a token depending on the user credentials and client IP | ||
251 | */ | ||
252 | public function testGenerateStaySignedInToken() | ||
253 | { | ||
254 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); | ||
255 | |||
256 | $this->assertEquals( | ||
257 | sha1($this->passwordHash . $this->clientIpAddress . $this->salt), | ||
258 | $this->loginManager->getStaySignedInToken() | ||
259 | ); | ||
260 | } | ||
261 | |||
262 | /** | ||
263 | * Check user login - Shaarli has not yet been configured | ||
264 | */ | ||
265 | public function testCheckLoginStateNotConfigured() | ||
266 | { | ||
267 | $configManager = new \FakeConfigManager([ | ||
268 | 'resource.ban_file' => $this->banFile, | ||
269 | ]); | ||
270 | $loginManager = new LoginManager($this->globals, $configManager, null); | ||
271 | $loginManager->checkLoginState([], ''); | ||
272 | |||
273 | $this->assertFalse($loginManager->isLoggedIn()); | ||
274 | } | ||
275 | |||
276 | /** | ||
277 | * Check user login - the client cookie does not match the server token | ||
278 | */ | ||
279 | public function testCheckLoginStateStaySignedInWithInvalidToken() | ||
280 | { | ||
281 | // simulate a previous login | ||
282 | $this->session = [ | ||
283 | 'ip' => $this->clientIpAddress, | ||
284 | 'expires_on' => time() + 100, | ||
285 | ]; | ||
286 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); | ||
287 | $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = 'nope'; | ||
288 | |||
289 | $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); | ||
290 | |||
291 | $this->assertTrue($this->loginManager->isLoggedIn()); | ||
292 | $this->assertTrue(empty($this->session['username'])); | ||
293 | } | ||
294 | |||
295 | /** | ||
296 | * Check user login - the client cookie matches the server token | ||
297 | */ | ||
298 | public function testCheckLoginStateStaySignedInWithValidToken() | ||
299 | { | ||
300 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); | ||
301 | $this->cookie[LoginManager::$STAY_SIGNED_IN_COOKIE] = $this->loginManager->getStaySignedInToken(); | ||
302 | |||
303 | $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); | ||
304 | |||
305 | $this->assertTrue($this->loginManager->isLoggedIn()); | ||
306 | $this->assertEquals($this->login, $this->session['username']); | ||
307 | $this->assertEquals($this->clientIpAddress, $this->session['ip']); | ||
308 | } | ||
309 | |||
310 | /** | ||
311 | * Check user login - the session has expired | ||
312 | */ | ||
313 | public function testCheckLoginStateSessionExpired() | ||
314 | { | ||
315 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); | ||
316 | $this->session['expires_on'] = time() - 100; | ||
317 | |||
318 | $this->loginManager->checkLoginState($this->cookie, $this->clientIpAddress); | ||
319 | |||
320 | $this->assertFalse($this->loginManager->isLoggedIn()); | ||
321 | } | ||
322 | |||
323 | /** | ||
324 | * Check user login - the remote client IP has changed | ||
325 | */ | ||
326 | public function testCheckLoginStateClientIpChanged() | ||
327 | { | ||
328 | $this->loginManager->generateStaySignedInToken($this->clientIpAddress); | ||
329 | |||
330 | $this->loginManager->checkLoginState($this->cookie, '10.7.157.98'); | ||
331 | |||
332 | $this->assertFalse($this->loginManager->isLoggedIn()); | ||
333 | } | ||
334 | |||
335 | /** | ||
336 | * Check user credentials - wrong login supplied | ||
337 | */ | ||
338 | public function testCheckCredentialsWrongLogin() | ||
339 | { | ||
340 | $this->assertFalse( | ||
341 | $this->loginManager->checkCredentials('', '', 'b4dl0g1n', $this->password) | ||
342 | ); | ||
343 | } | ||
344 | |||
345 | /** | ||
346 | * Check user credentials - wrong password supplied | ||
347 | */ | ||
348 | public function testCheckCredentialsWrongPassword() | ||
349 | { | ||
350 | $this->assertFalse( | ||
351 | $this->loginManager->checkCredentials('', '', $this->login, 'b4dp455wd') | ||
352 | ); | ||
353 | } | ||
354 | |||
355 | /** | ||
356 | * Check user credentials - wrong login and password supplied | ||
357 | */ | ||
358 | public function testCheckCredentialsWrongLoginAndPassword() | ||
359 | { | ||
360 | $this->assertFalse( | ||
361 | $this->loginManager->checkCredentials('', '', 'b4dl0g1n', 'b4dp455wd') | ||
362 | ); | ||
363 | } | ||
364 | |||
365 | /** | ||
366 | * Check user credentials - correct login and password supplied | ||
367 | */ | ||
368 | public function testCheckCredentialsGoodLoginAndPassword() | ||
369 | { | ||
370 | $this->assertTrue( | ||
371 | $this->loginManager->checkCredentials('', '', $this->login, $this->password) | ||
372 | ); | ||
373 | } | ||
199 | } | 374 | } |
diff --git a/tests/security/SessionManagerTest.php b/tests/security/SessionManagerTest.php new file mode 100644 index 00000000..9bd868f8 --- /dev/null +++ b/tests/security/SessionManagerTest.php | |||
@@ -0,0 +1,273 @@ | |||
1 | <?php | ||
2 | require_once 'tests/utils/FakeConfigManager.php'; | ||
3 | |||
4 | // Initialize reference data _before_ PHPUnit starts a session | ||
5 | require_once 'tests/utils/ReferenceSessionIdHashes.php'; | ||
6 | ReferenceSessionIdHashes::genAllHashes(); | ||
7 | |||
8 | use \Shaarli\Security\SessionManager; | ||
9 | use \PHPUnit\Framework\TestCase; | ||
10 | |||
11 | |||
12 | /** | ||
13 | * Test coverage for SessionManager | ||
14 | */ | ||
15 | class SessionManagerTest extends TestCase | ||
16 | { | ||
17 | /** @var array Session ID hashes */ | ||
18 | protected static $sidHashes = null; | ||
19 | |||
20 | /** @var \FakeConfigManager ConfigManager substitute for testing */ | ||
21 | protected $conf = null; | ||
22 | |||
23 | /** @var array $_SESSION array for testing */ | ||
24 | protected $session = []; | ||
25 | |||
26 | /** @var SessionManager Server-side session management abstraction */ | ||
27 | protected $sessionManager = null; | ||
28 | |||
29 | /** | ||
30 | * Assign reference data | ||
31 | */ | ||
32 | public static function setUpBeforeClass() | ||
33 | { | ||
34 | self::$sidHashes = ReferenceSessionIdHashes::getHashes(); | ||
35 | } | ||
36 | |||
37 | /** | ||
38 | * Initialize or reset test resources | ||
39 | */ | ||
40 | public function setUp() | ||
41 | { | ||
42 | $this->conf = new FakeConfigManager([ | ||
43 | 'credentials.login' => 'johndoe', | ||
44 | 'credentials.salt' => 'salt', | ||
45 | 'security.session_protection_disabled' => false, | ||
46 | ]); | ||
47 | $this->session = []; | ||
48 | $this->sessionManager = new SessionManager($this->session, $this->conf); | ||
49 | } | ||
50 | |||
51 | /** | ||
52 | * Generate a session token | ||
53 | */ | ||
54 | public function testGenerateToken() | ||
55 | { | ||
56 | $token = $this->sessionManager->generateToken(); | ||
57 | |||
58 | $this->assertEquals(1, $this->session['tokens'][$token]); | ||
59 | $this->assertEquals(40, strlen($token)); | ||
60 | } | ||
61 | |||
62 | /** | ||
63 | * Check a session token | ||
64 | */ | ||
65 | public function testCheckToken() | ||
66 | { | ||
67 | $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'; | ||
68 | $session = [ | ||
69 | 'tokens' => [ | ||
70 | $token => 1, | ||
71 | ], | ||
72 | ]; | ||
73 | $sessionManager = new SessionManager($session, $this->conf); | ||
74 | |||
75 | // check and destroy the token | ||
76 | $this->assertTrue($sessionManager->checkToken($token)); | ||
77 | $this->assertFalse(isset($session['tokens'][$token])); | ||
78 | |||
79 | // ensure the token has been destroyed | ||
80 | $this->assertFalse($sessionManager->checkToken($token)); | ||
81 | } | ||
82 | |||
83 | /** | ||
84 | * Generate and check a session token | ||
85 | */ | ||
86 | public function testGenerateAndCheckToken() | ||
87 | { | ||
88 | $token = $this->sessionManager->generateToken(); | ||
89 | |||
90 | // ensure a token has been generated | ||
91 | $this->assertEquals(1, $this->session['tokens'][$token]); | ||
92 | $this->assertEquals(40, strlen($token)); | ||
93 | |||
94 | // check and destroy the token | ||
95 | $this->assertTrue($this->sessionManager->checkToken($token)); | ||
96 | $this->assertFalse(isset($this->session['tokens'][$token])); | ||
97 | |||
98 | // ensure the token has been destroyed | ||
99 | $this->assertFalse($this->sessionManager->checkToken($token)); | ||
100 | } | ||
101 | |||
102 | /** | ||
103 | * Check an invalid session token | ||
104 | */ | ||
105 | public function testCheckInvalidToken() | ||
106 | { | ||
107 | $this->assertFalse($this->sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b')); | ||
108 | } | ||
109 | |||
110 | /** | ||
111 | * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES! | ||
112 | * | ||
113 | * This tests extensively covers all hash algorithms / bit representations | ||
114 | */ | ||
115 | public function testIsAnyHashSessionIdValid() | ||
116 | { | ||
117 | foreach (self::$sidHashes as $algo => $bpcs) { | ||
118 | foreach ($bpcs as $bpc => $hash) { | ||
119 | $this->assertTrue(SessionManager::checkId($hash)); | ||
120 | } | ||
121 | } | ||
122 | } | ||
123 | |||
124 | /** | ||
125 | * Test checkId with a valid ID - SHA-1 hashes | ||
126 | */ | ||
127 | public function testIsSha1SessionIdValid() | ||
128 | { | ||
129 | $this->assertTrue(SessionManager::checkId(sha1('shaarli'))); | ||
130 | } | ||
131 | |||
132 | /** | ||
133 | * Test checkId with a valid ID - SHA-256 hashes | ||
134 | */ | ||
135 | public function testIsSha256SessionIdValid() | ||
136 | { | ||
137 | $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli'))); | ||
138 | } | ||
139 | |||
140 | /** | ||
141 | * Test checkId with a valid ID - SHA-512 hashes | ||
142 | */ | ||
143 | public function testIsSha512SessionIdValid() | ||
144 | { | ||
145 | $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli'))); | ||
146 | } | ||
147 | |||
148 | /** | ||
149 | * Test checkId with invalid IDs. | ||
150 | */ | ||
151 | public function testIsSessionIdInvalid() | ||
152 | { | ||
153 | $this->assertFalse(SessionManager::checkId('')); | ||
154 | $this->assertFalse(SessionManager::checkId([])); | ||
155 | $this->assertFalse( | ||
156 | SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=') | ||
157 | ); | ||
158 | } | ||
159 | |||
160 | /** | ||
161 | * Store login information after a successful login | ||
162 | */ | ||
163 | public function testStoreLoginInfo() | ||
164 | { | ||
165 | $this->sessionManager->storeLoginInfo('ip_id'); | ||
166 | |||
167 | $this->assertGreaterThan(time(), $this->session['expires_on']); | ||
168 | $this->assertEquals('ip_id', $this->session['ip']); | ||
169 | $this->assertEquals('johndoe', $this->session['username']); | ||
170 | } | ||
171 | |||
172 | /** | ||
173 | * Extend a server-side session by SessionManager::$SHORT_TIMEOUT | ||
174 | */ | ||
175 | public function testExtendSession() | ||
176 | { | ||
177 | $this->sessionManager->extendSession(); | ||
178 | |||
179 | $this->assertGreaterThan(time(), $this->session['expires_on']); | ||
180 | $this->assertLessThanOrEqual( | ||
181 | time() + SessionManager::$SHORT_TIMEOUT, | ||
182 | $this->session['expires_on'] | ||
183 | ); | ||
184 | } | ||
185 | |||
186 | /** | ||
187 | * Extend a server-side session by SessionManager::$LONG_TIMEOUT | ||
188 | */ | ||
189 | public function testExtendSessionStaySignedIn() | ||
190 | { | ||
191 | $this->sessionManager->setStaySignedIn(true); | ||
192 | $this->sessionManager->extendSession(); | ||
193 | |||
194 | $this->assertGreaterThan(time(), $this->session['expires_on']); | ||
195 | $this->assertGreaterThan( | ||
196 | time() + SessionManager::$LONG_TIMEOUT - 10, | ||
197 | $this->session['expires_on'] | ||
198 | ); | ||
199 | $this->assertLessThanOrEqual( | ||
200 | time() + SessionManager::$LONG_TIMEOUT, | ||
201 | $this->session['expires_on'] | ||
202 | ); | ||
203 | } | ||
204 | |||
205 | /** | ||
206 | * Unset session variables after logging out | ||
207 | */ | ||
208 | public function testLogout() | ||
209 | { | ||
210 | $this->session = [ | ||
211 | 'ip' => 'ip_id', | ||
212 | 'expires_on' => time() + 1000, | ||
213 | 'username' => 'johndoe', | ||
214 | 'visibility' => 'public', | ||
215 | 'untaggedonly' => false, | ||
216 | ]; | ||
217 | $this->sessionManager->logout(); | ||
218 | |||
219 | $this->assertFalse(isset($this->session['ip'])); | ||
220 | $this->assertFalse(isset($this->session['expires_on'])); | ||
221 | $this->assertFalse(isset($this->session['username'])); | ||
222 | $this->assertFalse(isset($this->session['visibility'])); | ||
223 | $this->assertFalse(isset($this->session['untaggedonly'])); | ||
224 | } | ||
225 | |||
226 | /** | ||
227 | * The session is active and expiration time has been reached | ||
228 | */ | ||
229 | public function testHasExpiredTimeElapsed() | ||
230 | { | ||
231 | $this->session['expires_on'] = time() - 10; | ||
232 | |||
233 | $this->assertTrue($this->sessionManager->hasSessionExpired()); | ||
234 | } | ||
235 | |||
236 | /** | ||
237 | * The session is active and expiration time has not been reached | ||
238 | */ | ||
239 | public function testHasNotExpired() | ||
240 | { | ||
241 | $this->session['expires_on'] = time() + 1000; | ||
242 | |||
243 | $this->assertFalse($this->sessionManager->hasSessionExpired()); | ||
244 | } | ||
245 | |||
246 | /** | ||
247 | * Session hijacking protection is disabled, we assume the IP has not changed | ||
248 | */ | ||
249 | public function testHasClientIpChangedNoSessionProtection() | ||
250 | { | ||
251 | $this->conf->set('security.session_protection_disabled', true); | ||
252 | |||
253 | $this->assertFalse($this->sessionManager->hasClientIpChanged('')); | ||
254 | } | ||
255 | |||
256 | /** | ||
257 | * The client IP identifier has not changed | ||
258 | */ | ||
259 | public function testHasClientIpChangedNope() | ||
260 | { | ||
261 | $this->session['ip'] = 'ip_id'; | ||
262 | $this->assertFalse($this->sessionManager->hasClientIpChanged('ip_id')); | ||
263 | } | ||
264 | |||
265 | /** | ||
266 | * The client IP identifier has changed | ||
267 | */ | ||
268 | public function testHasClientIpChanged() | ||
269 | { | ||
270 | $this->session['ip'] = 'ip_id_one'; | ||
271 | $this->assertTrue($this->sessionManager->hasClientIpChanged('ip_id_two')); | ||
272 | } | ||
273 | } | ||
diff --git a/tests/utils/FakeConfigManager.php b/tests/utils/FakeConfigManager.php index 85434de7..360b34a9 100644 --- a/tests/utils/FakeConfigManager.php +++ b/tests/utils/FakeConfigManager.php | |||
@@ -42,4 +42,16 @@ class FakeConfigManager | |||
42 | } | 42 | } |
43 | return $key; | 43 | return $key; |
44 | } | 44 | } |
45 | |||
46 | /** | ||
47 | * Check if a setting exists | ||
48 | * | ||
49 | * @param string $setting Asked setting, keys separated with dots | ||
50 | * | ||
51 | * @return bool true if the setting exists, false otherwise | ||
52 | */ | ||
53 | public function exists($setting) | ||
54 | { | ||
55 | return array_key_exists($setting, $this->values); | ||
56 | } | ||
45 | } | 57 | } |
diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index d546be0a..322cddd5 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html | |||
@@ -136,7 +136,7 @@ | |||
136 | <div class="linklist-item-thumbnail">{$thumb}</div> | 136 | <div class="linklist-item-thumbnail">{$thumb}</div> |
137 | {/if} | 137 | {/if} |
138 | 138 | ||
139 | {if="isLoggedIn()"} | 139 | {if="$is_logged_in"} |
140 | <div class="linklist-item-editbuttons"> | 140 | <div class="linklist-item-editbuttons"> |
141 | {if="$value.private"} | 141 | {if="$value.private"} |
142 | <span class="label label-private">{$strPrivate}</span> | 142 | <span class="label label-private">{$strPrivate}</span> |
@@ -179,7 +179,7 @@ | |||
179 | 179 | ||
180 | <div class="linklist-item-infos-date-url-block pure-g"> | 180 | <div class="linklist-item-infos-date-url-block pure-g"> |
181 | <div class="linklist-item-infos-dateblock pure-u-lg-7-12 pure-u-1"> | 181 | <div class="linklist-item-infos-dateblock pure-u-lg-7-12 pure-u-1"> |
182 | {if="isLoggedIn()"} | 182 | {if="$is_logged_in"} |
183 | <div class="linklist-item-infos-controls-group pure-u-0 pure-u-lg-visible"> | 183 | <div class="linklist-item-infos-controls-group pure-u-0 pure-u-lg-visible"> |
184 | <span class="linklist-item-infos-controls-item ctrl-checkbox"> | 184 | <span class="linklist-item-infos-controls-item ctrl-checkbox"> |
185 | <input type="checkbox" class="delete-checkbox" value="{$value.id}"> | 185 | <input type="checkbox" class="delete-checkbox" value="{$value.id}"> |
@@ -196,7 +196,7 @@ | |||
196 | </div> | 196 | </div> |
197 | {/if} | 197 | {/if} |
198 | <a href="?{$value.shorturl}" title="{$strPermalink}"> | 198 | <a href="?{$value.shorturl}" title="{$strPermalink}"> |
199 | {if="!$hide_timestamps || isLoggedIn()"} | 199 | {if="!$hide_timestamps || $is_logged_in"} |
200 | {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink} | 200 | {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink} |
201 | <span class="linkdate" title="{$updated}"> | 201 | <span class="linkdate" title="{$updated}"> |
202 | <i class="fa fa-clock-o"></i> | 202 | <i class="fa fa-clock-o"></i> |
@@ -236,7 +236,7 @@ | |||
236 | {if="$link_plugin_counter - 1 != $counter"}·{/if} | 236 | {if="$link_plugin_counter - 1 != $counter"}·{/if} |
237 | {/loop} | 237 | {/loop} |
238 | {/if} | 238 | {/if} |
239 | {if="isLoggedIn()"} | 239 | {if="$is_logged_in"} |
240 | · | 240 | · |
241 | <a href="?delete_link&lf_linkdate={$value.id}&token={$token}" | 241 | <a href="?delete_link&lf_linkdate={$value.id}&token={$token}" |
242 | title="{$strDelete}" class="delete-link confirm-delete"> | 242 | title="{$strDelete}" class="delete-link confirm-delete"> |
diff --git a/tpl/default/linklist.paging.html b/tpl/default/linklist.paging.html index 72bdd931..5309e348 100644 --- a/tpl/default/linklist.paging.html +++ b/tpl/default/linklist.paging.html | |||
@@ -1,11 +1,11 @@ | |||
1 | <div class="linklist-paging"> | 1 | <div class="linklist-paging"> |
2 | <div class="paging pure-g"> | 2 | <div class="paging pure-g"> |
3 | <div class="linklist-filters pure-u-1-3"> | 3 | <div class="linklist-filters pure-u-1-3"> |
4 | {if="isLoggedIn() or !empty($action_plugin)"} | 4 | {if="$is_logged_in or !empty($action_plugin)"} |
5 | <span class="linklist-filters-text pure-u-0 pure-u-lg-visible"> | 5 | <span class="linklist-filters-text pure-u-0 pure-u-lg-visible"> |
6 | {'Filters'|t} | 6 | {'Filters'|t} |
7 | </span> | 7 | </span> |
8 | {if="isLoggedIn()"} | 8 | {if="$is_logged_in"} |
9 | <a href="?visibility=private" title="{'Only display private links'|t}" | 9 | <a href="?visibility=private" title="{'Only display private links'|t}" |
10 | class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}" | 10 | class="{if="$visibility==='private'"}filter-on{else}filter-off{/if}" |
11 | ><i class="fa fa-user-secret"></i></a> | 11 | ><i class="fa fa-user-secret"></i></a> |
diff --git a/tpl/default/page.footer.html b/tpl/default/page.footer.html index 34193743..5af39be7 100644 --- a/tpl/default/page.footer.html +++ b/tpl/default/page.footer.html | |||
@@ -4,7 +4,7 @@ | |||
4 | <div class="pure-u-2-24"></div> | 4 | <div class="pure-u-2-24"></div> |
5 | <div id="footer" class="pure-u-20-24 footer-container"> | 5 | <div id="footer" class="pure-u-20-24 footer-container"> |
6 | <strong><a href="https://github.com/shaarli/Shaarli">Shaarli</a></strong> | 6 | <strong><a href="https://github.com/shaarli/Shaarli">Shaarli</a></strong> |
7 | {if="isLoggedIn()===true"} | 7 | {if="$is_logged_in===true"} |
8 | {$version} | 8 | {$version} |
9 | {/if} | 9 | {/if} |
10 | · | 10 | · |
diff --git a/tpl/default/page.header.html b/tpl/default/page.header.html index 18aa77c8..82568d63 100644 --- a/tpl/default/page.header.html +++ b/tpl/default/page.header.html | |||
@@ -17,7 +17,7 @@ | |||
17 | {$shaarlititle} | 17 | {$shaarlititle} |
18 | </a> | 18 | </a> |
19 | </li> | 19 | </li> |
20 | {if="isLoggedIn() || $openshaarli"} | 20 | {if="$is_logged_in || $openshaarli"} |
21 | <li class="pure-menu-item"> | 21 | <li class="pure-menu-item"> |
22 | <a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare"> | 22 | <a href="?do=addlink" class="pure-menu-link" id="shaarli-menu-shaare"> |
23 | <i class="fa fa-plus" ></i> {'Shaare'|t} | 23 | <i class="fa fa-plus" ></i> {'Shaare'|t} |
@@ -50,7 +50,7 @@ | |||
50 | <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss"> | 50 | <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-rss"> |
51 | <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a> | 51 | <a href="?do={$feed_type}{$searchcrits}" class="pure-menu-link">{'RSS Feed'|t}</a> |
52 | </li> | 52 | </li> |
53 | {if="isLoggedIn()"} | 53 | {if="$is_logged_in"} |
54 | <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout"> | 54 | <li class="pure-menu-item pure-u-lg-0 shaarli-menu-mobile" id="shaarli-menu-mobile-logout"> |
55 | <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a> | 55 | <a href="?do=logout" class="pure-menu-link">{'Logout'|t}</a> |
56 | </li> | 56 | </li> |
@@ -74,7 +74,7 @@ | |||
74 | <i class="fa fa-rss"></i> | 74 | <i class="fa fa-rss"></i> |
75 | </a> | 75 | </a> |
76 | </li> | 76 | </li> |
77 | {if="!isLoggedIn()"} | 77 | {if="!$is_logged_in"} |
78 | <li class="pure-menu-item" id="shaarli-menu-desktop-login"> | 78 | <li class="pure-menu-item" id="shaarli-menu-desktop-login"> |
79 | <a href="?do=login" class="pure-menu-link" | 79 | <a href="?do=login" class="pure-menu-link" |
80 | data-open-id="header-login-form" | 80 | data-open-id="header-login-form" |
@@ -120,7 +120,7 @@ | |||
120 | </div> | 120 | </div> |
121 | </div> | 121 | </div> |
122 | </div> | 122 | </div> |
123 | {if="!isLoggedIn()"} | 123 | {if="!$is_logged_in"} |
124 | <form method="post" name="loginform"> | 124 | <form method="post" name="loginform"> |
125 | <div class="subheader-form header-login-form" id="header-login-form"> | 125 | <div class="subheader-form header-login-form" id="header-login-form"> |
126 | <input type="text" name="login" placeholder="{'Username'|t}" tabindex="3"> | 126 | <input type="text" name="login" placeholder="{'Username'|t}" tabindex="3"> |
@@ -155,7 +155,7 @@ | |||
155 | </div> | 155 | </div> |
156 | {/if} | 156 | {/if} |
157 | 157 | ||
158 | {if="!empty($plugin_errors) && isLoggedIn()"} | 158 | {if="!empty($plugin_errors) && $is_logged_in"} |
159 | <div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert"> | 159 | <div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert"> |
160 | <div class="pure-u-2-24"></div> | 160 | <div class="pure-u-2-24"></div> |
161 | <div class="pure-u-20-24"> | 161 | <div class="pure-u-20-24"> |
diff --git a/tpl/default/tag.list.html b/tpl/default/tag.list.html index 772d6ad3..bcddcd56 100644 --- a/tpl/default/tag.list.html +++ b/tpl/default/tag.list.html | |||
@@ -49,7 +49,7 @@ | |||
49 | {loop="tags"} | 49 | {loop="tags"} |
50 | <div class="tag-list-item pure-g" data-tag="{$key}"> | 50 | <div class="tag-list-item pure-g" data-tag="{$key}"> |
51 | <div class="pure-u-1"> | 51 | <div class="pure-u-1"> |
52 | {if="isLoggedIn()===true"} | 52 | {if="$is_logged_in===true"} |
53 | <a href="#" class="delete-tag"><i class="fa fa-trash"></i></a> | 53 | <a href="#" class="delete-tag"><i class="fa fa-trash"></i></a> |
54 | <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag"> | 54 | <a href="?do=changetag&fromtag={$key|urlencode}" class="rename-tag"> |
55 | <i class="fa fa-pencil-square-o {$key}"></i> | 55 | <i class="fa fa-pencil-square-o {$key}"></i> |
@@ -63,7 +63,7 @@ | |||
63 | {$value} | 63 | {$value} |
64 | {/loop} | 64 | {/loop} |
65 | </div> | 65 | </div> |
66 | {if="isLoggedIn()===true"} | 66 | {if="$is_logged_in===true"} |
67 | <div class="rename-tag-form pure-u-1"> | 67 | <div class="rename-tag-form pure-u-1"> |
68 | <input type="text" name="{$key}" value="{$key}" class="rename-tag-input" /> | 68 | <input type="text" name="{$key}" value="{$key}" class="rename-tag-input" /> |
69 | <a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a> | 69 | <a href="#" class="validate-rename-tag"><i class="fa fa-check"></i></a> |
@@ -81,7 +81,7 @@ | |||
81 | </div> | 81 | </div> |
82 | </div> | 82 | </div> |
83 | 83 | ||
84 | {if="isLoggedIn()===true"} | 84 | {if="$is_logged_in===true"} |
85 | <input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}" | 85 | <input type="hidden" name="taglist" value="{loop="$tags"}{$key} {/loop}" |
86 | {/if} | 86 | {/if} |
87 | 87 | ||
diff --git a/tpl/vintage/daily.html b/tpl/vintage/daily.html index 42db16a7..ede35910 100644 --- a/tpl/vintage/daily.html +++ b/tpl/vintage/daily.html | |||
@@ -53,7 +53,7 @@ | |||
53 | <img src="img/squiggle.png" width="25" height="26" title="permalink" alt="permalink"> | 53 | <img src="img/squiggle.png" width="25" height="26" title="permalink" alt="permalink"> |
54 | </a> | 54 | </a> |
55 | </div> | 55 | </div> |
56 | {if="!$hide_timestamps || isLoggedIn()"} | 56 | {if="!$hide_timestamps || $is_logged_in"} |
57 | <div class="dailyEntryLinkdate"> | 57 | <div class="dailyEntryLinkdate"> |
58 | <a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a> | 58 | <a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a> |
59 | </div> | 59 | </div> |
diff --git a/tpl/vintage/linklist.html b/tpl/vintage/linklist.html index e7137246..1ca51be3 100644 --- a/tpl/vintage/linklist.html +++ b/tpl/vintage/linklist.html | |||
@@ -82,7 +82,7 @@ | |||
82 | <a id="{$value.shorturl}"></a> | 82 | <a id="{$value.shorturl}"></a> |
83 | <div class="thumbnail">{$value.url|thumbnail}</div> | 83 | <div class="thumbnail">{$value.url|thumbnail}</div> |
84 | <div class="linkcontainer"> | 84 | <div class="linkcontainer"> |
85 | {if="isLoggedIn()"} | 85 | {if="$is_logged_in"} |
86 | <div class="linkeditbuttons"> | 86 | <div class="linkeditbuttons"> |
87 | <form method="GET" class="buttoneditform"> | 87 | <form method="GET" class="buttoneditform"> |
88 | <input type="hidden" name="edit_link" value="{$value.id}"> | 88 | <input type="hidden" name="edit_link" value="{$value.id}"> |
@@ -102,7 +102,7 @@ | |||
102 | </span> | 102 | </span> |
103 | <br> | 103 | <br> |
104 | {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if} | 104 | {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if} |
105 | {if="!$hide_timestamps || isLoggedIn()"} | 105 | {if="!$hide_timestamps || $is_logged_in"} |
106 | {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} | 106 | {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'} |
107 | <span class="linkdate" title="Permalink"> | 107 | <span class="linkdate" title="Permalink"> |
108 | <a href="?{$value.shorturl}"> | 108 | <a href="?{$value.shorturl}"> |
diff --git a/tpl/vintage/linklist.paging.html b/tpl/vintage/linklist.paging.html index e3b88ee6..35149a6b 100644 --- a/tpl/vintage/linklist.paging.html +++ b/tpl/vintage/linklist.paging.html | |||
@@ -1,5 +1,5 @@ | |||
1 | <div class="paging"> | 1 | <div class="paging"> |
2 | {if="isLoggedIn()"} | 2 | {if="$is_logged_in"} |
3 | <div class="paging_privatelinks"> | 3 | <div class="paging_privatelinks"> |
4 | <a href="?visibility=private"> | 4 | <a href="?visibility=private"> |
5 | {if="$visibility=='private'"} | 5 | {if="$visibility=='private'"} |
diff --git a/tpl/vintage/page.footer.html b/tpl/vintage/page.footer.html index 1485e1ce..f409721e 100644 --- a/tpl/vintage/page.footer.html +++ b/tpl/vintage/page.footer.html | |||
@@ -25,7 +25,7 @@ | |||
25 | 25 | ||
26 | <script src="js/shaarli.min.js"></script> | 26 | <script src="js/shaarli.min.js"></script> |
27 | 27 | ||
28 | {if="isLoggedIn()"} | 28 | {if="$is_logged_in"} |
29 | <script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script> | 29 | <script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script> |
30 | {/if} | 30 | {/if} |
31 | 31 | ||
diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html index 8a58844e..40c53e5b 100644 --- a/tpl/vintage/page.header.html +++ b/tpl/vintage/page.header.html | |||
@@ -17,7 +17,7 @@ | |||
17 | {ignore} When called as a popup from bookmarklet, do not display menu. {/ignore} | 17 | {ignore} When called as a popup from bookmarklet, do not display menu. {/ignore} |
18 | {else} | 18 | {else} |
19 | <li><a href="{$titleLink}" class="nomobile">Home</a></li> | 19 | <li><a href="{$titleLink}" class="nomobile">Home</a></li> |
20 | {if="isLoggedIn()"} | 20 | {if="$is_logged_in"} |
21 | <li><a href="?do=logout">Logout</a></li> | 21 | <li><a href="?do=logout">Logout</a></li> |
22 | <li><a href="?do=tools">Tools</a></li> | 22 | <li><a href="?do=tools">Tools</a></li> |
23 | <li><a href="?do=addlink">Add link</a></li> | 23 | <li><a href="?do=addlink">Add link</a></li> |
@@ -46,7 +46,7 @@ | |||
46 | </ul> | 46 | </ul> |
47 | </div> | 47 | </div> |
48 | 48 | ||
49 | {if="!empty($plugin_errors) && isLoggedIn()"} | 49 | {if="!empty($plugin_errors) && $is_logged_in"} |
50 | <ul class="errors"> | 50 | <ul class="errors"> |
51 | {loop="$plugin_errors"} | 51 | {loop="$plugin_errors"} |
52 | <li>{$value}</li> | 52 | <li>{$value}</li> |