aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/LoginManager.php134
-rw-r--r--index.php116
-rw-r--r--tests/LoginManagerTest.php199
-rw-r--r--tests/utils/FakeConfigManager.php35
-rw-r--r--tpl/default/loginform.html2
-rw-r--r--tpl/vintage/loginform.html2
6 files changed, 385 insertions, 103 deletions
diff --git a/application/LoginManager.php b/application/LoginManager.php
new file mode 100644
index 00000000..397bc6e3
--- /dev/null
+++ b/application/LoginManager.php
@@ -0,0 +1,134 @@
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/index.php b/index.php
index 067b8fcb..91c3f07e 100644
--- a/index.php
+++ b/index.php
@@ -78,6 +78,7 @@ 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\SessionManager; 82use \Shaarli\SessionManager;
82 83
83// Ensure the PHP version is supported 84// Ensure the PHP version is supported
@@ -122,6 +123,7 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli']))
122} 123}
123 124
124$conf = new ConfigManager(); 125$conf = new ConfigManager();
126$loginManager = new LoginManager($GLOBALS, $conf);
125$sessionManager = new SessionManager($_SESSION, $conf); 127$sessionManager = new SessionManager($_SESSION, $conf);
126 128
127// 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.
@@ -293,108 +295,22 @@ function logout() {
293 setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH); 295 setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH);
294} 296}
295 297
296
297// ------------------------------------------------------------------------------------------
298// Brute force protection system
299// Several consecutive failed logins will ban the IP address for 30 minutes.
300if (!is_file($conf->get('resource.ban_file', 'data/ipbans.php'))) {
301 // FIXME! globals
302 file_put_contents(
303 $conf->get('resource.ban_file', 'data/ipbans.php'),
304 "<?php\n\$GLOBALS['IPBANS']=".var_export(array('FAILURES'=>array(),'BANS'=>array()),true).";\n?>"
305 );
306}
307include $conf->get('resource.ban_file', 'data/ipbans.php');
308/**
309 * Signal a failed login. Will ban the IP if too many failures:
310 *
311 * @param ConfigManager $conf Configuration Manager instance.
312 */
313function ban_loginFailed($conf)
314{
315 $ip = $_SERVER['REMOTE_ADDR'];
316 $trusted = $conf->get('security.trusted_proxies', array());
317 if (in_array($ip, $trusted)) {
318 $ip = getIpAddressFromProxy($_SERVER, $trusted);
319 if (!$ip) {
320 return;
321 }
322 }
323 $gb = $GLOBALS['IPBANS'];
324 if (! isset($gb['FAILURES'][$ip])) {
325 $gb['FAILURES'][$ip]=0;
326 }
327 $gb['FAILURES'][$ip]++;
328 if ($gb['FAILURES'][$ip] > ($conf->get('security.ban_after') - 1))
329 {
330 $gb['BANS'][$ip] = time() + $conf->get('security.ban_after', 1800);
331 logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'IP address banned from login');
332 }
333 $GLOBALS['IPBANS'] = $gb;
334 file_put_contents(
335 $conf->get('resource.ban_file', 'data/ipbans.php'),
336 "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>"
337 );
338}
339
340/**
341 * Signals a successful login. Resets failed login counter.
342 *
343 * @param ConfigManager $conf Configuration Manager instance.
344 */
345function ban_loginOk($conf)
346{
347 $ip = $_SERVER['REMOTE_ADDR'];
348 $gb = $GLOBALS['IPBANS'];
349 unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]);
350 $GLOBALS['IPBANS'] = $gb;
351 file_put_contents(
352 $conf->get('resource.ban_file', 'data/ipbans.php'),
353 "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>"
354 );
355}
356
357/**
358 * Checks if the user CAN login. If 'true', the user can try to login.
359 *
360 * @param ConfigManager $conf Configuration Manager instance.
361 *
362 * @return bool: true if the user is allowed to login.
363 */
364function ban_canLogin($conf)
365{
366 $ip=$_SERVER["REMOTE_ADDR"]; $gb=$GLOBALS['IPBANS'];
367 if (isset($gb['BANS'][$ip]))
368 {
369 // User is banned. Check if the ban has expired:
370 if ($gb['BANS'][$ip]<=time())
371 { // Ban expired, user can try to login again.
372 logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Ban lifted.');
373 unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]);
374 file_put_contents(
375 $conf->get('resource.ban_file', 'data/ipbans.php'),
376 "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>"
377 );
378 return true; // Ban has expired, user can login.
379 }
380 return false; // User is banned.
381 }
382 return true; // User is not banned.
383}
384
385// ------------------------------------------------------------------------------------------ 298// ------------------------------------------------------------------------------------------
386// Process login form: Check if login/password is correct. 299// Process login form: Check if login/password is correct.
387if (isset($_POST['login'])) 300if (isset($_POST['login']))
388{ 301{
389 if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.')); 302 if (! $loginManager->canLogin($_SERVER)) {
303 die(t('I said: NO. You are banned for the moment. Go away.'));
304 }
390 if (isset($_POST['password']) 305 if (isset($_POST['password'])
391 && $sessionManager->checkToken($_POST['token']) 306 && $sessionManager->checkToken($_POST['token'])
392 && (check_auth($_POST['login'], $_POST['password'], $conf)) 307 && (check_auth($_POST['login'], $_POST['password'], $conf))
393 ) { // Login/password is OK. 308 ) {
394 ban_loginOk($conf); 309 // Login/password is OK.
310 $loginManager->handleSuccessfulLogin($_SERVER);
311
395 // If user wants to keep the session cookie even after the browser closes: 312 // If user wants to keep the session cookie even after the browser closes:
396 if (!empty($_POST['longlastingsession'])) 313 if (!empty($_POST['longlastingsession'])) {
397 {
398 $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year) 314 $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year)
399 $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now) 315 $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now)
400 setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH); 316 setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH);
@@ -437,10 +353,8 @@ if (isset($_POST['login']))
437 } 353 }
438 } 354 }
439 header('Location: ?'); exit; 355 header('Location: ?'); exit;
440 } 356 } else {
441 else 357 $loginManager->handleFailedLogin($_SERVER);
442 {
443 ban_loginFailed($conf);
444 $redir = '&username='. urlencode($_POST['login']); 358 $redir = '&username='. urlencode($_POST['login']);
445 if (isset($_GET['post'])) { 359 if (isset($_GET['post'])) {
446 $redir .= '&post=' . urlencode($_GET['post']); 360 $redir .= '&post=' . urlencode($_GET['post']);
@@ -684,8 +598,9 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
684 * @param LinkDB $LINKSDB 598 * @param LinkDB $LINKSDB
685 * @param History $history instance 599 * @param History $history instance
686 * @param SessionManager $sessionManager SessionManager instance 600 * @param SessionManager $sessionManager SessionManager instance
601 * @param LoginManager $loginManager LoginManager instance
687 */ 602 */
688function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager) 603function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $loginManager)
689{ 604{
690 $updater = new Updater( 605 $updater = new Updater(
691 read_updates_file($conf->get('resource.updates')), 606 read_updates_file($conf->get('resource.updates')),
@@ -761,6 +676,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
761 $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):'')); 676 $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):''));
762 // add default state of the 'remember me' checkbox 677 // add default state of the 'remember me' checkbox
763 $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default')); 678 $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default'));
679 $PAGE->assign('user_can_login', $loginManager->canLogin($_SERVER));
764 $PAGE->renderPage('loginform'); 680 $PAGE->renderPage('loginform');
765 exit; 681 exit;
766 } 682 }
@@ -2330,7 +2246,7 @@ $response = $app->run(true);
2330if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) { 2246if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
2331 // We use UTF-8 for proper international characters handling. 2247 // We use UTF-8 for proper international characters handling.
2332 header('Content-Type: text/html; charset=utf-8'); 2248 header('Content-Type: text/html; charset=utf-8');
2333 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager); 2249 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);
2334} else { 2250} else {
2335 $app->respond($response); 2251 $app->respond($response);
2336} 2252}
diff --git a/tests/LoginManagerTest.php b/tests/LoginManagerTest.php
new file mode 100644
index 00000000..4159038e
--- /dev/null
+++ b/tests/LoginManagerTest.php
@@ -0,0 +1,199 @@
1<?php
2namespace Shaarli;
3
4require_once 'tests/utils/FakeConfigManager.php';
5use \PHPUnit\Framework\TestCase;
6
7/**
8 * Test coverage for LoginManager
9 */
10class LoginManagerTest extends TestCase
11{
12 protected $configManager = null;
13 protected $loginManager = null;
14 protected $banFile = 'sandbox/ipbans.php';
15 protected $logFile = 'sandbox/shaarli.log';
16 protected $globals = [];
17 protected $ipAddr = '127.0.0.1';
18 protected $server = [];
19 protected $trustedProxy = '10.1.1.100';
20
21 /**
22 * Prepare or reset test resources
23 */
24 public function setUp()
25 {
26 if (file_exists($this->banFile)) {
27 unlink($this->banFile);
28 }
29
30 $this->configManager = new \FakeConfigManager([
31 'resource.ban_file' => $this->banFile,
32 'resource.log' => $this->logFile,
33 'security.ban_after' => 4,
34 'security.ban_duration' => 3600,
35 'security.trusted_proxies' => [$this->trustedProxy],
36 ]);
37
38 $this->globals = &$GLOBALS;
39 unset($this->globals['IPBANS']);
40
41 $this->loginManager = new LoginManager($this->globals, $this->configManager);
42 $this->server['REMOTE_ADDR'] = $this->ipAddr;
43 }
44
45 /**
46 * Wipe test resources
47 */
48 public function tearDown()
49 {
50 unset($this->globals['IPBANS']);
51 }
52
53 /**
54 * Instantiate a LoginManager and load ban records
55 */
56 public function testReadBanFile()
57 {
58 file_put_contents(
59 $this->banFile,
60 "<?php\n\$GLOBALS['IPBANS']=array('FAILURES' => array('127.0.0.1' => 99));\n?>"
61 );
62 new LoginManager($this->globals, $this->configManager);
63 $this->assertEquals(99, $this->globals['IPBANS']['FAILURES']['127.0.0.1']);
64 }
65
66 /**
67 * Record a failed login attempt
68 */
69 public function testHandleFailedLogin()
70 {
71 $this->loginManager->handleFailedLogin($this->server);
72 $this->assertEquals(1, $this->globals['IPBANS']['FAILURES'][$this->ipAddr]);
73
74 $this->loginManager->handleFailedLogin($this->server);
75 $this->assertEquals(2, $this->globals['IPBANS']['FAILURES'][$this->ipAddr]);
76 }
77
78 /**
79 * Record a failed login attempt - IP behind a trusted proxy
80 */
81 public function testHandleFailedLoginBehindTrustedProxy()
82 {
83 $server = [
84 'REMOTE_ADDR' => $this->trustedProxy,
85 'HTTP_X_FORWARDED_FOR' => $this->ipAddr,
86 ];
87 $this->loginManager->handleFailedLogin($server);
88 $this->assertEquals(1, $this->globals['IPBANS']['FAILURES'][$this->ipAddr]);
89
90 $this->loginManager->handleFailedLogin($server);
91 $this->assertEquals(2, $this->globals['IPBANS']['FAILURES'][$this->ipAddr]);
92 }
93
94 /**
95 * Record a failed login attempt - IP behind a trusted proxy but not forwarded
96 */
97 public function testHandleFailedLoginBehindTrustedProxyNoIp()
98 {
99 $server = [
100 'REMOTE_ADDR' => $this->trustedProxy,
101 ];
102 $this->loginManager->handleFailedLogin($server);
103 $this->assertFalse(isset($this->globals['IPBANS']['FAILURES'][$this->ipAddr]));
104
105 $this->loginManager->handleFailedLogin($server);
106 $this->assertFalse(isset($this->globals['IPBANS']['FAILURES'][$this->ipAddr]));
107 }
108
109 /**
110 * Record a failed login attempt and ban the IP after too many failures
111 */
112 public function testHandleFailedLoginBanIp()
113 {
114 $this->loginManager->handleFailedLogin($this->server);
115 $this->assertEquals(1, $this->globals['IPBANS']['FAILURES'][$this->ipAddr]);
116 $this->assertTrue($this->loginManager->canLogin($this->server));
117
118 $this->loginManager->handleFailedLogin($this->server);
119 $this->assertEquals(2, $this->globals['IPBANS']['FAILURES'][$this->ipAddr]);
120 $this->assertTrue($this->loginManager->canLogin($this->server));
121
122 $this->loginManager->handleFailedLogin($this->server);
123 $this->assertEquals(3, $this->globals['IPBANS']['FAILURES'][$this->ipAddr]);
124 $this->assertTrue($this->loginManager->canLogin($this->server));
125
126 $this->loginManager->handleFailedLogin($this->server);
127 $this->assertEquals(4, $this->globals['IPBANS']['FAILURES'][$this->ipAddr]);
128 $this->assertFalse($this->loginManager->canLogin($this->server));
129
130 // handleFailedLogin is not supposed to be called at this point:
131 // - no login form should be displayed once an IP has been banned
132 // - yet this could happen when using custom templates / scripts
133 $this->loginManager->handleFailedLogin($this->server);
134 $this->assertEquals(5, $this->globals['IPBANS']['FAILURES'][$this->ipAddr]);
135 $this->assertFalse($this->loginManager->canLogin($this->server));
136 }
137
138 /**
139 * Nothing to do
140 */
141 public function testHandleSuccessfulLogin()
142 {
143 $this->assertTrue($this->loginManager->canLogin($this->server));
144
145 $this->loginManager->handleSuccessfulLogin($this->server);
146 $this->assertTrue($this->loginManager->canLogin($this->server));
147 }
148
149 /**
150 * Erase failure records after successfully logging in from this IP
151 */
152 public function testHandleSuccessfulLoginAfterFailure()
153 {
154 $this->loginManager->handleFailedLogin($this->server);
155 $this->loginManager->handleFailedLogin($this->server);
156 $this->assertEquals(2, $this->globals['IPBANS']['FAILURES'][$this->ipAddr]);
157 $this->assertTrue($this->loginManager->canLogin($this->server));
158
159 $this->loginManager->handleSuccessfulLogin($this->server);
160 $this->assertTrue($this->loginManager->canLogin($this->server));
161 $this->assertFalse(isset($this->globals['IPBANS']['FAILURES'][$this->ipAddr]));
162 $this->assertFalse(isset($this->globals['IPBANS']['BANS'][$this->ipAddr]));
163 }
164
165 /**
166 * The IP is not banned
167 */
168 public function testCanLoginIpNotBanned()
169 {
170 $this->assertTrue($this->loginManager->canLogin($this->server));
171 }
172
173 /**
174 * The IP is banned
175 */
176 public function testCanLoginIpBanned()
177 {
178 // ban the IP for an hour
179 $this->globals['IPBANS']['FAILURES'][$this->ipAddr] = 10;
180 $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() + 3600;
181
182 $this->assertFalse($this->loginManager->canLogin($this->server));
183 }
184
185 /**
186 * The IP is banned, and the ban duration is over
187 */
188 public function testCanLoginIpBanExpired()
189 {
190 // ban the IP for an hour
191 $this->globals['IPBANS']['FAILURES'][$this->ipAddr] = 10;
192 $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() + 3600;
193 $this->assertFalse($this->loginManager->canLogin($this->server));
194
195 // lift the ban
196 $this->globals['IPBANS']['BANS'][$this->ipAddr] = time() - 3600;
197 $this->assertTrue($this->loginManager->canLogin($this->server));
198 }
199}
diff --git a/tests/utils/FakeConfigManager.php b/tests/utils/FakeConfigManager.php
index f29760cb..85434de7 100644
--- a/tests/utils/FakeConfigManager.php
+++ b/tests/utils/FakeConfigManager.php
@@ -5,8 +5,41 @@
5 */ 5 */
6class FakeConfigManager 6class FakeConfigManager
7{ 7{
8 public static function get($key) 8 protected $values = [];
9
10 /**
11 * Initialize with test values
12 *
13 * @param array $values Initial values
14 */
15 public function __construct($values = [])
16 {
17 $this->values = $values;
18 }
19
20 /**
21 * Set a given value
22 *
23 * @param string $key Key of the value to set
24 * @param mixed $value Value to set
25 */
26 public function set($key, $value)
27 {
28 $this->values[$key] = $value;
29 }
30
31 /**
32 * Get a given configuration value
33 *
34 * @param string $key Index of the value to retrieve
35 *
36 * @return mixed The value if set, else the name of the key
37 */
38 public function get($key)
9 { 39 {
40 if (isset($this->values[$key])) {
41 return $this->values[$key];
42 }
10 return $key; 43 return $key;
11 } 44 }
12} 45}
diff --git a/tpl/default/loginform.html b/tpl/default/loginform.html
index 5777a218..d481f452 100644
--- a/tpl/default/loginform.html
+++ b/tpl/default/loginform.html
@@ -5,7 +5,7 @@
5</head> 5</head>
6<body> 6<body>
7{include="page.header"} 7{include="page.header"}
8{if="!ban_canLogin($conf)"} 8{if="!$user_can_login"}
9<div class="pure-g pure-alert pure-alert-error pure-alert-closable center"> 9<div class="pure-g pure-alert pure-alert-error pure-alert-closable center">
10 <div class="pure-u-2-24"></div> 10 <div class="pure-u-2-24"></div>
11 <div class="pure-u-20-24"> 11 <div class="pure-u-20-24">
diff --git a/tpl/vintage/loginform.html b/tpl/vintage/loginform.html
index 1becd44f..2c9b710e 100644
--- a/tpl/vintage/loginform.html
+++ b/tpl/vintage/loginform.html
@@ -2,7 +2,7 @@
2<html> 2<html>
3<head>{include="includes"}</head> 3<head>{include="includes"}</head>
4<body 4<body
5{if="ban_canLogin($conf)"} 5{if="$user_can_login"}
6 {if="empty($username)"} 6 {if="empty($username)"}
7 onload="document.loginform.login.focus();" 7 onload="document.loginform.login.focus();"
8 {else} 8 {else}