= time() - 604800) $data = json_decode(file_get_contents($cacheFile)); else { // Otherwise, use the QWeb API to get its information $data = qweb_aca_ip_lookup($accessKey, $ip); // And cache the result if(isset($data->answer) && $data->answer == 'success') file_put_contents($cacheFile, json_encode($data)); } if(isset($data)) { if(isset($data->answer)) { if($data->answer == 'success') { // If we've received a successful response, we must be within the quota allowance so remove any previously set 'reached' flag if(get_transient('qweb_aca_quota_reached') !== false) delete_transient('qweb_aca_quota_reached'); // Check that the returned country code is in our allowlist and, if allow_known_proxies is false, check that the IP isn't a known proxy if(($data->is_proxy == 'yes' && !get_option('qweb_aca_allow_known_proxies')) || !in_array($data->country, $allowedCountries)) { // Access isn't allowed header('HTTP/1.0 403 Forbidden'); exit; } } else { // We don't want to bombard inboxes when an access quota is reached, so: $quotaReached = stripos($data->answer, 'this limit has already been reached') !== false; if(!$quotaReached || ($quotaReached && get_transient('qweb_aca_quota_reached') === false)) { if($quotaReached) { set_transient('qweb_aca_quota_reached', true); $data->answer .= ' Blocking is now paused until you fall back within your quota.'; } wp_mail(get_bloginfo('admin_email'), get_bloginfo('name').' - '.__('IP to country lookup failed!', 'admin-country-allowlist'), sprintf( __('The following response was received from the lookup API when attempting to verify the country of IP %1$s: %2$s', 'admin-country-allowlist'), esc_html($ip), esc_html($data->answer) )); } } } else { wp_mail(get_bloginfo('admin_email'), get_bloginfo('name').' - '.__('IP to country lookup failed!', 'admin-country-allowlist'), sprintf( __('The following error was generated when attempting to verify the country of IP %1$s: %2$s', 'admin-country-allowlist'), esc_html($ip), esc_html($data) )); } } } } return true; } // Function to create necessary folders/options and do basic sanity checks on activation function qweb_aca_activation() { // Create the cache folder $cacheDirectory = qweb_aca_cache_folder(); if(!is_dir($cacheDirectory)) mkdir($cacheDirectory, 0755); // Create the options add_option('qweb_aca_access_key', ''); add_option('qweb_aca_allowed_countries', array()); add_option('qweb_aca_allow_known_proxies', false); add_option('qweb_aca_block_xmlrpc_access', false); // Schedule the cache cleaning cron if(!wp_next_scheduled('qweb_aca_clear_old_cache_event')) wp_schedule_event(time(), 'daily', 'qweb_aca_clear_old_cache_event'); return true; } function qweb_aca_deactivation() { // We keep the cache and options in place until uninstallation, but we don't want to keep the cron running if the plugin is deactivated wp_unschedule_event(wp_next_scheduled('qweb_aca_clear_old_cache_event'), 'qweb_aca_clear_old_cache_event'); } // Function to clean up on uninstallation function qweb_aca_uninstallation() { // Unschedule the cache cleaning cron. We do this first, to make sure it doesn't run again after deleting the folder wp_unschedule_event(wp_next_scheduled('qweb_aca_clear_old_cache_event'), 'qweb_aca_clear_old_cache_event'); // Delete the cache folder $cacheDirectory = qweb_aca_cache_folder(); if(is_dir($cacheDirectory)) { // Folders must be empty before deletion foreach (array_diff(scandir($cacheDirectory), array('.','..')) as $file) { unlink($cacheDirectory.DIRECTORY_SEPARATOR.$file); } rmdir($cacheDirectory); } // Delete the options delete_option('qweb_aca_access_key'); delete_option('qweb_aca_allowed_countries'); delete_option('qweb_aca_allow_known_proxies'); delete_option('qweb_aca_block_xmlrpc_access'); // Remove htaccess entries if(extract_from_markers(get_home_path().'.htaccess', 'QWeb Admin Country Allowlist XMLRPC Blocking')) insert_with_markers(get_home_path().'.htaccess', 'QWeb Admin Country Allowlist XMLRPC Blocking', ''); return true; } // Function to return an array of recognised country codes. function qweb_aca_country_codes() { return array('AD','AE','AF','AG','AI','AL','AM','AO','AR','AT','AU','AW','AX','AZ','BA','BB','BD','BE','BF','BG','BH','BI','BJ','BL','BM','BN','BO','BQ','BR','BS','BT','BV','BW','BY','BZ','CA','CC','CD','CF','CG','CH','CI','CK','CL','CM','CN','CO','CR','CU','CV','CW','CX','CY','CZ','DE','DJ','DK','DM','DO','DZ','EC','EE','EG','ER','ES','ET','FI','FJ','FK','FM','FO','FR','GA','GB','GD','GE','GF','GG','GH','GI','GL','GM','GN','GP','GQ','GR','GS','GT','GU','GW','GY','HK','HM','HN','HR','HT','HU','ID','IE','IL','IM','IN','IO','IQ','IR','IS','IT','JE','JM','JO','JP','KE','KG','KH','KI','KM','KN','KP','KR','KW','KY','KZ','LA','LB','LC','LI','LK','LR','LS','LT','LU','LV','LY','MA','MC','MD','ME','MF','MG','MH','MK','ML','MM','MN','MO','MP','MQ','MR','MS','MT','MU','MV','MW','MX','MY','MZ','NA','NC','NE','NF','NG','NI','NL','NO','NP','NR','NU','NZ','OM','PA','PE','PF','PG','PH','PK','PL','PM','PN','PR','PS','PT','PW','PY','QA','RE','RO','RS','RU','RW','SA','SB','SC','SD','SE','SG','SH','SI','SK','SL','SM','SN','SO','SR','SS','ST','SV','SX','SY','SZ','TC','TD','TF','TG','TH','TJ','TK','TL','TM','TN','TO','TR','TT','TV','TW','TZ','UA','UG','UM','US','UY','UZ','VA','VC','VE','VG','VI','VN','VU','WF','WS','XK','YE','YT','ZA','ZM','ZW'); } // Function to generate the settings page function qweb_aca_settings_page() { // Check user capabilities if(!current_user_can('manage_options')) { return; } ?>

QWeb API Console and enter it here. Access keys are FREE!', 'admin-country-allowlist'); ?>

QWeb API Console. Simply log in, locate your access key from the list, and tap the Usage report icon.', 'admin-country-allowlist'); ?>

leave a review to help other website owners know what you think of this plugin.', 'admin-country-allowlist'); ?>

'string', 'description' => 'Your access key provided by the QWeb API portal', 'sanitize_callback' => function($input) { // Sanitise the field input // Use the QWeb API to get this visitors IP information using the entered key, because then we know if the key is valid, plus we can make sure the country is in the allowed list. // This purposefully doesn't use the cache mechanic because we need to know that the access key is valid $accessKey = trim((string)$input); if($accessKey != '') { $data = qweb_aca_ip_lookup($accessKey, qweb_aca_get_visitor_ip()); if($data->answer == 'success') { // Make sure $data->country is an allowed country $allowedCountries = get_option('qweb_aca_allowed_countries'); if(in_array($data->country, qweb_aca_country_codes()) && !in_array($data->country, $allowedCountries)) { // Add this country in to the allow list. This triggers our sanitise function the same as if the settings page was used, so we can leave that to convert to a sorted array array_push($allowedCountries, $data->country); update_option('qweb_aca_allowed_countries', implode(',', $allowedCountries)); } } else add_settings_error('qweb_aca_access_key', 'qweb_aca_access_key_error', sprintf( __('The following response was received from the lookup API when attempting to verify the country of IP %1$s: %2$s', 'admin-country-allowlist'), esc_html($ip), esc_html($data->answer) ), 'error'); } else add_settings_error('qweb_aca_access_key', 'qweb_aca_access_key_error', __('You must enter a valid access key', 'admin-country-allowlist'), 'error'); // Output sanitised value for Wordpress to save return $accessKey; })); register_setting( 'qweb_aca_options', 'qweb_aca_allowed_countries', array('type' => 'string', 'description' => 'Comma separated list of ISO 3166-1 alpha-2 country codes to allow', 'sanitize_callback' => function($input) { // Sanitise the field input // Turn the submitted string into an array of codes, iterate to verify each, and build a new array of those we accept $sanitisedCodes = array(); foreach(explode(',', $input) as $iKey => $iVal) { $code = strtoupper(trim($iVal)); if(in_array($code, qweb_aca_country_codes())) array_push($sanitisedCodes, $code); } // If we have an access key, make sure this visitors country is in the allow list $accessKey = trim(get_option('qweb_aca_access_key')); if($accessKey != '') { $data = qweb_aca_ip_lookup($accessKey, qweb_aca_get_visitor_ip()); if($data->answer == 'success') { if(in_array($data->country, qweb_aca_country_codes()) && !in_array($data->country, $sanitisedCodes)) array_push($sanitisedCodes, $data->country); } } // Alphabetical sorting sort($sanitisedCodes); // Output sanitised value for Wordpress to save // It doesn't matter if the array is empty at this point, it'll just trigger the qweb_aca_empty_countries_list notice if that happens return $sanitisedCodes; })); register_setting( 'qweb_aca_options', 'qweb_aca_allow_known_proxies', array('type' => 'boolean', 'description' => 'Should access be granted to known proxy IPs in these countries?', 'sanitize_callback' => function($input) { // Sanitise the field input // Output sanitised value for Wordpress to save // We basically only care if the submitted value is a yes. Anything else and we can just consider it a no. return ($input == 'yes'); })); register_setting( 'qweb_aca_options', 'qweb_aca_block_xmlrpc_access', array('type' => 'boolean', 'description' => 'Should access to XMLRPC be blocked completely?', 'sanitize_callback' => function($input) { // Sanitise the field input // If the submitted value is a yes, try to write a htaccess entry. Anything else and we can just consider it a no. if($input == 'yes') { if(!insert_with_markers(get_home_path().'.htaccess', 'QWeb Admin Country Allowlist XMLRPC Blocking', array( '', ' Order Deny,Allow', ' Deny from all', '', ))) { add_settings_error('qweb_aca_block_xmlrpc_access', 'qweb_aca_block_xmlrpc_access_error', __('Failed to update your .htaccess file with XMLRPC access blocking lines. Please ensure this file is accessible with read:write permissions', 'admin-country-allowlist'), 'error'); return get_option('qweb_aca_block_xmlrpc_access'); // Revert to the original setting } } else { // Remove htaccess entries if(extract_from_markers(get_home_path().'.htaccess', 'QWeb Admin Country Allowlist XMLRPC Blocking')) { if(!insert_with_markers(get_home_path().'.htaccess', 'QWeb Admin Country Allowlist XMLRPC Blocking', '')) { add_settings_error('qweb_aca_block_xmlrpc_access', 'qweb_aca_block_xmlrpc_access_error', __('Failed to remove the XMLRPC access blocking lines from your .htaccess file. Please ensure this file is accessible with read:write permissions', 'admin-country-allowlist'), 'error'); return get_option('qweb_aca_block_xmlrpc_access'); // Revert to the original setting } } } // If we get this far, then everything worked and we can store the submitted value as intended. return ($input == 'yes'); })); // Add a section to drop fields into add_settings_section('qweb_aca_options_general', 'Options', function() {}, __FILE__); // Create the fields add_settings_field('qweb_aca_access_key', 'Access key', function() { echo '

'.__('Your access key provided by the QWeb API Console.', 'admin-country-allowlist').'

'; }, __FILE__, 'qweb_aca_options_general'); add_settings_field( 'qweb_aca_allowed_countries', 'Allowed countries', function() { // TODO need a fancy mechanic to make populating this field easier, while keeping it a text input for copy + paste population echo '

'.__('Comma separated list of ISO 3166-1 alpha-2 country codes you allow admin panel access to.', 'admin-country-allowlist').'

'; }, __FILE__, 'qweb_aca_options_general'); add_settings_field( 'qweb_aca_allow_known_proxies', 'Allow known proxies?', function() { $currentValue = get_option('qweb_aca_allow_known_proxies'); echo '

'.__('If an IP is from an allowed country, but we know it to be a proxy server, should we still allow access?', 'admin-country-allowlist').'

'; }, __FILE__, 'qweb_aca_options_general'); add_settings_field( 'qweb_aca_block_xmlrpc_access', 'Block XMLRPC access?', function() { $currentValue = get_option('qweb_aca_block_xmlrpc_access'); echo '

'.__('If you\'re certain that nothing needs access to the Wordpress XMLRPC mechanism, then blocking access completely will save from having to perform API lookups for these requests, which can dramatically reduce the number of API requests that this website needs to invoke and help keep you within your API usage quota.', 'admin-country-allowlist').'

'.__('If you don\'t know what XMLRPC is, then you can almost certainly block access completely.', 'admin-country-allowlist').'

'; }, __FILE__, 'qweb_aca_options_general'); } // Function to add a settings link to the plugin in the plugins list function qweb_aca_list_settings_link($links) { // Filters the links in the plugin item of the plugins list, to include a settings link too array_push($links, ''.__('Settings', 'admin-country-allowlist').''); return $links; } // Function to clear old cache files function qweb_aca_clear_old_cache() { $cacheDirectory = qweb_aca_cache_folder(); // Only continue if this is a valid directory if(is_dir($cacheDirectory)) { // Delete cache older than 1 week $maxAge = time() - 604800; foreach (array_diff(scandir($cacheDirectory), array('.','..')) as $file) { if(filemtime($cacheDirectory.DIRECTORY_SEPARATOR.$file) < $maxAge) unlink($cacheDirectory.DIRECTORY_SEPARATOR.$file); } } } // Function to add a new item to the settings menu function qweb_aca_create_menu() { add_options_page('Admin Country Allowlist', 'Admin Country Allowlist', 'administrator', __FILE__, 'qweb_aca_settings_page'); } // Add settings link to the plugins list page add_filter('plugin_action_links_'.plugin_basename(__FILE__), 'qweb_aca_list_settings_link'); // Add settings link to the menu add_action('admin_menu', 'qweb_aca_create_menu'); // Register the settings add_action('admin_init', 'qweb_aca_register_settings'); // Plugin activation, deactivation, and uninstallation hooks register_activation_hook(__FILE__, 'qweb_aca_activation'); register_deactivation_hook(__FILE__, 'qweb_aca_deactivation'); register_uninstall_hook(__FILE__, 'qweb_aca_uninstallation'); // Cron event to delete old cache every week. This is scheduled during activation add_action('qweb_aca_clear_old_cache_event', 'qweb_aca_clear_old_cache'); // admin-ajax.php requests cause is_admin() to return true, but front end plugins also use it so we shouldn't block if(strpos($_SERVER['REQUEST_URI'], 'admin-ajax.php') === false) { if(is_admin()) { // Basic sanity checks and error outputs if logged in to the admin panel // Check cache directory exists $qwebAcaCacheDirectory = qweb_aca_cache_folder(); if(!is_dir($qwebAcaCacheDirectory) && !mkdir($qwebAcaCacheDirectory, 0755)) { function qweb_aca_cache_folder_missing() { printf( '

'.__('Admin Country Allowlist', 'admin-country-allowlist').'

%2$s %3$s

', esc_attr('notice notice-error'), esc_html__('Failed to automatically create the lookups cache folder. Please create the following folder, and ensure it\'s accessible with read:write permissions:', 'admin-country-allowlist'), esc_html($qwebAcaCacheDirectory) ); } add_action('admin_notices', 'qweb_aca_cache_folder_missing'); } elseif(!is_writable($qwebAcaCacheDirectory)) { function qweb_aca_cache_folder_not_writable() { printf( '

'.__('Admin Country Allowlist', 'admin-country-allowlist').'

%2$s %3$s

', esc_attr('notice notice-error'), esc_html__('The following folder isn\'t currently accessible with read:write permissions:', 'admin-country-allowlist'), esc_html($qwebAcaCacheDirectory) ); } add_action('admin_notices', 'qweb_aca_cache_folder_not_writable'); } // Check an access key is saved if(trim(get_option('qweb_aca_access_key')) == '') { function qweb_aca_access_key_missing() { printf( '

'.__('Admin Country Allowlist', 'admin-country-allowlist').'

%2$s

%3$s

', esc_attr('notice notice-error'), esc_html__('You need to obtain an API access key before this plugin can secure your website. Refer to the settings page for details.', 'admin-country-allowlist'), esc_html__('Settings', 'admin-country-allowlist') ); } add_action('admin_notices', 'qweb_aca_access_key_missing'); } else { // Check at least one country is allowed if(!array(get_option('qweb_aca_allowed_countries')) || empty(get_option('qweb_aca_allowed_countries'))) { function qweb_aca_empty_countries_list() { printf( '

'.__('Admin Country Allowlist', 'admin-country-allowlist').'

%2$s

', esc_attr('notice notice-error'), esc_html__('You need to allow at least one country before this plugin can secure your website.', 'admin-country-allowlist') ); } add_action('admin_notices', 'qweb_aca_empty_countries_list'); } } } // Hook in to the init routine of all admin pages and scripts add_filter('admin_init', 'qweb_aca_ip_check'); // Determine if this is a request for an admin page that doesn't trigger admin_init (because we're not yet logged in, for example), or for the XMLRPC mechanic which is basically an admin endpoint if((stripos($_SERVER['REQUEST_URI'], 'wp-login.php') !== false && ($GLOBALS['pagenow'] === 'wp-login.php' || $_SERVER['PHP_SELF'] === '/wp-login.php') && stripos($_SERVER['REQUEST_URI'], 'redirect_to='.admin_url()) !== false) || (defined('XMLRPC_REQUEST') && XMLRPC_REQUEST)) add_action('init', 'qweb_aca_ip_check'); }