Commit 4e4ea71e authored by Dries's avatar Dries

Patch #1161486 by rootatwc, sun, andypost, catch, marcingy, aspilicious,...

Patch #1161486 by rootatwc, sun, andypost, catch, marcingy, aspilicious, cam8001: Move IP blocking into its own module.
parent 2f31f5ce
......@@ -1888,56 +1888,6 @@ function drupal_set_title($title = NULL, $output = CHECK_PLAIN) {
return $stored_title;
}
/**
* Checks to see if an IP address has been blocked.
*
* Blocked IP addresses are stored in the database by default. However for
* performance reasons we allow an override in settings.php. This allows us
* to avoid querying the database at this critical stage of the bootstrap if
* an administrative interface for IP address blocking is not required.
*
* @param $ip
* IP address to check.
*
* @return bool
* TRUE if access is denied, FALSE if access is allowed.
*/
function drupal_is_denied($ip) {
// Because this function is called on every page request, we first check
// for an array of IP addresses in settings.php before querying the
// database.
$blocked_ips = variable_get('blocked_ips');
$denied = FALSE;
if (isset($blocked_ips) && is_array($blocked_ips)) {
$denied = in_array($ip, $blocked_ips);
}
// Only check if database.inc is loaded already. If
// $conf['page_cache_without_database'] = TRUE; is set in settings.php,
// then the database won't be loaded here so the IPs in the database
// won't be denied. However the user asked explicitly not to use the
// database and also in this case it's quite likely that the user relies
// on higher performance solutions like a firewall.
elseif (class_exists('Drupal\Core\Database\Database', FALSE)) {
$denied = (bool)db_query("SELECT 1 FROM {blocked_ips} WHERE ip = :ip", array(':ip' => $ip))->fetchField();
}
return $denied;
}
/**
* Handles denied users.
*
* @param $ip
* IP address to check. Prints a message and exits if access is denied.
*/
function drupal_block_denied($ip) {
// Deny access to blocked IP addresses - t() is not yet available.
if (drupal_is_denied($ip)) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
print 'Sorry, ' . check_plain(ip_address()) . ' has been banned.';
exit();
}
}
/**
* Returns a string of highly randomized bytes (over the full 8-bit range).
*
......@@ -2314,7 +2264,6 @@ function _drupal_bootstrap_page_cache() {
$config = config('system.performance');
$cache_enabled = $config->get('cache.page.enabled');
}
drupal_block_denied(ip_address());
// If there is no session cookie and cache is enabled (or forced), try
// to serve a cached page.
if (!isset($_COOKIE[session_name()]) && $cache_enabled) {
......
<?php
/**
* @file
* Administrative functionality for the Ban module.
*/
/**
* Page callback; Displays banned IP addresses.
*
* @param string $default_ip
* (optional) IP address to be passed on to drupal_get_form() for
* use as the default value of the IP address form field.
*/
function ban_admin_page($default_ip = '') {
$rows = array();
$header = array(t('banned IP addresses'), t('Operations'));
$result = db_query('SELECT * FROM {ban_ip}');
foreach ($result as $ip) {
$rows[] = array(
$ip->ip,
l(t('delete'), "admin/config/people/ban/delete/$ip->iid"),
);
}
$build['ban_ip_form'] = drupal_get_form('ban_ip_form', $default_ip);
$build['ban_ip_banning_table'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => t('No blocked IP addresses available.'),
);
return $build;
}
/**
* Form constructor for banning an IP address.
*
* @param string $default_ip
* An IP address to ban, used as default value.
*
* @see ban_ip_form_validate()
* @see ban_ip_form_submit()
*
* @ingroup forms
*/
function ban_ip_form($form, &$form_state, $default_ip) {
$form['ip'] = array(
'#title' => t('IP address'),
'#type' => 'textfield',
'#size' => 48,
'#maxlength' => 40,
'#default_value' => $default_ip,
'#description' => t('Enter a valid IP address.'),
);
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Add'),
);
return $form;
}
/**
* Form validation handler for ban_ip_form().
*
* @see ban_ip_form_submit()
*/
function ban_ip_form_validate($form, &$form_state) {
$ip = trim($form_state['values']['ip']);
if (db_query("SELECT * FROM {ban_ip} WHERE ip = :ip", array(':ip' => $ip))->fetchField()) {
form_set_error('ip', t('This IP address is already banned.'));
}
elseif ($ip == ip_address()) {
form_set_error('ip', t('You may not ban your own IP address.'));
}
elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) == FALSE) {
form_set_error('ip', t('Enter a valid IP address.'));
}
}
/**
* Form submission handler for ban_ip_form().
*
* @see ban_ip_form_validate()
*/
function ban_ip_form_submit($form, &$form_state) {
$ip = trim($form_state['values']['ip']);
db_insert('ban_ip')
->fields(array('ip' => $ip))
->execute();
drupal_set_message(t('The IP address %ip has been banned.', array('%ip' => $ip)));
$form_state['redirect'] = 'admin/config/people/ban';
}
/**
* Form constructor to unban an IP address.
*
* @param array $ban_ip
* The IP address record to unban, as provided by ban_ip_load().
*
* @see ban_ip_delete_submit()
*/
function ban_ip_delete_form($form, &$form_state, array $ban_ip) {
$form['ban_ip'] = array(
'#type' => 'value',
'#value' => $ban_ip,
);
return confirm_form($form,
t('Are you sure you want to delete %ip?', array('%ip' => $ban_ip['ip'])),
'admin/config/people/ban',
NULL,
t('Delete')
);
}
/**
* Form submission handler for ban_ip_delete_form().
*/
function ban_ip_delete_form_submit($form, &$form_state) {
$banned_ip = $form_state['values']['ban_ip'];
db_delete('ban_ip')
->condition('iid', $banned_ip['iid'])
->execute();
watchdog('user', 'Deleted %ip', array('%ip' => $banned_ip['ip']));
drupal_set_message(t('The IP address %ip was deleted.', array('%ip' => $banned_ip['ip'])));
$form_state['redirect'] = 'admin/config/people/ban';
}
name = Ban
description = Enables banning of IP addresses.
package = Core
version = VERSION
core = 8.x
configure = admin/config/people/ban
<?php
/**
* @file
* Install, update and uninstall functions for the Ban module.
*/
/**
* Implements hook_schema().
*/
function ban_schema() {
$schema['ban_ip'] = array(
'description' => 'Stores banned IP addresses.',
'fields' => array(
'iid' => array(
'description' => 'Primary Key: unique ID for IP addresses.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
'ip' => array(
'description' => 'IP address',
'type' => 'varchar',
'length' => 40,
'not null' => TRUE,
'default' => '',
),
),
'indexes' => array(
'ip' => array('ip'),
),
'primary key' => array('iid'),
);
return $schema;
}
<?php
/**
* @file
* Enables banning of IP addresses.
*/
/**
* Implements hook_help().
*/
function ban_help($path, $arg) {
switch ($path) {
case 'admin/help#ban':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Ban module allows administrators to ban visits to their site from given IP addresses.') . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Banning IP addresses') . '</dt>';
$output .= '<dd>' . t('Administrators can enter IP addresses to ban on the <a href="@bans">IP address bans</a> page.', array('@bans' => url('admin/config/people/ban'))) . '</dd>';
$output .= '</dl>';
return $output;
case 'admin/config/people/ban':
return '<p>' . t('IP addresses listed here are banned from your site. Banned addresses are completely forbidden from accessing the site and instead see a brief message explaining the situation.') . '</p>';
}
}
/**
* Implements hook_permission().
*/
function ban_permission() {
return array(
'ban IP addresses' => array(
'title' => t('Ban IP addresses'),
),
);
}
/**
* Implements hook_menu().
*/
function ban_menu() {
$items['admin/config/people/ban'] = array(
'title' => 'IP address bans',
'description' => 'Manage banned IP addresses.',
'page callback' => 'ban_admin_page',
'access arguments' => array('ban IP addresses'),
'file' => 'ban.admin.inc',
'weight' => 10,
);
$items['admin/config/people/ban/delete/%ban_ip'] = array(
'title' => 'Delete IP address',
'page callback' => 'drupal_get_form',
'page arguments' => array('ban_ip_delete_form', 5),
'access arguments' => array('ban IP addresses'),
'file' => 'ban.admin.inc',
);
return $items;
}
/**
* Implements hook_boot().
*/
function ban_boot() {
ban_block_denied(ip_address());
}
/**
* Returns whether an IP address is blocked.
*
* Blocked IP addresses are stored in the database by default. However, for
* performance reasons we allow an override in variables.
*
* @param string $ip
* The IP address to check.
*
* @return bool
* TRUE if access is denied, FALSE if access is allowed.
*/
function ban_is_denied($ip) {
$denied = FALSE;
// Because this function is called on every page request, we first check
// for an array of IP addresses in settings.php before querying the
// database.
$blocked_ips = variable_get('blocked_ips');
if (isset($blocked_ips) && is_array($blocked_ips)) {
$denied = in_array($ip, $blocked_ips);
}
// If $conf['page_cache_without_database'] = TRUE; is set in settings.php,
// then the database is not available yet, so IPs recorded in the database
// won't be denied. However, the user asked explicitly not to use the
// database, and in this case it's also quite likely that the user relies
// on higher performance solutions like a firewall.
elseif (class_exists('Drupal\Core\Database\Database', FALSE) && function_exists('db_query')) {
$denied = (bool) db_query("SELECT 1 FROM {ban_ip} WHERE ip = :ip", array(':ip' => $ip))->fetchField();
}
return $denied;
}
/**
* Prints a message and exits if access from a given IP address is denied.
*
* @param string $ip
* The IP address to check.
*/
function ban_block_denied($ip) {
// Check whether the given IP address has been blocked.
if (ban_is_denied($ip)) {
header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
// t() is not yet available.
print 'Sorry, ' . check_plain($ip) . ' has been banned.';
exit();
}
}
/**
* Loads a banned IP address record from the database.
*
* @param int $iid
* The ID of the banned IP address to retrieve.
*
* @return array
* The banned IP address record from the database as an array.
*/
function ban_ip_load($iid) {
return db_query("SELECT * FROM {ban_ip} WHERE iid = :iid", array(':iid' => $iid))->fetchAssoc();
}
/**
* Implements hook_action_info().
*/
function ban_action_info() {
return array(
'ban_ip_action' => array(
'type' => 'user',
'label' => t('Ban IP address of current user'),
'configurable' => FALSE,
'triggers' => array('any'),
),
);
}
/**
* Bans the current user's IP address.
*
* @ingroup actions
*/
function ban_ip_action() {
$ip = ip_address();
db_insert('blocked_ips')
->fields(array('ip' => $ip))
->execute();
watchdog('action', 'Banned IP address %ip', array('%ip' => $ip));
}
......@@ -2,88 +2,84 @@
/**
* @file
* Definition of Drupal\system\Tests\System\IpAddressBlockingTest.
* Definition of Drupal\ban\Tests\IpAddressBlockingTest.
*/
namespace Drupal\system\Tests\System;
namespace Drupal\ban\Tests;
use Drupal\simpletest\WebTestBase;
class IpAddressBlockingTest extends WebTestBase {
protected $blocking_user;
/**
* Implement getInfo().
* Modules to enable.
*
* @var array
*/
public static $modules = array('ban');
public static function getInfo() {
return array(
'name' => 'IP address blocking',
'description' => 'Test IP address blocking.',
'group' => 'System'
'name' => 'IP address banning',
'description' => 'Test IP address banning.',
'group' => 'Ban'
);
}
/**
* Implement setUp().
*/
function setUp() {
parent::setUp();
// Create user.
$this->blocking_user = $this->drupalCreateUser(array('block IP addresses'));
$this->drupalLogin($this->blocking_user);
}
/**
* Test a variety of user input to confirm correct validation and saving of data.
*/
function testIPAddressValidation() {
$this->drupalGet('admin/config/people/ip-blocking');
// Create user.
$admin_user = $this->drupalCreateUser(array('ban IP addresses'));
$this->drupalLogin($admin_user);
$this->drupalGet('admin/config/people/ban');
// Block a valid IP address.
// Ban a valid IP address.
$edit = array();
$edit['ip'] = '192.168.1.1';
$this->drupalPost('admin/config/people/ip-blocking', $edit, t('Add'));
$ip = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $edit['ip']))->fetchField();
$this->drupalPost('admin/config/people/ban', $edit, t('Add'));
$ip = db_query("SELECT iid from {ban_ip} WHERE ip = :ip", array(':ip' => $edit['ip']))->fetchField();
$this->assertTrue($ip, t('IP address found in database.'));
$this->assertRaw(t('The IP address %ip has been blocked.', array('%ip' => $edit['ip'])), t('IP address was blocked.'));
$this->assertRaw(t('The IP address %ip has been banned.', array('%ip' => $edit['ip'])), 'IP address was banned.');
// Try to block an IP address that's already blocked.
$edit = array();
$edit['ip'] = '192.168.1.1';
$this->drupalPost('admin/config/people/ip-blocking', $edit, t('Add'));
$this->assertText(t('This IP address is already blocked.'));
$this->drupalPost('admin/config/people/ban', $edit, t('Add'));
$this->assertText(t('This IP address is already banned.'));
// Try to block a reserved IP address.
$edit = array();
$edit['ip'] = '255.255.255.255';
$this->drupalPost('admin/config/people/ip-blocking', $edit, t('Add'));
$this->drupalPost('admin/config/people/ban', $edit, t('Add'));
$this->assertText(t('Enter a valid IP address.'));
// Try to block a reserved IP address.
$edit = array();
$edit['ip'] = 'test.example.com';
$this->drupalPost('admin/config/people/ip-blocking', $edit, t('Add'));
$this->drupalPost('admin/config/people/ban', $edit, t('Add'));
$this->assertText(t('Enter a valid IP address.'));
// Submit an empty form.
$edit = array();
$edit['ip'] = '';
$this->drupalPost('admin/config/people/ip-blocking', $edit, t('Add'));
$this->drupalPost('admin/config/people/ban', $edit, t('Add'));
$this->assertText(t('Enter a valid IP address.'));
// Pass an IP address as a URL parameter and submit it.
$submit_ip = '1.2.3.4';
$this->drupalPost('admin/config/people/ip-blocking/' . $submit_ip, NULL, t('Add'));
$ip = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $submit_ip))->fetchField();
$this->assertTrue($ip, t('IP address found in database'));
$this->assertRaw(t('The IP address %ip has been blocked.', array('%ip' => $submit_ip)), t('IP address was blocked.'));
$this->drupalPost('admin/config/people/ban/' . $submit_ip, NULL, t('Add'));
$ip = db_query("SELECT iid from {ban_ip} WHERE ip = :ip", array(':ip' => $submit_ip))->fetchField();
$this->assertTrue($ip, 'IP address found in database');
$this->assertRaw(t('The IP address %ip has been banned.', array('%ip' => $submit_ip)), 'IP address was banned.');
// Submit your own IP address. This fails, although it works when testing manually.
// TODO: on some systems this test fails due to a bug or inconsistency in cURL.
// $edit = array();
// $edit['ip'] = ip_address();
// $this->drupalPost('admin/config/people/ip-blocking', $edit, t('Save'));
// $this->assertText(t('You may not block your own IP address.'));
// Submit your own IP address. This fails, although it works when testing
// manually.
// TODO: on some systems this test fails due to a bug or inconsistency in cURL.
// $edit = array();
// $edit['ip'] = ip_address();
// $this->drupalPost('admin/config/people/ban', $edit, t('Save'));
// $this->assertText(t('You may not ban your own IP address.'));
}
}
......@@ -13,8 +13,8 @@
class StatisticsBlockVisitorsTest extends StatisticsTestBase {
public static function getInfo() {
return array(
'name' => 'Top visitor blocking',
'description' => 'Tests blocking of IP addresses via the top visitors report.',
'name' => 'Top visitor banning',
'description' => 'Tests banning of IP addresses via the top visitors report.',
'group' => 'Statistics'
);
}
......@@ -27,32 +27,32 @@ function testIPAddressBlocking() {
$test_ip_address = '192.168.1.1';
// Verify the IP address from accesslog appears on the top visitors page
// and that a 'block IP address' link is displayed.
// and that a 'ban IP address' link is displayed.
$this->drupalLogin($this->blocking_user);
$this->drupalGet('admin/reports/visitors');
$this->assertText($test_ip_address, t('IP address found.'));
$this->assertText(t('block IP address'), t('Block IP link displayed'));
$this->assertText($test_ip_address, 'IP address found.');
$this->assertText(t('ban IP address'), 'Ban IP link displayed');
// Block the IP address.
$this->clickLink('block IP address');
$this->assertText(t('IP address blocking'), t('IP blocking page displayed.'));
$this->clickLink('ban IP address');
$this->assertText(t('IP address bans'), 'IP banning page displayed.');
$edit = array();
$edit['ip'] = $test_ip_address;
$this->drupalPost('admin/config/people/ip-blocking', $edit, t('Add'));
$ip = db_query("SELECT iid from {blocked_ips} WHERE ip = :ip", array(':ip' => $edit['ip']))->fetchField();
$this->assertNotEqual($ip, FALSE, t('IP address found in database'));
$this->assertRaw(t('The IP address %ip has been blocked.', array('%ip' => $edit['ip'])), t('IP address was blocked.'));
$this->drupalPost('admin/config/people/ban', $edit, t('Add'));
$ip = db_query("SELECT iid from {ban_ip} WHERE ip = :ip", array(':ip' => $edit['ip']))->fetchField();
$this->assertNotEqual($ip, FALSE, 'IP address found in database');
$this->assertRaw(t('The IP address %ip has been banned.', array('%ip' => $edit['ip'])), 'IP address was banned.');
// Verify that the block/unblock link on the top visitors page has been
// altered.
$this->drupalGet('admin/reports/visitors');
$this->assertText(t('unblock IP address'), t('Unblock IP address link displayed'));
$this->assertText(t('unban IP address'), 'Unban IP address link displayed');
// Unblock the IP address.
$this->clickLink('unblock IP address');
$this->assertRaw(t('Are you sure you want to delete %ip?', array('%ip' => $test_ip_address)), t('IP address deletion confirmation found.'));
$this->clickLink('unban IP address');
$this->assertRaw(t('Are you sure you want to delete %ip?', array('%ip' => $test_ip_address)), 'IP address deletion confirmation found.');
$edit = array();
$this->drupalPost('admin/config/people/ip-blocking/delete/1', NULL, t('Delete'));
$this->assertRaw(t('The IP address %ip was deleted.', array('%ip' => $test_ip_address)), t('IP address deleted.'));
$this->drupalPost('admin/config/people/ban/delete/1', NULL, t('Delete'));
$this->assertRaw(t('The IP address %ip was deleted.', array('%ip' => $test_ip_address)), 'IP address deleted.');
}
}
......@@ -19,7 +19,7 @@ abstract class StatisticsTestBase extends WebTestBase {
*
* @var array
*/
public static $modules = array('node', 'block', 'statistics');
public static $modules = array('node', 'block', 'ban', 'statistics');
function setUp() {
parent::setUp();
......@@ -34,7 +34,7 @@ function setUp() {
'access administration pages',
'access site reports',
'access statistics',
'block IP addresses',
'ban IP addresses',
'administer blocks',
'administer statistics',
'administer users',
......
......@@ -121,17 +121,20 @@ function statistics_top_pages() {
* A render array containing the top visitors information.
*/
function statistics_top_visitors() {
$ban_exists = module_exists('ban');
$header = array(
array('data' => t('Hits'), 'field' => 'hits', 'sort' => 'desc'),
array('data' => t('Visitor'), 'field' => 'u.name'),
array('data' => t('Total page generation time'), 'field' => 'total'),
array('data' => user_access('block IP addresses') ? t('Operations') : '', 'colspan' => 2),
array('data' => $ban_exists && user_access('ban IP addresses') ? t('Operations') : '', 'colspan' => 2),
);
$query = db_select('accesslog', 'a', array('target' => 'slave'))
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->extend('Drupal\Core\Database\Query\TableSortExtender');
$query->leftJoin('blocked_ips', 'bl', 'a.hostname = bl.ip');
if ($ban_exists) {
$query->leftJoin('ban_ip', 'b', 'a.hostname = b.ip');
}
$query->leftJoin('users', 'u', 'a.uid = u.uid');
$query->addExpression('COUNT(a.uid)', 'hits');
......@@ -139,13 +142,16 @@ function statistics_top_visitors() {
$query
->fields('a', array('uid', 'hostname'))
->fields('u', array('name'))
->fields('bl', array('iid'))
->groupBy('a.hostname')
->groupBy('a.uid')
->groupBy('u.name')
->groupBy('bl.iid')
->limit(30)
->orderByHeader($header);
->limit(30);
if ($ban_exists) {
$query
->fields('b', array('iid'))
->groupBy('b.iid');
}
$query->orderByHeader($header);
$uniques_query = db_select('accesslog')->distinct();
$uniques_query->fields('accesslog', array('uid', 'hostname'));
......@@ -157,8 +163,20 @@ function statistics_top_visitors() {
$rows = array();
$destination = drupal_get_destination();
foreach ($result as $account) {
$ban_link = $account->iid ? l(t('unblock IP address'), "admin/config/people/ip-blocking/delete/$account->iid", array('query' => $destination)) : l(t('block IP address'), "admin/config/people/ip-blocking/$account->hostname", array('query' => $destination));
$rows[] = array($account->hits, ($account->uid ? theme('username', array('account' => $account)) : $account->hostname), format_interval(round($account->total / 1000)), (user_access('block IP addresses') && !$account->uid) ? $ban_link : '');
if ($ban_exists) {
if ($account->iid) {
$ban_link = l(t('unban IP address'), "admin/config/people/ban/delete/$account->iid", array('query' => $destination));
}
else {
$ban_link = l(t('ban IP address'), "admin/config/people/ban/$account->hostname", array('query' => $destination));
}
}
$row = array();
$row[] = $account->hits;
$row[] = ($account->uid ? theme('username', array('account' => $account)) : $account->hostname);
$row[] = format_interval(round($account->total / 1000));
$row[] = ($ban_exists && user_access('ban IP addresses') && !$account->uid ? $ban_link : '');
$rows[] = $row;
}
drupal_set_title(t('Top visitors in the past %interval', array('%interval' => format_interval(config('statistics.settings')->get('access_log.max_lifetime')))), PASS_THROUGH);
......
......@@ -1371,111 +1371,6 @@ function system_modules_uninstall_submit($form, &$form_state) {
}
}
/**
* Menu callback. Display blocked IP addresses.
*
* @param $default_ip
* Optional IP address to be passed on to drupal_get_form() for
* use as the default value of the IP address form field.
*/
function system_ip_blocking($default_ip = '') {
$rows = array();
$header = array(t('Blocked IP addresses'), t('Operations'));
$result = db_query('SELECT * FROM {blocked_ips}');
foreach ($result as $ip) {
$rows[] = array(
$ip->ip,
l(t('delete'), "admin/config/people/ip-blocking/delete/$ip->iid"),
);
}
$build['system_ip_blocking_form'] = drupal_get_form('system_ip_blocking_form', $default_ip);
$build['system_ip_blocking_table'] = array(
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => t('No blocked IP addresses available.'),
);
return $build;
}
/**
* Define the form for blocking IP addresses.
*
* @ingroup forms
* @see system_ip_blocking_form_validate()
* @see system_ip_blocking_form_submit()
*/
function system_ip_blocking_form($form, $form_state, $default_ip) {
$form['ip'] = array(
'#title' => t('IP address'),
'#type' => 'textfield',
'#size' => 48,