first commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_URI} \.php$
|
||||
RewriteRule .* - [F,L,NC]
|
||||
</IfModule>
|
||||
<IfModule !mod_rewrite.c>
|
||||
<FilesMatch "\.php$">
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
@@ -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
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_2faInitializationData {
|
||||
|
||||
private $user;
|
||||
private $raw_secret;
|
||||
private $base32_secret;
|
||||
private $otp_url;
|
||||
private $recovery_codes;
|
||||
|
||||
public function __construct($user) {
|
||||
$this->user = $user;
|
||||
$this->raw_secret = Model_Crypto::random_bytes(20);
|
||||
}
|
||||
|
||||
public function get_user() {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function get_raw_secret() {
|
||||
return $this->raw_secret;
|
||||
}
|
||||
|
||||
public function get_base32_secret() {
|
||||
if ($this->base32_secret === null)
|
||||
$this->base32_secret = Utility_BaseConversion::base32_encode($this->raw_secret);
|
||||
return $this->base32_secret;
|
||||
}
|
||||
|
||||
private function generate_otp_url() {
|
||||
return "otpauth://totp/" . rawurlencode(preg_replace('~^https?://~i', '', home_url()) . ' (' . $this->user->user_login . ')') . '?secret=' . $this->get_base32_secret() . '&algorithm=SHA1&digits=6&period=30&issuer=Wordfence';
|
||||
}
|
||||
|
||||
public function get_otp_url() {
|
||||
if ($this->otp_url === null)
|
||||
$this->otp_url = $this->generate_otp_url();
|
||||
return $this->otp_url;
|
||||
}
|
||||
|
||||
public function get_recovery_codes() {
|
||||
if ($this->recovery_codes === null)
|
||||
$this->recovery_codes = Controller_Users::shared()->regenerate_recovery_codes();
|
||||
return $this->recovery_codes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
abstract class Model_Asset {
|
||||
|
||||
protected $handle;
|
||||
protected $source;
|
||||
protected $dependencies;
|
||||
protected $version;
|
||||
protected $registered = false;
|
||||
|
||||
public function __construct($handle, $source = '', $dependencies = array(), $version = false) {
|
||||
$this->handle = $handle;
|
||||
$this->source = $source;
|
||||
$this->dependencies = $dependencies;
|
||||
$this->version = $version;
|
||||
}
|
||||
|
||||
public function getSourceUrl() {
|
||||
if (empty($this->source))
|
||||
return null;
|
||||
$url = $this->source;
|
||||
if (is_string($this->version))
|
||||
$url = add_query_arg('ver', $this->version, $this->source);
|
||||
return $url;
|
||||
}
|
||||
|
||||
public abstract function enqueue();
|
||||
|
||||
public abstract function isEnqueued();
|
||||
|
||||
public abstract function renderInline();
|
||||
|
||||
public function renderInlineIfNotEnqueued() {
|
||||
if (!$this->isEnqueued())
|
||||
$this->renderInline();
|
||||
}
|
||||
|
||||
public function setRegistered() {
|
||||
$this->registered = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function register() {
|
||||
return $this->setRegistered();
|
||||
}
|
||||
|
||||
public static function js($file) {
|
||||
return self::_pluginBaseURL() . 'js/' . self::_versionedFileName($file);
|
||||
}
|
||||
|
||||
public static function css($file) {
|
||||
return self::_pluginBaseURL() . 'css/' . self::_versionedFileName($file);
|
||||
}
|
||||
|
||||
public static function img($file) {
|
||||
return self::_pluginBaseURL() . 'img/' . $file;
|
||||
}
|
||||
|
||||
protected static function _pluginBaseURL() {
|
||||
return plugins_url('', WORDFENCE_LS_FCPATH) . '/';
|
||||
}
|
||||
|
||||
protected static function _versionedFileName($subpath) {
|
||||
$version = WORDFENCE_LS_BUILD_NUMBER;
|
||||
if ($version != 'WORDFENCE_LS_BUILD_NUMBER' && preg_match('/^(.+?)(\.[^\.]+)$/', $subpath, $matches)) {
|
||||
$prefix = $matches[1];
|
||||
$suffix = $matches[2];
|
||||
return $prefix . '.' . $version . $suffix;
|
||||
}
|
||||
|
||||
return $subpath;
|
||||
}
|
||||
|
||||
public static function create($handle, $source = '', $dependencies = array(), $version = false) {
|
||||
return new static($handle, $source, $dependencies, $version);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_Compat {
|
||||
public static function hex2bin($string) { //Polyfill for PHP < 5.4
|
||||
if (!is_string($string)) { return false; }
|
||||
if (strlen($string) % 2 == 1) { return false; }
|
||||
return pack('H*', $string);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
abstract class Model_Crypto {
|
||||
/**
|
||||
* Refreshes the secrets used by the plugin.
|
||||
*/
|
||||
public static function refresh_secrets() {
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_SHARED_HASH_SECRET_KEY, bin2hex(self::random_bytes(32)));
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_SHARED_SYMMETRIC_SECRET_KEY, bin2hex(self::random_bytes(32)));
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_LAST_SECRET_REFRESH, Controller_Time::time(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the secret for hashing.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function shared_hash_secret() {
|
||||
return Controller_Settings::shared()->get(Controller_Settings::OPTION_SHARED_HASH_SECRET_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the secret for symmetric encryption.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function shared_symmetric_secret() {
|
||||
return Controller_Settings::shared()->get(Controller_Settings::OPTION_SHARED_SYMMETRIC_SECRET_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the installation has the required crypto support for this to work.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_required_crypto_functions() {
|
||||
if (function_exists('openssl_get_publickey') && function_exists('openssl_get_cipher_methods')) {
|
||||
$ciphers = openssl_get_cipher_methods();
|
||||
return array_search('aes-256-cbc', $ciphers) !== false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility
|
||||
*/
|
||||
|
||||
public static function random_bytes($bytes) {
|
||||
$bytes = (int) $bytes;
|
||||
if (function_exists('random_bytes')) {
|
||||
try {
|
||||
$rand = random_bytes($bytes);
|
||||
if (is_string($rand) && self::strlen($rand) === $bytes) {
|
||||
return $rand;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Fall through
|
||||
} catch (\TypeError $e) {
|
||||
// Fall through
|
||||
} catch (\Error $e) {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
if (function_exists('mcrypt_create_iv')) {
|
||||
// phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.mcrypt_create_ivDeprecatedRemoved,PHPCompatibility.Extensions.RemovedExtensions.mcryptDeprecatedRemoved,PHPCompatibility.Constants.RemovedConstants.mcrypt_dev_urandomDeprecatedRemoved
|
||||
$rand = @mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
|
||||
if (is_string($rand) && self::strlen($rand) === $bytes) {
|
||||
return $rand;
|
||||
}
|
||||
}
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
$rand = @openssl_random_pseudo_bytes($bytes, $strong);
|
||||
if (is_string($rand) && self::strlen($rand) === $bytes) {
|
||||
return $rand;
|
||||
}
|
||||
}
|
||||
// Last resort is insecure
|
||||
$return = '';
|
||||
for ($i = 0; $i < $bytes; $i++) {
|
||||
$return .= chr(mt_rand(0, 255));
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill for random_int.
|
||||
*
|
||||
* @param int $min
|
||||
* @param int $max
|
||||
* @return int
|
||||
*/
|
||||
public static function random_int($min = 0, $max = 0x7FFFFFFF) {
|
||||
if (function_exists('random_int')) {
|
||||
try {
|
||||
return random_int($min, $max);
|
||||
} catch (\Exception $e) {
|
||||
// Fall through
|
||||
} catch (\TypeError $e) {
|
||||
// Fall through
|
||||
} catch (\Error $e) {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
$diff = $max - $min;
|
||||
$bytes = self::random_bytes(4);
|
||||
if ($bytes === false || self::strlen($bytes) != 4) {
|
||||
throw new \RuntimeException("Unable to get 4 bytes");
|
||||
}
|
||||
$val = @unpack("Nint", $bytes);
|
||||
$val = $val['int'] & 0x7FFFFFFF;
|
||||
$fp = (float) $val / 2147483647.0; // convert to [0,1]
|
||||
return (int) (round($fp * $diff) + $min);
|
||||
}
|
||||
|
||||
public static function uuid() {
|
||||
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||
// 32 bits for "time_low"
|
||||
self::random_int(0, 0xffff), self::random_int(0, 0xffff),
|
||||
|
||||
// 16 bits for "time_mid"
|
||||
self::random_int(0, 0xffff),
|
||||
|
||||
// 16 bits for "time_hi_and_version",
|
||||
// four most significant bits holds version number 4
|
||||
self::random_int(0, 0x0fff) | 0x4000,
|
||||
|
||||
// 16 bits, 8 bits for "clk_seq_hi_res",
|
||||
// 8 bits for "clk_seq_low",
|
||||
// two most significant bits holds zero and one for variant DCE1.1
|
||||
self::random_int(0, 0x3fff) | 0x8000,
|
||||
|
||||
// 48 bits for "node"
|
||||
self::random_int(0, 0xffff), self::random_int(0, 0xffff), self::random_int(0, 0xffff)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mbstring internal encoding to a binary safe encoding when func_overload
|
||||
* is enabled.
|
||||
*
|
||||
* When mbstring.func_overload is in use for multi-byte encodings, the results from
|
||||
* strlen() and similar functions respect the utf8 characters, causing binary data
|
||||
* to return incorrect lengths.
|
||||
*
|
||||
* This function overrides the mbstring encoding to a binary-safe encoding, and
|
||||
* resets it to the users expected encoding afterwards through the
|
||||
* `reset_mbstring_encoding` function.
|
||||
*
|
||||
* It is safe to recursively call this function, however each
|
||||
* `_mbstring_binary_safe_encoding()` call must be followed up with an equal number
|
||||
* of `_reset_mbstring_encoding()` calls.
|
||||
*
|
||||
* @see Model_Crypto::_reset_mbstring_encoding
|
||||
*
|
||||
* @staticvar array $encodings
|
||||
* @staticvar bool $overloaded
|
||||
*
|
||||
* @param bool $reset Optional. Whether to reset the encoding back to a previously-set encoding.
|
||||
* Default false.
|
||||
*/
|
||||
protected static function _mbstring_binary_safe_encoding($reset = false) {
|
||||
static $encodings = array();
|
||||
static $overloaded = null;
|
||||
|
||||
if (is_null($overloaded)) {
|
||||
// phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.mbstring_func_overloadDeprecated
|
||||
$overloaded = function_exists('mb_internal_encoding') && (ini_get('mbstring.func_overload') & 2);
|
||||
}
|
||||
|
||||
if (false === $overloaded) { return; }
|
||||
|
||||
if (!$reset) {
|
||||
$encoding = mb_internal_encoding();
|
||||
array_push($encodings, $encoding);
|
||||
mb_internal_encoding('ISO-8859-1');
|
||||
}
|
||||
|
||||
if ($reset && $encodings) {
|
||||
$encoding = array_pop($encodings);
|
||||
mb_internal_encoding($encoding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the mbstring internal encoding to a users previously set encoding.
|
||||
*
|
||||
* @see Model_Crypto::_mbstring_binary_safe_encoding
|
||||
*/
|
||||
protected static function _reset_mbstring_encoding() {
|
||||
self::_mbstring_binary_safe_encoding(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $function
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
protected static function _call_mb_string_function($function, $args) {
|
||||
self::_mbstring_binary_safe_encoding();
|
||||
$return = call_user_func_array($function, $args);
|
||||
self::_reset_mbstring_encoding();
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multibyte safe strlen.
|
||||
*
|
||||
* @param $binary
|
||||
* @return int
|
||||
*/
|
||||
public static function strlen($binary) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('strlen', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $haystack
|
||||
* @param $needle
|
||||
* @param int $offset
|
||||
* @return int
|
||||
*/
|
||||
public static function stripos($haystack, $needle, $offset = 0) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('stripos', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
* @return mixed
|
||||
*/
|
||||
public static function strtolower($string) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('strtolower', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
* @param $start
|
||||
* @param $length
|
||||
* @return mixed
|
||||
*/
|
||||
public static function substr($string, $start, $length = null) {
|
||||
if ($length === null) { $length = self::strlen($string); }
|
||||
return self::_call_mb_string_function('substr', array(
|
||||
$string, $start, $length
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $haystack
|
||||
* @param $needle
|
||||
* @param int $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public static function strpos($haystack, $needle, $offset = 0) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('strpos', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @param int $offset
|
||||
* @param int $length
|
||||
* @return mixed
|
||||
*/
|
||||
public static function substr_count($haystack, $needle, $offset = 0, $length = null) {
|
||||
if ($length === null) { $length = self::strlen($haystack); }
|
||||
return self::_call_mb_string_function('substr_count', array(
|
||||
$haystack, $needle, $offset, $length
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
* @return mixed
|
||||
*/
|
||||
public static function strtoupper($string) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('strtoupper', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @param int $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public static function strrpos($haystack, $needle, $offset = 0) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('strrpos', $args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Crypto;
|
||||
/**
|
||||
* Binary-to-text PHP Utilities
|
||||
*
|
||||
* @package binary-to-text-php
|
||||
* @link https://github.com/ademarre/binary-to-text-php
|
||||
* @author Andre DeMarre
|
||||
* @copyright 2009-2013 Andre DeMarre
|
||||
* @license http://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for binary-to-text encoding with a base of 2^n
|
||||
*
|
||||
* The Base2n class is for binary-to-text conversion. It employs a
|
||||
* generalization of the algorithms used by many encoding schemes that
|
||||
* use a fixed number of bits to encode each character. In other words,
|
||||
* the base is a power of 2.
|
||||
*
|
||||
* Earlier versions of this class were named
|
||||
* FixedBitNotation and FixedBitEncoding.
|
||||
*
|
||||
* @package binary-to-text-php
|
||||
*/
|
||||
class Model_Base2n
|
||||
{
|
||||
protected $_chars;
|
||||
protected $_bitsPerCharacter;
|
||||
protected $_radix;
|
||||
protected $_rightPadFinalBits;
|
||||
protected $_padFinalGroup;
|
||||
protected $_padCharacter;
|
||||
protected $_caseSensitive;
|
||||
protected $_charmap;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param integer $bitsPerCharacter Bits to use for each encoded character
|
||||
* @param string $chars Base character alphabet
|
||||
* @param boolean $caseSensitive To decode in a case-sensitive manner
|
||||
* @param boolean $rightPadFinalBits How to encode last character
|
||||
* @param boolean $padFinalGroup Add padding to end of encoded output
|
||||
* @param string $padCharacter Character to use for padding
|
||||
*
|
||||
* @throws InvalidArgumentException for incompatible parameters
|
||||
*/
|
||||
public function __construct(
|
||||
$bitsPerCharacter,
|
||||
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_',
|
||||
$caseSensitive = TRUE, $rightPadFinalBits = FALSE,
|
||||
$padFinalGroup = FALSE, $padCharacter = '=')
|
||||
{
|
||||
// Ensure validity of $chars
|
||||
if (!is_string($chars) || ($charLength = strlen($chars)) < 2) {
|
||||
throw new \InvalidArgumentException('$chars must be a string of at least two characters');
|
||||
}
|
||||
|
||||
// Ensure validity of $padCharacter
|
||||
if ($padFinalGroup) {
|
||||
if (!is_string($padCharacter) || !isset($padCharacter[0])) {
|
||||
throw new \InvalidArgumentException('$padCharacter must be a string of one character');
|
||||
}
|
||||
|
||||
if ($caseSensitive) {
|
||||
$padCharFound = strpos($chars, $padCharacter[0]);
|
||||
} else {
|
||||
$padCharFound = stripos($chars, $padCharacter[0]);
|
||||
}
|
||||
|
||||
if ($padCharFound !== FALSE) {
|
||||
throw new \InvalidArgumentException('$padCharacter can not be a member of $chars');
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure validity of $bitsPerCharacter
|
||||
if (!is_int($bitsPerCharacter)) {
|
||||
throw new \InvalidArgumentException('$bitsPerCharacter must be an integer');
|
||||
}
|
||||
|
||||
if ($bitsPerCharacter < 1) {
|
||||
// $bitsPerCharacter must be at least 1
|
||||
throw new \InvalidArgumentException('$bitsPerCharacter can not be less than 1');
|
||||
|
||||
} elseif ($charLength < 1 << $bitsPerCharacter) {
|
||||
// Character length of $chars is too small for $bitsPerCharacter
|
||||
// Find greatest acceptable value of $bitsPerCharacter
|
||||
$bitsPerCharacter = 1;
|
||||
$radix = 2;
|
||||
|
||||
while ($charLength >= ($radix <<= 1) && $bitsPerCharacter < 8) {
|
||||
$bitsPerCharacter++;
|
||||
}
|
||||
|
||||
$radix >>= 1;
|
||||
throw new \InvalidArgumentException(
|
||||
'$bitsPerCharacter can not be more than ' . $bitsPerCharacter
|
||||
. ' given $chars length of ' . $charLength
|
||||
. ' (max radix ' . $radix . ')');
|
||||
|
||||
} elseif ($bitsPerCharacter > 8) {
|
||||
// $bitsPerCharacter must not be greater than 8
|
||||
throw new \InvalidArgumentException('$bitsPerCharacter can not be greater than 8');
|
||||
|
||||
} else {
|
||||
$radix = 1 << $bitsPerCharacter;
|
||||
}
|
||||
|
||||
$this->_chars = $chars;
|
||||
$this->_bitsPerCharacter = $bitsPerCharacter;
|
||||
$this->_radix = $radix;
|
||||
$this->_rightPadFinalBits = $rightPadFinalBits;
|
||||
$this->_padFinalGroup = $padFinalGroup;
|
||||
$this->_padCharacter = $padCharacter[0];
|
||||
$this->_caseSensitive = $caseSensitive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a string
|
||||
*
|
||||
* @param string $rawString Binary data to encode
|
||||
* @return string
|
||||
*/
|
||||
public function encode($rawString)
|
||||
{
|
||||
// Unpack string into an array of bytes
|
||||
$bytes = unpack('C*', $rawString);
|
||||
$byteCount = count($bytes);
|
||||
|
||||
$encodedString = '';
|
||||
$byte = array_shift($bytes);
|
||||
$bitsRead = 0;
|
||||
$oldBits = 0;
|
||||
|
||||
$chars = $this->_chars;
|
||||
$bitsPerCharacter = $this->_bitsPerCharacter;
|
||||
$rightPadFinalBits = $this->_rightPadFinalBits;
|
||||
$padFinalGroup = $this->_padFinalGroup;
|
||||
$padCharacter = $this->_padCharacter;
|
||||
|
||||
$charsPerByte = 8 / $bitsPerCharacter;
|
||||
$encodedLength = $byteCount * $charsPerByte;
|
||||
|
||||
// Generate encoded output; each loop produces one encoded character
|
||||
for ($c = 0; $c < $encodedLength; $c++) {
|
||||
|
||||
// Get the bits needed for this encoded character
|
||||
if ($bitsRead + $bitsPerCharacter > 8) {
|
||||
// Not enough bits remain in this byte for the current character
|
||||
// Save the remaining bits before getting the next byte
|
||||
$oldBitCount = 8 - $bitsRead;
|
||||
$oldBits = $byte ^ ($byte >> $oldBitCount << $oldBitCount);
|
||||
$newBitCount = $bitsPerCharacter - $oldBitCount;
|
||||
|
||||
if (!$bytes) {
|
||||
// Last bits; match final character and exit loop
|
||||
if ($rightPadFinalBits) $oldBits <<= $newBitCount;
|
||||
$encodedString .= $chars[$oldBits];
|
||||
|
||||
if ($padFinalGroup) {
|
||||
// Array of the lowest common multiples of $bitsPerCharacter and 8, divided by 8
|
||||
$lcmMap = array(1 => 1, 2 => 1, 3 => 3, 4 => 1, 5 => 5, 6 => 3, 7 => 7, 8 => 1);
|
||||
$bytesPerGroup = $lcmMap[$bitsPerCharacter];
|
||||
$pads = $bytesPerGroup * $charsPerByte - ceil((strlen($rawString) % $bytesPerGroup) * $charsPerByte);
|
||||
$encodedString .= str_repeat($padCharacter, $pads);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Get next byte
|
||||
$byte = array_shift($bytes);
|
||||
$bitsRead = 0;
|
||||
|
||||
} else {
|
||||
$oldBitCount = 0;
|
||||
$newBitCount = $bitsPerCharacter;
|
||||
}
|
||||
|
||||
// Read only the needed bits from this byte
|
||||
$bits = $byte >> 8 - ($bitsRead + ($newBitCount));
|
||||
$bits ^= $bits >> $newBitCount << $newBitCount;
|
||||
$bitsRead += $newBitCount;
|
||||
|
||||
if ($oldBitCount) {
|
||||
// Bits come from seperate bytes, add $oldBits to $bits
|
||||
$bits = ($oldBits << $newBitCount) | $bits;
|
||||
}
|
||||
|
||||
$encodedString .= $chars[$bits];
|
||||
}
|
||||
|
||||
return $encodedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a string
|
||||
*
|
||||
* @param string $encodedString Data to decode
|
||||
* @param boolean $strict Returns NULL if $encodedString contains an undecodable character
|
||||
* @return string
|
||||
*/
|
||||
public function decode($encodedString, $strict = FALSE)
|
||||
{
|
||||
if (!$encodedString || !is_string($encodedString)) {
|
||||
// Empty string, nothing to decode
|
||||
return '';
|
||||
}
|
||||
|
||||
$chars = $this->_chars;
|
||||
$bitsPerCharacter = $this->_bitsPerCharacter;
|
||||
$radix = $this->_radix;
|
||||
$rightPadFinalBits = $this->_rightPadFinalBits;
|
||||
$padFinalGroup = $this->_padFinalGroup;
|
||||
$padCharacter = $this->_padCharacter;
|
||||
$caseSensitive = $this->_caseSensitive;
|
||||
|
||||
// Get index of encoded characters
|
||||
if ($this->_charmap) {
|
||||
$charmap = $this->_charmap;
|
||||
|
||||
} else {
|
||||
$charmap = array();
|
||||
|
||||
for ($i = 0; $i < $radix; $i++) {
|
||||
$charmap[$chars[$i]] = $i;
|
||||
}
|
||||
|
||||
$this->_charmap = $charmap;
|
||||
}
|
||||
|
||||
// The last encoded character is $encodedString[$lastNotatedIndex]
|
||||
$lastNotatedIndex = strlen($encodedString) - 1;
|
||||
|
||||
// Remove trailing padding characters
|
||||
if ($padFinalGroup) {
|
||||
while ($encodedString[$lastNotatedIndex] === $padCharacter) {
|
||||
$encodedString = substr($encodedString, 0, $lastNotatedIndex);
|
||||
$lastNotatedIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
$rawString = '';
|
||||
$byte = 0;
|
||||
$bitsWritten = 0;
|
||||
|
||||
// Convert each encoded character to a series of unencoded bits
|
||||
for ($c = 0; $c <= $lastNotatedIndex; $c++) {
|
||||
|
||||
if (!$caseSensitive && !isset($charmap[$encodedString[$c]])) {
|
||||
// Encoded character was not found; try other case
|
||||
if (isset($charmap[$cUpper = strtoupper($encodedString[$c])])) {
|
||||
$charmap[$encodedString[$c]] = $charmap[$cUpper];
|
||||
|
||||
} elseif (isset($charmap[$cLower = strtolower($encodedString[$c])])) {
|
||||
$charmap[$encodedString[$c]] = $charmap[$cLower];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($charmap[$encodedString[$c]])) {
|
||||
$bitsNeeded = 8 - $bitsWritten;
|
||||
$unusedBitCount = $bitsPerCharacter - $bitsNeeded;
|
||||
|
||||
// Get the new bits ready
|
||||
if ($bitsNeeded > $bitsPerCharacter) {
|
||||
// New bits aren't enough to complete a byte; shift them left into position
|
||||
$newBits = $charmap[$encodedString[$c]] << $bitsNeeded - $bitsPerCharacter;
|
||||
$bitsWritten += $bitsPerCharacter;
|
||||
|
||||
} elseif ($c !== $lastNotatedIndex || $rightPadFinalBits) {
|
||||
// Zero or more too many bits to complete a byte; shift right
|
||||
$newBits = $charmap[$encodedString[$c]] >> $unusedBitCount;
|
||||
$bitsWritten = 8; //$bitsWritten += $bitsNeeded;
|
||||
|
||||
} else {
|
||||
// Final bits don't need to be shifted
|
||||
$newBits = $charmap[$encodedString[$c]];
|
||||
$bitsWritten = 8;
|
||||
}
|
||||
|
||||
$byte |= $newBits;
|
||||
|
||||
if ($bitsWritten === 8 || $c === $lastNotatedIndex) {
|
||||
// Byte is ready to be written
|
||||
$rawString .= pack('C', $byte);
|
||||
|
||||
if ($c !== $lastNotatedIndex) {
|
||||
// Start the next byte
|
||||
$bitsWritten = $unusedBitCount;
|
||||
$byte = ($charmap[$encodedString[$c]] ^ ($newBits << $unusedBitCount)) << 8 - $bitsWritten;
|
||||
}
|
||||
}
|
||||
|
||||
} elseif ($strict) {
|
||||
// Unable to decode character; abort
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return $rawString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Crypto;
|
||||
|
||||
use WordfenceLS\Controller_Time;
|
||||
use WordfenceLS\Model_Crypto;
|
||||
|
||||
/**
|
||||
* Class Model_JWT
|
||||
* @package Wordfence2FA\Crypto
|
||||
* @property array $payload
|
||||
* @property int $expiration
|
||||
*/
|
||||
class Model_JWT {
|
||||
private $_payload;
|
||||
private $_expiration;
|
||||
|
||||
/**
|
||||
* Decodes and returns the payload of a JWT. This also validates the signature and expiration. Currently assumes HS256 JWTs.
|
||||
*
|
||||
* @param string $token
|
||||
* @return Model_JWT|bool The decoded JWT or false if the token is invalid or fails validation.
|
||||
*/
|
||||
public static function decode_jwt($token) {
|
||||
$components = explode('.', $token);
|
||||
if (count($components) != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = Model_Crypto::shared_hash_secret();
|
||||
$body = $components[0] . '.' . $components[1];
|
||||
$signature = hash_hmac('sha256', $body, $key, true);
|
||||
$testSignature = self::base64url_decode($components[2]);
|
||||
if (!hash_equals($signature, $testSignature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$json = self::base64url_decode($components[1]);
|
||||
$payload = @json_decode($json, true);
|
||||
$expiration = false;
|
||||
if (isset($payload['_exp'])) {
|
||||
$expiration = $payload['_exp'];
|
||||
|
||||
if ($payload['_exp'] < Controller_Time::time()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($payload['_exp']);
|
||||
}
|
||||
|
||||
return new self($payload, $expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model_JWT constructor.
|
||||
*
|
||||
* @param array $payload
|
||||
* @param bool|int $expiration
|
||||
*/
|
||||
public function __construct($payload, $expiration = false) {
|
||||
$this->_payload = $payload;
|
||||
$this->_expiration = $expiration;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
$payload = $this->_payload;
|
||||
if ($this->_expiration !== false) {
|
||||
$payload['_exp'] = $this->_expiration;
|
||||
}
|
||||
$key = Model_Crypto::shared_hash_secret();
|
||||
$header = '{"alg":"HS256","typ":"JWT"}';
|
||||
$body = self::base64url_encode($header) . '.' . self::base64url_encode(json_encode($payload));
|
||||
$signature = hash_hmac('sha256', $body, $key, true);
|
||||
return $body . '.' . self::base64url_encode($signature);
|
||||
}
|
||||
|
||||
public function __isset($key) {
|
||||
switch ($key) {
|
||||
case 'payload':
|
||||
case 'expiration':
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Invalid key: ' . $key);
|
||||
}
|
||||
|
||||
public function __get($key) {
|
||||
switch ($key) {
|
||||
case 'payload':
|
||||
return $this->_payload;
|
||||
case 'expiration':
|
||||
return $this->_expiration;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Invalid key: ' . $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base64URL-encodes the given payload. This is identical to base64_encode except it substitutes characters
|
||||
* not safe for use in URLs.
|
||||
*
|
||||
* @param string $payload
|
||||
* @return string
|
||||
*/
|
||||
public static function base64url_encode($payload) {
|
||||
return self::base64url_convert_to(base64_encode($payload));
|
||||
}
|
||||
|
||||
public static function base64url_convert_to($base64) {
|
||||
$intermediate = rtrim($base64, '=');
|
||||
$intermediate = str_replace('+', '-', $intermediate);
|
||||
$intermediate = str_replace('/', '_', $intermediate);
|
||||
return $intermediate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64URL-decodes the given payload. This is identical to base64_encode except it allows for the characters
|
||||
* substituted by base64url_encode.
|
||||
*
|
||||
* @param string $payload
|
||||
* @return string
|
||||
*/
|
||||
public static function base64url_decode($payload) {
|
||||
return base64_decode(self::base64url_convert_from($payload));
|
||||
}
|
||||
|
||||
public static function base64url_convert_from($base64url) {
|
||||
$intermediate = str_replace('_', '/', $base64url);
|
||||
$intermediate = str_replace('-', '+', $intermediate);
|
||||
return $intermediate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Crypto;
|
||||
|
||||
use WordfenceLS\Model_Crypto;
|
||||
|
||||
abstract class Model_Symmetric {
|
||||
/**
|
||||
* Returns $data encrypted with the shared symmetric key or false if unable to do so.
|
||||
*
|
||||
* @param string $data
|
||||
* @return bool|array
|
||||
*/
|
||||
public static function encrypt($data) {
|
||||
if (!Model_Crypto::has_required_crypto_functions()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$symmetricKey = Model_Crypto::shared_symmetric_secret();
|
||||
$iv = Model_Crypto::random_bytes(16);
|
||||
$encrypted = @openssl_encrypt($data, 'aes-256-cbc', $symmetricKey, OPENSSL_RAW_DATA, $iv);
|
||||
if ($encrypted) {
|
||||
return array('data' => base64_encode($encrypted), 'iv' => base64_encode($iv));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decrypted value of a payload encrypted by Model_Symmetric::encrypt
|
||||
*
|
||||
* @param array $encrypted
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function decrypt($encrypted) {
|
||||
if (!Model_Crypto::has_required_crypto_functions()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($encrypted['data']) || !isset($encrypted['iv'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$symmetricKey = Model_Crypto::shared_symmetric_secret();
|
||||
$iv = base64_decode($encrypted['iv']);
|
||||
$encrypted = base64_decode($encrypted['data']);
|
||||
$data = @openssl_decrypt($encrypted, 'aes-256-cbc', $symmetricKey, OPENSSL_RAW_DATA, $iv);
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_IP {
|
||||
/**
|
||||
* Returns the human-readable representation of a packed binary IP address.
|
||||
*
|
||||
* @param string $ip
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function inet_ntop($ip) {
|
||||
if (Model_Crypto::strlen($ip) == 16 && Model_Crypto::substr($ip, 0, 12) == "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff") {
|
||||
$ip = Model_Crypto::substr($ip, 12, 4);
|
||||
}
|
||||
|
||||
if (self::has_ipv6()) {
|
||||
return @inet_ntop($ip);
|
||||
}
|
||||
|
||||
// IPv4
|
||||
if (Model_Crypto::strlen($ip) === 4) {
|
||||
return ord($ip[0]) . '.' . ord($ip[1]) . '.' . ord($ip[2]) . '.' . ord($ip[3]);
|
||||
}
|
||||
|
||||
// IPv6
|
||||
if (Model_Crypto::strlen($ip) === 16) {
|
||||
// IPv4 mapped IPv6
|
||||
if (Model_Crypto::substr($ip, 0, 12) == "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff") {
|
||||
return "::ffff:" . ord($ip[12]) . '.' . ord($ip[13]) . '.' . ord($ip[14]) . '.' . ord($ip[15]);
|
||||
}
|
||||
|
||||
$hex = bin2hex($ip);
|
||||
$groups = str_split($hex, 4);
|
||||
$in_collapse = false;
|
||||
$done_collapse = false;
|
||||
foreach ($groups as $index => $group) {
|
||||
if ($group == '0000' && !$done_collapse) {
|
||||
if ($in_collapse) {
|
||||
$groups[$index] = '';
|
||||
continue;
|
||||
}
|
||||
$groups[$index] = ':';
|
||||
$in_collapse = true;
|
||||
continue;
|
||||
}
|
||||
if ($in_collapse) {
|
||||
$done_collapse = true;
|
||||
}
|
||||
$groups[$index] = ltrim($groups[$index], '0');
|
||||
if (strlen($groups[$index]) === 0) {
|
||||
$groups[$index] = '0';
|
||||
}
|
||||
}
|
||||
$ip = join(':', array_filter($groups, 'strlen'));
|
||||
$ip = str_replace(':::', '::', $ip);
|
||||
return $ip == ':' ? '::' : $ip;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the packed binary representation of an IP address from the human readable version.
|
||||
*
|
||||
* @param string $ip
|
||||
* @return string
|
||||
*/
|
||||
public static function inet_pton($ip) {
|
||||
if (self::has_ipv6()) {
|
||||
$pton = @inet_pton($ip);
|
||||
if ($pton === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (preg_match('/^(?:\d{1,3}(?:\.|$)){4}/', $ip)) { // IPv4
|
||||
$octets = explode('.', $ip);
|
||||
$pton = chr($octets[0]) . chr($octets[1]) . chr($octets[2]) . chr($octets[3]);
|
||||
}
|
||||
else if (preg_match('/^((?:[\da-f]{1,4}(?::|)){0,8})(::)?((?:[\da-f]{1,4}(?::|)){0,8})$/i', $ip)) { // IPv6
|
||||
if ($ip === '::') {
|
||||
$pton = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
||||
}
|
||||
else {
|
||||
$colon_count = substr_count($ip, ':');
|
||||
$dbl_colon_pos = strpos($ip, '::');
|
||||
if ($dbl_colon_pos !== false) {
|
||||
$ip = str_replace('::', str_repeat(':0000', (($dbl_colon_pos === 0 || $dbl_colon_pos === strlen($ip) - 2) ? 9 : 8) - $colon_count) . ':', $ip);
|
||||
$ip = trim($ip, ':');
|
||||
}
|
||||
|
||||
$ip_groups = explode(':', $ip);
|
||||
$ipv6_bin = '';
|
||||
foreach ($ip_groups as $ip_group) {
|
||||
$ipv6_bin .= pack('H*', str_pad($ip_group, 4, '0', STR_PAD_LEFT));
|
||||
}
|
||||
|
||||
if (Model_Crypto::strlen($ipv6_bin) == 16) {
|
||||
$pton = $ipv6_bin;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (preg_match('/^(?:\:(?:\:0{1,4}){0,4}\:|(?:0{1,4}\:){5})ffff\:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $ip, $matches)) { // IPv4 mapped IPv6
|
||||
$octets = explode('.', $matches[1]);
|
||||
$pton = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" . chr($octets[0]) . chr($octets[1]) . chr($octets[2]) . chr($octets[3]);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$pton = str_pad($pton, 16, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00", STR_PAD_LEFT);
|
||||
return $pton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify PHP was compiled with IPv6 support.
|
||||
*
|
||||
* Some hosts appear to not have inet_ntop, and others appear to have inet_ntop but are unable to process IPv6 addresses.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_ipv6() {
|
||||
return defined('AF_INET6');
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a compressed printable representation of an IPv6 address.
|
||||
*
|
||||
* @param string $ip
|
||||
* @return string
|
||||
*/
|
||||
public static function expand_ipv6_address($ip) {
|
||||
$hex = bin2hex(self::inet_pton($ip));
|
||||
$ip = substr(preg_replace("/([a-f0-9]{4})/i", "$1:", $hex), 0, -1);
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the IP is a valid format.
|
||||
*
|
||||
* @param string $ip
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid_ip($ip) {
|
||||
return filter_var($ip, FILTER_VALIDATE_IP) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the range is a valid CIDR range.
|
||||
*
|
||||
* @param string $range
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid_cidr_range($range) {
|
||||
$components = explode('/', $range);
|
||||
if (count($components) != 2) { return false; }
|
||||
|
||||
list($ip, $prefix) = $components;
|
||||
if (!self::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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the IP is in the IPv6-mapped-IPv4 format.
|
||||
*
|
||||
* @param string $ip
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_ipv6_mapped_ipv4($ip) {
|
||||
return preg_match('/^(?:\:(?:\:0{1,4}){0,4}\:|(?:0{1,4}\:){5})ffff\:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/i', $ip) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_Notice {
|
||||
const SEVERITY_CRITICAL = 'critical';
|
||||
const SEVERITY_WARNING = 'warning';
|
||||
const SEVERITY_INFO = 'info';
|
||||
|
||||
private $_id;
|
||||
private $_severity;
|
||||
private $_messageHTML;
|
||||
private $_category;
|
||||
|
||||
public function __construct($id, $severity, $messageHTML, $category) {
|
||||
$this->_id = $id;
|
||||
$this->_severity = $severity;
|
||||
$this->_messageHTML = $messageHTML;
|
||||
$this->_category = $category;
|
||||
}
|
||||
|
||||
public function display_notice() {
|
||||
$severityClass = 'notice-info';
|
||||
if ($this->_severity == self::SEVERITY_CRITICAL) {
|
||||
$severityClass = 'notice-error';
|
||||
}
|
||||
else if ($this->_severity == self::SEVERITY_WARNING) {
|
||||
$severityClass = 'notice-warning';
|
||||
}
|
||||
|
||||
echo '<div class="wfls-notice notice ' . $severityClass . '" data-notice-id="' . esc_attr($this->_id) . '" data-notice-type="' . esc_attr($this->_category) . '"><p>' . $this->_messageHTML . '</p><p>' . sprintf(__('<a class="wfls-btn wfls-btn-default wfls-btn-sm wfls-dismiss-link" href="#" onclick="GWFLS.dismiss_notice(\'%s\'); return false;">Dismiss</a>', 'wordfence-2fa'), esc_attr($this->_id)) . '</p></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_Request {
|
||||
const IP_SOURCE_AUTOMATIC = '';
|
||||
const IP_SOURCE_REMOTE_ADDR = 'REMOTE_ADDR';
|
||||
const IP_SOURCE_X_FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR';
|
||||
const IP_SOURCE_X_REAL_IP = 'HTTP_X_REAL_IP';
|
||||
|
||||
private $_cachedIP;
|
||||
|
||||
public static function current() {
|
||||
return new Model_Request();
|
||||
}
|
||||
|
||||
public function detected_ip_preview($source = null, $trusted_proxies = null) {
|
||||
if ($source === null) {
|
||||
$source = Controller_Settings::shared()->get(Controller_Settings::OPTION_IP_SOURCE);
|
||||
}
|
||||
|
||||
$record = $this->_ip($source);
|
||||
if (is_array($record)) {
|
||||
list($ip, $variable) = $record;
|
||||
if (isset($_SERVER[$variable]) && strpos($_SERVER[$variable], ',') !== false) {
|
||||
$items = preg_replace('/[\s,]/', '', explode(',', $_SERVER[$variable]));
|
||||
$output = '';
|
||||
foreach ($items as $i) {
|
||||
if ($ip == $i) {
|
||||
$output .= ', <strong>' . esc_html($i) . '</strong>';
|
||||
}
|
||||
else {
|
||||
$output .= ', ' . esc_html($i);
|
||||
}
|
||||
}
|
||||
|
||||
return substr($output, 2);
|
||||
}
|
||||
return '<strong>' . esc_html($ip) . '</strong>';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function ip($refreshCache = false) {
|
||||
if (WORDFENCE_LS_FROM_CORE) {
|
||||
return \wfUtils::getIP($refreshCache);
|
||||
}
|
||||
|
||||
if (!isset($this->_cachedIP) || $refreshCache) {
|
||||
$this->_cachedIP = $this->_ip(Controller_Settings::shared()->get(Controller_Settings::OPTION_IP_SOURCE), Controller_Settings::shared()->trusted_proxies());
|
||||
}
|
||||
|
||||
return $this->_cachedIP[0]; //Format is array(<text IP>, <field found in>)
|
||||
}
|
||||
|
||||
public function ip_for_field($source, $trusted_proxies) {
|
||||
return $this->_ip($source, $trusted_proxies);
|
||||
}
|
||||
|
||||
protected function _ip($source = null, $trusted_proxies = null) {
|
||||
if ($source === null) {
|
||||
$source = Controller_Settings::shared()->get(Controller_Settings::OPTION_IP_SOURCE);
|
||||
}
|
||||
|
||||
$possible_ips = $this->_possible_ips($source);
|
||||
if ($trusted_proxies === null) { $trusted_proxies = array(); }
|
||||
return $this->_find_preferred_ip($possible_ips, $trusted_proxies);
|
||||
}
|
||||
|
||||
protected function _possible_ips($source = null) {
|
||||
$defaultIP = (is_array($_SERVER) && isset($_SERVER[self::IP_SOURCE_REMOTE_ADDR])) ? array($_SERVER[self::IP_SOURCE_REMOTE_ADDR], self::IP_SOURCE_REMOTE_ADDR) : array('127.0.0.1', self::IP_SOURCE_REMOTE_ADDR);
|
||||
|
||||
if ($source) {
|
||||
if ($source == self::IP_SOURCE_REMOTE_ADDR) {
|
||||
return array($defaultIP);
|
||||
}
|
||||
|
||||
$check = array(
|
||||
array((isset($_SERVER[$source]) ? $_SERVER[$source] : ''), $source),
|
||||
$defaultIP,
|
||||
);
|
||||
return $check;
|
||||
}
|
||||
|
||||
$check = array($defaultIP);
|
||||
if (isset($_SERVER[self::IP_SOURCE_X_FORWARDED_FOR])) {
|
||||
$check[] = array($_SERVER[self::IP_SOURCE_X_FORWARDED_FOR], self::IP_SOURCE_X_FORWARDED_FOR);
|
||||
}
|
||||
if (isset($_SERVER[self::IP_SOURCE_X_REAL_IP])) {
|
||||
$check[] = array($_SERVER[self::IP_SOURCE_X_REAL_IP], self::IP_SOURCE_X_REAL_IP);
|
||||
}
|
||||
return $check;
|
||||
}
|
||||
|
||||
protected function _find_preferred_ip($possible_ips, $trusted_proxies) {
|
||||
$privates = array();
|
||||
foreach ($possible_ips as $entry) {
|
||||
list($value, $var) = $entry;
|
||||
if (is_array($value)) { // An array of IPs
|
||||
foreach ($value as $index => $j) {
|
||||
if (!Model_IP::is_valid_ip($j)) {
|
||||
$j = preg_replace('/:\d+$/', '', $j); //Strip off port if present
|
||||
}
|
||||
|
||||
if (Model_IP::is_valid_ip($j)) {
|
||||
if (Model_IP::is_ipv6_mapped_ipv4($j)) {
|
||||
$j = Model_IP::inet_ntop(Model_IP::inet_pton($j));
|
||||
}
|
||||
|
||||
foreach ($trusted_proxies as $proxy) {
|
||||
if (!empty($proxy)) {
|
||||
if (Controller_Whitelist::shared()->ip_in_range($j, $proxy) && $index < count($value) - 1) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filter_var($j, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
$privates[] = array($j, $var);
|
||||
}
|
||||
else {
|
||||
return array($j, $var);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$skipToNext = false;
|
||||
$separators = array(',', ' ', "\t");
|
||||
foreach ($separators as $char) { // A list of IPs separated by <separator>: 192.0.2.15,192.0.2.35,192.0.2.254
|
||||
if (strpos($value, $char) !== false) {
|
||||
$sp = explode($char, $value);
|
||||
$sp = array_reverse($sp);
|
||||
foreach ($sp as $index => $j) {
|
||||
$j = trim($j);
|
||||
if (!Model_IP::is_valid_ip($j)) {
|
||||
$j = preg_replace('/:\d+$/', '', $j); //Strip off port
|
||||
}
|
||||
|
||||
if (Model_IP::is_valid_ip($j)) {
|
||||
if (Model_IP::is_ipv6_mapped_ipv4($j)) {
|
||||
$j = Model_IP::inet_ntop(Model_IP::inet_pton($j));
|
||||
}
|
||||
|
||||
foreach ($trusted_proxies as $proxy) {
|
||||
if (!empty($proxy)) {
|
||||
if (Controller_Whitelist::shared()->ip_in_range($j, $proxy) && $index < count($sp) - 1) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filter_var($j, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
$privates[] = array($j, $var);
|
||||
}
|
||||
else {
|
||||
return array($j, $var);
|
||||
}
|
||||
}
|
||||
}
|
||||
$skipToNext = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($skipToNext) { continue; } //Skip to next item because this one had a comma/space/tab, but we didn't find a valid, non-private address
|
||||
|
||||
// A literal IP
|
||||
if (!Model_IP::is_valid_ip($value)) {
|
||||
$value = preg_replace('/:\d+$/', '', $value); //Strip off port
|
||||
}
|
||||
|
||||
if (Model_IP::is_valid_ip($value)) {
|
||||
if (Model_IP::is_ipv6_mapped_ipv4($value)) {
|
||||
$value = Model_IP::inet_ntop(Model_IP::inet_pton($value));
|
||||
}
|
||||
|
||||
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
$privates[] = array($value, $var);
|
||||
}
|
||||
else {
|
||||
return array($value, $var);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($privates) > 0) {
|
||||
return $privates[0];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_Script extends Model_Asset {
|
||||
|
||||
public function enqueue() {
|
||||
if ($this->registered) {
|
||||
wp_enqueue_script($this->handle);
|
||||
}
|
||||
else {
|
||||
wp_enqueue_script($this->handle, $this->source, $this->dependencies, $this->version);
|
||||
}
|
||||
}
|
||||
|
||||
public function isEnqueued() {
|
||||
return wp_script_is($this->handle);
|
||||
}
|
||||
|
||||
public function renderInline() {
|
||||
if (empty($this->source))
|
||||
return;
|
||||
?>
|
||||
<script type="text/javascript" src="<?php echo esc_attr($this->getSourceUrl()) ?>"></script>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function register() {
|
||||
wp_register_script($this->handle, $this->source, $this->dependencies, $this->version);
|
||||
return parent::register();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
abstract class Model_Settings {
|
||||
const AUTOLOAD_YES = 'yes';
|
||||
const AUTOLOAD_NO = 'no';
|
||||
|
||||
/**
|
||||
* Sets $value to $key.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param string $autoload Whether or not the key/value pair should autoload in storages that do that.
|
||||
* @param bool $allowOverwrite If false, only sets the value if key does not already exist.
|
||||
*/
|
||||
abstract public function set($key, $value, $autoload = self::AUTOLOAD_YES, $allowOverwrite = true);
|
||||
abstract public function set_multiple($values);
|
||||
abstract public function get($key, $default);
|
||||
abstract public function remove($key);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Settings;
|
||||
|
||||
use WordfenceLS\Controller_DB;
|
||||
use WordfenceLS\Model_Settings;
|
||||
|
||||
class Model_DB extends Model_Settings {
|
||||
const AUTOLOAD_NO = 'no';
|
||||
const AUTOLOAD_YES = 'yes';
|
||||
|
||||
public function set($key, $value, $autoload = self::AUTOLOAD_YES, $allowOverwrite = true) {
|
||||
global $wpdb;
|
||||
$table = Controller_DB::shared()->settings;
|
||||
if (!$allowOverwrite) {
|
||||
if ($this->_has_cached($key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM `{$table}` WHERE `name` = %s", $key), ARRAY_A);
|
||||
if (is_array($row)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($wpdb->query($wpdb->prepare("INSERT INTO `{$table}` (`name`, `value`, `autoload`) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), `autoload` = VALUES(`autoload`)", $key, $value, $autoload)) !== false && $autoload != self::AUTOLOAD_NO) {
|
||||
$this->_update_cached($key, $value);
|
||||
do_action('wfls_settings_set', $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function set_multiple($values) {
|
||||
foreach ($values as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$this->set($key, $value['value'], $value['autoload'], $value['allowOverwrite']);
|
||||
}
|
||||
else {
|
||||
$this->set($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function get($key, $default = false) {
|
||||
global $wpdb;
|
||||
|
||||
if ($this->_has_cached($key)) {
|
||||
return $this->_cached_value($key);
|
||||
}
|
||||
|
||||
$table = Controller_DB::shared()->settings;
|
||||
if (!($setting = $wpdb->get_row($wpdb->prepare("SELECT `name`, `value`, `autoload` FROM `{$table}` WHERE `name` = %s", $key)))) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ($setting->autoload != self::AUTOLOAD_NO) {
|
||||
$this->_update_cached($key, $setting->value);
|
||||
}
|
||||
return $setting->value;
|
||||
}
|
||||
|
||||
public function remove($key) {
|
||||
global $wpdb;
|
||||
$table = Controller_DB::shared()->settings;
|
||||
$wpdb->query($wpdb->prepare("DELETE FROM `{$table}` WHERE `name` = %s", $key));
|
||||
$this->_remove_cached($key);
|
||||
}
|
||||
|
||||
private function _cached() {
|
||||
global $wpdb;
|
||||
|
||||
$settings = wp_cache_get('allsettings', 'wordfence-ls');
|
||||
if (!$settings) {
|
||||
$table = Controller_DB::shared()->settings;
|
||||
$suppress = $wpdb->suppress_errors();
|
||||
$raw = $wpdb->get_results("SELECT `name`, `value` FROM `{$table}` WHERE `autoload` = 'yes'");
|
||||
$wpdb->suppress_errors($suppress);
|
||||
$settings = array();
|
||||
foreach ((array) $raw as $o) {
|
||||
$settings[$o->name] = $o->value;
|
||||
}
|
||||
|
||||
wp_cache_add_non_persistent_groups('wordfence-ls');
|
||||
wp_cache_add('allsettings', $settings, 'wordfence-ls');
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
private function _update_cached($key, $value) {
|
||||
$settings = $this->_cached();
|
||||
$settings[$key] = $value;
|
||||
wp_cache_set('allsettings', $settings, 'wordfence-ls');
|
||||
}
|
||||
|
||||
private function _remove_cached($key) {
|
||||
$settings = $this->_cached();
|
||||
if (isset($settings[$key])) {
|
||||
unset($settings[$key]);
|
||||
wp_cache_set('allsettings', $settings, 'wordfence-ls');
|
||||
}
|
||||
}
|
||||
|
||||
private function _cached_value($key) {
|
||||
global $wpdb;
|
||||
|
||||
$settings = $this->_cached();
|
||||
if (isset($settings[$key])) {
|
||||
return $settings[$key];
|
||||
}
|
||||
|
||||
$table = Controller_DB::shared()->settings;
|
||||
$value = $wpdb->get_var($wpdb->prepare("SELECT `value` FROM `{$table}` WHERE name = %s", $key));
|
||||
if ($value !== null) {
|
||||
$settings[$key] = $value;
|
||||
wp_cache_set('allsettings', $settings, 'wordfence-ls');
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
public function _has_cached($key) {
|
||||
$settings = $this->_cached();
|
||||
return isset($settings[$key]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Settings;
|
||||
|
||||
use WordfenceLS\Model_Settings;
|
||||
|
||||
class Model_WPOptions extends Model_Settings {
|
||||
protected $_prefix;
|
||||
|
||||
public function __construct($prefix = '') {
|
||||
$this->_prefix = $prefix;
|
||||
}
|
||||
|
||||
protected function _translate_key($key) {
|
||||
return strtolower(preg_replace('/[^a-z0-9]/i', '_', $key));
|
||||
}
|
||||
|
||||
public function set($key, $value, $autoload = self::AUTOLOAD_YES, $allowOverwrite = true) {
|
||||
$key = $this->_translate_key($this->_prefix . $key);
|
||||
if (!$allowOverwrite) {
|
||||
if (is_multisite()) {
|
||||
add_network_option(null, $key, $value);
|
||||
}
|
||||
else {
|
||||
add_option($key, $value, '', $autoload);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (is_multisite()) {
|
||||
update_network_option(null, $key, $value);
|
||||
}
|
||||
else {
|
||||
update_option($key, $value, $autoload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function set_multiple($values) {
|
||||
foreach ($values as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$this->set($key, $value['value'], $value['autoload'], $value['allowOverwrite']);
|
||||
}
|
||||
else {
|
||||
$this->set($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function get($key, $default = false) {
|
||||
$key = $this->_translate_key($this->_prefix . $key);
|
||||
if (is_multisite()) {
|
||||
$value = get_network_option($key, $default);
|
||||
}
|
||||
else {
|
||||
$value = get_option($key, $default);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function remove($key) {
|
||||
$key = $this->_translate_key($this->_prefix . $key);
|
||||
if (is_multisite()) {
|
||||
delete_network_option(null, $key);
|
||||
}
|
||||
else {
|
||||
delete_option($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_Style extends Model_Asset {
|
||||
|
||||
public function enqueue() {
|
||||
if ($this->registered) {
|
||||
wp_enqueue_style($this->handle);
|
||||
}
|
||||
else {
|
||||
wp_enqueue_style($this->handle, $this->source, $this->dependencies, $this->version);
|
||||
}
|
||||
}
|
||||
|
||||
public function isEnqueued() {
|
||||
return wp_style_is($this->handle);
|
||||
}
|
||||
|
||||
public function renderInline() {
|
||||
if (empty($this->source))
|
||||
return;
|
||||
$url = esc_attr($this->getSourceUrl());
|
||||
$linkTag = "<link rel=\"stylesheet\" type=\"text/css\" href=\"{$url}\">";
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
jQuery('head').append(<?php echo json_encode($linkTag) ?>);
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function register() {
|
||||
wp_register_style($this->handle, $this->source, $this->dependencies, $this->version);
|
||||
return parent::register();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Text;
|
||||
|
||||
/**
|
||||
* Represents text that is already HTML-safe and should not be encoded again.
|
||||
* @package Wordfence2FA\Text
|
||||
*/
|
||||
class Model_HTML {
|
||||
private $_html;
|
||||
|
||||
public static function esc_html($content) {
|
||||
if (is_object($content) && ($content instanceof Model_HTML)) {
|
||||
return (string) $content;
|
||||
}
|
||||
return esc_html($content);
|
||||
}
|
||||
|
||||
public function __construct($html) {
|
||||
$this->_html = $html;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
return $this->_html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Text;
|
||||
|
||||
/**
|
||||
* Represents text that is already JavaScript-safe and should not be encoded again.
|
||||
* @package Wordfence2FA\Text
|
||||
*/
|
||||
class Model_JavaScript {
|
||||
private $_javaScript;
|
||||
|
||||
/**
|
||||
* Returns a string escaped for use in JavaScript. This is almost identical in behavior to esc_js except that
|
||||
* we don't call _wp_specialchars and keep \r rather than stripping it.
|
||||
*
|
||||
* @param string|Model_JavaScript $content
|
||||
* @return string
|
||||
*/
|
||||
public static function esc_js($content) {
|
||||
if (is_object($content) && ($content instanceof Model_HTML)) {
|
||||
return (string) $content;
|
||||
}
|
||||
|
||||
$safe_text = wp_check_invalid_utf8($content);
|
||||
$safe_text = preg_replace('/&#(x)?0*(?(1)27|39);?/i', "'", stripslashes($safe_text));
|
||||
$safe_text = str_replace("\r", '\\r', $safe_text);
|
||||
$safe_text = str_replace("\n", '\\n', addslashes($safe_text));
|
||||
return apply_filters('js_escape', $safe_text, $content);
|
||||
}
|
||||
|
||||
public static function echo_string_literal($string) {
|
||||
echo "'" . self::esc_js($string) . "'";
|
||||
}
|
||||
|
||||
public function __construct($javaScript) {
|
||||
$this->_javaScript = $javaScript;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
return $this->_javaScript;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_TokenBucket {
|
||||
/* Constants to map from tokens per unit to tokens per second */
|
||||
const MICROSECOND = 0.000001;
|
||||
const MILLISECOND = 0.001;
|
||||
const SECOND = 1;
|
||||
const MINUTE = 60;
|
||||
const HOUR = 3600;
|
||||
const DAY = 86400;
|
||||
const WEEK = 604800;
|
||||
const MONTH = 2629743.83;
|
||||
const YEAR = 31556926;
|
||||
|
||||
const BACKING_REDIS = 'redis';
|
||||
const BACKING_WP_OPTIONS = 'wpoptions';
|
||||
|
||||
private $_identifier;
|
||||
private $_bucketSize;
|
||||
private $_tokensPerSecond;
|
||||
|
||||
private $_backing;
|
||||
private $_redis;
|
||||
|
||||
/**
|
||||
* Model_TokenBucket constructor.
|
||||
*
|
||||
* @param string $identifier The identifier for the bucket record in the database
|
||||
* @param int $bucketSize The maximum capacity of the bucket.
|
||||
* @param double $tokensPerSecond The number of tokens per second added to the bucket.
|
||||
* @param string $backing The backing storage to use.
|
||||
*/
|
||||
public function __construct($identifier, $bucketSize, $tokensPerSecond, $backing = self::BACKING_WP_OPTIONS) {
|
||||
$this->_identifier = $identifier;
|
||||
$this->_bucketSize = $bucketSize;
|
||||
$this->_tokensPerSecond = $tokensPerSecond;
|
||||
$this->_backing = $backing;
|
||||
|
||||
if ($backing == self::BACKING_REDIS) {
|
||||
$this->_redis = new \Redis();
|
||||
$this->_redis->pconnect('127.0.0.1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to acquire a lock for the bucket.
|
||||
*
|
||||
* @param int $timeout
|
||||
* @return bool Whether or not the lock was acquired.
|
||||
*/
|
||||
private function _lock($timeout = 30) {
|
||||
if ($this->_backing == self::BACKING_WP_OPTIONS) {
|
||||
$start = microtime(true);
|
||||
while (!$this->_wp_options_create_lock($this->_identifier)) {
|
||||
if (microtime(true) - $start > $timeout) {
|
||||
return false;
|
||||
}
|
||||
usleep(5000); // 5 ms
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if ($this->_backing == self::BACKING_REDIS) {
|
||||
if ($this->_redis === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
while (!$this->_redis->setnx('lock:' . $this->_identifier, '1')) {
|
||||
if (microtime(true) - $start > $timeout) {
|
||||
return false;
|
||||
}
|
||||
usleep(5000); // 5 ms
|
||||
}
|
||||
$this->_redis->expire('lock:' . $this->_identifier, 30);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function _unlock() {
|
||||
if ($this->_backing == self::BACKING_WP_OPTIONS) {
|
||||
$this->_wp_options_release_lock($this->_identifier);
|
||||
}
|
||||
else if ($this->_backing == self::BACKING_REDIS) {
|
||||
if ($this->_redis === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->_redis->del('lock:' . $this->_identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private function _wp_options_create_lock($name, $timeout = null) { //Our own version of WP_Upgrader::create_lock
|
||||
global $wpdb;
|
||||
|
||||
if (!$timeout) {
|
||||
$timeout = 3600;
|
||||
}
|
||||
|
||||
$lock_option = 'wfls_' . $name . '.lock';
|
||||
$lock_result = $wpdb->query($wpdb->prepare("INSERT IGNORE INTO `{$wpdb->options}` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, 'no')", $lock_option, time()));
|
||||
|
||||
if (!$lock_result) {
|
||||
$lock_result = get_option($lock_option);
|
||||
if (!$lock_result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($lock_result > (time() - $timeout)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->_wp_options_release_lock($name);
|
||||
return $this->_wp_options_create_lock($name, $timeout);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function _wp_options_release_lock($name) {
|
||||
return delete_option('wfls_' . $name . '.lock');
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically checks the available token count, creating the initial record if needed, and updates the available token count if the requested number of tokens is available.
|
||||
*
|
||||
* @param int $tokenCount
|
||||
* @return bool Whether or not there were enough tokens to satisfy the request.
|
||||
*/
|
||||
public function consume($tokenCount = 1) {
|
||||
if (!$this->_lock()) { return false; }
|
||||
|
||||
if ($this->_backing == self::BACKING_WP_OPTIONS) {
|
||||
$record = get_transient('wflsbucket:' . $this->_identifier);
|
||||
}
|
||||
else if ($this->_backing == self::BACKING_REDIS) {
|
||||
$record = $this->_redis->get('bucket:' . $this->_identifier);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($record === false) {
|
||||
if ($tokenCount > $this->_bucketSize) {
|
||||
$this->_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->_bootstrap($this->_bucketSize - $tokenCount);
|
||||
$this->_unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
$tokens = min($this->_secondsToTokens(microtime(true) - (float) $record), $this->_bucketSize);
|
||||
if ($tokenCount > $tokens) {
|
||||
$this->_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->_backing == self::BACKING_WP_OPTIONS) {
|
||||
set_transient('wflsbucket:' . $this->_identifier, (string) (microtime(true) - $this->_tokensToSeconds($tokens - $tokenCount)), ceil($this->_tokensToSeconds($this->_bucketSize)));
|
||||
}
|
||||
else if ($this->_backing == self::BACKING_REDIS) {
|
||||
$this->_redis->set('bucket:' . $this->_identifier, (string) (microtime(true) - $this->_tokensToSeconds($tokens - $tokenCount)));
|
||||
}
|
||||
|
||||
$this->_unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an initial record with the given number of tokens.
|
||||
*
|
||||
* @param int $initialTokens
|
||||
*/
|
||||
protected function _bootstrap($initialTokens) {
|
||||
$microtime = microtime(true) - $this->_tokensToSeconds($initialTokens);
|
||||
if ($this->_backing == self::BACKING_WP_OPTIONS) {
|
||||
set_transient('wflsbucket:' . $this->_identifier, (string) $microtime, ceil($this->_tokensToSeconds($this->_bucketSize)));
|
||||
}
|
||||
else if ($this->_backing == self::BACKING_REDIS) {
|
||||
$this->_redis->set('bucket:' . $this->_identifier, (string) $microtime);
|
||||
}
|
||||
}
|
||||
|
||||
protected function _tokensToSeconds($tokens) {
|
||||
return $tokens / $this->_tokensPerSecond;
|
||||
}
|
||||
|
||||
protected function _secondsToTokens($seconds) {
|
||||
return (int) $seconds * $this->_tokensPerSecond;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_View {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $file_extension = '.php';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Equivalent to the constructor but allows for call chaining.
|
||||
*
|
||||
* @param string $view
|
||||
* @param array $data
|
||||
* @return Model_View
|
||||
*/
|
||||
public static function create($view, $data = array()) {
|
||||
return new self($view, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $view
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct($view, $data = array()) {
|
||||
$this->path = WORDFENCE_LS_PATH . 'views';
|
||||
$this->view = $view;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @throws ViewNotFoundException
|
||||
*/
|
||||
public function render() {
|
||||
$view = preg_replace('/\.{2,}/', '.', $this->view);
|
||||
$path = $this->path . '/' . $view . $this->file_extension;
|
||||
if (!file_exists($path)) {
|
||||
throw new ViewNotFoundException('The view ' . $path . ' does not exist or is not readable.');
|
||||
}
|
||||
|
||||
extract($this->data, EXTR_SKIP);
|
||||
|
||||
ob_start();
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
include $path;
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString() {
|
||||
try {
|
||||
return $this->render();
|
||||
}
|
||||
catch (ViewNotFoundException $e) {
|
||||
return defined('WP_DEBUG') && WP_DEBUG ? $e->getMessage() : 'The view could not be loaded.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @return $this
|
||||
*/
|
||||
public function addData($data) {
|
||||
$this->data = array_merge($data, $this->data);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getData() {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return $this
|
||||
*/
|
||||
public function setData($data) {
|
||||
$this->data = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getView() {
|
||||
return $this->view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $view
|
||||
* @return $this
|
||||
*/
|
||||
public function setView($view) {
|
||||
$this->view = $view;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent POP
|
||||
*/
|
||||
public function __wakeup() {
|
||||
$this->path = WORDFENCE_LS_PATH . 'views';
|
||||
$this->view = null;
|
||||
$this->data = array();
|
||||
$this->file_extension = '.php';
|
||||
}
|
||||
}
|
||||
|
||||
class ViewNotFoundException extends \Exception { }
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\View;
|
||||
|
||||
/**
|
||||
* Represents a tab in the UI.
|
||||
*
|
||||
* @package Wordfence2FA\View
|
||||
* @property string $id
|
||||
* @property string $a
|
||||
* @property string $tabTitle
|
||||
* @property string $pageTitle
|
||||
* @property bool $active
|
||||
*/
|
||||
class Model_Tab {
|
||||
protected $_id;
|
||||
protected $_a;
|
||||
protected $_tabTitle;
|
||||
protected $_pageTitle;
|
||||
protected $_active;
|
||||
|
||||
public function __construct($id, $a, $tabTitle, $pageTitle, $active = false) {
|
||||
$this->_id = $id;
|
||||
$this->_a = $a;
|
||||
$this->_tabTitle = $tabTitle;
|
||||
$this->_pageTitle = $pageTitle;
|
||||
$this->_active = $active;
|
||||
}
|
||||
|
||||
public function __get($name) {
|
||||
switch ($name) {
|
||||
case 'id':
|
||||
return $this->_id;
|
||||
case 'a':
|
||||
return $this->_a;
|
||||
case 'tabTitle':
|
||||
return $this->_tabTitle;
|
||||
case 'pageTitle':
|
||||
return $this->_pageTitle;
|
||||
case 'active':
|
||||
return $this->_active;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Invalid key: ' . $name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\View;
|
||||
|
||||
/**
|
||||
* Class Model_Title
|
||||
* @package Wordfence2FA\Page
|
||||
* @var string $id A valid DOM ID for the title.
|
||||
* @var string|Model_HTML $title The title text or HTML.
|
||||
* @var string $helpURL The help URL.
|
||||
* @var string|Model_HTML $helpLink The text/HTML of the help link.
|
||||
*/
|
||||
class Model_Title {
|
||||
private $_id;
|
||||
private $_title;
|
||||
private $_helpURL;
|
||||
private $_helpLink;
|
||||
|
||||
public function __construct($id, $title, $helpURL = null, $helpLink = null) {
|
||||
$this->_id = $id;
|
||||
$this->_title = $title;
|
||||
$this->_helpURL = $helpURL;
|
||||
$this->_helpLink = $helpLink;
|
||||
}
|
||||
|
||||
public function __get($name) {
|
||||
switch ($name) {
|
||||
case 'id':
|
||||
return $this->_id;
|
||||
case 'title':
|
||||
return $this->_title;
|
||||
case 'helpURL':
|
||||
return $this->_helpURL;
|
||||
case 'helpLink':
|
||||
return $this->_helpLink;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Invalid key: ' . $name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Utility_Array {
|
||||
|
||||
public static function findOffset($array, $key) {
|
||||
$offset = 0;
|
||||
foreach ($array as $index => $value) {
|
||||
if ($index === $key)
|
||||
return $offset;
|
||||
$offset++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function insertAfter(&$array, $targetKey, $key, $value) {
|
||||
$offset = self::findOffset($array, $targetKey);
|
||||
if ($offset === null)
|
||||
return false;
|
||||
$array = array_merge(
|
||||
array_slice($array, 0, $offset + 1),
|
||||
array( $key => $value ),
|
||||
array_slice($array, $offset + 1)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
use WordfenceLS\Crypto\Model_Base2n;
|
||||
|
||||
class Utility_BaseConversion {
|
||||
|
||||
public static function get_base32() {
|
||||
static $base32 = null;
|
||||
if ($base32 === null)
|
||||
$base32 = new Model_Base2n(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', false, true, true);
|
||||
return $base32;
|
||||
}
|
||||
|
||||
public static function base32_encode($data) {
|
||||
$base32 = self::get_base32();
|
||||
return $base32->encode($data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class Utility_DatabaseLock implements Utility_Lock {
|
||||
|
||||
const DEFAULT_TIMEOUT = 30;
|
||||
const MAX_TIMEOUT = 120;
|
||||
|
||||
private $wpdb;
|
||||
private $table;
|
||||
private $key;
|
||||
private $timeout;
|
||||
private $expirationTimestamp;
|
||||
|
||||
public function __construct($dbController, $key, $timeout = null) {
|
||||
$this->wpdb = $dbController->get_wpdb();
|
||||
$this->table = $dbController->settings;
|
||||
$this->key = "lock:{$key}";
|
||||
$this->timeout = self::resolveTimeout($timeout);
|
||||
}
|
||||
|
||||
private static function resolveTimeout($timeout) {
|
||||
if ($timeout === null)
|
||||
$timeout = ini_get('max_execution_time');
|
||||
$timeout = (int) $timeout;
|
||||
if ($timeout <= 0 || $timeout > self::MAX_TIMEOUT)
|
||||
return self::DEFAULT_TIMEOUT;
|
||||
return $timeout;
|
||||
}
|
||||
|
||||
private function clearExpired($timestamp) {
|
||||
$this->wpdb->query($this->wpdb->prepare(<<<SQL
|
||||
DELETE
|
||||
FROM {$this->table}
|
||||
WHERE
|
||||
name = %s
|
||||
AND value < %d
|
||||
SQL
|
||||
, $this->key, $timestamp));
|
||||
}
|
||||
|
||||
private function insert($expirationTimestamp) {
|
||||
$result = $this->wpdb->query($this->wpdb->prepare(<<<SQL
|
||||
INSERT IGNORE
|
||||
INTO {$this->table}
|
||||
(name, value, autoload)
|
||||
VALUES(%s, %d, 'no')
|
||||
SQL
|
||||
, $this->key, $expirationTimestamp));
|
||||
return $result === 1;
|
||||
}
|
||||
|
||||
public function acquire($delay = self::DEFAULT_DELAY) {
|
||||
$attempts = (int) ($this->timeout * 1000000 / $delay);
|
||||
for (; $attempts > 0; $attempts--) {
|
||||
$timestamp = time();
|
||||
$this->clearExpired($timestamp);
|
||||
$expirationTimestamp = $timestamp + $this->timeout;
|
||||
$locked = $this->insert($expirationTimestamp);
|
||||
if ($locked) {
|
||||
$this->expirationTimestamp = $expirationTimestamp;
|
||||
return;
|
||||
}
|
||||
usleep($delay);
|
||||
}
|
||||
throw new RuntimeException("Failed to acquire lock {$this->key}");
|
||||
}
|
||||
|
||||
private function delete($expirationTimestamp) {
|
||||
$this->wpdb->delete(
|
||||
$this->table,
|
||||
array (
|
||||
'name' => $this->key,
|
||||
'value' => $expirationTimestamp
|
||||
),
|
||||
array (
|
||||
'%s',
|
||||
'%d'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function release() {
|
||||
if ($this->expirationTimestamp === null)
|
||||
return;
|
||||
$this->delete($this->expirationTimestamp);
|
||||
$this->expirationTimestamp = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
interface Utility_Lock {
|
||||
|
||||
const DEFAULT_DELAY = 100000;
|
||||
|
||||
public function acquire($delay = self::DEFAULT_DELAY);
|
||||
|
||||
public function release();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
/**
|
||||
* An implementation of the Utility_Lock that doesn't actually do any locking
|
||||
*/
|
||||
class Utility_NullLock implements Utility_Lock {
|
||||
|
||||
public function acquire($delay = self::DEFAULT_DELAY) {
|
||||
//Do nothing
|
||||
}
|
||||
|
||||
public function release() {
|
||||
//Do nothing
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Utility_Number {
|
||||
|
||||
public static function isInteger($value, $min = null, $max = null) {
|
||||
$options = array();
|
||||
if ($min !== null)
|
||||
$options['min_range'] = $min;
|
||||
if ($max !== null)
|
||||
$options['max_range'] = $max;
|
||||
return filter_var($value, FILTER_VALIDATE_INT, array('options' => $options)) !== false;
|
||||
}
|
||||
|
||||
public static function isUnixTimestamp($value) {
|
||||
return self::isInteger($value, 0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class Utility_Serialization {
|
||||
|
||||
public static function unserialize($data, $options = array(), $validator = null) {
|
||||
static $serializedFalse;
|
||||
if ($serializedFalse === null)
|
||||
$serializedFalse = serialize(false);
|
||||
if ($data === $serializedFalse)
|
||||
return false;
|
||||
if (!is_serialized($data))
|
||||
throw new RuntimeException('Input data is not serialized');
|
||||
if (version_compare(PHP_VERSION, '5.6', '<=')) {
|
||||
$unserialized = @unserialize($data);
|
||||
}
|
||||
else {
|
||||
$unserialized = @unserialize($data, $options);
|
||||
}
|
||||
if ($unserialized === false)
|
||||
throw new RuntimeException('Deserialization failed');
|
||||
if ($validator !== null && !$validator($unserialized))
|
||||
throw new RuntimeException('Validation of unserialized data failed');
|
||||
return $unserialized;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user