first commit
This commit is contained in:
@@ -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')
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user