<?php
/**
* This file implements the UI controller for the antispam management.
*
* This file is part of the b2evolution/evocms project - {@link http://b2evolution.net/}.
* See also {@link https://github.com/b2evolution/b2evolution}.
*
* @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
*
* @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}.
* Parts of this file are copyright (c)2004 by Vegar BERG GULDAL - {@link http://funky-m.com/}.
*
* @package admin
*
* @todo Allow applying / re-checking of the known data, not just after an update!
*/
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
param( 'display_mode', 'string' );
if( $display_mode != 'js' )
{
$AdminUI->set_path( 'options', 'antispam' );
}
else
{ // This is an Ajax response
// fp> TODO: have a more systematic way of handling AJAX responses
header_content_type( 'text/html', $io_charset );
}
param_action( '' );
param( 'confirm', 'string' );
param( 'keyword', 'string', '', true );
param( 'domain', 'string' );
param( 'filteron', 'string', '', true );
if( $action == 'iprange_edit' )
{ // Memorize action for users and sessions list below edit form:
memorize_param( 'action', 'string', '', $action );
}
else
{ // Don't initialize this param as array because it must be string for UserList:
param( 'filter', 'array:string', array() );
}
$tab = param( 'tab', 'string', '', true );
$tab3 = param( 'tab3', 'string', '', true );
$tool = param( 'tool', 'string', '', true );
if( isset($filter['off']) )
{
unset( $filteron );
forget_param( 'filteron' );
}
// Check permission:
if( ! ( check_user_perm( 'admin', 'normal' ) && check_user_perm( 'spamblacklist', 'view' ) ) &&
! ( check_user_perm( 'users', 'moderate' ) && ( ( $tab3 == 'tools' && $tool == 'whois' && empty( $action ) ) || $action == 'whois' ) ) )
{
debug_die( sprintf( /* %s is the application name, usually "b2evolution" */ TB_('Group/user permission denied by %s!'), $app_name ) );
}
if( param( 'iprange_ID', 'integer', '', true) )
{ // Load IP Range object
load_class( 'antispam/model/_iprange.class.php', 'IPRange' );
$IPRangeCache = & get_IPRangeCache();
if( ( $edited_IPRange = & $IPRangeCache->get_by_ID( $iprange_ID, false ) ) === false )
{ // We could not find the goal to edit:
unset( $edited_IPRange );
forget_param( 'iprange_ID' );
$Messages->add( sprintf( TB_('Requested «%s» object does not exist any longer.'), TB_('IP Range') ), 'error' );
}
}
switch( $action )
{
case 'ban': // only an action if further "actions" given
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'antispam' );
// Check permission:
check_user_perm( 'spamblacklist', 'edit', true ); // TODO: This should become different for 'edit'/'add' perm level - check for 'add' here.
$keyword = utf8_substr( $keyword, 0, 80 );
param( 'delhits', 'integer', 0 );
$all_statuses = get_visibility_statuses( 'keys', array( 'trash', 'redirected' ) );
$delstatuses = array();
foreach( $all_statuses as $status )
{ // collect which comments should be delteded
if( param( 'del'.$status, 'integer', 0 ) )
{ // matching comments with this status should be deleted
$delstatuses[] = $status;
}
}
$delcomments = count( $delstatuses );
param( 'blacklist_locally', 'integer', 0 );
param( 'report', 'integer', 0 );
// Check if the string is too short,
// it has to be a minimum of 5 characters to avoid being too generic
if( utf8_strlen( $keyword ) < 5 )
{
$Messages->add( sprintf( TB_('The keyword «%s» is too short, it has to be a minimum of 5 characters!'), htmlspecialchars( $keyword ) ), 'error' );
break;
}
if( empty( $confirm ) )
{ // No confirmed action, Execute the ban actions only after confirmation
break;
}
if( $delhits )
{ // Delete all banned hit-log entries
$r = $DB->query( 'DELETE FROM T_hitlog
WHERE hit_referer LIKE '.$DB->quote( '%'.$keyword.'%' ),
'Delete all banned hit-log entries' );
$Messages->add_to_group( sprintf( TB_('Deleted %d logged hits matching «%s».'), $r, htmlspecialchars( $keyword ) ), 'success', TB_('Banning keyword:') );
}
if( $delcomments )
{ // select banned comments
$del_condition = blog_restrict( $delstatuses );
$keyword_cond = '(comment_author LIKE '.$DB->quote( '%'.$keyword.'%' ).'
OR comment_author_email LIKE '.$DB->quote( '%'.utf8_strtolower( $keyword ).'%' ).'
OR comment_author_url LIKE '.$DB->quote( '%'.$keyword.'%' ).'
OR comment_content LIKE '.$DB->quote( '%'.$keyword.'%' ).')';
// asimo> we don't need transaction here
$query = 'SELECT comment_ID FROM T_comments
WHERE '.$keyword_cond.$del_condition;
$deleted_ids = $DB->get_col( $query, 0, 'Get comment ids awaiting for delete' );
$r = count( $deleted_ids );
$deleted_ids = implode( ',', $deleted_ids );
// Delete all comments data from DB
Comment::db_delete_where( $keyword_cond.$del_condition );
$Messages->add_to_group( sprintf( TB_('Deleted %d comments matching «%s».'), $r, htmlspecialchars( $keyword ) ), 'success', TB_('Banning keyword:') );
}
if( $blacklist_locally )
{ // Local blacklist:
if( antispam_create( $keyword ) )
{ // Success
$Messages->add_to_group( sprintf( TB_('The keyword «%s» has been blacklisted locally.'), htmlspecialchars( $keyword ) ), 'success', TB_('Banning keyword:') );
}
else
{ // Failed
$Messages->add( sprintf( TB_('Failed to add the keyword %s to black list locally.'), '<b>'.htmlspecialchars( $keyword ).'</b>' ), 'error' );
}
}
if( $report )
{ // Report this keyword as abuse remotely:
antispam_report_abuse( $keyword );
}
if( ! $blacklist_locally && ! $report && ! $delhits && ! $delcomments )
{ // If no action has been selected
$Messages->add( TB_('Please select at least one action to ban the keyword.' ), 'error' );
}
if( $display_mode != 'js' && ! $Messages->has_errors() )
{
// Redirect so that a reload doesn't write to the DB twice:
header_redirect( '?ctrl=antispam', 303 ); // Will EXIT
// We have EXITed already at this point!!
}
if( $display_mode == 'js' && ! $Messages->has_errors() )
{ // Initialize JS functions to execute after action from modal window:
$javascript_messages = array();
if( $delcomments && $r ) // $r not null => means the commentlist was deleted successfully
{
$javascript_messages['refreshAfterBan'] = array( $deleted_ids );
}
$javascript_messages['closeModalAfterBan'] = array();
}
// We'll ask the user later what to do, if no "sub-action" given.
break;
case 'remove':
// Remove a domain from ban list:
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'antispam' );
// Check permission:
check_user_perm( 'spamblacklist', 'edit', true );
param( 'hit_ID', 'integer', true ); // Required!
$Messages->add( sprintf( TB_('Removing entry #%d from the ban list...'), $hit_ID), 'note' );
antispam_delete( $hit_ID );
break;
case 'report':
// Report an entry as abuse to centralized blacklist:
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'antispam' );
// Check permission:
check_user_perm( 'spamblacklist', 'edit', true );
// Report this keyword as abuse:
antispam_report_abuse( $keyword );
break;
case 'poll':
// request abuse list from central blacklist:
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'antispam' );
// Check permission:
check_user_perm( 'spamblacklist', 'edit', true );
ob_start();
antispam_poll_abuse();
$Debuglog->add( ob_get_contents(), 'antispam_poll' );
ob_end_clean();
break;
case 'update':
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'antispam' );
// Check permission:
check_user_perm( 'options', 'edit', true );
// fp> Restore defaults has been removed because it's extra maintenance work and no real benefit to the user.
param_integer_range( 'antispam_threshold_publish', -100, 100, TB_('The threshold must be between -100 and 100.') );
$Settings->set( 'antispam_threshold_publish', $antispam_threshold_publish );
param_integer_range( 'antispam_threshold_delete', -100, 100, TB_('The threshold must be between -100 and 100.') );
$Settings->set( 'antispam_threshold_delete', $antispam_threshold_delete );
param( 'antispam_block_contact_form', 'integer', 0 );
$Settings->set( 'antispam_block_contact_form', $antispam_block_contact_form );
param( 'antispam_block_spam_referers', 'integer', 0 );
$Settings->set( 'antispam_block_spam_referers', $antispam_block_spam_referers );
param( 'antispam_report_to_central', 'integer', 0 );
$Settings->set( 'antispam_report_to_central', $antispam_report_to_central );
$changed_weight = false;
param( 'antispam_plugin_spam_weight', 'array:integer', array() );
foreach( $antispam_plugin_spam_weight as $l_plugin_ID => $l_weight )
{
if( ! is_numeric($l_weight) )
{
continue;
}
if( $l_weight < 0 || $l_weight > 100 )
{
param_error( 'antispam_plugin_spam_weight['.$l_plugin_ID.']', TB_('Spam weight has to be in the range of 0-100.') );
continue;
}
if( $DB->query( '
UPDATE T_plugins
SET plug_spam_weight = '.$DB->quote($l_weight).'
WHERE plug_ID = '.(int)$l_plugin_ID ) )
{
$changed_weight = true;
}
}
if( $changed_weight )
{ // Reload plugins table (for display):
$Plugins->loaded_plugins_table = false;
$Plugins->load_plugins_table();
}
// Suspicious users
$Settings->set( 'antispam_suspicious_group', param( 'antispam_suspicious_group', 'integer', 0 ) );
// Trust groups
$trust_groups = param( 'antispam_trust_groups', 'array:integer', array() );
$Settings->set( 'antispam_trust_groups', implode( ',', $trust_groups ) );
if( ! $Messages->has_errors() )
{
$Settings->dbupdate();
$Messages->add( TB_('Settings updated.'), 'success' );
// Redirect so that a reload doesn't write to the DB twice:
header_redirect( '?ctrl=antispam&tab3=settings', 303 ); // Will EXIT
// We have EXITed already at this point!!
}
case 'find_spam_comments':
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'antispam' );
// Check permission:
check_user_perm( 'options', 'edit', true );
$keywords = $DB->get_col( 'SELECT askw_string FROM T_antispam__keyword' );
$keywords = array_chunk( $keywords, 100 );
$rows_affected = 0;
@ignore_user_abort(true);
set_max_execution_time(900);
// Delete comments in chunks of 100 keywords per SQL query
foreach( $keywords as $chunk )
{
$arr = array();
foreach( $chunk as $word )
{
$arr[] = $DB->quote('%'.$word.'%');
}
$DB->query('DELETE FROM T_comments
WHERE (comment_author LIKE '.implode(' OR comment_author LIKE ', $arr).')
OR (comment_author_email LIKE '.implode(' OR comment_author_email LIKE ', $arr).')
OR (comment_author_url LIKE '.implode(' OR comment_author_url LIKE ', $arr).')
OR (comment_content LIKE '.implode(' OR comment_content LIKE ', $arr).')',
'Delete spam comments');
$rows_affected = $rows_affected + $DB->rows_affected;
}
$Messages->add( sprintf( TB_('Deleted %d comments'), $rows_affected ), 'success' );
break;
case 'find_spam_referers':
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'antispam' );
// Check permission:
check_user_perm( 'options', 'edit', true );
$keywords = $DB->get_col( 'SELECT askw_string FROM T_antispam__keyword' );
$keywords = array_chunk( $keywords, 100 );
$rows_affected = 0;
@ignore_user_abort(true);
set_max_execution_time(900);
// Delete hits in chunks of 100 keywords per SQL query
foreach( $keywords as $chunk )
{
$arr = array();
foreach( $chunk as $word )
{
$arr[] = $DB->quote('%'.$word.'%');
}
$DB->query('DELETE FROM T_hitlog
WHERE hit_referer LIKE '.implode(' OR hit_referer LIKE ', $arr),
'Delete all banned hit-log entries' );
$rows_affected = $rows_affected + $DB->rows_affected;
}
$Messages->add( sprintf( TB_('Deleted %d logged hits'), $rows_affected ), 'success' );
break;
case 'iprange_create':
// Create new IP Range...
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'iprange' );
// Check permission:
check_user_perm( 'spamblacklist', 'edit', true );
$edited_IPRange = new IPRange();
// load data from request
if( $edited_IPRange->load_from_Request() )
{ // We could load data from form without errors:
// Insert in DB:
$edited_IPRange->dbinsert();
$Messages->add( TB_('New IP Range created.'), 'success' );
// Redirect so that a reload doesn't write to the DB twice:
header_redirect( '?ctrl=antispam&tab3=ipranges&iprange_ID='.$edited_IPRange->ID.'&action=iprange_edit&filter=new', 303 ); // Will EXIT
// We have EXITed already at this point!!
}
$action = 'iprange_new';
break;
case 'iprange_update':
// Update IP Range...
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'iprange' );
// Check permission:
check_user_perm( 'spamblacklist', 'edit', true );
// Make sure we got an iprange_ID:
param( 'iprange_ID', 'integer', true );
// load data from request
if( $edited_IPRange->load_from_Request() )
{ // We could load data from form without errors:
// Update IP Range in DB:
$edited_IPRange->dbupdate();
$Messages->add( TB_('IP Range updated.'), 'success' );
// Redirect so that a reload doesn't write to the DB twice:
header_redirect( '?ctrl=antispam&tab='.$tab.'&tab3=ipranges&iprange_ID='.$edited_IPRange->ID.'&action=iprange_edit', 303 ); // Will EXIT
// We have EXITed already at this point!!
}
$action = 'iprange_edit';
break;
case 'iprange_delete':
// Delete IP Range...
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'iprange' );
// Check permission:
check_user_perm( 'spamblacklist', 'edit', true );
// Make sure we got an iprange_ID:
param( 'iprange_ID', 'integer', true );
if( $edited_IPRange->dbdelete() )
{
$Messages->add( TB_('IP Range deleted.'), 'success' );
// Redirect so that a reload doesn't write to the DB twice:
header_redirect( '?ctrl=antispam&tab='.$tab.'&tab3=ipranges', 303 ); // Will EXIT
// We have EXITed already at this point!!
}
break;
case 'bankruptcy_delete':
// Delete ALL comments from selected blogs
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'antispam' );
// Check permission:
check_user_perm( 'options', 'edit', true );
$bankruptcy_blogs_IDs = param( 'bankruptcy_blogs', 'array:integer', array() );
if( empty( $bankruptcy_blogs ) )
{
$Messages->add( TB_('Please select at least one blog.'), 'error' );
}
if( !param_errors_detected() )
{ // Try to obtain some serious time to do some serious processing (15 minutes)
set_max_execution_time( 900 );
// Turn off the output buffering to do the correct work of the function flush()
@ini_set( 'output_buffering', 'off' );
// Set this to start deleting in the template file
$delete_bankruptcy_blogs = true;
}
break;
case 'whois':
$tab = '';
$tab3 = 'tools';
$tool = 'whois';
$query = param( 'query', 'string', NULL );
if( empty( $query ) )
{
param_error( 'query', TB_('You must specify an IP address or domain to query') );
}
else
{
$template_action = 'whois';
}
break;
}
if( $display_mode != 'js' )
{
if( $tab == 'stats' )
{
// We should activate toolbar menu items for this controller and tab
$activate_collection_toolbar = true;
if( isset( $collections_Module ) )
{ // Display list of blogs:
if( check_user_perm( 'stats', 'view' ) )
{
$AdminUI->set_coll_list_params( 'stats', 'view', array( 'ctrl' => 'antispam', 'tab' => $tab, 'tab3' => $tab3 ), TB_('All'),
$admin_url.'?ctrl=antispam&tab='.$tab.'&tab3='.$tab3.'&blog=0', NULL, false, true );
}
else
{ // No permission to view aggregated stats:
$AdminUI->set_coll_list_params( 'stats', 'view', array( 'ctrl' => 'antispam', 'tab' => $tab, 'tab3' => $tab3 ), NULL,
'', NULL, false, true );
}
}
$AdminUI->breadcrumbpath_init( true, array( 'text' => TB_('Analytics'), 'url' => '?ctrl=stats&blog=$blog$' ) );
$AdminUI->breadcrumbpath_add( TB_('IPs'), $admin_url.'?ctrl=stats&blog=$blog$&tab='.$tab );
$AdminUI->breadcrumbpath_add( TB_('IP Ranges'), $admin_url.'?ctrl=stats&blog=$blog$&tab='.$tab.'&tab3='.$tab3 );
$AdminUI->set_path( 'stats', 'ips', 'ranges' );
}
else
{
$AdminUI->breadcrumbpath_init( false ); // fp> I'm playing with the idea of keeping the current blog in the path here...
$AdminUI->breadcrumbpath_add( TB_('System'), $admin_url.'?ctrl=system' );
$AdminUI->breadcrumbpath_add( TB_('Antispam'), $admin_url.'?ctrl=antispam' );
}
if( empty( $tab3 ) )
{
$tab3 = 'blacklist';
}
switch( $tab3 )
{
case 'settings':
$AdminUI->breadcrumbpath_add( TB_('Settings'), '?ctrl=antispam&tab3='.$tab3 );
init_hotkeys_js();
// Set an url for manual page:
$AdminUI->set_page_manual_link( 'antispam-settings' );
break;
case 'tools':
$AdminUI->breadcrumbpath_add( TB_('Tools'), '?ctrl=antispam&tab3='.$tab3 );
// Set an url for manual page:
$AdminUI->set_page_manual_link( 'antispam-tools' );
break;
case 'blacklist':
$AdminUI->breadcrumbpath_add( TB_('Blacklist'), '?ctrl=antispam' );
// Set an url for manual page:
$AdminUI->set_page_manual_link( 'antispam-tab' );
break;
case 'ipranges':
if( empty( $action ) )
{ // View a list of IP ranges
require_js_defer( 'customized:jquery/jeditable/jquery.jeditable.js', 'rsc_url' );
}
elseif( ! check_user_perm( 'spamblacklist', 'edit' ) )
{ // Check permission to create/edit IP range
$Messages->add( TB_('You have no permission to edit this IP range!'), 'error' );
$action = '';
}
$AdminUI->breadcrumbpath_add( TB_('IP Ranges'), '?ctrl=antispam&tab3='.$tab3 );
// Set an url for manual page:
if( $action == 'iprange_new' || $action == 'iprange_edit' )
{
init_tokeninput_js();
$AdminUI->set_page_manual_link( 'ip-range-editing' );
}
else
{
$AdminUI->set_page_manual_link( 'ip-ranges' );
}
break;
case 'countries':
if( check_user_perm( 'options', 'edit' ) )
{
require_js_defer( 'customized:jquery/jeditable/jquery.jeditable.js' );
}
// Set an url for manual page:
$AdminUI->set_page_manual_link( 'countries-list' );
$AdminUI->breadcrumbpath_add( TB_('Countries'), '?ctrl=antispam&tab3='.$tab3 );
break;
case 'domains':
load_funcs('sessions/model/_hitlog.funcs.php');
$AdminUI->breadcrumbpath_add( TB_('Referring domains'), '?ctrl=antispam&tab3='.$tab3 );
if( check_user_perm( 'stats', 'edit' ) )
{
require_js_defer( 'customized:jquery/jeditable/jquery.jeditable.js' );
}
// Load jquery UI to highlight cell on change domain type
require_js_defer( '#jqueryUI#' );
// Used for edit form
$tab_from = 'antispam';
$blog = 0; // Don't restrict domains by blog ID on this controller
// Set an url for manual page:
$AdminUI->set_page_manual_link( 'referring-domains-tab' );
break;
}
if( !empty( $tab3 ) )
{
$AdminUI->append_path_level( $tab3 );
}
}
if( in_array( $action, array( 'iprange_edit' ) ) )
{ // Initialize date picker
init_datepicker_js();
// Load jQuery QueryBuilder plugin files for user list filters:
init_querybuilder_js( 'rsc_url' );
// Initialize Hotkeys
init_hotkeys_js();
}
// Display <html><head>...</head> section! (Note: should be done early if actions do not redirect)
$AdminUI->disp_html_head();
// Display title, menu, messages, etc. (Note: messages MUST be displayed AFTER the actions)
$AdminUI->disp_body_top();
// Begin payload block:
$AdminUI->disp_payload_begin();
switch( $tab3 )
{
case 'settings':
$AdminUI->disp_view( 'antispam/views/_antispam_settings.form.php' );
break;
case 'tools':
// Check permission:
if( $tool != 'whois' )
{
check_user_perm( 'options', 'edit', true );
}
switch( $tool )
{
case 'bankruptcy':
$comment_status = param( 'comment_status', 'string', 'draft' );
$AdminUI->disp_view( 'antispam/views/_antispam_tools_bankruptcy.view.php' );
break;
case 'whois';
$AdminUI->disp_view( 'antispam/views/_antispam_whois.view.php' );
break;
default:
$AdminUI->disp_view( 'antispam/views/_antispam_tools.view.php' );
break;
}
break;
case 'ipranges':
switch( $action )
{
case 'iprange_new':
if( ! isset( $edited_IPRange ) )
{ // Define new IPRange object only when it was not defined before, e.g. in action 'iprange_create'
$edited_IPRange = new IPRange();
}
// Set IP Start and End from _GET request
$ip = param( 'ip', 'string', '' );
$ip_start = param( 'ip_start', 'string', '' );
$ip_end = param( 'ip_end', 'string', '' );
if( ! empty( $ip ) && is_valid_ip_format( $ip ) &&
( $ip = explode( '.', $ip ) ) && count( $ip ) == 4 )
{ // Prefill from single IP address:
$edited_IPRange->set( 'IPv4start', ip2int( implode( '.', array( $ip[0], $ip[1], $ip[2], 0 ) ) ) );
$edited_IPRange->set( 'IPv4end', ip2int( implode( '.', array( $ip[0], $ip[1], $ip[2], 255 ) ) ) );
}
else
{ // Prefill from start and end IP addresses:
if( ! empty( $ip_start ) && is_valid_ip_format( $ip_start ) )
{
$edited_IPRange->set( 'IPv4start', ip2int( $ip_start ) );
}
if( ! empty( $ip_end ) && is_valid_ip_format( $ip_end ) )
{
$edited_IPRange->set( 'IPv4end', ip2int( $ip_end ) );
}
}
$AdminUI->disp_view( 'antispam/views/_antispam_ipranges.form.php' );
break;
case 'iprange_edit':
$AdminUI->disp_view( 'antispam/views/_antispam_ipranges.form.php' );
break;
default: // View list of the IP Ranges
$AdminUI->disp_view( 'antispam/views/_antispam_ipranges.view.php' );
break;
}
break;
case 'countries':
$AdminUI->disp_view( 'regional/views/_country_list.view.php' );
break;
case 'domains':
$AdminUI->disp_view( 'sessions/views/_stats_refdomains.view.php' );
break;
case 'blacklist':
default:
if( ! empty( $javascript_messages ) )
{ // Don't display form and list when we should update a content from modal window by JS:
break;
}
if( $action == 'ban' && ( ! $Messages->has_errors() || ! empty( $confirm ) ) && !( $delhits || $delcomments ) )
{ // Nothing to do, ask user:
$AdminUI->disp_view( 'antispam/views/_antispam_ban.form.php' );
}
else
{ // Display blacklist:
$AdminUI->disp_view( 'antispam/views/_antispam_list.view.php' );
}
break;
}
// End payload block:
$AdminUI->disp_payload_end();
// Display body bottom, debug info and close </html>:
$AdminUI->disp_global_footer();
if( ! empty( $javascript_messages ) )
{ // Execute JS functions after action from modal window:
send_javascript_message( $javascript_messages, true, 'window.parent' );
}
?>