get_charset_collate();
//successful and failed login attempts
$sql = "CREATE TABLE {$wpdb->prefix}meow2_log (
id bigint(20) NOT NULL AUTO_INCREMENT,
ip varchar(39) NOT NULL DEFAULT '',
subnet varchar(42) NOT NULL DEFAULT '',
date_created datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
date_expires datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
type enum('alert','ban','fail','success') NOT NULL DEFAULT 'fail',
username varchar(50) NOT NULL DEFAULT '',
count smallint(5) NOT NULL DEFAULT '1',
pardoned tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (id),
KEY ip (ip),
KEY subnet (subnet),
KEY date_created (date_created),
KEY date_expires (date_expires),
KEY type (type)
) $charset";
//save it
dbDelta($sql);
//take this opportunity to clear old settings
if(get_option('meow_update', 0) != MEOW_UPDATE)
meow_version_cleanup();
//save db version and exit
update_option('meow_db_version', MEOW_DB);
return true;
}
register_activation_hook(__FILE__,'meow_db');
//-------------------------------------------------
// Clean Up
//
// remove settings and tables from old versions
//
// @param n/a
// @return true
function meow_version_cleanup(){
global $wpdb;
//does the old log table exist?
if(!is_null($wpdb->get_var("SHOW TABLES WHERE `Tables_in_" . DB_NAME . "` = '{$wpdb->prefix}meow_log'"))){
//migrate data
$wpdb->query("INSERT INTO `{$wpdb->prefix}meow2_log`(`ip`, `date_created`, `type`, `username`) (SELECT `ip`, FROM_UNIXTIME(`date`) AS `date_created`, IF(`success`,'success','fail'), `username` FROM `{$wpdb->prefix}meow_log` ORDER BY `date_created` ASC)");
//drop table
$wpdb->query("DROP TABLE IF EXISTS `{$wpdb->prefix}meow_log`");
}
//drop the old banned table if it exists
$wpdb->query("DROP TABLE IF EXISTS `{$wpdb->prefix}meow_log_banned`");
//make sure we have subnets calculated for all listings
$dbResult = $wpdb->get_results("SELECT DISTINCT `ip` FROM `{$wpdb->prefix}meow2_log` WHERE NOT(LENGTH(`subnet`)) ORDER BY `ip` ASC", ARRAY_A);
if(is_array($dbResult) && count($dbResult)){
$updates = array();
foreach($dbResult AS $Row)
$updates[$Row['ip']] = meow_ip_to_subnet($Row['ip']);
//update en masse, but in chunks
$updates = array_chunk($updates, 100, true);
foreach($updates AS $u){
$query = "UPDATE `{$wpdb->prefix}meow2_log` SET `subnet` = CASE `ip`";
foreach($u AS $k=>$v)
$query .= "\nWHEN '" . esc_sql($k) . "' THEN '" . esc_sql($v) . "'";
$query .= "\nEND WHERE `ip` IN ('" . implode("','", array_map('esc_sql', array_keys($u))) . "')";
$wpdb->query($query);
}
}
//remove old options
$old_options = array(
'meow_alerts'=>'login-alert_on_new',
'meow_apocalypse_content'=>'',
'meow_apocalypse_title'=>'',
'meow_clean_database'=>'prune-active',
'meow_data_expiration'=>'prune-limit',
'meow_disable_editor'=>'core-file_edit',
'meow_disable_xmlrpc'=>'core-xmlrpc',
'meow_fail_limit'=>'login-fail_limit',
'meow_fail_reset_on_success'=>'login-reset_on_success',
'meow_fail_window'=>'login-fail_window',
'meow_ip_exempt'=>'login-exempt',
'meow_ip_key'=>'login-key',
'meow_login_nonce'=>'login-nonce',
'meow_password_alpha'=>'password-alpha',
'meow_password_length'=>'password-length',
'meow_password_numeric'=>'password-numeric',
'meow_password_symbol'=>'password-symbol',
'meow_protect_login'=>'',
'meow_remove_adjacent_posts_tag'=>'template-adjacent_posts',
'meow_remove_generator_tag'=>'template-generator_tag',
'meow_store_ua'=>''
);
//new options
$new_options = meow_get_options();
//port new settings, if they exist
$changed = false;
foreach($old_options AS $k=>$v){
//no correlation
if(!strlen($v))
continue;
//check old option
$tmp = get_option($k, null);
if(is_null($tmp))
continue;
//update!
list($class, $option) = explode('-', $v);
if($tmp !== $new_options[$class][$option]){
$changed = true;
$new_options[$class][$option] = $tmp;
}
}
if($changed){
update_option('meow_options', $new_options);
$new_options = meow_get_options(true);
}
//and delete
foreach(array_keys($old_options) AS $o)
delete_option($o);
update_option('meow_update', MEOW_UPDATE);
}
//-------------------------------------------------
// Make Sure Tables Exist
//
// @param n/a
// @return true/false
function meow_tables_exist(){
static $exist;
if(is_null($exist)){
global $wpdb;
if(is_null($wpdb->get_var("SHOW TABLES WHERE `Tables_in_" . DB_NAME . "` = '{$wpdb->prefix}meow2_log'"))){
$exist = false;
delete_option('meow_db_version');
}
else
$exist = true;
}
return $exist;
}
//-------------------------------------------------
// Require Tables
//
// no point loading our sub-pages if the tables
// are messed up.
//
// @param n/a
// @return true or die
function meow_require_tables_exist(){
if(!meow_tables_exist())
wp_die('This page cannot be loaded until the database tables are populated.');
return true;
}
//-------------------------------------------------
// DB Updates?
//
// @param n/a
// @return true
function meow_db_upgrade(){
//don't do this on AJAX or CRON
if((defined('DOING_CRON') && DOING_CRON) || (defined('DOING_AJAX') && DOING_AJAX))
return true;
//update the DB table structure
if(get_option('meow_db_version') != MEOW_DB)
meow_db();
}
add_action('plugins_loaded', 'meow_db_upgrade');
//-------------------------------------------------
// Get Default Options
//
// @param n/a
// @return options
function meow_get_default_options(){
return array(
//JSON API
'api'=>array(
'access'=>'all' //all, users, none
),
//core settings
'core'=>array(
'file_edit'=>false, //disable file editor
'xmlrpc'=>false, //disable xmlrpc
'enumeration'=>false, //try to stop user enumeration
'enumeration_die'=>false //die on attempt
),
//login settings
'login'=>array(
'fail_limit'=>5, //max fails
'fail_window'=>43200, //fail window
'subnet_fail_limit'=>20, //limit to ban whole subnet
'reset_on_success'=>true, //reset fail count on success
'exempt'=>array(), //exempt IPs
'nonce'=>false, //add a nonce to login form
'key'=>'REMOTE_ADDR', //where in $_SERVER to find IP,
'alert_on_new'=>true, //alert on new login
'alert_by_subnet'=>true //use subnet to determine newness
),
//password strength requirements
'password'=>array(
'alpha'=>'required', //require letters
'numeric'=>'required', //require numbers
'symbol'=>'optional', //require symbols
'length'=>10, //min length
'bcrypt'=>false //hash passwords using bcrypt
),
//prune db after X days
'prune'=>array(
'active'=>true,
'limit'=>90 //clear after X days
),
//template settings
'template'=>array(
'generator_tag'=>true, //remove generator tag
'adjacent_posts'=>true, //remove previous/next post tags
'readme'=>false, //remove readme file,
'noopener'=>false //add rel="noopener" to vulnerable links
)
);
}
//-------------------------------------------------
// Deprecated Constants
//
// these constants used to be used to pre-set
// plugin options. the naming scheme has since been
// standardized, but we'll keep honoring them for
// a while.
//
// @param n/a
// @return constants
function meow_get_deprecated_constants(){
return array(
'core'=>array(
'xmlrpc'=>'MEOW_DISABLE_XMLRPC'
),
'prune'=>array(
'active'=>'MEOW_CLEAN_DATABASE',
'limit'=>'MEOW_DATA_EXPIRATION'
),
'login'=>array(
'fail_limit'=>'MEOW_FAIL_LIMIT',
'fail_window'=>'MEOW_FAIL_WINDOW',
'reset_on_success'=>'MEOW_FAIL_RESET_ON_SUCCESS',
'alert_on_new'=>'MEOW_ALERTS'
),
'template'=>array(
'generator_tag'=>'MEOW_REMOVE_GENERATOR_TAG',
'adjacent_posts'=>'MEOW_REMOVE_ADJACENT_POSTS_TAG'
)
);
}
//-------------------------------------------------
// Get Constant Option
//
// if an option is defined in wp-config.php, it
// will return that value, otherwise null
//
// @param class
// @param option
// @return value or null
function meow_get_constant_value($class, $option){
$deprecated = meow_get_deprecated_constants();
$constant = strtoupper("MEOW_{$class}_{$option}");
//there are a few values that are not allowed
//to be set this way
if($constant === 'MEOW_LOGIN_EXEMPT')
return null;
//constant override?
if(defined($constant))
return constant($constant);
//deprecated constant?
if(isset($deprecated[$class][$option]) && defined($deprecated[$class][$option]))
return constant($deprecated[$class][$option]);
return null;
}
//-------------------------------------------------
// Get Options
//
// options are variously sanitized, and also
// possibly defined in wp-config.php. this is a
// big function, but makes life easy every else
//
// @param refresh
// @return options
function meow_get_options($refresh=false){
static $options;
if(is_null($options) || $refresh){
$defaults = meow_get_default_options();
//pull settings from database. we'll loop through the
//arrays manually to make sure the keys match up at
//all depths
$raw = get_option('meow_options', array());
$options = $defaults;
if(is_array($raw) && count($raw)){
foreach($raw AS $k=>$v){
//should be an array
if(is_array($v) && isset($options[$k])){
foreach($v AS $k2=>$v2){
if(isset($options[$k][$k2]))
$options[$k][$k2] = $v2;
}
}
}//raw key is good
}//raw is array
//deprecated constants
$deprecated = meow_get_deprecated_constants();
//store our readonly values
$readonly = array();
//lightly sanitize and typecast data
foreach($options AS $k=>$v){
//at this level, it should be an array
//so the settings are mondo bad, revert
if(!is_array($v))
$options[$k] = $defaults[$k];
foreach($v AS $k2=>$v2){
//combined key for easier management
$key = "$k-$k2";
//defined as constant?
$tmp = meow_get_constant_value($k, $k2);
if(!is_null($tmp)){
$options[$k][$k2] = $tmp;
$readonly[] = $key;
}
//now let's typecast
//bools
if(in_array($key, array(
'core-file_edit',
'core-xmlrpc',
'core-enumeration',
'core-enumeration_die',
'password-bcrypt',
'prune-active',
'login-reset_on_success',
'login-nonce',
'login-alert_on_new',
'login-alert_by_subnet',
'template-generator_tag',
'template-adjacent_posts',
'template-readme',
'template-noopener'
)))
$options[$k][$k2] = (bool) $options[$k][$k2];
//ints
elseif(in_array($key, array(
'password-length',
'prune-limit',
'login-fail_limit',
'login-fail_window',
'login-subnet_fail_limit'
)))
$options[$k][$k2] = absint($options[$k][$k2]);
//arrays
elseif(in_array($key, array(
'login-exempt',
)))
$options[$k][$k2] = (array) $options[$k][$k2];
//everything else is a string
else
$options[$k][$k2] = (string) $options[$k][$k2];
//certain options have limits
//json api
if($key === 'api-access'){
$options[$k][$k2] = strtolower($options[$k][$k2]);
if(!in_array($options[$k][$k2], array('all','users','none')))
$options[$k][$k2] = 'all';
}
//fail limit
if($key === 'login-fail_limit'){
if($options[$k][$k2] < 3)
$options[$k][$k2] = 3;
elseif($options[$k][$k2] > 50)
$options[$k][$k2] = 50;
}
//subnet fail limit
elseif($key === 'login-subnet_fail_limit'){
$min = $options['login']['fail_limit'] > 10 ? $options['login']['fail_limit'] : 10;
if($options[$k][$k2] < $min)
$options[$k][$k2] = $min;
elseif($options[$k][$k2] > 100)
$options[$k][$k2] = 100;
}
//fail window
elseif($key === 'login-fail_window'){
if($options[$k][$k2] < 600)
$options[$k][$k2] = 600;
elseif($options[$k][$k2] > 86400)
$options[$k][$k2] = 86400;
}
//whitelist
elseif($key === 'login-exempt'){
if(count($options[$k][$k2])){
$tmp = $options[$k][$k2];
foreach($tmp AS $k3=>$v3){
$v3 = preg_replace('/[^\da-f\.\:\/\-]/i', '', $v3);
//regular IP
if(filter_var($v3, FILTER_VALIDATE_IP)){
if(false !== ($tmp[$k3] = apply_filters('meow_filter_ip', $v3)))
continue;
}
//a range?
elseif(substr_count($v3, '-') === 1){
$tmp2 = explode('-', $v3);
$tmp2[0] = apply_filters('meow_filter_ip', $tmp2[0]);
$tmp2[1] = apply_filters('meow_filter_ip', $tmp2[1]);
if(false !== $tmp2[0] && false !== $tmp2[1]){
//if they're equal, then just use an IP
if($tmp2[0] === $tmp2[1])
$tmp[$k3] = $tmp2[0];
else{
if(meow_ip_to_number($tmp2[0]) > meow_ip_to_number($tmp2[1]))
meow_switcheroo($tmp2[0], $tmp2[1]);
$tmp[$k3] = implode('-', $tmp2);
}
continue;
}
}
//a subnet?
elseif(substr_count($v3, '/') === 1){
if(false !== meow_cidr_to_range($v3)){
$tmp2 = explode('/', $v3);
if(false !== ($tmp2[0] = apply_filters('meow_filter_ip', $tmp2[0]))){
$tmp[$k3] = meow_filter_ip_sanitize($tmp2[0]) . '/' . intval($tmp2[1]);
continue;
}
}
}
//kill it
unset($tmp[$k3]);
}
$tmp = array_unique($tmp);
sort($tmp);
$options[$k][$k2] = $tmp;
}
}
//data expiration
elseif($key === 'prune-limit'){
if($options[$k][$k2] < 30)
$options[$k][$k2] = 30;
elseif($options[$k][$k2] > 365)
$options[$k][$k2] = 365;
}
//password alpha
elseif($key === 'password-alpha'){
$options[$k][$k2] = strtolower($options[$k][$k2]);
if(!in_array($options[$k][$k2], array('optional','required','required-both')))
$options[$k][$k2] = 'optional';
}
//password numeric/symbol
elseif($key === 'password-numeric' || $key === 'password-symbol'){
$options[$k][$k2] = strtolower($options[$k][$k2]);
if(!in_array($options[$k][$k2], array('optional','required')))
$options[$k][$k2] = 'optional';
}
//password length
elseif($key === 'password-length'){
if($options[$k][$k2] < 5)
$options[$k][$k2] = 5;
elseif($options[$k][$k2] > 100)
$options[$k][$k2] = 100;
}
//bcrypt
elseif($key === 'password-bcrypt'){
if($options[$k][$k2] && !version_compare(PHP_VERSION, '5.4.0', '>=')){
$options[$k][$k2] = false;
if(!in_array($key, $readonly))
$readonly[] = $key;
}
}
}//each option
}//each class
//if something changed, save it
if(json_encode($raw) !== json_encode($options))
update_option('meow_options', $options);
//lastly, stuff our readonly values into the array
//don't need to save this to the database, but it
//is useful for the settings page, etc.
$options['readonly'] = $readonly;
}//options not set
return $options;
}
//-------------------------------------------------
// Get Single Option
//
// @param class
// @param option
// @return option or false
function meow_get_option($class, $option=null){
$options = meow_get_options();
//just class
if(is_null($option))
return isset($options[$class]) ? $options[$class] : false;
//return specific option
return isset($options[$class][$option]) ? $options[$class][$option] : false;
}
//--------------------------------------------------------------------- end init
//---------------------------------------------------------------------
// Misc Helpers
//---------------------------------------------------------------------
//-------------------------------------------------
// Stricter Parse Args
//
// like wp_parse_args, but we only want to allow
// keys in the template
//
// @param args
// @param defaults
// @return parsed or false
function meow_parse_args($args, $defaults){
$args = (array) $args;
$defaults = (array) $defaults;
if(!count($defaults))
return false;
if(!count($args))
return $defaults;
foreach($defaults AS $k=>$v){
if(array_key_exists($k, $args))
$defaults[$k] = $args[$k];
}
return $defaults;
}
//-------------------------------------------------
// Switch two variables
//
// @param var1
// @param var2
// @return n/a
function meow_switcheroo(&$var1, &$var2){
$tmp = $var2;
$var2 = $var1;
$var1 = $tmp;
return true;
}
//-------------------------------------------------
// Relative Date Diff
//
// @param date1
// @param date2
// @return difference
function meow_relative_date_diff($date1, $date2){
if(false === ($date1 = strtotime($date1)) || false === ($date2 = strtotime($date2)) || $date1 === $date2)
return false;
$s = absint($date1 - $date2);
$h = floor($s / 60 / 60);
$s -= $h * 60 * 60;
$m = floor($s / 60);
$s -= $m * 60;
$out = array();
if($h > 0)
$out[] = "$h hour" . ($h === 1 ? '' : 's');
if($m > 0)
$out[] = "$m minute" . ($m === 1 ? '' : 's');
if($s > 0)
$out[] = "$s second" . ($s === 1 ? '' : 's');
return implode(', ', $out);
}
//-------------------------------------------------
// User Exists
//
// cache lookups so we can run in a loop and not
// pull the user object over and over for the same
// person
//
// @param user
// @return true
function meow_username_exists($username=''){
static $users;
if(is_null($users))
$users = array();
if(!strlen($username))
return false;
if(!isset($users[$username]))
$users[$username] = username_exists($username);
return $users[$username];
}
//--------------------------------------------------------------------- end helpers
//---------------------------------------------------------------------
// Core/Template
//---------------------------------------------------------------------
//-------------------------------------------------
// Core/Template Actions
//
// perform various functions at init
//
// @param n/a
// @return true
function meow_do_core_actions(){
$options = meow_get_options();
//disable file editor
if($options['core']['file_edit'] && !defined('DISALLOW_FILE_EDIT'))
define('DISALLOW_FILE_EDIT', true);
//disable xml-rpc
if($options['core']['xmlrpc'])
add_filter('xmlrpc_enabled', '__return_false');
//disble adjacent posts
if($options['template']['adjacent_posts']){
add_filter('previous_post_rel_link', '__return_false');
add_filter('next_post_rel_link', '__return_false');
}
//disable generator tag
if($options['template']['generator_tag'])
add_filter('the_generator', '__return_false');
//readme file
if($options['template']['readme'] && file_exists(trailingslashit(ABSPATH) . 'readme.html'))
@unlink(trailingslashit(ABSPATH) . 'readme.html');
//enqueue rel=noopener script
if($options['template']['noopener'])
add_action('wp_enqueue_scripts', 'meow_enqueue_noopener');
//user enumeration
if(meow_get_option('core','enumeration')){
if(isset($_GET['author']) && strlen(get_option('permalink_structure')))
meow_disable_user_enumeration();
add_filter('rest_authentication_errors', 'meow_disable_user_enumeration_api');
}
//api?
if(meow_get_option('api','access') !== 'all')
meow_restrict_api();
return true;
}
add_action('init', 'meow_do_core_actions');
//--------------------------------------------------
// restrict API access
//
// the API is pretty cool, but many sites don't use
// it and bad guys might...
//
// @param n/a
// @return n/a
function meow_restrict_api(){
//we used to be able to disable WP-REST entirely
if((meow_get_option('api','access') === 'none' || !is_user_logged_in()) && version_compare(get_bloginfo('version'), '4.7', '<')) {
//disable wholesale
add_filter('json_enabled', '__return_false');
add_filter('json_jsonp_enabled', '__return_false');
add_filter('rest_enabled', '__return_false');
add_filter('rest_jsonp_enabled', '__return_false');
//remove links from header
remove_action('xmlrpc_rsd_apis', 'rest_output_rsd');
remove_action('wp_head', 'rest_output_link_wp_head', 10);
remove_action('template_redirect', 'rest_output_link_header', 11);
return true;
}
//otherwise let's hook into the authentication filter
add_filter('rest_authentication_errors', 'meow_restrict_api_to_users');
}
//-------------------------------------------------
// restrict API access to users
//
// @param access
// @return access or error
function meow_restrict_api_to_users($access){
//nobody can access it
if(meow_get_option('api','access') === 'none')
return new WP_Error('rest_access_forbidden_all', 'The WP-REST API has been disabled.', array('status'=>403));
//only users can access it
elseif(!is_user_logged_in())
return new WP_Error('rest_access_forbidden_nonuser', 'The REST API is only available to logged-in users.', array('status'=>rest_authorization_required_code()));
return $access;
}
//--------------------------------------------------
// rel="noopener"
//
// offsite links (e.g. target="_blank") can leave
// the original window vulnerable to a redirect/
// phishing attack. this eliminates the attack in
// browsers supporting the noopener value
//
// @param n/a
// @return n/a
function meow_enqueue_noopener(){
wp_register_script(
'meow_js_noopener',
plugins_url('js/noopener.min.js', __FILE__),
array(),
MEOW_VERSION
);
wp_enqueue_script('meow_js_noopener');
}
//--------------------------------------------------
// Disable User Enumeration
//
// this blocks WordPress' rewriting of ?author=X
//
// @param n/a
// @return true or redirect
function meow_disable_user_enumeration(){
//don't run this when doing cron, ajax, or admin
if(
(defined('DOING_CRON') && DOING_CRON) ||
(defined('DOING_AJAX') && DOING_AJAX) ||
is_admin())
return true;
//trigger an error
if(meow_get_option('core','enumeration_die'))
wp_die('Author archives are not accessible by user ID.', 'Invalid Request', 400);
//otherwise send them to the home page
wp_redirect(site_url());
exit;
}
//-------------------------------------------------
// Disable User Enumeration (API)
//
// this blocks WordPress user API requests
//
// @param access
// @return true or error
function meow_disable_user_enumeration_api($access){
if(preg_match('/\/users\//i', trailingslashit($GLOBALS['wp']->query_vars['rest_route'])))
return new WP_Error('rest_access_enumeration_forbidden', 'WP-REST user access is disabled.', array('status'=>403));
return $access;
}
add_action('rest_api_init', 'meow_disable_user_enumeration_api', 100);
//--------------------------------------------------------------------- end core
//---------------------------------------------------------------------
// Login
//---------------------------------------------------------------------
//-------------------------------------------------
// Add: Login nonce
//
// add a nonce field to the login form
//
// @param n/a
// @return true
function meow_get_login_nonce(){
//not doing it
if(!meow_get_option('login', 'nonce'))
return false;
//simple enough
echo '';
}
add_action('login_form', 'meow_get_login_nonce');
//--------------------------------------------------
// Verify: Login nonce
//
// verify the nonce is present and good
//
// @param user
// @param username
// @param password
// @return wp_error or user
function meow_validate_login_nonce($user, $username, $password){
//validate nonce, but only if applicable
if(meow_get_option('login', 'nonce') && getenv('REQUEST_METHOD') === 'POST' && (!isset($_POST['meow-login-nonce']) || !wp_verify_nonce($_POST['meow-login-nonce'], 'meow-login')))
return new WP_Error('meow_login_nonce_error', 'ERROR: The form had expired. Please try again.');
return $user;
}
add_filter('authenticate', 'meow_validate_login_nonce', 50, 3);
//--------------------------------------------------
// Is an IP Banned?
//
// @param ip
// @param increase count
// @return true/false
function meow_is_banned($ip=null, $count=false){
global $wpdb;
//get the IP
if(is_null($ip))
$ip = meow_get_user_ip();
if(!filter_var($ip, FILTER_VALIDATE_IP))
return false;
$subnet = meow_ip_to_subnet($ip);
$id = (int) $wpdb->get_var("SELECT `id` FROM `{$wpdb->prefix}meow2_log` WHERE (`ip`='$ip' OR `subnet`='$subnet') AND `type`='ban' AND `date_expires`>'" . current_time('mysql') . "' ORDER BY `date_expires` ASC LIMIT 1");
if($id){
//update the count
if($count)
$wpdb->query("UPDATE `{$wpdb->prefix}meow2_log` SET `count`=`count`+1 WHERE `id`=$id");
//and yes, they're banned
return true;
}
return false;
}
//--------------------------------------------------
// Is an IP Whitelisted?
//
// @param ip
// @return true/false
function meow_is_whitelisted($ip=null){
//get the IP
if(is_null($ip))
$ip = meow_get_user_ip();
if(!filter_var($ip, FILTER_VALIDATE_IP))
return false;
$whitelist = meow_get_option('login', 'exempt');
if(!count($whitelist))
return false;
foreach($whitelist AS $range){
if(meow_ip_in_range($ip, $range))
return true;
}
return false;
}
//--------------------------------------------------
// Login Gatekeeper
//
// see if we should process a login or not
//
// @param n/a
// @return true
function meow_login_gatekeeper(){
//check for bans
if(meow_is_banned(null, true))
wp_die('For security reasons, logins from your network are temporarily prohibited.', 'Login Denied', 403);
//all is well
return true;
}
add_action('login_init', 'meow_login_gatekeeper');
//--------------------------------------------------
// Log Something
//
// @param args
// @return true
function meow_login_log($args=null){
global $wpdb;
//must have a valid IP
if(false === ($ip = meow_get_user_ip()))
return true;
$subnet = meow_ip_to_subnet($ip);
$defaults = array(
'ip'=>$ip,
'subnet'=>$subnet,
'date_created'=>current_time('mysql'),
'date_expires'=>'0000-00-00 00:00:00',
'type'=>'fail',
'username'=>'',
'count'=>1
);
$data = meow_parse_args($args, $defaults);
$wpdb->insert("{$wpdb->prefix}meow2_log", $data, '%s');
//we're done if not failing or if the user is whitelisted
if($data['type'] !== 'fail' || meow_is_whitelisted($ip))
return true;
$fail_window = meow_get_option('login', 'fail_window');
//has the ip fail limit reached?
if(false !== ($remaining = meow_ip_fails_remaining())){
if(!$remaining){
$limit = meow_ip_min_fail_date();
$date = $wpdb->get_var("SELECT MIN(`date_created`) AS `date` FROM `{$wpdb->prefix}meow2_log` WHERE `type`='fail' AND `ip`='$ip' AND `date_created` > '$limit'");
return meow_login_log(
array(
'ip'=>$ip,
'subnet'=>'0',
'date_expires'=>date('Y-m-d H:i:s', strtotime("+$fail_window seconds", strtotime($date))),
'type'=>'ban'
)
);
}
}
//has the ip fail limit reached?
if(false !== ($remaining = meow_subnet_fails_remaining())){
if(!$remaining){
$limit = meow_subnet_min_fail_date();
$date = $wpdb->get_var("SELECT MIN(`date_created`) AS `date` FROM `{$wpdb->prefix}meow2_log` WHERE `type`='fail' AND `subnet`='$subnet' AND `date_created` > '$limit'");
return meow_login_log(
array(
'ip'=>0,
'subnet'=>$subnet,
'date_expires'=>date('Y-m-d H:i:s', strtotime("+$fail_window seconds", strtotime($date))),
'type'=>'ban'
)
);
}
}
return true;
}
//--------------------------------------------------
// Minimum IP Fail Date
//
// login failures are only counted after this time
//
// @param n/a
// @return date or false
function meow_ip_min_fail_date(){
global $wpdb;
static $min_date;
if(is_null($min_date)){
//must have a valid IP
if(false === ($ip = meow_get_user_ip()) || meow_is_whitelisted($ip)){
$min_date = false;
return false;
}
$fail_window = meow_get_option('login', 'fail_window');
$reset_on_success = meow_get_option('login', 'reset_on_success');
$min = date('Y-m-d H:i:s', strtotime("-$fail_window seconds", current_time('timestamp')));
if($reset_on_success)
$min_date = $wpdb->get_var("SELECT MAX(`date_created`) FROM `{$wpdb->prefix}meow2_log` WHERE `type`='success' AND `ip`='$ip'");
if(is_null($min_date) || $min_date < $min)
$min_date = $min;
}
return $min_date;
}
//--------------------------------------------------
// IP Fails Remaining
//
// @param n/a
// @return fails allowed or false if not applicable
function meow_ip_fails_remaining(){
global $wpdb;
static $remaining;
if(is_null($remaining)){
//must have a valid IP
if(false === ($ip = meow_get_user_ip()) || meow_is_whitelisted($ip)){
$remaining = false;
return false;
}
$fail_limit = meow_get_option('login', 'fail_limit');
$min_date = meow_ip_min_fail_date();
$fails = (int) $wpdb->get_var("SELECT COUNT(*) AS `count` FROM `{$wpdb->prefix}meow2_log` WHERE `type`='fail' AND `ip`='$ip' AND `date_created` > '$min_date'");
$remaining = $fail_limit - $fails;
if($remaining < 0)
$remaining = 0;
}
return $remaining;
}
//--------------------------------------------------
// Minimum Subnet Fail Date
//
// login failures are only counted after this time
//
// @param n/a
// @return date or false
function meow_subnet_min_fail_date(){
global $wpdb;
static $min_date;
if(is_null($min_date)){
//must have a valid IP
if(false === ($ip = meow_get_user_ip()) || meow_is_whitelisted($ip)){
$min_date = false;
return false;
}
$subnet = meow_ip_to_subnet($ip);
$fail_window = meow_get_option('login', 'fail_window');
$reset_on_success = meow_get_option('login', 'reset_on_success');
$min = date('Y-m-d H:i:s', strtotime("-$fail_window seconds", current_time('timestamp')));
if($reset_on_success)
$min_date = $wpdb->get_var("SELECT MAX(`date_created`) FROM `{$wpdb->prefix}meow2_log` WHERE `type`='success' AND `subnet`='$subnet'");
if(is_null($min_date) || $min_date < $min)
$min_date = $min;
}
return $min_date;
}
//--------------------------------------------------
// Subnet Fails Remaining
//
// @param n/a
// @return fails allowed or false if not applicable
function meow_subnet_fails_remaining(){
global $wpdb;
static $remaining;
if(is_null($remaining)){
//must have a valid IP
if(false === ($ip = meow_get_user_ip()) || meow_is_whitelisted($ip)){
$remaining = false;
return false;
}
$subnet = meow_ip_to_subnet($ip);
$subnet_fail_limit = meow_get_option('login', 'subnet_fail_limit');
$min_date = meow_subnet_min_fail_date();
$fails = (int) $wpdb->get_var("SELECT COUNT(*) AS `count` FROM `{$wpdb->prefix}meow2_log` WHERE `type`='fail' AND `subnet`='$subnet' AND `date_created` > '$min_date'");
$remaining = $subnet_fail_limit - $fails;
if($remaining < 0)
$remaining = 0;
}
return $remaining;
}
//--------------------------------------------------
// Fails Remaining
//
// this will return the IP or subnet limit,
// whichever is lower
//
// @param n/a
// @return remaining or false
function meow_fails_remaining(){
$remaining = null;
if(false === ($remaining = meow_ip_fails_remaining()))
return false;
if(false !== ($remaining_subnet = meow_subnet_fails_remaining())){
if(false === $remaining || $remaining_subnet < $remaining)
$remaining = $remaining_subnet;
}
return is_null($remaining) ? false : $remaining;
}
//--------------------------------------------------
// Indicate Attempts Remaining
//
// @param errors
// @return errors
function meow_print_fails_remaining($errors=null, $redirect=null){
//WP jams non-errors into the error object for some reason
$has_errors = false;
if(is_wp_error($errors) && count($errors->errors)){
foreach ($errors->get_error_codes() as $code){
if($errors->get_error_data($code) !== 'message'){
$has_errors = true;
break;
}
}
}
//indicate how many opportunities are left
if($has_errors && false !== $remaining = meow_fails_remaining()){
if($remaining > 0)
$errors->add('attempts-remaining', "Login attempts remaining: $remaining", 'message');
else
$errors->add('attempts-remaining', 'ERROR: The account has been temporarily locked. Please try again later.');
}
return $errors;
}
add_filter('wp_login_errors', 'meow_print_fails_remaining', 100, 2);
//--------------------------------------------------
// Wrapper for meow_login_log on failure
//
// @param n/a
// @return true
function meow_login_error($username){
return meow_login_log(array('type'=>'fail', 'username'=>trim(strtolower($username))));
}
add_action('wp_login_failed', 'meow_login_error', 10, 1);
//--------------------------------------------------
// Wrapper for meow_login_log on success
//
// @param user login
// @param user
// @return true
function meow_login_success($username, $user){
return meow_login_log(array('type'=>'success', 'username'=>trim(strtolower($username))));
}
add_action('wp_login', 'meow_login_success', 10, 2);
//--------------------------------------------------
// Email alert on new location
//
// @param user login
// @param user
// @return true
function meow_email_alert($username, $user){
global $wpdb;
//this only works if we have a valid IP
if(false !== ($ip = meow_get_user_ip()) && meow_get_option('login','alert_on_new'))
{
$subnet = meow_get_option('login','alert_by_subnet') ? meow_ip_to_subnet($ip) : false;
//if this IP has been associated with just one successful login, it was the one just made
if(intval($wpdb->get_var("SELECT COUNT(*) FROM `{$wpdb->prefix}meow2_log` WHERE `type`='success' AND " . ($subnet ? "`subnet`='$subnet'" : "`ip`='$ip'"))) === 1)
{
//follow the basic wording used by other WordPress emails
$mbody = "Hi {$username},\n\nThis is an automated alert to inform you that your account at " . get_bloginfo('name') . " has been accessed from a new network address.\n\nIf this was not you, immediately visit " . admin_url('profile.php') . " and reset your password. You should also end all other sessions and make sure no unauthorized changes have been made to your site.\n\nLogin Time: " . current_time('l, F j, Y @ H:i:s') . "\nBrowser: {$_SERVER['HTTP_USER_AGENT']}\nIP: $ip\n\nThis email has been sent to {$user->user_email}\n\nRegards,\nAll at " . get_bloginfo('name') . "\n" . site_url();
wp_mail($user->user_email, "[" . get_bloginfo('name') . "] Login Alert", $mbody);
//and log it
meow_login_log(array('type'=>'alert', 'username'=>trim(strtolower($username))));
}
}
return true;
}
add_action('wp_login', 'meow_email_alert', 20, 2);
//--------------------------------------------------------------------- end login
//----------------------------------------------------------------------
// Password restrictions
//----------------------------------------------------------------------
// functions to ensure user passwords meet certain minimum safety
// standards
//--------------------------------------------------
// Common passwords
//
// this is mandatory :)
//
// Top 500 from http://www.whatsmypass.com/?p=415
// 370 Banned Twitter http://techcrunch.com/2009/12/27/twitter-banned-passwords/
//
// @param pass
// @return true/false
function meow_is_common_password($pass){
try {
$pass = trim(strtolower($pass));
if(strlen($pass) && file_exists(dirname(__FILE__) . '/bad-passwords.json')){
$common = json_decode(file_get_contents(dirname(__FILE__) . '/bad-passwords.json'));
if(is_array($common) && count($common)){
//try a few simple iterations
if(in_array($pass, $common))
return true;
//some people just toss 12345 onto the end
elseif(preg_match('/12345$/', $pass) && in_array(substr($pass, 0, strlen($pass) - 5), $common))
return true;
//some people just toss 123 onto the end
elseif(preg_match('/123$/', $pass) && in_array(substr($pass, 0, strlen($pass) - 3), $common))
return true;
//some people just toss 1 onto the end
elseif(preg_match('/1$/', $pass) && in_array(substr($pass, 0, strlen($pass) - 1), $common))
return true;
}
}
} catch(Exception $e){ return false; }
//if we're still here, something's gone wrong.
return false;
}
//--------------------------------------------------
// A wrapper function for meow_password_rules()
//
// @param $user WP user
// @param $pass1 password
// @param $pass2 password (again)
// @return true
function meow_password_rules_check($user, &$pass1, &$pass2){
meow_password_rules($pass1, $pass2);
}
add_action('check_passwords','meow_password_rules_check', 10, 3);
//--------------------------------------------------
// Hold Password Errors
//
// @param error
// @return current
function meow_current_password_error($new=null){
static $error;
if(is_null($error))
$error = false;
if(!is_null($new))
$error = $new;
return $error;
}
//--------------------------------------------------
// Enforce additional rules against user password choices
//
// based on user settings, passwords might be required to include at
// least one number, lowercase character, uppercase character, and/or
// symbol, and hit a certain overall length.
//
// @param $pass1 password
// @param $pass2 password (again)
// @return true/false
function meow_password_rules(&$pass1, &$pass2){
//WP can handle password mismatch or empty password errors
if($pass1 !== $pass2 || !strlen($pass1))
return false;
//can't be too common
if(meow_is_common_password($pass1)){
meow_current_password_error(""" . esc_html($pass1) . "" is actually one of the most commonly used passwords on the Internet; please try something else!");
return false;
}
//needs a letter
$alpha = meow_get_option('password', 'alpha');
if($alpha === 'required' && !preg_match('/[a-z]/i', $pass1)){
meow_current_password_error(__('The password must contain at least one letter.'));
return false;
}
//needs both upper- and lowercase letters
elseif($alpha === 'required-both' && (!preg_match('/[a-z]/', $pass1) || !preg_match('/[A-Z]/', $pass1))){
meow_current_password_error(__('The password must contain at least one uppercase and one lowercase letter.'));
return false;
}
//needs a number
if(meow_get_option('password','numeric') === 'required' && !preg_match('/\d/', $pass1)){
meow_current_password_error(__('The password must contain at least one number.'));
return false;
}
//needs a symbol
if(meow_get_option('password','symbol') === 'required' && !preg_match('/[^a-z0-9]/i', $pass1)){
meow_current_password_error(__('The password must contain at least one non-alphanumeric symbol.'));
return false;
}
//check password length
$length = meow_get_option('password','length');
if(strlen($pass1) < $length){
meow_current_password_error(__("The password must be at least $length characters long."));
return false;
}
return true;
}
add_action('password_rules','meow_password_rules', 10, 2);
//--------------------------------------------------
// Report password errors
//
// @since 1.0.0
//
// @param $errors array of errors
// @return true
function meow_password_rules_error($errors){
$error = meow_current_password_error();
if(false !== $error)
$errors->add('pass', $error, array('form-field'=>'pass1'));
return true;
}
add_action('user_profile_update_errors','meow_password_rules_error', 10, 1);
add_action('password_rules_error','meow_password_rules_error', 10, 1);
//-------------------------------------------------
// Validate reset password choice
//
// Wordpress uses a different hook for password
// resets (from the forgot password form) than
// changes made in the profile. Not sure why!
//
// @since 2.0.0
//
// @param $errors
// @param $user
// @return true
function meow_validate_reset_password(){
//this action is called even if we're not posting, which is silly
//this is the test wp-login.php uses, so I'll use it too
if(!isset($_POST['pass1']))
return;
global $errors;
//WP can handle password mismatch or empty password errors
if($_POST['pass1'] !== $_POST['pass2'] || !strlen($_POST['pass1']))
return;
if(false === ($result = meow_password_rules($_POST['pass1'], $_POST['pass2'])))
$errors->add('pass', meow_current_password_error());
return;
}
add_action('validate_password_reset', 'meow_validate_reset_password');
//-------------------------------------------------
// Override wp_hash_password()
//
// use bcrypt for password hashing. note this
// removes backward compatibility with MD5 hashes
//
// @param password
// @return hash
if(version_compare(PHP_VERSION, '5.4.0', '>=') && meow_get_option('password','bcrypt')){
function wp_hash_password($password=''){
return password_hash($password, PASSWORD_BCRYPT, array('cost'=>15));
}
function wp_check_password($password='', $hash='', $user_id = ''){
$check = password_verify($password, $hash);
return apply_filters('check_password', $check, $password, $hash, $user_id);
}
}
//---------------------------------------------------------------------- end password restrictions
//---------------------------------------------------------------------
// Pruning
//---------------------------------------------------------------------
//-------------------------------------------------
// Remove Old DB Records
//
// @param n/a
// @return true/false
function meow_cron_prune(){
global $wpdb;
//pruning disabled
if(!meow_get_option('prune', 'active'))
return false;
$days = meow_get_option('prune', 'limit');
$limit = date('Y-m-d H:i:s', strtotime("-$days days", current_time('timestamp')));
//delete logins
$wpdb->query("DELETE FROM `{$wpdb->prefix}meow2_log` WHERE `date_created` < '$limit'");
}
add_action('meow_cron_prune', 'meow_cron_prune');
if(!wp_next_scheduled( 'meow_cron_prune'))
wp_schedule_event(time(), 'twicedaily', 'meow_cron_prune');
//--------------------------------------------------------------------- end pruning
//---------------------------------------------------------------------
// Activity helpers
//---------------------------------------------------------------------
//-------------------------------------------------
// Get Banned Users
//
// return some simple data about anybody banned
//
// @param n/a
// @return banned
function meow_get_banned(){
$banned = array();
global $wpdb;
$dbResult = $wpdb->get_results("SELECT `id`, `ip`, `subnet`, `date_created`, `date_expires`, `count` FROM `{$wpdb->prefix}meow2_log` WHERE `type`='ban' AND `date_expires` > '" . current_time('mysql') . "' ORDER BY `date_expires` ASC", ARRAY_A);
if(is_array($dbResult) && count($dbResult)){
foreach($dbResult AS $Row){
$banned[] = array(
'id'=>(int) $Row['id'],
'ip'=>(strlen($Row['ip']) > 1 ? $Row['ip'] : false),
'subnet'=>(strlen($Row['subnet']) > 1 ? $Row['subnet'] : false),
'date_created'=>$Row['date_created'],
'date_expires'=>$Row['date_expires'],
'count'=>(int) $Row['count'],
'remaining'=>meow_relative_date_diff($Row['date_expires'], current_time('mysql'))
);
}
}
return $banned;
}
//-------------------------------------------------
// Get User Activity
//
// @param args
// @return activity and search parameters
function meow_get_activity($args=null){
global $wpdb;
$xout = array('activity'=>array(), 'search'=>array());
$min = $wpdb->get_var("SELECT MIN(DATE(`date_created`)) FROM `{$wpdb->prefix}meow2_log`");
$max = current_time('Y-m-d');
if(is_null($min))
$min = $max;
$defaults = array(
'from'=>0,
'results'=>50,
'ip'=>'',
'username'=>'',
'type'=>'',
'date_start'=>$min,
'date_end'=>$max
);
$xout['search'] = meow_parse_args($args, $defaults);
//sanitize search values
$xout['search']['from'] = (int) $xout['search']['from'];
if($xout['search']['from'] < 0)
$xout['search']['from'] = 0;
$xout['search']['results'] = absint($xout['search']['results']);
$xout['search']['ip'] = (string) apply_filters('meow_filter_ip', $xout['search']['ip']);
$xout['search']['type'] = in_array($xout['search']['type'], array('ban','fail','success','alert')) ? $xout['search']['type'] : '';
$xout['search']['date_start'] = date('Y-m-d', strtotime($xout['search']['date_start']));
$xout['search']['date_end'] = date('Y-m-d', strtotime($xout['search']['date_end']));
$xout['search']['username'] = trim(strtolower($xout['search']['username']));
//further scrutinize the dates
foreach(array('date_start', 'date_end') AS $field){
if($xout['search'][$field] < $min)
$xout['search'][$field] = $min;
elseif($xout['search'][$field] > $max)
$xout['search'][$field] = $max;
}
if($xout['search']['date_end'] < $xout['search']['date_start'])
meow_switcheroo($xout['search']['date_end'], $xout['search']['date_start']);
//build the search
$conds = array();
$conds[] = "DATE(`date_created`) >= '{$xout['search']['date_start']}'";
$conds[] = "DATE(`date_created`) <= '{$xout['search']['date_end']}'";
if(strlen($xout['search']['ip'])){
$subnet = meow_ip_to_subnet($xout['search']['ip']);
$conds[] = "(`ip`='{$xout['search']['ip']}' OR `subnet`='$subnet')";
}
if(strlen($xout['search']['type']))
$conds[] = "`type`='{$xout['search']['type']}'";
if(strlen($xout['search']['username']) && $xout['search']['type'] !== 'ban')
$conds[] = "`username`='" . esc_sql($xout['search']['username']) . "'";
//first, add some values to our arguments
$xout['search']['total'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM `{$wpdb->prefix}meow2_log` WHERE " . implode(' AND ', $conds));
$xout['search']['before'] = $xout['search']['from'] > 0;
$xout['search']['after'] = $xout['search']['from'] + $xout['search']['results'] < $xout['search']['total'];
//find activity
if($xout['search']['total'] > $xout['search']['from']){
$dbResult = $wpdb->get_results("SELECT `ip`, `subnet`, `type`, `date_created`, `date_expires`, `count`, `username`, `pardoned` FROM `{$wpdb->prefix}meow2_log` WHERE " . implode(' AND ', $conds) . " ORDER BY `date_created` DESC, `id` DESC LIMIT {$xout['search']['from']}, {$xout['search']['results']}", ARRAY_A);
foreach($dbResult AS $Row){
$xout['activity'][] = array(
'ip'=>(strlen($Row['ip']) > 1 ? $Row['ip'] : false),
'subnet'=>(strlen($Row['subnet']) > 1 ? $Row['subnet'] : false),
'date_created'=>$Row['date_created'],
'date_expires'=>$Row['date_expires'],
'count'=>(int) $Row['count'],
'type'=>$Row['type'],
'username'=>$Row['username'],
'usernameExists'=>meow_username_exists($Row['username']),
'pardoned'=>intval($Row['pardoned']) === 1
);
}
}
return $xout;
}
//--------------------------------------------------------------------- end activity
?>