aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndreas Fischer <bantu@phpbb.com>2011-06-11 19:42:04 +0200
committerAndreas Fischer <bantu@phpbb.com>2011-06-11 19:42:04 +0200
commitfe5d616349df911d7053c4862cc734a8de21b4fb (patch)
treef367d086f63d6077c72a51b69f9070784da21aa2
parent0cad4ed49f6272f029afad0fb613a8855f0a5b43 (diff)
parentbf2125f0f7c2b3d2f270ae4f3117941dd108f35a (diff)
downloadforums-fe5d616349df911d7053c4862cc734a8de21b4fb.tar
forums-fe5d616349df911d7053c4862cc734a8de21b4fb.tar.gz
forums-fe5d616349df911d7053c4862cc734a8de21b4fb.tar.bz2
forums-fe5d616349df911d7053c4862cc734a8de21b4fb.tar.xz
forums-fe5d616349df911d7053c4862cc734a8de21b4fb.zip
Merge branch 'develop-olympus' into develop
* develop-olympus: [ticket/9992] Clarify explanations of ip and account limits on login [ticket/9992] Add a comma to language for IP_LOGIN_LIMIT_MAX_EXPLAIN [ticket/9992] Use sql_fetchfield for single row and single column result [ticket/9992] Adding a limit on login attempts per IP. [ticket/9992] Make sql_create_table and sql_table_exists available in updater
-rw-r--r--phpBB/develop/create_schema_files.php20
-rw-r--r--phpBB/includes/acp/acp_board.php3
-rw-r--r--phpBB/includes/auth.php2
-rw-r--r--phpBB/includes/auth/auth_db.php63
-rw-r--r--phpBB/includes/constants.php1
-rw-r--r--phpBB/includes/db/db_tools.php13
-rw-r--r--phpBB/includes/session.php4
-rw-r--r--phpBB/install/database_update.php293
-rw-r--r--phpBB/install/schemas/firebird_schema.sql32
-rw-r--r--phpBB/install/schemas/mssql_schema.sql38
-rw-r--r--phpBB/install/schemas/mysql_40_schema.sql20
-rw-r--r--phpBB/install/schemas/mysql_41_schema.sql20
-rw-r--r--phpBB/install/schemas/oracle_schema.sql43
-rw-r--r--phpBB/install/schemas/postgres_schema.sql24
-rw-r--r--phpBB/install/schemas/schema_data.sql3
-rw-r--r--phpBB/install/schemas/sqlite_schema.sql19
-rw-r--r--phpBB/language/en/acp/board.php12
17 files changed, 595 insertions, 15 deletions
diff --git a/phpBB/develop/create_schema_files.php b/phpBB/develop/create_schema_files.php
index 0abd0763d7..db0752e4d5 100644
--- a/phpBB/develop/create_schema_files.php
+++ b/phpBB/develop/create_schema_files.php
@@ -1209,6 +1209,26 @@ function get_schema_struct()
),
);
+ $schema_data['phpbb_login_attempts'] = array(
+ 'COLUMNS' => array(
+ 'attempt_id' => array('UINT', NULL, 'auto_increment'),
+ 'attempt_ip' => array('VCHAR:40', ''),
+ 'attempt_browser' => array('VCHAR:150', ''),
+ 'attempt_forwarded_for' => array('VCHAR:255', ''),
+ 'attempt_time' => array('TIMESTAMP', 0),
+ 'user_id' => array('UINT', 0),
+ 'username' => array('VCHAR_UNI:255', 0),
+ 'username_clean' => array('VCHAR_CI', 0),
+ ),
+ 'PRIMARY_KEY' => 'attempt_id',
+ 'KEYS' => array(
+ 'attempt_ip' => array('INDEX', array('attempt_ip', 'attempt_time')),
+ 'attempt_forwarded_for' => array('INDEX', array('attempt_forwarded_for', 'attempt_time')),
+ 'attempt_time' => array('INDEX', array('attempt_time')),
+ 'user_id' => array('INDEX', 'user_id'),
+ ),
+ );
+
$schema_data['phpbb_moderator_cache'] = array(
'COLUMNS' => array(
'forum_id' => array('UINT', 0),
diff --git a/phpBB/includes/acp/acp_board.php b/phpBB/includes/acp/acp_board.php
index d4f32daf04..efc0fdc639 100644
--- a/phpBB/includes/acp/acp_board.php
+++ b/phpBB/includes/acp/acp_board.php
@@ -389,6 +389,9 @@ class acp_board
'pass_complex' => array('lang' => 'PASSWORD_TYPE', 'validate' => 'string', 'type' => 'select', 'method' => 'select_password_chars', 'explain' => true),
'chg_passforce' => array('lang' => 'FORCE_PASS_CHANGE', 'validate' => 'int:0', 'type' => 'text:3:3', 'explain' => true, 'append' => ' ' . $user->lang['DAYS']),
'max_login_attempts' => array('lang' => 'MAX_LOGIN_ATTEMPTS', 'validate' => 'int:0', 'type' => 'text:3:3', 'explain' => true),
+ 'ip_login_limit_max' => array('lang' => 'IP_LOGIN_LIMIT_MAX', 'validate' => 'int:0', 'type' => 'text:3:3', 'explain' => true),
+ 'ip_login_limit_time' => array('lang' => 'IP_LOGIN_LIMIT_TIME', 'validate' => 'int:0', 'type' => 'text:5:5', 'explain' => true),
+ 'ip_login_limit_use_forwarded' => array('lang' => 'IP_LOGIN_LIMIT_USE_FORWARDED', 'validate' => 'bool', 'type' => 'radio:yes_no', 'explain' => true),
'tpl_allow_php' => array('lang' => 'TPL_ALLOW_PHP', 'validate' => 'bool', 'type' => 'radio:yes_no', 'explain' => true),
'form_token_lifetime' => array('lang' => 'FORM_TIME_MAX', 'validate' => 'int:-1', 'type' => 'text:5:5', 'explain' => true, 'append' => ' ' . $user->lang['SECONDS']),
'form_token_sid_guests' => array('lang' => 'FORM_SID_GUESTS', 'validate' => 'bool', 'type' => 'radio:yes_no', 'explain' => true),
diff --git a/phpBB/includes/auth.php b/phpBB/includes/auth.php
index 22aca5faf9..53b0c5ff2c 100644
--- a/phpBB/includes/auth.php
+++ b/phpBB/includes/auth.php
@@ -908,7 +908,7 @@ class auth
$method = 'login_' . $method;
if (function_exists($method))
{
- $login = $method($username, $password);
+ $login = $method($username, $password, $user->ip, $user->browser, $user->forwarded_for);
// If the auth module wants us to create an empty profile do so and then treat the status as LOGIN_SUCCESS
if ($login['status'] == LOGIN_SUCCESS_CREATE_PROFILE)
diff --git a/phpBB/includes/auth/auth_db.php b/phpBB/includes/auth/auth_db.php
index 6304d6e49a..a43598cadd 100644
--- a/phpBB/includes/auth/auth_db.php
+++ b/phpBB/includes/auth/auth_db.php
@@ -23,8 +23,21 @@ if (!defined('IN_PHPBB'))
/**
* Login function
+*
+* @param string $username
+* @param string $password
+* @param string $ip IP address the login is taking place from. Used to
+* limit the number of login attempts per IP address.
+* @param string $browser The user agent used to login
+* @param string $forwarded_for X_FORWARDED_FOR header sent with login request
+* @return array A associative array of the format
+* array(
+* 'status' => status constant
+* 'error_msg' => string
+* 'user_row' => array
+* )
*/
-function login_db(&$username, &$password)
+function login_db($username, $password, $ip = '', $browser = '', $forwarded_for = '')
{
global $db, $config;
global $request;
@@ -48,13 +61,51 @@ function login_db(&$username, &$password)
);
}
+ $username_clean = utf8_clean_string($username);
+
$sql = 'SELECT user_id, username, user_password, user_passchg, user_pass_convert, user_email, user_type, user_login_attempts
FROM ' . USERS_TABLE . "
- WHERE username_clean = '" . $db->sql_escape(utf8_clean_string($username)) . "'";
+ WHERE username_clean = '" . $db->sql_escape($username_clean) . "'";
$result = $db->sql_query($sql);
$row = $db->sql_fetchrow($result);
$db->sql_freeresult($result);
+ if (($ip && !$config['ip_login_limit_use_forwarded']) ||
+ ($forwarded_for && $config['ip_login_limit_use_forwarded']))
+ {
+ $sql = 'SELECT COUNT(attempt_id) AS count
+ FROM ' . LOGIN_ATTEMPT_TABLE . '
+ WHERE attempt_time > ' . (time() - (int) $config['ip_login_limit_time']);
+ if ($config['ip_login_limit_use_forwarded'])
+ {
+ $sql .= " AND attempt_forwarded_for = '" . $db->sql_escape($forwarded_for) . "'";
+ }
+ else
+ {
+ $sql .= " AND attempt_ip = '" . $db->sql_escape($ip) . "' ";
+ }
+
+ $result = $db->sql_query($sql);
+ $attempts = (int) $db->sql_fetchfield('count');
+ $db->sql_freeresult($result);
+
+ $attempt_data = array(
+ 'attempt_ip' => $ip,
+ 'attempt_browser' => $browser,
+ 'attempt_forwarded_for' => $forwarded_for,
+ 'attempt_time' => time(),
+ 'user_id' => ($row) ? (int) $row['user_id'] : 0,
+ 'username' => $username,
+ 'username_clean' => $username_clean,
+ );
+ $sql = 'INSERT INTO ' . LOGIN_ATTEMPT_TABLE . $db->sql_build_array('INSERT', $attempt_data);
+ $result = $db->sql_query($sql);
+ }
+ else
+ {
+ $attempts = 0;
+ }
+
if (!$row)
{
return array(
@@ -63,7 +114,9 @@ function login_db(&$username, &$password)
'user_row' => array('user_id' => ANONYMOUS),
);
}
- $show_captcha = $config['max_login_attempts'] && $row['user_login_attempts'] >= $config['max_login_attempts'];
+
+ $show_captcha = ($config['max_login_attempts'] && $row['user_login_attempts'] >= $config['max_login_attempts']) ||
+ ($config['ip_login_limit_max'] && $attempts >= $config['ip_login_limit_max']);
// If there are too much login attempts, we need to check for an confirm image
// Every auth module is able to define what to do by itself...
@@ -177,6 +230,10 @@ function login_db(&$username, &$password)
$row['user_password'] = $hash;
}
+ $sql = 'DELETE FROM ' . LOGIN_ATTEMPT_TABLE . '
+ WHERE user_id = ' . $row['user_id'];
+ $db->sql_query($sql);
+
if ($row['user_login_attempts'] != 0)
{
// Successful, reset login attempts (the user passed all stages)
diff --git a/phpBB/includes/constants.php b/phpBB/includes/constants.php
index 216aac7489..8ef1a4655d 100644
--- a/phpBB/includes/constants.php
+++ b/phpBB/includes/constants.php
@@ -236,6 +236,7 @@ define('GROUPS_TABLE', $table_prefix . 'groups');
define('ICONS_TABLE', $table_prefix . 'icons');
define('LANG_TABLE', $table_prefix . 'lang');
define('LOG_TABLE', $table_prefix . 'log');
+define('LOGIN_ATTEMPT_TABLE', $table_prefix . 'login_attempts');
define('MODERATOR_CACHE_TABLE', $table_prefix . 'moderator_cache');
define('MODULES_TABLE', $table_prefix . 'modules');
define('POLL_OPTIONS_TABLE', $table_prefix . 'poll_options');
diff --git a/phpBB/includes/db/db_tools.php b/phpBB/includes/db/db_tools.php
index 8d0e54b881..b74be221e2 100644
--- a/phpBB/includes/db/db_tools.php
+++ b/phpBB/includes/db/db_tools.php
@@ -638,6 +638,19 @@ class phpbb_db_tools
$sqlite = true;
}
+ // Add tables?
+ if (!empty($schema_changes['add_tables']))
+ {
+ foreach ($schema_changes['add_tables'] as $table => $table_data)
+ {
+ $result = $this->sql_create_table($table, $table_data);
+ if ($this->return_statements)
+ {
+ $statements = array_merge($statements, $result);
+ }
+ }
+ }
+
// Change columns?
if (!empty($schema_changes['change_columns']))
{
diff --git a/phpBB/includes/session.php b/phpBB/includes/session.php
index 98d5eaf32b..9e1aefa309 100644
--- a/phpBB/includes/session.php
+++ b/phpBB/includes/session.php
@@ -1019,6 +1019,10 @@ class session
include($phpbb_root_path . "includes/captcha/captcha_factory." . $phpEx);
}
phpbb_captcha_factory::garbage_collect($config['captcha_plugin']);
+
+ $sql = 'DELETE FROM ' . LOGIN_ATTEMPT_TABLE . '
+ WHERE attempt_time < ' . (time() - (int) $config['ip_login_limit_time']);
+ $db->sql_query($sql);
}
return;
diff --git a/phpBB/install/database_update.php b/phpBB/install/database_update.php
index d58014c11e..58e090ba17 100644
--- a/phpBB/install/database_update.php
+++ b/phpBB/install/database_update.php
@@ -983,9 +983,29 @@ function database_update_info()
'3.0.7-PL1' => array(),
// No changes from 3.0.8-RC1 to 3.0.8
'3.0.8-RC1' => array(),
-
// Changes from 3.0.8 to 3.0.9-RC1
'3.0.8' => array(
+ 'add_tables' => array(
+ LOGIN_ATTEMPT_TABLE => array(
+ 'COLUMNS' => array(
+ 'attempt_id' => array('UINT', NULL, 'auto_increment'),
+ 'attempt_ip' => array('VCHAR:40', ''),
+ 'attempt_browser' => array('VCHAR:150', ''),
+ 'attempt_forwarded_for' => array('VCHAR:255', ''),
+ 'attempt_time' => array('TIMESTAMP', 0),
+ 'user_id' => array('UINT', 0),
+ 'username' => array('VCHAR_UNI:255', 0),
+ 'username_clean' => array('VCHAR_CI', 0),
+ ),
+ 'PRIMARY_KEY' => 'attempt_id',
+ 'KEYS' => array(
+ 'attempt_ip' => array('INDEX', array('attempt_ip', 'attempt_time')),
+ 'attempt_forwarded_for' => array('INDEX', array('attempt_forwarded_for', 'attempt_time')),
+ 'attempt_time' => array('INDEX', array('attempt_time')),
+ 'user_id' => array('INDEX', 'user_id'),
+ ),
+ ),
+ ),
'change_columns' => array(
BBCODES_TABLE => array(
'bbcode_id' => array('USINT', 0),
@@ -1954,6 +1974,10 @@ function change_database_data(&$no_updates, $version)
// Changes from 3.0.8 to 3.0.9-RC1
case '3.0.8':
+ set_config('ip_login_limit_max', '50');
+ set_config('ip_login_limit_time', '21600');
+ set_config('ip_login_limit_use_forwarded', '0');
+
// Update file extension group names to use language strings, again.
$sql = 'SELECT group_id, group_name
FROM ' . EXTENSION_GROUPS_TABLE . '
@@ -2452,6 +2476,260 @@ class updater_db_tools
}
/**
+ * Check if table exists
+ *
+ *
+ * @param string $table_name The table name to check for
+ * @return bool true if table exists, else false
+ */
+ function sql_table_exists($table_name)
+ {
+ $this->db->sql_return_on_error(true);
+ $result = $this->db->sql_query_limit('SELECT * FROM ' . $table_name, 1);
+ $this->db->sql_return_on_error(false);
+
+ if ($result)
+ {
+ $this->db->sql_freeresult($result);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Create SQL Table
+ *
+ * @param string $table_name The table name to create
+ * @param array $table_data Array containing table data.
+ * @return array Statements if $return_statements is true.
+ */
+ function sql_create_table($table_name, $table_data)
+ {
+ // holds the DDL for a column
+ $columns = $statements = array();
+
+ if ($this->sql_table_exists($table_name))
+ {
+ return $this->_sql_run_sql($statements);
+ }
+
+ // Begin transaction
+ $statements[] = 'begin';
+
+ // Determine if we have created a PRIMARY KEY in the earliest
+ $primary_key_gen = false;
+
+ // Determine if the table must be created with TEXTIMAGE
+ $create_textimage = false;
+
+ // Determine if the table requires a sequence
+ $create_sequence = false;
+
+ // Begin table sql statement
+ switch ($this->sql_layer)
+ {
+ case 'mssql':
+ case 'mssqlnative':
+ $table_sql = 'CREATE TABLE [' . $table_name . '] (' . "\n";
+ break;
+
+ default:
+ $table_sql = 'CREATE TABLE ' . $table_name . ' (' . "\n";
+ break;
+ }
+
+ // Iterate through the columns to create a table
+ foreach ($table_data['COLUMNS'] as $column_name => $column_data)
+ {
+ // here lies an array, filled with information compiled on the column's data
+ $prepared_column = $this->sql_prepare_column_data($table_name, $column_name, $column_data);
+
+ // here we add the definition of the new column to the list of columns
+ switch ($this->sql_layer)
+ {
+ case 'mssql':
+ case 'mssqlnative':
+ $columns[] = "\t [{$column_name}] " . $prepared_column['column_type_sql_default'];
+ break;
+
+ default:
+ $columns[] = "\t {$column_name} " . $prepared_column['column_type_sql'];
+ break;
+ }
+
+ // see if we have found a primary key set due to a column definition if we have found it, we can stop looking
+ if (!$primary_key_gen)
+ {
+ $primary_key_gen = isset($prepared_column['primary_key_set']) && $prepared_column['primary_key_set'];
+ }
+
+ // create textimage DDL based off of the existance of certain column types
+ if (!$create_textimage)
+ {
+ $create_textimage = isset($prepared_column['textimage']) && $prepared_column['textimage'];
+ }
+
+ // create sequence DDL based off of the existance of auto incrementing columns
+ if (!$create_sequence && isset($prepared_column['auto_increment']) && $prepared_column['auto_increment'])
+ {
+ $create_sequence = $column_name;
+ }
+ }
+
+ // this makes up all the columns in the create table statement
+ $table_sql .= implode(",\n", $columns);
+
+ // Close the table for two DBMS and add to the statements
+ switch ($this->sql_layer)
+ {
+ case 'firebird':
+ $table_sql .= "\n);";
+ $statements[] = $table_sql;
+ break;
+
+ case 'mssql':
+ case 'mssqlnative':
+ $table_sql .= "\n) ON [PRIMARY]" . (($create_textimage) ? ' TEXTIMAGE_ON [PRIMARY]' : '');
+ $statements[] = $table_sql;
+ break;
+ }
+
+ // we have yet to create a primary key for this table,
+ // this means that we can add the one we really wanted instead
+ if (!$primary_key_gen)
+ {
+ // Write primary key
+ if (isset($table_data['PRIMARY_KEY']))
+ {
+ if (!is_array($table_data['PRIMARY_KEY']))
+ {
+ $table_data['PRIMARY_KEY'] = array($table_data['PRIMARY_KEY']);
+ }
+
+ switch ($this->sql_layer)
+ {
+ case 'mysql_40':
+ case 'mysql_41':
+ case 'postgres':
+ case 'sqlite':
+ $table_sql .= ",\n\t PRIMARY KEY (" . implode(', ', $table_data['PRIMARY_KEY']) . ')';
+ break;
+
+ case 'firebird':
+ case 'mssql':
+ case 'mssqlnative':
+ // We need the data here
+ $old_return_statements = $this->return_statements;
+ $this->return_statements = true;
+
+ $primary_key_stmts = $this->sql_create_primary_key($table_name, $table_data['PRIMARY_KEY']);
+ foreach ($primary_key_stmts as $pk_stmt)
+ {
+ $statements[] = $pk_stmt;
+ }
+
+ $this->return_statements = $old_return_statements;
+ break;
+
+ case 'oracle':
+ $table_sql .= ",\n\t CONSTRAINT pk_{$table_name} PRIMARY KEY (" . implode(', ', $table_data['PRIMARY_KEY']) . ')';
+ break;
+ }
+ }
+ }
+
+ // close the table
+ switch ($this->sql_layer)
+ {
+ case 'mysql_41':
+ // make sure the table is in UTF-8 mode
+ $table_sql .= "\n) CHARACTER SET `utf8` COLLATE `utf8_bin`;";
+ $statements[] = $table_sql;
+ break;
+
+ case 'mysql_40':
+ case 'sqlite':
+ $table_sql .= "\n);";
+ $statements[] = $table_sql;
+ break;
+
+ case 'postgres':
+ // do we need to add a sequence for auto incrementing columns?
+ if ($create_sequence)
+ {
+ $statements[] = "CREATE SEQUENCE {$table_name}_seq;";
+ }
+
+ $table_sql .= "\n);";
+ $statements[] = $table_sql;
+ break;
+
+ case 'oracle':
+ $table_sql .= "\n);";
+ $statements[] = $table_sql;
+
+ // do we need to add a sequence and a tigger for auto incrementing columns?
+ if ($create_sequence)
+ {
+ // create the actual sequence
+ $statements[] = "CREATE SEQUENCE {$table_name}_seq";
+
+ // the trigger is the mechanism by which we increment the counter
+ $trigger = "CREATE OR REPLACE TRIGGER t_{$table_name}\n";
+ $trigger .= "BEFORE INSERT ON {$table_name}\n";
+ $trigger .= "FOR EACH ROW WHEN (\n";
+ $trigger .= "\tnew.{$create_sequence} IS NULL OR new.{$create_sequence} = 0\n";
+ $trigger .= ")\n";
+ $trigger .= "BEGIN\n";
+ $trigger .= "\tSELECT {$table_name}_seq.nextval\n";
+ $trigger .= "\tINTO :new.{$create_sequence}\n";
+ $trigger .= "\tFROM dual\n";
+ $trigger .= "END;";
+
+ $statements[] = $trigger;
+ }
+ break;
+
+ case 'firebird':
+ if ($create_sequence)
+ {
+ $statements[] = "CREATE SEQUENCE {$table_name}_seq;";
+ }
+ break;
+ }
+
+ // Write Keys
+ if (isset($table_data['KEYS']))
+ {
+ foreach ($table_data['KEYS'] as $key_name => $key_data)
+ {
+ if (!is_array($key_data[1]))
+ {
+ $key_data[1] = array($key_data[1]);
+ }
+
+ $old_return_statements = $this->return_statements;
+ $this->return_statements = true;
+
+ $key_stmts = ($key_data[0] == 'UNIQUE') ? $this->sql_create_unique_index($table_name, $key_name, $key_data[1]) : $this->sql_create_index($table_name, $key_name, $key_data[1]);
+
+ foreach ($key_stmts as $key_stmt)
+ {
+ $statements[] = $key_stmt;
+ }
+
+ $this->return_statements = $old_return_statements;
+ }
+ }
+
+ // Commit Transaction
+ $statements[] = 'commit';
+
+ return $this->_sql_run_sql($statements);
+ }
+
+ /**
* Handle passed database update array.
* Expected structure...
* Key being one of the following
@@ -2488,6 +2766,19 @@ class updater_db_tools
$sqlite = true;
}
+ // Add tables?
+ if (!empty($schema_changes['add_tables']))
+ {
+ foreach ($schema_changes['add_tables'] as $table => $table_data)
+ {
+ $result = $this->sql_create_table($table, $table_data);
+ if ($this->return_statements)
+ {
+ $statements = array_merge($statements, $result);
+ }
+ }
+ }
+
// Change columns?
if (!empty($schema_changes['change_columns']))
{
diff --git a/phpBB/install/schemas/firebird_schema.sql b/phpBB/install/schemas/firebird_schema.sql
index 73b9a283de..42feadd4bb 100644
--- a/phpBB/install/schemas/firebird_schema.sql
+++ b/phpBB/install/schemas/firebird_schema.sql
@@ -1,5 +1,5 @@
#
-# $Id$
+# $Id: $
#
@@ -547,6 +547,36 @@ BEGIN
END;;
+# Table: 'phpbb_login_attempts'
+CREATE TABLE phpbb_login_attempts (
+ attempt_id INTEGER NOT NULL,
+ attempt_ip VARCHAR(40) CHARACTER SET NONE DEFAULT '' NOT NULL,
+ attempt_browser VARCHAR(150) CHARACTER SET NONE DEFAULT '' NOT NULL,
+ attempt_forwarded_for VARCHAR(255) CHARACTER SET NONE DEFAULT '' NOT NULL,
+ attempt_time INTEGER DEFAULT 0 NOT NULL,
+ user_id INTEGER DEFAULT 0 NOT NULL,
+ username VARCHAR(255) CHARACTER SET UTF8 DEFAULT 0 NOT NULL COLLATE UNICODE,
+ username_clean VARCHAR(255) CHARACTER SET UTF8 DEFAULT 0 NOT NULL COLLATE UNICODE
+);;
+
+ALTER TABLE phpbb_login_attempts ADD PRIMARY KEY (attempt_id);;
+
+CREATE INDEX phpbb_login_attempts_attempt_ip ON phpbb_login_attempts(attempt_ip, attempt_time);;
+CREATE INDEX phpbb_login_attempts_attempt_forwarded_for ON phpbb_login_attempts(attempt_forwarded_for, attempt_time);;
+CREATE INDEX phpbb_login_attempts_attempt_time ON phpbb_login_attempts(attempt_time);;
+CREATE INDEX phpbb_login_attempts_user_id ON phpbb_login_attempts(user_id);;
+
+CREATE GENERATOR phpbb_login_attempts_gen;;
+SET GENERATOR phpbb_login_attempts_gen TO 0;;
+
+CREATE TRIGGER t_phpbb_login_attempts FOR phpbb_login_attempts
+BEFORE INSERT
+AS
+BEGIN
+ NEW.attempt_id = GEN_ID(phpbb_login_attempts_gen, 1);
+END;;
+
+
# Table: 'phpbb_moderator_cache'
CREATE TABLE phpbb_moderator_cache (
forum_id INTEGER DEFAULT 0 NOT NULL,
diff --git a/phpBB/install/schemas/mssql_schema.sql b/phpBB/install/schemas/mssql_schema.sql
index f73f0134db..bbbfc8fdc0 100644
--- a/phpBB/install/schemas/mssql_schema.sql
+++ b/phpBB/install/schemas/mssql_schema.sql
@@ -1,6 +1,6 @@
/*
- $Id$
+ $Id: $
*/
@@ -654,6 +654,41 @@ GO
/*
+ Table: 'phpbb_login_attempts'
+*/
+CREATE TABLE [phpbb_login_attempts] (
+ [attempt_id] [int] IDENTITY (1, 1) NOT NULL ,
+ [attempt_ip] [varchar] (40) DEFAULT ('') NOT NULL ,
+ [attempt_browser] [varchar] (150) DEFAULT ('') NOT NULL ,
+ [attempt_forwarded_for] [varchar] (255) DEFAULT ('') NOT NULL ,
+ [attempt_time] [int] DEFAULT (0) NOT NULL ,
+ [user_id] [int] DEFAULT (0) NOT NULL ,
+ [username] [varchar] (255) DEFAULT (0) NOT NULL ,
+ [username_clean] [varchar] (255) DEFAULT (0) NOT NULL
+) ON [PRIMARY]
+GO
+
+ALTER TABLE [phpbb_login_attempts] WITH NOCHECK ADD
+ CONSTRAINT [PK_phpbb_login_attempts] PRIMARY KEY CLUSTERED
+ (
+ [attempt_id]
+ ) ON [PRIMARY]
+GO
+
+CREATE INDEX [attempt_ip] ON [phpbb_login_attempts]([attempt_ip], [attempt_time]) ON [PRIMARY]
+GO
+
+CREATE INDEX [attempt_forwarded_for] ON [phpbb_login_attempts]([attempt_forwarded_for], [attempt_time]) ON [PRIMARY]
+GO
+
+CREATE INDEX [attempt_time] ON [phpbb_login_attempts]([attempt_time]) ON [PRIMARY]
+GO
+
+CREATE INDEX [user_id] ON [phpbb_login_attempts]([user_id]) ON [PRIMARY]
+GO
+
+
+/*
Table: 'phpbb_moderator_cache'
*/
CREATE TABLE [phpbb_moderator_cache] (
@@ -1735,3 +1770,4 @@ ALTER TABLE [phpbb_zebra] WITH NOCHECK ADD
) ON [PRIMARY]
GO
+
diff --git a/phpBB/install/schemas/mysql_40_schema.sql b/phpBB/install/schemas/mysql_40_schema.sql
index 6da9a71420..14d7c620af 100644
--- a/phpBB/install/schemas/mysql_40_schema.sql
+++ b/phpBB/install/schemas/mysql_40_schema.sql
@@ -1,5 +1,5 @@
#
-# $Id$
+# $Id: $
#
# Table: 'phpbb_attachments'
@@ -371,6 +371,24 @@ CREATE TABLE phpbb_log (
);
+# Table: 'phpbb_login_attempts'
+CREATE TABLE phpbb_login_attempts (
+ attempt_id mediumint(8) UNSIGNED NOT NULL auto_increment,
+ attempt_ip varbinary(40) DEFAULT '' NOT NULL,
+ attempt_browser varbinary(150) DEFAULT '' NOT NULL,
+ attempt_forwarded_for varbinary(255) DEFAULT '' NOT NULL,
+ attempt_time int(11) UNSIGNED DEFAULT '0' NOT NULL,
+ user_id mediumint(8) UNSIGNED DEFAULT '0' NOT NULL,
+ username blob NOT NULL,
+ username_clean blob NOT NULL,
+ PRIMARY KEY (attempt_id),
+ KEY attempt_ip (attempt_ip, attempt_time),
+ KEY attempt_forwarded_for (attempt_forwarded_for, attempt_time),
+ KEY attempt_time (attempt_time),
+ KEY user_id (user_id)
+);
+
+
# Table: 'phpbb_moderator_cache'
CREATE TABLE phpbb_moderator_cache (
forum_id mediumint(8) UNSIGNED DEFAULT '0' NOT NULL,
diff --git a/phpBB/install/schemas/mysql_41_schema.sql b/phpBB/install/schemas/mysql_41_schema.sql
index 743105e485..9a0ada8029 100644
--- a/phpBB/install/schemas/mysql_41_schema.sql
+++ b/phpBB/install/schemas/mysql_41_schema.sql
@@ -1,5 +1,5 @@
#
-# $Id$
+# $Id: $
#
# Table: 'phpbb_attachments'
@@ -371,6 +371,24 @@ CREATE TABLE phpbb_log (
) CHARACTER SET `utf8` COLLATE `utf8_bin`;
+# Table: 'phpbb_login_attempts'
+CREATE TABLE phpbb_login_attempts (
+ attempt_id mediumint(8) UNSIGNED NOT NULL auto_increment,
+ attempt_ip varchar(40) DEFAULT '' NOT NULL,
+ attempt_browser varchar(150) DEFAULT '' NOT NULL,
+ attempt_forwarded_for varchar(255) DEFAULT '' NOT NULL,
+ attempt_time int(11) UNSIGNED DEFAULT '0' NOT NULL,
+ user_id mediumint(8) UNSIGNED DEFAULT '0' NOT NULL,
+ username varchar(255) DEFAULT '0' NOT NULL,
+ username_clean varchar(255) DEFAULT '0' NOT NULL,
+ PRIMARY KEY (attempt_id),
+ KEY attempt_ip (attempt_ip, attempt_time),
+ KEY attempt_forwarded_for (attempt_forwarded_for, attempt_time),
+ KEY attempt_time (attempt_time),
+ KEY user_id (user_id)
+) CHARACTER SET `utf8` COLLATE `utf8_bin`;
+
+
# Table: 'phpbb_moderator_cache'
CREATE TABLE phpbb_moderator_cache (
forum_id mediumint(8) UNSIGNED DEFAULT '0' NOT NULL,
diff --git a/phpBB/install/schemas/oracle_schema.sql b/phpBB/install/schemas/oracle_schema.sql
index 13c7303a0d..5e142d01d6 100644
--- a/phpBB/install/schemas/oracle_schema.sql
+++ b/phpBB/install/schemas/oracle_schema.sql
@@ -1,6 +1,6 @@
/*
- $Id$
+ $Id: $
*/
@@ -744,6 +744,47 @@ END;
/*
+ Table: 'phpbb_login_attempts'
+*/
+CREATE TABLE phpbb_login_attempts (
+ attempt_id number(8) NOT NULL,
+ attempt_ip varchar2(40) DEFAULT '' ,
+ attempt_browser varchar2(150) DEFAULT '' ,
+ attempt_forwarded_for varchar2(255) DEFAULT '' ,
+ attempt_time number(11) DEFAULT '0' NOT NULL,
+ user_id number(8) DEFAULT '0' NOT NULL,
+ username varchar2(765) DEFAULT '0' NOT NULL,
+ username_clean varchar2(255) DEFAULT '0' NOT NULL,
+ CONSTRAINT pk_phpbb_login_attempts PRIMARY KEY (attempt_id)
+)
+/
+
+CREATE INDEX phpbb_login_attempts_attempt_ip ON phpbb_login_attempts (attempt_ip, attempt_time)
+/
+CREATE INDEX phpbb_login_attempts_attempt_forwarded_for ON phpbb_login_attempts (attempt_forwarded_for, attempt_time)
+/
+CREATE INDEX phpbb_login_attempts_attempt_time ON phpbb_login_attempts (attempt_time)
+/
+CREATE INDEX phpbb_login_attempts_user_id ON phpbb_login_attempts (user_id)
+/
+
+CREATE SEQUENCE phpbb_login_attempts_seq
+/
+
+CREATE OR REPLACE TRIGGER t_phpbb_login_attempts
+BEFORE INSERT ON phpbb_login_attempts
+FOR EACH ROW WHEN (
+ new.attempt_id IS NULL OR new.attempt_id = 0
+)
+BEGIN
+ SELECT phpbb_login_attempts_seq.nextval
+ INTO :new.attempt_id
+ FROM dual;
+END;
+/
+
+
+/*
Table: 'phpbb_moderator_cache'
*/
CREATE TABLE phpbb_moderator_cache (
diff --git a/phpBB/install/schemas/postgres_schema.sql b/phpBB/install/schemas/postgres_schema.sql
index d2634c7d40..7fba805a6b 100644
--- a/phpBB/install/schemas/postgres_schema.sql
+++ b/phpBB/install/schemas/postgres_schema.sql
@@ -1,6 +1,6 @@
/*
- $Id$
+ $Id: $
*/
@@ -527,6 +527,28 @@ CREATE INDEX phpbb_log_reportee_id ON phpbb_log (reportee_id);
CREATE INDEX phpbb_log_user_id ON phpbb_log (user_id);
/*
+ Table: 'phpbb_login_attempts'
+*/
+CREATE SEQUENCE phpbb_login_attempts_seq;
+
+CREATE TABLE phpbb_login_attempts (
+ attempt_id INT4 DEFAULT nextval('phpbb_login_attempts_seq'),
+ attempt_ip varchar(40) DEFAULT '' NOT NULL,
+ attempt_browser varchar(150) DEFAULT '' NOT NULL,
+ attempt_forwarded_for varchar(255) DEFAULT '' NOT NULL,
+ attempt_time INT4 DEFAULT '0' NOT NULL CHECK (attempt_time >= 0),
+ user_id INT4 DEFAULT '0' NOT NULL CHECK (user_id >= 0),
+ username varchar(255) DEFAULT '0' NOT NULL,
+ username_clean varchar_ci DEFAULT '0' NOT NULL,
+ PRIMARY KEY (attempt_id)
+);
+
+CREATE INDEX phpbb_login_attempts_attempt_ip ON phpbb_login_attempts (attempt_ip, attempt_time);
+CREATE INDEX phpbb_login_attempts_attempt_forwarded_for ON phpbb_login_attempts (attempt_forwarded_for, attempt_time);
+CREATE INDEX phpbb_login_attempts_attempt_time ON phpbb_login_attempts (attempt_time);
+CREATE INDEX phpbb_login_attempts_user_id ON phpbb_login_attempts (user_id);
+
+/*
Table: 'phpbb_moderator_cache'
*/
CREATE TABLE phpbb_moderator_cache (
diff --git a/phpBB/install/schemas/schema_data.sql b/phpBB/install/schemas/schema_data.sql
index 101202124d..db66916b98 100644
--- a/phpBB/install/schemas/schema_data.sql
+++ b/phpBB/install/schemas/schema_data.sql
@@ -136,6 +136,9 @@ INSERT INTO phpbb_config (config_name, config_value) VALUES ('img_max_thumb_widt
INSERT INTO phpbb_config (config_name, config_value) VALUES ('img_max_width', '0');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('img_min_thumb_filesize', '12000');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('ip_check', '3');
+INSERT INTO phpbb_config (config_name, config_value) VALUES ('ip_login_limit_max', '50');
+INSERT INTO phpbb_config (config_name, config_value) VALUES ('ip_login_limit_time', '21600');
+INSERT INTO phpbb_config (config_name, config_value) VALUES ('ip_login_limit_use_forwarded', '0');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('jab_enable', '0');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('jab_host', '');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('jab_password', '');
diff --git a/phpBB/install/schemas/sqlite_schema.sql b/phpBB/install/schemas/sqlite_schema.sql
index 0c706d67dd..4614ad6b06 100644
--- a/phpBB/install/schemas/sqlite_schema.sql
+++ b/phpBB/install/schemas/sqlite_schema.sql
@@ -1,5 +1,5 @@
#
-# $Id$
+# $Id: $
#
BEGIN TRANSACTION;
@@ -359,6 +359,23 @@ CREATE INDEX phpbb_log_topic_id ON phpbb_log (topic_id);
CREATE INDEX phpbb_log_reportee_id ON phpbb_log (reportee_id);
CREATE INDEX phpbb_log_user_id ON phpbb_log (user_id);
+# Table: 'phpbb_login_attempts'
+CREATE TABLE phpbb_login_attempts (
+ attempt_id INTEGER PRIMARY KEY NOT NULL ,
+ attempt_ip varchar(40) NOT NULL DEFAULT '',
+ attempt_browser varchar(150) NOT NULL DEFAULT '',
+ attempt_forwarded_for varchar(255) NOT NULL DEFAULT '',
+ attempt_time INTEGER UNSIGNED NOT NULL DEFAULT '0',
+ user_id INTEGER UNSIGNED NOT NULL DEFAULT '0',
+ username varchar(255) NOT NULL DEFAULT '0',
+ username_clean varchar(255) NOT NULL DEFAULT '0'
+);
+
+CREATE INDEX phpbb_login_attempts_attempt_ip ON phpbb_login_attempts (attempt_ip, attempt_time);
+CREATE INDEX phpbb_login_attempts_attempt_forwarded_for ON phpbb_login_attempts (attempt_forwarded_for, attempt_time);
+CREATE INDEX phpbb_login_attempts_attempt_time ON phpbb_login_attempts (attempt_time);
+CREATE INDEX phpbb_login_attempts_user_id ON phpbb_login_attempts (user_id);
+
# Table: 'phpbb_moderator_cache'
CREATE TABLE phpbb_moderator_cache (
forum_id INTEGER UNSIGNED NOT NULL DEFAULT '0',
diff --git a/phpBB/language/en/acp/board.php b/phpBB/language/en/acp/board.php
index 5306936812..0800aa27b4 100644
--- a/phpBB/language/en/acp/board.php
+++ b/phpBB/language/en/acp/board.php
@@ -461,12 +461,18 @@ $lang = array_merge($lang, array(
'FORM_TIME_MAX_EXPLAIN' => 'The time a user has to submit a form. Use -1 to disable. Note that a form might become invalid if the session expires, regardless of this setting.',
'FORM_SID_GUESTS' => 'Tie forms to guest sessions',
'FORM_SID_GUESTS_EXPLAIN' => 'If enabled, the form token issued to guests will be session-exclusive. This can cause problems with some ISPs.',
- 'FORWARDED_FOR_VALID' => 'Validated <var>X_FORWARDED_FOR</var> header',
+ 'FORWARDED_FOR_VALID' => 'Validate <var>X_FORWARDED_FOR</var> header',
'FORWARDED_FOR_VALID_EXPLAIN' => 'Sessions will only be continued if the sent <var>X_FORWARDED_FOR</var> header equals the one sent with the previous request. Bans will be checked against IPs in <var>X_FORWARDED_FOR</var> too.',
'IP_VALID' => 'Session IP validation',
'IP_VALID_EXPLAIN' => 'Determines how much of the users IP is used to validate a session; <samp>All</samp> compares the complete address, <samp>A.B.C</samp> the first x.x.x, <samp>A.B</samp> the first x.x, <samp>None</samp> disables checking. On IPv6 addresses <samp>A.B.C</samp> compares the first 4 blocks and <samp>A.B</samp> the first 3 blocks.',
- 'MAX_LOGIN_ATTEMPTS' => 'Maximum number of login attempts',
- 'MAX_LOGIN_ATTEMPTS_EXPLAIN' => 'After this number of failed logins the user needs to additionally solve the anti-spambot task.',
+ 'IP_LOGIN_LIMIT_MAX' => 'Maximum number of login attempts per IP address',
+ 'IP_LOGIN_LIMIT_MAX_EXPLAIN' => 'The threshold of login attempts allowed from a single IP address before an anti-spambot task is triggered.Enter 0 to prevent the anti-spambot task from being triggered by IP addresses.',
+ 'IP_LOGIN_LIMIT_TIME' => 'IP address login attempt expiration time',
+ 'IP_LOGIN_LIMIT_TIME_EXPLAIN' => 'Login attempts expire after this period, in seconds.',
+ 'IP_LOGIN_LIMIT_USE_FORWARDED' => 'Limit login attempts by <var>X_FORWARDED_FOR</var> header',
+ 'IP_LOGIN_LIMIT_USE_FORWARDED_EXPLAIN' => 'Instead of limiting login attempts by IP address they are limited by <var>X_FORWARDED_FOR</var> values. <br /><em><strong>Warning:</strong> Only enable this if you are operating a proxy server that sets <var>X_FORWARDED_FOR</var> to trustworthy values.</em>',
+ 'MAX_LOGIN_ATTEMPTS' => 'Maximum number of login attempts per username',
+ 'MAX_LOGIN_ATTEMPTS_EXPLAIN' => 'The number of login attempts allowed for a single account before the anti-spambot task is triggered. Enter 0 to prevent the anti-spambot task from being trigger for distinct user accounts.',
'NO_IP_VALIDATION' => 'None',
'NO_REF_VALIDATION' => 'None',
'PASSWORD_TYPE' => 'Password complexity',