first commit

This commit is contained in:
Rachit Bhargava
2023-07-21 17:12:10 -04:00
parent d0fe47dde4
commit 5d0f0734d8
14003 changed files with 2829464 additions and 0 deletions

View File

@@ -0,0 +1,558 @@
<?php
namespace WordfenceLS;
use WordfenceLS\Crypto\Model_JWT;
use WordfenceLS\Crypto\Model_Symmetric;
class Controller_AJAX {
const MAX_USERS_TO_NOTIFY = 100;
protected $_actions = null; //Populated on init
/**
* Returns the singleton Controller_AJAX.
*
* @return Controller_AJAX
*/
public static function shared() {
static $_shared = null;
if ($_shared === null) {
$_shared = new Controller_AJAX();
}
return $_shared;
}
public function init() {
$this->_actions = array(
'authenticate' => array(
'handler' => array($this, '_ajax_authenticate_callback'),
'nopriv' => true,
'nonce' => false,
'permissions' => array(), //Format is 'permission' => 'error message'
'required_parameters' => array(),
),
'register_support' => array(
'handler' => array($this, '_ajax_register_support_callback'),
'nopriv' => true,
'nonce' => false,
'permissions' => array(),
'required_parameters' => array('wfls-message-nonce', 'wfls-message'),
),
'activate' => array(
'handler' => array($this, '_ajax_activate_callback'),
'permissions' => array(),
'required_parameters' => array('nonce', 'secret', 'recovery', 'code', 'user'),
),
'deactivate' => array(
'handler' => array($this, '_ajax_deactivate_callback'),
'permissions' => array(),
'required_parameters' => array('nonce', 'user'),
),
'regenerate' => array(
'handler' => array($this, '_ajax_regenerate_callback'),
'permissions' => array(),
'required_parameters' => array('nonce', 'user'),
),
'save_options' => array(
'handler' => array($this, '_ajax_save_options_callback'),
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to change options.', 'wordfence-2fa')),
'required_parameters' => array('nonce', 'changes'),
),
'send_grace_period_notification' => array(
'handler' => array($this, '_ajax_send_grace_period_notification_callback'),
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to send notifications.', 'wordfence-2fa')),
'required_parameters' => array('nonce', 'role', 'url'),
),
'update_ip_preview' => array(
'handler' => array($this, '_ajax_update_ip_preview_callback'),
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to change options.', 'wordfence-2fa')),
'required_parameters' => array('nonce', 'ip_source', 'ip_source_trusted_proxies'),
),
'dismiss_notice' => array(
'handler' => array($this, '_ajax_dismiss_notice_callback'),
'permissions' => array(),
'required_parameters' => array('nonce', 'id'),
),
'reset_recaptcha_stats' => array(
'handler' => array($this, '_ajax_reset_recaptcha_stats_callback'),
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to reset reCAPTCHA statistics.', 'wordfence-2fa')),
'required_parameters' => array('nonce'),
),
'reset_2fa_grace_period' => array (
'handler' => array($this, '_ajax_reset_2fa_grace_period_callback'),
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to reset the 2FA grace period.', 'wordfence-2fa')),
'required_parameters' => array('nonce', 'user_id')
),
'revoke_2fa_grace_period' => array (
'handler' => array($this, '_ajax_revoke_2fa_grace_period_callback'),
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to revoke the 2FA grace period.', 'wordfence-2fa')),
'required_parameters' => array('nonce', 'user_id')
),
'reset_ntp_failure_count' => array(
'handler' => array($this, '_ajax_reset_ntp_failure_count_callback'),
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to reset the NTP failure count.', 'wordfence-2fa')),
'required_parameters' => array(),
),
'disable_ntp' => array(
'handler' => array($this, '_ajax_disable_ntp_callback'),
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to disable NTP.', 'wordfence-2fa')),
'required_parameters' => array(),
),
'dismiss_persistent_notice' => array(
'handler' => array($this, '_ajax_dismiss_persistent_notice_callback'),
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to dismiss this notice.', 'wordfence-2fa')),
'required_parameters' => array('nonce', 'notice_id')
)
);
$this->_init_actions();
}
public function _init_actions() {
foreach ($this->_actions as $action => $parameters) {
if (isset($parameters['nopriv']) && $parameters['nopriv']) {
add_action('wp_ajax_nopriv_wordfence_ls_' . $action, array($this, '_ajax_handler'));
}
add_action('wp_ajax_wordfence_ls_' . $action, array($this, '_ajax_handler'));
}
}
/**
* This is a convenience function for sending a JSON response and ensuring that execution stops after sending
* since wp_die() can be interrupted.
*
* @param $response
* @param int|null $status_code
*/
public static function send_json($response, $status_code = null) {
wp_send_json($response, $status_code);
die();
}
public function _ajax_handler() {
$action = (isset($_POST['action']) && is_string($_POST['action']) && $_POST['action']) ? $_POST['action'] : $_GET['action'];
if (preg_match('~wordfence_ls_([a-zA-Z_0-9]+)$~', $action, $matches)) {
$action = $matches[1];
if (!isset($this->_actions[$action])) {
self::send_json(array('error' => esc_html__('An unknown action was provided.', 'wordfence-2fa')));
}
$parameters = $this->_actions[$action];
if (!empty($parameters['required_parameters'])) {
foreach ($parameters['required_parameters'] as $k) {
if (!isset($_POST[$k])) {
self::send_json(array('error' => esc_html__('An expected parameter was not provided.', 'wordfence-2fa')));
}
}
}
if (!isset($parameters['nonce']) || $parameters['nonce']) {
$nonce = (isset($_POST['nonce']) && is_string($_POST['nonce']) && $_POST['nonce']) ? $_POST['nonce'] : $_GET['nonce'];
if (!is_string($nonce) || !wp_verify_nonce($nonce, 'wp-ajax')) {
self::send_json(array('error' => esc_html__('Your browser sent an invalid security token. Please try reloading this page.', 'wordfence-2fa'), 'tokenInvalid' => 1));
}
}
if (!empty($parameters['permissions'])) {
$user = wp_get_current_user();
foreach ($parameters['permissions'] as $permission => $error) {
if (!user_can($user, $permission)) {
self::send_json(array('error' => $error));
}
}
}
call_user_func($parameters['handler']);
}
}
public function _ajax_authenticate_callback() {
$credentialKeys = array(
'log' => 'pwd',
'username' => 'password'
);
$username = null;
$password = null;
foreach ($credentialKeys as $usernameKey => $passwordKey) {
if (array_key_exists($usernameKey, $_POST) && array_key_exists($passwordKey, $_POST) && is_string($_POST[$usernameKey]) && is_string($_POST[$passwordKey])) {
$username = $_POST[$usernameKey];
$password = $_POST[$passwordKey];
break;
}
}
if (empty($username) || empty($password)) {
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: A username and password must be provided. <a href="%s" title="Password Lost and Found">Lost your password</a>?', 'wordfence-2fa'), wp_lostpassword_url()), array('strong'=>array(), 'a'=>array('href'=>array(), 'title'=>array())))));
}
$legacy2FAActive = Controller_WordfenceLS::shared()->legacy_2fa_active();
if ($legacy2FAActive) { //Legacy 2FA is active, pass it on to the authenticate filter
self::send_json(array('login' => 1));
}
do_action_ref_array('wp_authenticate', array(&$username, &$password));
define('WORDFENCE_LS_AUTHENTICATION_CHECK', true); //Prevents our auth filter from recursing
$user = wp_authenticate($username, $password);
if (is_object($user) && ($user instanceof \WP_User)) {
if (!Controller_Users::shared()->has_2fa_active($user) || Controller_Whitelist::shared()->is_whitelisted(Model_Request::current()->ip()) || Controller_Users::shared()->has_remembered_2fa($user) || defined('WORDFENCE_LS_COMBINED_IS_VALID')) { //Not enabled for this user, is whitelisted, has a valid remembered cookie, or has already provided a 2FA code via the password field pass the credentials on to the normal login flow
self::send_json(array('login' => 1));
}
self::send_json(array('login' => 1, 'two_factor_required' => true));
}
else if (is_wp_error($user)) {
$errors = array();
$messages = array();
$reset = false;
foreach ($user->get_error_codes() as $code) {
if ($code == 'invalid_username' || $code == 'invalid_email' || $code == 'incorrect_password' || $code == 'authentication_failed') {
$errors[] = wp_kses(sprintf(__('<strong>ERROR</strong>: The username or password you entered is incorrect. <a href="%s" title="Password Lost and Found">Lost your password</a>?', 'wordfence-2fa'), wp_lostpassword_url()), array('strong'=>array(), 'a'=>array('href'=>array(), 'title'=>array())));
}
else {
if ($code == 'wfls_twofactor_invalid') {
$reset = true;
}
$severity = $user->get_error_data($code);
foreach ($user->get_error_messages($code) as $error_message) {
if ($severity == 'message') {
$messages[] = $error_message;
}
else {
$errors[] = $error_message;
}
}
}
}
if (!empty($errors)) {
$errors = implode('<br>', $errors);
$errors = apply_filters('login_errors', $errors);
self::send_json(array('error' => $errors, 'reset' => $reset));
}
if (!empty($messages)) {
$messages = implode('<br>', $messages);
$messages = apply_filters('login_errors', $messages);
self::send_json(array('message' => $messages, 'reset' => $reset));
}
}
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: The username or password you entered is incorrect. <a href="%s" title="Password Lost and Found">Lost your password</a>?', 'wordfence-2fa'), wp_lostpassword_url()), array('strong'=>array(), 'a'=>array('href'=>array(), 'title'=>array())))));
}
public function _ajax_register_support_callback() {
$email = null;
if (array_key_exists('email', $_POST) && is_string($_POST['email'])) {
$email = $_POST['email'];
}
else if (array_key_exists('user_email', $_POST) && is_string($_POST['user_email'])) {
$email = $_POST['user_email'];
}
if (
$email === null ||
!isset($_POST['wfls-message']) || !is_string($_POST['wfls-message']) ||
!isset($_POST['wfls-message-nonce']) || !is_string($_POST['wfls-message-nonce'])) {
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: Unable to send message. Please refresh the page and try again.', 'wordfence-2fa')), array('strong'=>array()))));
}
$email = sanitize_email($email);
$login = '';
if (array_key_exists('user_login', $_POST) && is_string($_POST['user_login']))
$login = sanitize_user($_POST['user_login']);
$message = strip_tags($_POST['wfls-message']);
$nonce = $_POST['wfls-message-nonce'];
if ((isset($_POST['user_login']) && empty($login)) || empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL) || empty($message)) {
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: Unable to send message. Please refresh the page and try again.', 'wordfence-2fa')), array('strong'=>array()))));
}
$jwt = Model_JWT::decode_jwt($_POST['wfls-message-nonce']);
if ($jwt && isset($jwt->payload['ip']) && isset($jwt->payload['score'])) {
$decryptedIP = Model_Symmetric::decrypt($jwt->payload['ip']);
$decryptedScore = Model_Symmetric::decrypt($jwt->payload['score']);
if ($decryptedIP === false || $decryptedScore === false || Model_IP::inet_pton($decryptedIP) !== Model_IP::inet_pton(Model_Request::current()->ip())) { //JWT IP and the current request's IP don't match, refuse the message
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: Unable to send message. Please refresh the page and try again.', 'wordfence-2fa')), array('strong'=>array()))));
}
$identifier = bin2hex(Model_IP::inet_pton($decryptedIP));
$tokenBucket = new Model_TokenBucket('rate:' . $identifier, 2, 1 / (6 * Model_TokenBucket::HOUR)); //Maximum of two requests, refilling at a rate of one per six hours
if (!$tokenBucket->consume(1)) {
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: Unable to send message. You have exceeded the maximum number of messages that may be sent at this time. Please try again later.', 'wordfence-2fa')), array('strong'=>array()))));
}
$email = array(
'to' => get_site_option('admin_email'),
'subject' => __('Blocked User Registration Contact Form', 'wordfence-2fa'),
'body' => sprintf(__("A visitor blocked from registration sent the following message.\n\n----------------------------------------\n\nIP: %s\nUsername: %s\nEmail: %s\nreCAPTCHA Score: %f\n\n----------------------------------------\n\n%s", 'wordfence-2fa'), $decryptedIP, $login, $email, $decryptedScore, $message),
'headers' => '',
);
$success = wp_mail($email['to'], $email['subject'], $email['body'], $email['headers']);
if ($success) {
self::send_json(array('message' => wp_kses(sprintf(__('<strong>MESSAGE SENT</strong>: Your message was sent to the site owner.', 'wordfence-2fa')), array('strong'=>array()))));
}
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: An error occurred while sending the message. Please try again.', 'wordfence-2fa')), array('strong'=>array()))));
}
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: Unable to send message. Please refresh the page and try again.', 'wordfence-2fa')), array('strong'=>array()))));
}
public function _ajax_activate_callback() {
$userID = (int) @$_POST['user'];
$user = wp_get_current_user();
if ($user->ID != $userID) {
if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_OTHERS)) {
self::send_json(array('error' => esc_html__('You do not have permission to activate the given user.', 'wordfence-2fa')));
}
else {
$user = new \WP_User($userID);
if (!$user->exists()) {
self::send_json(array('error' => esc_html__('The given user does not exist.', 'wordfence-2fa')));
}
}
}
else if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_SELF)) {
self::send_json(array('error' => esc_html__('You do not have permission to activate 2FA.', 'wordfence-2fa')));
}
if (Controller_Users::shared()->has_2fa_active($user)) {
self::send_json(array('error' => esc_html__('The given user already has two-factor authentication active.', 'wordfence-2fa')));
}
$matches = (isset($_POST['secret']) && isset($_POST['code']) && is_string($_POST['secret']) && is_string($_POST['code']) && Controller_TOTP::shared()->check_code($_POST['secret'], $_POST['code']));
if ($matches === false) {
self::send_json(array('error' => esc_html__('The code provided does not match the expected value. Please verify that the time on your authenticator device is correct and that this server\'s time is correct.', 'wordfence-2fa')));
}
Controller_TOTP::shared()->activate_2fa($user, $_POST['secret'], $_POST['recovery'], $matches);
Controller_Notices::shared()->remove_notice(false, 'wfls-will-be-required', $user);
self::send_json(array('activated' => 1, 'text' => sprintf(count($_POST['recovery']) == 1 ? esc_html__('%d unused recovery code remains. You may generate a new set by clicking below.', 'wordfence-2fa') : esc_html__('%d unused recovery codes remain. You may generate a new set by clicking below.', 'wordfence-2fa'), count($_POST['recovery']))));
}
public function _ajax_deactivate_callback() {
$userID = (int) @$_POST['user'];
$user = wp_get_current_user();
if ($user->ID != $userID) {
if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_OTHERS)) {
self::send_json(array('error' => esc_html__('You do not have permission to deactivate the given user.', 'wordfence-2fa')));
}
else {
$user = new \WP_User($userID);
if (!$user->exists()) {
self::send_json(array('error' => esc_html__('The user does not exist.', 'wordfence-2fa')));
}
}
}
else if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_SELF)) {
self::send_json(array('error' => esc_html__('You do not have permission to deactivate 2FA.', 'wordfence-2fa')));
}
if (!Controller_Users::shared()->has_2fa_active($user)) {
self::send_json(array('error' => esc_html__('The user specified does not have two-factor authentication active.', 'wordfence-2fa')));
}
Controller_Users::shared()->deactivate_2fa($user);
self::send_json(array('deactivated' => 1));
}
public function _ajax_regenerate_callback() {
$userID = (int) @$_POST['user'];
$user = wp_get_current_user();
if ($user->ID != $userID) {
if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_OTHERS)) {
self::send_json(array('error' => esc_html__('You do not have permission to generate new recovery codes for the given user.', 'wordfence-2fa')));
}
else {
$user = new \WP_User($userID);
if (!$user->exists()) {
self::send_json(array('error' => esc_html__('The user does not exist.', 'wordfence-2fa')));
}
}
}
else if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_SELF)) {
self::send_json(array('error' => esc_html__('You do not have permission to generate new recovery codes.', 'wordfence-2fa')));
}
if (!Controller_Users::shared()->has_2fa_active($user)) {
self::send_json(array('error' => esc_html__('The user specified does not have two-factor authentication active.', 'wordfence-2fa')));
}
$codes = Controller_Users::shared()->regenerate_recovery_codes($user);
self::send_json(array('regenerated' => 1, 'recovery' => array_map(function($r) { return implode(' ', str_split(bin2hex($r), 4)); }, $codes), 'text' => sprintf(count($codes) == 1 ? esc_html__('%d unused recovery code remains. You may generate a new set by clicking below.', 'wordfence-2fa') : esc_html__('%d unused recovery codes remain. You may generate a new set by clicking below.', 'wordfence-2fa'), count($codes))));
}
public function _ajax_save_options_callback() {
if (!empty($_POST['changes']) && is_string($_POST['changes']) && is_array($changes = json_decode(stripslashes($_POST['changes']), true))) {
try {
$errors = Controller_Settings::shared()->validate_multiple($changes);
if ($errors !== true) {
if (count($errors) == 1) {
$e = array_shift($errors);
self::send_json(array('error' => esc_html(sprintf(__('An error occurred while saving the configuration: %s', 'wordfence-2fa'), $e))));
}
else if (count($errors) > 1) {
$compoundMessage = array();
foreach ($errors as $e) {
$compoundMessage[] = esc_html($e);
}
self::send_json(array(
'error' => wp_kses(sprintf(__('Errors occurred while saving the configuration: %s', 'wordfence-2fa'), '<ul><li>' . implode('</li><li>', $compoundMessage) . '</li></ul>'), array('ul'=>array(), 'li'=>array())),
'html' => true,
));
}
self::send_json(array(
'error' => esc_html__('Errors occurred while saving the configuration.', 'wordfence-2fa'),
));
}
Controller_Settings::shared()->set_multiple($changes);
if (array_key_exists(Controller_Settings::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION, $changes) || array_key_exists(Controller_Settings::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION, $changes))
Controller_WordfenceLS::shared()->refresh_rewrite_rules();
$response = array('success' => true);
return self::send_json($response);
}
catch (\Exception $e) {
self::send_json(array(
'error' => $e->getMessage(),
));
}
}
self::send_json(array(
'error' => esc_html__('No configuration changes were provided to save.', 'wordfence-2fa'),
));
}
public function _ajax_send_grace_period_notification_callback() {
$notifyAll = isset($_POST['notify_all']);
$users = Controller_Users::shared()->get_users_by_role($_POST['role'], $notifyAll ? null: self::MAX_USERS_TO_NOTIFY + 1);
$url = $_POST['url'];
if (!empty($url)) {
$url = get_site_url(null, $url);
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
self::send_json(array('error' => esc_html__('The specified URL is invalid.', 'wordfence-2fa')));
}
}
$userCount = count($users);
if (!$notifyAll && $userCount > self::MAX_USERS_TO_NOTIFY)
self::send_json(array('error' => esc_html(sprintf(__('More than %d users exist for the selected role. This notification is not designed to handle large groups of users. In such instances, using a different solution for notifying users of upcoming 2FA requirements is recommended.', 'wordfence-2fa'), self::MAX_USERS_TO_NOTIFY)), 'limit_exceeded' => true));
$sent = 0;
foreach ($users as $user) {
Controller_Users::shared()->requires_2fa($user, $inGracePeriod, $requiredAt);
if ($inGracePeriod && !Controller_Users::shared()->has_2fa_active($user)) {
$subject = sprintf(__('2FA will soon be required on %s', 'wordfence-2fa'), home_url());
$requiredDate = Controller_Time::format_local_time('F j, Y g:i A', $requiredAt);
if (empty($url)) {
$userUrl = (is_multisite() && is_super_admin($user->ID)) ? network_admin_url('admin.php?page=WFLS') : admin_url('admin.php?page=WFLS');
}
else {
$userUrl = $url;
}
$message = sprintf(
__("<html><body><p>You do not currently have two-factor authentication active on your account, which will be required beginning %s.</p><p><a href=\"%s\">Configure 2FA</a></p></body></html>", 'wordfence-2fa'),
$requiredDate,
htmlentities($userUrl)
);
wp_mail($user->user_email, $subject, $message, array('Content-Type: text/html'));
$sent++;
}
}
if ($userCount == 0) {
self::send_json(array('error' => esc_html__('No users currently exist with the selected role.', 'wordfence-2fa')));
}
else if ($sent == 0) {
self::send_json(array('confirmation' => esc_html__('All users with the selected role already have two-factor authentication activated or have been locked out.', 'wordfence-2fa')));
}
else if ($sent == 1) {
self::send_json(array('confirmation' => esc_html(sprintf(__('A reminder to activate two-factor authentication was sent to %d user.', 'wordfence-2fa'), $sent))));
}
self::send_json(array('confirmation' => esc_html(sprintf(__('A reminder to activate two-factor authentication was sent to %d users.', 'wordfence-2fa'), $sent))));
}
public function _ajax_update_ip_preview_callback() {
$source = $_POST['ip_source'];
$raw_proxies = $_POST['ip_source_trusted_proxies'];
if (!is_string($source) || !is_string($raw_proxies)) {
die();
}
$valid = array();
$invalid = array();
$test = preg_split('/[\r\n,]+/', $raw_proxies);
foreach ($test as $value) {
if (strlen($value) > 0) {
if (Model_IP::is_valid_ip($value) || Model_IP::is_valid_cidr_range($value)) {
$valid[] = $value;
}
else {
$invalid[] = $value;
}
}
}
$trusted_proxies = $valid;
$preview = Model_Request::current()->detected_ip_preview($source, $trusted_proxies);
$ip = Model_Request::current()->ip_for_field($source, $trusted_proxies);
self::send_json(array('ip' => $ip[0], 'preview' => $preview));
}
public function _ajax_dismiss_notice_callback() {
Controller_Notices::shared()->remove_notice($_POST['id'], false, wp_get_current_user());
}
public function _ajax_reset_recaptcha_stats_callback() {
Controller_Settings::shared()->set_array(Controller_Settings::OPTION_CAPTCHA_STATS, array('counts' => array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 'avg' => 0));
$response = array('success' => true);
self::send_json($response);
}
public function _ajax_reset_2fa_grace_period_callback() {
$userId = (int) $_POST['user_id'];
$gracePeriodOverride = array_key_exists('grace_period_override', $_POST) ? (int) $_POST['grace_period_override'] : null;
$user = get_userdata($userId);
if ($user === false)
self::send_json(array('error' => esc_html__('Invalid user specified', 'wordfence-2fa')));
if ($gracePeriodOverride < 0 || $gracePeriodOverride > Controller_Settings::MAX_REQUIRE_2FA_USER_GRACE_PERIOD)
self::send_json(array('error' => esc_html__('Invalid grace period override', 'wordfence-2fa')));
$gracePeriodAllowed = Controller_Users::shared()->get_grace_period_allowed_flag($userId);
if (!$gracePeriodAllowed)
Controller_Users::shared()->allow_grace_period($userId);
if (!Controller_Users::shared()->reset_2fa_grace_period($user, $gracePeriodOverride))
self::send_json(array('error' => esc_html__('Failed to reset grace period', 'wordfence-2fa')));
self::send_json(array('success' => true));
}
public function _ajax_revoke_2fa_grace_period_callback() {
$user = get_userdata((int) $_POST['user_id']);
if ($user === false)
self::send_json(array('error' => esc_html__('Invalid user specified', 'wordfence-2fa')));
Controller_Users::shared()->revoke_grace_period($user);
self::send_json(array('success' => true));
}
public function _ajax_reset_ntp_failure_count_callback() {
Controller_Settings::shared()->reset_ntp_failure_count();
}
public function _ajax_disable_ntp_callback() {
Controller_Settings::shared()->disable_ntp_cron();
}
public function _ajax_dismiss_persistent_notice_callback() {
$userId = get_current_user_id();
$noticeId = $_POST['notice_id'];
if ($userId !== 0 && Controller_Notices::shared()->dismiss_persistent_notice($userId, $noticeId))
self::send_json(array('success' => true));
self::send_json(array(
'error' => esc_html__('Unable to dismiss notice', 'wordfence-2fa')
));
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace WordfenceLS;
class Controller_CAPTCHA {
const RESPONSE_MODE_ALLOW = 'allow';
const RESPONSE_MODE_REQUIRE_VERIFICATION = 'verify';
const RECAPTCHA_ENDPOINT = 'https://www.google.com/recaptcha/api/siteverify';
/**
* Returns the singleton Controller_CAPTCHA.
*
* @return Controller_CAPTCHA
*/
public static function shared() {
static $_shared = null;
if ($_shared === null) {
$_shared = new Controller_CAPTCHA();
}
return $_shared;
}
/**
* Returns whether or not the authentication CAPTCHA is enabled.
*
* @return bool
*/
public function enabled() {
$key = $this->site_key();
$secret = $this->_secret();
return Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_ENABLE_AUTH_CAPTCHA) && !empty($key) && !empty($secret);
}
/**
* Returns the public reCAPTCHA key if set.
*
* @return string|bool
*/
public function site_key() {
return Controller_Settings::shared()->get(Controller_Settings::OPTION_RECAPTCHA_SITE_KEY);
}
/**
* Returns the private reCAPTCHA secret if set.
*
* @return string|bool
*/
protected function _secret() {
return Controller_Settings::shared()->get(Controller_Settings::OPTION_RECAPTCHA_SECRET);
}
/**
* Returns the bot/human threshold for comparing the score against, defaulting to 0.5.
*
* @return float
*/
public function threshold() {
return Controller_Settings::shared()->get_float(Controller_Settings::OPTION_RECAPTCHA_THRESHOLD, 0.5);
}
/**
* Determine whether or not test mode for reCAPTCHA is enabled
*
* @return bool
*/
public function test_mode() {
return Controller_Settings::shared()->get_bool(\WordfenceLS\Controller_Settings::OPTION_CAPTCHA_TEST_MODE);
}
/**
* Queries the reCAPTCHA endpoint with the given token, verifies the action matches, and returns the corresponding
* score. If validation fails, false is returned. Any other failure (e.g., mangled response or connection dropped) returns 0.0.
*
* @param string $token
* @param string $action
* @param int $timeout
* @return float|false
*/
public function score($token, $action = 'login', $timeout = 10) {
try {
$payload = array(
'secret' => $this->_secret(),
'response' => $token,
'remoteip' => Model_Request::current()->ip(),
);
$response = wp_remote_post(self::RECAPTCHA_ENDPOINT,
array(
'body' => $payload,
'headers' => array(
'Referer' => false,
),
'timeout' => $timeout,
'blocking' => true,
));
if (!is_wp_error($response)) {
$jsonResponse = wp_remote_retrieve_body($response);
$decoded = @json_decode($jsonResponse, true);
if (is_array($decoded) && isset($decoded['success']) && isset($decoded['score']) && isset($decoded['action'])) {
if ($decoded['success'] && $decoded['action'] == $action) {
return (float) $decoded['score'];
}
return false;
}
}
}
catch (\Exception $e) {
//Fall through
}
return 0.0;
}
/**
* Returns true if the score is >= the threshold to be considered a human request.
*
* @param float $score
* @return bool
*/
public function is_human($score) {
if ($this->test_mode()) {
return true;
}
$threshold = $this->threshold();
return ($score >= $threshold || abs($score - $threshold) < 0.0001);
}
/**
* Check if the current request is an XML RPC request
* @return bool
*/
private static function is_xml_rpc() {
return defined('XMLRPC_REQUEST') && XMLRPC_REQUEST;
}
/**
* Check if captcha is required for the current request
* @return bool
*/
public function is_captcha_required() {
$required = $this->enabled() && !self::is_xml_rpc();
return apply_filters('wordfence_ls_require_captcha', $required);
}
/**
* Get the captcha token provided with the current request
* @param string $key if specified, override the default token parameter
* @return string|null the captcha token, if present, null otherwise
*/
public function get_token($key = 'wfls-captcha-token') {
return (isset($_POST[$key]) && is_string($_POST[$key]) && !empty($_POST[$key]) ? $_POST[$key] : null);
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace WordfenceLS;
use RuntimeException;
class Controller_DB {
const TABLE_2FA_SECRETS = 'wfls_2fa_secrets';
const TABLE_SETTINGS = 'wfls_settings';
const TABLE_ROLE_COUNTS = 'wfls_role_counts';
const TABLE_ROLE_COUNTS_TEMPORARY = 'wfls_role_counts_temporary';
const SCHEMA_VERSION = 2;
/**
* Returns the singleton Controller_DB.
*
* @return Controller_DB
*/
public static function shared() {
static $_shared = null;
if ($_shared === null) {
$_shared = new Controller_DB();
}
return $_shared;
}
/**
* Returns the table prefix for the main site on multisites and the site itself on single site installations.
*
* @return string
*/
public static function network_prefix() {
global $wpdb;
return $wpdb->base_prefix;
}
/**
* Returns the table with the site (single site installations) or network (multisite) prefix added.
*
* @param string $table
* @return string
*/
public static function network_table($table) {
return self::network_prefix() . $table;
}
public function __get($key) {
switch ($key) {
case 'secrets':
return self::network_table(self::TABLE_2FA_SECRETS);
case 'settings':
return self::network_table(self::TABLE_SETTINGS);
case 'role_counts':
return self::network_table(self::TABLE_ROLE_COUNTS);
case 'role_counts_temporary':
return self::network_table(self::TABLE_ROLE_COUNTS_TEMPORARY);
}
throw new \OutOfBoundsException('Unknown key: ' . $key);
}
public function install() {
$this->_create_schema();
global $wpdb;
$table = $this->secrets;
$wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `vtime` = LEAST(`vtime`, %d)", Controller_Time::time()));
}
public function uninstall() {
$tables = array(self::TABLE_2FA_SECRETS, self::TABLE_SETTINGS, self::TABLE_ROLE_COUNTS);
foreach ($tables as $table) {
global $wpdb;
$wpdb->query('DROP TABLE IF EXISTS `' . self::network_table($table) . '`');
}
}
private function create_table($name, $definition, $temporary = false) {
global $wpdb;
if (is_array($definition)) {
foreach ($definition as $attempt) {
if ($this->create_table($name, $attempt, $temporary))
return true;
}
return false;
}
else {
return $wpdb->query('CREATE ' . ($temporary ? 'TEMPORARY ' : '') . 'TABLE IF NOT EXISTS `' . self::network_table($name) . '` ' . $definition);
}
}
private function create_temporary_table($name, $definition) {
if (Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_DISABLE_TEMPORARY_TABLES))
return false;
if ($this->create_table($name, $definition, true))
return true;
Controller_Settings::shared()->set(Controller_Settings::OPTION_DISABLE_TEMPORARY_TABLES, true);
return false;
}
private function get_role_counts_table_definition($engine = null) {
$engineClause = $engine === null ? '' : "ENGINE={$engine}";
return <<<SQL
(
serialized_roles VARBINARY(255) NOT NULL,
two_factor_inactive TINYINT(1) NOT NULL,
user_count BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (serialized_roles, two_factor_inactive)
) {$engineClause};
SQL;
}
private function get_role_counts_table_definition_options() {
return array(
$this->get_role_counts_table_definition('MEMORY'),
$this->get_role_counts_table_definition('MyISAM'),
$this->get_role_counts_table_definition()
);
}
protected function _create_schema() {
$tables = array(
self::TABLE_2FA_SECRETS => '(
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) unsigned NOT NULL,
`secret` tinyblob NOT NULL,
`recovery` blob NOT NULL,
`ctime` int(10) unsigned NOT NULL,
`vtime` int(10) unsigned NOT NULL,
`mode` enum(\'authenticator\') NOT NULL DEFAULT \'authenticator\',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;',
self::TABLE_SETTINGS => '(
`name` varchar(191) NOT NULL DEFAULT \'\',
`value` longblob,
`autoload` enum(\'no\',\'yes\') NOT NULL DEFAULT \'yes\',
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;',
self::TABLE_ROLE_COUNTS => $this->get_role_counts_table_definition_options()
);
foreach ($tables as $table => $def) {
$this->create_table($table, $def);
}
Controller_Settings::shared()->set(Controller_Settings::OPTION_SCHEMA_VERSION, self::SCHEMA_VERSION);
}
public function require_schema_version($version) {
$current = Controller_Settings::shared()->get_int(Controller_Settings::OPTION_SCHEMA_VERSION);
if ($current < $version) {
$this->install();
}
}
public function query($query) {
global $wpdb;
if ($wpdb->query($query) === false)
throw new RuntimeException("Failed to execute query: {$query}");
}
public function get_wpdb() {
global $wpdb;
return $wpdb;
}
public function create_temporary_role_counts_table() {
return $this->create_temporary_table(self::TABLE_ROLE_COUNTS_TEMPORARY, $this->get_role_counts_table_definition_options());
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace WordfenceLS;
use WordfenceLS\Text\Model_HTML;
class Controller_Notices {
const USER_META_KEY = 'wfls_notices';
const PERSISTENT_NOTICE_DISMISS_PREFIX = 'wfls-dismiss-';
const PERSISTENT_NOTICE_WOOCOMMERCE_INTEGRATION = 'wfls-woocommerce-integration-notice';
/**
* Returns the singleton Controller_Notices.
*
* @return Controller_Notices
*/
public static function shared() {
static $_shared = null;
if ($_shared === null) {
$_shared = new Controller_Notices();
}
return $_shared;
}
private $persistentNotices = array();
/**
* Adds an admin notice to the display queue. If $user is provided, it will show only for that user, otherwise it
* will show for all administrators.
*
* @param string $severity
* @param string|Model_HTML $message
* @param bool|string $category If not false, notices with the same category will be removed prior to adding this one.
* @param bool|\WP_User $user If not false, the user that the notice should show for.
*/
public function add_notice($severity, $message, $category = false, $user = false) {
$notices = $this->_notices($user);
foreach ($notices as $id => $n) {
if ($category !== false && isset($n['category']) && $n['category'] == $category) { //Same category overwrites previous entry
unset($notices[$id]);
}
}
$id = Model_Crypto::uuid();
$notices[$id] = array(
'severity' => $severity,
'messageHTML' => Model_HTML::esc_html($message),
);
if ($category !== false) {
$notices[$id]['category'] = $category;
}
$this->_save_notices($notices, $user);
}
/**
* Removes a notice using one of two possible search methods:
*
* 1. If $id matches. $category is ignored but only notices for $user are checked.
* 2. If $category matches. Only notices for $user are checked.
*
* @param bool|int $id
* @param bool|string $category
* @param bool|\WP_User $user
*/
public function remove_notice($id = false, $category = false, $user = false) {
if ($id === false && $category === false) {
return;
}
else if ($id !== false) {
$category = false;
}
$notices = $this->_notices($user);
foreach ($notices as $nid => $n) {
if ($id == $nid) { //ID match
unset($notices[$nid]);
break;
}
else if ($id !== false) {
continue;
}
if ($category !== false && isset($n['category']) && $category == $n['category']) { //Category match
unset($notices[$nid]);
}
}
$this->_save_notices($notices, $user);
}
/**
* Returns whether or not a notice exists for the given user.
*
* @param bool|\WP_User $user
* @return bool
*/
public function has_notice($user) {
$notices = $this->_notices($user);
return !!count($notices) || $this->has_persistent_notices();
}
/**
* Enqueues a user's notices. For administrators this also includes global notices.
*
* @return bool Whether any notices were enqueued.
*/
public function enqueue_notices() {
$user = wp_get_current_user();
if ($user->ID == 0) {
return false;
}
$added = false;
$notices = array();
if (Controller_Permissions::shared()->can_manage_settings($user)) {
$globalNotices = $this->_notices(false);
$notices = array_merge($notices, $globalNotices);
}
$userNotices = $this->_notices($user);
$notices = array_merge($notices, $userNotices);
foreach ($notices as $nid => $n) {
$notice = new Model_Notice($nid, $n['severity'], $n['messageHTML'], $n['category']);
if (is_multisite()) {
add_action('network_admin_notices', array($notice, 'display_notice'));
}
else {
add_action('admin_notices', array($notice, 'display_notice'));
}
$added = true;
}
return $added;
}
/**
* Utility
*/
/**
* Returns the notices for a user if provided, otherwise the global notices.
*
* @param bool|\WP_User $user
* @return array
*/
protected function _notices($user) {
if ($user instanceof \WP_User) {
$notices = get_user_meta($user->ID, self::USER_META_KEY, true);
return array_filter((array) $notices);
}
return Controller_Settings::shared()->get_array(Controller_Settings::OPTION_GLOBAL_NOTICES);
}
/**
* Saves the notices.
*
* @param array $notices
* @param bool|\WP_User $user
*/
protected function _save_notices($notices, $user) {
if ($user instanceof \WP_User) {
update_user_meta($user->ID, self::USER_META_KEY, $notices);
return;
}
Controller_Settings::shared()->set_array(Controller_Settings::OPTION_GLOBAL_NOTICES, $notices, true);
}
public function get_persistent_notice_ids() {
return array(
self::PERSISTENT_NOTICE_WOOCOMMERCE_INTEGRATION
);
}
private static function get_persistent_notice_dismiss_key($noticeId) {
return self::PERSISTENT_NOTICE_DISMISS_PREFIX . $noticeId;
}
public function register_persistent_notice($noticeId) {
$this->persistentNotices[] = $noticeId;
}
public function has_persistent_notices() {
return count($this->persistentNotices) > 0;
}
public function dismiss_persistent_notice($userId, $noticeId) {
if (!in_array($noticeId, $this->get_persistent_notice_ids(), true))
return false;
update_user_option($userId, self::get_persistent_notice_dismiss_key($noticeId), true, true);
return true;
}
public function is_persistent_notice_dismissed($userId, $noticeId) {
return (bool) get_user_option(self::get_persistent_notice_dismiss_key($noticeId), $userId);
}
}

View File

@@ -0,0 +1,293 @@
<?php
namespace WordfenceLS;
class Controller_Permissions {
const CAP_ACTIVATE_2FA_SELF = 'wf2fa_activate_2fa_self'; //Activate 2FA on its own user account
const CAP_ACTIVATE_2FA_OTHERS = 'wf2fa_activate_2fa_others'; //Activate 2FA on user accounts other than its own
const CAP_MANAGE_SETTINGS = 'wf2fa_manage_settings'; //Edit settings for the plugin
const SITE_BATCH_SIZE = 50; //The maximum number of sites to process during a single request
private $network_roles = array();
/**
* Returns the singleton Controller_Permissions.
*
* @return Controller_Permissions
*/
public static function shared() {
static $_shared = null;
if ($_shared === null) {
$_shared = new Controller_Permissions();
}
return $_shared;
}
private function on_role_change() {
update_site_option('wfls_last_role_change', time());
if(is_multisite())
update_site_option('wfls_role_batch_position', 0);
}
public function install() {
$this->on_role_change();
if (is_multisite()) {
//Super Admin automatically gets all capabilities, so we don't need to explicitly add them
$this->_add_cap_multisite('administrator', self::CAP_ACTIVATE_2FA_SELF, $this->get_primary_sites());
}
else {
$this->_add_cap('administrator', self::CAP_ACTIVATE_2FA_SELF);
$this->_add_cap('administrator', self::CAP_ACTIVATE_2FA_OTHERS);
$this->_add_cap('administrator', self::CAP_MANAGE_SETTINGS);
}
}
public function init() {
global $wp_version;
if(is_multisite()){
if(version_compare($wp_version, '5.1.0', '>=')){
add_action('wp_initialize_site', array($this, '_wp_initialize_site'), 99);
}
else{
add_action('wpmu_new_blog', array($this, '_wpmu_new_blog'), 10, 5);
}
add_action('init', array($this, 'check_role_sync'), 1);
}
}
public function _wpmu_new_blog($site_id, $user_id, $domain, $path, $network_id) {
$this->sync_roles($network_id, $site_id);
}
public function _wp_initialize_site($new_site) {
$this->sync_roles($new_site->site_id, $new_site->blog_id);
}
public function check_role_sync() {
//Trigger an initial update for existing installations
$last_role_change=(int)get_site_option('wfls_last_role_change', 0);
if($last_role_change===0)
$this->on_role_change();
//Process the current batch if necessary
$position=(int)get_site_option('wfls_role_batch_position', 0);
if($position===-1)
return;
$sites=$this->get_sites($position, self::SITE_BATCH_SIZE);
if(empty($sites)){
$position=-1;
return;
}
else{
$network_id=get_current_site()->id;
foreach($sites as $site){
$site=(int)$site;
$this->sync_roles($network_id, $site);
}
$position=$site;
}
update_site_option('wfls_role_batch_position', $position);
//Update the current site if not already up to date
$site_id=get_current_blog_id();
if($last_role_change>=get_option('wfls_last_role_sync', 0)&&$site_id>=$position){
$this->sync_roles(get_current_site()->id, $site_id);
update_option('wfls_last_role_sync', time());
}
}
/**
* Get the primary site ID for a given network
*/
private function get_primary_site_id($network_id) {
global $wpdb;
if(function_exists('get_network')){
$network=get_network($network_id); //TODO: Support multi-network throughout plugin
return (int)$network->blog_id;
}
else{
return (int)$wpdb->get_var($wpdb->prepare("SELECT blogs.blog_id FROM {$wpdb->site} sites JOIN {$wpdb->blogs} blogs ON blogs.site_id=sites.id AND blogs.path=sites.path WHERE sites.id=%d", $network_id));
}
}
/**
* Get all primary sites in a multi-network setup
*/
private function get_primary_sites() {
global $wpdb;
if(function_exists('get_networks')){
return array_map(function($network){ return $network->blog_id; }, get_networks());
}
else{
return $wpdb->get_col("SELECT blogs.blog_id FROM {$wpdb->site} sites JOIN {$wpdb->blogs} blogs ON blogs.site_id=sites.id AND blogs.path=sites.path");
}
}
private function get_sites($from, $count) {
global $wpdb;
return $wpdb->get_col($wpdb->prepare("SELECT `blog_id` FROM `{$wpdb->blogs}` WHERE `deleted` = 0 AND blog_id > %d ORDER BY blog_id LIMIT %d", $from, $count));
}
/**
* Sync role capabilities from the default site to a newly added site
* @param int $network_id the relevant network
* @param int $site_id the newly added site(blog)
*/
private function sync_roles($network_id, $site_id){
if(array_key_exists($network_id, $this->network_roles)){
$current_roles=$this->network_roles[$network_id];
}
else{
$current_roles=$this->_wp_roles($this->get_primary_site_id($network_id));
$this->network_roles[$network_id]=$current_roles;
}
$new_site_roles=$this->_wp_roles($site_id);
$capabilities=array(
self::CAP_ACTIVATE_2FA_SELF,
self::CAP_ACTIVATE_2FA_OTHERS,
self::CAP_MANAGE_SETTINGS
);
foreach($current_roles->get_names() as $role_name=>$role_label){
if($new_site_roles->get_role($role_name)===null)
$new_site_roles->add_role($role_name, $role_label);
$role=$current_roles->get_role($role_name);
foreach($capabilities as $cap){
if($role->has_cap($cap)){
$this->_add_cap_multisite($role_name, $cap, array($site_id));
}
else{
$this->_remove_cap_multisite($role_name, $cap, array($site_id));
}
}
}
}
public function allow_2fa_self($role_name) {
$this->on_role_change();
if (is_multisite()) {
return $this->_add_cap_multisite($role_name, self::CAP_ACTIVATE_2FA_SELF, $this->get_primary_sites());
}
else {
return $this->_add_cap($role_name, self::CAP_ACTIVATE_2FA_SELF);
}
}
public function disallow_2fa_self($role_name) {
$this->on_role_change();
if (is_multisite()) {
return $this->_remove_cap_multisite($role_name, self::CAP_ACTIVATE_2FA_SELF, $this->get_primary_sites());
}
else {
if ($role_name == 'administrator') {
return true;
}
return $this->_remove_cap($role_name, self::CAP_ACTIVATE_2FA_SELF);
}
}
public function can_manage_settings($user = false) {
if ($user === false) {
$user = wp_get_current_user();
}
if (!($user instanceof \WP_User)) {
return false;
}
return $user->has_cap(self::CAP_MANAGE_SETTINGS);
}
public function can_role_manage_settings($role) {
if (is_string($role)) {
$role = get_role($role);
}
if ($role)
return $role->has_cap(self::CAP_MANAGE_SETTINGS);
return false;
}
private function _wp_roles($site_id = null) {
require(ABSPATH . 'wp-includes/version.php'); /** @var string $wp_version */
if (version_compare($wp_version, '4.9', '>=')) {
return new \WP_Roles($site_id);
}
//\WP_Roles in WP < 4.9 initializes based on the current blog ID
if (is_multisite()) {
switch_to_blog($site_id);
}
$wp_roles = new \WP_Roles();
if (is_multisite()) {
restore_current_blog();
}
return $wp_roles;
}
private function _add_cap_multisite($role_name, $cap, $blog_ids=null) {
if ($role_name === 'super-admin')
return true;
global $wpdb;
$blogs = $blog_ids===null?$wpdb->get_col("SELECT `blog_id` FROM `{$wpdb->blogs}` WHERE `deleted` = 0"):$blog_ids;
$added = false;
foreach ($blogs as $id) {
$wp_roles = $this->_wp_roles($id);
switch_to_blog($id);
$added = $this->_add_cap($role_name, $cap, $wp_roles) || $added;
restore_current_blog();
}
return $added;
}
private function _add_cap($role_name, $cap, $wp_roles = null) {
if ($wp_roles === null) { $wp_roles = $this->_wp_roles(); }
$role = $wp_roles->get_role($role_name);
if ($role === null) {
return false;
}
$wp_roles->add_cap($role_name, $cap);
return true;
}
private function _remove_cap_multisite($role_name, $cap, $blog_ids=null) {
if ($role_name === 'super-admin')
return false;
global $wpdb;
$blogs = $blog_ids===null?$wpdb->get_col("SELECT `blog_id` FROM `{$wpdb->blogs}` WHERE `deleted` = 0"):$blog_ids;
$removed = false;
foreach ($blogs as $id) {
$wp_roles = $this->_wp_roles($id);
switch_to_blog($id);
$removed = $this->_remove_cap($role_name, $cap, $wp_roles) || $removed;
restore_current_blog();
}
return $removed;
}
private function _remove_cap($role_name, $cap, $wp_roles = null) {
if ($wp_roles === null) { $wp_roles = $this->_wp_roles(); }
$role = $wp_roles->get_role($role_name);
if ($role === null) {
return false;
}
$wp_roles->remove_cap($role_name, $cap);
return true;
}
public function get_all_roles($user) {
if (is_multisite()) {
$roles = array();
if (is_super_admin($user->ID))
$roles[] = 'super-admin';
foreach (get_blogs_of_user($user->ID) as $id => $blog) {
switch_to_blog($id);
$blogUser = new \WP_User($user->ID);
$roles = array_merge($roles, $blogUser->roles);
restore_current_blog();
}
return array_unique($roles);
}
else {
return $user->roles;
}
}
}

View File

@@ -0,0 +1,576 @@
<?php
namespace WordfenceLS;
use WordfenceLS\Settings\Model_DB;
use WordfenceLS\Settings\Model_WPOptions;
class Controller_Settings {
//Configurable
const OPTION_XMLRPC_ENABLED = 'xmlrpc-enabled';
const OPTION_2FA_WHITELISTED = 'whitelisted';
const OPTION_IP_SOURCE = 'ip-source';
const OPTION_IP_TRUSTED_PROXIES = 'ip-trusted-proxies';
const OPTION_REQUIRE_2FA_ADMIN = 'require-2fa.administrator';
const OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED = 'require-2fa-grace-period-enabled';
const OPTION_REQUIRE_2FA_GRACE_PERIOD = 'require-2fa-grace-period';
const OPTION_REQUIRE_2FA_USER_GRACE_PERIOD = '2fa-user-grace-period';
const OPTION_REMEMBER_DEVICE_ENABLED = 'remember-device';
const OPTION_REMEMBER_DEVICE_DURATION = 'remember-device-duration';
const OPTION_ALLOW_XML_RPC = 'allow-xml-rpc';
const OPTION_ENABLE_AUTH_CAPTCHA = 'enable-auth-captcha';
const OPTION_CAPTCHA_TEST_MODE = 'recaptcha-test-mode';
const OPTION_RECAPTCHA_SITE_KEY = 'recaptcha-site-key';
const OPTION_RECAPTCHA_SECRET = 'recaptcha-secret';
const OPTION_RECAPTCHA_THRESHOLD = 'recaptcha-threshold';
const OPTION_DELETE_ON_DEACTIVATION = 'delete-deactivation';
const OPTION_PREFIX_REQUIRED_2FA_ROLE = 'required-2fa-role';
const OPTION_ENABLE_WOOCOMMERCE_INTEGRATION = 'enable-woocommerce-integration';
const OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION = 'enable-woocommerce-account-integration';
const OPTION_ENABLE_SHORTCODE = 'enable-shortcode';
const OPTION_ENABLE_LOGIN_HISTORY_COLUMNS = 'enable-login-history-columns';
const OPTION_STACK_UI_COLUMNS = 'stack-ui-columns';
//Internal
const OPTION_GLOBAL_NOTICES = 'global-notices';
const OPTION_LAST_SECRET_REFRESH = 'last-secret-refresh';
const OPTION_USE_NTP = 'use-ntp';
const OPTION_ALLOW_DISABLING_NTP = 'allow-disabling-ntp';
const OPTION_NTP_FAILURE_COUNT = 'ntp-failure-count';
const OPTION_NTP_OFFSET = 'ntp-offset';
const OPTION_SHARED_HASH_SECRET_KEY = 'shared-hash-secret';
const OPTION_SHARED_SYMMETRIC_SECRET_KEY = 'shared-symmetric-secret';
const OPTION_DISMISSED_FRESH_INSTALL_MODAL = 'dismissed-fresh-install-modal';
const OPTION_CAPTCHA_STATS = 'captcha-stats';
const OPTION_SCHEMA_VERSION = 'schema-version';
const OPTION_USER_COUNT_QUERY_STATE = 'user-count-query-state';
const OPTION_DISABLE_TEMPORARY_TABLES = 'disable-temporary-tables';
const DEFAULT_REQUIRE_2FA_USER_GRACE_PERIOD = 10;
const MAX_REQUIRE_2FA_USER_GRACE_PERIOD = 99;
const STATE_2FA_DISABLED = 'disabled';
const STATE_2FA_OPTIONAL = 'optional';
const STATE_2FA_REQUIRED = 'required';
protected $_settingsStorage;
/**
* Returns the singleton Controller_Settings.
*
* @return Controller_Settings
*/
public static function shared() {
static $_shared = null;
if ($_shared === null) {
$_shared = new Controller_Settings();
}
return $_shared;
}
public function __construct($settingsStorage = false) {
if (!$settingsStorage) {
$settingsStorage = new Model_DB();
}
$this->_settingsStorage = $settingsStorage;
$this->_migrate_admin_2fa_requirements_to_roles();
}
public function set_defaults() {
$this->_settingsStorage->set_multiple(array(
self::OPTION_XMLRPC_ENABLED => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_2FA_WHITELISTED => array('value' => '', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_IP_SOURCE => array('value' => Model_Request::IP_SOURCE_AUTOMATIC, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_IP_TRUSTED_PROXIES => array('value' => '', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_REQUIRE_2FA_ADMIN => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD => array('value' => self::DEFAULT_REQUIRE_2FA_USER_GRACE_PERIOD, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_GLOBAL_NOTICES => array('value' => '[]', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_REMEMBER_DEVICE_ENABLED => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_REMEMBER_DEVICE_DURATION => array('value' => (30 * 86400), 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ALLOW_XML_RPC => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ENABLE_AUTH_CAPTCHA => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_CAPTCHA_STATS => array('value' => '{"counts":[0,0,0,0,0,0,0,0,0,0,0],"avg":0}', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_RECAPTCHA_THRESHOLD => array('value' => 0.5, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_LAST_SECRET_REFRESH => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_DELETE_ON_DEACTIVATION => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ENABLE_SHORTCODE => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_STACK_UI_COLUMNS => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_SCHEMA_VERSION => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_USER_COUNT_QUERY_STATE => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
self::OPTION_DISABLE_TEMPORARY_TABLES => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false)
));
}
public function set($key, $value, $already_validated = false) {
return $this->set_multiple(array($key => $value), $already_validated);
}
public function set_array($key, $value, $already_validated = false) {
return $this->set_multiple(array($key => json_encode($value)), $already_validated);
}
public function set_multiple($changes, $already_validated = false) {
if (!$already_validated && $this->validate_multiple($changes) !== true) {
return false;
}
$changes = $this->clean_multiple($changes);
$changes = $this->preprocess_multiple($changes);
$this->_settingsStorage->set_multiple($changes);
return true;
}
public function get($key, $default = false) {
return $this->_settingsStorage->get($key, $default);
}
public function get_bool($key, $default = false) {
return $this->_truthy_to_bool($this->get($key, $default));
}
public function get_int($key, $default = 0) {
return intval($this->get($key, $default));
}
public function get_float($key, $default = 0.0) {
return (float) $this->get($key, $default);
}
public function get_array($key, $default = array()) {
$value = $this->get($key, null);
if (is_string($value)) {
$value = @json_decode($value, true);
}
else {
$value = null;
}
return is_array($value) ? $value : $default;
}
public function remove($key) {
$this->_settingsStorage->remove($key);
}
/**
* Validates whether a user-entered setting value is acceptable. Returns true if valid or an error message if not.
*
* @param string $key
* @param mixed $value
* @return bool|string
*/
public function validate($key, $value) {
switch ($key) {
//Boolean
case self::OPTION_XMLRPC_ENABLED:
case self::OPTION_REQUIRE_2FA_ADMIN:
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED:
case self::OPTION_REMEMBER_DEVICE_ENABLED:
case self::OPTION_ALLOW_XML_RPC:
case self::OPTION_ENABLE_AUTH_CAPTCHA:
case self::OPTION_CAPTCHA_TEST_MODE:
case self::OPTION_DISMISSED_FRESH_INSTALL_MODAL:
case self::OPTION_DELETE_ON_DEACTIVATION:
case self::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION:
case self::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION:
case self::OPTION_ENABLE_SHORTCODE:
case self::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS:
case self::OPTION_STACK_UI_COLUMNS:
case self::OPTION_USER_COUNT_QUERY_STATE:
case self::OPTION_DISABLE_TEMPORARY_TABLES:
return true;
//Int
case self::OPTION_LAST_SECRET_REFRESH:
return is_numeric($value); //Left using is_numeric to prevent issues with existing values
case self::OPTION_SCHEMA_VERSION:
return Utility_Number::isInteger($value, 0);
//Array
case self::OPTION_GLOBAL_NOTICES:
case self::OPTION_CAPTCHA_STATS:
return preg_match('/^\[.*\]$/', $value) || preg_match('/^\{.*\}$/', $value); //Only a rough JSON validation
//Special
case self::OPTION_IP_TRUSTED_PROXIES:
case self::OPTION_2FA_WHITELISTED:
$parsed = array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $value)));
foreach ($parsed as $entry) {
if (!Controller_Whitelist::shared()->is_valid_range($entry)) {
return sprintf(__('The IP/range %s is invalid.', 'wordfence-2fa'), esc_html($entry));
}
}
return true;
case self::OPTION_IP_SOURCE:
if (!in_array($value, array(Model_Request::IP_SOURCE_AUTOMATIC, Model_Request::IP_SOURCE_REMOTE_ADDR, Model_Request::IP_SOURCE_X_FORWARDED_FOR, Model_Request::IP_SOURCE_X_REAL_IP))) {
return __('An invalid IP source was provided.', 'wordfence-2fa');
}
return true;
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD:
$gracePeriodEnd = strtotime($value);
if ($gracePeriodEnd <= \WordfenceLS\Controller_Time::time()) {
return __('The grace period end time must be in the future.', 'wordfence-2fa');
}
return true;
case self::OPTION_REMEMBER_DEVICE_DURATION:
return is_numeric($value) && $value > 0;
case self::OPTION_RECAPTCHA_THRESHOLD:
return is_numeric($value) && $value >= 0 && $value <= 1;
case self::OPTION_RECAPTCHA_SITE_KEY:
if (empty($value)) {
return true;
}
$response = wp_remote_get('https://www.google.com/recaptcha/api.js?render=' . urlencode($value));
if (!is_wp_error($response)) {
$status = wp_remote_retrieve_response_code($response);
if ($status == 200) {
return true;
}
$data = wp_remote_retrieve_body($response);
if (strpos($data, 'grecaptcha') === false) {
return __('Unable to validate the reCAPTCHA site key. Please check the key and try again.', 'wordfence-2fa');
}
return true;
}
return sprintf(__('An error was encountered while validating the reCAPTCHA site key: %s', 'wordfence-2fa'), $response->get_error_message());
case self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD:
return is_numeric($value) && $value >= 0 && $value <= self::MAX_REQUIRE_2FA_USER_GRACE_PERIOD;
}
return true;
}
public function validate_multiple($values) {
$errors = array();
foreach ($values as $key => $value) {
$status = $this->validate($key, $value);
if ($status !== true) {
$errors[$key] = $status;
}
}
if (!empty($errors)) {
return $errors;
}
return true;
}
/**
* Cleans and normalizes a setting value for use in saving.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
public function clean($key, $value) {
switch ($key) {
//Boolean
case self::OPTION_XMLRPC_ENABLED:
case self::OPTION_REQUIRE_2FA_ADMIN:
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED:
case self::OPTION_REMEMBER_DEVICE_ENABLED:
case self::OPTION_ALLOW_XML_RPC:
case self::OPTION_ENABLE_AUTH_CAPTCHA:
case self::OPTION_CAPTCHA_TEST_MODE:
case self::OPTION_DISMISSED_FRESH_INSTALL_MODAL:
case self::OPTION_DELETE_ON_DEACTIVATION:
case self::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION:
case self::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION:
case self::OPTION_ENABLE_SHORTCODE;
case self::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS:
case self::OPTION_STACK_UI_COLUMNS:
case self::OPTION_USER_COUNT_QUERY_STATE:
case self::OPTION_DISABLE_TEMPORARY_TABLES:
return $this->_truthy_to_bool($value);
//Int
case self::OPTION_REMEMBER_DEVICE_DURATION:
case self::OPTION_LAST_SECRET_REFRESH:
case self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD:
case self::OPTION_SCHEMA_VERSION:
return (int) $value;
//Float
case self::OPTION_RECAPTCHA_THRESHOLD:
return (float) $value;
//Special
case self::OPTION_IP_TRUSTED_PROXIES:
case self::OPTION_2FA_WHITELISTED:
$parsed = array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $value)));
$cleaned = array();
foreach ($parsed as $item) {
$cleaned[] = $this->_sanitize_ip_range($item);
}
return implode("\n", $cleaned);
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD:
$dt = $this->_parse_local_time($value);
return $dt->format('U');
case self::OPTION_RECAPTCHA_SITE_KEY:
case self::OPTION_RECAPTCHA_SECRET:
return trim($value);
}
return $value;
}
public function clean_multiple($changes) {
$cleaned = array();
foreach ($changes as $key => $value) {
$cleaned[$key] = $this->clean($key, $value);
}
return $cleaned;
}
private function get_required_2fa_role_key($role) {
return implode('.', array(self::OPTION_PREFIX_REQUIRED_2FA_ROLE, $role));
}
public function get_required_2fa_role_activation_time($role) {
$time = $this->get_int($this->get_required_2fa_role_key($role), -1);
if ($time < 0)
return false;
return $time;
}
public function get_user_2fa_grace_period() {
return $this->get_int(self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD, self::DEFAULT_REQUIRE_2FA_USER_GRACE_PERIOD);
}
/**
* Preprocesses the value, returning true if it was saved here (e.g., saved 2fa enabled by assigning a role
* capability) or false if it is to be saved by the backing storage.
*
* @param string $key
* @param mixed $value
* @param array &$settings the array of settings to process, this function may append additional values from preprocessing
* @return bool
*/
public function preprocess($key, $value, &$settings) {
if (preg_match('/^enabled-roles\.(.+)$/', $key, $matches)) { //Enabled roles are stored as capabilities rather than in the settings storage
$role = $matches[1];
if ($role === 'super-admin') {
$roleValid = true;
}
elseif (in_array($value, array(self::STATE_2FA_OPTIONAL, self::STATE_2FA_REQUIRED))) {
$roleValid = Controller_Permissions::shared()->allow_2fa_self($role);
}
else {
$roleValid = Controller_Permissions::shared()->disallow_2fa_self($role);
}
if ($roleValid)
$settings[$this->get_required_2fa_role_key($role)] = ($value === self::STATE_2FA_REQUIRED ? time() : -1);
return true;
}
return false;
}
public function preprocess_multiple($changes) {
$remaining = array();
foreach ($changes as $key => $value) {
if (!$this->preprocess($key, $value, $remaining)) {
$remaining[$key] = $value;
}
}
return $remaining;
}
/**
* Convenience
*/
/**
* Returns a cleaned array containing the whitelist entries.
*
* @return array
*/
public function whitelisted_ips() {
return array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $this->get(self::OPTION_2FA_WHITELISTED, ''))));
}
/**
* Returns a cleaned array containing the trusted proxy entries.
*
* @return array
*/
public function trusted_proxies() {
return array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $this->get(self::OPTION_IP_TRUSTED_PROXIES, ''))));
}
public function get_ntp_failure_count() {
return $this->get_int(self::OPTION_NTP_FAILURE_COUNT, 0);
}
public function reset_ntp_failure_count() {
$this->set(self::OPTION_NTP_FAILURE_COUNT, 0);
}
public function increment_ntp_failure_count() {
$count = $this->get_ntp_failure_count();
if ($count < 0)
return false;
$count++;
$this->set(self::OPTION_NTP_FAILURE_COUNT, $count);
return $count;
}
public function is_ntp_disabled_via_constant() {
return defined('WORDFENCE_LS_DISABLE_NTP') && WORDFENCE_LS_DISABLE_NTP;
}
public function is_ntp_enabled($requireOffset = true) {
if ($this->is_ntp_cron_disabled())
return false;
if ($this->get_bool(self::OPTION_USE_NTP, true)) {
if ($requireOffset) {
$offset = $this->get(self::OPTION_NTP_OFFSET, null);
return $offset !== null && abs((int)$offset) <= Controller_TOTP::TIME_WINDOW_LENGTH;
}
else {
return true;
}
}
return false;
}
public function is_ntp_cron_disabled(&$failureCount = null) {
if ($this->is_ntp_disabled_via_constant())
return true;
$failureCount = $this->get_ntp_failure_count();
if ($failureCount >= Controller_Time::FAILURE_LIMIT) {
return true;
}
else if ($failureCount < 0) {
$failureCount = 0;
return true;
}
return false;
}
public function disable_ntp_cron() {
$this->set(self::OPTION_NTP_FAILURE_COUNT, -1);
}
public function are_login_history_columns_enabled() {
return Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS, true);
}
public function should_stack_ui_columns() {
return self::shared()->get_bool(Controller_Settings::OPTION_STACK_UI_COLUMNS, true);
}
/**
* Utility
*/
/**
* Translates a value to a boolean, correctly interpreting various textual representations.
*
* @param $value
* @return bool
*/
protected function _truthy_to_bool($value) {
if ($value === true || $value === false) {
return $value;
}
if (is_numeric($value)) {
return !!$value;
}
if (preg_match('/^(?:f(?:alse)?|no?|off)$/i', $value)) {
return false;
}
else if (preg_match('/^(?:t(?:rue)?|y(?:es)?|on)$/i', $value)) {
return true;
}
return !empty($value);
}
/**
* Parses the given time string and returns its DateTime with the server's configured time zone.
*
* @param string $timestring
* @return \DateTime
*/
protected function _parse_local_time($timestring) {
$utc = new \DateTimeZone('UTC');
$tz = get_option('timezone_string');
if (!empty($tz)) {
$tz = new \DateTimeZone($tz);
return new \DateTime($timestring, $tz);
}
else {
$gmt = get_option('gmt_offset');
if (!empty($gmt)) {
if (PHP_VERSION_ID < 50510) {
$timestamp = strtotime($timestring);
$dtStr = gmdate("c", (int) ($timestamp + $gmt * 3600)); //Have to do it this way because of < PHP 5.5.10
return new \DateTime($dtStr, $utc);
}
else {
$direction = ($gmt > 0 ? '+' : '-');
$gmt = abs($gmt);
$h = (int) $gmt;
$m = ($gmt - $h) * 60;
$tz = new \DateTimeZone($direction . str_pad($h, 2, '0', STR_PAD_LEFT) . str_pad($m, 2, '0', STR_PAD_LEFT));
return new \DateTime($timestring, $tz);
}
}
}
return new \DateTime($timestring);
}
/**
* Cleans a user-entered IP range of unnecessary characters and normalizes some glyphs.
*
* @param string $range
* @return string
*/
protected function _sanitize_ip_range($range) {
$range = preg_replace('/\s/', '', $range); //Strip whitespace
$range = preg_replace('/[\\x{2013}-\\x{2015}]/u', '-', $range); //Non-hyphen dashes to hyphen
$range = strtolower($range);
if (preg_match('/^\d+-\d+$/', $range)) { //v5 32 bit int style format
list($start, $end) = explode('-', $range);
$start = long2ip($start);
$end = long2ip($end);
$range = "{$start}-{$end}";
}
return $range;
}
private function _migrate_admin_2fa_requirements_to_roles() {
if (!$this->get_bool(self::OPTION_REQUIRE_2FA_ADMIN))
return;
$time = time();
if (is_multisite()) {
$this->set($this->get_required_2fa_role_key('super-admin'), $time, true);
}
else {
$roles = new \WP_Roles();
foreach ($roles->roles as $key => $data) {
$role = $roles->get_role($key);
if (Controller_Permissions::shared()->can_role_manage_settings($role) && Controller_Permissions::shared()->allow_2fa_self($role->name)) {
$this->set($this->get_required_2fa_role_key($role->name), $time, true);
}
}
}
$this->remove(self::OPTION_REQUIRE_2FA_ADMIN);
$this->remove(self::OPTION_REQUIRE_2FA_GRACE_PERIOD);
$this->remove(self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED);
}
public function reset_ntp_disabled_flag() {
$this->remove(self::OPTION_USE_NTP);
$this->remove(self::OPTION_NTP_OFFSET);
$this->remove(self::OPTION_NTP_FAILURE_COUNT);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace WordfenceLS;
class Controller_Support {
const ITEM_INDEX = 'index';
const ITEM_CHANGELOG = 'changelog';
const ITEM_VERSION_WORDPRESS = 'version-wordpress';
const ITEM_VERSION_PHP = 'version-php';
const ITEM_VERSION_OPENSSL = 'version-ssl';
const ITEM_GDPR = 'gdpr';
const ITEM_GDPR_DPA = 'gdpr-dpa';
const ITEM_MODULE_LOGIN_SECURITY = 'module-login-security';
const ITEM_MODULE_LOGIN_SECURITY_2FA = 'module-login-security-2fa';
const ITEM_MODULE_LOGIN_SECURITY_2FA_APPS = 'module-login-security-2fa-apps';
const ITEM_MODULE_LOGIN_SECURITY_CAPTCHA = 'module-login-security-captcha';
const ITEM_MODULE_LOGIN_SECURITY_ROLES = 'module-login-security-roles';
const ITEM_MODULE_LOGIN_SECURITY_OPTION_WOOCOMMERCE_ACCOUNT_INTEGRATION = 'module-login-security-option-woocommerce-account-integration';
const ITEM_MODULE_LOGIN_SECURITY_OPTION_SHORTCODE = 'module-login-security-option-shortcode';
const ITEM_MODULE_LOGIN_SECURITY_OPTION_STACK_UI_COLUMNS = 'module-login-security-option-stack-ui-columns';
const ITEM_MODULE_LOGIN_SECURITY_2FA_NOTIFICATIONS = 'module-login-security-2fa-notifications';
public static function esc_supportURL($item = self::ITEM_INDEX) {
return esc_url(self::supportURL($item));
}
public static function supportURL($item = self::ITEM_INDEX) {
$base = 'https://www.wordfence.com/help/';
switch ($item) {
case self::ITEM_INDEX:
return 'https://www.wordfence.com/help/';
//These all fall through to the query format
case self::ITEM_VERSION_WORDPRESS:
case self::ITEM_VERSION_PHP:
case self::ITEM_VERSION_OPENSSL:
case self::ITEM_GDPR:
case self::ITEM_GDPR_DPA:
case self::ITEM_MODULE_LOGIN_SECURITY:
case self::ITEM_MODULE_LOGIN_SECURITY_2FA:
case self::ITEM_MODULE_LOGIN_SECURITY_CAPTCHA:
case self::ITEM_MODULE_LOGIN_SECURITY_ROLES:
case self::ITEM_MODULE_LOGIN_SECURITY_OPTION_WOOCOMMERCE_ACCOUNT_INTEGRATION:
case self::ITEM_MODULE_LOGIN_SECURITY_OPTION_SHORTCODE:
case self::ITEM_MODULE_LOGIN_SECURITY_OPTION_STACK_UI_COLUMNS:
case self::ITEM_MODULE_LOGIN_SECURITY_2FA_NOTIFICATIONS:
return $base . '?query=' . $item;
}
return '';
}
}

View File

@@ -0,0 +1,194 @@
<?php
namespace WordfenceLS;
class Controller_Time {
const NTP_VERSION = 3; // https://www.ietf.org/rfc/rfc1305.txt
const NTP_EPOCH_CONVERT = 2208988800; //RFC 5905, page 13
const FAILURE_LIMIT = 3;
/**
* Returns the singleton Controller_Time.
*
* @return Controller_Time
*/
public static function shared() {
static $_shared = null;
if ($_shared === null) {
$_shared = new Controller_Time();
}
return $_shared;
}
public function install() {
wp_clear_scheduled_hook('wordfence_ls_ntp_cron');
if (is_main_site()) {
wp_schedule_event(time() + 10, 'hourly', 'wordfence_ls_ntp_cron');
}
Controller_Settings::shared()->reset_ntp_disabled_flag();
}
public function uninstall() {
wp_clear_scheduled_hook('wordfence_ls_ntp_cron');
Controller_Settings::shared()->reset_ntp_disabled_flag();
}
public function init() {
$this->_init_actions();
}
public function _init_actions() {
add_action('wordfence_ls_ntp_cron', array($this, '_wordfence_ls_ntp_cron'));
}
public function _wordfence_ls_ntp_cron() {
if (Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_ALLOW_DISABLING_NTP) && Controller_Settings::shared()->is_ntp_cron_disabled())
return;
$ntp = self::ntp_time();
$time = time();
if ($ntp === false) {
$failureCount = Controller_Settings::shared()->increment_ntp_failure_count();
if ($failureCount >= self::FAILURE_LIMIT) {
Controller_Settings::shared()->set(Controller_Settings::OPTION_USE_NTP, false);
Controller_Settings::shared()->set(Controller_Settings::OPTION_NTP_OFFSET, 0);
}
}
else {
Controller_Settings::shared()->reset_ntp_failure_count();
Controller_Settings::shared()->set(Controller_Settings::OPTION_USE_NTP, true);
Controller_Settings::shared()->set(Controller_Settings::OPTION_NTP_OFFSET, $ntp - $time);
}
Controller_Settings::shared()->set(Controller_Settings::OPTION_ALLOW_DISABLING_NTP, true);
}
/**
* Returns the current UTC timestamp, offset as needed to reflect the time retrieved from an NTP request or (if
* running in the complete plugin) offset as needed from the Wordfence server's true time.
*
* @param bool|int $time The timestamp to apply any offset to. If `false`, it will use the current timestamp.
* @return int
*/
public static function time($time = false) {
if ($time === false) {
$time = time();
}
$offset = 0;
if (Controller_Settings::shared()->is_ntp_enabled()) {
$offset = Controller_Settings::shared()->get_int(Controller_Settings::OPTION_NTP_OFFSET);
}
else if (WORDFENCE_LS_FROM_CORE) {
$offset = \wfUtils::normalizedTime($time) - $time;
}
return $time + $offset;
}
/**
* Returns the current timestamp from ntp.org using the NTP protocol. If unable to (e.g., UDP connections are blocked),
* it will return false.
*
* @return bool|float
*/
public static function ntp_time() {
$servers = array('0.pool.ntp.org', '1.pool.ntp.org', '2.pool.ntp.org', '3.pool.ntp.org');
//Header - RFC 5905, page 18
$header = '00'; //LI (leap indicator) - 2 bits: 00 for "no warning"
$header .= sprintf('%03d', decbin(self::NTP_VERSION)); //VN (version number) - 3 bits: 011 for version 3
$header .= '011'; //Mode (association mode) - 3 bit: 011 for "client"
$packet = chr(bindec($header));
$packet .= str_repeat("\x0", 39);
foreach ($servers as $s) {
$socket = @fsockopen('udp://' . $s, 123, $err_no, $err_str, 1);
if ($socket) {
stream_set_timeout($socket, 1);
$remote_originate = microtime(true);
$secondsNTP = ((int) $remote_originate) + self::NTP_EPOCH_CONVERT;
$fractional = sprintf('%010d', round(($remote_originate - ((int) $remote_originate)) * 0x100000000));
$packed = pack('N', $secondsNTP) . pack('N', $fractional);
if (@fwrite($socket, $packet . $packed)) {
$response = fread($socket, 48);
$local_transmitted = microtime(true);
}
@fclose($socket);
if (isset($response) && Model_Crypto::strlen($response) == 48) {
break;
}
}
}
if (isset($response) && Model_Crypto::strlen($response) == 48) {
$longs = unpack("N12", $response);
$remote_originate_seconds = sprintf('%u', $longs[7]) - self::NTP_EPOCH_CONVERT;
$remote_received_seconds = sprintf('%u', $longs[9]) - self::NTP_EPOCH_CONVERT;
$remote_transmitted_seconds = sprintf('%u', $longs[11]) - self::NTP_EPOCH_CONVERT;
$remote_originate_fraction = sprintf('%u', $longs[8]) / 0x100000000;
$remote_received_fraction = sprintf('%u', $longs[10]) / 0x100000000;
$remote_transmitted_fraction = sprintf('%u', $longs[12]) / 0x100000000;
$remote_originate = $remote_originate_seconds + $remote_originate_fraction;
$remote_received = $remote_received_seconds + $remote_received_fraction;
$remote_transmitted = $remote_transmitted_seconds + $remote_transmitted_fraction;
$delay = (($local_transmitted - $remote_originate) / 2) - ($remote_transmitted - $remote_received);
$ntp_time = $remote_transmitted - $delay;
return $ntp_time;
}
return false;
}
/**
* Formats and returns the given timestamp using the time zone set for the WordPress installation.
*
* @param string $format See the PHP docs on DateTime for the format options.
* @param int|bool $timestamp Assumed to be in UTC. If false, defaults to the current timestamp.
* @return string
*/
public static function format_local_time($format, $timestamp = false) {
if ($timestamp === false) {
$timestamp = self::time();
}
$utc = new \DateTimeZone('UTC');
if (!function_exists('date_timestamp_set')) {
$dtStr = gmdate("c", (int) $timestamp); //Have to do it this way because of PHP 5.2
$dt = new \DateTime($dtStr, $utc);
}
else {
$dt = new \DateTime('now', $utc);
$dt->setTimestamp($timestamp);
}
$tz = get_option('timezone_string');
if (!empty($tz)) {
$dt->setTimezone(new \DateTimeZone($tz));
}
else {
$gmt = get_option('gmt_offset');
if (!empty($gmt)) {
if (PHP_VERSION_ID < 50510) {
$dtStr = gmdate("c", (int) ($timestamp + $gmt * 3600)); //Have to do it this way because of < PHP 5.5.10
$dt = new \DateTime($dtStr, $utc);
}
else {
$direction = ($gmt > 0 ? '+' : '-');
$gmt = abs($gmt);
$h = (int) $gmt;
$m = ($gmt - $h) * 60;
$dt->setTimezone(new \DateTimeZone($direction . str_pad($h, 2, '0', STR_PAD_LEFT) . str_pad($m, 2, '0', STR_PAD_LEFT)));
}
}
}
return $dt->format($format);
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace WordfenceLS;
class Controller_TOTP {
const TIME_WINDOW_LENGTH = 30;
/**
* Returns the singleton Controller_TOTP.
*
* @return Controller_TOTP
*/
public static function shared() {
static $_shared = null;
if ($_shared === null) {
$_shared = new Controller_TOTP();
}
return $_shared;
}
public function init() {
}
/**
* Activates a user with the given TOTP parameters.
*
* @param \WP_User $user
* @param string $secret The secret as a hex string.
* @param string[] $recovery An array of recovery codes as hex strings.
* @param bool|int $vtime The timestamp of the verification code or false to use the current timestamp.
*/
public function activate_2fa($user, $secret, $recovery, $vtime = false) {
if ($vtime === false) {
$vtime = Controller_Time::time();
}
global $wpdb;
$table = Controller_DB::shared()->secrets;
$wpdb->query($wpdb->prepare("INSERT INTO `{$table}` (`user_id`, `secret`, `recovery`, `ctime`, `vtime`, `mode`) VALUES (%d, %s, %s, UNIX_TIMESTAMP(), %d, 'authenticator')", $user->ID, Model_Compat::hex2bin($secret), implode('', array_map(function($r) { return Model_Compat::hex2bin($r); }, $recovery)), $vtime));
}
/**
* Validates the 2FA (or recovery) code for the given user. This will return `null` if the user does not have 2FA
* enabled. This check will mark the code as used, preventing its use again.
*
* @param \WP_User $user
* @param string $code
* @return bool|null Returns null if the user does not have 2FA enabled, false if the code is invalid, and true if valid.
*/
public function validate_2fa($user, $code, $update = true) {
global $wpdb;
$table = Controller_DB::shared()->secrets;
$record = $wpdb->get_row($wpdb->prepare("SELECT * FROM `{$table}` WHERE `user_id` = %d FOR UPDATE", $user->ID), ARRAY_A);
if (!$record) {
return null;
}
if (preg_match('/^(?:[a-f0-9]{4}\s*){4}$/i', $code)) { //Recovery code
$code = strtolower(preg_replace('/\s/i', '', $code));
$recoveryCodes = str_split(strtolower(bin2hex($record['recovery'])), 16);
$index = array_search($code, $recoveryCodes);
if ($index !== false) {
if ($update) {
unset($recoveryCodes[$index]);
$updatedRecoveryCodes = implode('', $recoveryCodes);
$wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `recovery` = X%s WHERE `id` = %d", $updatedRecoveryCodes, $record['id']));
}
$wpdb->query('COMMIT');
return true;
}
}
else if (preg_match('/^(?:[0-9]{3}\s*){2}$/i', $code)) { //TOTP code
$code = preg_replace('/\s/i', '', $code);
$secret = bin2hex($record['secret']);
$matches = $this->check_code($secret, $code, floor($record['vtime'] / self::TIME_WINDOW_LENGTH));
if ($matches !== false) {
if ($update) {
$wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `vtime` = %d WHERE `id` = %d", $matches, $record['id']));
}
$wpdb->query('COMMIT');
return true;
}
}
$wpdb->query('ROLLBACK');
return false;
}
/**
* Checks whether or not the code is valid for the given secret. If it is, it returns the time window (as a timestamp)
* that matched. If no time windows are provided, it checks the current and one on each side.
*
* @param string $secret The secret as a hex string.
* @param string $code The code.
* @param null|int The last-used time window (as a timestamp).
* @param null|array $windows An array of time windows or null to use the default.
* @return bool|int The time window if matches, otherwise false.
*/
public function check_code($secret, $code, $previous = null, $windows = null) {
$timeCode = floor(Controller_Time::time() / self::TIME_WINDOW_LENGTH);
if ($windows === null) {
$windows = array();
$validRange = array(-1, 1); //90 second range for authenticator
$lowRange = $validRange[0];
$highRange = $validRange[1];
for ($i = 0; $i >= $lowRange; $i--) {
$windows[] = $timeCode + $i;
}
for ($i = 1; $i <= $highRange; $i++) {
$windows[] = $timeCode + $i;
}
}
foreach ($windows as $w) {
if ($previous !== null && $previous >= $w) {
continue;
}
$expectedCode = $this->_generate_totp($secret, dechex($w));
if (hash_equals($expectedCode, $code)) {
return $w * self::TIME_WINDOW_LENGTH;
}
}
return false;
}
/**
* Generates a TOTP value using the provided parameters.
*
* @param $key The key in hex.
* @param $time The desired time code in hex.
* @param int $digits The number of digits.
* @return string The TOTP value.
*/
private function _generate_totp($key, $time, $digits = 6)
{
$time = Model_Compat::hex2bin(str_pad($time, 16, '0', STR_PAD_LEFT));
$key = Model_Compat::hex2bin($key);
$hash = hash_hmac('sha1', $time, $key);
$offset = hexdec(substr($hash, -2)) & 0xf;
$intermediate = ( ((hexdec(substr($hash, $offset * 2, 2)) & 0x7f) << 24) |
((hexdec(substr($hash, ($offset + 1) * 2, 2)) & 0xff) << 16) |
((hexdec(substr($hash, ($offset + 2) * 2, 2)) & 0xff) << 8) |
((hexdec(substr($hash, ($offset + 3) * 2, 2)) & 0xff))
);
$otp = $intermediate % pow(10, $digits);
return str_pad("{$otp}", $digits, '0', STR_PAD_LEFT);
}
}

View File

@@ -0,0 +1,317 @@
<?php
namespace WordfenceLS;
class Controller_Whitelist {
private $_cachedStatus = array();
/**
* Returns the singleton Controller_Whitelist.
*
* @return Controller_Whitelist
*/
public static function shared() {
static $_shared = null;
if ($_shared === null) {
$_shared = new Controller_Whitelist();
}
return $_shared;
}
public function is_whitelisted($ip) {
$ipHash = hash('sha256', Model_IP::inet_pton($ip));
if (isset($this->_cachedStatus[$ipHash])) {
return $this->_cachedStatus[$ipHash];
}
$whitelist = Controller_Settings::shared()->whitelisted_ips();
foreach ($whitelist as $entry) {
if ($this->ip_in_range($ip, $entry)) {
$this->_cachedStatus[$ipHash] = true;
return true;
}
}
$this->_cachedStatus[$ipHash] = false;
return false;
}
/**
* Check if the supplied IP address is within the user supplied range.
*
* @param string $ip
* @return bool
*/
public function ip_in_range($ip, $range) {
if (strpos($range, '/') !== false) { //CIDR range -- 127.0.0.1/24
return $this->_cidr_contains_ip($range, $ip);
}
else if (strpos($range, '[') !== false) { //Bracketed range -- 127.0.0.[1-100]
// IPv4 range
if (strpos($range, '.') !== false && strpos($ip, '.') !== false) {
// IPv4-mapped-IPv6
if (preg_match('/:ffff:([^:]+)$/i', $range, $matches)) {
$range = $matches[1];
}
if (preg_match('/:ffff:([^:]+)$/i', $ip, $matches)) {
$ip = $matches[1];
}
// Range check
if (preg_match('/\[\d+\-\d+\]/', $range)) {
$ipParts = explode('.', $ip);
$whiteParts = explode('.', $range);
$mismatch = false;
if (count($whiteParts) != 4 || count($ipParts) != 4) {
return false;
}
for ($i = 0; $i <= 3; $i++) {
if (preg_match('/^\[(\d+)\-(\d+)\]$/', $whiteParts[$i], $m)) {
if ($ipParts[$i] < $m[1] || $ipParts[$i] > $m[2]) {
$mismatch = true;
}
}
else if ($whiteParts[$i] != $ipParts[$i]) {
$mismatch = true;
}
}
if ($mismatch === false) {
return true; // Is whitelisted because we did not get a mismatch
}
}
else if ($range == $ip) {
return true;
}
// IPv6 range
}
else if (strpos($range, ':') !== false && strpos($ip, ':') !== false) {
$ip = strtolower(Model_IP::expand_ipv6_address($ip));
$range = strtolower($this->_expand_ipv6_range($range));
if (preg_match('/\[[a-f0-9]+\-[a-f0-9]+\]/i', $range)) {
$IPparts = explode(':', $ip);
$whiteParts = explode(':', $range);
$mismatch = false;
if (count($whiteParts) != 8 || count($IPparts) != 8) {
return false;
}
for ($i = 0; $i <= 7; $i++) {
if (preg_match('/^\[([a-f0-9]+)\-([a-f0-9]+)\]$/i', $whiteParts[$i], $m)) {
$ip_group = hexdec($IPparts[$i]);
$range_group_from = hexdec($m[1]);
$range_group_to = hexdec($m[2]);
if ($ip_group < $range_group_from || $ip_group > $range_group_to) {
$mismatch = true;
break;
}
}
else if ($whiteParts[$i] != $IPparts[$i]) {
$mismatch = true;
break;
}
}
if ($mismatch === false) {
return true; // Is whitelisted because we did not get a mismatch
}
}
else if ($range == $ip) {
return true;
}
}
}
else if (strpos($range, '-') !== false) { //Linear range -- 127.0.0.1 - 127.0.1.100
list($ip1, $ip2) = explode('-', $range);
$ip1N = Model_IP::inet_pton($ip1);
$ip2N = Model_IP::inet_pton($ip2);
$ipN = Model_IP::inet_pton($ip);
return (strcmp($ip1N, $ipN) <= 0 && strcmp($ip2N, $ipN) >= 0);
}
else { //Treat as a literal IP
$ip1 = Model_IP::inet_pton($range);
$ip2 = Model_IP::inet_pton($ip);
if ($ip1 !== false && $ip1 === $ip2) {
return true;
}
}
return false;
}
/**
* Utility
*/
/**
* Returns whether or not the CIDR-formatted subnet contains $ip.
*
* @param string $subnet
* @param string $ip A human-readable IP.
* @return bool
*/
protected function _cidr_contains_ip($subnet, $ip) {
list($network, $prefix) = array_pad(explode('/', $subnet, 2), 2, null);
if (filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
// If no prefix was supplied, 32 is implied for IPv4
if ($prefix === null) {
$prefix = 32;
}
// Validate the IPv4 network prefix
if ($prefix < 0 || $prefix > 32) {
return false;
}
// Increase the IPv4 network prefix to work in the IPv6 address space
$prefix += 96;
}
else {
// If no prefix was supplied, 128 is implied for IPv6
if ($prefix === null) {
$prefix = 128;
}
// Validate the IPv6 network prefix
if ($prefix < 1 || $prefix > 128) {
return false;
}
}
$bin_network = Model_Crypto::substr(Model_IP::inet_pton($network), 0, ceil($prefix / 8));
$bin_ip = Model_Crypto::substr(Model_IP::inet_pton($ip), 0, ceil($prefix / 8));
if ($prefix % 8 != 0) { //Adjust the last relevant character to fit the mask length since the character's bits are split over it
$pos = intval($prefix / 8);
$adjustment = chr(((0xff << (8 - ($prefix % 8))) & 0xff));
$bin_network[$pos] = ($bin_network[$pos] & $adjustment);
$bin_ip[$pos] = ($bin_ip[$pos] & $adjustment);
}
return ($bin_network === $bin_ip);
}
/**
* Expands a compressed printable range representation of an IPv6 address.
*
* @param string $range
* @return string
*/
protected function _expand_ipv6_range($range) {
$colon_count = substr_count($range, ':');
$dbl_colon_count = substr_count($range, '::');
if ($dbl_colon_count > 1) {
return false;
}
$dbl_colon_pos = strpos($range, '::');
if ($dbl_colon_pos !== false) {
$range = str_replace('::', str_repeat(':0000', (($dbl_colon_pos === 0 || $dbl_colon_pos === strlen($range) - 2) ? 9 : 8) - $colon_count) . ':', $range);
$range = trim($range, ':');
}
$colon_count = substr_count($range, ':');
if ($colon_count != 7) {
return false;
}
$groups = explode(':', $range);
$expanded = '';
foreach ($groups as $group) {
if (preg_match('/\[([a-f0-9]{1,4})\-([a-f0-9]{1,4})\]/i', $group, $matches)) {
$expanded .= sprintf('[%s-%s]', str_pad(strtolower($matches[1]), 4, '0', STR_PAD_LEFT), str_pad(strtolower($matches[2]), 4, '0', STR_PAD_LEFT)) . ':';
}
else if (preg_match('/[a-f0-9]{1,4}/i', $group)) {
$expanded .= str_pad(strtolower($group), 4, '0', STR_PAD_LEFT) . ':';
}
else {
return false;
}
}
return trim($expanded, ':');
}
/**
* @return bool
*/
public function is_valid_range($range) {
return $this->_is_valid_cidr_range($range) || $this->_is_valid_bracketed_range($range) || $this->_is_valid_linear_range($range) || Model_IP::is_valid_ip($range);
}
protected function _is_valid_cidr_range($range) { //e.g., 192.0.2.1/24
if (preg_match('/[^0-9a-f:\/\.]/i', $range)) { return false; }
$components = explode('/', $range);
if (count($components) != 2) { return false; }
list($ip, $prefix) = $components;
if (!Model_IP::is_valid_ip($ip)) { return false; }
if (!preg_match('/^\d+$/', $prefix)) { return false; }
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
if ($prefix < 0 || $prefix > 32) { return false; }
}
else {
if ($prefix < 1 || $prefix > 128) { return false; }
}
return true;
}
protected function _is_valid_bracketed_range($range) { //e.g., 192.0.2.[1-10]
if (preg_match('/[^0-9a-f:\.\[\]\-]/i', $range)) { return false; }
if (strpos($range, '.') !== false) { //IPv4
if (preg_match_all('/(\d+)/', $range, $matches) > 0) {
foreach ($matches[1] as $match) {
$group = (int) $match;
if ($group > 255 || $group < 0) {
return false;
}
}
}
$group_regex = '([0-9]{1,3}|\[[0-9]{1,3}\-[0-9]{1,3}\])';
return preg_match('/^' . str_repeat("{$group_regex}\\.", 3) . $group_regex . '$/i', $range) > 0;
}
//IPv6
if (strpos($range, '::') !== false) {
$range = $this->_expand_ipv6_range($range);
}
if (!$range) {
return false;
}
$group_regex = '([a-f0-9]{1,4}|\[[a-f0-9]{1,4}\-[a-f0-9]{1,4}\])';
return preg_match('/^' . str_repeat($group_regex . ':', 7) . $group_regex . '$/i', $range) > 0;
}
protected function _is_valid_linear_range($range) { //e.g., 192.0.2.1-192.0.2.100
if (preg_match('/[^0-9a-f:\.\-]/i', $range)) { return false; }
list($ip1, $ip2) = explode("-", $range);
$ip1N = Model_IP::inet_pton($ip1);
$ip2N = Model_IP::inet_pton($ip2);
if ($ip1N === false || !Model_IP::is_valid_ip($ip1) || $ip2N === false || !Model_IP::is_valid_ip($ip2)) {
return false;
}
return strcmp($ip1N, $ip2N) <= 0;
}
protected function _is_mixed_range($range) { //e.g., 192.0.2.1-2001:db8::ffff
if (preg_match('/[^0-9a-f:\.\-]/i', $range)) { return false; }
list($ip1, $ip2) = explode("-", $range);
$ipv4Count = 0;
$ipv4Count += filter_var($ip1, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false ? 1 : 0;
$ipv4Count += filter_var($ip2, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false ? 1 : 0;
$ipv6Count = 0;
$ipv6Count += filter_var($ip1, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false ? 1 : 0;
$ipv6Count += filter_var($ip2, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false ? 1 : 0;
if ($ipv4Count != 2 && $ipv6Count != 2) {
return true;
}
return false;
}
}