$_field_opts) { $default_options[$_field_name] = $_field_opts['default']; } } $default_options['version'] = self::$plugin_version; return $default_options; } /** * Return plugin option fields for various functions * * @return array */ private static function get_option_fields() { $checkbox_value = '1'; $option_fields = array( 'section_display' => array( 'title' => esc_html(__('Where To Display', '2bc-form-security')), 'callback' => 'options_display_cb', 'fields' => array( // display checkboxes 'display_register' => array( 'title' => esc_html(__('Registration Form', '2bc-form-security')), 'type' => 'checkbox', 'value' => $checkbox_value, 'description' => esc_html(__('Display form security tools in the WordPress registration form', '2bc-form-security')), 'default' => 0, ), 'display_login' => array( 'title' => esc_html(__('Login Form', '2bc-form-security')), 'type' => 'checkbox', 'value' => $checkbox_value, 'description' => esc_html(__('Display form security tools in the WordPress login form', '2bc-form-security')), 'default' => 0, ), 'display_comment' => array( 'title' => esc_html(__('Comment Form', '2bc-form-security')), 'type' => 'checkbox', 'value' => $checkbox_value, 'description' => esc_html(__('Display form security tools in the WordPress comment form', '2bc-form-security')), 'default' => 0, ), ), ), 'section_recaptcha' => array( 'title' => esc_html(__('Google reCAPTCHA Options', '2bc-form-security')), 'callback' => 'options_recaptcha_cb', 'fields' => array( 'enable_recaptcha' => array( 'title' => esc_html(__('Enable reCAPTCHA', '2bc-form-security')), 'type' => 'checkbox', 'description' => esc_html(__('Enable the Google reCAPTCHA tool for the checked forms', '2bc-form-security')), 'value' => $checkbox_value, 'default' => 0, ), 'site_key' => array( 'title' => esc_html(__('Site Key', '2bc-form-security')), 'type' => 'text', 'description' => esc_html(__('Enter your Google reCAPTCHA Site key for this site', '2bc-form-security')), 'default' => '', 'class' => array('regular-text', 'code'), ), 'secret_key' => array( 'title' => esc_html(__('Secret Key', '2bc-form-security')), 'type' => 'text', 'description' => esc_html(__('Enter your Google reCAPTCHA Secret key for this site', '2bc-form-security')), 'default' => '', 'class' => array('regular-text', 'code'), ), 'record_ips' => array( 'title' => esc_html(__('Record Users IP', '2bc-form-security')), 'type' => 'checkbox', 'description' => esc_html(__('Attempt to get the users IP. Will send to Google for extra security validation, and will be displayed in the Reports tab.', '2bc-form-security')), 'value' => $checkbox_value, 'default' => 0, ), 'recaptcha_theme' => array( 'title' => esc_html(__('reCAPTCHA Theme', '2bc-form-security')), 'type' => 'select', 'description' => esc_html(__('Select which theme to display the Google reCAPTCHA tool in', '2bc-form-security')), 'default' => 'light', 'options' => array( 'light' => esc_html(__('Light', '2bc-form-security')), 'dark' => esc_html(__('Dark', '2bc-form-security')), ), ), ), ), 'section_errors' => array( 'title' => esc_html(__('Error Handling', '2bc-form-security')), 'callback' => 'options_errors_cb', 'fields' => array( 'login_errors' => array( 'title' => esc_html(__('Login Errors', '2bc-form-security')), 'type' => 'radio', 'description' => esc_html(__('Select how to handle login errors', '2bc-form-security')), 'default' => 'formsec_error', 'options' => array( 'formsec_error' => wp_kses_post(__('2BC Form Security Error – Return a twobc_form_security error message that says Security checks failed', '2bc-form-security')), 'generic_errors' => wp_kses_post(__('Generic Errors – Return a generic login_failed error that simply says Login failed, to prevent hackers from learning valid user account names', '2bc-form-security')), ), ), 'comment_status' => array( 'title' => esc_html(__('Comment Status', '2bc-form-security')), 'type' => 'select', 'description' => wp_kses_post(__('Control how failed comments are handled. Options are to automatically mark as Spam (default), or put into the Moderation Queue.', '2bc-form-security')), 'default' => 'spam', 'options' => array( 'spam' => esc_html(__('Spam', '2bc-form-security')), 'zero' => esc_html(__('Moderation Queue', '2bc-form-security')), ), ), ), ), 'section_reports' => array( 'title' => esc_html(__('Reports', '2bc-form-security')), 'callback' => 'options_reports_cb', 'fields' => array( 'enable_reporting' => array( 'title' => esc_html(__('Enable Reporting', '2bc-form-security')), 'type' => 'checkbox', 'value' => $checkbox_value, 'description' => wp_kses_post(__('Record security failures in the Reports tab to help gauge the effectivness of the fields', '2bc-form-security')), 'default' => 0, ), ), ), ); return $option_fields; } public static function options_recaptcha_cb() { } public static function options_display_cb() { } public static function options_honeypot_cb() { } public static function options_errors_cb() { } public static function options_compatibility_cb() { } public static function options_reports_cb() { } /************************************************* * Plugin Init ************************************************/ /** * plugins_loaded hook * Generate Honeypot name * Set log table in wpdb * Add twobc_wpadmin_input_fields * Handle install and upgrade */ public static function hook_plugins_loaded() { // generate honeypot name self::$honeypot_name = self::sanitize_html_class(wp_create_nonce('twobc_formsec_hp')); // UPDATE - add the logs table to $wpdb global global $wpdb; $wpdb->twobc_formsecurity_log = $wpdb->prefix . 'twobc_formsecurity_log'; // add twobc_wpadmin_input_fields for option fields require_once(self::$plugin_path . 'includes/class_twobc_wpadmin_input_fields_1_0_1.php'); // handle install and upgrade $plugin_version = self::$plugin_version; $plugin_options_default = self::get_options_default(); // UPDATE - add activated option to default options $plugin_options_default['activated'] = true; $plugin_options = self::$plugin_options; $update_options = false; // install check if ( empty($plugin_options) ) { // init with default values $update_options = true; $plugin_options = $plugin_options_default; } // handle upgrade check if ( $plugin_version != $plugin_options['version'] ) { // init any empty db fields to catch any new additions foreach ( $plugin_options_default as $_name => $_value ) { if ( !isset($plugin_options[$_name]) ) { $plugin_options[$_name] = $_value; } // UPDATE - rewrite deprecated arguments if ( 'login_errors' == $_name && 'incorrect_password' == $plugin_options['login_errors'] ) { $plugin_options['login_errors'] = $plugin_options_default['login_errors']; } } // set the updated settings $update_options = true; } if ( $update_options ) { update_option('twobc_formsecurity_options', $plugin_options); // update log structure self::init_logs(); } } /** * init hook - load plugin textdomain */ public static function hook_init() { load_plugin_textdomain('2bc-form-security', false, self::$plugin_path . 'lang'); } /************************************************* * Admin - Options ************************************************/ /** * admin_menu hook - add the 2BC Form Security menu option */ public static function hook_admin_menu() { add_options_page( esc_html(__('2BC Form Security', '2bc-form-security')), // page title esc_html(__('2BC Form Security', '2bc-form-security')), // menu title 'manage_options', // capability required 'twobc_formsecurity', // page slug array(self::get_instance(), 'settings_page_cb') // display callback ); } /** * admin_enqueue_scripts hook * Selectively load the recaptcha api, admin script, and admin style */ public static function hook_admin_enqueue_scripts() { $current_screen = get_current_screen(); switch ($current_screen->base) { case 'settings_page_twobc_formsecurity' : // load recaptcha API, for validation of API keys wp_enqueue_script( 'twobc_formsecurity_captcha', // handle 'https://www.google.com/recaptcha/api.js', //src array(), // dependencies self::$plugin_version, // version false // in footer ); // no break, continue processing case 'dashboard' : // load plugin JS wp_enqueue_script( 'twobc_formsecurity_admin_js', // string self::$plugin_url . 'includes/js/2bc-form-security-admin.js', // src array('jquery'), // dependencies self::$plugin_version, // script version true // in footer ); wp_localize_script( 'twobc_formsecurity_admin_js', // script handle 'twoBCFormSecurity', // object name array( // data to pass '_ajax_nonce' => wp_create_nonce('twobc-formsecurity-ajaxnonce'), 'errorSecretKey' => esc_html(__('ERROR: Invalid Secret Key', 'Error', '2bc-form-security')), 'errorResponse' => esc_html(__('ERROR: The API keys are good, however Google says you are a robot… maybe you should try again?', 'Error', '2bc-form-security')), 'instructMessage1' => esc_html(__('Complete the reCAPTCHA widget below to confirm the API keys.', '2bc-form-security')), 'instructMessage2' => wp_kses_post(__('API Keys Confirmed! Click the Save all settings button to activate the reCAPTCHA widget.', '2bc-form-security')), 'clearLogsButton' => esc_html(__('Are you sure you want to clear the tracking log?', '2bc-form-security')), 'recaptchaValidNonce' => wp_create_nonce('twobc_formsecurity_nonce_recaptcha_valid'), ) ); // load plugin styles wp_enqueue_style( 'twobc_formsecurity_admin_css', // handle self::$plugin_url . 'includes/css/2bc-form-security-admin.css', // src array(), // dependencies self::$plugin_version // style version ); break; default : } } /** * admin_init hook - register settings for options screen * * @uses get_option_fields() */ public static function hook_admin_init() { $core = self::get_instance(); // admin menu and pages register_setting( 'twobc_formsecurity_options', // option group name, declared with settings_fields() 'twobc_formsecurity_options', // option name, best to set same as group name array($core, 'options_sanitize_cb') // sanitization callback ); $options_fields = self::get_option_fields(); foreach ( $options_fields as $_section_name => $_section_opts ) { // add sections add_settings_section( 'twobc_formsecurity_options_' . $_section_name, // section id $_section_opts['title'], // section title array($core, $_section_opts['callback']), // display callback 'twobc_formsecurity' // page to display on ); // add fields foreach ( $_section_opts['fields'] as $_field_name => $_field_opts ) { $additional_args = array( 'type' => $_field_opts['type'], 'name' => $_field_name, 'description' => $_field_opts['description'], 'default' => $_field_opts['default'], ); $additional_args = array_merge($additional_args, $_field_opts); add_settings_field( $_field_name, //field id $_field_opts['title'], // field title array($core, 'wpaf_option_field'), // callback 'twobc_formsecurity', // page to display on 'twobc_formsecurity_options_' . $_section_name, // section to display in $additional_args ); } } // UPDATE - save tab selection as cookie if ( !empty($_GET['tab']) ) { if ( 'settings' == $_GET['tab'] || 'reports' == $_GET['tab'] ) setcookie('twobc_formsec_opt_tab', esc_attr($_GET['tab']), time()*60*60*24*14, '/'); } } /** * Prepare form field for the options screen * * @param $field_args * * @uses twobc_wpadmin_input_fields_1_0_1 */ public static function wpaf_option_field($field_args) { $wpaf = new twobc_wpadmin_input_fields_1_0_1( array( 'nonce' => false, ) ); $field_args = array_merge($wpaf->field_default_args(), $field_args); $current_value = self::$plugin_options; $field_args['current_value'] = ( isset($current_value[$field_args['name']]) ? $current_value[$field_args['name']] : '' ); // field nonce echo ' '; // handle the disabling of the recaptcha api fields switch ( $field_args['name']) { case 'site_key' : if ( empty(self::$plugin_options['recaptcha_valid']) && !self::recaptcha_has_args() ) { echo '

'; printf(wp_kses_post(__('Enter your Google reCAPTCHA V2 API keys below. If you do not have API keys, see the 2BC blog post on
%1$sHow To Get Google reCAPTCHA V2 API Keys%2$s', '2bc-form-security')), '', ''); echo '

'; } // no break, continue processing case 'secret_key' : if ( !empty(self::$plugin_options['recaptcha_valid']) ) { // output hidden fields so that the API keys are stored in the plugin options echo ' '; $field_args['disabled'] = true; } break; case 'enable_recaptcha' : if ( empty(self::$plugin_options['recaptcha_valid']) ) { $field_args['disabled'] = true; } else { $field_args['class'] = array('recaptcha_valid'); } break; default : } // fix name $field_args['name'] = 'twobc_formsecurity_options[' . $field_args['name'] . ']'; echo '
'; $wpaf->field($field_args); // UPDATE - add button to change current API keys if ( 'twobc_formsecurity_options[secret_key]' == $field_args['name'] ) { echo ' '; } echo '
'; } /** * Options page callback */ public static function settings_page_cb() { //must check that the user has the required capability if ( !current_user_can('manage_options') ) { wp_die(esc_html(__('You do not have sufficient permissions to access this page.', '2bc-form-security'))); } echo '
'; settings_errors( 'twobc_formsecurity_options', // settings group name false, // re-sanitize values on errors true // hide on update - set to true to get rid of duplicate Updated messages ); // UPDATE - trying to get tabs to work - settings, and reports $tab_class_settings = $tab_class_reports = 'nav-tab'; if ( empty($_GET['tab']) ) { // try to get value from cookie if ( !empty($_COOKIE['twobc_formsec_opt_tab']) ) { $active_tab = esc_attr($_COOKIE['twobc_formsec_opt_tab']); } else { // default to settings $active_tab = 'settings'; } } else { $active_tab = esc_attr($_GET['tab']); } switch ( $active_tab ) { case 'reports' : $tab_class_reports .= ' nav-tab-active'; break; case 'settings' : default : $tab_class_settings .= ' nav-tab-active'; } echo ' '; echo '

' . esc_html(__('2BC Form Security Options', '2bc-form-security')) . '

'; echo '

'; printf(wp_kses_post(__('More help available at the %1$s2BC Form Security documentation page%2$s.', '2bc-form-security')), '', ''); echo '

'; // tab logic switch ($active_tab) { case 'settings' : echo '
'; echo '
'; // setup form nonces settings_fields('twobc_formsecurity_options'); do_settings_sections('twobc_formsecurity'); submit_button(esc_html(__('Save all settings', '2bc-form-security'))); echo '
'; echo '
'; break; case 'reports' : echo '
'; echo self::get_reports_screen(); echo '
'; break; default : echo '

' . wp_kses_post(__('ERROR: Invalid tab!', '2bc-form-security')) . '

'; } echo '
'; } /** * Sanitize settings from options screen * * @param $saved_settings * * @uses get_option_fields * * @return mixed */ public static function options_sanitize_cb($saved_settings) { $settings_errors = array( 'updated' => false, 'error' => array(), ); $option_fields = self::get_option_fields(); // get field names and types $known_fields = array(); foreach ($option_fields as $_section) { foreach ($_section['fields'] as $_field_name => $_field_opts) { $known_fields[$_field_name] = $_field_opts['type']; } } foreach ( $saved_settings as $setting_key => $setting_val ) { // security checks - nonce, capability if ( // check that field is known isset($known_fields[$setting_key]) // check user capabilities && current_user_can('manage_options') // check form nonce && check_admin_referer( 'twobc_formsecurity_options-options' // action ) // check field nonce && ( !empty($_REQUEST['twobc_formsecurity_nonce_' . $setting_key]) && wp_verify_nonce( $_REQUEST['twobc_formsecurity_nonce_' . $setting_key], // nonce to verify 'twobc_formsecurity_nonce_' . $setting_key // custom nonce action ) ) ) { $settings_errors['updated'] = true; switch ( $known_fields[$setting_key] ) { case 'text' : $saved_settings[$setting_key] = sanitize_text_field($setting_val); if ( 'honeypot_name' == $setting_key ) $saved_settings[$setting_key] = sanitize_title($saved_settings[$setting_key]); break; case 'checkbox' : $saved_settings[$setting_key] = '1'; break; case 'number' : if ( is_numeric($setting_val) ) { $saved_settings[$setting_key] = intval($setting_val); } else { unset($saved_settings[$setting_key]); } break; case 'select' : case 'radio' : $saved_settings[$setting_key] = sanitize_text_field($setting_val); break; case 'colorpicker' : if ( preg_match('|^#([A-Fa-f0-9]{3}){1,2}$|', $setting_val) ) $saved_settings[$setting_key] = ($setting_val); break; default : // unknown field type? Shouldn't happen, but unset to be safe unset($saved_settings[$setting_key]); } } elseif ( // UPDATE - check for recaptcha_valid field // check that field is known 'recaptcha_valid' == $setting_key // check user capabilities && current_user_can('manage_options') // check form nonce && check_admin_referer( 'twobc_formsecurity_options-options' // action ) // check field nonce && ( !empty($_REQUEST['twobc_formsecurity_nonce_recaptcha_valid']) && wp_verify_nonce( $_REQUEST['twobc_formsecurity_nonce_recaptcha_valid'], // nonce to verify 'twobc_formsecurity_nonce_recaptcha_valid' // custom nonce action ) ) ) { self::$plugin_options['recaptcha_valid'] = ( !empty($setting_val) ? '1' : '0' ); } else { // unknown field or security fail, unset to be safe unset($saved_settings[$setting_key]); } } // separate validation for un-checked checkboxes foreach ( $known_fields as $field_name => $field_type ) { if ( 'checkbox' == $field_type && !isset($saved_settings[$field_name]) ) { $saved_settings[$field_name] = '0'; $settings_errors['updated'] = true; } } // register errors if ( !empty($settings_errors['errors']) && is_array($settings_errors['errors']) ) { foreach ( $settings_errors['errors'] as $error ) { add_settings_error( 'twobc_formsecurity_options', // Slug title of the setting 'twobc_formsecurity_options_error', // Slug of error $error, // Error message 'error' // Type of error (**error** or **updated**) ); } } if ( true === $settings_errors['updated'] ) { add_settings_error( 'twobc_formsecurity_options', // Slug title of the setting 'twobc_formsecurity_options_error', // Slug of error esc_html(__('Settings saved.', '2bc-form-security')), // Error message 'updated' // Type of error (**error** or **updated**) ); } // SYSTEM FIELDS // recaptcha_valid if ( !isset($saved_settings['recaptcha_valid']) ) $saved_settings['recaptcha_valid'] = self::$plugin_options['recaptcha_valid']; // UPDATE - mark activated as false $saved_settings['activated'] = false; // set plugin version number $saved_settings['version'] = self::$plugin_version; // update the static class property self::$plugin_options = $saved_settings; // return the database options to be saved return $saved_settings; } /************************************************* * Admin - Reports ************************************************/ /** * Initialize or update the report log table */ public static function init_logs() { // add upgrade.php for dbDelta() function require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); global $wpdb; $logs_table = "CREATE TABLE {$wpdb->twobc_formsecurity_log} ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_ip varchar(40) NOT NULL, register_hp bigint(20) unsigned NOT NULL default 0, register_rcp_null bigint(20) unsigned NOT NULL default 0, register_rcp_bad bigint(20) unsigned NOT NULL default 0, login_hp bigint(20) unsigned NOT NULL default 0, login_rcp_null bigint(20) unsigned NOT NULL default 0, login_rcp_bad bigint(20) unsigned NOT NULL default 0, comment_hp bigint(20) unsigned NOT NULL default 0, comment_rcp_null bigint(20) unsigned NOT NULL default 0, comment_rcp_bad bigint(20) unsigned NOT NULL default 0, total bigint(20) unsigned NOT NULL default 0, PRIMARY KEY (id), KEY user_ip (user_ip) ) {$wpdb->get_charset_collate()};"; dbDelta($logs_table); $undefined_check = $wpdb->get_results( "SELECT ID FROM $wpdb->twobc_formsecurity_log WHERE user_ip = 'undefined' " ); if ( empty($undefined_check) ) { $db_row = array( 'user_ip' => 'undefined', 'register_hp' => 0, 'register_rcp_null' => 0, 'register_rcp_bad' => 0, 'login_hp' => 0, 'login_rcp_null' => 0, 'login_rcp_bad' => 0, 'comment_hp' => 0, 'comment_rcp_null' => 0, 'comment_rcp_bad' => 0, 'total' => 0, ); $db_formats = array( '%s', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', ); $wpdb->insert($wpdb->twobc_formsecurity_log, $db_row, $db_formats); } } /** * Update a row in the log table for reports screen * * @param $args * * @return bool|false|int */ private static function update_report_log($args) { $default_args = array( 'ip' => 'undefined', 'location' => '', 'honeypot' => false, 'recap_null' => false, 'recap_bad' => false, ); $args = wp_parse_args($args, $default_args); // exit conditions - empty location, or if location isn't valid $locations_valid = array( 'register' => 'register', 'login' => 'login', 'comment' => 'comment', ); if ( empty($args['location']) || !isset($locations_valid[$args['location']]) ) { // TODO: add error for debug mode return false; } // first, check for existing row global $wpdb; $row_test = $wpdb->get_results( "SELECT * FROM $wpdb->twobc_formsecurity_log WHERE user_ip = '{$args['ip']}' LIMIT 1 ", ARRAY_A ); // prepare new db row switch ( true ) { case (is_array($row_test) && empty($row_test)) : // no results - insert new row $db_row = array( 'id' => '', 'user_ip' => $args['ip'], 'register_hp' => 0, 'register_rcp_null' => 0, 'register_rcp_bad' => 0, 'login_hp' => 0, 'login_rcp_null' => 0, 'login_rcp_bad' => 0, 'comment_hp' => 0, 'comment_rcp_null' => 0, 'comment_rcp_bad' => 0, 'total' => 0, ); break; case (is_array($row_test) && !empty($row_test)) : // success, we got something - update $db_row = reset($row_test); break; case (is_null($row_test)) : default : // TODO: add error for debugging messages return false; } // compute which fields to update if ( $args['honeypot'] ) { $db_row[$args['location'] . '_hp']++; } if ( $args['recap_null'] ) { $db_row[$args['location'] . '_rcp_null']++; } if ( $args['recap_bad'] ) { $db_row[$args['location'] . '_rcp_bad']++; } // finally, update or insert the new row $db_formats = array( '%d', '%s', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', '%d', ); // calculate total $array_sum = $db_row; unset($array_sum['id']); unset($array_sum['user_ip']); unset($array_sum['total']); $db_row['total'] = array_sum($array_sum); // use *replace* to update or insert as needed $return = $wpdb->replace($wpdb->twobc_formsecurity_log, $db_row, $db_formats); return $return; } /** * Get HTML display for the reports tab * * @uses get_pagination_buttons * * @return string */ private static function get_reports_screen() { $return = ''; global $wpdb; $return .= '

' . esc_html(__('Total', '2bc-form-security')) . '

'; $total = $wpdb->get_var( "SELECT sum(total) FROM $wpdb->twobc_formsecurity_log" ); $return .= ' '; $return .= '

' . esc_html(__('Security Methods', '2bc-form-security')) . '

'; $security_methods = array( 'honeypot' => $wpdb->get_var( "SELECT sum(register_hp+login_hp+comment_hp) FROM $wpdb->twobc_formsecurity_log" ), 'recap_null' => $wpdb->get_var( "SELECT sum(register_rcp_null+login_rcp_null+comment_rcp_null) FROM $wpdb->twobc_formsecurity_log" ), 'recap_bad' => $wpdb->get_var( "SELECT sum(register_rcp_bad+login_rcp_bad+comment_rcp_bad) FROM $wpdb->twobc_formsecurity_log" ), ); $return .= ' '; $return .= '

' . esc_html(__('Locations', '2bc-form-security')) . '

'; $locations = array( 'register' => $wpdb->get_var( "SELECT sum(register_hp+register_rcp_null+register_rcp_bad) FROM $wpdb->twobc_formsecurity_log" ), 'login' => $wpdb->get_var( "SELECT sum(login_hp+login_rcp_null+login_rcp_bad) FROM $wpdb->twobc_formsecurity_log" ), 'comment' => $wpdb->get_var( "SELECT sum(comment_hp+comment_rcp_null+comment_rcp_bad) FROM $wpdb->twobc_formsecurity_log" ), ); $return .= ' '; $return .= '

' . esc_html(__('Log', '2bc-form-security')) . self::get_clear_logs_button() . '

'; $page_num = (!empty($_REQUEST['page_num']) ? intval($_REQUEST['page_num']) : 1); $page_limit = 500; $id_start = ($page_limit * $page_num) - $page_limit; $table_rows = $wpdb->get_results( "SELECT * FROM $wpdb->twobc_formsecurity_log WHERE id > $id_start ORDER BY id ASC LIMIT $page_limit", ARRAY_A ); $return .= self::get_pagination_buttons($page_num, count($table_rows)); $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; if ( !empty($table_rows) ) { $last_key = count($table_rows) - 1; foreach ($table_rows as $_key => $_row_vars) { $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; $return .= ' '; } } $return .= '
' . esc_html(__('ID', '2bc-form-security')) . '' . esc_html(__('IP Address', '2bc-form-security')) . '' . esc_html(__('Registration', '2bc-form-security')) . '' . esc_html(__('Comment', '2bc-form-security')) . '' . esc_html(__('Honeypots', '2bc-form-security')) . '' . esc_html(__('reCAPTCHA | null', '2bc-form-security')) . '' . esc_html(__('reCAPTCHA | bad', '2bc-form-security')) . '' . esc_html(__('Total', '2bc-form-security')) . '
' . $_row_vars['id'] . '' . $_row_vars['user_ip'] . '' . number_format($_row_vars['register_hp'] + $_row_vars['register_rcp_null'] + $_row_vars['register_rcp_bad']) . '' . number_format($_row_vars['comment_hp'] + $_row_vars['comment_rcp_null'] + $_row_vars['comment_rcp_bad']) . '' . number_format($_row_vars['register_hp'] + $_row_vars['login_hp'] + $_row_vars['comment_hp']) . '' . number_format($_row_vars['register_rcp_null'] + $_row_vars['login_rcp_null'] + $_row_vars['comment_rcp_null']) . '' . number_format($_row_vars['register_rcp_bad'] + $_row_vars['login_rcp_bad'] + $_row_vars['comment_rcp_bad']) . '' . number_format($_row_vars['total']) . '
'; $return .= self::get_pagination_buttons($page_num, count($table_rows)); return $return; } /** * Get the HTML markup for the Clear Logs button * * @return string */ private static function get_clear_logs_button() { $return = ' '; return $return; } /** * Get the pagination buttons for the reports screen based on arguments * * @param $page_num * @param $count * @param int $per_page * * @return string */ private static function get_pagination_buttons($page_num, $count, $per_page = 500) { $admin_url = self::get_admin_url( '/options-general.php', // path array ( // query args 'page' => 'twobc_formsecurity', 'tab' => 'reports', ) ); $prev = '' . wp_kses_post(__('« Previous Page ', '2bc-form-security')) . ' '; $next = '' . wp_kses_post(__('Next Page »', '2bc-form-security')) . ' '; switch (true) { // no pages to display case ( 1 == $page_num && $count < $per_page ) : $return = ''; break; // first page case ( 1 == $page_num && $count == $per_page ) : $return = $next; break; // last page case ( $count < $per_page ) : $return = $prev; break; // all other pages default : $return = $prev . $next; } return $return; } function hook_admin_notices() { global $current_screen; if ( !empty($current_screen) && 'settings_page_twobc_formsecurity' == $current_screen->base && !empty($_GET['tab']) && 'reports' == $_GET['tab'] && empty(self::$plugin_options['enable_reporting']) ) { echo '

'; echo wp_kses_post(__('ERROR: Reporting is not currently enabled! Click the Settings tab to turn reporting on.', '2bc-form-security')); echo '

'; } // UPDATE - check for plugin activation if ( self::$plugin_options['activated'] && ( !empty($current_screen) && 'settings_page_twobc_formsecurity' != $current_screen->base ) ) { echo '

'; $admin_url = self::get_admin_url( '/options-general.php', // path array( // query args 'page' => 'twobc_formsecurity', ) ); printf(wp_kses_post(__('2BC Form Security has been activated! Visit the %1$sSettings Page%2$s to choose which forms to protect. Save the settings to dismiss this message.', '2bc-form-security')), '', ''); echo '

'; } } /************************************************* * Form Utilities ************************************************/ /** * Get HTML markup for the recaptcha div * * @param bool $noscript - return the additional noscript tag for non-javascript cases * * @return string */ private static function get_recaptcha_div($noscript = true) { $return = ''; if ( self::recaptcha_has_args() && '1' == self::$plugin_options['recaptcha_valid'] ) { $return = '
'; if ( !empty($noscript) ) { $return .= ''; } } return $return; } /** * Get the HTML markup for the honeypot * * @return string */ private static function get_honeypot() { $return = ' '; return $return; } /** * Get the CSS necessary to hide the honeypot * * @return string */ private static function get_honeypot_css() { $return = ' '; return apply_filters('twobc_form_security_hp_css', $return); } /** * Validate the security fields according to which location is being called * * @param $location * * @return bool|int */ private static function validate_security_fields($location) { // exit conditions - location must be present switch ( true ) { case ( empty($location) ) : return -1; default : } $return = true; // init reporting array $reporting = array( 'ip' => (!empty(self::$plugin_options['record_ips']) && self::validate_ip($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'undefined'), 'location' => $location, 'honeypot' => false, 'recap_null' => false, 'recap_bad' => false, ); // check the honeypots $hp_name = (empty(self::$plugin_options['honeypot_name']) ? self::$honeypot_name : self::$plugin_options['honeypot_name']); if ( !empty($_REQUEST[$hp_name]) && is_array($_REQUEST[$hp_name]) ) { // remove empty values $honeypots = array_filter($_REQUEST[$hp_name], 'strlen'); if ( !empty($honeypots) ) { $return = false; $reporting['honeypot'] = true; } } else { // fields changed or missing, throw an error $return = false; $reporting['honeypot'] = true; } // check for valid reCAPTCHA response if ( !empty(self::$plugin_options['enable_recaptcha']) && self::recaptcha_has_args() && self::$plugin_options['recaptcha_valid'] ) { if ( empty($_REQUEST['g-recaptcha-response']) ) { $return = false; $reporting['recap_null'] = true; } else { // make request to Google API to check response $http_args = array( 'method' => 'POST', 'body' => array( 'secret' => self::$plugin_options['secret_key'], 'response' => $_REQUEST['g-recaptcha-response'], ), ); if ( !empty(self::$plugin_options['record_ips']) && self::validate_ip($_SERVER['REMOTE_ADDR']) ) $http_args['body']['remoteip'] = $_SERVER['REMOTE_ADDR']; $google_response = wp_remote_retrieve_body(wp_remote_post(self::$google_verify_url, $http_args)); if ( !is_wp_error($google_response) ) { $google_response = json_decode($google_response, true); } // TODO: Add debugging messages if ( empty($google_response['success']) ) { // detect bad sitekey error if ( !empty($google_response['error-codes']) ) { if ( false === strpos($google_response['error-codes'], 'missing-input-secret') && false === strpos($google_response['error-codes'], 'invalid-input-secret') ) { // must be bad response, return false $return = false; $reporting['recap_bad'] = true; } else { // site key is invalid despite api check // make sure to set to false self::$plugin_options['recaptcha_valid'] = '0'; self::$plugin_options['enable_recaptcha'] = '0'; update_option('twobc_formsecurity_options', self::$plugin_options); } } } } } // UPDATE - trigger log update if ( !empty(self::$plugin_options['enable_reporting']) ) { self::update_report_log($reporting); } return $return; } /************************************************* * Login Form ************************************************/ /** * login_enqueue_scripts hook - load captcha api and plugin style */ public static function hook_login_enqueue_scripts() { if ( !empty(self::$plugin_options['display_login']) ) { wp_enqueue_script( 'twobc_formsecurity_captcha', // handle 'https://www.google.com/recaptcha/api.js', //src array(), // dependencies self::$plugin_version, // version false // in footer ); wp_enqueue_style( 'twobc_formsecurity_captcha_style', // handle self::$plugin_url . 'includes/css/2bc-form-security.css', // URL src array(), // dependencies self::$plugin_version // version ); } } /** * login_head hook - display honeypot CSS */ public static function hook_login_head() { if ( !empty(self::$plugin_options['display_login']) ) { echo self::get_honeypot_css(); } } /** * login_form hook - get the reCAPTCHA and Honeypot markup for the login form */ public static function hook_login_form() { if ( !empty(self::$plugin_options['display_login']) ) { // recaptcha if ( !empty(self::$plugin_options['enable_recaptcha']) ) echo self::get_recaptcha_div(); // honeypot echo self::get_honeypot(); } } /** * wp_authenticate_user hook - validate security fields and return appropriate error * * @param $user_maybe * @param $password * * @return WP_Error|WP_User */ public static function hook_wp_authenticate_user($user_maybe, $password) { $validated = false; // only deal with users who have entered all details if ( $user_maybe instanceof WP_User ) { $validated = self::validate_security_fields('login'); if ( !$validated ) $user_maybe = self::get_plugin_error('twobc_form_security'); } return $user_maybe; } /************************************************* * Registration Form ************************************************/ /** * register_form hook - get reCAPTCHA and Honeypot markup for the registration form */ public static function hook_register_form() { if ( !empty(self::$plugin_options['display_register']) ) { // recaptcha if ( !empty(self::$plugin_options['enable_recaptcha']) ) echo self::get_recaptcha_div(); // honeypot echo self::get_honeypot(); } } /** * registration_errors hook - validate security fields on registration form * * @param $errors * @param $sanitized_user_login * @param $user_email * * @return mixed */ public static function hook_registration_errors($errors, $sanitized_user_login, $user_email) { $validated = self::validate_security_fields('register'); if ( !$validated && !isset($errors->errors['twobc_form_security']) ) { $errors->add('twobc_form_security', wp_kses_post(__('ERROR: Security checks failed', '2bc-form-security'))); } return $errors; } /************************************************* * Comment Form ************************************************/ /** * wp_enqueue_scripts hook - add reCAPTCHA api and plugin styling */ public static function hook_wp_enqueue_scripts() { // add recaptcha api settings are appropriate if ( !empty(self::$plugin_options['display_comment']) && comments_open() ) { wp_enqueue_script( 'twobc_formsecurity_captcha', // handle 'https://www.google.com/recaptcha/api.js', //src array(), // dependencies self::$plugin_version, // version true // in footer ); wp_enqueue_style( 'twobc_formsecurity_style', // handle self::$plugin_url . 'includes/css/2bc-form-security.css', // URL src array(), // dependencies self::$plugin_version, // version false // in footer ); } } /** * wp_head hook - display honeypot CSS when comment form is going to be present */ public static function hook_wp_head() { if ( !empty(self::$plugin_options['display_comment']) && comments_open() ) { echo self::get_honeypot_css(); } } /** * comment_form hook - display reCAPTCHA and Honeypot markup on the comment form */ public static function hook_comment_form() { if ( !empty(self::$plugin_options['display_comment']) ) { // recaptcha if ( !empty(self::$plugin_options['enable_recaptcha']) ) echo self::get_recaptcha_div(); // honeypot echo self::get_honeypot(); } } /** * Validate security fields on comment form and put comment into appropriate queue if it failed * * @param $approved * @param $commentdata * * @return int|string */ public static function hook_pre_comment_approved($approved, $commentdata) { $failed_comment_status = ( 'zero' == self::$plugin_options['comment_status'] ? 0 : 'spam' ); $approved = ( self::validate_security_fields('comment') ? 1 : $failed_comment_status ); return $approved; } /************************************************* * Error Handling ************************************************/ /** * Filter login errors and rewrite any specific errors to a generic error * * Used to try to prevent hackers from learning valid usernames * * @param $errors * @param $redirect_to * * @return WP_Error */ public static function hook_wp_login_errors($errors, $redirect_to) { if ( !empty($errors->errors) ) { switch ( self::$plugin_options['login_errors'] ) { case 'generic_errors' : // selectively replace errors $break = false; foreach ( $errors->errors as $_error_code => $_error_vals ) { switch ( $_error_code ) { case 'empty_password' : case 'empty_username' : case 'invalid_username' : case 'incorrect_password' : case 'twobc_form_security' : if ( !isset($errors->errors['generic_login_error']) ) { $errors = self::get_plugin_error('generic_login_error'); $break = true; } break; default : } // break out of loop if we've replaced with generic error if ( $break ) break; } break; case 'formsec_error' : default : } } return $errors; } /************************************************* * Dashboard Widget ************************************************/ /** * wp_dashboard_setup hook - setup plugin widget on dashboard */ public static function hook_wp_dashboard_setup() { if ( current_user_can('manage_options') ) { wp_add_dashboard_widget( 'twobc_formsecurity_admin_widget', // widget slug esc_html(__('2BC Form Security Summary', '2bc-form-security')), // widget title array(self::get_instance(), 'admin_widget_display_cb') // display callback ); } } /** * Dashboard widget display callback */ public static function admin_widget_display_cb() { $output = ''; global $wpdb; $admin_url = self::get_admin_url( '/options-general.php', // path array( // query args 'page' => 'twobc_formsecurity', ) ); $output .= '
'; $output .= '

' . esc_html(__('Report Summary', '2bc-form-security')) . '

'; $total = $wpdb->get_var( "SELECT sum(total) FROM $wpdb->twobc_formsecurity_log" ); $output .= ' '; $output .= ' '; $output .= ' '; $output .= '
'; $output .= '
'; $output .= '

' . esc_html(__('Current Settings', '2bc-form-security')) . '

'; $output .= ' '; $output .= '
'; echo $output; } /************************************************* * Plugin Utilities ************************************************/ /** * Verify reCAPTCHA has the necessary options set * * @return bool */ private static function recaptcha_has_args() { if ( !empty(self::$plugin_options['site_key']) && !empty(self::$plugin_options['secret_key']) ) { $return = true; } else { $return = false; } return $return; } /** * Ensures an IP address is both valid, and does not fall within a private network range. * * @url http://stackoverflow.com/questions/1634782/what-is-the-most-accurate-way-to-retrieve-a-users-correct-ip-address-in-php?rq=1 * * @param $ip string | IPv4 or IPv6 address * * @return bool */ private static function validate_ip($ip) { if ( false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) ) { return false; } else { return true; } } /** * Get a WordPress error that contains custom error messages from 2BC Form Security * * Building in the ability to return the incorrect_password error, so that other security plugins that check * for this error will continue to function * * @param $type string | twobc_form_security, incorrect_password, generic_login_error * * @return WP_Error */ private static function get_plugin_error($type) { $translate_security = wp_kses_post(__('ERROR: Security checks failed.', '2bc-form-security')); $translate_generic = wp_kses_post(__('ERROR: Login failed.')); switch ( $type ) { case 'twobc_form_security' : $returned_error = new WP_Error('twobc_form_security', $translate_security); break; case 'incorrect_password' : $returned_error = new WP_Error('incorrect_password', $translate_security); break; case 'generic_login_error' : $returned_error = new WP_Error('generic_login_error', $translate_generic); break; default : $returned_error = new WP_Error(); } return $returned_error; } /** * Leverages WordPress's sanitize_html_class, while also ensuring that the class name does not * begin with a number. This is accomplished by spelling out the number, if appropriate * * @param $class * * @return mixed|string */ private static function sanitize_html_class($class) { $class = sanitize_html_class($class); $first_char = substr($class, 0, 1); if ( is_numeric($first_char) ) { // replace first digit with phonetic spelling of number switch ( $first_char ) { case '0' : $replacement = 'zero'; break; case '1' : $replacement = 'one'; break; case '2' : $replacement = 'two'; break; case '3' : $replacement = 'three'; break; case '4' : $replacement = 'four'; break; case '5' : $replacement = 'five'; break; case '6' : $replacement = 'six'; break; case '7' : $replacement = 'seven'; break; case '8' : $replacement = 'eight'; break; case '9' : $replacement = 'nine'; break; default : // we can't handle numbers that don't exist... what, like imaginary numbers? not sure I need this return $class; } $class = substr_replace($class, $replacement, 0, 1); } return $class; } private static function get_admin_url($path = '', $query_args = array()) { $admin_url = ( !is_multisite() ? admin_url($path) : network_admin_url($path) ); if ( !empty($query_args) && is_array($query_args) ) $admin_url = add_query_arg($query_args, $admin_url); return $admin_url; } /** * Reset the report log via an AJAX request */ public static function ajax_reset_report_log() { // verify ajax nonce check_ajax_referer('twobc-formsecurity-ajaxnonce'); // verify user permissions if ( !current_user_can('manage_options') ) die(__('You do not have sufficient permissions to access this page.', '2bc-form-security')); // proceed with clearing logs global $wpdb; $wpdb->query( "DROP TABLE IF EXISTS $wpdb->twobc_formsecurity_log" ); self::init_logs(); die(); } /** * Verify the API keys by calling Google API * Called from the server through an AJAX request so that Google will respond correctly */ public static function ajax_recaptcha_verify_api() { // verify ajax nonce check_ajax_referer('twobc-formsecurity-ajaxnonce'); // verify user permissions if ( !current_user_can('manage_options') ) die(esc_html(__('You do not have sufficient permissions to access this page.', '2bc-form-security'))); $site_key = ( !empty($_REQUEST['twobcfs_site_key']) ? esc_attr($_REQUEST['twobcfs_site_key']) : false ); $secret_key = ( !empty($_REQUEST['twobcfs_secret_key']) ? esc_attr($_REQUEST['twobcfs_secret_key']) : false ); if ( empty($site_key) || empty($secret_key) ) die(esc_html(__('Google reCAPTCHA API keys missing or invalid.', '2bc-form-security'))); $response = ( !empty($_REQUEST['twobcfsRecaptchaResponse']) ? esc_attr($_REQUEST['twobcfsRecaptchaResponse']) : false ); if ( empty($response) ) die(esc_html(__('Google reCAPTCHA not clicked.', '2bc-form-security'))); if ( empty($secret_key) ) die(esc_html(__('Could not get secret key', '2bc-form-security'))); $http_args = array( 'method' => 'POST', 'body' => array( 'secret' => $secret_key, 'response' => $response, ), ); $return = wp_remote_retrieve_body(wp_remote_post(self::$google_verify_url, $http_args)); if ( is_wp_error($return) ) { $return = $return->get_error_message(); } die($return); } } // end of class 2bc_form_security