first commit
@@ -0,0 +1,16 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_URI} \.php$
|
||||
RewriteRule .* - [F,L,NC]
|
||||
</IfModule>
|
||||
<IfModule !mod_rewrite.c>
|
||||
<FilesMatch "\.php$">
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
@@ -0,0 +1,558 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
use WordfenceLS\Crypto\Model_JWT;
|
||||
use WordfenceLS\Crypto\Model_Symmetric;
|
||||
|
||||
class Controller_AJAX {
|
||||
|
||||
const MAX_USERS_TO_NOTIFY = 100;
|
||||
|
||||
protected $_actions = null; //Populated on init
|
||||
|
||||
/**
|
||||
* Returns the singleton Controller_AJAX.
|
||||
*
|
||||
* @return Controller_AJAX
|
||||
*/
|
||||
public static function shared() {
|
||||
static $_shared = null;
|
||||
if ($_shared === null) {
|
||||
$_shared = new Controller_AJAX();
|
||||
}
|
||||
return $_shared;
|
||||
}
|
||||
|
||||
public function init() {
|
||||
$this->_actions = array(
|
||||
'authenticate' => array(
|
||||
'handler' => array($this, '_ajax_authenticate_callback'),
|
||||
'nopriv' => true,
|
||||
'nonce' => false,
|
||||
'permissions' => array(), //Format is 'permission' => 'error message'
|
||||
'required_parameters' => array(),
|
||||
),
|
||||
'register_support' => array(
|
||||
'handler' => array($this, '_ajax_register_support_callback'),
|
||||
'nopriv' => true,
|
||||
'nonce' => false,
|
||||
'permissions' => array(),
|
||||
'required_parameters' => array('wfls-message-nonce', 'wfls-message'),
|
||||
),
|
||||
'activate' => array(
|
||||
'handler' => array($this, '_ajax_activate_callback'),
|
||||
'permissions' => array(),
|
||||
'required_parameters' => array('nonce', 'secret', 'recovery', 'code', 'user'),
|
||||
),
|
||||
'deactivate' => array(
|
||||
'handler' => array($this, '_ajax_deactivate_callback'),
|
||||
'permissions' => array(),
|
||||
'required_parameters' => array('nonce', 'user'),
|
||||
),
|
||||
'regenerate' => array(
|
||||
'handler' => array($this, '_ajax_regenerate_callback'),
|
||||
'permissions' => array(),
|
||||
'required_parameters' => array('nonce', 'user'),
|
||||
),
|
||||
'save_options' => array(
|
||||
'handler' => array($this, '_ajax_save_options_callback'),
|
||||
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to change options.', 'wordfence-2fa')),
|
||||
'required_parameters' => array('nonce', 'changes'),
|
||||
),
|
||||
'send_grace_period_notification' => array(
|
||||
'handler' => array($this, '_ajax_send_grace_period_notification_callback'),
|
||||
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to send notifications.', 'wordfence-2fa')),
|
||||
'required_parameters' => array('nonce', 'role', 'url'),
|
||||
),
|
||||
'update_ip_preview' => array(
|
||||
'handler' => array($this, '_ajax_update_ip_preview_callback'),
|
||||
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to change options.', 'wordfence-2fa')),
|
||||
'required_parameters' => array('nonce', 'ip_source', 'ip_source_trusted_proxies'),
|
||||
),
|
||||
'dismiss_notice' => array(
|
||||
'handler' => array($this, '_ajax_dismiss_notice_callback'),
|
||||
'permissions' => array(),
|
||||
'required_parameters' => array('nonce', 'id'),
|
||||
),
|
||||
'reset_recaptcha_stats' => array(
|
||||
'handler' => array($this, '_ajax_reset_recaptcha_stats_callback'),
|
||||
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to reset reCAPTCHA statistics.', 'wordfence-2fa')),
|
||||
'required_parameters' => array('nonce'),
|
||||
),
|
||||
'reset_2fa_grace_period' => array (
|
||||
'handler' => array($this, '_ajax_reset_2fa_grace_period_callback'),
|
||||
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to reset the 2FA grace period.', 'wordfence-2fa')),
|
||||
'required_parameters' => array('nonce', 'user_id')
|
||||
),
|
||||
'revoke_2fa_grace_period' => array (
|
||||
'handler' => array($this, '_ajax_revoke_2fa_grace_period_callback'),
|
||||
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to revoke the 2FA grace period.', 'wordfence-2fa')),
|
||||
'required_parameters' => array('nonce', 'user_id')
|
||||
),
|
||||
'reset_ntp_failure_count' => array(
|
||||
'handler' => array($this, '_ajax_reset_ntp_failure_count_callback'),
|
||||
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to reset the NTP failure count.', 'wordfence-2fa')),
|
||||
'required_parameters' => array(),
|
||||
),
|
||||
'disable_ntp' => array(
|
||||
'handler' => array($this, '_ajax_disable_ntp_callback'),
|
||||
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to disable NTP.', 'wordfence-2fa')),
|
||||
'required_parameters' => array(),
|
||||
),
|
||||
'dismiss_persistent_notice' => array(
|
||||
'handler' => array($this, '_ajax_dismiss_persistent_notice_callback'),
|
||||
'permissions' => array(Controller_Permissions::CAP_MANAGE_SETTINGS => __('You do not have permission to dismiss this notice.', 'wordfence-2fa')),
|
||||
'required_parameters' => array('nonce', 'notice_id')
|
||||
)
|
||||
);
|
||||
|
||||
$this->_init_actions();
|
||||
}
|
||||
|
||||
public function _init_actions() {
|
||||
foreach ($this->_actions as $action => $parameters) {
|
||||
if (isset($parameters['nopriv']) && $parameters['nopriv']) {
|
||||
add_action('wp_ajax_nopriv_wordfence_ls_' . $action, array($this, '_ajax_handler'));
|
||||
}
|
||||
add_action('wp_ajax_wordfence_ls_' . $action, array($this, '_ajax_handler'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a convenience function for sending a JSON response and ensuring that execution stops after sending
|
||||
* since wp_die() can be interrupted.
|
||||
*
|
||||
* @param $response
|
||||
* @param int|null $status_code
|
||||
*/
|
||||
public static function send_json($response, $status_code = null) {
|
||||
wp_send_json($response, $status_code);
|
||||
die();
|
||||
}
|
||||
|
||||
public function _ajax_handler() {
|
||||
$action = (isset($_POST['action']) && is_string($_POST['action']) && $_POST['action']) ? $_POST['action'] : $_GET['action'];
|
||||
if (preg_match('~wordfence_ls_([a-zA-Z_0-9]+)$~', $action, $matches)) {
|
||||
$action = $matches[1];
|
||||
if (!isset($this->_actions[$action])) {
|
||||
self::send_json(array('error' => esc_html__('An unknown action was provided.', 'wordfence-2fa')));
|
||||
}
|
||||
|
||||
$parameters = $this->_actions[$action];
|
||||
if (!empty($parameters['required_parameters'])) {
|
||||
foreach ($parameters['required_parameters'] as $k) {
|
||||
if (!isset($_POST[$k])) {
|
||||
self::send_json(array('error' => esc_html__('An expected parameter was not provided.', 'wordfence-2fa')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($parameters['nonce']) || $parameters['nonce']) {
|
||||
$nonce = (isset($_POST['nonce']) && is_string($_POST['nonce']) && $_POST['nonce']) ? $_POST['nonce'] : $_GET['nonce'];
|
||||
if (!is_string($nonce) || !wp_verify_nonce($nonce, 'wp-ajax')) {
|
||||
self::send_json(array('error' => esc_html__('Your browser sent an invalid security token. Please try reloading this page.', 'wordfence-2fa'), 'tokenInvalid' => 1));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($parameters['permissions'])) {
|
||||
$user = wp_get_current_user();
|
||||
foreach ($parameters['permissions'] as $permission => $error) {
|
||||
if (!user_can($user, $permission)) {
|
||||
self::send_json(array('error' => $error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
call_user_func($parameters['handler']);
|
||||
}
|
||||
}
|
||||
|
||||
public function _ajax_authenticate_callback() {
|
||||
$credentialKeys = array(
|
||||
'log' => 'pwd',
|
||||
'username' => 'password'
|
||||
);
|
||||
$username = null;
|
||||
$password = null;
|
||||
foreach ($credentialKeys as $usernameKey => $passwordKey) {
|
||||
if (array_key_exists($usernameKey, $_POST) && array_key_exists($passwordKey, $_POST) && is_string($_POST[$usernameKey]) && is_string($_POST[$passwordKey])) {
|
||||
$username = $_POST[$usernameKey];
|
||||
$password = $_POST[$passwordKey];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (empty($username) || empty($password)) {
|
||||
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: A username and password must be provided. <a href="%s" title="Password Lost and Found">Lost your password</a>?', 'wordfence-2fa'), wp_lostpassword_url()), array('strong'=>array(), 'a'=>array('href'=>array(), 'title'=>array())))));
|
||||
}
|
||||
|
||||
$legacy2FAActive = Controller_WordfenceLS::shared()->legacy_2fa_active();
|
||||
if ($legacy2FAActive) { //Legacy 2FA is active, pass it on to the authenticate filter
|
||||
self::send_json(array('login' => 1));
|
||||
}
|
||||
|
||||
do_action_ref_array('wp_authenticate', array(&$username, &$password));
|
||||
|
||||
define('WORDFENCE_LS_AUTHENTICATION_CHECK', true); //Prevents our auth filter from recursing
|
||||
$user = wp_authenticate($username, $password);
|
||||
if (is_object($user) && ($user instanceof \WP_User)) {
|
||||
if (!Controller_Users::shared()->has_2fa_active($user) || Controller_Whitelist::shared()->is_whitelisted(Model_Request::current()->ip()) || Controller_Users::shared()->has_remembered_2fa($user) || defined('WORDFENCE_LS_COMBINED_IS_VALID')) { //Not enabled for this user, is whitelisted, has a valid remembered cookie, or has already provided a 2FA code via the password field pass the credentials on to the normal login flow
|
||||
self::send_json(array('login' => 1));
|
||||
}
|
||||
self::send_json(array('login' => 1, 'two_factor_required' => true));
|
||||
}
|
||||
else if (is_wp_error($user)) {
|
||||
$errors = array();
|
||||
$messages = array();
|
||||
$reset = false;
|
||||
foreach ($user->get_error_codes() as $code) {
|
||||
if ($code == 'invalid_username' || $code == 'invalid_email' || $code == 'incorrect_password' || $code == 'authentication_failed') {
|
||||
$errors[] = wp_kses(sprintf(__('<strong>ERROR</strong>: The username or password you entered is incorrect. <a href="%s" title="Password Lost and Found">Lost your password</a>?', 'wordfence-2fa'), wp_lostpassword_url()), array('strong'=>array(), 'a'=>array('href'=>array(), 'title'=>array())));
|
||||
}
|
||||
else {
|
||||
if ($code == 'wfls_twofactor_invalid') {
|
||||
$reset = true;
|
||||
}
|
||||
|
||||
$severity = $user->get_error_data($code);
|
||||
foreach ($user->get_error_messages($code) as $error_message) {
|
||||
if ($severity == 'message') {
|
||||
$messages[] = $error_message;
|
||||
}
|
||||
else {
|
||||
$errors[] = $error_message;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
$errors = implode('<br>', $errors);
|
||||
$errors = apply_filters('login_errors', $errors);
|
||||
self::send_json(array('error' => $errors, 'reset' => $reset));
|
||||
}
|
||||
|
||||
if (!empty($messages)) {
|
||||
$messages = implode('<br>', $messages);
|
||||
$messages = apply_filters('login_errors', $messages);
|
||||
self::send_json(array('message' => $messages, 'reset' => $reset));
|
||||
}
|
||||
}
|
||||
|
||||
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: The username or password you entered is incorrect. <a href="%s" title="Password Lost and Found">Lost your password</a>?', 'wordfence-2fa'), wp_lostpassword_url()), array('strong'=>array(), 'a'=>array('href'=>array(), 'title'=>array())))));
|
||||
}
|
||||
|
||||
public function _ajax_register_support_callback() {
|
||||
$email = null;
|
||||
if (array_key_exists('email', $_POST) && is_string($_POST['email'])) {
|
||||
$email = $_POST['email'];
|
||||
}
|
||||
else if (array_key_exists('user_email', $_POST) && is_string($_POST['user_email'])) {
|
||||
$email = $_POST['user_email'];
|
||||
}
|
||||
if (
|
||||
$email === null ||
|
||||
!isset($_POST['wfls-message']) || !is_string($_POST['wfls-message']) ||
|
||||
!isset($_POST['wfls-message-nonce']) || !is_string($_POST['wfls-message-nonce'])) {
|
||||
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: Unable to send message. Please refresh the page and try again.', 'wordfence-2fa')), array('strong'=>array()))));
|
||||
}
|
||||
|
||||
$email = sanitize_email($email);
|
||||
$login = '';
|
||||
if (array_key_exists('user_login', $_POST) && is_string($_POST['user_login']))
|
||||
$login = sanitize_user($_POST['user_login']);
|
||||
$message = strip_tags($_POST['wfls-message']);
|
||||
$nonce = $_POST['wfls-message-nonce'];
|
||||
|
||||
if ((isset($_POST['user_login']) && empty($login)) || empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL) || empty($message)) {
|
||||
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: Unable to send message. Please refresh the page and try again.', 'wordfence-2fa')), array('strong'=>array()))));
|
||||
}
|
||||
|
||||
$jwt = Model_JWT::decode_jwt($_POST['wfls-message-nonce']);
|
||||
if ($jwt && isset($jwt->payload['ip']) && isset($jwt->payload['score'])) {
|
||||
$decryptedIP = Model_Symmetric::decrypt($jwt->payload['ip']);
|
||||
$decryptedScore = Model_Symmetric::decrypt($jwt->payload['score']);
|
||||
if ($decryptedIP === false || $decryptedScore === false || Model_IP::inet_pton($decryptedIP) !== Model_IP::inet_pton(Model_Request::current()->ip())) { //JWT IP and the current request's IP don't match, refuse the message
|
||||
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: Unable to send message. Please refresh the page and try again.', 'wordfence-2fa')), array('strong'=>array()))));
|
||||
}
|
||||
|
||||
$identifier = bin2hex(Model_IP::inet_pton($decryptedIP));
|
||||
$tokenBucket = new Model_TokenBucket('rate:' . $identifier, 2, 1 / (6 * Model_TokenBucket::HOUR)); //Maximum of two requests, refilling at a rate of one per six hours
|
||||
if (!$tokenBucket->consume(1)) {
|
||||
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: Unable to send message. You have exceeded the maximum number of messages that may be sent at this time. Please try again later.', 'wordfence-2fa')), array('strong'=>array()))));
|
||||
}
|
||||
|
||||
$email = array(
|
||||
'to' => get_site_option('admin_email'),
|
||||
'subject' => __('Blocked User Registration Contact Form', 'wordfence-2fa'),
|
||||
'body' => sprintf(__("A visitor blocked from registration sent the following message.\n\n----------------------------------------\n\nIP: %s\nUsername: %s\nEmail: %s\nreCAPTCHA Score: %f\n\n----------------------------------------\n\n%s", 'wordfence-2fa'), $decryptedIP, $login, $email, $decryptedScore, $message),
|
||||
'headers' => '',
|
||||
);
|
||||
$success = wp_mail($email['to'], $email['subject'], $email['body'], $email['headers']);
|
||||
if ($success) {
|
||||
self::send_json(array('message' => wp_kses(sprintf(__('<strong>MESSAGE SENT</strong>: Your message was sent to the site owner.', 'wordfence-2fa')), array('strong'=>array()))));
|
||||
}
|
||||
|
||||
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: An error occurred while sending the message. Please try again.', 'wordfence-2fa')), array('strong'=>array()))));
|
||||
}
|
||||
|
||||
self::send_json(array('error' => wp_kses(sprintf(__('<strong>ERROR</strong>: Unable to send message. Please refresh the page and try again.', 'wordfence-2fa')), array('strong'=>array()))));
|
||||
}
|
||||
|
||||
public function _ajax_activate_callback() {
|
||||
$userID = (int) @$_POST['user'];
|
||||
$user = wp_get_current_user();
|
||||
if ($user->ID != $userID) {
|
||||
if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_OTHERS)) {
|
||||
self::send_json(array('error' => esc_html__('You do not have permission to activate the given user.', 'wordfence-2fa')));
|
||||
}
|
||||
else {
|
||||
$user = new \WP_User($userID);
|
||||
if (!$user->exists()) {
|
||||
self::send_json(array('error' => esc_html__('The given user does not exist.', 'wordfence-2fa')));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_SELF)) {
|
||||
self::send_json(array('error' => esc_html__('You do not have permission to activate 2FA.', 'wordfence-2fa')));
|
||||
}
|
||||
|
||||
if (Controller_Users::shared()->has_2fa_active($user)) {
|
||||
self::send_json(array('error' => esc_html__('The given user already has two-factor authentication active.', 'wordfence-2fa')));
|
||||
}
|
||||
|
||||
$matches = (isset($_POST['secret']) && isset($_POST['code']) && is_string($_POST['secret']) && is_string($_POST['code']) && Controller_TOTP::shared()->check_code($_POST['secret'], $_POST['code']));
|
||||
if ($matches === false) {
|
||||
self::send_json(array('error' => esc_html__('The code provided does not match the expected value. Please verify that the time on your authenticator device is correct and that this server\'s time is correct.', 'wordfence-2fa')));
|
||||
}
|
||||
|
||||
Controller_TOTP::shared()->activate_2fa($user, $_POST['secret'], $_POST['recovery'], $matches);
|
||||
Controller_Notices::shared()->remove_notice(false, 'wfls-will-be-required', $user);
|
||||
self::send_json(array('activated' => 1, 'text' => sprintf(count($_POST['recovery']) == 1 ? esc_html__('%d unused recovery code remains. You may generate a new set by clicking below.', 'wordfence-2fa') : esc_html__('%d unused recovery codes remain. You may generate a new set by clicking below.', 'wordfence-2fa'), count($_POST['recovery']))));
|
||||
}
|
||||
|
||||
public function _ajax_deactivate_callback() {
|
||||
$userID = (int) @$_POST['user'];
|
||||
$user = wp_get_current_user();
|
||||
if ($user->ID != $userID) {
|
||||
if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_OTHERS)) {
|
||||
self::send_json(array('error' => esc_html__('You do not have permission to deactivate the given user.', 'wordfence-2fa')));
|
||||
}
|
||||
else {
|
||||
$user = new \WP_User($userID);
|
||||
if (!$user->exists()) {
|
||||
self::send_json(array('error' => esc_html__('The user does not exist.', 'wordfence-2fa')));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_SELF)) {
|
||||
self::send_json(array('error' => esc_html__('You do not have permission to deactivate 2FA.', 'wordfence-2fa')));
|
||||
}
|
||||
|
||||
if (!Controller_Users::shared()->has_2fa_active($user)) {
|
||||
self::send_json(array('error' => esc_html__('The user specified does not have two-factor authentication active.', 'wordfence-2fa')));
|
||||
}
|
||||
|
||||
Controller_Users::shared()->deactivate_2fa($user);
|
||||
self::send_json(array('deactivated' => 1));
|
||||
}
|
||||
|
||||
public function _ajax_regenerate_callback() {
|
||||
$userID = (int) @$_POST['user'];
|
||||
$user = wp_get_current_user();
|
||||
if ($user->ID != $userID) {
|
||||
if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_OTHERS)) {
|
||||
self::send_json(array('error' => esc_html__('You do not have permission to generate new recovery codes for the given user.', 'wordfence-2fa')));
|
||||
}
|
||||
else {
|
||||
$user = new \WP_User($userID);
|
||||
if (!$user->exists()) {
|
||||
self::send_json(array('error' => esc_html__('The user does not exist.', 'wordfence-2fa')));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!user_can($user, Controller_Permissions::CAP_ACTIVATE_2FA_SELF)) {
|
||||
self::send_json(array('error' => esc_html__('You do not have permission to generate new recovery codes.', 'wordfence-2fa')));
|
||||
}
|
||||
|
||||
if (!Controller_Users::shared()->has_2fa_active($user)) {
|
||||
self::send_json(array('error' => esc_html__('The user specified does not have two-factor authentication active.', 'wordfence-2fa')));
|
||||
}
|
||||
|
||||
$codes = Controller_Users::shared()->regenerate_recovery_codes($user);
|
||||
self::send_json(array('regenerated' => 1, 'recovery' => array_map(function($r) { return implode(' ', str_split(bin2hex($r), 4)); }, $codes), 'text' => sprintf(count($codes) == 1 ? esc_html__('%d unused recovery code remains. You may generate a new set by clicking below.', 'wordfence-2fa') : esc_html__('%d unused recovery codes remain. You may generate a new set by clicking below.', 'wordfence-2fa'), count($codes))));
|
||||
}
|
||||
|
||||
public function _ajax_save_options_callback() {
|
||||
if (!empty($_POST['changes']) && is_string($_POST['changes']) && is_array($changes = json_decode(stripslashes($_POST['changes']), true))) {
|
||||
try {
|
||||
$errors = Controller_Settings::shared()->validate_multiple($changes);
|
||||
if ($errors !== true) {
|
||||
if (count($errors) == 1) {
|
||||
$e = array_shift($errors);
|
||||
self::send_json(array('error' => esc_html(sprintf(__('An error occurred while saving the configuration: %s', 'wordfence-2fa'), $e))));
|
||||
}
|
||||
else if (count($errors) > 1) {
|
||||
$compoundMessage = array();
|
||||
foreach ($errors as $e) {
|
||||
$compoundMessage[] = esc_html($e);
|
||||
}
|
||||
self::send_json(array(
|
||||
'error' => wp_kses(sprintf(__('Errors occurred while saving the configuration: %s', 'wordfence-2fa'), '<ul><li>' . implode('</li><li>', $compoundMessage) . '</li></ul>'), array('ul'=>array(), 'li'=>array())),
|
||||
'html' => true,
|
||||
));
|
||||
}
|
||||
|
||||
self::send_json(array(
|
||||
'error' => esc_html__('Errors occurred while saving the configuration.', 'wordfence-2fa'),
|
||||
));
|
||||
}
|
||||
|
||||
Controller_Settings::shared()->set_multiple($changes);
|
||||
|
||||
if (array_key_exists(Controller_Settings::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION, $changes) || array_key_exists(Controller_Settings::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION, $changes))
|
||||
Controller_WordfenceLS::shared()->refresh_rewrite_rules();
|
||||
|
||||
$response = array('success' => true);
|
||||
return self::send_json($response);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
self::send_json(array(
|
||||
'error' => $e->getMessage(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self::send_json(array(
|
||||
'error' => esc_html__('No configuration changes were provided to save.', 'wordfence-2fa'),
|
||||
));
|
||||
}
|
||||
|
||||
public function _ajax_send_grace_period_notification_callback() {
|
||||
$notifyAll = isset($_POST['notify_all']);
|
||||
$users = Controller_Users::shared()->get_users_by_role($_POST['role'], $notifyAll ? null: self::MAX_USERS_TO_NOTIFY + 1);
|
||||
$url = $_POST['url'];
|
||||
if (!empty($url)) {
|
||||
$url = get_site_url(null, $url);
|
||||
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
|
||||
self::send_json(array('error' => esc_html__('The specified URL is invalid.', 'wordfence-2fa')));
|
||||
}
|
||||
}
|
||||
$userCount = count($users);
|
||||
if (!$notifyAll && $userCount > self::MAX_USERS_TO_NOTIFY)
|
||||
self::send_json(array('error' => esc_html(sprintf(__('More than %d users exist for the selected role. This notification is not designed to handle large groups of users. In such instances, using a different solution for notifying users of upcoming 2FA requirements is recommended.', 'wordfence-2fa'), self::MAX_USERS_TO_NOTIFY)), 'limit_exceeded' => true));
|
||||
$sent = 0;
|
||||
foreach ($users as $user) {
|
||||
Controller_Users::shared()->requires_2fa($user, $inGracePeriod, $requiredAt);
|
||||
if ($inGracePeriod && !Controller_Users::shared()->has_2fa_active($user)) {
|
||||
$subject = sprintf(__('2FA will soon be required on %s', 'wordfence-2fa'), home_url());
|
||||
$requiredDate = Controller_Time::format_local_time('F j, Y g:i A', $requiredAt);
|
||||
if (empty($url)) {
|
||||
$userUrl = (is_multisite() && is_super_admin($user->ID)) ? network_admin_url('admin.php?page=WFLS') : admin_url('admin.php?page=WFLS');
|
||||
}
|
||||
else {
|
||||
$userUrl = $url;
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
__("<html><body><p>You do not currently have two-factor authentication active on your account, which will be required beginning %s.</p><p><a href=\"%s\">Configure 2FA</a></p></body></html>", 'wordfence-2fa'),
|
||||
$requiredDate,
|
||||
htmlentities($userUrl)
|
||||
);
|
||||
|
||||
wp_mail($user->user_email, $subject, $message, array('Content-Type: text/html'));
|
||||
$sent++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($userCount == 0) {
|
||||
self::send_json(array('error' => esc_html__('No users currently exist with the selected role.', 'wordfence-2fa')));
|
||||
}
|
||||
else if ($sent == 0) {
|
||||
self::send_json(array('confirmation' => esc_html__('All users with the selected role already have two-factor authentication activated or have been locked out.', 'wordfence-2fa')));
|
||||
}
|
||||
else if ($sent == 1) {
|
||||
self::send_json(array('confirmation' => esc_html(sprintf(__('A reminder to activate two-factor authentication was sent to %d user.', 'wordfence-2fa'), $sent))));
|
||||
}
|
||||
self::send_json(array('confirmation' => esc_html(sprintf(__('A reminder to activate two-factor authentication was sent to %d users.', 'wordfence-2fa'), $sent))));
|
||||
}
|
||||
|
||||
public function _ajax_update_ip_preview_callback() {
|
||||
$source = $_POST['ip_source'];
|
||||
$raw_proxies = $_POST['ip_source_trusted_proxies'];
|
||||
if (!is_string($source) || !is_string($raw_proxies)) {
|
||||
die();
|
||||
}
|
||||
|
||||
$valid = array();
|
||||
$invalid = array();
|
||||
$test = preg_split('/[\r\n,]+/', $raw_proxies);
|
||||
foreach ($test as $value) {
|
||||
if (strlen($value) > 0) {
|
||||
if (Model_IP::is_valid_ip($value) || Model_IP::is_valid_cidr_range($value)) {
|
||||
$valid[] = $value;
|
||||
}
|
||||
else {
|
||||
$invalid[] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
$trusted_proxies = $valid;
|
||||
|
||||
$preview = Model_Request::current()->detected_ip_preview($source, $trusted_proxies);
|
||||
$ip = Model_Request::current()->ip_for_field($source, $trusted_proxies);
|
||||
self::send_json(array('ip' => $ip[0], 'preview' => $preview));
|
||||
}
|
||||
|
||||
public function _ajax_dismiss_notice_callback() {
|
||||
Controller_Notices::shared()->remove_notice($_POST['id'], false, wp_get_current_user());
|
||||
}
|
||||
|
||||
public function _ajax_reset_recaptcha_stats_callback() {
|
||||
Controller_Settings::shared()->set_array(Controller_Settings::OPTION_CAPTCHA_STATS, array('counts' => array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 'avg' => 0));
|
||||
$response = array('success' => true);
|
||||
self::send_json($response);
|
||||
}
|
||||
|
||||
public function _ajax_reset_2fa_grace_period_callback() {
|
||||
$userId = (int) $_POST['user_id'];
|
||||
$gracePeriodOverride = array_key_exists('grace_period_override', $_POST) ? (int) $_POST['grace_period_override'] : null;
|
||||
$user = get_userdata($userId);
|
||||
if ($user === false)
|
||||
self::send_json(array('error' => esc_html__('Invalid user specified', 'wordfence-2fa')));
|
||||
if ($gracePeriodOverride < 0 || $gracePeriodOverride > Controller_Settings::MAX_REQUIRE_2FA_USER_GRACE_PERIOD)
|
||||
self::send_json(array('error' => esc_html__('Invalid grace period override', 'wordfence-2fa')));
|
||||
$gracePeriodAllowed = Controller_Users::shared()->get_grace_period_allowed_flag($userId);
|
||||
if (!$gracePeriodAllowed)
|
||||
Controller_Users::shared()->allow_grace_period($userId);
|
||||
if (!Controller_Users::shared()->reset_2fa_grace_period($user, $gracePeriodOverride))
|
||||
self::send_json(array('error' => esc_html__('Failed to reset grace period', 'wordfence-2fa')));
|
||||
self::send_json(array('success' => true));
|
||||
}
|
||||
|
||||
public function _ajax_revoke_2fa_grace_period_callback() {
|
||||
$user = get_userdata((int) $_POST['user_id']);
|
||||
if ($user === false)
|
||||
self::send_json(array('error' => esc_html__('Invalid user specified', 'wordfence-2fa')));
|
||||
Controller_Users::shared()->revoke_grace_period($user);
|
||||
self::send_json(array('success' => true));
|
||||
}
|
||||
|
||||
public function _ajax_reset_ntp_failure_count_callback() {
|
||||
Controller_Settings::shared()->reset_ntp_failure_count();
|
||||
}
|
||||
|
||||
public function _ajax_disable_ntp_callback() {
|
||||
Controller_Settings::shared()->disable_ntp_cron();
|
||||
}
|
||||
|
||||
public function _ajax_dismiss_persistent_notice_callback() {
|
||||
$userId = get_current_user_id();
|
||||
$noticeId = $_POST['notice_id'];
|
||||
if ($userId !== 0 && Controller_Notices::shared()->dismiss_persistent_notice($userId, $noticeId))
|
||||
self::send_json(array('success' => true));
|
||||
self::send_json(array(
|
||||
'error' => esc_html__('Unable to dismiss notice', 'wordfence-2fa')
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Controller_CAPTCHA {
|
||||
const RESPONSE_MODE_ALLOW = 'allow';
|
||||
const RESPONSE_MODE_REQUIRE_VERIFICATION = 'verify';
|
||||
|
||||
const RECAPTCHA_ENDPOINT = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
|
||||
/**
|
||||
* Returns the singleton Controller_CAPTCHA.
|
||||
*
|
||||
* @return Controller_CAPTCHA
|
||||
*/
|
||||
public static function shared() {
|
||||
static $_shared = null;
|
||||
if ($_shared === null) {
|
||||
$_shared = new Controller_CAPTCHA();
|
||||
}
|
||||
return $_shared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the authentication CAPTCHA is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function enabled() {
|
||||
$key = $this->site_key();
|
||||
$secret = $this->_secret();
|
||||
return Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_ENABLE_AUTH_CAPTCHA) && !empty($key) && !empty($secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public reCAPTCHA key if set.
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
public function site_key() {
|
||||
return Controller_Settings::shared()->get(Controller_Settings::OPTION_RECAPTCHA_SITE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the private reCAPTCHA secret if set.
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
protected function _secret() {
|
||||
return Controller_Settings::shared()->get(Controller_Settings::OPTION_RECAPTCHA_SECRET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bot/human threshold for comparing the score against, defaulting to 0.5.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function threshold() {
|
||||
return Controller_Settings::shared()->get_float(Controller_Settings::OPTION_RECAPTCHA_THRESHOLD, 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not test mode for reCAPTCHA is enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function test_mode() {
|
||||
return Controller_Settings::shared()->get_bool(\WordfenceLS\Controller_Settings::OPTION_CAPTCHA_TEST_MODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the reCAPTCHA endpoint with the given token, verifies the action matches, and returns the corresponding
|
||||
* score. If validation fails, false is returned. Any other failure (e.g., mangled response or connection dropped) returns 0.0.
|
||||
*
|
||||
* @param string $token
|
||||
* @param string $action
|
||||
* @param int $timeout
|
||||
* @return float|false
|
||||
*/
|
||||
public function score($token, $action = 'login', $timeout = 10) {
|
||||
try {
|
||||
$payload = array(
|
||||
'secret' => $this->_secret(),
|
||||
'response' => $token,
|
||||
'remoteip' => Model_Request::current()->ip(),
|
||||
);
|
||||
|
||||
$response = wp_remote_post(self::RECAPTCHA_ENDPOINT,
|
||||
array(
|
||||
'body' => $payload,
|
||||
'headers' => array(
|
||||
'Referer' => false,
|
||||
),
|
||||
'timeout' => $timeout,
|
||||
'blocking' => true,
|
||||
));
|
||||
|
||||
if (!is_wp_error($response)) {
|
||||
$jsonResponse = wp_remote_retrieve_body($response);
|
||||
$decoded = @json_decode($jsonResponse, true);
|
||||
if (is_array($decoded) && isset($decoded['success']) && isset($decoded['score']) && isset($decoded['action'])) {
|
||||
if ($decoded['success'] && $decoded['action'] == $action) {
|
||||
return (float) $decoded['score'];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
//Fall through
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the score is >= the threshold to be considered a human request.
|
||||
*
|
||||
* @param float $score
|
||||
* @return bool
|
||||
*/
|
||||
public function is_human($score) {
|
||||
if ($this->test_mode()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$threshold = $this->threshold();
|
||||
return ($score >= $threshold || abs($score - $threshold) < 0.0001);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current request is an XML RPC request
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_xml_rpc() {
|
||||
return defined('XMLRPC_REQUEST') && XMLRPC_REQUEST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if captcha is required for the current request
|
||||
* @return bool
|
||||
*/
|
||||
public function is_captcha_required() {
|
||||
$required = $this->enabled() && !self::is_xml_rpc();
|
||||
return apply_filters('wordfence_ls_require_captcha', $required);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the captcha token provided with the current request
|
||||
* @param string $key if specified, override the default token parameter
|
||||
* @return string|null the captcha token, if present, null otherwise
|
||||
*/
|
||||
public function get_token($key = 'wfls-captcha-token') {
|
||||
return (isset($_POST[$key]) && is_string($_POST[$key]) && !empty($_POST[$key]) ? $_POST[$key] : null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class Controller_DB {
|
||||
const TABLE_2FA_SECRETS = 'wfls_2fa_secrets';
|
||||
const TABLE_SETTINGS = 'wfls_settings';
|
||||
const TABLE_ROLE_COUNTS = 'wfls_role_counts';
|
||||
const TABLE_ROLE_COUNTS_TEMPORARY = 'wfls_role_counts_temporary';
|
||||
|
||||
const SCHEMA_VERSION = 2;
|
||||
|
||||
/**
|
||||
* Returns the singleton Controller_DB.
|
||||
*
|
||||
* @return Controller_DB
|
||||
*/
|
||||
public static function shared() {
|
||||
static $_shared = null;
|
||||
if ($_shared === null) {
|
||||
$_shared = new Controller_DB();
|
||||
}
|
||||
return $_shared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table prefix for the main site on multisites and the site itself on single site installations.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function network_prefix() {
|
||||
global $wpdb;
|
||||
return $wpdb->base_prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table with the site (single site installations) or network (multisite) prefix added.
|
||||
*
|
||||
* @param string $table
|
||||
* @return string
|
||||
*/
|
||||
public static function network_table($table) {
|
||||
return self::network_prefix() . $table;
|
||||
}
|
||||
|
||||
public function __get($key) {
|
||||
switch ($key) {
|
||||
case 'secrets':
|
||||
return self::network_table(self::TABLE_2FA_SECRETS);
|
||||
case 'settings':
|
||||
return self::network_table(self::TABLE_SETTINGS);
|
||||
case 'role_counts':
|
||||
return self::network_table(self::TABLE_ROLE_COUNTS);
|
||||
case 'role_counts_temporary':
|
||||
return self::network_table(self::TABLE_ROLE_COUNTS_TEMPORARY);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Unknown key: ' . $key);
|
||||
}
|
||||
|
||||
public function install() {
|
||||
$this->_create_schema();
|
||||
|
||||
global $wpdb;
|
||||
$table = $this->secrets;
|
||||
$wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `vtime` = LEAST(`vtime`, %d)", Controller_Time::time()));
|
||||
}
|
||||
|
||||
public function uninstall() {
|
||||
$tables = array(self::TABLE_2FA_SECRETS, self::TABLE_SETTINGS, self::TABLE_ROLE_COUNTS);
|
||||
foreach ($tables as $table) {
|
||||
global $wpdb;
|
||||
$wpdb->query('DROP TABLE IF EXISTS `' . self::network_table($table) . '`');
|
||||
}
|
||||
}
|
||||
|
||||
private function create_table($name, $definition, $temporary = false) {
|
||||
global $wpdb;
|
||||
if (is_array($definition)) {
|
||||
foreach ($definition as $attempt) {
|
||||
if ($this->create_table($name, $attempt, $temporary))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return $wpdb->query('CREATE ' . ($temporary ? 'TEMPORARY ' : '') . 'TABLE IF NOT EXISTS `' . self::network_table($name) . '` ' . $definition);
|
||||
}
|
||||
}
|
||||
|
||||
private function create_temporary_table($name, $definition) {
|
||||
if (Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_DISABLE_TEMPORARY_TABLES))
|
||||
return false;
|
||||
if ($this->create_table($name, $definition, true))
|
||||
return true;
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_DISABLE_TEMPORARY_TABLES, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
private function get_role_counts_table_definition($engine = null) {
|
||||
$engineClause = $engine === null ? '' : "ENGINE={$engine}";
|
||||
return <<<SQL
|
||||
(
|
||||
serialized_roles VARBINARY(255) NOT NULL,
|
||||
two_factor_inactive TINYINT(1) NOT NULL,
|
||||
user_count BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (serialized_roles, two_factor_inactive)
|
||||
) {$engineClause};
|
||||
SQL;
|
||||
}
|
||||
|
||||
private function get_role_counts_table_definition_options() {
|
||||
return array(
|
||||
$this->get_role_counts_table_definition('MEMORY'),
|
||||
$this->get_role_counts_table_definition('MyISAM'),
|
||||
$this->get_role_counts_table_definition()
|
||||
);
|
||||
}
|
||||
|
||||
protected function _create_schema() {
|
||||
$tables = array(
|
||||
self::TABLE_2FA_SECRETS => '(
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) unsigned NOT NULL,
|
||||
`secret` tinyblob NOT NULL,
|
||||
`recovery` blob NOT NULL,
|
||||
`ctime` int(10) unsigned NOT NULL,
|
||||
`vtime` int(10) unsigned NOT NULL,
|
||||
`mode` enum(\'authenticator\') NOT NULL DEFAULT \'authenticator\',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `user_id` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;',
|
||||
self::TABLE_SETTINGS => '(
|
||||
`name` varchar(191) NOT NULL DEFAULT \'\',
|
||||
`value` longblob,
|
||||
`autoload` enum(\'no\',\'yes\') NOT NULL DEFAULT \'yes\',
|
||||
PRIMARY KEY (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;',
|
||||
self::TABLE_ROLE_COUNTS => $this->get_role_counts_table_definition_options()
|
||||
);
|
||||
|
||||
foreach ($tables as $table => $def) {
|
||||
$this->create_table($table, $def);
|
||||
}
|
||||
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_SCHEMA_VERSION, self::SCHEMA_VERSION);
|
||||
}
|
||||
|
||||
public function require_schema_version($version) {
|
||||
$current = Controller_Settings::shared()->get_int(Controller_Settings::OPTION_SCHEMA_VERSION);
|
||||
if ($current < $version) {
|
||||
$this->install();
|
||||
}
|
||||
}
|
||||
|
||||
public function query($query) {
|
||||
global $wpdb;
|
||||
if ($wpdb->query($query) === false)
|
||||
throw new RuntimeException("Failed to execute query: {$query}");
|
||||
}
|
||||
|
||||
public function get_wpdb() {
|
||||
global $wpdb;
|
||||
return $wpdb;
|
||||
}
|
||||
|
||||
public function create_temporary_role_counts_table() {
|
||||
return $this->create_temporary_table(self::TABLE_ROLE_COUNTS_TEMPORARY, $this->get_role_counts_table_definition_options());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
use WordfenceLS\Text\Model_HTML;
|
||||
|
||||
class Controller_Notices {
|
||||
const USER_META_KEY = 'wfls_notices';
|
||||
const PERSISTENT_NOTICE_DISMISS_PREFIX = 'wfls-dismiss-';
|
||||
const PERSISTENT_NOTICE_WOOCOMMERCE_INTEGRATION = 'wfls-woocommerce-integration-notice';
|
||||
|
||||
/**
|
||||
* Returns the singleton Controller_Notices.
|
||||
*
|
||||
* @return Controller_Notices
|
||||
*/
|
||||
public static function shared() {
|
||||
static $_shared = null;
|
||||
if ($_shared === null) {
|
||||
$_shared = new Controller_Notices();
|
||||
}
|
||||
return $_shared;
|
||||
}
|
||||
|
||||
private $persistentNotices = array();
|
||||
|
||||
/**
|
||||
* Adds an admin notice to the display queue. If $user is provided, it will show only for that user, otherwise it
|
||||
* will show for all administrators.
|
||||
*
|
||||
* @param string $severity
|
||||
* @param string|Model_HTML $message
|
||||
* @param bool|string $category If not false, notices with the same category will be removed prior to adding this one.
|
||||
* @param bool|\WP_User $user If not false, the user that the notice should show for.
|
||||
*/
|
||||
public function add_notice($severity, $message, $category = false, $user = false) {
|
||||
$notices = $this->_notices($user);
|
||||
foreach ($notices as $id => $n) {
|
||||
if ($category !== false && isset($n['category']) && $n['category'] == $category) { //Same category overwrites previous entry
|
||||
unset($notices[$id]);
|
||||
}
|
||||
}
|
||||
|
||||
$id = Model_Crypto::uuid();
|
||||
$notices[$id] = array(
|
||||
'severity' => $severity,
|
||||
'messageHTML' => Model_HTML::esc_html($message),
|
||||
);
|
||||
|
||||
if ($category !== false) {
|
||||
$notices[$id]['category'] = $category;
|
||||
}
|
||||
|
||||
$this->_save_notices($notices, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a notice using one of two possible search methods:
|
||||
*
|
||||
* 1. If $id matches. $category is ignored but only notices for $user are checked.
|
||||
* 2. If $category matches. Only notices for $user are checked.
|
||||
*
|
||||
* @param bool|int $id
|
||||
* @param bool|string $category
|
||||
* @param bool|\WP_User $user
|
||||
*/
|
||||
public function remove_notice($id = false, $category = false, $user = false) {
|
||||
if ($id === false && $category === false) {
|
||||
return;
|
||||
}
|
||||
else if ($id !== false) {
|
||||
$category = false;
|
||||
}
|
||||
|
||||
$notices = $this->_notices($user);
|
||||
foreach ($notices as $nid => $n) {
|
||||
if ($id == $nid) { //ID match
|
||||
unset($notices[$nid]);
|
||||
break;
|
||||
}
|
||||
else if ($id !== false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($category !== false && isset($n['category']) && $category == $n['category']) { //Category match
|
||||
unset($notices[$nid]);
|
||||
}
|
||||
}
|
||||
$this->_save_notices($notices, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not a notice exists for the given user.
|
||||
*
|
||||
* @param bool|\WP_User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function has_notice($user) {
|
||||
$notices = $this->_notices($user);
|
||||
return !!count($notices) || $this->has_persistent_notices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a user's notices. For administrators this also includes global notices.
|
||||
*
|
||||
* @return bool Whether any notices were enqueued.
|
||||
*/
|
||||
public function enqueue_notices() {
|
||||
$user = wp_get_current_user();
|
||||
if ($user->ID == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$added = false;
|
||||
$notices = array();
|
||||
if (Controller_Permissions::shared()->can_manage_settings($user)) {
|
||||
$globalNotices = $this->_notices(false);
|
||||
$notices = array_merge($notices, $globalNotices);
|
||||
}
|
||||
|
||||
$userNotices = $this->_notices($user);
|
||||
$notices = array_merge($notices, $userNotices);
|
||||
|
||||
foreach ($notices as $nid => $n) {
|
||||
$notice = new Model_Notice($nid, $n['severity'], $n['messageHTML'], $n['category']);
|
||||
if (is_multisite()) {
|
||||
add_action('network_admin_notices', array($notice, 'display_notice'));
|
||||
}
|
||||
else {
|
||||
add_action('admin_notices', array($notice, 'display_notice'));
|
||||
}
|
||||
|
||||
$added = true;
|
||||
}
|
||||
|
||||
return $added;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the notices for a user if provided, otherwise the global notices.
|
||||
*
|
||||
* @param bool|\WP_User $user
|
||||
* @return array
|
||||
*/
|
||||
protected function _notices($user) {
|
||||
if ($user instanceof \WP_User) {
|
||||
$notices = get_user_meta($user->ID, self::USER_META_KEY, true);
|
||||
return array_filter((array) $notices);
|
||||
}
|
||||
return Controller_Settings::shared()->get_array(Controller_Settings::OPTION_GLOBAL_NOTICES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the notices.
|
||||
*
|
||||
* @param array $notices
|
||||
* @param bool|\WP_User $user
|
||||
*/
|
||||
protected function _save_notices($notices, $user) {
|
||||
if ($user instanceof \WP_User) {
|
||||
update_user_meta($user->ID, self::USER_META_KEY, $notices);
|
||||
return;
|
||||
}
|
||||
Controller_Settings::shared()->set_array(Controller_Settings::OPTION_GLOBAL_NOTICES, $notices, true);
|
||||
}
|
||||
|
||||
public function get_persistent_notice_ids() {
|
||||
return array(
|
||||
self::PERSISTENT_NOTICE_WOOCOMMERCE_INTEGRATION
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_persistent_notice_dismiss_key($noticeId) {
|
||||
return self::PERSISTENT_NOTICE_DISMISS_PREFIX . $noticeId;
|
||||
}
|
||||
|
||||
public function register_persistent_notice($noticeId) {
|
||||
$this->persistentNotices[] = $noticeId;
|
||||
}
|
||||
|
||||
public function has_persistent_notices() {
|
||||
return count($this->persistentNotices) > 0;
|
||||
}
|
||||
|
||||
public function dismiss_persistent_notice($userId, $noticeId) {
|
||||
if (!in_array($noticeId, $this->get_persistent_notice_ids(), true))
|
||||
return false;
|
||||
update_user_option($userId, self::get_persistent_notice_dismiss_key($noticeId), true, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function is_persistent_notice_dismissed($userId, $noticeId) {
|
||||
return (bool) get_user_option(self::get_persistent_notice_dismiss_key($noticeId), $userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Controller_Permissions {
|
||||
const CAP_ACTIVATE_2FA_SELF = 'wf2fa_activate_2fa_self'; //Activate 2FA on its own user account
|
||||
const CAP_ACTIVATE_2FA_OTHERS = 'wf2fa_activate_2fa_others'; //Activate 2FA on user accounts other than its own
|
||||
const CAP_MANAGE_SETTINGS = 'wf2fa_manage_settings'; //Edit settings for the plugin
|
||||
|
||||
const SITE_BATCH_SIZE = 50; //The maximum number of sites to process during a single request
|
||||
|
||||
private $network_roles = array();
|
||||
|
||||
/**
|
||||
* Returns the singleton Controller_Permissions.
|
||||
*
|
||||
* @return Controller_Permissions
|
||||
*/
|
||||
public static function shared() {
|
||||
static $_shared = null;
|
||||
if ($_shared === null) {
|
||||
$_shared = new Controller_Permissions();
|
||||
}
|
||||
return $_shared;
|
||||
}
|
||||
|
||||
private function on_role_change() {
|
||||
update_site_option('wfls_last_role_change', time());
|
||||
if(is_multisite())
|
||||
update_site_option('wfls_role_batch_position', 0);
|
||||
}
|
||||
|
||||
public function install() {
|
||||
$this->on_role_change();
|
||||
if (is_multisite()) {
|
||||
//Super Admin automatically gets all capabilities, so we don't need to explicitly add them
|
||||
$this->_add_cap_multisite('administrator', self::CAP_ACTIVATE_2FA_SELF, $this->get_primary_sites());
|
||||
}
|
||||
else {
|
||||
$this->_add_cap('administrator', self::CAP_ACTIVATE_2FA_SELF);
|
||||
$this->_add_cap('administrator', self::CAP_ACTIVATE_2FA_OTHERS);
|
||||
$this->_add_cap('administrator', self::CAP_MANAGE_SETTINGS);
|
||||
}
|
||||
}
|
||||
|
||||
public function init() {
|
||||
global $wp_version;
|
||||
if(is_multisite()){
|
||||
if(version_compare($wp_version, '5.1.0', '>=')){
|
||||
add_action('wp_initialize_site', array($this, '_wp_initialize_site'), 99);
|
||||
}
|
||||
else{
|
||||
add_action('wpmu_new_blog', array($this, '_wpmu_new_blog'), 10, 5);
|
||||
}
|
||||
add_action('init', array($this, 'check_role_sync'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
public function _wpmu_new_blog($site_id, $user_id, $domain, $path, $network_id) {
|
||||
$this->sync_roles($network_id, $site_id);
|
||||
}
|
||||
|
||||
public function _wp_initialize_site($new_site) {
|
||||
$this->sync_roles($new_site->site_id, $new_site->blog_id);
|
||||
}
|
||||
|
||||
public function check_role_sync() {
|
||||
//Trigger an initial update for existing installations
|
||||
$last_role_change=(int)get_site_option('wfls_last_role_change', 0);
|
||||
if($last_role_change===0)
|
||||
$this->on_role_change();
|
||||
//Process the current batch if necessary
|
||||
$position=(int)get_site_option('wfls_role_batch_position', 0);
|
||||
if($position===-1)
|
||||
return;
|
||||
$sites=$this->get_sites($position, self::SITE_BATCH_SIZE);
|
||||
if(empty($sites)){
|
||||
$position=-1;
|
||||
return;
|
||||
}
|
||||
else{
|
||||
$network_id=get_current_site()->id;
|
||||
foreach($sites as $site){
|
||||
$site=(int)$site;
|
||||
$this->sync_roles($network_id, $site);
|
||||
}
|
||||
$position=$site;
|
||||
}
|
||||
update_site_option('wfls_role_batch_position', $position);
|
||||
//Update the current site if not already up to date
|
||||
$site_id=get_current_blog_id();
|
||||
if($last_role_change>=get_option('wfls_last_role_sync', 0)&&$site_id>=$position){
|
||||
$this->sync_roles(get_current_site()->id, $site_id);
|
||||
update_option('wfls_last_role_sync', time());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary site ID for a given network
|
||||
*/
|
||||
private function get_primary_site_id($network_id) {
|
||||
global $wpdb;
|
||||
if(function_exists('get_network')){
|
||||
$network=get_network($network_id); //TODO: Support multi-network throughout plugin
|
||||
return (int)$network->blog_id;
|
||||
}
|
||||
else{
|
||||
return (int)$wpdb->get_var($wpdb->prepare("SELECT blogs.blog_id FROM {$wpdb->site} sites JOIN {$wpdb->blogs} blogs ON blogs.site_id=sites.id AND blogs.path=sites.path WHERE sites.id=%d", $network_id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all primary sites in a multi-network setup
|
||||
*/
|
||||
private function get_primary_sites() {
|
||||
global $wpdb;
|
||||
if(function_exists('get_networks')){
|
||||
return array_map(function($network){ return $network->blog_id; }, get_networks());
|
||||
}
|
||||
else{
|
||||
return $wpdb->get_col("SELECT blogs.blog_id FROM {$wpdb->site} sites JOIN {$wpdb->blogs} blogs ON blogs.site_id=sites.id AND blogs.path=sites.path");
|
||||
}
|
||||
}
|
||||
|
||||
private function get_sites($from, $count) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_col($wpdb->prepare("SELECT `blog_id` FROM `{$wpdb->blogs}` WHERE `deleted` = 0 AND blog_id > %d ORDER BY blog_id LIMIT %d", $from, $count));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync role capabilities from the default site to a newly added site
|
||||
* @param int $network_id the relevant network
|
||||
* @param int $site_id the newly added site(blog)
|
||||
*/
|
||||
private function sync_roles($network_id, $site_id){
|
||||
if(array_key_exists($network_id, $this->network_roles)){
|
||||
$current_roles=$this->network_roles[$network_id];
|
||||
}
|
||||
else{
|
||||
$current_roles=$this->_wp_roles($this->get_primary_site_id($network_id));
|
||||
$this->network_roles[$network_id]=$current_roles;
|
||||
}
|
||||
$new_site_roles=$this->_wp_roles($site_id);
|
||||
$capabilities=array(
|
||||
self::CAP_ACTIVATE_2FA_SELF,
|
||||
self::CAP_ACTIVATE_2FA_OTHERS,
|
||||
self::CAP_MANAGE_SETTINGS
|
||||
);
|
||||
foreach($current_roles->get_names() as $role_name=>$role_label){
|
||||
if($new_site_roles->get_role($role_name)===null)
|
||||
$new_site_roles->add_role($role_name, $role_label);
|
||||
$role=$current_roles->get_role($role_name);
|
||||
foreach($capabilities as $cap){
|
||||
if($role->has_cap($cap)){
|
||||
$this->_add_cap_multisite($role_name, $cap, array($site_id));
|
||||
}
|
||||
else{
|
||||
$this->_remove_cap_multisite($role_name, $cap, array($site_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function allow_2fa_self($role_name) {
|
||||
$this->on_role_change();
|
||||
if (is_multisite()) {
|
||||
return $this->_add_cap_multisite($role_name, self::CAP_ACTIVATE_2FA_SELF, $this->get_primary_sites());
|
||||
}
|
||||
else {
|
||||
return $this->_add_cap($role_name, self::CAP_ACTIVATE_2FA_SELF);
|
||||
}
|
||||
}
|
||||
|
||||
public function disallow_2fa_self($role_name) {
|
||||
$this->on_role_change();
|
||||
if (is_multisite()) {
|
||||
return $this->_remove_cap_multisite($role_name, self::CAP_ACTIVATE_2FA_SELF, $this->get_primary_sites());
|
||||
}
|
||||
else {
|
||||
if ($role_name == 'administrator') {
|
||||
return true;
|
||||
}
|
||||
return $this->_remove_cap($role_name, self::CAP_ACTIVATE_2FA_SELF);
|
||||
}
|
||||
}
|
||||
|
||||
public function can_manage_settings($user = false) {
|
||||
if ($user === false) {
|
||||
$user = wp_get_current_user();
|
||||
}
|
||||
|
||||
if (!($user instanceof \WP_User)) {
|
||||
return false;
|
||||
}
|
||||
return $user->has_cap(self::CAP_MANAGE_SETTINGS);
|
||||
}
|
||||
|
||||
public function can_role_manage_settings($role) {
|
||||
if (is_string($role)) {
|
||||
$role = get_role($role);
|
||||
}
|
||||
if ($role)
|
||||
return $role->has_cap(self::CAP_MANAGE_SETTINGS);
|
||||
return false;
|
||||
}
|
||||
|
||||
private function _wp_roles($site_id = null) {
|
||||
require(ABSPATH . 'wp-includes/version.php'); /** @var string $wp_version */
|
||||
if (version_compare($wp_version, '4.9', '>=')) {
|
||||
return new \WP_Roles($site_id);
|
||||
}
|
||||
|
||||
//\WP_Roles in WP < 4.9 initializes based on the current blog ID
|
||||
if (is_multisite()) {
|
||||
switch_to_blog($site_id);
|
||||
}
|
||||
$wp_roles = new \WP_Roles();
|
||||
if (is_multisite()) {
|
||||
restore_current_blog();
|
||||
}
|
||||
return $wp_roles;
|
||||
}
|
||||
|
||||
private function _add_cap_multisite($role_name, $cap, $blog_ids=null) {
|
||||
if ($role_name === 'super-admin')
|
||||
return true;
|
||||
global $wpdb;
|
||||
$blogs = $blog_ids===null?$wpdb->get_col("SELECT `blog_id` FROM `{$wpdb->blogs}` WHERE `deleted` = 0"):$blog_ids;
|
||||
$added = false;
|
||||
foreach ($blogs as $id) {
|
||||
$wp_roles = $this->_wp_roles($id);
|
||||
switch_to_blog($id);
|
||||
$added = $this->_add_cap($role_name, $cap, $wp_roles) || $added;
|
||||
restore_current_blog();
|
||||
}
|
||||
return $added;
|
||||
}
|
||||
|
||||
private function _add_cap($role_name, $cap, $wp_roles = null) {
|
||||
if ($wp_roles === null) { $wp_roles = $this->_wp_roles(); }
|
||||
$role = $wp_roles->get_role($role_name);
|
||||
if ($role === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$wp_roles->add_cap($role_name, $cap);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function _remove_cap_multisite($role_name, $cap, $blog_ids=null) {
|
||||
if ($role_name === 'super-admin')
|
||||
return false;
|
||||
global $wpdb;
|
||||
$blogs = $blog_ids===null?$wpdb->get_col("SELECT `blog_id` FROM `{$wpdb->blogs}` WHERE `deleted` = 0"):$blog_ids;
|
||||
$removed = false;
|
||||
foreach ($blogs as $id) {
|
||||
$wp_roles = $this->_wp_roles($id);
|
||||
switch_to_blog($id);
|
||||
$removed = $this->_remove_cap($role_name, $cap, $wp_roles) || $removed;
|
||||
restore_current_blog();
|
||||
}
|
||||
return $removed;
|
||||
}
|
||||
|
||||
private function _remove_cap($role_name, $cap, $wp_roles = null) {
|
||||
if ($wp_roles === null) { $wp_roles = $this->_wp_roles(); }
|
||||
$role = $wp_roles->get_role($role_name);
|
||||
if ($role === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$wp_roles->remove_cap($role_name, $cap);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get_all_roles($user) {
|
||||
if (is_multisite()) {
|
||||
$roles = array();
|
||||
if (is_super_admin($user->ID))
|
||||
$roles[] = 'super-admin';
|
||||
foreach (get_blogs_of_user($user->ID) as $id => $blog) {
|
||||
switch_to_blog($id);
|
||||
$blogUser = new \WP_User($user->ID);
|
||||
$roles = array_merge($roles, $blogUser->roles);
|
||||
restore_current_blog();
|
||||
}
|
||||
return array_unique($roles);
|
||||
}
|
||||
else {
|
||||
return $user->roles;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
use WordfenceLS\Settings\Model_DB;
|
||||
use WordfenceLS\Settings\Model_WPOptions;
|
||||
|
||||
class Controller_Settings {
|
||||
//Configurable
|
||||
const OPTION_XMLRPC_ENABLED = 'xmlrpc-enabled';
|
||||
const OPTION_2FA_WHITELISTED = 'whitelisted';
|
||||
const OPTION_IP_SOURCE = 'ip-source';
|
||||
const OPTION_IP_TRUSTED_PROXIES = 'ip-trusted-proxies';
|
||||
const OPTION_REQUIRE_2FA_ADMIN = 'require-2fa.administrator';
|
||||
const OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED = 'require-2fa-grace-period-enabled';
|
||||
const OPTION_REQUIRE_2FA_GRACE_PERIOD = 'require-2fa-grace-period';
|
||||
const OPTION_REQUIRE_2FA_USER_GRACE_PERIOD = '2fa-user-grace-period';
|
||||
const OPTION_REMEMBER_DEVICE_ENABLED = 'remember-device';
|
||||
const OPTION_REMEMBER_DEVICE_DURATION = 'remember-device-duration';
|
||||
const OPTION_ALLOW_XML_RPC = 'allow-xml-rpc';
|
||||
const OPTION_ENABLE_AUTH_CAPTCHA = 'enable-auth-captcha';
|
||||
const OPTION_CAPTCHA_TEST_MODE = 'recaptcha-test-mode';
|
||||
const OPTION_RECAPTCHA_SITE_KEY = 'recaptcha-site-key';
|
||||
const OPTION_RECAPTCHA_SECRET = 'recaptcha-secret';
|
||||
const OPTION_RECAPTCHA_THRESHOLD = 'recaptcha-threshold';
|
||||
const OPTION_DELETE_ON_DEACTIVATION = 'delete-deactivation';
|
||||
const OPTION_PREFIX_REQUIRED_2FA_ROLE = 'required-2fa-role';
|
||||
const OPTION_ENABLE_WOOCOMMERCE_INTEGRATION = 'enable-woocommerce-integration';
|
||||
const OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION = 'enable-woocommerce-account-integration';
|
||||
const OPTION_ENABLE_SHORTCODE = 'enable-shortcode';
|
||||
const OPTION_ENABLE_LOGIN_HISTORY_COLUMNS = 'enable-login-history-columns';
|
||||
const OPTION_STACK_UI_COLUMNS = 'stack-ui-columns';
|
||||
|
||||
//Internal
|
||||
const OPTION_GLOBAL_NOTICES = 'global-notices';
|
||||
const OPTION_LAST_SECRET_REFRESH = 'last-secret-refresh';
|
||||
const OPTION_USE_NTP = 'use-ntp';
|
||||
const OPTION_ALLOW_DISABLING_NTP = 'allow-disabling-ntp';
|
||||
const OPTION_NTP_FAILURE_COUNT = 'ntp-failure-count';
|
||||
const OPTION_NTP_OFFSET = 'ntp-offset';
|
||||
const OPTION_SHARED_HASH_SECRET_KEY = 'shared-hash-secret';
|
||||
const OPTION_SHARED_SYMMETRIC_SECRET_KEY = 'shared-symmetric-secret';
|
||||
const OPTION_DISMISSED_FRESH_INSTALL_MODAL = 'dismissed-fresh-install-modal';
|
||||
const OPTION_CAPTCHA_STATS = 'captcha-stats';
|
||||
const OPTION_SCHEMA_VERSION = 'schema-version';
|
||||
const OPTION_USER_COUNT_QUERY_STATE = 'user-count-query-state';
|
||||
const OPTION_DISABLE_TEMPORARY_TABLES = 'disable-temporary-tables';
|
||||
|
||||
const DEFAULT_REQUIRE_2FA_USER_GRACE_PERIOD = 10;
|
||||
const MAX_REQUIRE_2FA_USER_GRACE_PERIOD = 99;
|
||||
|
||||
const STATE_2FA_DISABLED = 'disabled';
|
||||
const STATE_2FA_OPTIONAL = 'optional';
|
||||
const STATE_2FA_REQUIRED = 'required';
|
||||
|
||||
protected $_settingsStorage;
|
||||
|
||||
/**
|
||||
* Returns the singleton Controller_Settings.
|
||||
*
|
||||
* @return Controller_Settings
|
||||
*/
|
||||
public static function shared() {
|
||||
static $_shared = null;
|
||||
if ($_shared === null) {
|
||||
$_shared = new Controller_Settings();
|
||||
}
|
||||
return $_shared;
|
||||
}
|
||||
|
||||
public function __construct($settingsStorage = false) {
|
||||
if (!$settingsStorage) {
|
||||
$settingsStorage = new Model_DB();
|
||||
}
|
||||
$this->_settingsStorage = $settingsStorage;
|
||||
$this->_migrate_admin_2fa_requirements_to_roles();
|
||||
}
|
||||
|
||||
public function set_defaults() {
|
||||
$this->_settingsStorage->set_multiple(array(
|
||||
self::OPTION_XMLRPC_ENABLED => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_2FA_WHITELISTED => array('value' => '', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_IP_SOURCE => array('value' => Model_Request::IP_SOURCE_AUTOMATIC, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_IP_TRUSTED_PROXIES => array('value' => '', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_REQUIRE_2FA_ADMIN => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD => array('value' => self::DEFAULT_REQUIRE_2FA_USER_GRACE_PERIOD, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_GLOBAL_NOTICES => array('value' => '[]', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_REMEMBER_DEVICE_ENABLED => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_REMEMBER_DEVICE_DURATION => array('value' => (30 * 86400), 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_ALLOW_XML_RPC => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_ENABLE_AUTH_CAPTCHA => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_CAPTCHA_STATS => array('value' => '{"counts":[0,0,0,0,0,0,0,0,0,0,0],"avg":0}', 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_RECAPTCHA_THRESHOLD => array('value' => 0.5, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_LAST_SECRET_REFRESH => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_DELETE_ON_DEACTIVATION => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_ENABLE_SHORTCODE => array('value' => false, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_STACK_UI_COLUMNS => array('value' => true, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_SCHEMA_VERSION => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_USER_COUNT_QUERY_STATE => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false),
|
||||
self::OPTION_DISABLE_TEMPORARY_TABLES => array('value' => 0, 'autoload' => Model_Settings::AUTOLOAD_YES, 'allowOverwrite' => false)
|
||||
));
|
||||
}
|
||||
|
||||
public function set($key, $value, $already_validated = false) {
|
||||
return $this->set_multiple(array($key => $value), $already_validated);
|
||||
}
|
||||
|
||||
public function set_array($key, $value, $already_validated = false) {
|
||||
return $this->set_multiple(array($key => json_encode($value)), $already_validated);
|
||||
}
|
||||
|
||||
public function set_multiple($changes, $already_validated = false) {
|
||||
if (!$already_validated && $this->validate_multiple($changes) !== true) {
|
||||
return false;
|
||||
}
|
||||
$changes = $this->clean_multiple($changes);
|
||||
$changes = $this->preprocess_multiple($changes);
|
||||
$this->_settingsStorage->set_multiple($changes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get($key, $default = false) {
|
||||
return $this->_settingsStorage->get($key, $default);
|
||||
}
|
||||
|
||||
public function get_bool($key, $default = false) {
|
||||
return $this->_truthy_to_bool($this->get($key, $default));
|
||||
}
|
||||
|
||||
public function get_int($key, $default = 0) {
|
||||
return intval($this->get($key, $default));
|
||||
}
|
||||
|
||||
public function get_float($key, $default = 0.0) {
|
||||
return (float) $this->get($key, $default);
|
||||
}
|
||||
|
||||
public function get_array($key, $default = array()) {
|
||||
$value = $this->get($key, null);
|
||||
if (is_string($value)) {
|
||||
$value = @json_decode($value, true);
|
||||
}
|
||||
else {
|
||||
$value = null;
|
||||
}
|
||||
return is_array($value) ? $value : $default;
|
||||
}
|
||||
|
||||
public function remove($key) {
|
||||
$this->_settingsStorage->remove($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether a user-entered setting value is acceptable. Returns true if valid or an error message if not.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return bool|string
|
||||
*/
|
||||
public function validate($key, $value) {
|
||||
switch ($key) {
|
||||
//Boolean
|
||||
case self::OPTION_XMLRPC_ENABLED:
|
||||
case self::OPTION_REQUIRE_2FA_ADMIN:
|
||||
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED:
|
||||
case self::OPTION_REMEMBER_DEVICE_ENABLED:
|
||||
case self::OPTION_ALLOW_XML_RPC:
|
||||
case self::OPTION_ENABLE_AUTH_CAPTCHA:
|
||||
case self::OPTION_CAPTCHA_TEST_MODE:
|
||||
case self::OPTION_DISMISSED_FRESH_INSTALL_MODAL:
|
||||
case self::OPTION_DELETE_ON_DEACTIVATION:
|
||||
case self::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION:
|
||||
case self::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION:
|
||||
case self::OPTION_ENABLE_SHORTCODE:
|
||||
case self::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS:
|
||||
case self::OPTION_STACK_UI_COLUMNS:
|
||||
case self::OPTION_USER_COUNT_QUERY_STATE:
|
||||
case self::OPTION_DISABLE_TEMPORARY_TABLES:
|
||||
return true;
|
||||
|
||||
//Int
|
||||
case self::OPTION_LAST_SECRET_REFRESH:
|
||||
return is_numeric($value); //Left using is_numeric to prevent issues with existing values
|
||||
case self::OPTION_SCHEMA_VERSION:
|
||||
return Utility_Number::isInteger($value, 0);
|
||||
|
||||
//Array
|
||||
case self::OPTION_GLOBAL_NOTICES:
|
||||
case self::OPTION_CAPTCHA_STATS:
|
||||
return preg_match('/^\[.*\]$/', $value) || preg_match('/^\{.*\}$/', $value); //Only a rough JSON validation
|
||||
|
||||
//Special
|
||||
case self::OPTION_IP_TRUSTED_PROXIES:
|
||||
case self::OPTION_2FA_WHITELISTED:
|
||||
$parsed = array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $value)));
|
||||
foreach ($parsed as $entry) {
|
||||
if (!Controller_Whitelist::shared()->is_valid_range($entry)) {
|
||||
return sprintf(__('The IP/range %s is invalid.', 'wordfence-2fa'), esc_html($entry));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case self::OPTION_IP_SOURCE:
|
||||
if (!in_array($value, array(Model_Request::IP_SOURCE_AUTOMATIC, Model_Request::IP_SOURCE_REMOTE_ADDR, Model_Request::IP_SOURCE_X_FORWARDED_FOR, Model_Request::IP_SOURCE_X_REAL_IP))) {
|
||||
return __('An invalid IP source was provided.', 'wordfence-2fa');
|
||||
}
|
||||
return true;
|
||||
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD:
|
||||
$gracePeriodEnd = strtotime($value);
|
||||
if ($gracePeriodEnd <= \WordfenceLS\Controller_Time::time()) {
|
||||
return __('The grace period end time must be in the future.', 'wordfence-2fa');
|
||||
}
|
||||
return true;
|
||||
case self::OPTION_REMEMBER_DEVICE_DURATION:
|
||||
return is_numeric($value) && $value > 0;
|
||||
case self::OPTION_RECAPTCHA_THRESHOLD:
|
||||
return is_numeric($value) && $value >= 0 && $value <= 1;
|
||||
case self::OPTION_RECAPTCHA_SITE_KEY:
|
||||
if (empty($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$response = wp_remote_get('https://www.google.com/recaptcha/api.js?render=' . urlencode($value));
|
||||
|
||||
if (!is_wp_error($response)) {
|
||||
$status = wp_remote_retrieve_response_code($response);
|
||||
if ($status == 200) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$data = wp_remote_retrieve_body($response);
|
||||
if (strpos($data, 'grecaptcha') === false) {
|
||||
return __('Unable to validate the reCAPTCHA site key. Please check the key and try again.', 'wordfence-2fa');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return sprintf(__('An error was encountered while validating the reCAPTCHA site key: %s', 'wordfence-2fa'), $response->get_error_message());
|
||||
case self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD:
|
||||
return is_numeric($value) && $value >= 0 && $value <= self::MAX_REQUIRE_2FA_USER_GRACE_PERIOD;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validate_multiple($values) {
|
||||
$errors = array();
|
||||
foreach ($values as $key => $value) {
|
||||
$status = $this->validate($key, $value);
|
||||
if ($status !== true) {
|
||||
$errors[$key] = $status;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
return $errors;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans and normalizes a setting value for use in saving.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function clean($key, $value) {
|
||||
switch ($key) {
|
||||
//Boolean
|
||||
case self::OPTION_XMLRPC_ENABLED:
|
||||
case self::OPTION_REQUIRE_2FA_ADMIN:
|
||||
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED:
|
||||
case self::OPTION_REMEMBER_DEVICE_ENABLED:
|
||||
case self::OPTION_ALLOW_XML_RPC:
|
||||
case self::OPTION_ENABLE_AUTH_CAPTCHA:
|
||||
case self::OPTION_CAPTCHA_TEST_MODE:
|
||||
case self::OPTION_DISMISSED_FRESH_INSTALL_MODAL:
|
||||
case self::OPTION_DELETE_ON_DEACTIVATION:
|
||||
case self::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION:
|
||||
case self::OPTION_ENABLE_WOOCOMMERCE_ACCOUNT_INTEGRATION:
|
||||
case self::OPTION_ENABLE_SHORTCODE;
|
||||
case self::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS:
|
||||
case self::OPTION_STACK_UI_COLUMNS:
|
||||
case self::OPTION_USER_COUNT_QUERY_STATE:
|
||||
case self::OPTION_DISABLE_TEMPORARY_TABLES:
|
||||
return $this->_truthy_to_bool($value);
|
||||
|
||||
//Int
|
||||
case self::OPTION_REMEMBER_DEVICE_DURATION:
|
||||
case self::OPTION_LAST_SECRET_REFRESH:
|
||||
case self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD:
|
||||
case self::OPTION_SCHEMA_VERSION:
|
||||
return (int) $value;
|
||||
|
||||
//Float
|
||||
case self::OPTION_RECAPTCHA_THRESHOLD:
|
||||
return (float) $value;
|
||||
|
||||
//Special
|
||||
case self::OPTION_IP_TRUSTED_PROXIES:
|
||||
case self::OPTION_2FA_WHITELISTED:
|
||||
$parsed = array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $value)));
|
||||
$cleaned = array();
|
||||
foreach ($parsed as $item) {
|
||||
$cleaned[] = $this->_sanitize_ip_range($item);
|
||||
}
|
||||
return implode("\n", $cleaned);
|
||||
case self::OPTION_REQUIRE_2FA_GRACE_PERIOD:
|
||||
$dt = $this->_parse_local_time($value);
|
||||
return $dt->format('U');
|
||||
case self::OPTION_RECAPTCHA_SITE_KEY:
|
||||
case self::OPTION_RECAPTCHA_SECRET:
|
||||
return trim($value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function clean_multiple($changes) {
|
||||
$cleaned = array();
|
||||
foreach ($changes as $key => $value) {
|
||||
$cleaned[$key] = $this->clean($key, $value);
|
||||
}
|
||||
return $cleaned;
|
||||
}
|
||||
|
||||
private function get_required_2fa_role_key($role) {
|
||||
return implode('.', array(self::OPTION_PREFIX_REQUIRED_2FA_ROLE, $role));
|
||||
}
|
||||
|
||||
public function get_required_2fa_role_activation_time($role) {
|
||||
$time = $this->get_int($this->get_required_2fa_role_key($role), -1);
|
||||
if ($time < 0)
|
||||
return false;
|
||||
return $time;
|
||||
}
|
||||
|
||||
public function get_user_2fa_grace_period() {
|
||||
return $this->get_int(self::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD, self::DEFAULT_REQUIRE_2FA_USER_GRACE_PERIOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocesses the value, returning true if it was saved here (e.g., saved 2fa enabled by assigning a role
|
||||
* capability) or false if it is to be saved by the backing storage.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param array &$settings the array of settings to process, this function may append additional values from preprocessing
|
||||
* @return bool
|
||||
*/
|
||||
public function preprocess($key, $value, &$settings) {
|
||||
if (preg_match('/^enabled-roles\.(.+)$/', $key, $matches)) { //Enabled roles are stored as capabilities rather than in the settings storage
|
||||
$role = $matches[1];
|
||||
if ($role === 'super-admin') {
|
||||
$roleValid = true;
|
||||
}
|
||||
elseif (in_array($value, array(self::STATE_2FA_OPTIONAL, self::STATE_2FA_REQUIRED))) {
|
||||
$roleValid = Controller_Permissions::shared()->allow_2fa_self($role);
|
||||
}
|
||||
else {
|
||||
$roleValid = Controller_Permissions::shared()->disallow_2fa_self($role);
|
||||
}
|
||||
if ($roleValid)
|
||||
$settings[$this->get_required_2fa_role_key($role)] = ($value === self::STATE_2FA_REQUIRED ? time() : -1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function preprocess_multiple($changes) {
|
||||
$remaining = array();
|
||||
foreach ($changes as $key => $value) {
|
||||
if (!$this->preprocess($key, $value, $remaining)) {
|
||||
$remaining[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $remaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a cleaned array containing the whitelist entries.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function whitelisted_ips() {
|
||||
return array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $this->get(self::OPTION_2FA_WHITELISTED, ''))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cleaned array containing the trusted proxy entries.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function trusted_proxies() {
|
||||
return array_filter(array_map(function($s) { return trim($s); }, preg_split('/[\r\n]/', $this->get(self::OPTION_IP_TRUSTED_PROXIES, ''))));
|
||||
}
|
||||
|
||||
public function get_ntp_failure_count() {
|
||||
return $this->get_int(self::OPTION_NTP_FAILURE_COUNT, 0);
|
||||
}
|
||||
|
||||
public function reset_ntp_failure_count() {
|
||||
$this->set(self::OPTION_NTP_FAILURE_COUNT, 0);
|
||||
}
|
||||
|
||||
public function increment_ntp_failure_count() {
|
||||
$count = $this->get_ntp_failure_count();
|
||||
if ($count < 0)
|
||||
return false;
|
||||
$count++;
|
||||
$this->set(self::OPTION_NTP_FAILURE_COUNT, $count);
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function is_ntp_disabled_via_constant() {
|
||||
return defined('WORDFENCE_LS_DISABLE_NTP') && WORDFENCE_LS_DISABLE_NTP;
|
||||
}
|
||||
|
||||
public function is_ntp_enabled($requireOffset = true) {
|
||||
if ($this->is_ntp_cron_disabled())
|
||||
return false;
|
||||
if ($this->get_bool(self::OPTION_USE_NTP, true)) {
|
||||
if ($requireOffset) {
|
||||
$offset = $this->get(self::OPTION_NTP_OFFSET, null);
|
||||
return $offset !== null && abs((int)$offset) <= Controller_TOTP::TIME_WINDOW_LENGTH;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function is_ntp_cron_disabled(&$failureCount = null) {
|
||||
if ($this->is_ntp_disabled_via_constant())
|
||||
return true;
|
||||
$failureCount = $this->get_ntp_failure_count();
|
||||
if ($failureCount >= Controller_Time::FAILURE_LIMIT) {
|
||||
return true;
|
||||
}
|
||||
else if ($failureCount < 0) {
|
||||
$failureCount = 0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function disable_ntp_cron() {
|
||||
$this->set(self::OPTION_NTP_FAILURE_COUNT, -1);
|
||||
}
|
||||
|
||||
public function are_login_history_columns_enabled() {
|
||||
return Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_ENABLE_LOGIN_HISTORY_COLUMNS, true);
|
||||
}
|
||||
|
||||
public function should_stack_ui_columns() {
|
||||
return self::shared()->get_bool(Controller_Settings::OPTION_STACK_UI_COLUMNS, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility
|
||||
*/
|
||||
|
||||
/**
|
||||
* Translates a value to a boolean, correctly interpreting various textual representations.
|
||||
*
|
||||
* @param $value
|
||||
* @return bool
|
||||
*/
|
||||
protected function _truthy_to_bool($value) {
|
||||
if ($value === true || $value === false) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return !!$value;
|
||||
}
|
||||
|
||||
if (preg_match('/^(?:f(?:alse)?|no?|off)$/i', $value)) {
|
||||
return false;
|
||||
}
|
||||
else if (preg_match('/^(?:t(?:rue)?|y(?:es)?|on)$/i', $value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !empty($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given time string and returns its DateTime with the server's configured time zone.
|
||||
*
|
||||
* @param string $timestring
|
||||
* @return \DateTime
|
||||
*/
|
||||
protected function _parse_local_time($timestring) {
|
||||
$utc = new \DateTimeZone('UTC');
|
||||
$tz = get_option('timezone_string');
|
||||
if (!empty($tz)) {
|
||||
$tz = new \DateTimeZone($tz);
|
||||
return new \DateTime($timestring, $tz);
|
||||
}
|
||||
else {
|
||||
$gmt = get_option('gmt_offset');
|
||||
if (!empty($gmt)) {
|
||||
if (PHP_VERSION_ID < 50510) {
|
||||
$timestamp = strtotime($timestring);
|
||||
$dtStr = gmdate("c", (int) ($timestamp + $gmt * 3600)); //Have to do it this way because of < PHP 5.5.10
|
||||
return new \DateTime($dtStr, $utc);
|
||||
}
|
||||
else {
|
||||
$direction = ($gmt > 0 ? '+' : '-');
|
||||
$gmt = abs($gmt);
|
||||
$h = (int) $gmt;
|
||||
$m = ($gmt - $h) * 60;
|
||||
$tz = new \DateTimeZone($direction . str_pad($h, 2, '0', STR_PAD_LEFT) . str_pad($m, 2, '0', STR_PAD_LEFT));
|
||||
return new \DateTime($timestring, $tz);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new \DateTime($timestring);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans a user-entered IP range of unnecessary characters and normalizes some glyphs.
|
||||
*
|
||||
* @param string $range
|
||||
* @return string
|
||||
*/
|
||||
protected function _sanitize_ip_range($range) {
|
||||
$range = preg_replace('/\s/', '', $range); //Strip whitespace
|
||||
$range = preg_replace('/[\\x{2013}-\\x{2015}]/u', '-', $range); //Non-hyphen dashes to hyphen
|
||||
$range = strtolower($range);
|
||||
|
||||
if (preg_match('/^\d+-\d+$/', $range)) { //v5 32 bit int style format
|
||||
list($start, $end) = explode('-', $range);
|
||||
$start = long2ip($start);
|
||||
$end = long2ip($end);
|
||||
$range = "{$start}-{$end}";
|
||||
}
|
||||
|
||||
return $range;
|
||||
}
|
||||
|
||||
private function _migrate_admin_2fa_requirements_to_roles() {
|
||||
if (!$this->get_bool(self::OPTION_REQUIRE_2FA_ADMIN))
|
||||
return;
|
||||
$time = time();
|
||||
if (is_multisite()) {
|
||||
$this->set($this->get_required_2fa_role_key('super-admin'), $time, true);
|
||||
}
|
||||
else {
|
||||
$roles = new \WP_Roles();
|
||||
foreach ($roles->roles as $key => $data) {
|
||||
$role = $roles->get_role($key);
|
||||
if (Controller_Permissions::shared()->can_role_manage_settings($role) && Controller_Permissions::shared()->allow_2fa_self($role->name)) {
|
||||
$this->set($this->get_required_2fa_role_key($role->name), $time, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->remove(self::OPTION_REQUIRE_2FA_ADMIN);
|
||||
$this->remove(self::OPTION_REQUIRE_2FA_GRACE_PERIOD);
|
||||
$this->remove(self::OPTION_REQUIRE_2FA_GRACE_PERIOD_ENABLED);
|
||||
}
|
||||
|
||||
public function reset_ntp_disabled_flag() {
|
||||
$this->remove(self::OPTION_USE_NTP);
|
||||
$this->remove(self::OPTION_NTP_OFFSET);
|
||||
$this->remove(self::OPTION_NTP_FAILURE_COUNT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Controller_Support {
|
||||
const ITEM_INDEX = 'index';
|
||||
|
||||
const ITEM_CHANGELOG = 'changelog';
|
||||
|
||||
const ITEM_VERSION_WORDPRESS = 'version-wordpress';
|
||||
const ITEM_VERSION_PHP = 'version-php';
|
||||
const ITEM_VERSION_OPENSSL = 'version-ssl';
|
||||
|
||||
const ITEM_GDPR = 'gdpr';
|
||||
const ITEM_GDPR_DPA = 'gdpr-dpa';
|
||||
|
||||
const ITEM_MODULE_LOGIN_SECURITY = 'module-login-security';
|
||||
const ITEM_MODULE_LOGIN_SECURITY_2FA = 'module-login-security-2fa';
|
||||
const ITEM_MODULE_LOGIN_SECURITY_2FA_APPS = 'module-login-security-2fa-apps';
|
||||
const ITEM_MODULE_LOGIN_SECURITY_CAPTCHA = 'module-login-security-captcha';
|
||||
const ITEM_MODULE_LOGIN_SECURITY_ROLES = 'module-login-security-roles';
|
||||
const ITEM_MODULE_LOGIN_SECURITY_OPTION_WOOCOMMERCE_ACCOUNT_INTEGRATION = 'module-login-security-option-woocommerce-account-integration';
|
||||
const ITEM_MODULE_LOGIN_SECURITY_OPTION_SHORTCODE = 'module-login-security-option-shortcode';
|
||||
const ITEM_MODULE_LOGIN_SECURITY_OPTION_STACK_UI_COLUMNS = 'module-login-security-option-stack-ui-columns';
|
||||
const ITEM_MODULE_LOGIN_SECURITY_2FA_NOTIFICATIONS = 'module-login-security-2fa-notifications';
|
||||
|
||||
public static function esc_supportURL($item = self::ITEM_INDEX) {
|
||||
return esc_url(self::supportURL($item));
|
||||
}
|
||||
|
||||
public static function supportURL($item = self::ITEM_INDEX) {
|
||||
$base = 'https://www.wordfence.com/help/';
|
||||
switch ($item) {
|
||||
case self::ITEM_INDEX:
|
||||
return 'https://www.wordfence.com/help/';
|
||||
|
||||
//These all fall through to the query format
|
||||
|
||||
case self::ITEM_VERSION_WORDPRESS:
|
||||
case self::ITEM_VERSION_PHP:
|
||||
case self::ITEM_VERSION_OPENSSL:
|
||||
|
||||
case self::ITEM_GDPR:
|
||||
case self::ITEM_GDPR_DPA:
|
||||
|
||||
case self::ITEM_MODULE_LOGIN_SECURITY:
|
||||
case self::ITEM_MODULE_LOGIN_SECURITY_2FA:
|
||||
case self::ITEM_MODULE_LOGIN_SECURITY_CAPTCHA:
|
||||
case self::ITEM_MODULE_LOGIN_SECURITY_ROLES:
|
||||
case self::ITEM_MODULE_LOGIN_SECURITY_OPTION_WOOCOMMERCE_ACCOUNT_INTEGRATION:
|
||||
case self::ITEM_MODULE_LOGIN_SECURITY_OPTION_SHORTCODE:
|
||||
case self::ITEM_MODULE_LOGIN_SECURITY_OPTION_STACK_UI_COLUMNS:
|
||||
case self::ITEM_MODULE_LOGIN_SECURITY_2FA_NOTIFICATIONS:
|
||||
return $base . '?query=' . $item;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Controller_Time {
|
||||
const NTP_VERSION = 3; // https://www.ietf.org/rfc/rfc1305.txt
|
||||
const NTP_EPOCH_CONVERT = 2208988800; //RFC 5905, page 13
|
||||
const FAILURE_LIMIT = 3;
|
||||
|
||||
/**
|
||||
* Returns the singleton Controller_Time.
|
||||
*
|
||||
* @return Controller_Time
|
||||
*/
|
||||
public static function shared() {
|
||||
static $_shared = null;
|
||||
if ($_shared === null) {
|
||||
$_shared = new Controller_Time();
|
||||
}
|
||||
return $_shared;
|
||||
}
|
||||
|
||||
public function install() {
|
||||
wp_clear_scheduled_hook('wordfence_ls_ntp_cron');
|
||||
if (is_main_site()) {
|
||||
wp_schedule_event(time() + 10, 'hourly', 'wordfence_ls_ntp_cron');
|
||||
}
|
||||
Controller_Settings::shared()->reset_ntp_disabled_flag();
|
||||
}
|
||||
|
||||
public function uninstall() {
|
||||
wp_clear_scheduled_hook('wordfence_ls_ntp_cron');
|
||||
Controller_Settings::shared()->reset_ntp_disabled_flag();
|
||||
}
|
||||
|
||||
public function init() {
|
||||
$this->_init_actions();
|
||||
}
|
||||
|
||||
public function _init_actions() {
|
||||
add_action('wordfence_ls_ntp_cron', array($this, '_wordfence_ls_ntp_cron'));
|
||||
}
|
||||
|
||||
public function _wordfence_ls_ntp_cron() {
|
||||
if (Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_ALLOW_DISABLING_NTP) && Controller_Settings::shared()->is_ntp_cron_disabled())
|
||||
return;
|
||||
$ntp = self::ntp_time();
|
||||
$time = time();
|
||||
|
||||
if ($ntp === false) {
|
||||
$failureCount = Controller_Settings::shared()->increment_ntp_failure_count();
|
||||
if ($failureCount >= self::FAILURE_LIMIT) {
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_USE_NTP, false);
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_NTP_OFFSET, 0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
Controller_Settings::shared()->reset_ntp_failure_count();
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_USE_NTP, true);
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_NTP_OFFSET, $ntp - $time);
|
||||
}
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_ALLOW_DISABLING_NTP, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current UTC timestamp, offset as needed to reflect the time retrieved from an NTP request or (if
|
||||
* running in the complete plugin) offset as needed from the Wordfence server's true time.
|
||||
*
|
||||
* @param bool|int $time The timestamp to apply any offset to. If `false`, it will use the current timestamp.
|
||||
* @return int
|
||||
*/
|
||||
public static function time($time = false) {
|
||||
if ($time === false) {
|
||||
$time = time();
|
||||
}
|
||||
|
||||
$offset = 0;
|
||||
if (Controller_Settings::shared()->is_ntp_enabled()) {
|
||||
$offset = Controller_Settings::shared()->get_int(Controller_Settings::OPTION_NTP_OFFSET);
|
||||
}
|
||||
else if (WORDFENCE_LS_FROM_CORE) {
|
||||
$offset = \wfUtils::normalizedTime($time) - $time;
|
||||
}
|
||||
|
||||
return $time + $offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current timestamp from ntp.org using the NTP protocol. If unable to (e.g., UDP connections are blocked),
|
||||
* it will return false.
|
||||
*
|
||||
* @return bool|float
|
||||
*/
|
||||
public static function ntp_time() {
|
||||
$servers = array('0.pool.ntp.org', '1.pool.ntp.org', '2.pool.ntp.org', '3.pool.ntp.org');
|
||||
|
||||
//Header - RFC 5905, page 18
|
||||
$header = '00'; //LI (leap indicator) - 2 bits: 00 for "no warning"
|
||||
$header .= sprintf('%03d', decbin(self::NTP_VERSION)); //VN (version number) - 3 bits: 011 for version 3
|
||||
$header .= '011'; //Mode (association mode) - 3 bit: 011 for "client"
|
||||
|
||||
$packet = chr(bindec($header));
|
||||
$packet .= str_repeat("\x0", 39);
|
||||
|
||||
foreach ($servers as $s) {
|
||||
$socket = @fsockopen('udp://' . $s, 123, $err_no, $err_str, 1);
|
||||
if ($socket) {
|
||||
stream_set_timeout($socket, 1);
|
||||
$remote_originate = microtime(true);
|
||||
$secondsNTP = ((int) $remote_originate) + self::NTP_EPOCH_CONVERT;
|
||||
$fractional = sprintf('%010d', round(($remote_originate - ((int) $remote_originate)) * 0x100000000));
|
||||
$packed = pack('N', $secondsNTP) . pack('N', $fractional);
|
||||
|
||||
if (@fwrite($socket, $packet . $packed)) {
|
||||
$response = fread($socket, 48);
|
||||
$local_transmitted = microtime(true);
|
||||
}
|
||||
@fclose($socket);
|
||||
|
||||
if (isset($response) && Model_Crypto::strlen($response) == 48) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($response) && Model_Crypto::strlen($response) == 48) {
|
||||
$longs = unpack("N12", $response);
|
||||
|
||||
$remote_originate_seconds = sprintf('%u', $longs[7]) - self::NTP_EPOCH_CONVERT;
|
||||
$remote_received_seconds = sprintf('%u', $longs[9]) - self::NTP_EPOCH_CONVERT;
|
||||
$remote_transmitted_seconds = sprintf('%u', $longs[11]) - self::NTP_EPOCH_CONVERT;
|
||||
|
||||
$remote_originate_fraction = sprintf('%u', $longs[8]) / 0x100000000;
|
||||
$remote_received_fraction = sprintf('%u', $longs[10]) / 0x100000000;
|
||||
$remote_transmitted_fraction = sprintf('%u', $longs[12]) / 0x100000000;
|
||||
|
||||
$remote_originate = $remote_originate_seconds + $remote_originate_fraction;
|
||||
$remote_received = $remote_received_seconds + $remote_received_fraction;
|
||||
$remote_transmitted = $remote_transmitted_seconds + $remote_transmitted_fraction;
|
||||
|
||||
$delay = (($local_transmitted - $remote_originate) / 2) - ($remote_transmitted - $remote_received);
|
||||
|
||||
$ntp_time = $remote_transmitted - $delay;
|
||||
return $ntp_time;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats and returns the given timestamp using the time zone set for the WordPress installation.
|
||||
*
|
||||
* @param string $format See the PHP docs on DateTime for the format options.
|
||||
* @param int|bool $timestamp Assumed to be in UTC. If false, defaults to the current timestamp.
|
||||
* @return string
|
||||
*/
|
||||
public static function format_local_time($format, $timestamp = false) {
|
||||
if ($timestamp === false) {
|
||||
$timestamp = self::time();
|
||||
}
|
||||
|
||||
$utc = new \DateTimeZone('UTC');
|
||||
if (!function_exists('date_timestamp_set')) {
|
||||
$dtStr = gmdate("c", (int) $timestamp); //Have to do it this way because of PHP 5.2
|
||||
$dt = new \DateTime($dtStr, $utc);
|
||||
}
|
||||
else {
|
||||
$dt = new \DateTime('now', $utc);
|
||||
$dt->setTimestamp($timestamp);
|
||||
}
|
||||
|
||||
$tz = get_option('timezone_string');
|
||||
if (!empty($tz)) {
|
||||
$dt->setTimezone(new \DateTimeZone($tz));
|
||||
}
|
||||
else {
|
||||
$gmt = get_option('gmt_offset');
|
||||
if (!empty($gmt)) {
|
||||
if (PHP_VERSION_ID < 50510) {
|
||||
$dtStr = gmdate("c", (int) ($timestamp + $gmt * 3600)); //Have to do it this way because of < PHP 5.5.10
|
||||
$dt = new \DateTime($dtStr, $utc);
|
||||
}
|
||||
else {
|
||||
$direction = ($gmt > 0 ? '+' : '-');
|
||||
$gmt = abs($gmt);
|
||||
$h = (int) $gmt;
|
||||
$m = ($gmt - $h) * 60;
|
||||
$dt->setTimezone(new \DateTimeZone($direction . str_pad($h, 2, '0', STR_PAD_LEFT) . str_pad($m, 2, '0', STR_PAD_LEFT)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return $dt->format($format);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Controller_TOTP {
|
||||
const TIME_WINDOW_LENGTH = 30;
|
||||
|
||||
/**
|
||||
* Returns the singleton Controller_TOTP.
|
||||
*
|
||||
* @return Controller_TOTP
|
||||
*/
|
||||
public static function shared() {
|
||||
static $_shared = null;
|
||||
if ($_shared === null) {
|
||||
$_shared = new Controller_TOTP();
|
||||
}
|
||||
return $_shared;
|
||||
}
|
||||
|
||||
public function init() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates a user with the given TOTP parameters.
|
||||
*
|
||||
* @param \WP_User $user
|
||||
* @param string $secret The secret as a hex string.
|
||||
* @param string[] $recovery An array of recovery codes as hex strings.
|
||||
* @param bool|int $vtime The timestamp of the verification code or false to use the current timestamp.
|
||||
*/
|
||||
public function activate_2fa($user, $secret, $recovery, $vtime = false) {
|
||||
if ($vtime === false) {
|
||||
$vtime = Controller_Time::time();
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = Controller_DB::shared()->secrets;
|
||||
$wpdb->query($wpdb->prepare("INSERT INTO `{$table}` (`user_id`, `secret`, `recovery`, `ctime`, `vtime`, `mode`) VALUES (%d, %s, %s, UNIX_TIMESTAMP(), %d, 'authenticator')", $user->ID, Model_Compat::hex2bin($secret), implode('', array_map(function($r) { return Model_Compat::hex2bin($r); }, $recovery)), $vtime));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the 2FA (or recovery) code for the given user. This will return `null` if the user does not have 2FA
|
||||
* enabled. This check will mark the code as used, preventing its use again.
|
||||
*
|
||||
* @param \WP_User $user
|
||||
* @param string $code
|
||||
* @return bool|null Returns null if the user does not have 2FA enabled, false if the code is invalid, and true if valid.
|
||||
*/
|
||||
public function validate_2fa($user, $code, $update = true) {
|
||||
global $wpdb;
|
||||
$table = Controller_DB::shared()->secrets;
|
||||
$record = $wpdb->get_row($wpdb->prepare("SELECT * FROM `{$table}` WHERE `user_id` = %d FOR UPDATE", $user->ID), ARRAY_A);
|
||||
if (!$record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (preg_match('/^(?:[a-f0-9]{4}\s*){4}$/i', $code)) { //Recovery code
|
||||
$code = strtolower(preg_replace('/\s/i', '', $code));
|
||||
$recoveryCodes = str_split(strtolower(bin2hex($record['recovery'])), 16);
|
||||
|
||||
$index = array_search($code, $recoveryCodes);
|
||||
if ($index !== false) {
|
||||
if ($update) {
|
||||
unset($recoveryCodes[$index]);
|
||||
$updatedRecoveryCodes = implode('', $recoveryCodes);
|
||||
$wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `recovery` = X%s WHERE `id` = %d", $updatedRecoveryCodes, $record['id']));
|
||||
}
|
||||
$wpdb->query('COMMIT');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (preg_match('/^(?:[0-9]{3}\s*){2}$/i', $code)) { //TOTP code
|
||||
$code = preg_replace('/\s/i', '', $code);
|
||||
$secret = bin2hex($record['secret']);
|
||||
|
||||
$matches = $this->check_code($secret, $code, floor($record['vtime'] / self::TIME_WINDOW_LENGTH));
|
||||
if ($matches !== false) {
|
||||
if ($update) {
|
||||
$wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `vtime` = %d WHERE `id` = %d", $matches, $record['id']));
|
||||
}
|
||||
$wpdb->query('COMMIT');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$wpdb->query('ROLLBACK');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not the code is valid for the given secret. If it is, it returns the time window (as a timestamp)
|
||||
* that matched. If no time windows are provided, it checks the current and one on each side.
|
||||
*
|
||||
* @param string $secret The secret as a hex string.
|
||||
* @param string $code The code.
|
||||
* @param null|int The last-used time window (as a timestamp).
|
||||
* @param null|array $windows An array of time windows or null to use the default.
|
||||
* @return bool|int The time window if matches, otherwise false.
|
||||
*/
|
||||
public function check_code($secret, $code, $previous = null, $windows = null) {
|
||||
$timeCode = floor(Controller_Time::time() / self::TIME_WINDOW_LENGTH);
|
||||
|
||||
if ($windows === null) {
|
||||
$windows = array();
|
||||
$validRange = array(-1, 1); //90 second range for authenticator
|
||||
|
||||
$lowRange = $validRange[0];
|
||||
$highRange = $validRange[1];
|
||||
for ($i = 0; $i >= $lowRange; $i--) {
|
||||
$windows[] = $timeCode + $i;
|
||||
}
|
||||
for ($i = 1; $i <= $highRange; $i++) {
|
||||
$windows[] = $timeCode + $i;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($windows as $w) {
|
||||
if ($previous !== null && $previous >= $w) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$expectedCode = $this->_generate_totp($secret, dechex($w));
|
||||
if (hash_equals($expectedCode, $code)) {
|
||||
return $w * self::TIME_WINDOW_LENGTH;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a TOTP value using the provided parameters.
|
||||
*
|
||||
* @param $key The key in hex.
|
||||
* @param $time The desired time code in hex.
|
||||
* @param int $digits The number of digits.
|
||||
* @return string The TOTP value.
|
||||
*/
|
||||
private function _generate_totp($key, $time, $digits = 6)
|
||||
{
|
||||
$time = Model_Compat::hex2bin(str_pad($time, 16, '0', STR_PAD_LEFT));
|
||||
$key = Model_Compat::hex2bin($key);
|
||||
$hash = hash_hmac('sha1', $time, $key);
|
||||
|
||||
$offset = hexdec(substr($hash, -2)) & 0xf;
|
||||
$intermediate = ( ((hexdec(substr($hash, $offset * 2, 2)) & 0x7f) << 24) |
|
||||
((hexdec(substr($hash, ($offset + 1) * 2, 2)) & 0xff) << 16) |
|
||||
((hexdec(substr($hash, ($offset + 2) * 2, 2)) & 0xff) << 8) |
|
||||
((hexdec(substr($hash, ($offset + 3) * 2, 2)) & 0xff))
|
||||
);
|
||||
$otp = $intermediate % pow(10, $digits);
|
||||
|
||||
return str_pad("{$otp}", $digits, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_2faInitializationData {
|
||||
|
||||
private $user;
|
||||
private $raw_secret;
|
||||
private $base32_secret;
|
||||
private $otp_url;
|
||||
private $recovery_codes;
|
||||
|
||||
public function __construct($user) {
|
||||
$this->user = $user;
|
||||
$this->raw_secret = Model_Crypto::random_bytes(20);
|
||||
}
|
||||
|
||||
public function get_user() {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function get_raw_secret() {
|
||||
return $this->raw_secret;
|
||||
}
|
||||
|
||||
public function get_base32_secret() {
|
||||
if ($this->base32_secret === null)
|
||||
$this->base32_secret = Utility_BaseConversion::base32_encode($this->raw_secret);
|
||||
return $this->base32_secret;
|
||||
}
|
||||
|
||||
private function generate_otp_url() {
|
||||
return "otpauth://totp/" . rawurlencode(preg_replace('~^https?://~i', '', home_url()) . ' (' . $this->user->user_login . ')') . '?secret=' . $this->get_base32_secret() . '&algorithm=SHA1&digits=6&period=30&issuer=Wordfence';
|
||||
}
|
||||
|
||||
public function get_otp_url() {
|
||||
if ($this->otp_url === null)
|
||||
$this->otp_url = $this->generate_otp_url();
|
||||
return $this->otp_url;
|
||||
}
|
||||
|
||||
public function get_recovery_codes() {
|
||||
if ($this->recovery_codes === null)
|
||||
$this->recovery_codes = Controller_Users::shared()->regenerate_recovery_codes();
|
||||
return $this->recovery_codes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
abstract class Model_Asset {
|
||||
|
||||
protected $handle;
|
||||
protected $source;
|
||||
protected $dependencies;
|
||||
protected $version;
|
||||
protected $registered = false;
|
||||
|
||||
public function __construct($handle, $source = '', $dependencies = array(), $version = false) {
|
||||
$this->handle = $handle;
|
||||
$this->source = $source;
|
||||
$this->dependencies = $dependencies;
|
||||
$this->version = $version;
|
||||
}
|
||||
|
||||
public function getSourceUrl() {
|
||||
if (empty($this->source))
|
||||
return null;
|
||||
$url = $this->source;
|
||||
if (is_string($this->version))
|
||||
$url = add_query_arg('ver', $this->version, $this->source);
|
||||
return $url;
|
||||
}
|
||||
|
||||
public abstract function enqueue();
|
||||
|
||||
public abstract function isEnqueued();
|
||||
|
||||
public abstract function renderInline();
|
||||
|
||||
public function renderInlineIfNotEnqueued() {
|
||||
if (!$this->isEnqueued())
|
||||
$this->renderInline();
|
||||
}
|
||||
|
||||
public function setRegistered() {
|
||||
$this->registered = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function register() {
|
||||
return $this->setRegistered();
|
||||
}
|
||||
|
||||
public static function js($file) {
|
||||
return self::_pluginBaseURL() . 'js/' . self::_versionedFileName($file);
|
||||
}
|
||||
|
||||
public static function css($file) {
|
||||
return self::_pluginBaseURL() . 'css/' . self::_versionedFileName($file);
|
||||
}
|
||||
|
||||
public static function img($file) {
|
||||
return self::_pluginBaseURL() . 'img/' . $file;
|
||||
}
|
||||
|
||||
protected static function _pluginBaseURL() {
|
||||
return plugins_url('', WORDFENCE_LS_FCPATH) . '/';
|
||||
}
|
||||
|
||||
protected static function _versionedFileName($subpath) {
|
||||
$version = WORDFENCE_LS_BUILD_NUMBER;
|
||||
if ($version != 'WORDFENCE_LS_BUILD_NUMBER' && preg_match('/^(.+?)(\.[^\.]+)$/', $subpath, $matches)) {
|
||||
$prefix = $matches[1];
|
||||
$suffix = $matches[2];
|
||||
return $prefix . '.' . $version . $suffix;
|
||||
}
|
||||
|
||||
return $subpath;
|
||||
}
|
||||
|
||||
public static function create($handle, $source = '', $dependencies = array(), $version = false) {
|
||||
return new static($handle, $source, $dependencies, $version);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_Compat {
|
||||
public static function hex2bin($string) { //Polyfill for PHP < 5.4
|
||||
if (!is_string($string)) { return false; }
|
||||
if (strlen($string) % 2 == 1) { return false; }
|
||||
return pack('H*', $string);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
abstract class Model_Crypto {
|
||||
/**
|
||||
* Refreshes the secrets used by the plugin.
|
||||
*/
|
||||
public static function refresh_secrets() {
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_SHARED_HASH_SECRET_KEY, bin2hex(self::random_bytes(32)));
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_SHARED_SYMMETRIC_SECRET_KEY, bin2hex(self::random_bytes(32)));
|
||||
Controller_Settings::shared()->set(Controller_Settings::OPTION_LAST_SECRET_REFRESH, Controller_Time::time(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the secret for hashing.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function shared_hash_secret() {
|
||||
return Controller_Settings::shared()->get(Controller_Settings::OPTION_SHARED_HASH_SECRET_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the secret for symmetric encryption.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function shared_symmetric_secret() {
|
||||
return Controller_Settings::shared()->get(Controller_Settings::OPTION_SHARED_SYMMETRIC_SECRET_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the installation has the required crypto support for this to work.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_required_crypto_functions() {
|
||||
if (function_exists('openssl_get_publickey') && function_exists('openssl_get_cipher_methods')) {
|
||||
$ciphers = openssl_get_cipher_methods();
|
||||
return array_search('aes-256-cbc', $ciphers) !== false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility
|
||||
*/
|
||||
|
||||
public static function random_bytes($bytes) {
|
||||
$bytes = (int) $bytes;
|
||||
if (function_exists('random_bytes')) {
|
||||
try {
|
||||
$rand = random_bytes($bytes);
|
||||
if (is_string($rand) && self::strlen($rand) === $bytes) {
|
||||
return $rand;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Fall through
|
||||
} catch (\TypeError $e) {
|
||||
// Fall through
|
||||
} catch (\Error $e) {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
if (function_exists('mcrypt_create_iv')) {
|
||||
// phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.mcrypt_create_ivDeprecatedRemoved,PHPCompatibility.Extensions.RemovedExtensions.mcryptDeprecatedRemoved,PHPCompatibility.Constants.RemovedConstants.mcrypt_dev_urandomDeprecatedRemoved
|
||||
$rand = @mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM);
|
||||
if (is_string($rand) && self::strlen($rand) === $bytes) {
|
||||
return $rand;
|
||||
}
|
||||
}
|
||||
if (function_exists('openssl_random_pseudo_bytes')) {
|
||||
$rand = @openssl_random_pseudo_bytes($bytes, $strong);
|
||||
if (is_string($rand) && self::strlen($rand) === $bytes) {
|
||||
return $rand;
|
||||
}
|
||||
}
|
||||
// Last resort is insecure
|
||||
$return = '';
|
||||
for ($i = 0; $i < $bytes; $i++) {
|
||||
$return .= chr(mt_rand(0, 255));
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill for random_int.
|
||||
*
|
||||
* @param int $min
|
||||
* @param int $max
|
||||
* @return int
|
||||
*/
|
||||
public static function random_int($min = 0, $max = 0x7FFFFFFF) {
|
||||
if (function_exists('random_int')) {
|
||||
try {
|
||||
return random_int($min, $max);
|
||||
} catch (\Exception $e) {
|
||||
// Fall through
|
||||
} catch (\TypeError $e) {
|
||||
// Fall through
|
||||
} catch (\Error $e) {
|
||||
// Fall through
|
||||
}
|
||||
}
|
||||
$diff = $max - $min;
|
||||
$bytes = self::random_bytes(4);
|
||||
if ($bytes === false || self::strlen($bytes) != 4) {
|
||||
throw new \RuntimeException("Unable to get 4 bytes");
|
||||
}
|
||||
$val = @unpack("Nint", $bytes);
|
||||
$val = $val['int'] & 0x7FFFFFFF;
|
||||
$fp = (float) $val / 2147483647.0; // convert to [0,1]
|
||||
return (int) (round($fp * $diff) + $min);
|
||||
}
|
||||
|
||||
public static function uuid() {
|
||||
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||
// 32 bits for "time_low"
|
||||
self::random_int(0, 0xffff), self::random_int(0, 0xffff),
|
||||
|
||||
// 16 bits for "time_mid"
|
||||
self::random_int(0, 0xffff),
|
||||
|
||||
// 16 bits for "time_hi_and_version",
|
||||
// four most significant bits holds version number 4
|
||||
self::random_int(0, 0x0fff) | 0x4000,
|
||||
|
||||
// 16 bits, 8 bits for "clk_seq_hi_res",
|
||||
// 8 bits for "clk_seq_low",
|
||||
// two most significant bits holds zero and one for variant DCE1.1
|
||||
self::random_int(0, 0x3fff) | 0x8000,
|
||||
|
||||
// 48 bits for "node"
|
||||
self::random_int(0, 0xffff), self::random_int(0, 0xffff), self::random_int(0, 0xffff)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mbstring internal encoding to a binary safe encoding when func_overload
|
||||
* is enabled.
|
||||
*
|
||||
* When mbstring.func_overload is in use for multi-byte encodings, the results from
|
||||
* strlen() and similar functions respect the utf8 characters, causing binary data
|
||||
* to return incorrect lengths.
|
||||
*
|
||||
* This function overrides the mbstring encoding to a binary-safe encoding, and
|
||||
* resets it to the users expected encoding afterwards through the
|
||||
* `reset_mbstring_encoding` function.
|
||||
*
|
||||
* It is safe to recursively call this function, however each
|
||||
* `_mbstring_binary_safe_encoding()` call must be followed up with an equal number
|
||||
* of `_reset_mbstring_encoding()` calls.
|
||||
*
|
||||
* @see Model_Crypto::_reset_mbstring_encoding
|
||||
*
|
||||
* @staticvar array $encodings
|
||||
* @staticvar bool $overloaded
|
||||
*
|
||||
* @param bool $reset Optional. Whether to reset the encoding back to a previously-set encoding.
|
||||
* Default false.
|
||||
*/
|
||||
protected static function _mbstring_binary_safe_encoding($reset = false) {
|
||||
static $encodings = array();
|
||||
static $overloaded = null;
|
||||
|
||||
if (is_null($overloaded)) {
|
||||
// phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.mbstring_func_overloadDeprecated
|
||||
$overloaded = function_exists('mb_internal_encoding') && (ini_get('mbstring.func_overload') & 2);
|
||||
}
|
||||
|
||||
if (false === $overloaded) { return; }
|
||||
|
||||
if (!$reset) {
|
||||
$encoding = mb_internal_encoding();
|
||||
array_push($encodings, $encoding);
|
||||
mb_internal_encoding('ISO-8859-1');
|
||||
}
|
||||
|
||||
if ($reset && $encodings) {
|
||||
$encoding = array_pop($encodings);
|
||||
mb_internal_encoding($encoding);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the mbstring internal encoding to a users previously set encoding.
|
||||
*
|
||||
* @see Model_Crypto::_mbstring_binary_safe_encoding
|
||||
*/
|
||||
protected static function _reset_mbstring_encoding() {
|
||||
self::_mbstring_binary_safe_encoding(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable $function
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
protected static function _call_mb_string_function($function, $args) {
|
||||
self::_mbstring_binary_safe_encoding();
|
||||
$return = call_user_func_array($function, $args);
|
||||
self::_reset_mbstring_encoding();
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multibyte safe strlen.
|
||||
*
|
||||
* @param $binary
|
||||
* @return int
|
||||
*/
|
||||
public static function strlen($binary) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('strlen', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $haystack
|
||||
* @param $needle
|
||||
* @param int $offset
|
||||
* @return int
|
||||
*/
|
||||
public static function stripos($haystack, $needle, $offset = 0) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('stripos', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
* @return mixed
|
||||
*/
|
||||
public static function strtolower($string) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('strtolower', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
* @param $start
|
||||
* @param $length
|
||||
* @return mixed
|
||||
*/
|
||||
public static function substr($string, $start, $length = null) {
|
||||
if ($length === null) { $length = self::strlen($string); }
|
||||
return self::_call_mb_string_function('substr', array(
|
||||
$string, $start, $length
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $haystack
|
||||
* @param $needle
|
||||
* @param int $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public static function strpos($haystack, $needle, $offset = 0) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('strpos', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @param int $offset
|
||||
* @param int $length
|
||||
* @return mixed
|
||||
*/
|
||||
public static function substr_count($haystack, $needle, $offset = 0, $length = null) {
|
||||
if ($length === null) { $length = self::strlen($haystack); }
|
||||
return self::_call_mb_string_function('substr_count', array(
|
||||
$haystack, $needle, $offset, $length
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
* @return mixed
|
||||
*/
|
||||
public static function strtoupper($string) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('strtoupper', $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @param int $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public static function strrpos($haystack, $needle, $offset = 0) {
|
||||
$args = func_get_args();
|
||||
return self::_call_mb_string_function('strrpos', $args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Crypto;
|
||||
/**
|
||||
* Binary-to-text PHP Utilities
|
||||
*
|
||||
* @package binary-to-text-php
|
||||
* @link https://github.com/ademarre/binary-to-text-php
|
||||
* @author Andre DeMarre
|
||||
* @copyright 2009-2013 Andre DeMarre
|
||||
* @license http://opensource.org/licenses/MIT MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for binary-to-text encoding with a base of 2^n
|
||||
*
|
||||
* The Base2n class is for binary-to-text conversion. It employs a
|
||||
* generalization of the algorithms used by many encoding schemes that
|
||||
* use a fixed number of bits to encode each character. In other words,
|
||||
* the base is a power of 2.
|
||||
*
|
||||
* Earlier versions of this class were named
|
||||
* FixedBitNotation and FixedBitEncoding.
|
||||
*
|
||||
* @package binary-to-text-php
|
||||
*/
|
||||
class Model_Base2n
|
||||
{
|
||||
protected $_chars;
|
||||
protected $_bitsPerCharacter;
|
||||
protected $_radix;
|
||||
protected $_rightPadFinalBits;
|
||||
protected $_padFinalGroup;
|
||||
protected $_padCharacter;
|
||||
protected $_caseSensitive;
|
||||
protected $_charmap;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param integer $bitsPerCharacter Bits to use for each encoded character
|
||||
* @param string $chars Base character alphabet
|
||||
* @param boolean $caseSensitive To decode in a case-sensitive manner
|
||||
* @param boolean $rightPadFinalBits How to encode last character
|
||||
* @param boolean $padFinalGroup Add padding to end of encoded output
|
||||
* @param string $padCharacter Character to use for padding
|
||||
*
|
||||
* @throws InvalidArgumentException for incompatible parameters
|
||||
*/
|
||||
public function __construct(
|
||||
$bitsPerCharacter,
|
||||
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_',
|
||||
$caseSensitive = TRUE, $rightPadFinalBits = FALSE,
|
||||
$padFinalGroup = FALSE, $padCharacter = '=')
|
||||
{
|
||||
// Ensure validity of $chars
|
||||
if (!is_string($chars) || ($charLength = strlen($chars)) < 2) {
|
||||
throw new \InvalidArgumentException('$chars must be a string of at least two characters');
|
||||
}
|
||||
|
||||
// Ensure validity of $padCharacter
|
||||
if ($padFinalGroup) {
|
||||
if (!is_string($padCharacter) || !isset($padCharacter[0])) {
|
||||
throw new \InvalidArgumentException('$padCharacter must be a string of one character');
|
||||
}
|
||||
|
||||
if ($caseSensitive) {
|
||||
$padCharFound = strpos($chars, $padCharacter[0]);
|
||||
} else {
|
||||
$padCharFound = stripos($chars, $padCharacter[0]);
|
||||
}
|
||||
|
||||
if ($padCharFound !== FALSE) {
|
||||
throw new \InvalidArgumentException('$padCharacter can not be a member of $chars');
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure validity of $bitsPerCharacter
|
||||
if (!is_int($bitsPerCharacter)) {
|
||||
throw new \InvalidArgumentException('$bitsPerCharacter must be an integer');
|
||||
}
|
||||
|
||||
if ($bitsPerCharacter < 1) {
|
||||
// $bitsPerCharacter must be at least 1
|
||||
throw new \InvalidArgumentException('$bitsPerCharacter can not be less than 1');
|
||||
|
||||
} elseif ($charLength < 1 << $bitsPerCharacter) {
|
||||
// Character length of $chars is too small for $bitsPerCharacter
|
||||
// Find greatest acceptable value of $bitsPerCharacter
|
||||
$bitsPerCharacter = 1;
|
||||
$radix = 2;
|
||||
|
||||
while ($charLength >= ($radix <<= 1) && $bitsPerCharacter < 8) {
|
||||
$bitsPerCharacter++;
|
||||
}
|
||||
|
||||
$radix >>= 1;
|
||||
throw new \InvalidArgumentException(
|
||||
'$bitsPerCharacter can not be more than ' . $bitsPerCharacter
|
||||
. ' given $chars length of ' . $charLength
|
||||
. ' (max radix ' . $radix . ')');
|
||||
|
||||
} elseif ($bitsPerCharacter > 8) {
|
||||
// $bitsPerCharacter must not be greater than 8
|
||||
throw new \InvalidArgumentException('$bitsPerCharacter can not be greater than 8');
|
||||
|
||||
} else {
|
||||
$radix = 1 << $bitsPerCharacter;
|
||||
}
|
||||
|
||||
$this->_chars = $chars;
|
||||
$this->_bitsPerCharacter = $bitsPerCharacter;
|
||||
$this->_radix = $radix;
|
||||
$this->_rightPadFinalBits = $rightPadFinalBits;
|
||||
$this->_padFinalGroup = $padFinalGroup;
|
||||
$this->_padCharacter = $padCharacter[0];
|
||||
$this->_caseSensitive = $caseSensitive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a string
|
||||
*
|
||||
* @param string $rawString Binary data to encode
|
||||
* @return string
|
||||
*/
|
||||
public function encode($rawString)
|
||||
{
|
||||
// Unpack string into an array of bytes
|
||||
$bytes = unpack('C*', $rawString);
|
||||
$byteCount = count($bytes);
|
||||
|
||||
$encodedString = '';
|
||||
$byte = array_shift($bytes);
|
||||
$bitsRead = 0;
|
||||
$oldBits = 0;
|
||||
|
||||
$chars = $this->_chars;
|
||||
$bitsPerCharacter = $this->_bitsPerCharacter;
|
||||
$rightPadFinalBits = $this->_rightPadFinalBits;
|
||||
$padFinalGroup = $this->_padFinalGroup;
|
||||
$padCharacter = $this->_padCharacter;
|
||||
|
||||
$charsPerByte = 8 / $bitsPerCharacter;
|
||||
$encodedLength = $byteCount * $charsPerByte;
|
||||
|
||||
// Generate encoded output; each loop produces one encoded character
|
||||
for ($c = 0; $c < $encodedLength; $c++) {
|
||||
|
||||
// Get the bits needed for this encoded character
|
||||
if ($bitsRead + $bitsPerCharacter > 8) {
|
||||
// Not enough bits remain in this byte for the current character
|
||||
// Save the remaining bits before getting the next byte
|
||||
$oldBitCount = 8 - $bitsRead;
|
||||
$oldBits = $byte ^ ($byte >> $oldBitCount << $oldBitCount);
|
||||
$newBitCount = $bitsPerCharacter - $oldBitCount;
|
||||
|
||||
if (!$bytes) {
|
||||
// Last bits; match final character and exit loop
|
||||
if ($rightPadFinalBits) $oldBits <<= $newBitCount;
|
||||
$encodedString .= $chars[$oldBits];
|
||||
|
||||
if ($padFinalGroup) {
|
||||
// Array of the lowest common multiples of $bitsPerCharacter and 8, divided by 8
|
||||
$lcmMap = array(1 => 1, 2 => 1, 3 => 3, 4 => 1, 5 => 5, 6 => 3, 7 => 7, 8 => 1);
|
||||
$bytesPerGroup = $lcmMap[$bitsPerCharacter];
|
||||
$pads = $bytesPerGroup * $charsPerByte - ceil((strlen($rawString) % $bytesPerGroup) * $charsPerByte);
|
||||
$encodedString .= str_repeat($padCharacter, $pads);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Get next byte
|
||||
$byte = array_shift($bytes);
|
||||
$bitsRead = 0;
|
||||
|
||||
} else {
|
||||
$oldBitCount = 0;
|
||||
$newBitCount = $bitsPerCharacter;
|
||||
}
|
||||
|
||||
// Read only the needed bits from this byte
|
||||
$bits = $byte >> 8 - ($bitsRead + ($newBitCount));
|
||||
$bits ^= $bits >> $newBitCount << $newBitCount;
|
||||
$bitsRead += $newBitCount;
|
||||
|
||||
if ($oldBitCount) {
|
||||
// Bits come from seperate bytes, add $oldBits to $bits
|
||||
$bits = ($oldBits << $newBitCount) | $bits;
|
||||
}
|
||||
|
||||
$encodedString .= $chars[$bits];
|
||||
}
|
||||
|
||||
return $encodedString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a string
|
||||
*
|
||||
* @param string $encodedString Data to decode
|
||||
* @param boolean $strict Returns NULL if $encodedString contains an undecodable character
|
||||
* @return string
|
||||
*/
|
||||
public function decode($encodedString, $strict = FALSE)
|
||||
{
|
||||
if (!$encodedString || !is_string($encodedString)) {
|
||||
// Empty string, nothing to decode
|
||||
return '';
|
||||
}
|
||||
|
||||
$chars = $this->_chars;
|
||||
$bitsPerCharacter = $this->_bitsPerCharacter;
|
||||
$radix = $this->_radix;
|
||||
$rightPadFinalBits = $this->_rightPadFinalBits;
|
||||
$padFinalGroup = $this->_padFinalGroup;
|
||||
$padCharacter = $this->_padCharacter;
|
||||
$caseSensitive = $this->_caseSensitive;
|
||||
|
||||
// Get index of encoded characters
|
||||
if ($this->_charmap) {
|
||||
$charmap = $this->_charmap;
|
||||
|
||||
} else {
|
||||
$charmap = array();
|
||||
|
||||
for ($i = 0; $i < $radix; $i++) {
|
||||
$charmap[$chars[$i]] = $i;
|
||||
}
|
||||
|
||||
$this->_charmap = $charmap;
|
||||
}
|
||||
|
||||
// The last encoded character is $encodedString[$lastNotatedIndex]
|
||||
$lastNotatedIndex = strlen($encodedString) - 1;
|
||||
|
||||
// Remove trailing padding characters
|
||||
if ($padFinalGroup) {
|
||||
while ($encodedString[$lastNotatedIndex] === $padCharacter) {
|
||||
$encodedString = substr($encodedString, 0, $lastNotatedIndex);
|
||||
$lastNotatedIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
$rawString = '';
|
||||
$byte = 0;
|
||||
$bitsWritten = 0;
|
||||
|
||||
// Convert each encoded character to a series of unencoded bits
|
||||
for ($c = 0; $c <= $lastNotatedIndex; $c++) {
|
||||
|
||||
if (!$caseSensitive && !isset($charmap[$encodedString[$c]])) {
|
||||
// Encoded character was not found; try other case
|
||||
if (isset($charmap[$cUpper = strtoupper($encodedString[$c])])) {
|
||||
$charmap[$encodedString[$c]] = $charmap[$cUpper];
|
||||
|
||||
} elseif (isset($charmap[$cLower = strtolower($encodedString[$c])])) {
|
||||
$charmap[$encodedString[$c]] = $charmap[$cLower];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($charmap[$encodedString[$c]])) {
|
||||
$bitsNeeded = 8 - $bitsWritten;
|
||||
$unusedBitCount = $bitsPerCharacter - $bitsNeeded;
|
||||
|
||||
// Get the new bits ready
|
||||
if ($bitsNeeded > $bitsPerCharacter) {
|
||||
// New bits aren't enough to complete a byte; shift them left into position
|
||||
$newBits = $charmap[$encodedString[$c]] << $bitsNeeded - $bitsPerCharacter;
|
||||
$bitsWritten += $bitsPerCharacter;
|
||||
|
||||
} elseif ($c !== $lastNotatedIndex || $rightPadFinalBits) {
|
||||
// Zero or more too many bits to complete a byte; shift right
|
||||
$newBits = $charmap[$encodedString[$c]] >> $unusedBitCount;
|
||||
$bitsWritten = 8; //$bitsWritten += $bitsNeeded;
|
||||
|
||||
} else {
|
||||
// Final bits don't need to be shifted
|
||||
$newBits = $charmap[$encodedString[$c]];
|
||||
$bitsWritten = 8;
|
||||
}
|
||||
|
||||
$byte |= $newBits;
|
||||
|
||||
if ($bitsWritten === 8 || $c === $lastNotatedIndex) {
|
||||
// Byte is ready to be written
|
||||
$rawString .= pack('C', $byte);
|
||||
|
||||
if ($c !== $lastNotatedIndex) {
|
||||
// Start the next byte
|
||||
$bitsWritten = $unusedBitCount;
|
||||
$byte = ($charmap[$encodedString[$c]] ^ ($newBits << $unusedBitCount)) << 8 - $bitsWritten;
|
||||
}
|
||||
}
|
||||
|
||||
} elseif ($strict) {
|
||||
// Unable to decode character; abort
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return $rawString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Crypto;
|
||||
|
||||
use WordfenceLS\Controller_Time;
|
||||
use WordfenceLS\Model_Crypto;
|
||||
|
||||
/**
|
||||
* Class Model_JWT
|
||||
* @package Wordfence2FA\Crypto
|
||||
* @property array $payload
|
||||
* @property int $expiration
|
||||
*/
|
||||
class Model_JWT {
|
||||
private $_payload;
|
||||
private $_expiration;
|
||||
|
||||
/**
|
||||
* Decodes and returns the payload of a JWT. This also validates the signature and expiration. Currently assumes HS256 JWTs.
|
||||
*
|
||||
* @param string $token
|
||||
* @return Model_JWT|bool The decoded JWT or false if the token is invalid or fails validation.
|
||||
*/
|
||||
public static function decode_jwt($token) {
|
||||
$components = explode('.', $token);
|
||||
if (count($components) != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = Model_Crypto::shared_hash_secret();
|
||||
$body = $components[0] . '.' . $components[1];
|
||||
$signature = hash_hmac('sha256', $body, $key, true);
|
||||
$testSignature = self::base64url_decode($components[2]);
|
||||
if (!hash_equals($signature, $testSignature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$json = self::base64url_decode($components[1]);
|
||||
$payload = @json_decode($json, true);
|
||||
$expiration = false;
|
||||
if (isset($payload['_exp'])) {
|
||||
$expiration = $payload['_exp'];
|
||||
|
||||
if ($payload['_exp'] < Controller_Time::time()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($payload['_exp']);
|
||||
}
|
||||
|
||||
return new self($payload, $expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model_JWT constructor.
|
||||
*
|
||||
* @param array $payload
|
||||
* @param bool|int $expiration
|
||||
*/
|
||||
public function __construct($payload, $expiration = false) {
|
||||
$this->_payload = $payload;
|
||||
$this->_expiration = $expiration;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
$payload = $this->_payload;
|
||||
if ($this->_expiration !== false) {
|
||||
$payload['_exp'] = $this->_expiration;
|
||||
}
|
||||
$key = Model_Crypto::shared_hash_secret();
|
||||
$header = '{"alg":"HS256","typ":"JWT"}';
|
||||
$body = self::base64url_encode($header) . '.' . self::base64url_encode(json_encode($payload));
|
||||
$signature = hash_hmac('sha256', $body, $key, true);
|
||||
return $body . '.' . self::base64url_encode($signature);
|
||||
}
|
||||
|
||||
public function __isset($key) {
|
||||
switch ($key) {
|
||||
case 'payload':
|
||||
case 'expiration':
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Invalid key: ' . $key);
|
||||
}
|
||||
|
||||
public function __get($key) {
|
||||
switch ($key) {
|
||||
case 'payload':
|
||||
return $this->_payload;
|
||||
case 'expiration':
|
||||
return $this->_expiration;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Invalid key: ' . $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base64URL-encodes the given payload. This is identical to base64_encode except it substitutes characters
|
||||
* not safe for use in URLs.
|
||||
*
|
||||
* @param string $payload
|
||||
* @return string
|
||||
*/
|
||||
public static function base64url_encode($payload) {
|
||||
return self::base64url_convert_to(base64_encode($payload));
|
||||
}
|
||||
|
||||
public static function base64url_convert_to($base64) {
|
||||
$intermediate = rtrim($base64, '=');
|
||||
$intermediate = str_replace('+', '-', $intermediate);
|
||||
$intermediate = str_replace('/', '_', $intermediate);
|
||||
return $intermediate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64URL-decodes the given payload. This is identical to base64_encode except it allows for the characters
|
||||
* substituted by base64url_encode.
|
||||
*
|
||||
* @param string $payload
|
||||
* @return string
|
||||
*/
|
||||
public static function base64url_decode($payload) {
|
||||
return base64_decode(self::base64url_convert_from($payload));
|
||||
}
|
||||
|
||||
public static function base64url_convert_from($base64url) {
|
||||
$intermediate = str_replace('_', '/', $base64url);
|
||||
$intermediate = str_replace('-', '+', $intermediate);
|
||||
return $intermediate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Crypto;
|
||||
|
||||
use WordfenceLS\Model_Crypto;
|
||||
|
||||
abstract class Model_Symmetric {
|
||||
/**
|
||||
* Returns $data encrypted with the shared symmetric key or false if unable to do so.
|
||||
*
|
||||
* @param string $data
|
||||
* @return bool|array
|
||||
*/
|
||||
public static function encrypt($data) {
|
||||
if (!Model_Crypto::has_required_crypto_functions()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$symmetricKey = Model_Crypto::shared_symmetric_secret();
|
||||
$iv = Model_Crypto::random_bytes(16);
|
||||
$encrypted = @openssl_encrypt($data, 'aes-256-cbc', $symmetricKey, OPENSSL_RAW_DATA, $iv);
|
||||
if ($encrypted) {
|
||||
return array('data' => base64_encode($encrypted), 'iv' => base64_encode($iv));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decrypted value of a payload encrypted by Model_Symmetric::encrypt
|
||||
*
|
||||
* @param array $encrypted
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function decrypt($encrypted) {
|
||||
if (!Model_Crypto::has_required_crypto_functions()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($encrypted['data']) || !isset($encrypted['iv'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$symmetricKey = Model_Crypto::shared_symmetric_secret();
|
||||
$iv = base64_decode($encrypted['iv']);
|
||||
$encrypted = base64_decode($encrypted['data']);
|
||||
$data = @openssl_decrypt($encrypted, 'aes-256-cbc', $symmetricKey, OPENSSL_RAW_DATA, $iv);
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_IP {
|
||||
/**
|
||||
* Returns the human-readable representation of a packed binary IP address.
|
||||
*
|
||||
* @param string $ip
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function inet_ntop($ip) {
|
||||
if (Model_Crypto::strlen($ip) == 16 && Model_Crypto::substr($ip, 0, 12) == "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff") {
|
||||
$ip = Model_Crypto::substr($ip, 12, 4);
|
||||
}
|
||||
|
||||
if (self::has_ipv6()) {
|
||||
return @inet_ntop($ip);
|
||||
}
|
||||
|
||||
// IPv4
|
||||
if (Model_Crypto::strlen($ip) === 4) {
|
||||
return ord($ip[0]) . '.' . ord($ip[1]) . '.' . ord($ip[2]) . '.' . ord($ip[3]);
|
||||
}
|
||||
|
||||
// IPv6
|
||||
if (Model_Crypto::strlen($ip) === 16) {
|
||||
// IPv4 mapped IPv6
|
||||
if (Model_Crypto::substr($ip, 0, 12) == "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff") {
|
||||
return "::ffff:" . ord($ip[12]) . '.' . ord($ip[13]) . '.' . ord($ip[14]) . '.' . ord($ip[15]);
|
||||
}
|
||||
|
||||
$hex = bin2hex($ip);
|
||||
$groups = str_split($hex, 4);
|
||||
$in_collapse = false;
|
||||
$done_collapse = false;
|
||||
foreach ($groups as $index => $group) {
|
||||
if ($group == '0000' && !$done_collapse) {
|
||||
if ($in_collapse) {
|
||||
$groups[$index] = '';
|
||||
continue;
|
||||
}
|
||||
$groups[$index] = ':';
|
||||
$in_collapse = true;
|
||||
continue;
|
||||
}
|
||||
if ($in_collapse) {
|
||||
$done_collapse = true;
|
||||
}
|
||||
$groups[$index] = ltrim($groups[$index], '0');
|
||||
if (strlen($groups[$index]) === 0) {
|
||||
$groups[$index] = '0';
|
||||
}
|
||||
}
|
||||
$ip = join(':', array_filter($groups, 'strlen'));
|
||||
$ip = str_replace(':::', '::', $ip);
|
||||
return $ip == ':' ? '::' : $ip;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the packed binary representation of an IP address from the human readable version.
|
||||
*
|
||||
* @param string $ip
|
||||
* @return string
|
||||
*/
|
||||
public static function inet_pton($ip) {
|
||||
if (self::has_ipv6()) {
|
||||
$pton = @inet_pton($ip);
|
||||
if ($pton === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (preg_match('/^(?:\d{1,3}(?:\.|$)){4}/', $ip)) { // IPv4
|
||||
$octets = explode('.', $ip);
|
||||
$pton = chr($octets[0]) . chr($octets[1]) . chr($octets[2]) . chr($octets[3]);
|
||||
}
|
||||
else if (preg_match('/^((?:[\da-f]{1,4}(?::|)){0,8})(::)?((?:[\da-f]{1,4}(?::|)){0,8})$/i', $ip)) { // IPv6
|
||||
if ($ip === '::') {
|
||||
$pton = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
||||
}
|
||||
else {
|
||||
$colon_count = substr_count($ip, ':');
|
||||
$dbl_colon_pos = strpos($ip, '::');
|
||||
if ($dbl_colon_pos !== false) {
|
||||
$ip = str_replace('::', str_repeat(':0000', (($dbl_colon_pos === 0 || $dbl_colon_pos === strlen($ip) - 2) ? 9 : 8) - $colon_count) . ':', $ip);
|
||||
$ip = trim($ip, ':');
|
||||
}
|
||||
|
||||
$ip_groups = explode(':', $ip);
|
||||
$ipv6_bin = '';
|
||||
foreach ($ip_groups as $ip_group) {
|
||||
$ipv6_bin .= pack('H*', str_pad($ip_group, 4, '0', STR_PAD_LEFT));
|
||||
}
|
||||
|
||||
if (Model_Crypto::strlen($ipv6_bin) == 16) {
|
||||
$pton = $ipv6_bin;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (preg_match('/^(?:\:(?:\:0{1,4}){0,4}\:|(?:0{1,4}\:){5})ffff\:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $ip, $matches)) { // IPv4 mapped IPv6
|
||||
$octets = explode('.', $matches[1]);
|
||||
$pton = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" . chr($octets[0]) . chr($octets[1]) . chr($octets[2]) . chr($octets[3]);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$pton = str_pad($pton, 16, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00", STR_PAD_LEFT);
|
||||
return $pton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify PHP was compiled with IPv6 support.
|
||||
*
|
||||
* Some hosts appear to not have inet_ntop, and others appear to have inet_ntop but are unable to process IPv6 addresses.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function has_ipv6() {
|
||||
return defined('AF_INET6');
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a compressed printable representation of an IPv6 address.
|
||||
*
|
||||
* @param string $ip
|
||||
* @return string
|
||||
*/
|
||||
public static function expand_ipv6_address($ip) {
|
||||
$hex = bin2hex(self::inet_pton($ip));
|
||||
$ip = substr(preg_replace("/([a-f0-9]{4})/i", "$1:", $hex), 0, -1);
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the IP is a valid format.
|
||||
*
|
||||
* @param string $ip
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid_ip($ip) {
|
||||
return filter_var($ip, FILTER_VALIDATE_IP) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the range is a valid CIDR range.
|
||||
*
|
||||
* @param string $range
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_valid_cidr_range($range) {
|
||||
$components = explode('/', $range);
|
||||
if (count($components) != 2) { return false; }
|
||||
|
||||
list($ip, $prefix) = $components;
|
||||
if (!self::is_valid_ip($ip)) { return false; }
|
||||
|
||||
if (!preg_match('/^\d+$/', $prefix)) { return false; }
|
||||
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
if ($prefix < 0 || $prefix > 32) { return false; }
|
||||
}
|
||||
else {
|
||||
if ($prefix < 1 || $prefix > 128) { return false; }
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the IP is in the IPv6-mapped-IPv4 format.
|
||||
*
|
||||
* @param string $ip
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_ipv6_mapped_ipv4($ip) {
|
||||
return preg_match('/^(?:\:(?:\:0{1,4}){0,4}\:|(?:0{1,4}\:){5})ffff\:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/i', $ip) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_Notice {
|
||||
const SEVERITY_CRITICAL = 'critical';
|
||||
const SEVERITY_WARNING = 'warning';
|
||||
const SEVERITY_INFO = 'info';
|
||||
|
||||
private $_id;
|
||||
private $_severity;
|
||||
private $_messageHTML;
|
||||
private $_category;
|
||||
|
||||
public function __construct($id, $severity, $messageHTML, $category) {
|
||||
$this->_id = $id;
|
||||
$this->_severity = $severity;
|
||||
$this->_messageHTML = $messageHTML;
|
||||
$this->_category = $category;
|
||||
}
|
||||
|
||||
public function display_notice() {
|
||||
$severityClass = 'notice-info';
|
||||
if ($this->_severity == self::SEVERITY_CRITICAL) {
|
||||
$severityClass = 'notice-error';
|
||||
}
|
||||
else if ($this->_severity == self::SEVERITY_WARNING) {
|
||||
$severityClass = 'notice-warning';
|
||||
}
|
||||
|
||||
echo '<div class="wfls-notice notice ' . $severityClass . '" data-notice-id="' . esc_attr($this->_id) . '" data-notice-type="' . esc_attr($this->_category) . '"><p>' . $this->_messageHTML . '</p><p>' . sprintf(__('<a class="wfls-btn wfls-btn-default wfls-btn-sm wfls-dismiss-link" href="#" onclick="GWFLS.dismiss_notice(\'%s\'); return false;">Dismiss</a>', 'wordfence-2fa'), esc_attr($this->_id)) . '</p></div>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_Request {
|
||||
const IP_SOURCE_AUTOMATIC = '';
|
||||
const IP_SOURCE_REMOTE_ADDR = 'REMOTE_ADDR';
|
||||
const IP_SOURCE_X_FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR';
|
||||
const IP_SOURCE_X_REAL_IP = 'HTTP_X_REAL_IP';
|
||||
|
||||
private $_cachedIP;
|
||||
|
||||
public static function current() {
|
||||
return new Model_Request();
|
||||
}
|
||||
|
||||
public function detected_ip_preview($source = null, $trusted_proxies = null) {
|
||||
if ($source === null) {
|
||||
$source = Controller_Settings::shared()->get(Controller_Settings::OPTION_IP_SOURCE);
|
||||
}
|
||||
|
||||
$record = $this->_ip($source);
|
||||
if (is_array($record)) {
|
||||
list($ip, $variable) = $record;
|
||||
if (isset($_SERVER[$variable]) && strpos($_SERVER[$variable], ',') !== false) {
|
||||
$items = preg_replace('/[\s,]/', '', explode(',', $_SERVER[$variable]));
|
||||
$output = '';
|
||||
foreach ($items as $i) {
|
||||
if ($ip == $i) {
|
||||
$output .= ', <strong>' . esc_html($i) . '</strong>';
|
||||
}
|
||||
else {
|
||||
$output .= ', ' . esc_html($i);
|
||||
}
|
||||
}
|
||||
|
||||
return substr($output, 2);
|
||||
}
|
||||
return '<strong>' . esc_html($ip) . '</strong>';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function ip($refreshCache = false) {
|
||||
if (WORDFENCE_LS_FROM_CORE) {
|
||||
return \wfUtils::getIP($refreshCache);
|
||||
}
|
||||
|
||||
if (!isset($this->_cachedIP) || $refreshCache) {
|
||||
$this->_cachedIP = $this->_ip(Controller_Settings::shared()->get(Controller_Settings::OPTION_IP_SOURCE), Controller_Settings::shared()->trusted_proxies());
|
||||
}
|
||||
|
||||
return $this->_cachedIP[0]; //Format is array(<text IP>, <field found in>)
|
||||
}
|
||||
|
||||
public function ip_for_field($source, $trusted_proxies) {
|
||||
return $this->_ip($source, $trusted_proxies);
|
||||
}
|
||||
|
||||
protected function _ip($source = null, $trusted_proxies = null) {
|
||||
if ($source === null) {
|
||||
$source = Controller_Settings::shared()->get(Controller_Settings::OPTION_IP_SOURCE);
|
||||
}
|
||||
|
||||
$possible_ips = $this->_possible_ips($source);
|
||||
if ($trusted_proxies === null) { $trusted_proxies = array(); }
|
||||
return $this->_find_preferred_ip($possible_ips, $trusted_proxies);
|
||||
}
|
||||
|
||||
protected function _possible_ips($source = null) {
|
||||
$defaultIP = (is_array($_SERVER) && isset($_SERVER[self::IP_SOURCE_REMOTE_ADDR])) ? array($_SERVER[self::IP_SOURCE_REMOTE_ADDR], self::IP_SOURCE_REMOTE_ADDR) : array('127.0.0.1', self::IP_SOURCE_REMOTE_ADDR);
|
||||
|
||||
if ($source) {
|
||||
if ($source == self::IP_SOURCE_REMOTE_ADDR) {
|
||||
return array($defaultIP);
|
||||
}
|
||||
|
||||
$check = array(
|
||||
array((isset($_SERVER[$source]) ? $_SERVER[$source] : ''), $source),
|
||||
$defaultIP,
|
||||
);
|
||||
return $check;
|
||||
}
|
||||
|
||||
$check = array($defaultIP);
|
||||
if (isset($_SERVER[self::IP_SOURCE_X_FORWARDED_FOR])) {
|
||||
$check[] = array($_SERVER[self::IP_SOURCE_X_FORWARDED_FOR], self::IP_SOURCE_X_FORWARDED_FOR);
|
||||
}
|
||||
if (isset($_SERVER[self::IP_SOURCE_X_REAL_IP])) {
|
||||
$check[] = array($_SERVER[self::IP_SOURCE_X_REAL_IP], self::IP_SOURCE_X_REAL_IP);
|
||||
}
|
||||
return $check;
|
||||
}
|
||||
|
||||
protected function _find_preferred_ip($possible_ips, $trusted_proxies) {
|
||||
$privates = array();
|
||||
foreach ($possible_ips as $entry) {
|
||||
list($value, $var) = $entry;
|
||||
if (is_array($value)) { // An array of IPs
|
||||
foreach ($value as $index => $j) {
|
||||
if (!Model_IP::is_valid_ip($j)) {
|
||||
$j = preg_replace('/:\d+$/', '', $j); //Strip off port if present
|
||||
}
|
||||
|
||||
if (Model_IP::is_valid_ip($j)) {
|
||||
if (Model_IP::is_ipv6_mapped_ipv4($j)) {
|
||||
$j = Model_IP::inet_ntop(Model_IP::inet_pton($j));
|
||||
}
|
||||
|
||||
foreach ($trusted_proxies as $proxy) {
|
||||
if (!empty($proxy)) {
|
||||
if (Controller_Whitelist::shared()->ip_in_range($j, $proxy) && $index < count($value) - 1) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filter_var($j, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
$privates[] = array($j, $var);
|
||||
}
|
||||
else {
|
||||
return array($j, $var);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$skipToNext = false;
|
||||
$separators = array(',', ' ', "\t");
|
||||
foreach ($separators as $char) { // A list of IPs separated by <separator>: 192.0.2.15,192.0.2.35,192.0.2.254
|
||||
if (strpos($value, $char) !== false) {
|
||||
$sp = explode($char, $value);
|
||||
$sp = array_reverse($sp);
|
||||
foreach ($sp as $index => $j) {
|
||||
$j = trim($j);
|
||||
if (!Model_IP::is_valid_ip($j)) {
|
||||
$j = preg_replace('/:\d+$/', '', $j); //Strip off port
|
||||
}
|
||||
|
||||
if (Model_IP::is_valid_ip($j)) {
|
||||
if (Model_IP::is_ipv6_mapped_ipv4($j)) {
|
||||
$j = Model_IP::inet_ntop(Model_IP::inet_pton($j));
|
||||
}
|
||||
|
||||
foreach ($trusted_proxies as $proxy) {
|
||||
if (!empty($proxy)) {
|
||||
if (Controller_Whitelist::shared()->ip_in_range($j, $proxy) && $index < count($sp) - 1) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filter_var($j, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
$privates[] = array($j, $var);
|
||||
}
|
||||
else {
|
||||
return array($j, $var);
|
||||
}
|
||||
}
|
||||
}
|
||||
$skipToNext = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($skipToNext) { continue; } //Skip to next item because this one had a comma/space/tab, but we didn't find a valid, non-private address
|
||||
|
||||
// A literal IP
|
||||
if (!Model_IP::is_valid_ip($value)) {
|
||||
$value = preg_replace('/:\d+$/', '', $value); //Strip off port
|
||||
}
|
||||
|
||||
if (Model_IP::is_valid_ip($value)) {
|
||||
if (Model_IP::is_ipv6_mapped_ipv4($value)) {
|
||||
$value = Model_IP::inet_ntop(Model_IP::inet_pton($value));
|
||||
}
|
||||
|
||||
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
||||
$privates[] = array($value, $var);
|
||||
}
|
||||
else {
|
||||
return array($value, $var);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count($privates) > 0) {
|
||||
return $privates[0];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_Script extends Model_Asset {
|
||||
|
||||
public function enqueue() {
|
||||
if ($this->registered) {
|
||||
wp_enqueue_script($this->handle);
|
||||
}
|
||||
else {
|
||||
wp_enqueue_script($this->handle, $this->source, $this->dependencies, $this->version);
|
||||
}
|
||||
}
|
||||
|
||||
public function isEnqueued() {
|
||||
return wp_script_is($this->handle);
|
||||
}
|
||||
|
||||
public function renderInline() {
|
||||
if (empty($this->source))
|
||||
return;
|
||||
?>
|
||||
<script type="text/javascript" src="<?php echo esc_attr($this->getSourceUrl()) ?>"></script>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function register() {
|
||||
wp_register_script($this->handle, $this->source, $this->dependencies, $this->version);
|
||||
return parent::register();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
abstract class Model_Settings {
|
||||
const AUTOLOAD_YES = 'yes';
|
||||
const AUTOLOAD_NO = 'no';
|
||||
|
||||
/**
|
||||
* Sets $value to $key.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param string $autoload Whether or not the key/value pair should autoload in storages that do that.
|
||||
* @param bool $allowOverwrite If false, only sets the value if key does not already exist.
|
||||
*/
|
||||
abstract public function set($key, $value, $autoload = self::AUTOLOAD_YES, $allowOverwrite = true);
|
||||
abstract public function set_multiple($values);
|
||||
abstract public function get($key, $default);
|
||||
abstract public function remove($key);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Settings;
|
||||
|
||||
use WordfenceLS\Controller_DB;
|
||||
use WordfenceLS\Model_Settings;
|
||||
|
||||
class Model_DB extends Model_Settings {
|
||||
const AUTOLOAD_NO = 'no';
|
||||
const AUTOLOAD_YES = 'yes';
|
||||
|
||||
public function set($key, $value, $autoload = self::AUTOLOAD_YES, $allowOverwrite = true) {
|
||||
global $wpdb;
|
||||
$table = Controller_DB::shared()->settings;
|
||||
if (!$allowOverwrite) {
|
||||
if ($this->_has_cached($key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM `{$table}` WHERE `name` = %s", $key), ARRAY_A);
|
||||
if (is_array($row)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($wpdb->query($wpdb->prepare("INSERT INTO `{$table}` (`name`, `value`, `autoload`) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), `autoload` = VALUES(`autoload`)", $key, $value, $autoload)) !== false && $autoload != self::AUTOLOAD_NO) {
|
||||
$this->_update_cached($key, $value);
|
||||
do_action('wfls_settings_set', $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
public function set_multiple($values) {
|
||||
foreach ($values as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$this->set($key, $value['value'], $value['autoload'], $value['allowOverwrite']);
|
||||
}
|
||||
else {
|
||||
$this->set($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function get($key, $default = false) {
|
||||
global $wpdb;
|
||||
|
||||
if ($this->_has_cached($key)) {
|
||||
return $this->_cached_value($key);
|
||||
}
|
||||
|
||||
$table = Controller_DB::shared()->settings;
|
||||
if (!($setting = $wpdb->get_row($wpdb->prepare("SELECT `name`, `value`, `autoload` FROM `{$table}` WHERE `name` = %s", $key)))) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ($setting->autoload != self::AUTOLOAD_NO) {
|
||||
$this->_update_cached($key, $setting->value);
|
||||
}
|
||||
return $setting->value;
|
||||
}
|
||||
|
||||
public function remove($key) {
|
||||
global $wpdb;
|
||||
$table = Controller_DB::shared()->settings;
|
||||
$wpdb->query($wpdb->prepare("DELETE FROM `{$table}` WHERE `name` = %s", $key));
|
||||
$this->_remove_cached($key);
|
||||
}
|
||||
|
||||
private function _cached() {
|
||||
global $wpdb;
|
||||
|
||||
$settings = wp_cache_get('allsettings', 'wordfence-ls');
|
||||
if (!$settings) {
|
||||
$table = Controller_DB::shared()->settings;
|
||||
$suppress = $wpdb->suppress_errors();
|
||||
$raw = $wpdb->get_results("SELECT `name`, `value` FROM `{$table}` WHERE `autoload` = 'yes'");
|
||||
$wpdb->suppress_errors($suppress);
|
||||
$settings = array();
|
||||
foreach ((array) $raw as $o) {
|
||||
$settings[$o->name] = $o->value;
|
||||
}
|
||||
|
||||
wp_cache_add_non_persistent_groups('wordfence-ls');
|
||||
wp_cache_add('allsettings', $settings, 'wordfence-ls');
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
private function _update_cached($key, $value) {
|
||||
$settings = $this->_cached();
|
||||
$settings[$key] = $value;
|
||||
wp_cache_set('allsettings', $settings, 'wordfence-ls');
|
||||
}
|
||||
|
||||
private function _remove_cached($key) {
|
||||
$settings = $this->_cached();
|
||||
if (isset($settings[$key])) {
|
||||
unset($settings[$key]);
|
||||
wp_cache_set('allsettings', $settings, 'wordfence-ls');
|
||||
}
|
||||
}
|
||||
|
||||
private function _cached_value($key) {
|
||||
global $wpdb;
|
||||
|
||||
$settings = $this->_cached();
|
||||
if (isset($settings[$key])) {
|
||||
return $settings[$key];
|
||||
}
|
||||
|
||||
$table = Controller_DB::shared()->settings;
|
||||
$value = $wpdb->get_var($wpdb->prepare("SELECT `value` FROM `{$table}` WHERE name = %s", $key));
|
||||
if ($value !== null) {
|
||||
$settings[$key] = $value;
|
||||
wp_cache_set('allsettings', $settings, 'wordfence-ls');
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
public function _has_cached($key) {
|
||||
$settings = $this->_cached();
|
||||
return isset($settings[$key]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Settings;
|
||||
|
||||
use WordfenceLS\Model_Settings;
|
||||
|
||||
class Model_WPOptions extends Model_Settings {
|
||||
protected $_prefix;
|
||||
|
||||
public function __construct($prefix = '') {
|
||||
$this->_prefix = $prefix;
|
||||
}
|
||||
|
||||
protected function _translate_key($key) {
|
||||
return strtolower(preg_replace('/[^a-z0-9]/i', '_', $key));
|
||||
}
|
||||
|
||||
public function set($key, $value, $autoload = self::AUTOLOAD_YES, $allowOverwrite = true) {
|
||||
$key = $this->_translate_key($this->_prefix . $key);
|
||||
if (!$allowOverwrite) {
|
||||
if (is_multisite()) {
|
||||
add_network_option(null, $key, $value);
|
||||
}
|
||||
else {
|
||||
add_option($key, $value, '', $autoload);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (is_multisite()) {
|
||||
update_network_option(null, $key, $value);
|
||||
}
|
||||
else {
|
||||
update_option($key, $value, $autoload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function set_multiple($values) {
|
||||
foreach ($values as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$this->set($key, $value['value'], $value['autoload'], $value['allowOverwrite']);
|
||||
}
|
||||
else {
|
||||
$this->set($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function get($key, $default = false) {
|
||||
$key = $this->_translate_key($this->_prefix . $key);
|
||||
if (is_multisite()) {
|
||||
$value = get_network_option($key, $default);
|
||||
}
|
||||
else {
|
||||
$value = get_option($key, $default);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function remove($key) {
|
||||
$key = $this->_translate_key($this->_prefix . $key);
|
||||
if (is_multisite()) {
|
||||
delete_network_option(null, $key);
|
||||
}
|
||||
else {
|
||||
delete_option($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_Style extends Model_Asset {
|
||||
|
||||
public function enqueue() {
|
||||
if ($this->registered) {
|
||||
wp_enqueue_style($this->handle);
|
||||
}
|
||||
else {
|
||||
wp_enqueue_style($this->handle, $this->source, $this->dependencies, $this->version);
|
||||
}
|
||||
}
|
||||
|
||||
public function isEnqueued() {
|
||||
return wp_style_is($this->handle);
|
||||
}
|
||||
|
||||
public function renderInline() {
|
||||
if (empty($this->source))
|
||||
return;
|
||||
$url = esc_attr($this->getSourceUrl());
|
||||
$linkTag = "<link rel=\"stylesheet\" type=\"text/css\" href=\"{$url}\">";
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
jQuery('head').append(<?php echo json_encode($linkTag) ?>);
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function register() {
|
||||
wp_register_style($this->handle, $this->source, $this->dependencies, $this->version);
|
||||
return parent::register();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Text;
|
||||
|
||||
/**
|
||||
* Represents text that is already HTML-safe and should not be encoded again.
|
||||
* @package Wordfence2FA\Text
|
||||
*/
|
||||
class Model_HTML {
|
||||
private $_html;
|
||||
|
||||
public static function esc_html($content) {
|
||||
if (is_object($content) && ($content instanceof Model_HTML)) {
|
||||
return (string) $content;
|
||||
}
|
||||
return esc_html($content);
|
||||
}
|
||||
|
||||
public function __construct($html) {
|
||||
$this->_html = $html;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
return $this->_html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\Text;
|
||||
|
||||
/**
|
||||
* Represents text that is already JavaScript-safe and should not be encoded again.
|
||||
* @package Wordfence2FA\Text
|
||||
*/
|
||||
class Model_JavaScript {
|
||||
private $_javaScript;
|
||||
|
||||
/**
|
||||
* Returns a string escaped for use in JavaScript. This is almost identical in behavior to esc_js except that
|
||||
* we don't call _wp_specialchars and keep \r rather than stripping it.
|
||||
*
|
||||
* @param string|Model_JavaScript $content
|
||||
* @return string
|
||||
*/
|
||||
public static function esc_js($content) {
|
||||
if (is_object($content) && ($content instanceof Model_HTML)) {
|
||||
return (string) $content;
|
||||
}
|
||||
|
||||
$safe_text = wp_check_invalid_utf8($content);
|
||||
$safe_text = preg_replace('/&#(x)?0*(?(1)27|39);?/i', "'", stripslashes($safe_text));
|
||||
$safe_text = str_replace("\r", '\\r', $safe_text);
|
||||
$safe_text = str_replace("\n", '\\n', addslashes($safe_text));
|
||||
return apply_filters('js_escape', $safe_text, $content);
|
||||
}
|
||||
|
||||
public static function echo_string_literal($string) {
|
||||
echo "'" . self::esc_js($string) . "'";
|
||||
}
|
||||
|
||||
public function __construct($javaScript) {
|
||||
$this->_javaScript = $javaScript;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
return $this->_javaScript;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_TokenBucket {
|
||||
/* Constants to map from tokens per unit to tokens per second */
|
||||
const MICROSECOND = 0.000001;
|
||||
const MILLISECOND = 0.001;
|
||||
const SECOND = 1;
|
||||
const MINUTE = 60;
|
||||
const HOUR = 3600;
|
||||
const DAY = 86400;
|
||||
const WEEK = 604800;
|
||||
const MONTH = 2629743.83;
|
||||
const YEAR = 31556926;
|
||||
|
||||
const BACKING_REDIS = 'redis';
|
||||
const BACKING_WP_OPTIONS = 'wpoptions';
|
||||
|
||||
private $_identifier;
|
||||
private $_bucketSize;
|
||||
private $_tokensPerSecond;
|
||||
|
||||
private $_backing;
|
||||
private $_redis;
|
||||
|
||||
/**
|
||||
* Model_TokenBucket constructor.
|
||||
*
|
||||
* @param string $identifier The identifier for the bucket record in the database
|
||||
* @param int $bucketSize The maximum capacity of the bucket.
|
||||
* @param double $tokensPerSecond The number of tokens per second added to the bucket.
|
||||
* @param string $backing The backing storage to use.
|
||||
*/
|
||||
public function __construct($identifier, $bucketSize, $tokensPerSecond, $backing = self::BACKING_WP_OPTIONS) {
|
||||
$this->_identifier = $identifier;
|
||||
$this->_bucketSize = $bucketSize;
|
||||
$this->_tokensPerSecond = $tokensPerSecond;
|
||||
$this->_backing = $backing;
|
||||
|
||||
if ($backing == self::BACKING_REDIS) {
|
||||
$this->_redis = new \Redis();
|
||||
$this->_redis->pconnect('127.0.0.1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to acquire a lock for the bucket.
|
||||
*
|
||||
* @param int $timeout
|
||||
* @return bool Whether or not the lock was acquired.
|
||||
*/
|
||||
private function _lock($timeout = 30) {
|
||||
if ($this->_backing == self::BACKING_WP_OPTIONS) {
|
||||
$start = microtime(true);
|
||||
while (!$this->_wp_options_create_lock($this->_identifier)) {
|
||||
if (microtime(true) - $start > $timeout) {
|
||||
return false;
|
||||
}
|
||||
usleep(5000); // 5 ms
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if ($this->_backing == self::BACKING_REDIS) {
|
||||
if ($this->_redis === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
while (!$this->_redis->setnx('lock:' . $this->_identifier, '1')) {
|
||||
if (microtime(true) - $start > $timeout) {
|
||||
return false;
|
||||
}
|
||||
usleep(5000); // 5 ms
|
||||
}
|
||||
$this->_redis->expire('lock:' . $this->_identifier, 30);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function _unlock() {
|
||||
if ($this->_backing == self::BACKING_WP_OPTIONS) {
|
||||
$this->_wp_options_release_lock($this->_identifier);
|
||||
}
|
||||
else if ($this->_backing == self::BACKING_REDIS) {
|
||||
if ($this->_redis === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->_redis->del('lock:' . $this->_identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private function _wp_options_create_lock($name, $timeout = null) { //Our own version of WP_Upgrader::create_lock
|
||||
global $wpdb;
|
||||
|
||||
if (!$timeout) {
|
||||
$timeout = 3600;
|
||||
}
|
||||
|
||||
$lock_option = 'wfls_' . $name . '.lock';
|
||||
$lock_result = $wpdb->query($wpdb->prepare("INSERT IGNORE INTO `{$wpdb->options}` (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, 'no')", $lock_option, time()));
|
||||
|
||||
if (!$lock_result) {
|
||||
$lock_result = get_option($lock_option);
|
||||
if (!$lock_result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($lock_result > (time() - $timeout)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->_wp_options_release_lock($name);
|
||||
return $this->_wp_options_create_lock($name, $timeout);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function _wp_options_release_lock($name) {
|
||||
return delete_option('wfls_' . $name . '.lock');
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically checks the available token count, creating the initial record if needed, and updates the available token count if the requested number of tokens is available.
|
||||
*
|
||||
* @param int $tokenCount
|
||||
* @return bool Whether or not there were enough tokens to satisfy the request.
|
||||
*/
|
||||
public function consume($tokenCount = 1) {
|
||||
if (!$this->_lock()) { return false; }
|
||||
|
||||
if ($this->_backing == self::BACKING_WP_OPTIONS) {
|
||||
$record = get_transient('wflsbucket:' . $this->_identifier);
|
||||
}
|
||||
else if ($this->_backing == self::BACKING_REDIS) {
|
||||
$record = $this->_redis->get('bucket:' . $this->_identifier);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($record === false) {
|
||||
if ($tokenCount > $this->_bucketSize) {
|
||||
$this->_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->_bootstrap($this->_bucketSize - $tokenCount);
|
||||
$this->_unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
$tokens = min($this->_secondsToTokens(microtime(true) - (float) $record), $this->_bucketSize);
|
||||
if ($tokenCount > $tokens) {
|
||||
$this->_unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->_backing == self::BACKING_WP_OPTIONS) {
|
||||
set_transient('wflsbucket:' . $this->_identifier, (string) (microtime(true) - $this->_tokensToSeconds($tokens - $tokenCount)), ceil($this->_tokensToSeconds($this->_bucketSize)));
|
||||
}
|
||||
else if ($this->_backing == self::BACKING_REDIS) {
|
||||
$this->_redis->set('bucket:' . $this->_identifier, (string) (microtime(true) - $this->_tokensToSeconds($tokens - $tokenCount)));
|
||||
}
|
||||
|
||||
$this->_unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an initial record with the given number of tokens.
|
||||
*
|
||||
* @param int $initialTokens
|
||||
*/
|
||||
protected function _bootstrap($initialTokens) {
|
||||
$microtime = microtime(true) - $this->_tokensToSeconds($initialTokens);
|
||||
if ($this->_backing == self::BACKING_WP_OPTIONS) {
|
||||
set_transient('wflsbucket:' . $this->_identifier, (string) $microtime, ceil($this->_tokensToSeconds($this->_bucketSize)));
|
||||
}
|
||||
else if ($this->_backing == self::BACKING_REDIS) {
|
||||
$this->_redis->set('bucket:' . $this->_identifier, (string) $microtime);
|
||||
}
|
||||
}
|
||||
|
||||
protected function _tokensToSeconds($tokens) {
|
||||
return $tokens / $this->_tokensPerSecond;
|
||||
}
|
||||
|
||||
protected function _secondsToTokens($seconds) {
|
||||
return (int) $seconds * $this->_tokensPerSecond;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Model_View {
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $file_extension = '.php';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Equivalent to the constructor but allows for call chaining.
|
||||
*
|
||||
* @param string $view
|
||||
* @param array $data
|
||||
* @return Model_View
|
||||
*/
|
||||
public static function create($view, $data = array()) {
|
||||
return new self($view, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $view
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct($view, $data = array()) {
|
||||
$this->path = WORDFENCE_LS_PATH . 'views';
|
||||
$this->view = $view;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @throws ViewNotFoundException
|
||||
*/
|
||||
public function render() {
|
||||
$view = preg_replace('/\.{2,}/', '.', $this->view);
|
||||
$path = $this->path . '/' . $view . $this->file_extension;
|
||||
if (!file_exists($path)) {
|
||||
throw new ViewNotFoundException('The view ' . $path . ' does not exist or is not readable.');
|
||||
}
|
||||
|
||||
extract($this->data, EXTR_SKIP);
|
||||
|
||||
ob_start();
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
include $path;
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString() {
|
||||
try {
|
||||
return $this->render();
|
||||
}
|
||||
catch (ViewNotFoundException $e) {
|
||||
return defined('WP_DEBUG') && WP_DEBUG ? $e->getMessage() : 'The view could not be loaded.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @return $this
|
||||
*/
|
||||
public function addData($data) {
|
||||
$this->data = array_merge($data, $this->data);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getData() {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return $this
|
||||
*/
|
||||
public function setData($data) {
|
||||
$this->data = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getView() {
|
||||
return $this->view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $view
|
||||
* @return $this
|
||||
*/
|
||||
public function setView($view) {
|
||||
$this->view = $view;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent POP
|
||||
*/
|
||||
public function __wakeup() {
|
||||
$this->path = WORDFENCE_LS_PATH . 'views';
|
||||
$this->view = null;
|
||||
$this->data = array();
|
||||
$this->file_extension = '.php';
|
||||
}
|
||||
}
|
||||
|
||||
class ViewNotFoundException extends \Exception { }
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\View;
|
||||
|
||||
/**
|
||||
* Represents a tab in the UI.
|
||||
*
|
||||
* @package Wordfence2FA\View
|
||||
* @property string $id
|
||||
* @property string $a
|
||||
* @property string $tabTitle
|
||||
* @property string $pageTitle
|
||||
* @property bool $active
|
||||
*/
|
||||
class Model_Tab {
|
||||
protected $_id;
|
||||
protected $_a;
|
||||
protected $_tabTitle;
|
||||
protected $_pageTitle;
|
||||
protected $_active;
|
||||
|
||||
public function __construct($id, $a, $tabTitle, $pageTitle, $active = false) {
|
||||
$this->_id = $id;
|
||||
$this->_a = $a;
|
||||
$this->_tabTitle = $tabTitle;
|
||||
$this->_pageTitle = $pageTitle;
|
||||
$this->_active = $active;
|
||||
}
|
||||
|
||||
public function __get($name) {
|
||||
switch ($name) {
|
||||
case 'id':
|
||||
return $this->_id;
|
||||
case 'a':
|
||||
return $this->_a;
|
||||
case 'tabTitle':
|
||||
return $this->_tabTitle;
|
||||
case 'pageTitle':
|
||||
return $this->_pageTitle;
|
||||
case 'active':
|
||||
return $this->_active;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Invalid key: ' . $name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS\View;
|
||||
|
||||
/**
|
||||
* Class Model_Title
|
||||
* @package Wordfence2FA\Page
|
||||
* @var string $id A valid DOM ID for the title.
|
||||
* @var string|Model_HTML $title The title text or HTML.
|
||||
* @var string $helpURL The help URL.
|
||||
* @var string|Model_HTML $helpLink The text/HTML of the help link.
|
||||
*/
|
||||
class Model_Title {
|
||||
private $_id;
|
||||
private $_title;
|
||||
private $_helpURL;
|
||||
private $_helpLink;
|
||||
|
||||
public function __construct($id, $title, $helpURL = null, $helpLink = null) {
|
||||
$this->_id = $id;
|
||||
$this->_title = $title;
|
||||
$this->_helpURL = $helpURL;
|
||||
$this->_helpLink = $helpLink;
|
||||
}
|
||||
|
||||
public function __get($name) {
|
||||
switch ($name) {
|
||||
case 'id':
|
||||
return $this->_id;
|
||||
case 'title':
|
||||
return $this->_title;
|
||||
case 'helpURL':
|
||||
return $this->_helpURL;
|
||||
case 'helpLink':
|
||||
return $this->_helpLink;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Invalid key: ' . $name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Utility_Array {
|
||||
|
||||
public static function findOffset($array, $key) {
|
||||
$offset = 0;
|
||||
foreach ($array as $index => $value) {
|
||||
if ($index === $key)
|
||||
return $offset;
|
||||
$offset++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function insertAfter(&$array, $targetKey, $key, $value) {
|
||||
$offset = self::findOffset($array, $targetKey);
|
||||
if ($offset === null)
|
||||
return false;
|
||||
$array = array_merge(
|
||||
array_slice($array, 0, $offset + 1),
|
||||
array( $key => $value ),
|
||||
array_slice($array, $offset + 1)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
use WordfenceLS\Crypto\Model_Base2n;
|
||||
|
||||
class Utility_BaseConversion {
|
||||
|
||||
public static function get_base32() {
|
||||
static $base32 = null;
|
||||
if ($base32 === null)
|
||||
$base32 = new Model_Base2n(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', false, true, true);
|
||||
return $base32;
|
||||
}
|
||||
|
||||
public static function base32_encode($data) {
|
||||
$base32 = self::get_base32();
|
||||
return $base32->encode($data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class Utility_DatabaseLock implements Utility_Lock {
|
||||
|
||||
const DEFAULT_TIMEOUT = 30;
|
||||
const MAX_TIMEOUT = 120;
|
||||
|
||||
private $wpdb;
|
||||
private $table;
|
||||
private $key;
|
||||
private $timeout;
|
||||
private $expirationTimestamp;
|
||||
|
||||
public function __construct($dbController, $key, $timeout = null) {
|
||||
$this->wpdb = $dbController->get_wpdb();
|
||||
$this->table = $dbController->settings;
|
||||
$this->key = "lock:{$key}";
|
||||
$this->timeout = self::resolveTimeout($timeout);
|
||||
}
|
||||
|
||||
private static function resolveTimeout($timeout) {
|
||||
if ($timeout === null)
|
||||
$timeout = ini_get('max_execution_time');
|
||||
$timeout = (int) $timeout;
|
||||
if ($timeout <= 0 || $timeout > self::MAX_TIMEOUT)
|
||||
return self::DEFAULT_TIMEOUT;
|
||||
return $timeout;
|
||||
}
|
||||
|
||||
private function clearExpired($timestamp) {
|
||||
$this->wpdb->query($this->wpdb->prepare(<<<SQL
|
||||
DELETE
|
||||
FROM {$this->table}
|
||||
WHERE
|
||||
name = %s
|
||||
AND value < %d
|
||||
SQL
|
||||
, $this->key, $timestamp));
|
||||
}
|
||||
|
||||
private function insert($expirationTimestamp) {
|
||||
$result = $this->wpdb->query($this->wpdb->prepare(<<<SQL
|
||||
INSERT IGNORE
|
||||
INTO {$this->table}
|
||||
(name, value, autoload)
|
||||
VALUES(%s, %d, 'no')
|
||||
SQL
|
||||
, $this->key, $expirationTimestamp));
|
||||
return $result === 1;
|
||||
}
|
||||
|
||||
public function acquire($delay = self::DEFAULT_DELAY) {
|
||||
$attempts = (int) ($this->timeout * 1000000 / $delay);
|
||||
for (; $attempts > 0; $attempts--) {
|
||||
$timestamp = time();
|
||||
$this->clearExpired($timestamp);
|
||||
$expirationTimestamp = $timestamp + $this->timeout;
|
||||
$locked = $this->insert($expirationTimestamp);
|
||||
if ($locked) {
|
||||
$this->expirationTimestamp = $expirationTimestamp;
|
||||
return;
|
||||
}
|
||||
usleep($delay);
|
||||
}
|
||||
throw new RuntimeException("Failed to acquire lock {$this->key}");
|
||||
}
|
||||
|
||||
private function delete($expirationTimestamp) {
|
||||
$this->wpdb->delete(
|
||||
$this->table,
|
||||
array (
|
||||
'name' => $this->key,
|
||||
'value' => $expirationTimestamp
|
||||
),
|
||||
array (
|
||||
'%s',
|
||||
'%d'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function release() {
|
||||
if ($this->expirationTimestamp === null)
|
||||
return;
|
||||
$this->delete($this->expirationTimestamp);
|
||||
$this->expirationTimestamp = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
interface Utility_Lock {
|
||||
|
||||
const DEFAULT_DELAY = 100000;
|
||||
|
||||
public function acquire($delay = self::DEFAULT_DELAY);
|
||||
|
||||
public function release();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
/**
|
||||
* An implementation of the Utility_Lock that doesn't actually do any locking
|
||||
*/
|
||||
class Utility_NullLock implements Utility_Lock {
|
||||
|
||||
public function acquire($delay = self::DEFAULT_DELAY) {
|
||||
//Do nothing
|
||||
}
|
||||
|
||||
public function release() {
|
||||
//Do nothing
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
class Utility_Number {
|
||||
|
||||
public static function isInteger($value, $min = null, $max = null) {
|
||||
$options = array();
|
||||
if ($min !== null)
|
||||
$options['min_range'] = $min;
|
||||
if ($max !== null)
|
||||
$options['max_range'] = $max;
|
||||
return filter_var($value, FILTER_VALIDATE_INT, array('options' => $options)) !== false;
|
||||
}
|
||||
|
||||
public static function isUnixTimestamp($value) {
|
||||
return self::isInteger($value, 0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace WordfenceLS;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class Utility_Serialization {
|
||||
|
||||
public static function unserialize($data, $options = array(), $validator = null) {
|
||||
static $serializedFalse;
|
||||
if ($serializedFalse === null)
|
||||
$serializedFalse = serialize(false);
|
||||
if ($data === $serializedFalse)
|
||||
return false;
|
||||
if (!is_serialized($data))
|
||||
throw new RuntimeException('Input data is not serialized');
|
||||
if (version_compare(PHP_VERSION, '5.6', '<=')) {
|
||||
$unserialized = @unserialize($data);
|
||||
}
|
||||
else {
|
||||
$unserialized = @unserialize($data, $options);
|
||||
}
|
||||
if ($unserialized === false)
|
||||
throw new RuntimeException('Deserialization failed');
|
||||
if ($validator !== null && !$validator($unserialized))
|
||||
throw new RuntimeException('Validation of unserialized data failed');
|
||||
return $unserialized;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
#wfls-management-embedded .wfls-flex-row-wrapped{flex-wrap:wrap;-webkit-flex-wrap:wrap}#wfls-management-embedded ul.wfls-recovery-codes{margin-left:0;padding-left:0}#wfls-management-embedded .wfls-btn{text-decoration:none}#wfls-management-embedded #wfls-qr-code-text{margin-top:0.5rem}#wfls-management-embedded>div>div:not(:last-child){margin-bottom:0.75rem}#wfls-management-embedded>div.wfls-no-bottom-column-margin>div:not(:last-child){margin-bottom:0}#wfls-management-embedded input[type=text]{width:auto}.wfls-modal-footer ul{list-style-type:none}
|
||||
@@ -0,0 +1 @@
|
||||
#wfls-prompt-overlay{position:absolute;top:0px;right:0px;bottom:0px;left:0px;background-color:#fff;padding:26px 24px 46px;display:-webkit-flex;display:flex;-webkit-align-items:center;align-items:center;-webkit-justify-content:stretch;justify-content:stretch}.woocommerce #wfls-prompt-overlay{padding:0;display:block}#wfls-prompt-wrapper{-webkit-flex-grow:1;flex-grow:1;width:100%}.login form .wfls-textarea{font-size:1rem;width:100%;padding:3px;margin:2px 6px 16px 0;background:#fbfbfb;height:150px}.login form .wfls-remember-device-wrapper{font-weight:400;float:left;margin-bottom:0}.login form .wfls-remember-device-wrapper label{font-size:12px;line-height:19px}.wfls-2fa-code-help{text-decoration:none}.wfls-registration-captcha-contact{text-decoration:underline}.Zebra_Tooltip{background:0 0;position:absolute;z-index:8000}.Zebra_Tooltip .Zebra_Tooltip_Message{background:#000;border-radius:5px;box-shadow:0 0 6px rgba(0,0,0,0.6);color:#fff;font-size:12px;font-family:Tahoma,Arial,Helvetica,sans-serif;line-height:1.4;*margin-right:0;max-width:250px;padding:10px;position:relative;_width:expression(document.body.clientWidth > 250px ? '250px': 'auto');border:0 solid #000}.Zebra_Tooltip .Zebra_Tooltip_Message.Zebra_Tooltip_Has_Close{padding-right:23px}.Zebra_Tooltip .Zebra_Tooltip_Arrow{position:absolute;width:20px;height:10px;overflow:hidden}.Zebra_Tooltip .Zebra_Tooltip_Arrow.Zebra_Tooltip_Arrow_Bottom{bottom:0}.Zebra_Tooltip .Zebra_Tooltip_Arrow.Zebra_Tooltip_Arrow_Bottom div{top:0;border-color:#000 transparent transparent;_border-bottom-color:pink}.Zebra_Tooltip .Zebra_Tooltip_Arrow.Zebra_Tooltip_Arrow_Bottom div.Zebra_Tooltip_Arrow_Border{border-color:#000 transparent transparent}.Zebra_Tooltip .Zebra_Tooltip_Arrow.Zebra_Tooltip_Arrow_Top{top:0}.Zebra_Tooltip .Zebra_Tooltip_Arrow.Zebra_Tooltip_Arrow_Top div{bottom:0;border-color:transparent transparent #000;_border-top-color:pink}.Zebra_Tooltip .Zebra_Tooltip_Arrow.Zebra_Tooltip_Arrow_Top div.Zebra_Tooltip_Arrow_Border{border-color:transparent transparent #000}.Zebra_Tooltip .Zebra_Tooltip_Arrow div{position:absolute;border-style:solid;border-width:10px;width:0;height:0;left:0;_border-left-color:pink;_border-right-color:pink;_filter:chroma(color=pink)}.Zebra_Tooltip .Zebra_Tooltip_Arrow div.Zebra_Tooltip_Arrow_Border{border-width:10px;box-shadow:0 0 6px rgba(0,0,0,0.6);left:0}.Zebra_Tooltip .Zebra_Tooltip_Close{color:#fff;font-family:Arial,sans-serif;font-size:18px;line-height:1;padding:0 4px;position:absolute;right:2px;text-decoration:none;top:2px}.Zebra_Tooltip .Zebra_Tooltip_Close:hover{color:#000;background:#c2d076;border-radius:5px}.grecaptcha-badge{z-index:65535}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||
<g>
|
||||
<path d="M16,8.59L17.59,12.08L17.6,12.11L17.06,12.11L17.06,12.12C17.07,12.17 17.08,12.22 17.08,12.28C17.08,12.45 17.04,12.61 16.96,12.75C16.93,12.82 16.88,12.88 16.84,12.93C16.84,12.93 16.77,13.69 16.78,15.11C16.78,15.96 16.83,17.21 16.93,18.58C18.59,18.67 20.13,18.86 21.55,19.11L21.55,13.19L21.14,13.19L22.2,10.87L23.22,13.2L22.81,13.2L22.81,19.35C24.32,19.66 25.66,20.04 26.82,20.42L26.82,15.27L26.41,15.27L27.47,12.95L28.49,15.28L28.08,15.28L28.08,20.86C29.45,21.37 30.48,21.85 31.11,22.17C31.9,14.72 30.28,8.26 30.28,8.26C22.71,8.01 16,4 16,4L16,8.59Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M28.04,22.18L28.04,28L29.81,28C30.06,27.19 30.27,26.36 30.45,25.55C30.68,24.74 30.81,24.07 30.9,23.59C30.9,23.59 30.9,23.57 30.9,23.56C30.91,23.53 30.91,23.5 30.91,23.47C30.47,23.23 29.49,22.73 28.04,22.18Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M22.79,20.61L22.79,28L26.8,28L26.8,21.72C25.66,21.33 24.31,20.94 22.79,20.61Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M21.54,28L21.54,20.36C20.16,20.11 18.64,19.91 17.02,19.81C17.02,19.82 17.02,19.82 17.02,19.83C17.02,19.86 17.13,20.68 17.14,20.81C17.39,22.7 17.9,25.67 18.43,27.99L18.4,27.99L21.54,27.99L21.54,28Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M13.57,28C14.09,25.68 14.6,22.71 14.86,20.82L14.85,20.82L14.86,20.82C14.88,20.68 14.98,19.87 14.98,19.84C14.98,19.83 14.98,19.83 14.98,19.82C13.35,19.92 11.83,20.12 10.46,20.37L10.46,28L13.59,28L13.57,28Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M3.96,20.86L3.96,15.28L3.55,15.28L4.57,12.95L5.63,15.27L5.22,15.27L5.22,20.42C6.38,20.04 7.72,19.67 9.23,19.35L9.23,13.2L8.82,13.2L9.84,10.87L10.9,13.19L10.49,13.19L10.49,19.11C11.91,18.86 13.45,18.67 15.11,18.58C15.21,17.21 15.26,15.96 15.26,15.11C15.27,13.7 15.2,12.93 15.2,12.93C15.15,12.87 15.11,12.81 15.08,12.75C15,12.61 14.96,12.45 14.96,12.28C14.96,12.23 14.97,12.17 14.98,12.12L14.98,12.11L14.44,12.11L14.45,12.08L16,8.59L16,4C16,4 9.29,8.01 1.75,8.26C1.75,8.26 0.13,14.72 0.92,22.17C1.56,21.85 2.58,21.36 3.96,20.86Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M5.2,21.72L5.2,28L9.21,28L9.21,20.61C7.64,20.95 6.28,21.35 5.2,21.72Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M1.09,23.47C1.08,23.47 1.08,23.47 1.09,23.47C1.09,23.5 1.1,23.53 1.1,23.56C1.1,23.57 1.1,23.58 1.1,23.59C1.18,24.07 1.32,24.74 1.55,25.55C1.73,26.36 1.95,27.19 2.19,28L3.95,28L3.95,22.17C2.51,22.73 1.53,23.23 1.09,23.47Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 157 B |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
|
||||
<g>
|
||||
<path d="M16,8.59L17.59,12.08L17.6,12.11L17.06,12.11L17.06,12.12C17.07,12.17 17.08,12.22 17.08,12.28C17.08,12.45 17.04,12.61 16.96,12.75C16.93,12.82 16.88,12.88 16.84,12.93C16.84,12.93 16.77,13.69 16.78,15.11C16.78,15.96 16.83,17.21 16.93,18.58C18.59,18.67 20.13,18.86 21.55,19.11L21.55,13.19L21.14,13.19L22.2,10.87L23.22,13.2L22.81,13.2L22.81,19.35C24.32,19.66 25.66,20.04 26.82,20.42L26.82,15.27L26.41,15.27L27.47,12.95L28.49,15.28L28.08,15.28L28.08,20.86C29.45,21.37 30.48,21.85 31.11,22.17C31.9,14.72 30.28,8.26 30.28,8.26C22.71,8.01 16,4 16,4L16,8.59Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M28.04,22.18L28.04,28L29.81,28C30.06,27.19 30.27,26.36 30.45,25.55C30.68,24.74 30.81,24.07 30.9,23.59C30.9,23.59 30.9,23.57 30.9,23.56C30.91,23.53 30.91,23.5 30.91,23.47C30.47,23.23 29.49,22.73 28.04,22.18Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M22.79,20.61L22.79,28L26.8,28L26.8,21.72C25.66,21.33 24.31,20.94 22.79,20.61Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M21.54,28L21.54,20.36C20.16,20.11 18.64,19.91 17.02,19.81C17.02,19.82 17.02,19.82 17.02,19.83C17.02,19.86 17.13,20.68 17.14,20.81C17.39,22.7 17.9,25.67 18.43,27.99L18.4,27.99L21.54,27.99L21.54,28Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M13.57,28C14.09,25.68 14.6,22.71 14.86,20.82L14.85,20.82L14.86,20.82C14.88,20.68 14.98,19.87 14.98,19.84C14.98,19.83 14.98,19.83 14.98,19.82C13.35,19.92 11.83,20.12 10.46,20.37L10.46,28L13.59,28L13.57,28Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M3.96,20.86L3.96,15.28L3.55,15.28L4.57,12.95L5.63,15.27L5.22,15.27L5.22,20.42C6.38,20.04 7.72,19.67 9.23,19.35L9.23,13.2L8.82,13.2L9.84,10.87L10.9,13.19L10.49,13.19L10.49,19.11C11.91,18.86 13.45,18.67 15.11,18.58C15.21,17.21 15.26,15.96 15.26,15.11C15.27,13.7 15.2,12.93 15.2,12.93C15.15,12.87 15.11,12.81 15.08,12.75C15,12.61 14.96,12.45 14.96,12.28C14.96,12.23 14.97,12.17 14.98,12.12L14.98,12.11L14.44,12.11L14.45,12.08L16,8.59L16,4C16,4 9.29,8.01 1.75,8.26C1.75,8.26 0.13,14.72 0.92,22.17C1.56,21.85 2.58,21.36 3.96,20.86Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M5.2,21.72L5.2,28L9.21,28L9.21,20.61C7.64,20.95 6.28,21.35 5.2,21.72Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
<path d="M1.09,23.47C1.08,23.47 1.08,23.47 1.09,23.47C1.09,23.5 1.1,23.53 1.1,23.56C1.1,23.57 1.1,23.58 1.1,23.59C1.18,24.07 1.32,24.74 1.55,25.55C1.73,26.36 1.95,27.19 2.19,28L3.95,28L3.95,22.17C2.51,22.73 1.53,23.23 1.09,23.47Z" style="fill:rgb(33,141,193);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,90 @@
|
||||
(function($) {
|
||||
window['GWFLS'] = {
|
||||
init: function() {
|
||||
this.register_create_user_events();
|
||||
|
||||
$('.wfls-persistent-notice').on('click', 'button', function() {
|
||||
GWFLS.ajax(
|
||||
'wordfence_ls_dismiss_persistent_notice',
|
||||
{notice_id: $(this).parent('.notice').attr('id')},
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a WP AJAX call, automatically adding our nonce.
|
||||
*
|
||||
* @param string action
|
||||
* @param string|array|object payload
|
||||
* @param function successCallback
|
||||
* @param function failureCallback
|
||||
*/
|
||||
ajax: function(action, payload, successCallback, failureCallback) {
|
||||
if (typeof(payload) == 'string') {
|
||||
if (payload.length > 0) {
|
||||
payload += '&';
|
||||
}
|
||||
payload += 'action=' + action + '&nonce=' + GWFLSVars.nonce;
|
||||
}
|
||||
else if (typeof(payload) == 'object' && payload instanceof Array) {
|
||||
// jQuery serialized form data
|
||||
payload.push({
|
||||
name: 'action',
|
||||
value: action
|
||||
});
|
||||
payload.push({
|
||||
name: 'nonce',
|
||||
value: GWFLSVars.nonce
|
||||
});
|
||||
}
|
||||
else if (typeof(payload) == 'object') {
|
||||
payload['action'] = action;
|
||||
payload['nonce'] = GWFLSVars.nonce;
|
||||
}
|
||||
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: GWFLSVars.ajaxurl,
|
||||
dataType: "json",
|
||||
data: payload,
|
||||
success: function(json) {
|
||||
typeof successCallback == 'function' && successCallback(json);
|
||||
},
|
||||
error: function() {
|
||||
typeof failureCallback == 'function' && failureCallback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
dismiss_notice: function(nid) {
|
||||
this.ajax('wordfence_ls_dismiss_notice', {
|
||||
id: nid
|
||||
},
|
||||
function(res) { $('.wfls-notice[data-notice-id="' + nid + '"]').fadeOut(); },
|
||||
function() { $('.wfls-notice[data-notice-id="' + nid + '"]').fadeOut(); }
|
||||
);
|
||||
},
|
||||
|
||||
register_create_user_events: function() {
|
||||
var container = $('#wfls-grace-period-toggle-container');
|
||||
if (container.length) {
|
||||
var gracePeriodToggle = container.detach().find('tr');
|
||||
$('#createuser #role').on('change', function() {
|
||||
var select = $(this);
|
||||
gracePeriodToggle.detach();
|
||||
var role = select.val();
|
||||
var row = select.closest('tr');
|
||||
if (role === 'administrator') {
|
||||
gracePeriodToggle.insertAfter(row);
|
||||
}
|
||||
}).trigger('change');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$(function() {
|
||||
GWFLS.init();
|
||||
});
|
||||
})(jQuery);
|
||||
|
||||
@@ -0,0 +1,931 @@
|
||||
(function($) {
|
||||
window['WFLS'] = {
|
||||
panelIsOpen: false,
|
||||
basePageName: '',
|
||||
panelQueue: [],
|
||||
pendingChanges: {},
|
||||
userIsActivating: false,
|
||||
|
||||
//Screen sizes
|
||||
SCREEN_XS: 'xs',
|
||||
SCREEN_SM: 'sm',
|
||||
SCREEN_MD: 'md',
|
||||
SCREEN_LG: 'lg',
|
||||
|
||||
init: function() {
|
||||
this.basePageName = document.title;
|
||||
|
||||
var tabs = $('.wfls-page-tabs').find('.wfls-tab a');
|
||||
if (tabs.length > 0) {
|
||||
tabs.click(function() {
|
||||
$('.wfls-page-tabs').find('.wfls-tab').removeClass('wfls-active');
|
||||
$('.wfls-tab-content').removeClass('wfls-active');
|
||||
|
||||
var tab = $(this).closest('.wfls-tab');
|
||||
tab.addClass('wfls-active');
|
||||
var content = $('#' + tab.data('target'));
|
||||
content.addClass('wfls-active');
|
||||
document.title = tab.data('pageTitle') + " \u2039 " + WFLS.basePageName;
|
||||
$(window).trigger('wfls-tab-change', [tab.data('target')]);
|
||||
});
|
||||
if (window.location.hash) {
|
||||
var hashes = WFLS.parseHashes();
|
||||
var hash = hashes[hashes.length - 1];
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
if (hash == $(tabs[i]).closest('.wfls-tab').data('target')) {
|
||||
$(tabs[i]).trigger('click');
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$(tabs[0]).trigger('click');
|
||||
}
|
||||
$(window).on('hashchange', function () {
|
||||
var hashes = WFLS.parseHashes();
|
||||
var hash = hashes[hashes.length - 1];
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
if (hash == $(tabs[i]).closest('.wfls-tab').data('target')) {
|
||||
$(tabs[i]).trigger('click');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//On/Off Option
|
||||
$('.wfls-option.wfls-option-toggled .wfls-option-checkbox').each(function() {
|
||||
$(this).on('keydown', function(e) {
|
||||
if (e.keyCode == 32) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$(this).on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var optionElement = $(this).closest('.wfls-option');
|
||||
if (optionElement.hasClass('wfls-option-premium') || optionElement.hasClass('wfls-disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var option = optionElement.data('option');
|
||||
var value = false;
|
||||
var isActive = $(this).hasClass('wfls-checked');
|
||||
if (isActive) {
|
||||
$(this).removeClass('wfls-checked').attr('aria-checked', 'false');
|
||||
value = optionElement.data('disabledValue');
|
||||
}
|
||||
else {
|
||||
$(this).addClass('wfls-checked').attr('aria-checked', 'true');
|
||||
value = optionElement.data('enabledValue');
|
||||
}
|
||||
|
||||
var originalValue = optionElement.data('originalValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
$(optionElement).trigger('change', [false]);
|
||||
WFLS.updatePendingChanges();
|
||||
});
|
||||
|
||||
$(this).parent().find('.wfls-option-title').on('click', function(e) {
|
||||
var links = $(this).find('a');
|
||||
var buffer = 10;
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var t = $(links[i]).offset().top;
|
||||
var l = $(links[i]).offset().left;
|
||||
var b = t + $(links[i]).height();
|
||||
var r = l + $(links[i]).width();
|
||||
|
||||
if (e.pageX > l - buffer && e.pageX < r + buffer && e.pageY > t - buffer && e.pageY < b + buffer) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$(this).parent().find('.wfls-option-checkbox').trigger('click');
|
||||
}).css('cursor', 'pointer');
|
||||
});
|
||||
|
||||
//On/Off Boolean Switch Option
|
||||
$('.wfls-option.wfls-option-toggled-boolean-switch .wfls-boolean-switch').each(function() {
|
||||
$(this).on('keydown', function(e) {
|
||||
if (e.keyCode == 32) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$(this).on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$(this).find('.wfls-boolean-switch-handle').trigger('click');
|
||||
});
|
||||
|
||||
$(this).find('.wfls-boolean-switch-handle').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var optionElement = $(this).closest('.wfls-option');
|
||||
if (optionElement.hasClass('wfls-option-premium') || optionElement.hasClass('wfls-disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var switchElement = $(this).closest('.wfls-boolean-switch');
|
||||
var option = optionElement.data('option');
|
||||
var value = false;
|
||||
var isActive = switchElement.hasClass('wfls-active');
|
||||
if (isActive) {
|
||||
switchElement.removeClass('wfls-active').attr('aria-checked', 'false');
|
||||
value = optionElement.data('disabledValue');
|
||||
}
|
||||
else {
|
||||
switchElement.addClass('wfls-active').attr('aria-checked', 'true');
|
||||
value = optionElement.data('enabledValue');
|
||||
}
|
||||
|
||||
var originalValue = optionElement.data('originalValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
$(optionElement).trigger('change', [false]);
|
||||
WFLS.updatePendingChanges();
|
||||
});
|
||||
|
||||
$(this).parent().find('.wfls-option-title').on('click', function(e) {
|
||||
var links = $(this).find('a');
|
||||
var buffer = 10;
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var t = $(links[i]).offset().top;
|
||||
var l = $(links[i]).offset().left;
|
||||
var b = t + $(links[i]).height();
|
||||
var r = l + $(links[i]).width();
|
||||
|
||||
if (e.pageX > l - buffer && e.pageX < r + buffer && e.pageY > t - buffer && e.pageY < b + buffer) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$(this).parent().find('.wfls-boolean-switch-handle').trigger('click');
|
||||
}).css('cursor', 'pointer');
|
||||
});
|
||||
|
||||
//On/Off Segmented Option
|
||||
$('.wfls-option.wfls-option-toggled-segmented [type=radio]').each(function() {
|
||||
$(this).on('click', function(e) {
|
||||
var optionElement = $(this).closest('.wfls-option');
|
||||
if (optionElement.hasClass('wfls-option-premium') || optionElement.hasClass('wfls-disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var option = optionElement.data('option');
|
||||
var value = this.value;
|
||||
|
||||
var originalValue = optionElement.data('originalValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
$(optionElement).trigger('change', [false]);
|
||||
WFLS.updatePendingChanges();
|
||||
});
|
||||
});
|
||||
|
||||
//On/Off Multiple Option
|
||||
$('.wfls-option.wfls-option-toggled-multiple .wfls-option-checkbox').each(function() {
|
||||
$(this).on('keydown', function(e) {
|
||||
if (e.keyCode == 32) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$(this).on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var optionElement = $(this).closest('.wfls-option');
|
||||
if (optionElement.hasClass('wfls-option-premium') || optionElement.hasClass('wfls-disabled') || $(this).hasClass('wfls-disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var checkboxElement = $(this).closest('ul');
|
||||
var option = checkboxElement.data('option');
|
||||
var value = false;
|
||||
var isActive = $(this).hasClass('wfls-checked');
|
||||
if (isActive) {
|
||||
$(this).removeClass('wfls-checked').attr('aria-checked', 'false');
|
||||
value = checkboxElement.data('disabledValue');
|
||||
}
|
||||
else {
|
||||
$(this).addClass('wfls-checked').attr('aria-checked', 'true');
|
||||
value = checkboxElement.data('enabledValue');
|
||||
}
|
||||
|
||||
var originalValue = checkboxElement.data('originalValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
$(optionElement).trigger('change', [false]);
|
||||
WFLS.updatePendingChanges();
|
||||
});
|
||||
|
||||
$(this).parent().find('.wfls-option-title').on('click', function(e) {
|
||||
var links = $(this).find('a');
|
||||
var buffer = 10;
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var t = $(links[i]).offset().top;
|
||||
var l = $(links[i]).offset().left;
|
||||
var b = t + $(links[i]).height();
|
||||
var r = l + $(links[i]).width();
|
||||
|
||||
if (e.pageX > l - buffer && e.pageX < r + buffer && e.pageY > t - buffer && e.pageY < b + buffer) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$(this).parent().find('.wfls-option-checkbox').trigger('click');
|
||||
}).css('cursor', 'pointer');
|
||||
});
|
||||
|
||||
//Text field option
|
||||
$('.wfls-option.wfls-option-text > .wfls-option-content > ul > li.wfls-option-text input').on('change paste keyup', function() {
|
||||
var e = this;
|
||||
|
||||
setTimeout(function() {
|
||||
var optionElement = $(e).closest('.wfls-option');
|
||||
var option = optionElement.data('textOption');
|
||||
|
||||
if (typeof option !== 'undefined') {
|
||||
var value = $(e).val();
|
||||
|
||||
var originalValue = optionElement.data('originalTextValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
$(optionElement).trigger('change', [false]);
|
||||
WFLS.updatePendingChanges();
|
||||
}
|
||||
}, 4);
|
||||
});
|
||||
|
||||
//Menu option
|
||||
$('.wfls-option.wfls-option-toggled-select > .wfls-option-content > ul > li.wfls-option-select select, .wfls-option.wfls-option-select > .wfls-option-content > ul > li.wfls-option-select select, .wf-option.wfls-option-select > li.wfls-option-select select').each(function() {
|
||||
if (!$.fn.wfselect2) { return; }
|
||||
|
||||
var width = (WFLS.screenSize(500) ? '200px' : 'resolve');
|
||||
if ($(this).data('preferredWidth')) {
|
||||
width = $(this).data('preferredWidth');
|
||||
}
|
||||
|
||||
$(this).wfselect2({
|
||||
minimumResultsForSearch: -1,
|
||||
width: width
|
||||
}).on('change', function () {
|
||||
var optionElement = $(this).closest('.wfls-option');
|
||||
var option = optionElement.data('selectOption');
|
||||
var value = $(this).val();
|
||||
|
||||
var originalValue = optionElement.data('originalSelectValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
$(optionElement).trigger('change', [false]);
|
||||
WFLS.updatePendingChanges();
|
||||
});
|
||||
}).triggerHandler('change');
|
||||
|
||||
//Text area option
|
||||
$('.wfls-option.wfls-option-textarea > .wfls-option-content > ul > li.wfls-option-textarea textarea').on('change paste keyup', function() {
|
||||
var e = this;
|
||||
|
||||
setTimeout(function() {
|
||||
var optionElement = $(e).closest('.wfls-option');
|
||||
var option = optionElement.data('textOption');
|
||||
var value = $(e).val();
|
||||
|
||||
var originalValue = optionElement.data('originalTextValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
$(optionElement).trigger('change', [false]);
|
||||
WFLS.updatePendingChanges();
|
||||
}, 4);
|
||||
});
|
||||
|
||||
//Switch Option
|
||||
$('.wfls-option.wfls-option-switch .wfls-switch > li').each(function(index, element) {
|
||||
$(this).on('keydown', function(e) {
|
||||
if (e.keyCode == 32) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$(element).on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var optionElement = $(this).closest('ul.wfls-option-switch, div.wfls-option-switch');
|
||||
var optionName = optionElement.data('optionName');
|
||||
var originalValue = optionElement.data('originalValue');
|
||||
var value = $(this).data('optionValue');
|
||||
|
||||
var control = $(this).closest('.wfls-switch');
|
||||
control.find('li').each(function() {
|
||||
$(this).toggleClass('wfls-active', value == $(this).data('optionValue')).attr('aria-checked', value == $(this).data('optionValue') ? 'true' : 'false');
|
||||
});
|
||||
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[optionName];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[optionName] = value;
|
||||
}
|
||||
|
||||
$(optionElement).trigger('change', [false]);
|
||||
WFLS.updatePendingChanges();
|
||||
});
|
||||
});
|
||||
|
||||
//Dropdown/Text Options
|
||||
$('select.wfls-option-select, input.wfls-option-input').each(function() {
|
||||
$(this).data('original', $(this).val());
|
||||
}).on('change input', function(e) {
|
||||
var input = $(this);
|
||||
var name = input.attr('name');
|
||||
var value = input.val();
|
||||
var original = input.data('original');
|
||||
if (value === original || (input.hasClass('wfls-option-input-required') && value === '')) {
|
||||
delete WFLS.pendingChanges[name];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[name] = value;
|
||||
}
|
||||
WFLS.updatePendingChanges();
|
||||
});
|
||||
|
||||
$('#wfls-save-changes').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
WFLS.saveOptions(function(res) {
|
||||
WFLS.pendingChanges = {};
|
||||
WFLS.updatePendingChanges();
|
||||
|
||||
if (res.redirect) {
|
||||
window.location.href = res.redirect;
|
||||
}
|
||||
else {
|
||||
window.location.reload(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#wfls-cancel-changes').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
//On/Off options
|
||||
$('.wfls-option.wfls-option-toggled').each(function() {
|
||||
var enabledValue = $(this).data('enabledValue');
|
||||
var disabledValue = $(this).data('disabledValue');
|
||||
var originalValue = $(this).data('originalValue');
|
||||
if (enabledValue == originalValue) {
|
||||
$(this).find('.wfls-option-checkbox').addClass('wfls-checked').attr('aria-checked', 'true');
|
||||
}
|
||||
else {
|
||||
$(this).find('.wfls-option-checkbox').removeClass('wfls-checked').attr('aria-checked', 'false');
|
||||
}
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
|
||||
$('.wfls-option-toggled-boolean-switch').each(function() {
|
||||
var enabledValue = $(this).data('enabledValue');
|
||||
var disabledValue = $(this).data('disabledValue');
|
||||
var originalValue = $(this).data('originalValue');
|
||||
if (enabledValue == originalValue) {
|
||||
$(this).find('.wfls-boolean-switch').addClass('wfls-active').attr('aria-checked', 'true');
|
||||
}
|
||||
else {
|
||||
$(this).find('.wfls-boolean-switch').removeClass('wfls-active').attr('aria-checked', 'false');
|
||||
}
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
|
||||
$('.wfls-option.wfls-option-toggled-segmented').each(function() {
|
||||
var originalValue = $(this).data('originalValue');
|
||||
$(this).find('[type=radio]').each(function() {
|
||||
if (this.value == originalValue) {
|
||||
this.checked = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
|
||||
//On/Off multiple options
|
||||
$('.wfls-option.wfls-option-toggled-multiple').each(function() {
|
||||
$(this).find('.wfls-option-checkboxes > ul').each(function() {
|
||||
var enabledValue = $(this).data('enabledValue');
|
||||
var disabledValue = $(this).data('disabledValue');
|
||||
var originalValue = $(this).data('originalValue');
|
||||
if (enabledValue == originalValue) {
|
||||
$(this).find('.wfls-option-checkbox').addClass('wfls-checked').attr('aria-checked', 'true');
|
||||
}
|
||||
else {
|
||||
$(this).find('.wfls-option-checkbox').removeClass('wfls-checked').attr('aria-checked', 'false');
|
||||
}
|
||||
});
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
|
||||
//On/Off options with menu
|
||||
$('.wfls-option.wfls-option-toggled-select').each(function() {
|
||||
var selectElement = $(this).find('.wfls-option-select select');
|
||||
var enabledToggleValue = $(this).data('enabledToggleValue');
|
||||
var disabledToggleValue = $(this).data('disabledToggleValue');
|
||||
var originalToggleValue = $(this).data('originalToggleValue');
|
||||
if (enabledToggleValue == originalToggleValue) {
|
||||
$(this).find('.wfls-option-checkbox').addClass('wfls-checked').attr('aria-checked', 'true');
|
||||
selectElement.attr('disabled', false);
|
||||
}
|
||||
else {
|
||||
$(this).find('.wfls-option-checkbox').removeClass('wfls-checked').attr('aria-checked', 'false');
|
||||
selectElement.attr('disabled', true);
|
||||
}
|
||||
|
||||
var originalSelectValue = $(this).data('originalSelectValue');
|
||||
$(this).find('.wfls-option-select select').val(originalSelectValue).trigger('change');
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
|
||||
//Menu options
|
||||
$('.wfls-option.wfls-option-select').each(function() {
|
||||
var originalSelectValue = $(this).data('originalSelectValue');
|
||||
$(this).find('.wfls-option-select select').val(originalSelectValue).trigger('change');
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
|
||||
//Text options
|
||||
$('.wfls-option.wfls-option-text').each(function() {
|
||||
var originalTextValue = $(this).data('originalTextValue');
|
||||
if (typeof originalTextValue !== 'undefined') {
|
||||
$(this).find('.wfls-option-text input').val(originalTextValue);
|
||||
}
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
|
||||
//Text area options
|
||||
$('.wfls-option.wfls-option-textarea').each(function() {
|
||||
var originalTextValue = $(this).data('originalTextValue');
|
||||
$(this).find('.wfls-option-textarea textarea').val(originalTextValue);
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
|
||||
//Token options
|
||||
$('.wfls-option.wfls-option-token').each(function() {
|
||||
var originalTokenValue = $(this).data('originalTokenValue');
|
||||
$(this).find('select').val(originalTokenValue).trigger('change');
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
|
||||
//Switch options
|
||||
$('.wfls-option.wfls-option-switch').each(function() {
|
||||
var originalValue = $(this).data('originalValue');
|
||||
$(this).find('.wfls-switch > li').each(function() {
|
||||
$(this).toggleClass('wfls-active', originalValue == $(this).data('optionValue')).attr('aria-checked', originalValue == $(this).data('optionValue') ? 'true' : 'false');
|
||||
});
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
|
||||
//Other options
|
||||
$(window).trigger('wflsOptionsReset');
|
||||
|
||||
WFLS.pendingChanges = {};
|
||||
WFLS.updatePendingChanges();
|
||||
});
|
||||
},
|
||||
|
||||
updatePendingChanges: function() {
|
||||
$(window).off('beforeunload', WFLS._unsavedOptionsHandler);
|
||||
if (Object.keys(WFLS.pendingChanges).length) {
|
||||
$('#wfls-cancel-changes').removeClass('wfls-disabled');
|
||||
$('#wfls-save-changes').removeClass('wfls-disabled');
|
||||
$(window).on('beforeunload', WFLS._unsavedOptionsHandler);
|
||||
}
|
||||
else {
|
||||
$('#wfls-cancel-changes').addClass('wfls-disabled');
|
||||
$('#wfls-save-changes').addClass('wfls-disabled');
|
||||
}
|
||||
},
|
||||
|
||||
_unsavedOptionsHandler: function(e) {
|
||||
var message = "You have unsaved changes to your options. If you leave this page, those changes will be lost."; //Only shows on older browsers, newer browsers don't allow message customization
|
||||
e = e || window.event;
|
||||
if (e) {
|
||||
e.returnValue = message; //IE and Firefox
|
||||
}
|
||||
return message; //Others
|
||||
},
|
||||
|
||||
setOptions: function(options, successCallback, failureCallback) {
|
||||
if (!Object.keys(options).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ajax('wordfence_ls_save_options', {changes: JSON.stringify(options)}, function(res) {
|
||||
if (res.success) {
|
||||
typeof successCallback == 'function' && successCallback(res);
|
||||
}
|
||||
else {
|
||||
if (res.hasOwnProperty('html') && res.html) {
|
||||
WFLS.panelModalHTML((WFLS.screenSize(500) ? '300px' : '400px'), 'Error Saving Options', res.error);
|
||||
}
|
||||
else {
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), 'Error Saving Options', res.error);
|
||||
}
|
||||
|
||||
typeof failureCallback == 'function' && failureCallback
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
saveOptions: function(successCallback, failureCallback) {
|
||||
this.setOptions(WFLS.pendingChanges, successCallback, failureCallback);
|
||||
},
|
||||
|
||||
updateIPPreview: function(value, successCallback) {
|
||||
this.ajax('wordfence_ls_update_ip_preview', value, function(response) {
|
||||
if (successCallback) {
|
||||
successCallback(response);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a WP AJAX call, automatically adding our nonce.
|
||||
*
|
||||
* @param string action
|
||||
* @param string|array|object payload
|
||||
* @param function successCallback
|
||||
* @param function failureCallback
|
||||
*/
|
||||
ajax: function(action, payload, successCallback, failureCallback) {
|
||||
if (typeof(payload) == 'string') {
|
||||
if (payload.length > 0) {
|
||||
payload += '&';
|
||||
}
|
||||
payload += 'action=' + action + '&nonce=' + WFLSVars.nonce;
|
||||
}
|
||||
else if (typeof(payload) == 'object' && payload instanceof Array) {
|
||||
// jQuery serialized form data
|
||||
payload.push({
|
||||
name: 'action',
|
||||
value: action
|
||||
});
|
||||
payload.push({
|
||||
name: 'nonce',
|
||||
value: WFLSVars.nonce
|
||||
});
|
||||
}
|
||||
else if (typeof(payload) == 'object') {
|
||||
payload['action'] = action;
|
||||
payload['nonce'] = WFLSVars.nonce;
|
||||
}
|
||||
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: WFLSVars.ajaxurl,
|
||||
dataType: "json",
|
||||
data: payload,
|
||||
success: function(json) {
|
||||
typeof successCallback == 'function' && successCallback(json);
|
||||
},
|
||||
error: function() {
|
||||
typeof failureCallback == 'function' && failureCallback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a generic panel.
|
||||
*
|
||||
* @param @param string width A width string in the format '100px'
|
||||
* @param string heading
|
||||
* @param string body
|
||||
* @param object settings
|
||||
*/
|
||||
panel: function(width, heading, body, settings) {
|
||||
if (typeof settings === 'undefined') {
|
||||
settings = {};
|
||||
}
|
||||
WFLS.panelQueue.push([width, "<h3>" + heading + "</h3><p>" + body + "</p>", settings]);
|
||||
WFLS._panelServiceQueue();
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a modal panel with fixed HTML content.
|
||||
*
|
||||
* @param @param string width A width string in the format '100px'
|
||||
* @param string heading
|
||||
* @param string body
|
||||
* @param object settings
|
||||
*/
|
||||
panelModalHTML: function(width, heading, body, settings) {
|
||||
if (typeof settings === 'undefined') {
|
||||
settings = {};
|
||||
}
|
||||
|
||||
var prompt = $.tmpl(WFLSVars.modalHTMLTemplate, {title: heading, message: body});
|
||||
var promptHTML = $("<div />").append(prompt).html();
|
||||
var callback = settings.onComplete;
|
||||
settings.overlayClose = false;
|
||||
settings.closeButton = false;
|
||||
settings.className = 'wfls-modal';
|
||||
settings.onComplete = function() {
|
||||
$('#wfls-generic-modal-close').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
WFLS.panelClose();
|
||||
});
|
||||
|
||||
typeof callback === 'function' && callback();
|
||||
};
|
||||
WFLS.panelHTML(width, promptHTML, settings)
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a modal panel, automatically escaping the content.
|
||||
*
|
||||
* @param @param string width A width string in the format '100px'
|
||||
* @param string heading
|
||||
* @param string body
|
||||
* @param object settings
|
||||
*/
|
||||
panelModal: function(width, heading, body, settings) {
|
||||
if (typeof settings === 'undefined') {
|
||||
settings = {};
|
||||
}
|
||||
|
||||
if (width === null)
|
||||
width = WFLS.screenSize(500) ? '300px' : '400px';
|
||||
|
||||
var includeDefaultButtons = typeof settings.includeDefaultButtons === 'undefined' ? false : settings.includeDefaultButtons;
|
||||
var prompt = $.tmpl(WFLSVars[includeDefaultButtons ? 'modalTemplate' : 'modalNoButtonsTemplate'], {title: heading, message: body});
|
||||
|
||||
if (typeof settings.additional_buttons !== 'undefined') {
|
||||
var buttonSection = prompt.find('.wfls-modal-footer > ul');
|
||||
for(index in settings.additional_buttons) {
|
||||
var buttonSettings = settings.additional_buttons[index];
|
||||
var button = $('<button>').text(buttonSettings.label)
|
||||
.addClass('wfls-btn wfls-btn-callout-subtle wfls-additional-button')
|
||||
.attr('id', buttonSettings.id);
|
||||
var buttonType = typeof buttonSettings.type === 'undefined' ? 'default' : buttonSettings.type;
|
||||
button.addClass('wfls-btn-' + buttonType);
|
||||
buttonSection.prepend($("<li>").addClass('wfls-padding-add-left-small').append(button));
|
||||
}
|
||||
}
|
||||
|
||||
var promptHTML = $("<div />").append(prompt).html();
|
||||
var callback = settings.onComplete;
|
||||
settings.overlayClose = false;
|
||||
settings.closeButton = false;
|
||||
settings.className = 'wfls-modal';
|
||||
settings.onComplete = function() {
|
||||
$('#wfls-generic-modal-close').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
WFLS.panelClose();
|
||||
});
|
||||
|
||||
typeof callback === 'function' && callback();
|
||||
};
|
||||
WFLS.panelHTML(width, promptHTML, settings)
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a modal with the given title and message text.
|
||||
*
|
||||
* @param string title the modal title
|
||||
* @param string message the message (this will be treated as text, not HTML)
|
||||
* @param array buttons the buttons to include in the modal footer
|
||||
* Each item in the array should be an object with the following properties:
|
||||
* - label: The button text
|
||||
* - id: An ID for the button
|
||||
* - type: The type of button for styling purposes - i.e. default, primary (default: 'default')
|
||||
* @param object settings
|
||||
*
|
||||
* @see WFLS.panelModal
|
||||
*/
|
||||
displayModalMessage: function(title, message, buttons, settings) {
|
||||
if (typeof settings !== 'object')
|
||||
settings = {};
|
||||
var width = typeof settings.width === 'undefined' ? null : settings.width;
|
||||
settings.includeDefaultButtons = false;
|
||||
settings.additional_buttons = buttons;
|
||||
WFLS.panelModal(width, title, message, settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a modal panel with the error formatting.
|
||||
*
|
||||
* @param string errorMsg
|
||||
* @param bool isTokenError Whether or not this error is an expired nonce error.
|
||||
*/
|
||||
panelError: function(errorMsg, isTokenError) {
|
||||
var callback = false;
|
||||
if (isTokenError) {
|
||||
if (WFLS.tokenErrorShowing) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback = function() {
|
||||
setTimeout(function() {
|
||||
WFLS.tokenErrorShowing = false;
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
WFLS.tokenErrorShowing = true;
|
||||
}
|
||||
|
||||
var prompt = $.tmpl(WFLSVars.tokenInvalidTemplate, {title: 'An error occurred', message: errorMsg});
|
||||
var promptHTML = $("<div />").append(prompt).html();
|
||||
var settings = {};
|
||||
settings.overlayClose = false;
|
||||
settings.closeButton = false;
|
||||
settings.className = 'wfls-modal';
|
||||
settings.onComplete = function() {
|
||||
$('#wfls-token-invalid-modal-reload').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
window.location.reload(true);
|
||||
});
|
||||
|
||||
typeof callback === 'function' && callback();
|
||||
};
|
||||
WFLS.panelHTML((WFLS.screenSize(500) ? '300px' : '400px'), promptHTML, settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays a panel with fixed HTML content.
|
||||
*
|
||||
* @param string width A width string in the format '100px'
|
||||
* @param string html
|
||||
* @param object settings
|
||||
*/
|
||||
panelHTML: function(width, html, settings) {
|
||||
if (typeof settings === 'undefined') {
|
||||
settings = {};
|
||||
}
|
||||
WFLS.panelQueue.push([width, html, settings]);
|
||||
WFLS._panelServiceQueue();
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays the next panel in the queue.
|
||||
*/
|
||||
_panelServiceQueue: function() {
|
||||
if (WFLS.panelIsOpen) {
|
||||
return;
|
||||
}
|
||||
if (WFLS.panelQueue.length < 1) {
|
||||
return;
|
||||
}
|
||||
var elem = WFLS.panelQueue.shift();
|
||||
WFLS._panelOpen(elem[0], elem[1], elem[2]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Does the actual function call to display the panel.
|
||||
*
|
||||
* @param string width A width string in the format '100px'
|
||||
* @param string html
|
||||
* @param object settings
|
||||
*/
|
||||
_panelOpen: function(width, html, settings) {
|
||||
this.panelIsOpen = true;
|
||||
$.extend(settings, {
|
||||
width: width,
|
||||
html: html,
|
||||
onClosed: function() {
|
||||
WFLS.panelClose();
|
||||
}
|
||||
});
|
||||
$.wflscolorbox(settings);
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes the current panel.
|
||||
*/
|
||||
panelClose: function() {
|
||||
WFLS.panelIsOpen = false;
|
||||
if (WFLS.panelQueue.length < 1) {
|
||||
$.wflscolorbox.close();
|
||||
}
|
||||
else {
|
||||
WFLS._panelServiceQueue();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses and returns the hash portion of a URL, working around user agents that URL-encode the # character.
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
parseHashes: function() {
|
||||
var hashes = window.location.hash.replace('%23', '#');
|
||||
var splitHashes = hashes.split('#');
|
||||
var result = [];
|
||||
for (var i = 0; i < splitHashes.length; i++) {
|
||||
if (splitHashes[i].length > 0) {
|
||||
result.push(splitHashes[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether or not the screen size is within the size given. This may be a numerical value
|
||||
* or one of the WFLS_SCREEN_ constants.
|
||||
*
|
||||
* @param size
|
||||
* @returns {boolean}
|
||||
*/
|
||||
screenSize: function(size) {
|
||||
switch (size) {
|
||||
case WFLS.SCREEN_XS:
|
||||
return window.matchMedia("only screen and (max-width: 767px)").matches;
|
||||
case WFLS.SCREEN_SM:
|
||||
return window.matchMedia("only screen and (max-width: 991px)").matches;
|
||||
case WFLS.SCREEN_MD:
|
||||
return window.matchMedia("only screen and (max-width: 1199px)").matches;
|
||||
case WFLS.SCREEN_LG:
|
||||
return window.matchMedia("only screen and (max-width: 32767px)").matches;
|
||||
}
|
||||
|
||||
var parsed = parseInt(size);
|
||||
if (isNaN(parsed)) {
|
||||
return false;
|
||||
}
|
||||
return window.matchMedia("only screen and (max-width: " + parsed + "px)").matches;
|
||||
},
|
||||
};
|
||||
|
||||
$(function() {
|
||||
WFLS.init();
|
||||
});
|
||||
|
||||
$.fn.crossfade = function(incoming, duration, complete) {
|
||||
duration = duration || 400;
|
||||
complete = complete || function() { };
|
||||
|
||||
return this.each(function() {
|
||||
$(this).fadeOut(duration, function() {
|
||||
$(incoming).fadeIn(duration, complete);
|
||||
});
|
||||
});
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
|
||||
var saveAs=saveAs||function(e){"use strict";if(typeof e==="undefined"||typeof navigator!=="undefined"&&/MSIE [1-9]\./.test(navigator.userAgent)){return}var t=e.document,n=function(){return e.URL||e.webkitURL||e},r=t.createElementNS("http://www.w3.org/1999/xhtml","a"),o="download"in r,i=function(e){var t=new MouseEvent("click");e.dispatchEvent(t)},a=/constructor/i.test(e.HTMLElement),f=/CriOS\/[\d]+/.test(navigator.userAgent),u=function(t){(e.setImmediate||e.setTimeout)(function(){throw t},0)},d="application/octet-stream",s=1e3*40,c=function(e){var t=function(){if(typeof e==="string"){n().revokeObjectURL(e)}else{e.remove()}};setTimeout(t,s)},l=function(e,t,n){t=[].concat(t);var r=t.length;while(r--){var o=e["on"+t[r]];if(typeof o==="function"){try{o.call(e,n||e)}catch(i){u(i)}}}},p=function(e){if(/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)){return new Blob([String.fromCharCode(65279),e],{type:e.type})}return e},v=function(t,u,s){if(!s){t=p(t)}var v=this,w=t.type,m=w===d,y,h=function(){l(v,"writestart progress write writeend".split(" "))},S=function(){if((f||m&&a)&&e.FileReader){var r=new FileReader;r.onloadend=function(){var t=f?r.result:r.result.replace(/^data:[^;]*;/,"data:attachment/file;");var n=e.open(t,"_blank");if(!n)e.location.href=t;t=undefined;v.readyState=v.DONE;h()};r.readAsDataURL(t);v.readyState=v.INIT;return}if(!y){y=n().createObjectURL(t)}if(m){e.location.href=y}else{var o=e.open(y,"_blank");if(!o){e.location.href=y}}v.readyState=v.DONE;h();c(y)};v.readyState=v.INIT;if(o){y=n().createObjectURL(t);setTimeout(function(){r.href=y;r.download=u;i(r);h();c(y);v.readyState=v.DONE});return}S()},w=v.prototype,m=function(e,t,n){return new v(e,t||e.name||"download",n)};if(typeof navigator!=="undefined"&&navigator.msSaveOrOpenBlob){return function(e,t,n){t=t||e.name||"download";if(!n){e=p(e)}return navigator.msSaveOrOpenBlob(e,t)}}w.abort=function(){};w.readyState=w.INIT=0;w.WRITING=1;w.DONE=2;w.error=w.onwritestart=w.onprogress=w.onwrite=w.onabort=w.onerror=w.onwriteend=null;return m}(typeof self!=="undefined"&&self||typeof window!=="undefined"&&window||this.content);if(typeof module!=="undefined"&&module.exports){module.exports.saveAs=saveAs}else if(typeof define!=="undefined"&&define!==null&&define.amd!==null){define([],function(){return saveAs})}
|
||||
|
||||
!function(t){"use strict";if(t.URL=t.URL||t.webkitURL,t.Blob&&t.URL)try{return void new Blob}catch(e){}var n=t.BlobBuilder||t.WebKitBlobBuilder||t.MozBlobBuilder||function(t){var e=function(t){return Object.prototype.toString.call(t).match(/^\[object\s(.*)\]$/)[1]},n=function(){this.data=[]},o=function(t,e,n){this.data=t,this.size=t.length,this.type=e,this.encoding=n},i=n.prototype,a=o.prototype,r=t.FileReaderSync,c=function(t){this.code=this[this.name=t]},l="NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR".split(" "),s=l.length,u=t.URL||t.webkitURL||t,d=u.createObjectURL,f=u.revokeObjectURL,R=u,p=t.btoa,h=t.atob,b=t.ArrayBuffer,g=t.Uint8Array,w=/^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/;for(o.fake=a.fake=!0;s--;)c.prototype[l[s]]=s+1;return u.createObjectURL||(R=t.URL=function(t){var e,n=document.createElementNS("http://www.w3.org/1999/xhtml","a");return n.href=t,"origin"in n||("data:"===n.protocol.toLowerCase()?n.origin=null:(e=t.match(w),n.origin=e&&e[1])),n}),R.createObjectURL=function(t){var e,n=t.type;return null===n&&(n="application/octet-stream"),t instanceof o?(e="data:"+n,"base64"===t.encoding?e+";base64,"+t.data:"URI"===t.encoding?e+","+decodeURIComponent(t.data):p?e+";base64,"+p(t.data):e+","+encodeURIComponent(t.data)):d?d.call(u,t):void 0},R.revokeObjectURL=function(t){"data:"!==t.substring(0,5)&&f&&f.call(u,t)},i.append=function(t){var n=this.data;if(g&&(t instanceof b||t instanceof g)){for(var i="",a=new g(t),l=0,s=a.length;s>l;l++)i+=String.fromCharCode(a[l]);n.push(i)}else if("Blob"===e(t)||"File"===e(t)){if(!r)throw new c("NOT_READABLE_ERR");var u=new r;n.push(u.readAsBinaryString(t))}else t instanceof o?"base64"===t.encoding&&h?n.push(h(t.data)):"URI"===t.encoding?n.push(decodeURIComponent(t.data)):"raw"===t.encoding&&n.push(t.data):("string"!=typeof t&&(t+=""),n.push(unescape(encodeURIComponent(t))))},i.getBlob=function(t){return arguments.length||(t=null),new o(this.data.join(""),t,"raw")},i.toString=function(){return"[object BlobBuilder]"},a.slice=function(t,e,n){var i=arguments.length;return 3>i&&(n=null),new o(this.data.slice(t,i>1?e:this.data.length),n,this.encoding)},a.toString=function(){return"[object Blob]"},a.close=function(){this.size=0,delete this.data},n}(t);t.Blob=function(t,e){var o=e?e.type||"":"",i=new n;if(t)for(var a=0,r=t.length;r>a;a++)Uint8Array&&t[a]instanceof Uint8Array?i.append(t[a].buffer):i.append(t[a]);var c=i.getBlob(o);return!c.slice&&c.webkitSlice&&(c.slice=c.webkitSlice),c};var o=Object.getPrototypeOf||function(t){return t.__proto__};t.Blob.prototype=o(new t.Blob)}("undefined"!=typeof self&&self||"undefined"!=typeof window&&window||this.content||this);
|
||||
@@ -0,0 +1,28 @@
|
||||
(function(r){r.fn.qrcode=function(h){var s;function u(a){this.mode=s;this.data=a}function o(a,c){this.typeNumber=a;this.errorCorrectLevel=c;this.modules=null;this.moduleCount=0;this.dataCache=null;this.dataList=[]}function q(a,c){if(void 0==a.length)throw Error(a.length+"/"+c);for(var d=0;d<a.length&&0==a[d];)d++;this.num=Array(a.length-d+c);for(var b=0;b<a.length-d;b++)this.num[b]=a[b+d]}function p(a,c){this.totalCount=a;this.dataCount=c}function t(){this.buffer=[];this.length=0}u.prototype={getLength:function(){return this.data.length},
|
||||
write:function(a){for(var c=0;c<this.data.length;c++)a.put(this.data.charCodeAt(c),8)}};o.prototype={addData:function(a){this.dataList.push(new u(a));this.dataCache=null},isDark:function(a,c){if(0>a||this.moduleCount<=a||0>c||this.moduleCount<=c)throw Error(a+","+c);return this.modules[a][c]},getModuleCount:function(){return this.moduleCount},make:function(){if(1>this.typeNumber){for(var a=1,a=1;40>a;a++){for(var c=p.getRSBlocks(a,this.errorCorrectLevel),d=new t,b=0,e=0;e<c.length;e++)b+=c[e].dataCount;
|
||||
for(e=0;e<this.dataList.length;e++)c=this.dataList[e],d.put(c.mode,4),d.put(c.getLength(),j.getLengthInBits(c.mode,a)),c.write(d);if(d.getLengthInBits()<=8*b)break}this.typeNumber=a}this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17;this.modules=Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=Array(this.moduleCount);for(var b=0;b<this.moduleCount;b++)this.modules[d][b]=null}this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-
|
||||
7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(a,c);7<=this.typeNumber&&this.setupTypeNumber(a);null==this.dataCache&&(this.dataCache=o.createData(this.typeNumber,this.errorCorrectLevel,this.dataList));this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,c){for(var d=-1;7>=d;d++)if(!(-1>=a+d||this.moduleCount<=a+d))for(var b=-1;7>=b;b++)-1>=c+b||this.moduleCount<=c+b||(this.modules[a+d][c+b]=
|
||||
0<=d&&6>=d&&(0==b||6==b)||0<=b&&6>=b&&(0==d||6==d)||2<=d&&4>=d&&2<=b&&4>=b?!0:!1)},getBestMaskPattern:function(){for(var a=0,c=0,d=0;8>d;d++){this.makeImpl(!0,d);var b=j.getLostPoint(this);if(0==d||a>b)a=b,c=d}return c},createMovieClip:function(a,c,d){a=a.createEmptyMovieClip(c,d);this.make();for(c=0;c<this.modules.length;c++)for(var d=1*c,b=0;b<this.modules[c].length;b++){var e=1*b;this.modules[c][b]&&(a.beginFill(0,100),a.moveTo(e,d),a.lineTo(e+1,d),a.lineTo(e+1,d+1),a.lineTo(e,d+1),a.endFill())}return a},
|
||||
setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=0==a%2);for(a=8;a<this.moduleCount-8;a++)null==this.modules[6][a]&&(this.modules[6][a]=0==a%2)},setupPositionAdjustPattern:function(){for(var a=j.getPatternPosition(this.typeNumber),c=0;c<a.length;c++)for(var d=0;d<a.length;d++){var b=a[c],e=a[d];if(null==this.modules[b][e])for(var f=-2;2>=f;f++)for(var i=-2;2>=i;i++)this.modules[b+f][e+i]=-2==f||2==f||-2==i||2==i||0==f&&0==i?!0:!1}},setupTypeNumber:function(a){for(var c=
|
||||
j.getBCHTypeNumber(this.typeNumber),d=0;18>d;d++){var b=!a&&1==(c>>d&1);this.modules[Math.floor(d/3)][d%3+this.moduleCount-8-3]=b}for(d=0;18>d;d++)b=!a&&1==(c>>d&1),this.modules[d%3+this.moduleCount-8-3][Math.floor(d/3)]=b},setupTypeInfo:function(a,c){for(var d=j.getBCHTypeInfo(this.errorCorrectLevel<<3|c),b=0;15>b;b++){var e=!a&&1==(d>>b&1);6>b?this.modules[b][8]=e:8>b?this.modules[b+1][8]=e:this.modules[this.moduleCount-15+b][8]=e}for(b=0;15>b;b++)e=!a&&1==(d>>b&1),8>b?this.modules[8][this.moduleCount-
|
||||
b-1]=e:9>b?this.modules[8][15-b-1+1]=e:this.modules[8][15-b-1]=e;this.modules[this.moduleCount-8][8]=!a},mapData:function(a,c){for(var d=-1,b=this.moduleCount-1,e=7,f=0,i=this.moduleCount-1;0<i;i-=2)for(6==i&&i--;;){for(var g=0;2>g;g++)if(null==this.modules[b][i-g]){var n=!1;f<a.length&&(n=1==(a[f]>>>e&1));j.getMask(c,b,i-g)&&(n=!n);this.modules[b][i-g]=n;e--; -1==e&&(f++,e=7)}b+=d;if(0>b||this.moduleCount<=b){b-=d;d=-d;break}}}};o.PAD0=236;o.PAD1=17;o.createData=function(a,c,d){for(var c=p.getRSBlocks(a,
|
||||
c),b=new t,e=0;e<d.length;e++){var f=d[e];b.put(f.mode,4);b.put(f.getLength(),j.getLengthInBits(f.mode,a));f.write(b)}for(e=a=0;e<c.length;e++)a+=c[e].dataCount;if(b.getLengthInBits()>8*a)throw Error("code length overflow. ("+b.getLengthInBits()+">"+8*a+")");for(b.getLengthInBits()+4<=8*a&&b.put(0,4);0!=b.getLengthInBits()%8;)b.putBit(!1);for(;!(b.getLengthInBits()>=8*a);){b.put(o.PAD0,8);if(b.getLengthInBits()>=8*a)break;b.put(o.PAD1,8)}return o.createBytes(b,c)};o.createBytes=function(a,c){for(var d=
|
||||
0,b=0,e=0,f=Array(c.length),i=Array(c.length),g=0;g<c.length;g++){var n=c[g].dataCount,h=c[g].totalCount-n,b=Math.max(b,n),e=Math.max(e,h);f[g]=Array(n);for(var k=0;k<f[g].length;k++)f[g][k]=255&a.buffer[k+d];d+=n;k=j.getErrorCorrectPolynomial(h);n=(new q(f[g],k.getLength()-1)).mod(k);i[g]=Array(k.getLength()-1);for(k=0;k<i[g].length;k++)h=k+n.getLength()-i[g].length,i[g][k]=0<=h?n.get(h):0}for(k=g=0;k<c.length;k++)g+=c[k].totalCount;d=Array(g);for(k=n=0;k<b;k++)for(g=0;g<c.length;g++)k<f[g].length&&
|
||||
(d[n++]=f[g][k]);for(k=0;k<e;k++)for(g=0;g<c.length;g++)k<i[g].length&&(d[n++]=i[g][k]);return d};s=4;for(var j={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,
|
||||
78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var c=a<<10;0<=j.getBCHDigit(c)-j.getBCHDigit(j.G15);)c^=j.G15<<j.getBCHDigit(c)-j.getBCHDigit(j.G15);return(a<<10|c)^j.G15_MASK},getBCHTypeNumber:function(a){for(var c=a<<12;0<=j.getBCHDigit(c)-
|
||||
j.getBCHDigit(j.G18);)c^=j.G18<<j.getBCHDigit(c)-j.getBCHDigit(j.G18);return a<<12|c},getBCHDigit:function(a){for(var c=0;0!=a;)c++,a>>>=1;return c},getPatternPosition:function(a){return j.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,c,d){switch(a){case 0:return 0==(c+d)%2;case 1:return 0==c%2;case 2:return 0==d%3;case 3:return 0==(c+d)%3;case 4:return 0==(Math.floor(c/2)+Math.floor(d/3))%2;case 5:return 0==c*d%2+c*d%3;case 6:return 0==(c*d%2+c*d%3)%2;case 7:return 0==(c*d%3+(c+d)%2)%2;default:throw Error("bad maskPattern:"+
|
||||
a);}},getErrorCorrectPolynomial:function(a){for(var c=new q([1],0),d=0;d<a;d++)c=c.multiply(new q([1,l.gexp(d)],0));return c},getLengthInBits:function(a,c){if(1<=c&&10>c)switch(a){case 1:return 10;case 2:return 9;case s:return 8;case 8:return 8;default:throw Error("mode:"+a);}else if(27>c)switch(a){case 1:return 12;case 2:return 11;case s:return 16;case 8:return 10;default:throw Error("mode:"+a);}else if(41>c)switch(a){case 1:return 14;case 2:return 13;case s:return 16;case 8:return 12;default:throw Error("mode:"+
|
||||
a);}else throw Error("type:"+c);},getLostPoint:function(a){for(var c=a.getModuleCount(),d=0,b=0;b<c;b++)for(var e=0;e<c;e++){for(var f=0,i=a.isDark(b,e),g=-1;1>=g;g++)if(!(0>b+g||c<=b+g))for(var h=-1;1>=h;h++)0>e+h||c<=e+h||0==g&&0==h||i==a.isDark(b+g,e+h)&&f++;5<f&&(d+=3+f-5)}for(b=0;b<c-1;b++)for(e=0;e<c-1;e++)if(f=0,a.isDark(b,e)&&f++,a.isDark(b+1,e)&&f++,a.isDark(b,e+1)&&f++,a.isDark(b+1,e+1)&&f++,0==f||4==f)d+=3;for(b=0;b<c;b++)for(e=0;e<c-6;e++)a.isDark(b,e)&&!a.isDark(b,e+1)&&a.isDark(b,e+
|
||||
2)&&a.isDark(b,e+3)&&a.isDark(b,e+4)&&!a.isDark(b,e+5)&&a.isDark(b,e+6)&&(d+=40);for(e=0;e<c;e++)for(b=0;b<c-6;b++)a.isDark(b,e)&&!a.isDark(b+1,e)&&a.isDark(b+2,e)&&a.isDark(b+3,e)&&a.isDark(b+4,e)&&!a.isDark(b+5,e)&&a.isDark(b+6,e)&&(d+=40);for(e=f=0;e<c;e++)for(b=0;b<c;b++)a.isDark(b,e)&&f++;a=Math.abs(100*f/c/c-50)/5;return d+10*a}},l={glog:function(a){if(1>a)throw Error("glog("+a+")");return l.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;256<=a;)a-=255;return l.EXP_TABLE[a]},EXP_TABLE:Array(256),
|
||||
LOG_TABLE:Array(256)},m=0;8>m;m++)l.EXP_TABLE[m]=1<<m;for(m=8;256>m;m++)l.EXP_TABLE[m]=l.EXP_TABLE[m-4]^l.EXP_TABLE[m-5]^l.EXP_TABLE[m-6]^l.EXP_TABLE[m-8];for(m=0;255>m;m++)l.LOG_TABLE[l.EXP_TABLE[m]]=m;q.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var c=Array(this.getLength()+a.getLength()-1),d=0;d<this.getLength();d++)for(var b=0;b<a.getLength();b++)c[d+b]^=l.gexp(l.glog(this.get(d))+l.glog(a.get(b)));return new q(c,0)},mod:function(a){if(0>
|
||||
this.getLength()-a.getLength())return this;for(var c=l.glog(this.get(0))-l.glog(a.get(0)),d=Array(this.getLength()),b=0;b<this.getLength();b++)d[b]=this.get(b);for(b=0;b<a.getLength();b++)d[b]^=l.gexp(l.glog(a.get(b))+c);return(new q(d,0)).mod(a)}};p.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],
|
||||
[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,
|
||||
116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,
|
||||
43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,
|
||||
3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,
|
||||
55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,
|
||||
45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];p.getRSBlocks=function(a,c){var d=p.getRsBlockTable(a,c);if(void 0==d)throw Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+c);for(var b=d.length/3,e=[],f=0;f<b;f++)for(var h=d[3*f+0],g=d[3*f+1],j=d[3*f+2],l=0;l<h;l++)e.push(new p(g,j));return e};p.getRsBlockTable=function(a,c){switch(c){case 1:return p.RS_BLOCK_TABLE[4*(a-1)+0];case 0:return p.RS_BLOCK_TABLE[4*(a-1)+1];case 3:return p.RS_BLOCK_TABLE[4*
|
||||
(a-1)+2];case 2:return p.RS_BLOCK_TABLE[4*(a-1)+3]}};t.prototype={get:function(a){return 1==(this.buffer[Math.floor(a/8)]>>>7-a%8&1)},put:function(a,c){for(var d=0;d<c;d++)this.putBit(1==(a>>>c-d-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var c=Math.floor(this.length/8);this.buffer.length<=c&&this.buffer.push(0);a&&(this.buffer[c]|=128>>>this.length%8);this.length++}};"string"===typeof h&&(h={text:h});h=r.extend({},{render:"canvas",width:256,height:256,typeNumber:-1,
|
||||
correctLevel:2,background:"#ffffff",foreground:"#000000"},h);return this.each(function(){var a;if("canvas"==h.render){a=new o(h.typeNumber,h.correctLevel);a.addData(h.text);a.make();var c=document.createElement("canvas");c.width=h.width;c.height=h.height;for(var d=c.getContext("2d"),b=h.width/a.getModuleCount(),e=h.height/a.getModuleCount(),f=0;f<a.getModuleCount();f++)for(var i=0;i<a.getModuleCount();i++){d.fillStyle=a.isDark(f,i)?h.foreground:h.background;var g=Math.ceil((i+1)*b)-Math.floor(i*b),
|
||||
j=Math.ceil((f+1)*b)-Math.floor(f*b);d.fillRect(Math.round(i*b),Math.round(f*e),g,j)}}else{a=new o(h.typeNumber,h.correctLevel);a.addData(h.text);a.make();c=r("<table></table>").css("width",h.width+"px").css("height",h.height+"px").css("border","0px").css("border-collapse","collapse").css("background-color",h.background);d=h.width/a.getModuleCount();b=h.height/a.getModuleCount();for(e=0;e<a.getModuleCount();e++){f=r("<tr></tr>").css("height",b+"px").appendTo(c);for(i=0;i<a.getModuleCount();i++)r("<td></td>").css("width",
|
||||
d+"px").css("background-color",a.isDark(e,i)?h.foreground:h.background).appendTo(f)}}a=c;jQuery(a).appendTo(this)})}})(jQuery);
|
||||
@@ -0,0 +1,16 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_URI} \.php$
|
||||
RewriteRule .* - [F,L,NC]
|
||||
</IfModule>
|
||||
<IfModule !mod_rewrite.c>
|
||||
<FilesMatch "\.php$">
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order deny,allow
|
||||
Deny from all
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents a modal prompt.
|
||||
*
|
||||
* @var string|\WordfenceLS\Text\Model_HTML $title The title for the prompt. Required.
|
||||
* @var string|\WordfenceLS\Text\Model_HTML $message The message for the prompt. Required.
|
||||
* @var array $primaryButton The parameters for the primary button. The array is in the format array('id' => <element id>, 'label' => <button text>, 'link' => <href value>). Optional.
|
||||
* @var array $secondaryButtons The parameters for any secondary buttons. It is an array of arrays in the format array('id' => <element id>, 'label' => <button text>, 'link' => <href value>). The ordering of entries is the right-to-left order the buttons will be displayed. Optional.
|
||||
*/
|
||||
|
||||
$titleHTML = \WordfenceLS\Text\Model_HTML::esc_html($title);
|
||||
$messageHTML = \WordfenceLS\Text\Model_HTML::esc_html($message);
|
||||
$embedded = isset($embedded) ? $embedded : false;
|
||||
|
||||
if (!isset($secondaryButtons)) {
|
||||
$secondaryButtons = array();
|
||||
}
|
||||
$secondaryButtons = array_reverse($secondaryButtons);
|
||||
?>
|
||||
<div class="wfls-modal">
|
||||
<div class="wfls-modal-header">
|
||||
<div class="wfls-modal-header-content">
|
||||
<div class="wfls-modal-title">
|
||||
<strong><?php echo $titleHTML; ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfls-modal-header-action">
|
||||
<div class="wfls-padding-add-left-small wfls-modal-header-action-close"><a href="#" onclick="WFLS.panelClose(); return false"><i class="<?php echo (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-times-circle' : 'wfls-fa wfls-fa-times-circle'); ?>" aria-hidden="true"></i></a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfls-modal-content">
|
||||
<?php echo $messageHTML; ?>
|
||||
</div>
|
||||
<div class="wfls-modal-footer">
|
||||
<ul class="wfls-flex-horizontal wfls-flex-align-right wfls-full-width">
|
||||
<?php foreach ($secondaryButtons as $button): ?>
|
||||
<li class="wfls-padding-add-left-small"><a href="<?php echo esc_url($button['link']); ?>" class="wfls-btn <?php echo isset($button['type']) ? $button['type'] : 'wfls-btn-default'; ?> wfls-btn-callout-subtle" id="<?php echo esc_attr($button['id']); ?>"><?php echo isset($button['labelHTML']) ? $button['labelHTML'] : esc_html($button['label']); ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
<?php if (isset($primaryButton) && is_array($primaryButton)): ?>
|
||||
<li class="wfls-padding-add-left-small"><a href="<?php echo esc_url($primaryButton['link']); ?>" class="wfls-btn <?php echo isset($primaryButton['type']) ? $primaryButton['type'] : 'wfls-btn-primary'; ?> wfls-btn-callout-subtle" id="<?php echo esc_attr($primaryButton['id']); ?>"><?php echo isset($primaryButton['labelHTML']) ? $primaryButton['labelHTML'] : esc_html($primaryButton['label']); ?></a></li>
|
||||
<?php endif ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
|
||||
if (!isset($defaultGracePeriod))
|
||||
$defaultGracePeriod = \WordfenceLS\Controller_Settings::shared()->get_user_2fa_grace_period();
|
||||
$defaultGracePeriod = max($defaultGracePeriod, 1);
|
||||
$errorMessage = $gracePeriod === null ? __('Unable to Activate Grace Period', 'wordfence-2fa') : __('Unable to Reset Grace Period', 'wordfence-2fa');
|
||||
?>
|
||||
<div class="wfls-add-top wfls-add-bottom wfls-grace-period-container">
|
||||
<div class="wfls-grace-period-input-container">
|
||||
<label for="wfls-user-grace-period-override" style="display: none"><?php esc_html_e('Grace Period Override', 'wordfence-2fa') ?></label>
|
||||
<input type="text" id="wfls-user-grace-period-override" maxlength="2" pattern="[0-9]+" value="<?php echo (int) $defaultGracePeriod ?>">
|
||||
<label for="wfls-user-grace-period-override"><?php esc_html_e('days', 'wordfence-2fa') ?></label>
|
||||
</div>
|
||||
<div class="wfls-grace-period-button-container">
|
||||
<button class="wfls-btn wfls-btn-default" id="wfls-reset-grace-period">
|
||||
<?php echo $gracePeriod === null ? esc_html__('Activate Grace Period', 'wordfence-2fa') : esc_html__('Reset Grace Period', 'wordfence-2fa') ?>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p id="wfls-reset-grace-period-failed" style="display: none"><strong><?php echo esc_html($errorMessage) ?></strong></p>
|
||||
</div>
|
||||
<script type="application/javascript">
|
||||
(function($) {
|
||||
$(function() {
|
||||
var failureMessage = $('#wfls-reset-grace-period-failed');
|
||||
var overrideInput = $('#wfls-user-grace-period-override');
|
||||
var button = $('#wfls-reset-grace-period');
|
||||
function reset2faGracePeriod(userId, gracePeriodOverride, success, failure) {
|
||||
var ajaxContext = (typeof WFLS === 'undefined' ? GWFLS : WFLS);
|
||||
ajaxContext.ajax(
|
||||
'wordfence_ls_reset_2fa_grace_period',
|
||||
{
|
||||
user_id: userId,
|
||||
grace_period_override: gracePeriodOverride
|
||||
},
|
||||
success,
|
||||
failure
|
||||
);
|
||||
}
|
||||
function handleError() {
|
||||
if (typeof WFLS === 'object') {
|
||||
WFLS.panelModal(
|
||||
(WFLS.screenSize(500) ? '300px' : '400px'),
|
||||
<?php echo json_encode($errorMessage) ?>,
|
||||
<?php echo json_encode($gracePeriod === null ? __('An unexpected error occurred while attempting to activate the grace period.', 'wordfence-2fa') : __('An unexpected error occurred while attempting to reset the grace period.', 'wordfence-2fa')) ?>
|
||||
);
|
||||
}
|
||||
else {
|
||||
failureMessage.show();
|
||||
}
|
||||
button.prop('disabled', false);
|
||||
overrideInput.prop('disabled', false);
|
||||
}
|
||||
button.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
button.prop('disabled', true);
|
||||
overrideInput.prop('disabled', true);
|
||||
failureMessage.hide();
|
||||
reset2faGracePeriod(
|
||||
<?php echo json_encode($user->ID, true) ?>,
|
||||
overrideInput.val(),
|
||||
function(data) {
|
||||
if ('error' in data) {
|
||||
handleError();
|
||||
return;
|
||||
}
|
||||
if (typeof WFLS === 'undefined')
|
||||
window.location.href = '#wfls-user-settings';
|
||||
window.location.reload();
|
||||
},
|
||||
handleError
|
||||
);
|
||||
});
|
||||
overrideInput.on('input', function(e) {
|
||||
var value = $(this).val();
|
||||
value = value.replace(/[^0-9]/g, '');
|
||||
value = parseInt(value);
|
||||
if (isNaN(value) || value === 0)
|
||||
value = '';
|
||||
button.prop('disabled', value < 1);
|
||||
$(this).val(value);
|
||||
}).trigger('input');
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
|
||||
$errorMessage = __('Unable to Revoke Grace Period', 'wordfence-2fa');
|
||||
?>
|
||||
<div class="wfls-add-top wfls-add-bottom wfls-grace-period-container">
|
||||
<div class="wfls-grace-period-button-container">
|
||||
<button class="wfls-btn wfls-btn-default" id="wfls-revoke-grace-period">
|
||||
<?php esc_html_e('Revoke Grace Period', 'wordfence-2fa') ?>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p id="wfls-revoke-grace-period-failed" style="display: none"><strong><?php echo esc_html($errorMessage) ?></strong></p>
|
||||
</div>
|
||||
<script type="application/javascript">
|
||||
(function($) {
|
||||
$(function() {
|
||||
var failureMessage = $('#wfls-revoke-grace-period-failed');
|
||||
var button = $('#wfls-revoke-grace-period');
|
||||
function revoke2faGracePeriod(userId, success, failure) {
|
||||
var ajaxContext = (typeof WFLS === 'undefined' ? GWFLS : WFLS);
|
||||
ajaxContext.ajax(
|
||||
'wordfence_ls_revoke_2fa_grace_period',
|
||||
{
|
||||
user_id: userId
|
||||
},
|
||||
success,
|
||||
failure
|
||||
);
|
||||
}
|
||||
function handleError() {
|
||||
if (typeof WFLS === 'object') {
|
||||
WFLS.panelModal(
|
||||
(WFLS.screenSize(500) ? '300px' : '400px'),
|
||||
<?php echo json_encode($errorMessage) ?>,
|
||||
<?php echo json_encode(__('An unexpected error occurred while attempting to revoke the grace period.', 'wordfence-2fa')) ?>
|
||||
);
|
||||
}
|
||||
else {
|
||||
failureMessage.show();
|
||||
}
|
||||
button.prop('disabled', false);
|
||||
}
|
||||
button.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
button.prop('disabled', true);
|
||||
failureMessage.hide();
|
||||
revoke2faGracePeriod(
|
||||
<?php echo json_encode($user->ID, true) ?>,
|
||||
function(data) {
|
||||
if ('error' in data) {
|
||||
handleError();
|
||||
return;
|
||||
}
|
||||
if (typeof WFLS === 'undefined')
|
||||
window.location.href = '#wfls-user-settings';
|
||||
window.location.reload();
|
||||
},
|
||||
handleError
|
||||
);
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* @var string $ip The requesting IP. Required.
|
||||
* @var string $siteName The site name. Required.
|
||||
* @var string $siteURL The site URL. Required.
|
||||
* @var string $verificationURL The verification URL. Required.
|
||||
* @var bool $canEnable2FA Whether or not the user this is being sent to can enable 2FA. Optional
|
||||
*/
|
||||
?>
|
||||
<strong><?php echo wp_kses(sprintf(__('Please verify a login attempt for your account on <a href="%s"><strong>%s</strong></a>.', 'wordfence-2fa'), esc_url($siteURL), $siteName), array('a'=>array('href'=>array()), 'strong'=>array())); ?></strong>
|
||||
<br><br>
|
||||
<?php echo '<strong>' . esc_html__('Request Time:', 'wordfence-2fa') . '</strong> ' . esc_html(\WordfenceLS\Controller_Time::format_local_time('F j, Y h:i:s A')); ?><br>
|
||||
<?php echo '<strong>' . esc_html__('IP:', 'wordfence-2fa') . '</strong> ' . esc_html($ip); ?>
|
||||
<br><br>
|
||||
<?php echo wp_kses(__('The request was flagged as suspicious, and we need verification that you attempted to log in to allow it to proceed. This verification link <b>will be valid for 15 minutes</b> from the time it was sent. If you did not attempt this login, please change your password immediately.', 'wordfence-2fa'), array('b'=>array())); ?>
|
||||
<br><br>
|
||||
<?php if (isset($canEnable2FA) && $canEnable2FA): ?>
|
||||
<?php esc_html_e('You may bypass this verification step permanently by enabling two-factor authentication on your account.', 'wordfence-2fa'); ?>
|
||||
<br><br>
|
||||
<?php endif; ?>
|
||||
<?php echo wp_kses(sprintf(__('<a href="%s"><b>Verify and Log In</b></a>', 'wordfence-2fa'), esc_url($verificationURL)), array('a'=>array('href'=>array()), 'b'=>array())); ?>
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* @var \WordfenceLS\Model_2faInitializationData $initializationData The initialization data for setting up 2FA for a specific user. Required.
|
||||
*/
|
||||
$user = $initializationData->get_user();
|
||||
$recovery = $initializationData->get_recovery_codes();
|
||||
?>
|
||||
<div class="wfls-block wfls-always-active wfls-flex-item-full-width">
|
||||
<div class="wfls-block-header wfls-block-header-border-bottom">
|
||||
<div class="wfls-block-header-content">
|
||||
<div class="wfls-block-title">
|
||||
<strong><?php esc_html_e('2. Enter Code from Authenticator App', 'wordfence-2fa'); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfls-block-content wfls-padding-add-bottom">
|
||||
<p><?php esc_html_e('Download Recovery Codes', 'wordfence-2fa'); ?> <em class="wfls-text-small"><?php esc_html_e('Optional', 'wordfence-2fa'); ?></em></p>
|
||||
<p><?php echo esc_html(sprintf(__('Use one of these %d codes to log in if you lose access to your authenticator device. Codes are %d characters long plus optional spaces. Each one may be used only once.', 'wordfence-2fa'), count($recovery), \WordfenceLS\Model_Crypto::strlen($recovery[0]) * 2)); ?></p>
|
||||
<ul class="wfls-recovery-codes">
|
||||
<?php
|
||||
$recoveryCodeFileContents = sprintf(__('Two-Factor Authentication Recovery Codes - %s (%s)', 'wordfence-2fa'), home_url(), $user->user_login) . "\r\n";
|
||||
$recoveryCodeFileContents .= "\r\n" . sprintf(__('Each line of %d letters and numbers is a single recovery code, with optional spaces for readability. To use a recovery code, after entering your username and password, enter the code like "1234 5678 90AB CDEF" at the 2FA prompt. If your site has a custom login prompt and does not show a 2FA prompt, you can use the single-step method by entering your password and the code together in the Password field, like "mypassword1234 5678 90AB CDEF". Your recovery codes are:', 'wordfence-2fa'), \WordfenceLS\Model_Crypto::strlen($recovery[0]) * 2) . "\r\n\r\n";
|
||||
foreach ($recovery as $c) {
|
||||
$hex = bin2hex($c);
|
||||
$blocks = str_split($hex, 4);
|
||||
echo '<li>' . implode(' ', $blocks) . '</li>';
|
||||
$recoveryCodeFileContents .= implode(' ', $blocks) . "\r\n";
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
<p class="wfls-center"><a href="#" class="wfls-btn wfls-btn-default" id="wfls-recovery-download" target="_blank" rel="noopener noreferrer"><i class="dashicons dashicons-download"></i> <?php esc_html_e('Download', 'wordfence-2fa'); ?></a></p>
|
||||
|
||||
<hr class="wfls-half">
|
||||
|
||||
<p><?php esc_html_e('Enter the code from your authenticator app below to verify and activate two-factor authentication for this account.', 'wordfence-2fa'); ?></p>
|
||||
<p><input type="text" id="wfls-activate-field" value="" size="6" maxlength="6" placeholder="123456" autocomplete="off"></p>
|
||||
</div>
|
||||
<div class="wfls-block-footer">
|
||||
<div class="wfls-block-footer-content">
|
||||
<div class="wfls-block-title" id="wfls-activation-help-link-container">
|
||||
<a href="<?php echo \WordfenceLS\Controller_Support::esc_supportURL(\WordfenceLS\Controller_Support::ITEM_MODULE_LOGIN_SECURITY_2FA); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e('For help on setting up an app, visit our help article.', 'wordfence-2fa'); ?></a>
|
||||
</div>
|
||||
<div class="wfls-block-footer-action"><a href="#" id="wfls-activate" class="wfls-btn wfls-btn-default wfls-disabled"><?php esc_html_e('Activate', 'wordfence-2fa'); ?></a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/javascript">
|
||||
(function($) {
|
||||
$(function() {
|
||||
$('#wfls-activate-field').on('keyup', function(e) {
|
||||
$('#wfls-activate').toggleClass('wfls-disabled', $('#wfls-activate-field').val().length != 6);
|
||||
|
||||
if (e.keyCode == 13) {
|
||||
$('#wfls-activate').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$('#wfls-recovery-download').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
saveAs(new Blob(["<?php echo str_replace("\n", "\\n", str_replace("\r", "\\r", addslashes($recoveryCodeFileContents))); ?>"], {type: "text/plain;charset=" + document.characterSet}), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(preg_replace('~^https?://~i', '', home_url())) . '_' . \WordfenceLS\Text\Model_JavaScript::esc_js($user->user_login) . '_recoverycodes.txt'; ?>');
|
||||
WFLS.savedRecoveryCodes = true;
|
||||
});
|
||||
|
||||
$('#wfls-activate').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (WFLS.userIsActivating) { //Likely a double-click
|
||||
return;
|
||||
}
|
||||
|
||||
WFLS.userIsActivating = true;
|
||||
|
||||
var payload = {
|
||||
secret: '<?php echo bin2hex($initializationData->get_raw_secret()); ?>',
|
||||
recovery: ['<?php echo implode('\', \'', array_map(function($c) { return bin2hex($c); }, $recovery)); ?>'],
|
||||
code: $('#wfls-activate-field').val(),
|
||||
user: <?php echo $user->ID; ?>,
|
||||
};
|
||||
|
||||
WFLS.ajax(
|
||||
'wordfence_ls_activate',
|
||||
payload,
|
||||
function(response) {
|
||||
if (response.error) {
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Error Activating 2FA', 'wordfence-2fa')); ?>', response.error);
|
||||
}
|
||||
else {
|
||||
$('#wfls-activation-controls').crossfade($('#wfls-deactivation-controls'));
|
||||
$('#wfls-recovery-code-count').text(response.text);
|
||||
$('#wfls-activate-field').val('');
|
||||
|
||||
$('.wfls-notice[data-notice-type="wfls-will-be-required"]').find('.wfls-dismiss-link').trigger('click');
|
||||
|
||||
if (!WFLS.savedRecoveryCodes) {
|
||||
var prompt = $('#wfls-tmpl-recovery-skipped-prompt').tmpl({});
|
||||
var promptHTML = $("<div />").append(prompt).html();
|
||||
WFLS.panelHTML((WFLS.screenSize(500) ? '300px' : '400px'), promptHTML, { fixed: true, overlayClose: false, closeButton: false, className: 'wfls-modal', onComplete: function() {
|
||||
$('#wfls-recovery-skipped-download').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
saveAs(new Blob(["<?php echo str_replace("\n", "\\n", str_replace("\r", "\\r", addslashes($recoveryCodeFileContents))); ?>"], {type: "text/plain;charset=" + document.characterSet}), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(preg_replace('~^https?://~i', '', home_url())) . '_' . \WordfenceLS\Text\Model_JavaScript::esc_js($user->user_login) . '_recoverycodes.txt'; ?>');
|
||||
WFLS.panelClose();
|
||||
});
|
||||
$('#wfls-recovery-skipped-skip').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
WFLS.panelClose();
|
||||
});
|
||||
}});
|
||||
}
|
||||
WFLS.savedRecoveryCodes = false;
|
||||
WFLS.userIsActivating = false;
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Error Activating 2FA', 'wordfence-2fa')); ?>', '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('An error was encountered while trying to activate two-factor authentication. Please try again.', 'wordfence-2fa')); ?>');
|
||||
WFLS.userIsActivating = false;
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
<script type="text/x-jquery-template" id="wfls-tmpl-recovery-skipped-prompt">
|
||||
<?php
|
||||
echo \WordfenceLS\Model_View::create('common/modal-prompt', array(
|
||||
'title' => __('Download Recovery Codes', 'wordfence-2fa'),
|
||||
'message' => __('Reminder: If you lose access to your authenticator device, you can use recovery codes to log in. If you have not saved a copy of your recovery codes, we recommend downloading them now.', 'wordfence-2fa'),
|
||||
'primaryButton' => array('id' => 'wfls-recovery-skipped-download', 'label' => __('Download', 'wordfence-2fa'), 'link' => '#'),
|
||||
'secondaryButtons' => array(array('id' => 'wfls-recovery-skipped-skip', 'label' => __('Skip', 'wordfence-2fa'), 'link' => '#')),
|
||||
))->render();
|
||||
?>
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* @var \WordfenceLS\Model_2faInitializationData $initializationData The initialization data for setting up 2FA for a specific user. Required.
|
||||
*/
|
||||
?>
|
||||
<div class="wfls-block wfls-always-active wfls-flex-item-full-width">
|
||||
<div class="wfls-block-header wfls-block-header-border-bottom">
|
||||
<div class="wfls-block-header-content">
|
||||
<div class="wfls-block-title">
|
||||
<strong><?php esc_html_e('1. Scan Code or Enter Key', 'wordfence-2fa'); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfls-block-content wfls-padding-add-bottom">
|
||||
<p>Scan the code below with your authenticator app to add this account. Some authenticator apps also allow you to type in the text version instead.</p>
|
||||
<div id="wfls-qr-code"></div>
|
||||
<p class="wfls-center wfls-no-bottom"><input id="wfls-qr-code-text" class="wfls-center" type="text" value="<?php echo esc_attr($initializationData->get_base32_secret()); ?>" onclick="this.select();" size="32" readonly></p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/javascript">
|
||||
(function($) {
|
||||
$(function() {
|
||||
var narrowPreviously = null;
|
||||
function renderQrCode() {
|
||||
var narrow = WFLS.screenSize(500);
|
||||
if (narrow !== narrowPreviously) {
|
||||
$('#wfls-qr-code').empty().qrcode({text: '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js($initializationData->get_otp_url()); ?>', width: (narrow ? 175 : 256), height: (narrow ? 175 : 256)});
|
||||
$('#wfls-qr-code-text').css('font-family', narrow ? '' : 'monospace');
|
||||
}
|
||||
narrowPreviously = narrow;
|
||||
}
|
||||
$(window).on('resize', renderQrCode);
|
||||
renderQrCode();
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* @var \WP_User $user The user being edited. Required.
|
||||
*/
|
||||
|
||||
$ownAccount = false;
|
||||
$ownUser = wp_get_current_user();
|
||||
if ($ownUser->ID == $user->ID) {
|
||||
$ownAccount = true;
|
||||
}
|
||||
?>
|
||||
<div class="wfls-block wfls-always-active wfls-flex-item-full-width">
|
||||
<div class="wfls-block-header wfls-block-header-border-bottom">
|
||||
<div class="wfls-block-header-content">
|
||||
<div class="wfls-block-title">
|
||||
<strong><?php esc_html_e('Wordfence 2FA Active', 'wordfence-2fa'); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfls-block-content wfls-padding-add-bottom">
|
||||
<p><?php if ($ownAccount) { esc_html_e('Wordfence two-factor authentication is currently active on your account. You may deactivate it by clicking the button below.', 'wordfence-2fa'); } else { echo wp_kses(sprintf(__('Wordfence two-factor authentication is currently active on the account <strong>%s</strong>. You may deactivate it by clicking the button below.', 'wordfence-2fa'), esc_html($user->user_login)), array('strong'=>array())); } ?></p>
|
||||
<p class="wfls-center wfls-add-top"><a href="#" class="wfls-btn wfls-btn-default" id="wfls-deactivate" target="_blank" rel="noopener noreferrer"><?php esc_html_e('Deactivate', 'wordfence-2fa'); ?></a></p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/x-jquery-template" id="wfls-tmpl-deactivate-prompt">
|
||||
<?php
|
||||
echo \WordfenceLS\Model_View::create('common/modal-prompt', array(
|
||||
'title' => __('Deactivate 2FA', 'wordfence-2fa'),
|
||||
'message' => __('Are you sure you want to deactivate two-factor authentication?', 'wordfence-2fa'),
|
||||
'primaryButton' => array('id' => 'wfls-deactivate-prompt-cancel', 'label' => __('Cancel', 'wordfence-2fa'), 'link' => '#'),
|
||||
'secondaryButtons' => array(array('id' => 'wfls-deactivate-prompt-confirm', 'label' => __('Deactivate', 'wordfence-2fa'), 'link' => '#')),
|
||||
))->render();
|
||||
?>
|
||||
</script>
|
||||
<script type="application/javascript">
|
||||
(function($) {
|
||||
$(function() {
|
||||
$('#wfls-deactivate').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var prompt = $('#wfls-tmpl-deactivate-prompt').tmpl({});
|
||||
var promptHTML = $("<div />").append(prompt).html();
|
||||
WFLS.panelHTML((WFLS.screenSize(500) ? '300px' : '400px'), promptHTML, {overlayClose: false, closeButton: false, className: 'wfls-modal', onComplete: function() {
|
||||
$('#wfls-deactivate-prompt-cancel').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
WFLS.panelClose();
|
||||
});
|
||||
|
||||
$('#wfls-deactivate-prompt-confirm').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var payload = {
|
||||
user: <?php echo (int) $user->ID; ?>,
|
||||
};
|
||||
|
||||
WFLS.ajax(
|
||||
'wordfence_ls_deactivate',
|
||||
payload,
|
||||
function(response) {
|
||||
if (response.error) {
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Error Deactivating 2FA', 'wordfence-2fa')); ?>', response.error);
|
||||
}
|
||||
else {
|
||||
$('#wfls-deactivation-controls').crossfade($('#wfls-activation-controls'));
|
||||
}
|
||||
|
||||
WFLS.panelClose(); //The prompt
|
||||
},
|
||||
function(error) {
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Error Deactivating 2FA', 'wordfence-2fa')); ?>', '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('An error was encountered while trying to deactivate two-factor authentication. Please try again.', 'wordfence-2fa')); ?>');
|
||||
WFLS.panelClose(); //The prompt
|
||||
}
|
||||
);
|
||||
});
|
||||
}});
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* @var \WP_User $user The user being edited. Required.
|
||||
* @var bool $inGracePeriod
|
||||
* @var bool $lockedOut
|
||||
* @var int $requiredAt
|
||||
*/
|
||||
|
||||
$ownAccount = false;
|
||||
$ownUser = wp_get_current_user();
|
||||
if ($ownUser->ID == $user->ID) {
|
||||
$ownAccount = true;
|
||||
}
|
||||
$defaultGracePeriod = \WordfenceLS\Controller_Settings::shared()->get_user_2fa_grace_period();
|
||||
$hasGracePeriod = $defaultGracePeriod > 0;
|
||||
?>
|
||||
<div class="wfls-block wfls-always-active wfls-flex-item-full-width">
|
||||
<div class="wfls-block-header wfls-block-header-border-bottom">
|
||||
<div class="wfls-block-header-content">
|
||||
<div class="wfls-block-title">
|
||||
<strong><?php echo $gracePeriod ? esc_html__('Grace Period', 'wordfence-2fa') : esc_html__('Locked Out', 'wordfence-2fa') ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfls-block-content">
|
||||
<?php if ($gracePeriod): ?>
|
||||
<p><?php
|
||||
$requiredDateFormatted = \WordfenceLS\Controller_Time::format_local_time('F j, Y g:i A', $requiredAt);
|
||||
echo $ownAccount ?
|
||||
sprintf(wp_kses(__('Two-factor authentication will be required for your account beginning <strong>%s</strong>', 'wordfence-2fa'), array('strong'=>array())), $requiredDateFormatted) :
|
||||
sprintf(wp_kses(__('Two-factor authentication will be required for user <strong>%s</strong> beginning <strong>%s</strong>.', 'wordfence-2fa'), array('strong'=>array())), esc_html($user->user_login), $requiredDateFormatted)
|
||||
?></p>
|
||||
<?php if (\WordfenceLS\Controller_Users::shared()->has_revokable_grace_period($user)): ?>
|
||||
<?php echo \WordfenceLS\Model_View::create(
|
||||
'common/revoke-grace-period',
|
||||
array(
|
||||
'user' => $user
|
||||
))->render() ?>
|
||||
<?php endif ?>
|
||||
<?php else: ?>
|
||||
<p>
|
||||
<?php echo $ownAccount ?
|
||||
esc_html__('Two-factor authentication is required for your account, but has not been configured.', 'wordfence-2fa') :
|
||||
esc_html__('Two-factor authentication is required for this account, but has not been configured.', 'wordfence-2fa') ?>
|
||||
</p>
|
||||
<?php echo \WordfenceLS\Model_View::create(
|
||||
'common/reset-grace-period',
|
||||
array(
|
||||
'user' => $user,
|
||||
'gracePeriod' => $gracePeriod,
|
||||
'defaultGracePeriod' => $defaultGracePeriod
|
||||
))->render() ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* @var \WP_User $user The user being edited. Required.
|
||||
* @var int $remaining The number of unused recovery codes. Required.
|
||||
*/
|
||||
?>
|
||||
<div class="wfls-block wfls-always-active wfls-flex-item-full-width">
|
||||
<div class="wfls-block-header wfls-block-header-border-bottom">
|
||||
<div class="wfls-block-header-content">
|
||||
<div class="wfls-block-title">
|
||||
<strong><?php esc_html_e('Recovery Codes', 'wordfence-2fa'); ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfls-block-content wfls-padding-add-bottom">
|
||||
<p id="wfls-recovery-code-count"><?php echo esc_html(sprintf($remaining == 1 ? __('%d unused recovery code remains. You may generate a new set by clicking below.', 'wordfence-2fa') : __('%d unused recovery codes remain. You may generate a new set by clicking below.', 'wordfence-2fa'), $remaining)); ?></p>
|
||||
<p class="wfls-center wfls-add-top"><a href="#" class="wfls-btn wfls-btn-default" id="wfls-recovery" target="_blank" rel="noopener noreferrer"><?php esc_html_e('Generate New Codes', 'wordfence-2fa'); ?></a></p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/x-jquery-template" id="wfls-tmpl-recovery-prompt">
|
||||
<?php
|
||||
echo \WordfenceLS\Model_View::create('common/modal-prompt', array(
|
||||
'title' => __('Generate New Recovery Codes', 'wordfence-2fa'),
|
||||
'message' => __('Are you sure you want to generate new recovery codes? Any remaining unused codes will be disabled.', 'wordfence-2fa'),
|
||||
'primaryButton' => array('id' => 'wfls-recovery-prompt-cancel', 'label' => __('Cancel', 'wordfence-2fa'), 'link' => '#'),
|
||||
'secondaryButtons' => array(array('id' => 'wfls-recovery-prompt-confirm', 'label' => __('Generate', 'wordfence-2fa'), 'link' => '#')),
|
||||
))->render();
|
||||
?>
|
||||
</script>
|
||||
<script type="application/javascript">
|
||||
(function($) {
|
||||
$(function() {
|
||||
$('#wfls-recovery').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var prompt = $('#wfls-tmpl-recovery-prompt').tmpl({});
|
||||
var promptHTML = $("<div />").append(prompt).html();
|
||||
WFLS.panelHTML((WFLS.screenSize(500) ? '300px' : '400px'), promptHTML, {overlayClose: false, closeButton: false, className: 'wfls-modal', onComplete: function() {
|
||||
$('#wfls-recovery-prompt-cancel').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
WFLS.panelClose();
|
||||
});
|
||||
|
||||
$('#wfls-recovery-prompt-confirm').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var payload = {
|
||||
user: <?php echo (int) $user->ID; ?>,
|
||||
};
|
||||
|
||||
WFLS.ajax(
|
||||
'wordfence_ls_regenerate',
|
||||
payload,
|
||||
function(response) {
|
||||
if (response.error) {
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), '<?php echo esc_js(__('Error Generating New Codes', 'wordfence-2fa')); ?>', response.error);
|
||||
}
|
||||
else if (response.recovery) {
|
||||
$('#wfls-recovery-code-count').text(response.text);
|
||||
|
||||
var message = '<p><?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(sprintf(__('Use one of these %d codes to log in if you lose access to your authenticator device. Codes are %d characters long plus optional spaces. Each one may be used only once.', 'wordfence-2fa'), \WordfenceLS\Controller_Users::RECOVERY_CODE_COUNT, \WordfenceLS\Controller_Users::RECOVERY_CODE_SIZE * 2)); ?></p><ul class="wfls-recovery-codes">';
|
||||
|
||||
var recoveryCodeFileContents = '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(sprintf(__('Two-Factor Authentication Recovery Codes - %s (%s)', 'wordfence-2fa'), preg_replace('~^https?://~i', '', home_url()), $user->user_login)); ?>' + "\r\n";
|
||||
recoveryCodeFileContents = recoveryCodeFileContents + "\r\n" + '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(sprintf(__('Each line of %d letters and numbers is a single recovery code, with optional spaces for readability. To use a recovery code, after entering your username and password, enter the code like "1234 5678 90AB CDEF" at the 2FA prompt. If your site has a custom login prompt and does not show a 2FA prompt, you can use the single-step method by entering your password and the code together in the Password field, like "mypassword1234 5678 90AB CDEF". Your recovery codes are:', 'wordfence-2fa'), \WordfenceLS\Controller_Users::RECOVERY_CODE_SIZE * 2)); ?>' + "\r\n\r\n";
|
||||
for (var i = 0; i < response.recovery.length; i++) {
|
||||
message = message + '<li>' + response.recovery[i] + '</li>';
|
||||
recoveryCodeFileContents = recoveryCodeFileContents + response.recovery[i] + "\r\n";
|
||||
}
|
||||
|
||||
message = message + "</ul>";
|
||||
|
||||
message = message + "<p class=\"wfls-center\"><a href=\"#\" class=\"wfls-btn wfls-btn-default\" id=\"wfls-recovery-new-download\" target=\"_blank\" rel=\"noopener noreferrer\"><i class=\"dashicons dashicons-download\"></i> <?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Download', 'wordfence-2fa')); ?></a></p>";
|
||||
|
||||
|
||||
WFLS.panelModalHTML((WFLS.screenSize(500) ? '300px' : '400px'), "<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('New Recovery Codes', 'wordfence-2fa')); ?>", message, {onComplete: function() {
|
||||
$('#wfls-recovery-new-download').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
saveAs(new Blob([recoveryCodeFileContents], {type: "text/plain;charset=" + document.characterSet}), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(preg_replace('~^https?://~i', '', home_url()) . '_' . $user->user_login . '_recoverycodes.txt'); ?>');
|
||||
});
|
||||
}});
|
||||
}
|
||||
|
||||
WFLS.panelClose(); //The prompt
|
||||
},
|
||||
function(error) {
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Error Generating New Codes', 'wordfence-2fa')); ?>', '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('An error was encountered while trying to generate new recovery codes. Please try again.', 'wordfence-2fa')); ?>');
|
||||
WFLS.panelClose(); //The prompt
|
||||
}
|
||||
);
|
||||
});
|
||||
}});
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents the fresh install plugin header for standalone installations.
|
||||
*/
|
||||
?>
|
||||
<div id="wfls-onboarding-standalone-modal">
|
||||
<div id="wfls-onboarding-standalone-modal-header">
|
||||
<div id="wfls-onboarding-standalone-modal-header-title"><?php esc_html_e('Wordfence Login Security Installed', 'wordfence-2fa'); ?></div>
|
||||
<div id="wfls-onboarding-standalone-modal-header-accessory"><a href="#" id="wfls-onboarding-standalone-modal-dismiss">×</a></div>
|
||||
</div>
|
||||
<div id="wfls-onboarding-standalone-modal-content">
|
||||
<p><?php esc_html_e('You have just installed the Wordfence Login Security plugin. It contains a subset of the functionality found in the full Wordfence plugin: Two-factor Authentication, XML-RPC Protection and Login Page CAPTCHA.', 'wordfence-2fa'); ?></p>
|
||||
<p><?php printf(__('If you\'re looking for a more comprehensive solution, the <a href="%s" target="_blank" rel="noopener noreferrer">full Wordfence plugin</a> includes all of the features in this plugin as well as a full-featured WordPress firewall, a security scanner, live traffic, and more. The standard installation includes a robust set of free features that can be upgraded via a Premium license key.', 'wordfence-2fa'), 'https://wordpress.org/plugins/wordfence/'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/javascript">
|
||||
(function($) {
|
||||
$(function() {
|
||||
$('#wfls-onboarding-standalone-modal-dismiss').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$('#wfls-onboarding-standalone-modal').slideUp(400, function() {
|
||||
$('#wfls-onboarding-standalone-modal').remove();
|
||||
});
|
||||
|
||||
WFLS.setOptions({'dismissed-fresh-install-modal': true});
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
|
||||
$optionName = \WordfenceLS\Controller_Settings::OPTION_RECAPTCHA_THRESHOLD;
|
||||
$currentValue = \WordfenceLS\Controller_Settings::shared()->get_float($optionName, 0.5);
|
||||
$selectOptions = array(
|
||||
array('label' => __('1.0 (definitely a human)', 'wordfence-2fa'), 'value' => 1.0),
|
||||
array('label' => __('0.9', 'wordfence-2fa'), 'value' => 0.9),
|
||||
array('label' => __('0.8', 'wordfence-2fa'), 'value' => 0.8),
|
||||
array('label' => __('0.7', 'wordfence-2fa'), 'value' => 0.7),
|
||||
array('label' => __('0.6', 'wordfence-2fa'), 'value' => 0.6),
|
||||
array('label' => __('0.5 (probably a human)', 'wordfence-2fa'), 'value' => 0.5),
|
||||
array('label' => __('0.4', 'wordfence-2fa'), 'value' => 0.4),
|
||||
array('label' => __('0.3', 'wordfence-2fa'), 'value' => 0.3),
|
||||
array('label' => __('0.2', 'wordfence-2fa'), 'value' => 0.2),
|
||||
array('label' => __('0.1', 'wordfence-2fa'), 'value' => 0.1),
|
||||
array('label' => __('0.0 (definitely a bot)', 'wordfence-2fa'), 'value' => 0.0),
|
||||
);
|
||||
?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li>
|
||||
<ul id="wfls-option-recaptcha-threshold" class="wfls-option wfls-option-select" data-select-option="<?php echo esc_attr($optionName); ?>" data-original-select-value="<?php echo esc_attr($currentValue); ?>">
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<li class="wfls-option-content">
|
||||
<ul>
|
||||
<li class="wfls-option-title">
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li><span id="wfls-option-recaptcha-threshold-label"><strong><?php esc_html_e('reCAPTCHA human/bot threshold score', 'wordfence-2fa'); ?></strong></span></li>
|
||||
<li class="wfls-option-subtitle"><?php esc_html_e('A reCAPTCHA score equal to or higher than this value will be considered human. Anything lower will be treated as a bot and require additional verification for login and registration.', 'wordfence-2fa'); ?></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="wfls-option-select wfls-padding-add-top-xs-small">
|
||||
<select aria-labelledby="wfls-option-recaptcha-threshold-label">
|
||||
<?php foreach ($selectOptions as $o): ?>
|
||||
<option class="wfls-option-select-option" value="<?php echo esc_attr($o['value']); ?>"<?php if (((int) ($o['value'] * 10)) == ((int) ($currentValue * 10))) { echo ' selected'; } ?>><?php echo esc_html($o['label']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul class="wfls-option">
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<li class="wfls-recaptcha-score-history">
|
||||
<div class="wfls-recaptcha-chart-container">
|
||||
<canvas id="wfls-recaptcha-score-history"></canvas>
|
||||
</div>
|
||||
<div class="wfls-center">
|
||||
<a href="#" id="wfls-reset-recaptcha-score-stats" class="wfls-text-small"><?php esc_html_e('Reset Score Statistics', 'wordfence-2fa'); ?></a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<script type="application/javascript">
|
||||
<?php
|
||||
$stats = \WordfenceLS\Controller_Settings::shared()->get_array(\WordfenceLS\Controller_Settings::OPTION_CAPTCHA_STATS);
|
||||
?>
|
||||
(function($) {
|
||||
$(function() {
|
||||
$('#wfls-reset-recaptcha-score-stats').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
WFLS.ajax('wordfence_ls_reset_recaptcha_stats', {}, function(res) {
|
||||
if (res.success) {
|
||||
window.location.reload(true);
|
||||
}
|
||||
else {
|
||||
if (res.hasOwnProperty('html') && res.html) {
|
||||
WFLS.panelModalHTML((WFLS.screenSize(500) ? '300px' : '400px'), 'Error Resetting reCAPTCHA Statistics', res.error);
|
||||
}
|
||||
else {
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), 'Error Resetting reCAPTCHA Statistics', res.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$(window).on('wfls-tab-change.recaptcha-score-history', function(e, target) {
|
||||
if (target == 'settings') {
|
||||
var barChartData = {
|
||||
labels: ['0.0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1.0'],
|
||||
datasets: [{
|
||||
label: '<?php esc_attr_e('Requests', 'wordfence-2fa'); ?>',
|
||||
backgroundColor: 'rgba(75,192,192,0.4)',
|
||||
borderColor: 'rgba(75,192,192,1.0)',
|
||||
borderWidth: 1,
|
||||
data: <?php echo json_encode($stats['counts']) ?>
|
||||
}]
|
||||
};
|
||||
|
||||
new Chart($('#wfls-recaptcha-score-history'), {
|
||||
type: 'bar',
|
||||
data: barChartData,
|
||||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: '<?php esc_attr_e('reCAPTCHA Score History', 'wordfence-2fa'); ?>'
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: '<?php esc_attr_e('Count', 'wordfence-2fa'); ?>'
|
||||
},
|
||||
ticks: {
|
||||
min: 0,
|
||||
precision: 0,
|
||||
stepSize: <?php echo max(10, pow(10, floor(log10(array_sum($stats['counts']) / 5)))); ?>
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
$(window).off('wfls-tab-change.recaptcha-score-history');
|
||||
}
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
|
||||
$enableOptionName = \WordfenceLS\Controller_Settings::OPTION_ENABLE_AUTH_CAPTCHA;
|
||||
$currentEnableValue = \WordfenceLS\Controller_Settings::shared()->get_bool($enableOptionName);
|
||||
|
||||
$siteKeyOptionName = \WordfenceLS\Controller_Settings::OPTION_RECAPTCHA_SITE_KEY;
|
||||
$siteKeyValue = \WordfenceLS\Controller_Settings::shared()->get($siteKeyOptionName);
|
||||
|
||||
$secretOptionName = \WordfenceLS\Controller_Settings::OPTION_RECAPTCHA_SECRET;
|
||||
$secretValue = \WordfenceLS\Controller_Settings::shared()->get($secretOptionName);
|
||||
?>
|
||||
<ul id="wfls-option-enable-auth-captcha" data-option="<?php echo esc_attr($enableOptionName); ?>" data-enabled-value="1" data-disabled-value="0" data-original-value="<?php echo $currentEnableValue ? '1' : '0'; ?>">
|
||||
<li>
|
||||
<ul class="wfls-option wfls-padding-add-bottom-small">
|
||||
<li id="wfls-enable-auth-captcha" class="wfls-option-checkbox<?php echo ($currentEnableValue ? ' wfls-checked' : ''); ?>" role="checkbox" aria-checked="<?php echo ($currentEnableValue ? 'true' : 'false'); ?>" tabindex="0"><i class="wfls-ion-ios-checkmark-empty" aria-hidden="true" aria-labelledby="wfls-enable-auth-captcha-label"></i></li>
|
||||
<li class="wfls-option-title">
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li>
|
||||
<strong id="wfls-enable-auth-captcha-label"><?php esc_html_e('Enable reCAPTCHA on the login and user registration pages', 'wordfence-2fa'); ?></strong>
|
||||
</li>
|
||||
<li class="wfls-option-subtitle"><?php printf(__('reCAPTCHA v3 does not make users solve puzzles or click a checkbox like previous versions. The only visible part is the reCAPTCHA logo. If a visitor\'s browser fails the CAPTCHA, Wordfence will send an email to the user\'s address with a link they can click to verify that they are a user of your site. You can read further details <a href="%s" target="_blank" rel="noopener noreferrer">in our documentation</a>.', 'wordfence-2fa'), \WordfenceLS\Controller_Support::esc_supportURL(\WordfenceLS\Controller_Support::ITEM_MODULE_LOGIN_SECURITY_CAPTCHA)); ?></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul class="wfls-option wfls-padding-no-top">
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<li>
|
||||
<table>
|
||||
<tr class="wfls-option wfls-option-text" data-original-value="<?php echo esc_attr($siteKeyValue); ?>" data-text-option="<?php echo esc_attr($siteKeyOptionName); ?>">
|
||||
<th id="wfls-enable-captcha-site-key-label" class="wfls-padding-add-bottom"><?php esc_html_e('reCAPTCHA v3 Site Key', 'wordfence-2fa'); ?></th>
|
||||
<td class="wfls-option-text wfls-padding-add-bottom"><input type="text" name="recaptchaSiteKey" id="input-recaptchaSiteKey" class="wfls-form-control" value="<?php echo esc_attr($siteKeyValue); ?>"<?php if (!$currentEnableValue) { echo ' disabled'; } ?>></td>
|
||||
</tr>
|
||||
<tr class="wfls-option wfls-option-text" data-original-value="<?php echo esc_attr($secretValue); ?>" data-text-option="<?php echo esc_attr($secretOptionName); ?>">
|
||||
<th id="wfls-enable-captcha-secret-label"><?php esc_html_e('reCAPTCHA v3 Secret', 'wordfence-2fa'); ?></th>
|
||||
<td class="wfls-option-text"><input type="text" name="recaptchaSecret" id="input-recaptchaSecret" class="wfls-form-control" value="<?php echo esc_attr($secretValue); ?>"<?php if (!$currentEnableValue) { echo ' disabled'; } ?>></td>
|
||||
</tr>
|
||||
</table>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="wfls-option wfls-padding-no-top">
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<li class="wfls-option-title">
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li class="wfls-option-subtitle"><?php echo wp_kses(__('Note: This feature requires a free site key and secret for the <a href="https://www.google.com/recaptcha/about/" target="_blank" rel="noopener noreferrer">Google reCAPTCHA v3 Service</a>. To set up new reCAPTCHA keys, log into your Google account and go to the <a href="https://www.google.com/recaptcha/admin" target="_blank" rel="noopener noreferrer">reCAPTCHA admin page</a>.', 'wordfence-2fa'), array('a'=>array('href'=>array(), 'target'=>array(), 'rel'=>array()))); ?></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<script type="application/javascript">
|
||||
(function($) {
|
||||
$(function() {
|
||||
$('#wfls-enable-auth-captcha').on('keydown', function(e) {
|
||||
if (e.keyCode == 32) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$('#wfls-enable-auth-captcha').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var optionElement = $('#wfls-option-enable-auth-captcha');
|
||||
if (optionElement.hasClass('wfls-disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var option = optionElement.data('option');
|
||||
var value = false;
|
||||
var isActive = $(this).hasClass('wfls-checked');
|
||||
if (isActive) {
|
||||
$(this).removeClass('wfls-checked').attr('aria-checked', 'false');
|
||||
$('#input-recaptchaSiteKey, #input-recaptchaSecret').attr('disabled', true);
|
||||
value = optionElement.data('disabledValue');
|
||||
}
|
||||
else {
|
||||
$(this).addClass('wfls-checked').attr('aria-checked', 'true');
|
||||
$('#input-recaptchaSiteKey, #input-recaptchaSecret').attr('disabled', false);
|
||||
value = optionElement.data('enabledValue');
|
||||
}
|
||||
|
||||
var originalValue = optionElement.data('originalValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
$(optionElement).trigger('change', [false]);
|
||||
WFLS.updatePendingChanges();
|
||||
});
|
||||
|
||||
$('#wfls-enable-auth-captcha-label').on('click', function(e) {
|
||||
var links = $(this).find('a');
|
||||
var buffer = 10;
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var t = $(links[i]).offset().top;
|
||||
var l = $(links[i]).offset().left;
|
||||
var b = t + $(links[i]).height();
|
||||
var r = l + $(links[i]).width();
|
||||
|
||||
if (e.pageX > l - buffer && e.pageX < r + buffer && e.pageY > t - buffer && e.pageY < b + buffer) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$(this).closest('.wfls-option').find('.wfls-option-checkbox').trigger('click');
|
||||
}).css('cursor', 'pointer');
|
||||
|
||||
$('#input-recaptchaSiteKey, #input-recaptchaSecret').on('change paste keyup', function() {
|
||||
var e = this;
|
||||
|
||||
setTimeout(function() {
|
||||
var optionElement = $(e).closest('.wfls-option');
|
||||
var option = optionElement.data('textOption');
|
||||
|
||||
if (typeof option !== 'undefined') {
|
||||
var value = $(e).val();
|
||||
|
||||
var originalValue = $(optionElement).data('originalValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
$(optionElement).trigger('change', [false]);
|
||||
WFLS.updatePendingChanges();
|
||||
}
|
||||
}, 4);
|
||||
});
|
||||
|
||||
$(window).on('wflsOptionsReset', function() {
|
||||
$('#wfls-enable-auth-captcha').each(function() {
|
||||
var enabledValue = $(this).data('enabledValue');
|
||||
var disabledValue = $(this).data('disabledValue');
|
||||
var originalValue = $(this).data('originalValue');
|
||||
if (enabledValue == originalValue) {
|
||||
$(this).find('#wfls-enable-auth-captcha.wfls-option-checkbox').addClass('wfls-checked').attr('aria-checked', 'true');
|
||||
}
|
||||
else {
|
||||
$(this).find('#wfls-enable-auth-captcha.wfls-option-checkbox').removeClass('wfls-checked').attr('aria-checked', 'false');
|
||||
}
|
||||
$(this).trigger('change', [true]);
|
||||
});
|
||||
$('#input-recaptchaSiteKey, #input-recaptchaSecret').each(function() {
|
||||
$(this).val($(this).data('originalValue'));
|
||||
$(this).attr('disabled', !$('#wfls-enable-auth-captcha.wfls-option-checkbox').hasClass('wfls-checked'));
|
||||
});
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents the global option OPTION_IP_SOURCE with a value select menu and text area (hidden by default) for trusted proxies.
|
||||
*/
|
||||
|
||||
$selectOptions = array(
|
||||
array('value' => \WordfenceLS\Model_Request::IP_SOURCE_AUTOMATIC, 'label' => esc_html__('Use the most secure method to get visitor IP addresses. Prevents spoofing and works with most sites.', 'wordfence-2fa') . ' <strong>' . esc_html__('(Recommended)', 'wordfence-2fa') . '</strong>'),
|
||||
array('value' => \WordfenceLS\Model_Request::IP_SOURCE_REMOTE_ADDR, 'label' => esc_html__('Use PHP\'s built in REMOTE_ADDR and don\'t use anything else. Very secure if this is compatible with your site.', 'wordfence-2fa')),
|
||||
array('value' => \WordfenceLS\Model_Request::IP_SOURCE_X_FORWARDED_FOR, 'label' => esc_html__('Use the X-Forwarded-For HTTP header. Only use if you have a front-end proxy or spoofing may result.', 'wordfence-2fa')),
|
||||
array('value' => \WordfenceLS\Model_Request::IP_SOURCE_X_REAL_IP, 'label' => esc_html__('Use the X-Real-IP HTTP header. Only use if you have a front-end proxy or spoofing may result.', 'wordfence-2fa')),
|
||||
);
|
||||
?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-full-width">
|
||||
<li>
|
||||
<ul id="wfls-option-ip-source" class="wfls-option wfls-option-ip-source" data-option="<?php echo esc_attr(\WordfenceLS\Controller_Settings::OPTION_IP_SOURCE); ?>" data-original-value="<?php echo esc_attr(\WordfenceLS\Controller_Settings::shared()->get(\WordfenceLS\Controller_Settings::OPTION_IP_SOURCE)); ?>" data-text-area-option="<?php echo esc_attr(\WordfenceLS\Controller_Settings::OPTION_IP_TRUSTED_PROXIES); ?>" data-original-text-area-value="<?php echo esc_attr(\WordfenceLS\Controller_Settings::shared()->get(\WordfenceLS\Controller_Settings::OPTION_IP_TRUSTED_PROXIES)); ?>">
|
||||
<li class="wfls-option-content wfls-no-right">
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li class="wfls-option-title"><strong><?php esc_html_e('How to get IPs', 'wordfence-2fa'); ?></strong></li>
|
||||
<li>
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li class="wfls-padding-add-left">
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left" role="radiogroup">
|
||||
<?php foreach ($selectOptions as $o): ?>
|
||||
<li class="wfls-padding-add-top-small"><input type="radio" class="wfls-option-radio" name="wfls-ip-source" value="<?php echo esc_attr($o['value']); ?>" id="wfls-ip-source-<?php echo esc_attr(preg_replace('/[^a-z0-9]/i', '-', $o['value'])); ?>"<?php if ($o['value'] == \WordfenceLS\Controller_Settings::shared()->get(\WordfenceLS\Controller_Settings::OPTION_IP_SOURCE)) { echo ' checked'; } ?>><label for="wfls-ip-source-<?php echo esc_attr(preg_replace('/[^a-z0-9]/i', '-', $o['value'])); ?>"> </label><?php echo $o['label']; ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="wfls-option-ip-source-details wfls-padding-add-top">
|
||||
<div class="wfls-left">Detected IP(s): <span id="wfls-ip-source-preview-all"><?php echo \WordfenceLS\Model_Request::current()->detected_ip_preview(); ?></span></div>
|
||||
<div class="wfls-left">Your IP with this setting: <span id="wfls-ip-source-preview-single"><?php echo esc_html(\WordfenceLS\Model_Request::current()->ip()); ?></span></div>
|
||||
<div class="wfls-left"><a href="#" id="wfls-ip-source-trusted-proxies-show">+ Edit trusted proxies</a></div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li id="wfls-ip-source-trusted-proxies">
|
||||
<ul id="wfls-option-ip-source-trusted-proxies" class="wfls-option wfls-option-textarea" data-text-option="<?php echo esc_attr(\WordfenceLS\Controller_Settings::OPTION_IP_TRUSTED_PROXIES); ?>" data-original-text-value="<?php echo esc_attr(\WordfenceLS\Controller_Settings::shared()->get(\WordfenceLS\Controller_Settings::OPTION_IP_TRUSTED_PROXIES)); ?>">
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<li class="wfls-option-content wfls-no-right">
|
||||
<ul>
|
||||
<li class="wfls-option-title">
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li><?php esc_html_e('Trusted Proxies', 'wordfence-2fa'); ?></li>
|
||||
<li class="wfls-option-subtitle"><?php esc_html_e('These IPs (or CIDR ranges) will be ignored when determining the requesting IP via the X-Forwarded-For HTTP header. Enter one IP or CIDR range per line.', 'wordfence-2fa'); ?></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="wfls-option-textarea">
|
||||
<textarea spellcheck="false" autocapitalize="none" autocomplete="off" name="howGetIPs_trusted_proxies"><?php echo esc_html(\WordfenceLS\Controller_Settings::shared()->get(\WordfenceLS\Controller_Settings::OPTION_IP_TRUSTED_PROXIES)); ?></textarea>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<script type="application/javascript">
|
||||
(function($) {
|
||||
$(function() {
|
||||
var updateIPPreview = function() {
|
||||
WFLS.updateIPPreview({ip_source: $('input[name="wfls-ip-source"]:checked').val(), ip_source_trusted_proxies: $('#wfls-ip-source-trusted-proxies textarea').val()}, function(ret) {
|
||||
if (ret && ret.ip) {
|
||||
$('#wfls-ip-source-preview-all').html(ret.preview);
|
||||
$('#wfls-ip-source-preview-single').html(ret.ip);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$('input[name="wfls-ip-source"]').on('change', function() {
|
||||
var optionElement = $(this).closest('.wfls-option.wfls-option-ip-source');
|
||||
var option = optionElement.data('option');
|
||||
var value = $('input[name="wfls-ip-source"]:checked').val();
|
||||
|
||||
var originalValue = optionElement.data('originalValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
WFLS.updatePendingChanges();
|
||||
|
||||
updateIPPreview();
|
||||
});
|
||||
|
||||
var coalescingUpdateTimer;
|
||||
$('#wfls-ip-source-trusted-proxies textarea').on('change paste keyup', function() {
|
||||
var e = this;
|
||||
|
||||
setTimeout(function() {
|
||||
clearTimeout(coalescingUpdateTimer);
|
||||
coalescingUpdateTimer = setTimeout(updateIPPreview, 1000);
|
||||
|
||||
var optionElement = $(e).closest('.wfls-option.wfls-option-textarea');
|
||||
var option = optionElement.data('textOption');
|
||||
var value = $(e).val();
|
||||
|
||||
var originalValue = optionElement.data('originalTextValue');
|
||||
if (originalValue == value) {
|
||||
delete WFLS.pendingChanges[option];
|
||||
}
|
||||
else {
|
||||
WFLS.pendingChanges[option] = value;
|
||||
}
|
||||
|
||||
WFLS.updatePendingChanges();
|
||||
}, 4);
|
||||
});
|
||||
|
||||
$(window).on('wflsOptionsReset', function() {
|
||||
$('input[name="wfls-ip-source"]').each(function() {
|
||||
var optionElement = $(this).closest('.wfls-option.wfls-option-ip-source');
|
||||
var option = optionElement.data('option');
|
||||
var originalValue = optionElement.data('originalValue');
|
||||
|
||||
$(this).prop('checked', originalValue == $(this).attr('value'));
|
||||
});
|
||||
|
||||
$('#wfls-ip-source-trusted-proxies textarea').each(function() {
|
||||
var optionElement = $(this).closest('.wfls-option.wfls-option-textarea');
|
||||
var originalValue = optionElement.data('originalTextAreaValue');
|
||||
$(this).val(originalValue);
|
||||
});
|
||||
|
||||
updateIPPreview();
|
||||
});
|
||||
|
||||
$('#wfls-ip-source-trusted-proxies-show').each(function() {
|
||||
$(this).on('keydown', function(e) {
|
||||
if (e.keyCode == 32) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
$(this).trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
$(this).on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
var isActive = $('#wfls-ip-source-trusted-proxies').hasClass('wfls-active');
|
||||
if (isActive) {
|
||||
$('#wfls-ip-source-trusted-proxies').slideUp({
|
||||
always: function() {
|
||||
$('#wfls-ip-source-trusted-proxies').removeClass('wfls-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
$(this).parent().slideUp();
|
||||
$('#wfls-ip-source-trusted-proxies').slideDown({
|
||||
always: function() {
|
||||
$('#wfls-ip-source-trusted-proxies').addClass('wfls-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents an option-styled text value.
|
||||
*
|
||||
* Expects $title (or $titleHTML) to be defined. $helpLink may also be defined.
|
||||
*
|
||||
* @var string $title The title shown for the option.
|
||||
* @var string $titleHTML The raw HTML title shown for the option. This supersedes $title.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page.
|
||||
*/
|
||||
|
||||
if (!isset($titleHTML)) {
|
||||
$titleHTML = esc_html($title);
|
||||
}
|
||||
?>
|
||||
<ul class="wfls-option wfls-option-label">
|
||||
<?php if (!isset($noSpacer) || !$noSpacer): ?>
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<?php endif; ?>
|
||||
<li class="wfls-option-content">
|
||||
<ul>
|
||||
<li class="wfls-option-title">
|
||||
<?php if (isset($subtitle)): ?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li>
|
||||
<?php endif; ?>
|
||||
<?php echo $titleHTML; ?><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?>
|
||||
<?php if (isset($subtitle)): ?>
|
||||
</li>
|
||||
<li class="wfls-option-subtitle"><?php echo esc_html($subtitle); ?></li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
$maxFailures = (int) \WordfenceLS\Controller_Time::FAILURE_LIMIT;
|
||||
$cronDisabled = \WordfenceLS\Controller_Settings::shared()->is_ntp_cron_disabled($failureCount);
|
||||
$id = 'wfls-option-ntp';
|
||||
?>
|
||||
<ul id="<?php echo esc_attr($id); ?>" class="wfls-option wfls-flex-vertical wfls-flex-align-left">
|
||||
<li class="wfls-option-title"><strong><?php esc_html_e('NTP', 'wordfence-2fa') ?></strong></li>
|
||||
<li class="wfls-option-content">
|
||||
<p><?php esc_html_e('NTP is a protocol that allows for remote time synchronization. Wordfence Login Security uses this protocol to ensure that it has the most accurate time which is necessary for TOTP-based two-factor authentication.', 'wordfence-2fa') ?></p>
|
||||
<?php if (\WordfenceLS\Controller_Settings::shared()->is_ntp_disabled_via_constant()): ?>
|
||||
<p><?php esc_html_e('The constant WORDFENCE_LS_DISABLE_NTP is defined which disables NTP entirely. Remove this constant or set it to a falsy value to enable NTP.', 'wordfence-2fa') ?></p>
|
||||
<?php elseif ($cronDisabled): ?>
|
||||
<?php if ($failureCount > 0): ?>
|
||||
<p><strong><?php echo sprintf(esc_html__('NTP is currently disabled as %d subsequent attempts have failed.', 'wordfence-2fa'), $maxFailures) ?></strong></p>
|
||||
<?php else: ?>
|
||||
<p><?php esc_html_e('NTP was manually disabled.', 'wordfence-2fa') ?></p>
|
||||
<?php endif ?>
|
||||
<button id="wfls-reset-ntp-failure-count" class="wfls-btn wfls-btn-sm wfls-btn-default"><?php esc_html_e('Reset', 'wordfence-2fa') ?></button>
|
||||
<?php else: ?>
|
||||
<p><?php echo wp_kses(__('NTP is currently <strong>enabled</strong>.', 'wordfence-2fa'), array('strong'=>array())); ?></p>
|
||||
<?php if ($failureCount > 0): ?>
|
||||
<?php $remainingAttempts = $maxFailures - $failureCount; ?>
|
||||
<p>
|
||||
<strong><?php esc_html_e('NTP updates are currently failing.', 'wordfence-2fa') ?></strong>
|
||||
<?php echo $remainingAttempts > 0 ? sprintf(esc_html__('NTP will be automatically disabled after %d more attempts.', 'wordfence-2fa'), $remainingAttempts) : esc_html__('NTP will be automatically disabled after 1 more attempt.', 'wordfence-2fa') ?>
|
||||
</p>
|
||||
<?php endif ?>
|
||||
<button id="wfls-disable-ntp" class="wfls-btn wfls-btn-sm wfls-btn-default"><?php esc_html_e('Disable', 'wordfence-2fa') ?></button>
|
||||
<?php endif ?>
|
||||
</li>
|
||||
</ul>
|
||||
<script>
|
||||
(function($) {
|
||||
$(function() {
|
||||
$('#wfls-reset-ntp-failure-count').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
function handleError(message) {
|
||||
WFLS.panelModal(
|
||||
(WFLS.screenSize(500) ? '300px' : '400px'),
|
||||
'<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Error Resetting NTP', 'wordfence-2fa')); ?>',
|
||||
typeof message === 'undefined' ? '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('An error was encountered while trying to reset the NTP state. Please try again.', 'wordfence-2fa')); ?>' : message
|
||||
);
|
||||
}
|
||||
|
||||
WFLS.ajax('wordfence_ls_reset_ntp_failure_count', [],
|
||||
function(response) {
|
||||
if (response.error) {
|
||||
handleError(response.error);
|
||||
}
|
||||
else {
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
handleError();
|
||||
});
|
||||
});
|
||||
$('#wfls-disable-ntp').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
function handleError(message) {
|
||||
WFLS.panelModal(
|
||||
(WFLS.screenSize(500) ? '300px' : '400px'),
|
||||
'<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Error Disabling NTP', 'wordfence-2fa')); ?>',
|
||||
typeof message === 'undefined' ? '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('An error was encountered while trying to disable NTP. Please try again.', 'wordfence-2fa')); ?>' : message
|
||||
);
|
||||
}
|
||||
|
||||
WFLS.ajax('wordfence_ls_disable_ntp', [],
|
||||
function(response) {
|
||||
if (response.error) {
|
||||
handleError(response.error);
|
||||
}
|
||||
else {
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
handleError();
|
||||
});
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
|
||||
use WordfenceLS\Controller_Settings;
|
||||
use WordfenceLS\Text\Model_JavaScript;
|
||||
|
||||
$states = array(
|
||||
Controller_Settings::STATE_2FA_DISABLED => __('Disabled', 'wordfence-2fa'),
|
||||
Controller_Settings::STATE_2FA_OPTIONAL => __('Optional', 'wordfence-2fa'),
|
||||
Controller_Settings::STATE_2FA_REQUIRED => __('Required', 'wordfence-2fa')
|
||||
);
|
||||
|
||||
$gracePeriod = Controller_Settings::shared()->get_int(Controller_Settings::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD, Controller_Settings::DEFAULT_REQUIRE_2FA_USER_GRACE_PERIOD);
|
||||
$woocommerceIntegrationEnabled = Controller_Settings::shared()->get_bool(\WordfenceLS\Controller_Settings::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION);
|
||||
|
||||
$requiredRoles = array();
|
||||
foreach ($options as $option) {
|
||||
if ($option['state'] === Controller_Settings::STATE_2FA_REQUIRED) {
|
||||
$requiredRoles[$option['role']] = $option['title'];
|
||||
}
|
||||
}
|
||||
|
||||
$customerRoleWarning = __('Requiring 2FA for customers is not recommended as some customers may experience difficulties setting up or using two-factor authentication. Instead, using the "Optional" mode for users with the customer role is recommended which will allow customers to enable 2FA, but will not require them to do so.', 'wordfence-2fa');
|
||||
|
||||
?>
|
||||
<ul class="wfls-option wfls-option-2fa-roles">
|
||||
<li class="wfls-option-title">
|
||||
<label><?php esc_html_e('2FA Roles', 'wordfence-2fa') ?></label>
|
||||
</li>
|
||||
<li class="wfls-option-content">
|
||||
<ul>
|
||||
<?php foreach ($options as $option): ?>
|
||||
<?php $selectId = 'wfls-2fa-role-' . $option['name']; ?>
|
||||
<li>
|
||||
<label for="<?php echo esc_attr($selectId) ?>"><?php echo esc_html($option['title']) ?></label>
|
||||
<select id="<?php echo esc_attr($selectId) ?>" name="<?php echo esc_attr($option['name']) ?>" class="wfls-option-select">
|
||||
<?php foreach ($states as $key => $label): ?>
|
||||
<?php if (!$option['allow_disabling'] && $key === Controller_Settings::STATE_2FA_DISABLED) continue; ?>
|
||||
<option
|
||||
value="<?php echo esc_attr($key); ?>"
|
||||
<?php if($option['state'] === $key): ?> selected<?php endif ?>
|
||||
<?php if(!$option['editable']): ?> disabled<?php endif ?>
|
||||
>
|
||||
<?php echo esc_html($label) ?>
|
||||
</option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
<p id="wfls-customer-2fa-required-warning" class="wfls-notice" style="display: none;"><?php echo esc_html($customerRoleWarning) ?></p>
|
||||
<?php if ($hasWoocommerce && !$woocommerceIntegrationEnabled): ?>
|
||||
<p class="wfls-woocommerce-customer-integration-message"><small><?php esc_html_e('In order to use 2FA with the WooCommerce customer role, you must either enable the "WooCommerce integration" option or use the "wordfence_2fa_management" shortcode to provide customers with access to the 2FA management interface. The default interface is only available through WordPress admin pages which are not accessible to users in the customer role.', 'wordfence-2fa') ?></small></p>
|
||||
<?php endif ?>
|
||||
</li>
|
||||
<li class="wfls-2fa-grace-period-container">
|
||||
<label for="wfls-2fa-grace-period" class="wfls-primary-label"><?php esc_html_e('Grace Period', 'wordfence-2fa') ?></label>
|
||||
<input id="wfls-2fa-grace-period" type="text" pattern="[0-9]+" value="<?php echo (int)$gracePeriod; ?>" class="wfls-option-input wfls-option-input-required" name="<?php echo esc_html(Controller_Settings::OPTION_REQUIRE_2FA_USER_GRACE_PERIOD) ?>" maxlength="2">
|
||||
<label for="wfls-2fa-grace-period"><?php esc_html_e('days', 'wordfence-2fa') ?></label>
|
||||
<div id="wfls-grace-period-zero-warning" style="display: none;">
|
||||
<strong><?php esc_html_e('Setting the grace period to 0 will prevent users in roles where 2FA is required, including newly created users, from logging in if they have not already enabled two-factor authentication.', 'wordfence-2fa') ?></strong>
|
||||
<a href="<?php echo esc_attr(\WordfenceLS\Controller_Support::esc_supportURL(\WordfenceLS\Controller_Support::ITEM_MODULE_LOGIN_SECURITY_ROLES)) ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e('Learn More', 'wordfence-2fa') ?></a>
|
||||
</div>
|
||||
<small><?php esc_html_e('For roles that require 2FA, users will have this many days to set up 2FA. Failure to set up 2FA during this period will result in the user losing account access. This grace period will apply to new users from the time of account creation. For existing users, this grace period will apply relative to the time at which the requirement is implemented. This grace period will not automatically apply to admins and must be manually enabled for each admin user.', 'wordfence-2fa') ?></small>
|
||||
</li>
|
||||
<?php if (!empty($requiredRoles)): ?>
|
||||
<li class="wfls-2fa-notification-action">
|
||||
<h4><?php esc_html_e('2FA Notifications', 'wordfence-2fa') ?></h4>
|
||||
<p>
|
||||
<small><?php esc_html_e('Send an email to users with the selected role to notify them of the grace period for enabling 2FA. Select the desired role and optionally specify the URL to be sent in the email to setup 2FA. If left blank, the URL defaults to the standard wordpress login and Wordfence’s Two-Factor Authentication plugin page. For example, if using WooCommerce, input the relative URL of the account page.', 'wordfence-2fa') ?></small>
|
||||
<a href="<?php echo \WordfenceLS\Controller_Support::esc_supportURL(\WordfenceLS\Controller_Support::ITEM_MODULE_LOGIN_SECURITY_2FA_NOTIFICATIONS) ?>" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="<?php echo \WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o'; ?>" aria-hidden="true"></i></a>
|
||||
</p>
|
||||
<div>
|
||||
<label><?php esc_html_e('2FA Role', 'wordfence-2fa') ?></label>
|
||||
<select id="wfls-grace-period-notification-role">
|
||||
<?php foreach ($requiredRoles as $role => $label): ?>
|
||||
<option value="<?php echo esc_attr($role) ?>"><?php echo esc_html($label) ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label><?php esc_html_e('2FA Relative URL (optional)', 'wordfence-2fa') ?></label>
|
||||
<input id="wfls-grace-period-notification-url" type="text" placeholder="ex: /my-account/">
|
||||
</div>
|
||||
<button class="wfls-btn wfls-btn-default wfls-btn-sm" id="wfls-send-grace-period-notification"><?php esc_html_e('Notify', 'wordfence-2fa') ?></button>
|
||||
</li>
|
||||
<?php endif ?>
|
||||
</ul>
|
||||
<script>
|
||||
(function($) {
|
||||
function sendGracePeriodNotification(notifyAll) {
|
||||
var request = {
|
||||
role: $('#wfls-grace-period-notification-role').val(),
|
||||
url: $('#wfls-grace-period-notification-url').val(),
|
||||
};
|
||||
if (typeof notifyAll !== "undefined" && notifyAll)
|
||||
request.notify_all = true;
|
||||
WFLS.ajax('wordfence_ls_send_grace_period_notification', request,
|
||||
function(response) {
|
||||
if (response.error) {
|
||||
var settings = {
|
||||
additional_buttons: []
|
||||
};
|
||||
if (response.limit_exceeded) {
|
||||
settings.additional_buttons.push({
|
||||
label: '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Send Anyway', 'wordfence-2fa')); ?>',
|
||||
id: 'wfls-send-grace-period-notification-over-limit'
|
||||
});
|
||||
}
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Error Sending Notification', 'wordfence-2fa')); ?>', response.error, settings);
|
||||
}
|
||||
else {
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Notification Sent', 'wordfence-2fa')); ?>', response.confirmation);
|
||||
}
|
||||
if (request.notify_all) {
|
||||
WFLS.panelClose();
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
WFLS.panelModal((WFLS.screenSize(500) ? '300px' : '400px'), '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('Error Sending Notification', 'wordfence-2fa')); ?>', '<?php echo \WordfenceLS\Text\Model_JavaScript::esc_js(__('An error was encountered while trying to send the notification. Please try again.', 'wordfence-2fa')); ?>');
|
||||
if (request.notify_all) {
|
||||
WFLS.panelClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
$('#wfls-send-grace-period-notification').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
sendGracePeriodNotification();
|
||||
});
|
||||
$(document).on('click', '#wfls-send-grace-period-notification-over-limit', function() {
|
||||
sendGracePeriodNotification(true);
|
||||
$(this).prop("disabled", true);
|
||||
});
|
||||
$('#wfls-2fa-grace-period').on('input', function(e) {
|
||||
var value = $(this).val();
|
||||
value = value.replace(/[^0-9]/g, '');
|
||||
value = parseInt(value);
|
||||
if (isNaN(value))
|
||||
value = '';
|
||||
if (value === 0) {
|
||||
$("#wfls-grace-period-zero-warning").show();
|
||||
}
|
||||
else {
|
||||
$("#wfls-grace-period-zero-warning").hide();
|
||||
}
|
||||
$(this).val(value);
|
||||
}).trigger('input');
|
||||
var customerRoleInput = $('#wfls-2fa-role-enabled-roles\\.customer');
|
||||
function isCustomerRoleRequired() {
|
||||
return customerRoleInput.val() === 'required';
|
||||
}
|
||||
function toggleCustomerRoleWarning() {
|
||||
$("#wfls-customer-2fa-required-warning").toggle(isCustomerRoleRequired());
|
||||
}
|
||||
toggleCustomerRoleWarning();
|
||||
customerRoleInput.on('change', function(e) {
|
||||
toggleCustomerRoleWarning();
|
||||
if (isCustomerRoleRequired()) {
|
||||
WFLS.displayModalMessage(
|
||||
<?php Model_JavaScript::echo_string_literal(__('Not Recommended', 'wordfence-2fa')) ?>,
|
||||
<?php Model_JavaScript::echo_string_literal($customerRoleWarning) ?>,
|
||||
[
|
||||
{
|
||||
label: <?php Model_JavaScript::echo_string_literal(__('Make Optional', 'wordfence-2fa')) ?>,
|
||||
id: 'wfls-customer-role-warning-revert',
|
||||
type: 'primary'
|
||||
},
|
||||
{
|
||||
label: <?php Model_JavaScript::echo_string_literal(__('Proceed', 'wordfence-2fa')) ?>,
|
||||
id: 'wfls-generic-modal-close',
|
||||
type: 'danger'
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
});
|
||||
$('body').on('click', '#wfls-customer-role-warning-revert', function() {
|
||||
customerRoleInput.val('optional').trigger('change');
|
||||
$('#wfls-generic-modal-close').trigger('click');
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents an option with a popup menu for detailed value selection.
|
||||
*
|
||||
* Expects $selectOptionName, $selectOptions, $selectValue, and $title to be defined. $helpLink may also be defined.
|
||||
*
|
||||
* @var string $selectOptionName The option name for the select portion.
|
||||
* @var array $selectOptions An array of the possible values for $selectOptionName. The array is of the format array(array('value' => <the internal value>, 'label' => <a display label>), ...)
|
||||
* @var string $selectValue The current value of $selectOptionName.
|
||||
* @var string $title The title shown for the option.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page.
|
||||
* @var bool $premium If defined, the option will be tagged as premium only and not allow its value to change for free users.
|
||||
*/
|
||||
|
||||
$id = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $selectOptionName);
|
||||
?>
|
||||
<ul id="<?php echo esc_attr($id); ?>" class="wfls-option wfls-option-select<?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' wfls-option-premium'; } ?>" data-select-option="<?php echo esc_attr($selectOptionName); ?>" data-original-select-value="<?php echo esc_attr($selectValue); ?>">
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<li class="wfls-option-content">
|
||||
<ul>
|
||||
<li class="wfls-option-title"><span id="<?php echo esc_attr($id); ?>-label"><?php echo esc_html($title); ?></span><?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' <a href="https://www.wordfence.com/gnl1optionUpgrade/wordfence-signup/" target="_blank" rel="noopener noreferrer" class="wfls-premium-link">' . esc_html__('Premium Feature', 'wordfence-2fa') . '</a>'; } ?><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?></li>
|
||||
<li class="wfls-option-select wfls-padding-add-top-xs-small">
|
||||
<select<?php echo (!(!wfConfig::p() && isset($premium) && $premium) ? '' : ' disabled'); ?> aria-labelledby="<?php echo esc_attr($id); ?>-label">
|
||||
<?php foreach ($selectOptions as $o): ?>
|
||||
<option class="wfls-option-select-option" value="<?php echo esc_attr($o['value']); ?>"<?php if ($o['value'] == $selectValue) { echo ' selected'; } ?>><?php echo esc_html($o['label']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents a switch option.
|
||||
*
|
||||
* @var string $optionName The option name for the switch. Required.
|
||||
* @var string $value The current value of $optionName. Required.
|
||||
* @var string|\WordfenceLS\Text\Model_HTML $title The title shown for the option. Required.
|
||||
* @var array $states An array of the possible states for the switch. The array matches the format array('value' => <value>, 'label' => <label>) Required.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page. Optional.
|
||||
* @var string $alignment If defined, controls the alignment of the switch control. Optional.
|
||||
*/
|
||||
|
||||
$id = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $optionName);
|
||||
?>
|
||||
<ul id="<?php echo esc_attr($id); ?>" class="wfls-option wfls-option-switch" data-option-name="<?php echo esc_attr($optionName); ?>" data-original-value="<?php echo esc_attr($value); ?>">
|
||||
<?php if (!isset($noSpacer) || !$noSpacer): ?>
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<?php endif; ?>
|
||||
<li class="wfls-option-content wfls-no-right">
|
||||
<ul>
|
||||
<li class="wfls-option-title">
|
||||
<?php if (isset($subtitle)): ?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li>
|
||||
<?php endif; ?>
|
||||
<span id="<?php echo esc_attr($id); ?>-label"><?php echo \WordfenceLS\Text\Model_HTML::esc_html($title); ?></span><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?>
|
||||
<?php if (isset($subtitle)): ?>
|
||||
</li>
|
||||
<li class="wfls-option-subtitle"><?php echo \WordfenceLS\Text\Model_HTML::esc_html($subtitle); ?></li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<li class="wfls-option-switch<?php if (isset($alignment)) { echo ' ' . $alignment; } ?> wfls-padding-add-top-xs-small">
|
||||
<ul class="wfls-switch" role="radiogroup" aria-labelledby="<?php echo esc_attr($id); ?>-label">
|
||||
<?php foreach ($states as $s): ?>
|
||||
<li<?php if ($s['value'] == $value) { echo ' class="wfls-active"'; } ?> data-option-value="<?php echo esc_attr($s['value']); ?>" role="radio" aria-checked="<?php echo ($s['value'] == $value ? 'true' : 'false'); ?>" tabindex="0"><?php echo \WordfenceLS\Text\Model_HTML::esc_html($s['label']); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents a text field option.
|
||||
*
|
||||
* Expects $textOptionName, $textValue, and $title to be defined. $placeholder and $helpLink may also be defined.
|
||||
*
|
||||
* @var string $textOptionName The option name for the text field.
|
||||
* @var string $textValue The current value of $textOptionName.
|
||||
* @var string $title The title shown for the option.
|
||||
* @var string $placeholder If defined, the placeholder for the text field.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page.
|
||||
* @var bool $premium If defined, the option will be tagged as premium only and not allow its value to change for free users.
|
||||
*/
|
||||
|
||||
if (!isset($placeholder)) {
|
||||
$placeholder = '';
|
||||
}
|
||||
$id = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $textOptionName);
|
||||
?>
|
||||
<ul id="<?php echo esc_attr($id); ?>" class="wfls-option wfls-option-text<?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' wfls-option-premium'; } ?>" data-text-option="<?php echo esc_attr($textOptionName); ?>" data-original-text-value="<?php echo esc_attr($textValue); ?>">
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<li class="wfls-option-content">
|
||||
<ul>
|
||||
<li class="wfls-option-title">
|
||||
<?php if (isset($subtitle)): ?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li>
|
||||
<?php endif; ?>
|
||||
<span id="<?php echo esc_attr($id); ?>-label"><?php echo esc_html($title); ?></span><?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' <a href="https://www.wordfence.com/gnl1optionUpgrade/wordfence-signup/" target="_blank" rel="noopener noreferrer" class="wfls-premium-link">' . esc_html__('Premium Feature', 'wordfence-2fa') . '</a>'; } ?><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?>
|
||||
<?php if (isset($subtitle)): ?>
|
||||
</li>
|
||||
<li class="wfls-option-subtitle"><?php echo esc_html($subtitle); ?></li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<li class="wfls-option-text">
|
||||
<input type="text" value="<?php echo esc_attr($textValue); ?>" placeholder="<?php echo esc_attr($placeholder); ?>"<?php echo (!(!wfConfig::p() && isset($premium) && $premium) ? '' : ' disabled'); ?> aria-labelledby="<?php echo esc_attr($id); ?>-label">
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents a text area option.
|
||||
*
|
||||
* Expects $textOptionName, $textValue, and $title to be defined. $helpLink, $premium, and $noSpacer may also be defined.
|
||||
*
|
||||
* @var string $textOptionName The option name for the text field. Required.
|
||||
* @var string $textValue The current value of $textOptionName. Required.
|
||||
* @var string|\WordfenceLS\Text\Model_HTML $title The title shown for the option. Required.
|
||||
* @var string|\WordfenceLS\Text\Model_HTML $subtitle The title shown for the option. Optional.
|
||||
* @var string $subtitlePosition The position for the subtitle: 'value' for below the value, 'title' for below the title. Optional.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page. Optional.
|
||||
* @var bool $noSpacer If defined and truthy, the spacer will be omitted. Optional.
|
||||
*/
|
||||
|
||||
if (!isset($subtitlePosition)) { //May be 'title' to appear below the title or 'value' to appear below the field
|
||||
$subtitlePosition = 'title';
|
||||
}
|
||||
|
||||
$id = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $textOptionName);
|
||||
?>
|
||||
<ul id="<?php echo esc_attr($id); ?>" class="wfls-option wfls-option-textarea" data-text-option="<?php echo esc_attr($textOptionName); ?>" data-original-text-value="<?php echo esc_attr($textValue); ?>">
|
||||
<?php if (!isset($noSpacer) || !$noSpacer): ?>
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<?php endif; ?>
|
||||
<li class="wfls-option-content wfls-no-right">
|
||||
<ul>
|
||||
<li class="wfls-option-title<?php if (isset($alignTitle)) { echo $alignTitle == 'top' ? ' wfls-option-title-top' : ($alignTitle == 'bottom' ? 'wfls-option-title-bottom' : ''); } ?>">
|
||||
<?php if (isset($subtitleHTML) && $subtitlePosition == 'title'): ?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li>
|
||||
<?php endif; ?>
|
||||
<span id="<?php echo esc_attr($id); ?>-label"><?php echo \WordfenceLS\Text\Model_HTML::esc_html($title); ?></span><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?>
|
||||
<?php if (isset($subtitle) && $subtitlePosition == 'title'): ?>
|
||||
</li>
|
||||
<li class="wfls-option-subtitle"><?php echo \WordfenceLS\Text\Model_HTML::esc_html($subtitle); ?></li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<li class="wfls-option-textarea">
|
||||
<?php if (isset($subtitle) && $subtitlePosition == 'value'): ?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left wfls-flex-full-width">
|
||||
<li>
|
||||
<?php endif; ?>
|
||||
<textarea aria-labelledby="<?php echo esc_attr($id); ?>-label"><?php echo esc_html($textValue); ?></textarea>
|
||||
<?php if (isset($subtitle) && $subtitlePosition == 'value'): ?>
|
||||
</li>
|
||||
<li class="wfls-option-subtitle"><?php echo \WordfenceLS\Text\Model_HTML::esc_html($subtitle); ?></li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents a boolean option with a switch toggle control.
|
||||
*
|
||||
* Expects $optionName, $enabledValue, $disabledValue, $value, and $title to be defined. $helpLink may also be defined.
|
||||
*
|
||||
* @var string $optionName The option name.
|
||||
* @var string $enabledValue The value to save in $option if the toggle is enabled.
|
||||
* @var string $disabledValue The value to save in $option if the toggle is disabled.
|
||||
* @var string $value The current value of $optionName.
|
||||
* @var string $title The title shown for the option.
|
||||
* @var string $htmlTitle The unescaped title shown for the option.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page.
|
||||
* @var bool $premium If defined, the option will be tagged as premium only and not allow its value to change for free users.
|
||||
* @var bool $disabled If defined and truthy, the option will start out disabled.
|
||||
*/
|
||||
|
||||
if (isset($subtitle) && !isset($subtitleHTML)) {
|
||||
$subtitleHTML = esc_html($subtitle);
|
||||
}
|
||||
|
||||
$id = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $optionName);
|
||||
?>
|
||||
<ul id="<?php echo esc_attr($id); ?>" class="wfls-option wfls-option-toggled-boolean-switch<?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' wfls-option-premium'; } ?><?php if (isset($disabled) && $disabled) { echo ' wfls-disabled'; } ?>" data-option="<?php echo esc_attr($optionName); ?>" data-enabled-value="<?php echo esc_attr($enabledValue); ?>" data-disabled-value="<?php echo esc_attr($disabledValue); ?>" data-original-value="<?php echo esc_attr($value == $enabledValue ? $enabledValue : $disabledValue); ?>">
|
||||
<li class="wfls-boolean-switch<?php echo ($value == $enabledValue ? ' wfls-active' : ''); ?>" role="checkbox" aria-checked="<?php echo ($value == $enabledValue ? 'true' : 'false'); ?>" tabindex="0" aria-labelledby="<?php echo esc_attr($id); ?>-label"><a href="#" class="wfls-boolean-switch-handle"></a></li>
|
||||
<li class="wfls-option-title">
|
||||
<?php if (isset($subtitleHTML)): ?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li>
|
||||
<?php endif; ?>
|
||||
<span id="<?php echo esc_attr($id); ?>-label"><?php echo (!empty($title)) ? esc_html($title) : ''; echo (!empty($htmlTitle)) ? wp_kses($htmlTitle, 'post') : ''; ?></span><?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' <a href="https://www.wordfence.com/gnl1optionUpgrade/wordfence-signup/" target="_blank" rel="noopener noreferrer" class="wfls-premium-link">' . esc_html__('Premium Feature', 'wordfence-2fa') . '</a>'; } ?><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?>
|
||||
<?php if (isset($subtitleHTML)): ?>
|
||||
</li>
|
||||
<li class="wfls-option-subtitle"><?php echo $subtitleHTML; ?></li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents multiple boolean options under a single heading with a checkbox toggle control for each.
|
||||
*
|
||||
* @var array $options The options shown. The structure is defined below. Required.
|
||||
* @var string|\WordfenceLS\Text\Model_HTML $title The overall title shown for the options. Required.
|
||||
* @var string $helpLink The link to the corresponding external help page. Optional.
|
||||
* @var bool $wrap Whether or not the options should be allowed to wrap. Optional, defaults to false.
|
||||
*
|
||||
* $options is an array of
|
||||
* array(
|
||||
* 'name' => string <option name>,
|
||||
* 'enabledValue' => string <value saved if the toggle is enabled>,
|
||||
* 'disabledValue' => string <value saved if the toggle is disabled>,
|
||||
* 'value' => string <current value of the option>,
|
||||
* 'title' => string|\Wordfence2FA\Text\Model_HTML <title displayed to label the checkbox>,
|
||||
* 'editable' => bool Whether or not the option can be edited, defaults to true.
|
||||
* )
|
||||
*/
|
||||
?>
|
||||
<ul class="wfls-option wfls-option-toggled-multiple">
|
||||
<li class="wfls-option-title"><?php echo \WordfenceLS\Text\Model_HTML::esc_html($title); ?><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?></li>
|
||||
<li class="wfls-option-checkboxes<?php if (isset($wrap) && $wrap) { echo ' wfls-option-checkboxes-wrap'; } ?>">
|
||||
<?php
|
||||
foreach ($options as $o):
|
||||
$id = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $o['name']);
|
||||
?>
|
||||
<ul id="<?php echo esc_attr($id); ?>" data-option="<?php echo esc_attr($o['name']); ?>" data-enabled-value="<?php echo esc_attr($o['enabledValue']); ?>" data-disabled-value="<?php echo esc_attr($o['disabledValue']); ?>" data-original-value="<?php echo esc_attr($o['value'] == $o['enabledValue'] ? $o['enabledValue'] : $o['disabledValue']); ?>">
|
||||
<li class="wfls-option-checkbox<?php echo ($o['value'] == $o['enabledValue'] ? ' wfls-checked' : ''); ?><?php echo (isset($o['editable']) && !$o['editable'] ? ' wfls-disabled' : ''); ?>" role="checkbox" aria-checked="<?php echo ($o['value'] == $o['enabledValue'] ? 'true' : 'false'); ?>" tabindex="0" aria-labelledby="<?php echo esc_attr($id); ?>-label"><i class="wfls-ion-ios-checkmark-empty" aria-hidden="true"></i></li>
|
||||
<li id="<?php echo esc_attr($id); ?>-label" class="wfls-option-title"><?php echo esc_html($o['title']); ?></li>
|
||||
</ul>
|
||||
<?php endforeach; ?>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents a boolean option with a checkbox toggle control.
|
||||
*
|
||||
* Expects $optionName, $enabledValue, $disabledValue, $value, and $title to be defined. $helpLink may also be defined.
|
||||
*
|
||||
* @var string $optionName The option name.
|
||||
* @var string $enabledValue The value to save in $option if the toggle is enabled.
|
||||
* @var string $disabledValue The value to save in $option if the toggle is disabled.
|
||||
* @var string $value The current value of $optionName.
|
||||
* @var string $title The title shown for the option.
|
||||
* @var string $htmlTitle The unescaped title shown for the option.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page.
|
||||
* @var bool $premium If defined, the option will be tagged as premium only and not allow its value to change for free users.
|
||||
*/
|
||||
|
||||
$id = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $optionName);
|
||||
?>
|
||||
<ul id="<?php echo esc_attr($id); ?>" class="wfls-option wfls-option-toggled-segmented<?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' wfls-option-premium'; } ?>" data-option="<?php echo esc_attr($optionName); ?>" data-enabled-value="<?php echo esc_attr($enabledValue); ?>" data-disabled-value="<?php echo esc_attr($disabledValue); ?>" data-original-value="<?php echo esc_attr($value == $enabledValue ? $enabledValue : $disabledValue); ?>">
|
||||
<li class="wfls-option-title"><?php echo (!empty($title)) ? esc_html($title) : ''; echo (!empty($htmlTitle)) ? wp_kses($htmlTitle, 'post') : ''; ?><?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' <a href="https://www.wordfence.com/gnl1optionUpgrade/wordfence-signup/" target="_blank" rel="noopener noreferrer" class="wfls-premium-link">' . esc_html__('Premium Feature', 'wordfence-2fa') . '</a>'; } ?><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?></li>
|
||||
<li class="wfls-option-segments">
|
||||
<?php
|
||||
$onId = sanitize_key('wfls-segment-' . $optionName . '-on');
|
||||
$offId = sanitize_key('wfls-segment-' . $optionName . '-off');
|
||||
?>
|
||||
<input id="<?php echo esc_attr($onId) ?>" type="radio" name="<?php echo esc_attr($optionName) ?>" value="<?php echo esc_attr($enabledValue) ?>"<?php echo ($value == $enabledValue ? ' checked' : ''); ?><?php echo (!(!wfConfig::p() && isset($premium) && $premium) ? '' : ' disabled'); ?>>
|
||||
<label class="wfls-segment-first" for="<?php echo esc_attr($onId) ?>">On</label>
|
||||
|
||||
<input id="<?php echo esc_attr($offId) ?>" type="radio" name="<?php echo esc_attr($optionName) ?>" value="<?php echo esc_attr($disabledValue) ?>"<?php echo ($value == $disabledValue ? ' checked' : ''); ?><?php echo (!(!wfConfig::p() && isset($premium) && $premium) ? '' : ' disabled'); ?>>
|
||||
<label class="wfls-segment-last" for="<?php echo esc_attr($offId) ?>">Off</label>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents an option with a boolean on/off toggle checkbox and popup menu for detailed value selection.
|
||||
*
|
||||
* Expects $toggleOptionName, $enabledToggleValue, $disabledToggleValue, $toggleValue, $selectOptionName, $selectOptions, $selectValue, and $title to be defined. $helpLink may also be defined.
|
||||
*
|
||||
* @var string $toggleOptionName The option name for the toggle portion.
|
||||
* @var string $enabledToggleValue The value to save in $toggleOption if the toggle is enabled.
|
||||
* @var string $disabledToggleValue The value to save in $toggleOption if the toggle is disabled.
|
||||
* @var string $toggleValue The current value of $toggleOptionName.
|
||||
* @var string $selectOptionName The option name for the select portion.
|
||||
* @var array $selectOptions An array of the possible values for $selectOptionName. The array is of the format array(array('value' => <the internal value>, 'label' => <a display label>), ...)
|
||||
* @var string $selectValue The current value of $selectOptionName.
|
||||
* @var string $title The title shown for the option.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page.
|
||||
* @var bool $premium If defined, the option will be tagged as premium only and not allow its value to change for free users.
|
||||
*/
|
||||
|
||||
$toggleID = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $toggleOptionName);
|
||||
$selectID = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $selectOptionName);
|
||||
?>
|
||||
<ul class="wfls-option wfls-option-toggled-select<?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' wfls-option-premium'; } ?>" data-toggle-option="<?php echo esc_attr($toggleOptionName); ?>" data-enabled-toggle-value="<?php echo esc_attr($enabledToggleValue); ?>" data-disabled-toggle-value="<?php echo esc_attr($disabledToggleValue); ?>" data-original-toggle-value="<?php echo esc_attr($toggleValue == $enabledToggleValue ? $enabledToggleValue : $disabledToggleValue); ?>" data-select-option="<?php echo esc_attr($selectOptionName); ?>" data-original-select-value="<?php echo esc_attr($selectValue); ?>">
|
||||
<li id="<?php echo esc_attr($toggleID); ?>" class="wfls-option-checkbox<?php echo ($toggleValue == $enabledToggleValue ? ' wfls-checked' : ''); ?>" role="checkbox" aria-checked="<?php echo ($toggleValue == $enabledToggleValue ? 'true' : 'false'); ?>" tabindex="0"><i class="wfls-ion-ios-checkmark-empty" aria-hidden="true"></i></li>
|
||||
<li class="wfls-option-content">
|
||||
<ul id="<?php echo esc_attr($selectID); ?>">
|
||||
<li class="wfls-option-title"><span id="<?php echo esc_attr($selectID); ?>-label"><?php echo esc_html($title); ?></span><?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' <a href="https://www.wordfence.com/gnl1optionUpgrade/wordfence-signup/" target="_blank" rel="noopener noreferrer" class="wfls-premium-link">' . esc_html__('Premium Feature', 'wordfence-2fa') . '</a>'; } ?><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?></li>
|
||||
<li class="wfls-option-select wfls-padding-add-top-xs-small">
|
||||
<select<?php echo ($toggleValue == $enabledToggleValue && !(!wfConfig::p() && isset($premium) && $premium) ? '' : ' disabled'); ?> aria-labelledby="<?php echo esc_attr($selectID); ?>-label">
|
||||
<?php foreach ($selectOptions as $o): ?>
|
||||
<option class="wfls-option-select-option" value="<?php echo esc_attr($o['value']); ?>"<?php if ($o['value'] == $selectValue) { echo ' selected'; } ?>><?php echo esc_html($o['label']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents a boolean option with a checkbox toggle control and a sub-option toggle.
|
||||
*
|
||||
* Expects $optionName, $enabledValue, $disabledValue, $value, and $title to be defined for the primary option. $helpLink may also be defined.
|
||||
* Expects $subOptionName, $subEnabledValue, $subDisabledValue, $subValue, and $subTitle to be defined for the sub-option. $subHelpLink may also be defined.
|
||||
*
|
||||
* @var string $optionName The option name.
|
||||
* @var string $enabledValue The value to save in $optionName if the toggle is enabled.
|
||||
* @var string $disabledValue The value to save in $optionName if the toggle is disabled.
|
||||
* @var string $value The current value of $optionName.
|
||||
* @var string $title The title shown for the option.
|
||||
* @var string $htmlTitle The unescaped title shown for the option.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page.
|
||||
* @var bool $premium If defined, the option will be tagged as premium only and not allow its value to change for free users.
|
||||
*
|
||||
* @var string $subOptionName The sub-option name.
|
||||
* @var string $subEnabledValue The value to save in $subOptionName if the toggle is enabled.
|
||||
* @var string $subDisabledValue The value to save in $subOptionName if the toggle is disabled.
|
||||
* @var string $subValue The current value of $subOptionName.
|
||||
* @var string $subTitle The title shown for the sub-option.
|
||||
* @var string $subHtmlTitle The unescaped title shown for the sub-option.
|
||||
* @var string $subHelpLink If defined, the link to the corresponding external help page for the sub-option.
|
||||
* @var bool $subPremium If defined, the sub-option will be tagged as premium only and not allow its value to change for free users.
|
||||
*/
|
||||
|
||||
if (isset($title) && !isset($htmlTitle)) {
|
||||
$htmlTitle = esc_html($title);
|
||||
}
|
||||
|
||||
if (isset($subTitle) && !isset($subHtmlTitle)) {
|
||||
$subHtmlTitle = esc_html($subTitle);
|
||||
}
|
||||
|
||||
$id = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $optionName);
|
||||
$subID = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $subOptionName);
|
||||
?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-full-width">
|
||||
<li>
|
||||
<ul id="<?php echo esc_attr($id); ?>" class="wfls-option wfls-option-toggled<?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' wfls-option-premium'; } ?>" data-option="<?php echo esc_attr($optionName); ?>" data-enabled-value="<?php echo esc_attr($enabledValue); ?>" data-disabled-value="<?php echo esc_attr($disabledValue); ?>" data-original-value="<?php echo esc_attr($value == $enabledValue ? $enabledValue : $disabledValue); ?>">
|
||||
<li class="wfls-option-checkbox<?php echo ($value == $enabledValue ? ' wfls-checked' : ''); ?>" role="checkbox" aria-checked="<?php echo ($value == $enabledValue ? 'true' : 'false'); ?>" tabindex="0" aria-labelledby="<?php echo esc_attr($id); ?>-label"><i class="wfls-ion-ios-checkmark-empty" aria-hidden="true"></i></li>
|
||||
<li class="wfls-option-title">
|
||||
<span id="<?php echo esc_attr($id); ?>-label"><?php echo $htmlTitle; ?></span><?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' <a href="https://www.wordfence.com/gnl1optionUpgrade/wordfence-signup/" target="_blank" rel="noopener noreferrer" class="wfls-premium-link">' . esc_html__('Premium Feature', 'wordfence-2fa') . '</a>'; } ?><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="wfls-option-sub">
|
||||
<ul id="<?php echo esc_attr($subID); ?>" class="wfls-option wfls-option-toggled<?php if (!wfConfig::p() && isset($subPremium) && $subPremium) { echo ' wfls-option-premium'; } ?>" data-option="<?php echo esc_attr($subOptionName); ?>" data-enabled-value="<?php echo esc_attr($subEnabledValue); ?>" data-disabled-value="<?php echo esc_attr($subDisabledValue); ?>" data-original-value="<?php echo esc_attr($subValue == $subEnabledValue ? $subEnabledValue : $subDisabledValue); ?>">
|
||||
<li class="wfls-option-checkbox<?php echo ($subValue == $subEnabledValue ? ' wfls-checked' : ''); ?>" role="checkbox" aria-checked="<?php echo ($subValue == $subEnabledValue ? 'true' : 'false'); ?>" tabindex="0" aria-labelledby="<?php echo esc_attr($subID); ?>-label"><i class="wfls-ion-ios-checkmark-empty" aria-hidden="true"></i></li>
|
||||
<li class="wfls-option-title">
|
||||
<span id="<?php echo esc_attr($subID); ?>-label"><?php echo $subHtmlTitle; ?></span><?php if (!wfConfig::p() && isset($subPremium) && $subPremium) { echo ' <a href="https://www.wordfence.com/gnl1optionUpgrade/wordfence-signup/" target="_blank" rel="noopener noreferrer" class="wfls-premium-link">' . esc_html__('Premium Feature', 'wordfence-2fa') . '</a>'; } ?><?php if (isset($subHelpLink)) { echo ' <a href="' . esc_attr($subHelpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents an option with a boolean on/off toggle checkbox and text area for detailed value entry.
|
||||
*
|
||||
* Expects $toggleOptionName, $enabledToggleValue, $disabledToggleValue, $toggleValue, $textAreaOptionName, $textAreaValue, and $title to be defined. $helpLink may also be defined.
|
||||
*
|
||||
* @var string $toggleOptionName The option name for the toggle portion.
|
||||
* @var string $enabledToggleValue The value to save in $toggleOption if the toggle is enabled.
|
||||
* @var string $disabledToggleValue The value to save in $toggleOption if the toggle is disabled.
|
||||
* @var string $toggleValue The current value of $toggleOptionName.
|
||||
* @var string $textAreaOptionName The option name for the text area portion.
|
||||
* @var string $textAreaValue The current value of $textAreaOptionName.
|
||||
* @var string $title The title shown for the option.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page.
|
||||
* @var bool $premium If defined, the option will be tagged as premium only and not allow its value to change for free users.
|
||||
*/
|
||||
|
||||
$toggleID = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $toggleOptionName);
|
||||
$textAreaID = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $textAreaOptionName);
|
||||
?>
|
||||
<ul class="wfls-option wfls-option-toggled-textarea<?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' wfls-option-premium'; } ?>" data-toggle-option="<?php echo esc_attr($toggleOptionName); ?>" data-enabled-toggle-value="<?php echo esc_attr($enabledToggleValue); ?>" data-disabled-toggle-value="<?php echo esc_attr($disabledToggleValue); ?>" data-original-toggle-value="<?php echo esc_attr($toggleValue == $enabledToggleValue ? $enabledToggleValue : $disabledToggleValue); ?>" data-text-area-option="<?php echo esc_attr($textAreaOptionName); ?>" data-original-text-area-value="<?php echo esc_attr($textAreaValue); ?>">
|
||||
<li id="<?php echo esc_attr($toggleID); ?>" class="wfls-option-checkbox<?php echo ($toggleValue == $enabledToggleValue ? ' wfls-checked' : ''); ?>" role="checkbox" aria-checked="<?php echo ($toggleValue == $enabledToggleValue ? 'true' : 'false'); ?>" tabindex="0"><i class="wfls-ion-ios-checkmark-empty" aria-hidden="true" aria-labelledby="<?php echo esc_attr($toggleID); ?>-label"></i></li>
|
||||
<li class="wfls-option-title"><span id="<?php echo esc_attr($toggleID); ?>-label"><?php echo esc_html($title); ?></span><?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' <a href="https://www.wordfence.com/gnl1optionUpgrade/wordfence-signup/" target="_blank" rel="noopener noreferrer" class="wfls-premium-link">' . esc_html__('Premium Feature', 'wordfence-2fa') . '</a>'; } ?><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?></li>
|
||||
<li id="<?php echo esc_attr($textAreaID); ?>" class="wfls-option-textarea">
|
||||
<select<?php echo ($toggleValue == $enabledToggleValue && !(!wfConfig::p() && isset($premium) && $premium) ? '' : ' disabled'); ?> aria-labelledby="<?php echo esc_attr($toggleID); ?>-label">
|
||||
<textarea<?php echo (!(!wfConfig::p() && isset($premium) && $premium) ? '' : ' disabled'); ?>><?php echo esc_html($textAreaValue); ?></textarea>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents a boolean option with a checkbox toggle control.
|
||||
*
|
||||
* @var string $optionName The option name. Required.
|
||||
* @var string $enabledValue The value to save in $option if the toggle is enabled. Required.
|
||||
* @var string $disabledValue The value to save in $option if the toggle is disabled. Required.
|
||||
* @var string $value The current value of $optionName. Required.
|
||||
* @var string|\WordfenceLS\Text\Model_HTML $title The title shown for the option. Required.
|
||||
* @var string|\WordfenceLS\Text\Model_HTML $subtitle The title shown for the option. Optional.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page. Optional.
|
||||
* @var bool $disabled If defined and truthy, the option will start out disabled. Optional.
|
||||
* @var bool $child If true, this option will be rendered ar a child option. Optional.
|
||||
*/
|
||||
|
||||
$id = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $optionName);
|
||||
?>
|
||||
<ul id="<?php echo esc_attr($id); ?>" class="wfls-option wfls-option-toggled<?php if (isset($disabled) && $disabled) { echo ' wfls-disabled'; } if (isset($child) && $child) { echo ' wfls-child-option'; }?>" data-option="<?php echo esc_attr($optionName); ?>" data-enabled-value="<?php echo esc_attr($enabledValue); ?>" data-disabled-value="<?php echo esc_attr($disabledValue); ?>" data-original-value="<?php echo esc_attr($value == $enabledValue ? $enabledValue : $disabledValue); ?>">
|
||||
<li class="wfls-option-checkbox<?php echo ($value == $enabledValue ? ' wfls-checked' : ''); ?>" role="checkbox" aria-checked="<?php echo ($value == $enabledValue ? 'true' : 'false'); ?>" tabindex="0" aria-labelledby="<?php echo esc_attr($id); ?>-label"><i class="wfls-ion-ios-checkmark-empty" aria-hidden="true"></i></li>
|
||||
<li class="wfls-option-title">
|
||||
<?php if (isset($subtitle)): ?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li>
|
||||
<?php endif; ?>
|
||||
<span id="<?php echo esc_attr($id); ?>-label"><?php echo \WordfenceLS\Text\Model_HTML::esc_html($title); ?></span><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?>
|
||||
<?php if (isset($subtitle)): ?>
|
||||
</li>
|
||||
<li class="wfls-option-subtitle"><?php echo \WordfenceLS\Text\Model_HTML::esc_html($subtitle); ?></li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
if (!defined('WORDFENCE_LS_VERSION')) { exit; }
|
||||
/**
|
||||
* Presents an option with a token field for value entry.
|
||||
*
|
||||
* Expects $tokenOptionName, $tokenValue, and $title to be defined. $helpLink may also be defined.
|
||||
*
|
||||
* @var string $tokenOptionName The option name.
|
||||
* @var array $tokenValue The current value of $tokenOptionName. It will be JSON-encoded as an array of strings.
|
||||
* @var string $title The title shown for the option.
|
||||
* @var string $helpLink If defined, the link to the corresponding external help page.
|
||||
* @var bool $premium If defined, the option will be tagged as premium only and not allow its value to change for free users.
|
||||
*/
|
||||
|
||||
$id = 'wfls-option-' . preg_replace('/[^a-z0-9]/i', '-', $tokenOptionName);
|
||||
?>
|
||||
<ul id="<?php echo esc_attr($id); ?>" class="wfls-option wfls-option-token<?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' wfls-option-premium'; } ?>" data-token-option="<?php echo esc_attr($tokenOptionName); ?>" data-original-token-value="<?php echo esc_attr(json_encode($tokenValue)); ?>">
|
||||
<li class="wfls-option-spacer"></li>
|
||||
<li class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<div class="wfls-option-title">
|
||||
<?php if (isset($subtitle)): ?>
|
||||
<ul class="wfls-flex-vertical wfls-flex-align-left">
|
||||
<li>
|
||||
<?php endif; ?>
|
||||
<span id="<?php echo esc_attr($id); ?>-label"><?php echo esc_html($title); ?></span><?php if (!wfConfig::p() && isset($premium) && $premium) { echo ' <a href="https://www.wordfence.com/gnl1optionUpgrade/wordfence-signup/" target="_blank" rel="noopener noreferrer" class="wfls-premium-link">' . esc_html__('Premium Feature', 'wordfence-2fa') . '</a>'; } ?><?php if (isset($helpLink)) { echo ' <a href="' . esc_attr($helpLink) . '" target="_blank" rel="noopener noreferrer" class="wfls-inline-help"><i class="' . (\WordfenceLS\Controller_WordfenceLS::shared()->should_use_core_font_awesome_styles() ? 'wf-fa wf-fa-question-circle-o' : 'wfls-fa wfls-fa-question-circle-o') . '" aria-hidden="true"></i></a>'; } ?>
|
||||
<?php if (isset($subtitle)): ?>
|
||||
</li>
|
||||
<li class="wfls-option-subtitle"><?php echo esc_html($subtitle); ?></li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<select multiple<?php echo (!(!wfConfig::p() && isset($premium) && $premium) ? '' : ' disabled'); ?> aria-labelledby="<?php echo esc_attr($id); ?>-label">
|
||||
<?php foreach ($tokenValue as $o): ?>
|
||||
<option value="<?php echo esc_attr($o); ?>" selected><?php echo esc_html($o); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="wfls-option-token-tags"></div>
|
||||
</li>
|
||||
</ul>
|
||||