first commit

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

View File

@@ -0,0 +1,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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>';
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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]);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 { }

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace WordfenceLS;
interface Utility_Lock {
const DEFAULT_DELAY = 100000;
public function acquire($delay = self::DEFAULT_DELAY);
public function release();
}

View File

@@ -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
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}