aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/HttpUtils.php33
-rw-r--r--application/LoginManager.php134
-rw-r--r--application/PageBuilder.php9
-rw-r--r--application/SessionManager.php83
-rw-r--r--application/security/LoginManager.php265
-rw-r--r--application/security/SessionManager.php199
-rw-r--r--composer.json3
-rw-r--r--index.php240
-rw-r--r--tests/HttpUtils/ClientIpIdTest.php52
-rw-r--r--tests/SessionManagerTest.php149
-rw-r--r--tests/security/LoginManagerTest.php (renamed from tests/LoginManagerTest.php)183
-rw-r--r--tests/security/SessionManagerTest.php273
-rw-r--r--tests/utils/FakeConfigManager.php12
-rw-r--r--tpl/default/linklist.html8
-rw-r--r--tpl/default/linklist.paging.html4
-rw-r--r--tpl/default/page.footer.html2
-rw-r--r--tpl/default/page.header.html10
-rw-r--r--tpl/default/tag.list.html6
-rw-r--r--tpl/vintage/daily.html2
-rw-r--r--tpl/vintage/linklist.html4
-rw-r--r--tpl/vintage/linklist.paging.html2
-rw-r--r--tpl/vintage/page.footer.html2
-rw-r--r--tpl/vintage/page.header.html4
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 */
435function 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
2namespace Shaarli;
3
4/**
5 * User login management
6 */
7class 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
2namespace Shaarli;
3
4/**
5 * Manages the server-side session
6 */
7class 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
2namespace Shaarli\Security;
3
4use Shaarli\Config\ConfigManager;
5
6/**
7 * User login management
8 */
9class 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
2namespace Shaarli\Security;
3
4use Shaarli\Config\ConfigManager;
5
6/**
7 * Manages the server-side session
8 */
9class 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}
diff --git a/index.php b/index.php
index 2fe3f821..c34434dd 100644
--- a/index.php
+++ b/index.php
@@ -78,8 +78,8 @@ require_once 'application/Updater.php';
78use \Shaarli\Languages; 78use \Shaarli\Languages;
79use \Shaarli\ThemeUtils; 79use \Shaarli\ThemeUtils;
80use \Shaarli\Config\ConfigManager; 80use \Shaarli\Config\ConfigManager;
81use \Shaarli\LoginManager; 81use \Shaarli\Security\LoginManager;
82use \Shaarli\SessionManager; 82use \Shaarli\Security\SessionManager;
83 83
84// Ensure the PHP version is supported 84// Ensure the PHP version is supported
85try { 85try {
@@ -101,8 +101,6 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
101// Set default cookie expiration and path. 101// Set default cookie expiration and path.
102session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']); 102session_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.
105define('INACTIVITY_TIMEOUT', 3600); // in seconds.
106// Use cookies to store session. 104// Use cookies to store session.
107ini_set('session.use_cookies', 1); 105ini_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.
130if (! defined('LC_MESSAGES')) { 130if (! 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);
181define('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 */
190function 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.)
235function 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 */
249function 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 */
266function 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.
280function isLoggedIn() 189function isLoggedIn()
281{ 190{
282 global $userIsLoggedIn; 191 global $loginManager;
283 return $userIsLoggedIn; 192 return $loginManager->isLoggedIn();
284} 193}
285 194
286// Force logout.
287function 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.
300if (isset($_POST['login'])) 198if (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 */
385function showDailyRSS($conf) { 290function 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 */
487function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) 393function 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 */
589function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) { 495function 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 */
1596function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) 1504function 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
6require_once 'application/HttpUtils.php';
7
8/**
9 * Unitary tests for client_ip_id()
10 */
11class 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
2require_once 'tests/utils/FakeConfigManager.php';
3
4// Initialize reference data _before_ PHPUnit starts a session
5require_once 'tests/utils/ReferenceSessionIdHashes.php';
6ReferenceSessionIdHashes::genAllHashes();
7
8use \Shaarli\SessionManager;
9use \PHPUnit\Framework\TestCase;
10
11
12/**
13 * Test coverage for SessionManager
14 */
15class 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
2namespace Shaarli; 2namespace Shaarli\Security;
3 3
4require_once 'tests/utils/FakeConfigManager.php'; 4require_once 'tests/utils/FakeConfigManager.php';
5use \PHPUnit\Framework\TestCase; 5use \PHPUnit\Framework\TestCase;
@@ -9,15 +9,54 @@ use \PHPUnit\Framework\TestCase;
9 */ 9 */
10class LoginManagerTest extends TestCase 10class 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
2require_once 'tests/utils/FakeConfigManager.php';
3
4// Initialize reference data _before_ PHPUnit starts a session
5require_once 'tests/utils/ReferenceSessionIdHashes.php';
6ReferenceSessionIdHashes::genAllHashes();
7
8use \Shaarli\Security\SessionManager;
9use \PHPUnit\Framework\TestCase;
10
11
12/**
13 * Test coverage for SessionManager
14 */
15class 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"}&middot;{/if} 236 {if="$link_plugin_counter - 1 != $counter"}&middot;{/if}
237 {/loop} 237 {/loop}
238 {/if} 238 {/if}
239 {if="isLoggedIn()"} 239 {if="$is_logged_in"}
240 &middot; 240 &middot;
241 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;token={$token}" 241 <a href="?delete_link&amp;lf_linkdate={$value.id}&amp;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 &middot; 10 &middot;
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>&nbsp;&nbsp; 53 <a href="#" class="delete-tag"><i class="fa fa-trash"></i></a>&nbsp;&nbsp;
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>