Initial commit: Atomaste website
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
defined('ABSPATH') or die();
|
||||
/**
|
||||
Schedule cron jobs if useCron is true
|
||||
Else start the functions for testing
|
||||
*/
|
||||
define('RSSSL_USE_CRON', true );
|
||||
if ( RSSSL_USE_CRON ) {
|
||||
add_action( 'plugins_loaded', 'rsssl_schedule_cron' );
|
||||
function rsssl_schedule_cron() {
|
||||
if ( ! wp_next_scheduled( 'rsssl_every_day_hook' ) ) {
|
||||
wp_schedule_event( time(), 'rsssl_daily', 'rsssl_every_day_hook' );
|
||||
}
|
||||
|
||||
if ( ! wp_next_scheduled( 'rsssl_every_three_hours_hook' ) ) {
|
||||
wp_schedule_event( time(), 'rsssl_every_three_hours', 'rsssl_every_three_hours_hook' );
|
||||
}
|
||||
|
||||
if ( ! wp_next_scheduled( 'rsssl_every_five_minutes_hook' ) ) {
|
||||
wp_schedule_event( time(), 'rsssl_five_minutes', 'rsssl_every_five_minutes_hook' );
|
||||
}
|
||||
if ( ! wp_next_scheduled( 'rsssl_every_week_hook' ) ) {
|
||||
wp_schedule_event( time(), 'rsssl_weekly', 'rsssl_every_week_hook' );
|
||||
}
|
||||
if ( ! wp_next_scheduled( 'rsssl_every_month_hook' ) ) {
|
||||
wp_schedule_event( time(), 'rsssl_monthly', 'rsssl_every_month_hook' );
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Fire three hours cron hook
|
||||
* @return void
|
||||
*/
|
||||
function rsssl_three_hours_cron(){
|
||||
do_action('rsssl_three_hours_cron');
|
||||
}
|
||||
add_action( 'rsssl_every_three_hours_hook', 'rsssl_three_hours_cron' );
|
||||
|
||||
/**
|
||||
* Fire daily cron hook
|
||||
*/
|
||||
function rsssl_daily_cron(){
|
||||
do_action('rsssl_daily_cron');
|
||||
}
|
||||
add_action( 'rsssl_every_day_hook', 'rsssl_daily_cron' );
|
||||
/**
|
||||
* Fire five minutes cron hook
|
||||
*/
|
||||
function rsssl_five_minutes_cron() {
|
||||
do_action( 'rsssl_five_minutes_cron' );
|
||||
}
|
||||
add_action( 'rsssl_every_five_minutes_hook', 'rsssl_five_minutes_cron' );
|
||||
/**
|
||||
* Fire weekly cron hook
|
||||
*/
|
||||
function rsssl_weekly_cron() {
|
||||
do_action( 'rsssl_weekly_cron' );
|
||||
}
|
||||
add_action( 'rsssl_every_week_hook', 'rsssl_weekly_cron' );
|
||||
/**
|
||||
* Fire montly cron hook
|
||||
*/
|
||||
function rsssl_monthly_cron() {
|
||||
do_action( 'rsssl_monthly_cron' );
|
||||
}
|
||||
add_action( 'rsssl_every_month_hook', 'rsssl_monthly_cron' );
|
||||
|
||||
|
||||
/**
|
||||
* For testing without cron enabled. Not recommended for production
|
||||
*/
|
||||
if ( !RSSSL_USE_CRON ) {
|
||||
add_action( 'admin_init', 'rsssl_schedule_non_cron' );
|
||||
function rsssl_schedule_non_cron(){
|
||||
do_action( 'rsssl_daily_cron' );
|
||||
do_action( 'rsssl_five_minutes_cron' );
|
||||
do_action('rsssl_week_cron');
|
||||
do_action('rsssl_month_cron');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add our schedules
|
||||
* @param array $schedules
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function rsssl_filter_cron_schedules( $schedules ) {
|
||||
$schedules['rsssl_five_minutes'] = array(
|
||||
'interval' => 5 * MINUTE_IN_SECONDS, // seconds
|
||||
'display' => __('Once every 5 minutes')
|
||||
);
|
||||
$schedules['rsssl_daily'] = array(
|
||||
'interval' => DAY_IN_SECONDS,
|
||||
'display' => __( 'Once every day' )
|
||||
);
|
||||
$schedules['rsssl_every_three_hours'] = array(
|
||||
'interval' => 3 * HOUR_IN_SECONDS,
|
||||
'display' => __( 'Every three hours' )
|
||||
);
|
||||
$schedules['rsssl_weekly'] = array(
|
||||
'interval' => WEEK_IN_SECONDS,
|
||||
'display' => __( 'Once every week' )
|
||||
);
|
||||
$schedules['rsssl_monthly'] = array(
|
||||
'interval' => MONTH_IN_SECONDS,
|
||||
'display' => __( 'Once every month' )
|
||||
);
|
||||
return $schedules;
|
||||
}
|
||||
add_filter( 'cron_schedules', 'rsssl_filter_cron_schedules' );
|
||||
/**
|
||||
* Clear on deactivation
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rsssl_clear_scheduled_hooks() {
|
||||
wp_clear_scheduled_hook( 'rsssl_every_day_hook' );
|
||||
wp_clear_scheduled_hook( 'rsssl_every_week_hook' );
|
||||
wp_clear_scheduled_hook( 'rsssl_every_month_hook' );
|
||||
wp_clear_scheduled_hook( 'rsssl_every_five_minutes_hook' );
|
||||
wp_clear_scheduled_hook( 'rsssl_every_three_hours_hook' );
|
||||
wp_clear_scheduled_hook( 'rsssl_ssl_process_hook' );
|
||||
}
|
||||
register_deactivation_hook( rsssl_file, 'rsssl_clear_scheduled_hooks' );
|
||||
|
||||
/**
|
||||
* Multisite cron
|
||||
*/
|
||||
|
||||
add_action('plugins_loaded', 'rsssl_multisite_schedule_cron', 15);
|
||||
function rsssl_multisite_schedule_cron()
|
||||
{
|
||||
if ( get_site_option('rsssl_ssl_activation_active') ) {
|
||||
if ( !wp_next_scheduled('rsssl_ssl_process_hook') ) {
|
||||
wp_schedule_event(time(), 'rsssl_one_minute', 'rsssl_ssl_process_hook');
|
||||
}
|
||||
} else {
|
||||
wp_clear_scheduled_hook('rsssl_ssl_process_hook');
|
||||
}
|
||||
add_action( 'rsssl_ssl_process_hook', array( RSSSL()->multisite, 'run_ssl_process' ) );
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
defined('ABSPATH') or die();
|
||||
/**
|
||||
* If a plugin is deactivated, add to deactivated list.
|
||||
* @param string $field_id
|
||||
* @param mixed $new_value
|
||||
* @param mixed $prev_value
|
||||
* @param string $type
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rsssl_handle_integration_deactivation($field_id, $new_value, $prev_value, $type){
|
||||
if (!rsssl_user_can_manage()) {
|
||||
return;
|
||||
}
|
||||
if ($new_value !== $prev_value && $new_value === 0 ){
|
||||
//check if this field id exists in the list of plugins
|
||||
global $rsssl_integrations_list;
|
||||
foreach ( $rsssl_integrations_list as $plugin => $plugin_data ) {
|
||||
if (
|
||||
isset($plugin_data['has_deactivation']) &&
|
||||
$plugin_data['has_deactivation'] &&
|
||||
isset($plugin_data['option_id']) &&
|
||||
$plugin_data['option_id'] === $field_id
|
||||
) {
|
||||
//add to deactivated list
|
||||
$current_list = get_option('rsssl_deactivate_list', []);
|
||||
if ( !in_array($plugin, $current_list)) {
|
||||
$current_list[] = $plugin;
|
||||
update_option('rsssl_deactivate_list', $current_list, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action( "rsssl_after_save_field", "rsssl_handle_integration_deactivation", 10, 4 );
|
||||
|
||||
/**
|
||||
* Remove a plugin from the deactivation list if deactivation procedure was completed
|
||||
* @param string $plugin
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rsssl_remove_from_deactivation_list($plugin){
|
||||
if (!rsssl_user_can_manage()) {
|
||||
return;
|
||||
}
|
||||
$deactivate_list = get_option('rsssl_deactivate_list', []);
|
||||
if ( in_array($plugin, $deactivate_list )) {
|
||||
$index = array_search($plugin, $deactivate_list);
|
||||
unset($deactivate_list[$index]);
|
||||
update_option('rsssl_deactivate_list', $deactivate_list, false );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,850 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) || die();
|
||||
|
||||
/**
|
||||
* Class to handle the creation and include of the firewall
|
||||
*/
|
||||
class rsssl_firewall_manager {
|
||||
/**
|
||||
* Firewall object
|
||||
*
|
||||
* @var rsssl_firewall_manager
|
||||
*/
|
||||
private static $this;
|
||||
/**
|
||||
* File
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $file = WP_CONTENT_DIR . '/advanced-headers.php';
|
||||
/**
|
||||
* If we can use a dynamic path
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $use_dynamic_path = WP_CONTENT_DIR === ABSPATH . 'wp-content';
|
||||
|
||||
//rules to add to the firewall.
|
||||
private $rules;
|
||||
|
||||
/**
|
||||
* Our constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
|
||||
if ( isset( self::$this ) ) {
|
||||
wp_die();
|
||||
}
|
||||
self::$this = $this;
|
||||
|
||||
// trigger this action to force rules update
|
||||
add_action( 'rsssl_update_rules', array( $this, 'install' ), 10 );
|
||||
add_action( 'rsssl_after_saved_fields', array( $this, 'install' ), 100 );
|
||||
add_action( 'rsssl_deactivate', array( $this, 'uninstall' ), 20 );
|
||||
add_filter( 'rsssl_notices', array( $this, 'notices' ) );
|
||||
add_filter( 'before_rocket_htaccess_rules', array( $this, 'add_htaccess_rules_before_wp_rocket' ), 999 );
|
||||
|
||||
//handle activation and deactivation of wp rocket
|
||||
add_action( 'rocket_activation', array( $this, 'remove_prepend_file_in_htaccess' ) );
|
||||
add_action( 'rocket_deactivation', array( $this, 'include_prepend_file_in_htaccess' ) );
|
||||
|
||||
if ( ! defined( 'RSSSL_IS_WP_ENGINE' ) ) {
|
||||
define( 'RSSSL_IS_WP_ENGINE', isset( $_SERVER['IS_WPE'] ) );
|
||||
}
|
||||
if ( ! defined( 'RSSSL_IS_FLYWHEEL' ) ) {
|
||||
define( 'RSSSL_IS_FLYWHEEL', isset( $_SERVER['SERVER_SOFTWARE'] ) && strpos( $_SERVER['SERVER_SOFTWARE'], 'Flywheel/' ) === 0 );
|
||||
}
|
||||
if ( ! defined( 'RSSSL_IS_PRESSABLE' ) ) {
|
||||
define( 'RSSSL_IS_PRESSABLE', ( defined( 'IS_ATOMIC' ) && IS_ATOMIC ) || ( defined( 'IS_PRESSABLE' ) && IS_PRESSABLE ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main installer for the firewall file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function install(): void {
|
||||
if ( ! rsssl_admin_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( wp_doing_ajax() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( empty( $this->rules ) ) {
|
||||
$this->rules = apply_filters( 'rsssl_firewall_rules', '' );
|
||||
}
|
||||
|
||||
// no rules? remove the file.
|
||||
if ( empty( trim( $this->rules ) ) ) {
|
||||
// $this->delete_file();
|
||||
$this->remove_prepend_file_in_htaccess();
|
||||
$this->remove_prepend_file_in_wp_config();
|
||||
return;
|
||||
}
|
||||
// update the file to be included.
|
||||
$this->update_firewall( $this->rules );
|
||||
|
||||
$this->include_prepend_file_in_wp_config();
|
||||
if ( $this->uses_htaccess() ) {
|
||||
$this->include_prepend_file_in_htaccess();
|
||||
}
|
||||
|
||||
if ( $this->has_user_ini_file() ) {
|
||||
$this->include_prepend_file_in_user_ini();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove file and file inclusions
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function uninstall(): void {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( wp_doing_ajax() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->remove_prepend_file_in_htaccess();
|
||||
$this->remove_prepend_file_in_wp_config();
|
||||
$this->remove_auto_prepend_file_in_user_ini();
|
||||
|
||||
$this->delete_file();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if our firewall file exists
|
||||
*
|
||||
* @param string $file // filename, including path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function file_exists( string $file ): bool {
|
||||
return file_exists( $file );
|
||||
// $wp_filesystem = $this->init_file_system();
|
||||
// return $wp_filesystem->is_file($this->file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the WP_Filesystem
|
||||
*
|
||||
* @return false|WP_Filesystem_Base
|
||||
*/
|
||||
private function init_file_system() {
|
||||
if ( ! function_exists( 'WP_Filesystem' ) ) {
|
||||
include_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
if ( false === ( $creds = request_filesystem_credentials( site_url(), '', false, false, null ) ) ) {
|
||||
return false; // stop processing here.
|
||||
}
|
||||
global $wp_filesystem;
|
||||
if ( ! WP_Filesystem( $creds ) ) {
|
||||
// request_filesystem_credentials(site_url(), '', true, false, null);//phpcs:ingore
|
||||
return false;
|
||||
}
|
||||
return $wp_filesystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the file that contains the firewall rules, advanced-headers.php
|
||||
*
|
||||
* @param string $rules //rules to add to the firewall.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function update_firewall( string $rules ): void {
|
||||
if ( ! rsssl_admin_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
$contents = '<?php' . "\n";
|
||||
$contents .= '/**' . "\n";
|
||||
$contents .= '* This file is created by Really Simple Security' . "\n";
|
||||
$contents .= '*/' . "\n\n";
|
||||
$contents .= 'if (defined("SHORTINIT") && SHORTINIT) return;' . "\n\n";
|
||||
$contents .= '$base_path = dirname(__FILE__);' . "\n";
|
||||
$contents .= 'if( file_exists( $base_path . "/rsssl-safe-mode.lock" ) ) {' . "\n";
|
||||
$contents .= ' if ( ! defined( "RSSSL_SAFE_MODE" ) ) {' . "\n";
|
||||
$contents .= ' define( "RSSSL_SAFE_MODE", true );' . "\n";
|
||||
$contents .= ' }' . "\n";
|
||||
$contents .= ' return;' . "\n";
|
||||
$contents .= '}' . "\n\n";
|
||||
// allow disabling of headers for detection purposes.
|
||||
$contents .= 'if ( isset($_GET["rsssl_header_test"]) && (int) $_GET["rsssl_header_test"] === ' . $this->get_headers_nonce() . ' ) return;' . "\n\n";
|
||||
//if already included at some point, don't execute again.
|
||||
$contents .= 'if ( defined("RSSSL_HEADERS_ACTIVE" ) ) return;' . "\n";
|
||||
$contents .= 'define( "RSSSL_HEADERS_ACTIVE", true );' . "\n";
|
||||
$contents .= "//RULES START\n" . $rules;
|
||||
|
||||
$this->put_contents( $this->file, $contents );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save data
|
||||
*
|
||||
* @param string $file //filename, including path.
|
||||
* @param string $contents //data to save.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function put_contents( $file, $contents ): void {
|
||||
if ( ! rsssl_admin_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !file_exists($file) || $this->is_writable($file)) {
|
||||
// $wp_filesystem = $this->init_file_system();
|
||||
// $result = $wp_filesystem->put_contents($contents, $this->file);
|
||||
file_put_contents( $file, $contents );//phpcs:ignore
|
||||
} else if ( !$this->is_writable($file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
//only chmod other files than .htaccess and wpconfig. We leave these as is.
|
||||
if ( strpos($file, 'htaccess') === false || strpos($file, 'wp-config.php') === false ) {
|
||||
if ( $this->file_exists( $this->file) ) {
|
||||
chmod( $this->file, 0644 );//phpcs:ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a file
|
||||
*
|
||||
* @param string $file //filename, including path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_contents( string $file ): string {
|
||||
if ( ! $this->file_exists( $file ) ) {
|
||||
return '';
|
||||
}
|
||||
// $wp_filesystem = $this->init_file_system();
|
||||
// $result = $wp_filesystem->get_contents($file);
|
||||
return file_get_contents( $file );//phpcs:ignore
|
||||
}
|
||||
/**
|
||||
* Delete a file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function delete_file(): void {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->file_exists( $this->file ) ) {
|
||||
unlink( $this->file );//phpcs:ignore
|
||||
}
|
||||
// $wp_filesystem = $this->init_file_system();
|
||||
// $wp_filesystem->delete($this->file);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*
|
||||
* Check if installation uses htaccess.conf (Bitnami)
|
||||
*/
|
||||
private function uses_htaccess_conf() {
|
||||
$htaccess_conf_file = dirname( ABSPATH ) . '/conf/htaccess.conf';
|
||||
//conf/htaccess.conf can be outside of open basedir, return false if so
|
||||
$open_basedir = ini_get( 'open_basedir' );
|
||||
if ( ! empty( $open_basedir ) ) {
|
||||
return false;
|
||||
}
|
||||
return is_file( $htaccess_conf_file );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the .htaccess path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function htaccess_path(): string {
|
||||
|
||||
if ( $this->uses_htaccess_conf() ) {
|
||||
$htaccess_file = realpath( dirname( ABSPATH ) . '/conf/htaccess.conf' );
|
||||
} else {
|
||||
$htaccess_file = $this->get_home_path() . '.htaccess';
|
||||
}
|
||||
|
||||
return $htaccess_file;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the home path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_home_path(): string {
|
||||
if ( ! function_exists( 'get_home_path' ) ) {
|
||||
include_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
if ( RSSSL_IS_FLYWHEEL && isset( $_SERVER['DOCUMENT_ROOT'] ) ) {
|
||||
return trailingslashit( $this->sanitize_path( wp_unslash( $_SERVER['DOCUMENT_ROOT'] ) ) );
|
||||
}
|
||||
return get_home_path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a path
|
||||
*
|
||||
* @param string $path //string to sanitize.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function sanitize_path( $path ): string {
|
||||
// prevent path traversal.
|
||||
return str_replace( '../', '/', realpath( sanitize_text_field( $path ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this server uses .htaccess. Not by checking the server header, but simply by checking
|
||||
* if the htaccess file exists.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function uses_htaccess(): bool {
|
||||
return $this->file_exists( $this->htaccess_path() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the prepend file in the .htaccess
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function include_prepend_file_in_htaccess(): void {
|
||||
if ( ! $this->file_exists( $this->file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the wp-config contains the if constant condition, to prevent duplicate loading. If not, try upgrading. If that fails, skip.
|
||||
if ( ! $this->wp_config_contains_latest() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rules = $this->get_htaccess_rules();
|
||||
$start = '#Begin Really Simple Auto Prepend File' . "\n";
|
||||
$end = "\n" . '#End Really Simple Auto Prepend File' . "\n";
|
||||
$pattern_content = '/' . $start . '(.*?)' . $end . '/is';
|
||||
$htaccess_file = $this->htaccess_path();
|
||||
if ( $this->file_exists( $htaccess_file ) ) {
|
||||
$content = $this->get_contents( $htaccess_file );
|
||||
// remove first, to ensure we are at the top of the file.
|
||||
$content = preg_replace( $pattern_content, '', $content );
|
||||
if ( ! empty( $rules ) ) {
|
||||
if ( ! $this->is_writable( $htaccess_file ) ) {
|
||||
update_site_option( 'rsssl_htaccess_error', 'not-writable' );
|
||||
update_site_option( 'rsssl_htaccess_rules', $rules . get_site_option( 'rsssl_htaccess_rules' ) );
|
||||
} else {
|
||||
delete_site_option( 'rsssl_htaccess_error' );
|
||||
delete_site_option( 'rsssl_htaccess_rules' );
|
||||
// add rules as new block.
|
||||
$content = $start . $rules . $end . $content;
|
||||
|
||||
// clean up.
|
||||
if ( strpos( $content, "\n\n\n" ) !== false ) {
|
||||
$content = str_replace( "\n\n\n", "\n\n", $content );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//by putting this outside the empty($rules) condition, the rules get removed if disabled or not available
|
||||
if ( $this->is_writable( $htaccess_file ) ) {
|
||||
$this->put_contents( $htaccess_file, $content );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the .htaccess rules for the prepend file
|
||||
* Add user.ini blocking rules if user.ini filename exist.
|
||||
*
|
||||
* @return string //the string containing the lines of rules
|
||||
*/
|
||||
private function get_htaccess_rules() : string {
|
||||
if ( defined('RSSSL_HTACCESS_SKIP_AUTO_PREPEND') && RSSSL_HTACCESS_SKIP_AUTO_PREPEND ) {
|
||||
return '';
|
||||
}
|
||||
if (isset(RSSSL()->server) ) {
|
||||
$config = RSSSL()->server->auto_prepend_config();
|
||||
} else {
|
||||
$config = get_option('rsssl_auto_prepend_config');
|
||||
if (empty($config)) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
$file = addcslashes($this->file, "'");
|
||||
switch ($config) {
|
||||
case 'litespeed':
|
||||
$rules = array(
|
||||
'<IfModule LiteSpeed>',
|
||||
'php_value auto_prepend_file ' . $file ,
|
||||
'</IfModule>',
|
||||
'<IfModule lsapi_module>',
|
||||
'php_value auto_prepend_file ' . $file,
|
||||
'</IfModule>',
|
||||
);
|
||||
break;
|
||||
case 'apache-mod_php':
|
||||
default:
|
||||
$rules = array(
|
||||
'<IfModule mod_php7.c>',
|
||||
'php_value auto_prepend_file ' . $file ,
|
||||
'</IfModule>',
|
||||
'<IfModule mod_php.c>',
|
||||
'php_value auto_prepend_file ' . $file,
|
||||
'</IfModule>',
|
||||
);
|
||||
}
|
||||
|
||||
$userIni = ini_get('user_ini.filename');
|
||||
if ($userIni) {
|
||||
$rules = array_merge(
|
||||
$rules,
|
||||
array(
|
||||
sprintf('<Files "%s">', addcslashes($userIni, '"')),
|
||||
'<IfModule mod_authz_core.c>' ,
|
||||
'Require all denied',
|
||||
'</IfModule>',
|
||||
'<IfModule !mod_authz_core.c>',
|
||||
'Order deny,allow',
|
||||
'Deny from all',
|
||||
'</IfModule>',
|
||||
'</Files>',
|
||||
));
|
||||
}
|
||||
|
||||
return implode( "\n", $rules );
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the file in the wp-config
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function include_prepend_file_in_wp_config(): void {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
$file = $this->wpconfig_path();
|
||||
$content = $this->get_contents( $file );
|
||||
if ( $this->is_writable( $file ) && strpos( $content, 'advanced-headers.php' ) === false ) {
|
||||
$rule = $this->get_wp_config_rule();
|
||||
|
||||
// if RSSSL comment is found, insert after.
|
||||
$rsssl_comment = '//END Really Simple Security Server variable fix';
|
||||
if ( strpos( $content, $rsssl_comment ) !== false ) {
|
||||
$pos = strrpos( $content, $rsssl_comment );
|
||||
$updated = substr_replace( $content, $rsssl_comment . "\n" . $rule . "\n", $pos, strlen( $rsssl_comment ) );
|
||||
} else {
|
||||
$updated = preg_replace( '/<\?php/', "<?php\n" . $rule . "\n", $content, 1 );
|
||||
}
|
||||
|
||||
if ( strpos( $updated, "\n\n\n" ) !== false ) {
|
||||
$updated = str_replace( "\n\n\n", "\n\n", $updated );
|
||||
}
|
||||
|
||||
$this->put_contents( $file, $updated );
|
||||
}
|
||||
|
||||
// save errors.
|
||||
if ( $this->is_writable( WP_CONTENT_DIR ) && ( $this->is_writable( $file ) || strpos( $content, 'advanced-headers.php' ) !== false ) ) {
|
||||
update_option( 'rsssl_firewall_error', false, false );
|
||||
} elseif ( ! $this->is_writable( $file ) ) {
|
||||
update_option( 'rsssl_firewall_error', 'wpconfig-notwritable', false );
|
||||
} elseif ( ! $this->is_writable( WP_CONTENT_DIR ) ) {
|
||||
update_option( 'rsssl_firewall_error', 'advanced-headers-notwritable', false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the rules
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function remove_prepend_file_in_htaccess(): void {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
$start = '#Begin Really Simple Auto Prepend File' . "\n";
|
||||
$end = "\n" . '#End Really Simple Auto Prepend File' . "\n";
|
||||
$pattern = '/' . $start . '(.*?)' . $end . '/is';
|
||||
$htaccess_file = $this->htaccess_path();
|
||||
if ( $this->file_exists( $htaccess_file ) ) {
|
||||
$content = $this->get_contents( $htaccess_file );
|
||||
// remove first, to ensure we are at the top of the file.
|
||||
$content = preg_replace( $pattern, '', $content );
|
||||
$this->put_contents( $htaccess_file, $content );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the prepend file from the config
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function remove_prepend_file_in_wp_config(): void {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$file = $this->wpconfig_path();
|
||||
if ( $this->is_writable( $file ) ) {
|
||||
$content = $this->get_contents( $file );
|
||||
$rule = $this->get_wp_config_rule();
|
||||
if ( strpos( $content, $rule ) !== false ) {
|
||||
$content = str_replace( $rule, '', $content );
|
||||
if ( strpos( $content, "\n\n\n" ) !== false ) {
|
||||
$content = str_replace( "\n\n\n", "\n\n", $content );
|
||||
}
|
||||
$this->put_contents( $file, $content );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function
|
||||
*
|
||||
* @param string $file // filename, including path.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_writable( $file ): bool {
|
||||
// $wp_filesystem = $this->init_file_system();
|
||||
// return $wp_filesystem->is_writable($file);
|
||||
return is_writable( $file );//phpcs:ignore
|
||||
}
|
||||
|
||||
/**
|
||||
* This class has it's own settings page, to ensure it can always be called
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_settings_page() {
|
||||
if ( rsssl_is_logged_in_rest() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( isset( $_GET['page'] ) && 'really-simple-security' === $_GET['page'] ) {//phpcs:ignore
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and return a random nonce
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_headers_nonce() {
|
||||
if ( ! get_site_option( 'rsssl_header_detection_nonce' ) ) {
|
||||
update_site_option( 'rsssl_header_detection_nonce', wp_rand( 1000, 999999999 ) );
|
||||
}
|
||||
return (int) get_site_option( 'rsssl_header_detection_nonce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any rules were added
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_rules() {
|
||||
|
||||
if ( empty( $this->rules ) ) {
|
||||
$this->rules = apply_filters( 'rsssl_firewall_rules', '' );
|
||||
}
|
||||
return ! empty( trim( $this->rules ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status for the firewall rules writing
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function firewall_write_error() {
|
||||
return get_site_option( 'rsssl_firewall_error' );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the status for the firewall
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function firewall_active_error() {
|
||||
if ( ! $this->has_rules() ) {
|
||||
return false;
|
||||
}
|
||||
return ! defined( 'RSSSL_HEADERS_ACTIVE' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Show some notices
|
||||
*
|
||||
* @param array $notices //array of notices.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function notices( $notices ) {
|
||||
$notices['firewall-error'] = array(
|
||||
'callback' => 'RSSSL_SECURITY()->firewall_manager->firewall_write_error',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'wpconfig-notwritable' => array(
|
||||
'title' => __( 'Firewall', 'really-simple-ssl' ),
|
||||
'msg' => __( 'A firewall rule was enabled, but the wp-config.php is not writable.', 'really-simple-ssl' ) . ' ' . __( 'Please set the wp-config.php to writable until the rule has been written.', 'really-simple-ssl' ),
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
),
|
||||
'advanced-headers-notwritable' => array(
|
||||
'title' => __( 'Firewall', 'really-simple-ssl' ),
|
||||
'msg' => __( 'A firewall rule was enabled, but /the wp-content/ folder is not writable.', 'really-simple-ssl' ) . ' ' . __( 'Please set the wp-content folder to writable:', 'really-simple-ssl' ),
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
),
|
||||
),
|
||||
'show_with_options' => array(
|
||||
'disable_http_methods',
|
||||
),
|
||||
);
|
||||
$notices['firewall-active'] = array(
|
||||
'condition' => array( 'RSSSL_SECURITY()->firewall_manager->firewall_active_error' ),
|
||||
'callback' => '_true_',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'true' => array(
|
||||
'title' => __( 'Firewall', 'really-simple-ssl' ),
|
||||
'msg' => __( 'A firewall rule was enabled, but the firewall does not seem to get loaded correctly.', 'really-simple-ssl' ) . ' ' . __( 'Please check if the advanced-headers.php file is included in the wp-config.php, and exists in the wp-content folder.', 'really-simple-ssl' ),
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
),
|
||||
),
|
||||
'show_with_options' => array(
|
||||
'disable_http_methods',
|
||||
),
|
||||
);
|
||||
return $notices;
|
||||
}
|
||||
|
||||
/**
|
||||
* // As WP_CONTENT_DIR is not defined at this point in the wp-config, we can't use that.
|
||||
* // for those setups where the WP_CONTENT_DIR is not in the default location, we hardcode the path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_wp_config_rule() {
|
||||
if ( $this->use_dynamic_path ) {
|
||||
$rule = 'if (!defined("RSSSL_HEADERS_ACTIVE") && file_exists( ABSPATH . "wp-content/advanced-headers.php")) {' . "\n";
|
||||
$rule .= "\t" . 'require_once ABSPATH . "wp-content/advanced-headers.php";' . "\n" . '}';
|
||||
} else {
|
||||
$rule = 'if (!defined("RSSSL_HEADERS_ACTIVE") && file_exists(\'' . WP_CONTENT_DIR . '/advanced-headers.php\')) {' . "\n";
|
||||
$rule .= "\t" . 'require_once \'' . WP_CONTENT_DIR . '/advanced-headers.php\';' . "\n" . '}';
|
||||
}
|
||||
return $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the wp-config contains the if constant condition, to prevent duplicate loading. If not, try upgrading. If that fails, skip.
|
||||
* Wrapper function added for clearer purpose in code
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function wp_config_contains_latest(): bool {
|
||||
return $this->update_wp_config_rule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in upgrade.php, to upgrade older rules to the latest.
|
||||
* Returns true if the wpconfig contains the upgraded lines
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function update_wp_config_rule(): bool {
|
||||
$file = $this->wpconfig_path();
|
||||
if ( ! $file ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$content = $this->get_contents( $file );
|
||||
$find = '(file_exists( ABSPATH . "wp-content/advanced-headers.php"))';
|
||||
if ( false !== strpos( $content, $find ) ) {
|
||||
if ( ! $this->is_writable( $file ) ) {
|
||||
return false;
|
||||
}
|
||||
$replace = '(!defined("RSSSL_HEADERS_ACTIVE") && file_exists( ABSPATH . "wp-content/advanced-headers.php"))';
|
||||
$content = str_replace( $find, $replace, $content );
|
||||
$this->put_contents( $file, $content );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin is not always loaded here, so we define our own function
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function wpconfig_path() {
|
||||
|
||||
// Allow the wp-config.php path to be overridden via a filter.
|
||||
$filtered_path = apply_filters( 'rsssl_wpconfig_path', '' );
|
||||
|
||||
// If a filtered path is provided, validate it.
|
||||
if ( ! empty( $filtered_path ) ) {
|
||||
$directory = dirname( $filtered_path );
|
||||
|
||||
// Ensure the directory exists before checking for the file.
|
||||
if ( is_dir( $directory ) && file_exists( $filtered_path ) ) {
|
||||
return $filtered_path;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit number of iterations to 5.
|
||||
$i = 0;
|
||||
$maxiterations = 5;
|
||||
$dir = ABSPATH;
|
||||
do {
|
||||
++ $i;
|
||||
if ( $this->file_exists( $dir . 'wp-config.php' ) ) {
|
||||
return $dir . 'wp-config.php';
|
||||
}
|
||||
} while ( ( $dir = realpath( "$dir/.." ) ) && ( $i < $maxiterations ) );//phpcs:ignore
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the headers
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function remove_advanced_headers() {
|
||||
$this->uninstall();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return .htaccess redirect when using WP Rocket
|
||||
* @return string
|
||||
*/
|
||||
public function add_htaccess_rules_before_wp_rocket($rules) {
|
||||
if ( !file_exists( $this->file ) ) {
|
||||
return $rules;
|
||||
}
|
||||
|
||||
$rules = $this->get_htaccess_rules()."\n".$rules;
|
||||
if ( ! empty( $rules ) ) {
|
||||
$start = '#Begin Really Simple Auto Prepend File' . "\n";
|
||||
$end = "\n" . '#End Really Simple Auto Prepend File' . "\n";
|
||||
$rules = $start . $rules . $end;
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user.ini file exists or is in user.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function has_user_ini_file():bool {
|
||||
$userIni = ini_get('user_ini.filename');
|
||||
if ( $userIni ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add auto prepend file to user.ini
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function include_prepend_file_in_user_ini():void{
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( defined('RSSSL_HTACCESS_SKIP_AUTO_PREPEND') && RSSSL_HTACCESS_SKIP_AUTO_PREPEND ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = RSSSL()->server->auto_prepend_config();
|
||||
if ( !$this->has_user_ini_file() ) {
|
||||
return;
|
||||
}
|
||||
$autoPrependIni = '';
|
||||
$userIniPath = $this->get_user_ini_path();
|
||||
// .user.ini configuration
|
||||
switch ($config) {
|
||||
case 'cgi':
|
||||
case 'nginx':
|
||||
case 'apache-suphp':
|
||||
case 'litespeed':
|
||||
case 'iis':
|
||||
$autoPrependIni = sprintf("; BEGIN Really Simple Auto Prepend File
|
||||
auto_prepend_file = '%s'
|
||||
; END Really Simple Auto Prepend File", addcslashes($this->file, "'"));
|
||||
break;
|
||||
}
|
||||
|
||||
if ( !empty($autoPrependIni) ) {
|
||||
// Modify .user.ini
|
||||
$userIniContent = $this->get_contents($userIniPath);
|
||||
if ( $userIniContent ) {
|
||||
$userIniContent = str_replace('auto_prepend_file', ';auto_prepend_file', $userIniContent);
|
||||
$regex = '/; BEGIN Really Simple Auto Prepend File.*?; END Really Simple Auto Prepend File/is';
|
||||
if (preg_match($regex, $userIniContent, $matches)) {
|
||||
$userIniContent = preg_replace($regex, $autoPrependIni, $userIniContent);
|
||||
} else {
|
||||
$userIniContent .= "\n" . $autoPrependIni;
|
||||
}
|
||||
} else {
|
||||
$userIniContent = $autoPrependIni;
|
||||
}
|
||||
|
||||
$this->put_contents($userIniPath, $userIniContent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user.ini path
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function get_user_ini_path() {
|
||||
$userIni = ini_get('user_ini.filename');
|
||||
if ($userIni) {
|
||||
return $this->get_home_path() . $userIni;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the added auto prepend file
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function remove_auto_prepend_file_in_user_ini() {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->has_user_ini_file() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userIniPath = $this->get_user_ini_path();
|
||||
$userIniContent = $this->get_contents( $userIniPath );
|
||||
$userIniContent = preg_replace( '/; BEGIN Really Simple Auto Prepend File.*?; END Really Simple Auto Prepend File/is', '', $userIniContent );
|
||||
$userIniContent = str_replace( 'auto_prepend_file', ';auto_prepend_file', $userIniContent );
|
||||
$this->put_contents( $userIniPath, $userIniContent );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) or die( );
|
||||
/**
|
||||
* Back-end available only
|
||||
*/
|
||||
if ( !function_exists('rsssl_do_fix')) {
|
||||
/**
|
||||
* Complete a fix for an issue, either user triggered, or automatic
|
||||
*
|
||||
* @param $fix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rsssl_do_fix( $fix ) {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! rsssl_has_fix( $fix ) && function_exists( $fix ) ) {
|
||||
$completed[] = $fix;
|
||||
$fix();
|
||||
$completed = get_option( 'rsssl_completed_fixes', [] );
|
||||
$completed[] = $fix;
|
||||
update_option( 'rsssl_completed_fixes', $completed );
|
||||
} else if ( $fix && ! function_exists( $fix ) ) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if ( !function_exists('rsssl_has_fix')) {
|
||||
|
||||
/**
|
||||
* Check if this has been fixed already
|
||||
*
|
||||
* @param $fix
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_has_fix( $fix ) {
|
||||
$completed = get_option( 'rsssl_completed_fixes', [] );
|
||||
if ( ! in_array( $fix, $completed ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !function_exists('rsssl_admin_url')) {
|
||||
/**
|
||||
* Get admin url, adjusted for multisite
|
||||
* @param array $args //query args
|
||||
* @param string $path //hash slug for the settings pages (e.g. #dashboard)
|
||||
* @return string
|
||||
*/
|
||||
function rsssl_admin_url(array $args = [], string $path = ''): string {
|
||||
$url = is_multisite() ? network_admin_url('admin.php') : admin_url('admin.php');
|
||||
$args = wp_parse_args($args, ['page' => 'really-simple-security']);
|
||||
return add_query_arg($args, $url) . $path;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !function_exists('rsssl_maybe_clear_transients')) {
|
||||
/**
|
||||
* If the corresponding setting has been changed, clear the test cache and re-run it.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rsssl_maybe_clear_transients( $field_id, $field_value, $prev_value, $field_type ) {
|
||||
if ( $field_id === 'mixed_content_fixer' && $field_value ) {
|
||||
delete_transient( 'rsssl_mixed_content_fixer_detected' );
|
||||
RSSSL()->admin->mixed_content_fixer_detected();
|
||||
}
|
||||
|
||||
//expire in five minutes
|
||||
$headers = get_transient('rsssl_can_use_curl_headers_check');
|
||||
set_transient('rsssl_can_use_curl_headers_check', $headers, 5 * MINUTE_IN_SECONDS);
|
||||
|
||||
//no change
|
||||
if ( $field_value === $prev_value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $field_id === 'disable_http_methods' ) {
|
||||
delete_option( 'rsssl_http_methods_allowed' );
|
||||
rsssl_http_methods_allowed();
|
||||
}
|
||||
if ( $field_id === 'xmlrpc' ) {
|
||||
delete_transient( 'rsssl_xmlrpc_allowed' );
|
||||
rsssl_xmlrpc_allowed();
|
||||
}
|
||||
if ( $field_id === 'disable_indexing' ) {
|
||||
delete_transient( 'rsssl_directory_indexing_status' );
|
||||
rsssl_directory_indexing_allowed();
|
||||
}
|
||||
if ( $field_id === 'block_code_execution_uploads' ) {
|
||||
delete_transient( 'rsssl_code_execution_allowed_status' );
|
||||
rsssl_code_execution_allowed();
|
||||
}
|
||||
if ( $field_id === 'hide_wordpress_version' ) {
|
||||
delete_option( 'rsssl_wp_version_detected' );
|
||||
rsssl_src_contains_wp_version();
|
||||
}
|
||||
if ( $field_id === 'rename_admin_user' ) {
|
||||
delete_transient('rsssl_admin_user_count');
|
||||
rsssl_has_admin_user();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
add_action( "rsssl_after_save_field", 'rsssl_maybe_clear_transients', 100, 4 );
|
||||
}
|
||||
|
||||
if ( !function_exists('rsssl_remove_htaccess_security_edits') ) {
|
||||
/**
|
||||
* Clean up on deactivation
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rsssl_remove_htaccess_security_edits() {
|
||||
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! rsssl_uses_htaccess() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$htaccess_file = RSSSL()->admin->htaccess_file();
|
||||
if ( ! file_exists( $htaccess_file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$start = "\n" . '#Begin Really Simple Security';
|
||||
$end = '#End Really Simple Security' . "\n";
|
||||
$pattern = '/'.$start.'(.*?)'.$end.'/is';
|
||||
|
||||
/**
|
||||
* htaccess in uploads dir
|
||||
*/
|
||||
$upload_dir = wp_get_upload_dir();
|
||||
$htaccess_file_uploads = trailingslashit( $upload_dir['basedir']).'.htaccess';
|
||||
$content_htaccess_uploads = file_exists($htaccess_file_uploads ) ? file_get_contents($htaccess_file_uploads) : '';
|
||||
if (preg_match($pattern, $content_htaccess_uploads) && is_writable( $htaccess_file_uploads )) {
|
||||
$content_htaccess_uploads = preg_replace($pattern, "", $content_htaccess_uploads);
|
||||
file_put_contents( $htaccess_file_uploads, $content_htaccess_uploads );
|
||||
}
|
||||
|
||||
/**
|
||||
* htaccess in root dir
|
||||
*/
|
||||
|
||||
$htaccess_file = RSSSL()->admin->htaccess_file();
|
||||
$content_htaccess = file_get_contents($htaccess_file);
|
||||
//remove old style rules
|
||||
$pattern_1 = "/#\s?BEGIN\s?rlrssslReallySimpleSSL.*?#\s?END\s?rlrssslReallySimpleSSL/s";
|
||||
$pattern_2 = "/#\s?BEGIN\s?Really Simple Security Redirect.*?#\s?END\s?Really Simple Security Redirect/s";
|
||||
$content_htaccess = preg_replace([$pattern_1, $pattern_2], "", $content_htaccess);
|
||||
if (preg_match($pattern, $content_htaccess) && is_writable( $htaccess_file ) ) {
|
||||
$content_htaccess = preg_replace($pattern, "", $content_htaccess);
|
||||
file_put_contents( $htaccess_file, $content_htaccess );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wrap the security headers
|
||||
*/
|
||||
|
||||
if ( ! function_exists('rsssl_wrap_htaccess' ) ) {
|
||||
function rsssl_wrap_htaccess() {
|
||||
if ( !rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! rsssl_uses_htaccess() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( rsssl_get_option('do_not_edit_htaccess') ) {
|
||||
if ( !empty( get_site_option('rsssl_htaccess_error') ) ) {
|
||||
delete_site_option( 'rsssl_htaccess_error' );
|
||||
delete_site_option( 'rsssl_htaccess_rules' );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!rsssl_is_logged_in_rest() &&
|
||||
!RSSSL()->admin->is_settings_page() &&
|
||||
current_filter() !== 'rocket_activation' &&
|
||||
current_filter() !== 'rocket_deactivation'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( get_site_option('rsssl_htaccess_error') ) {
|
||||
delete_site_option( 'rsssl_htaccess_error' );
|
||||
delete_site_option( 'rsssl_htaccess_rules' );
|
||||
}
|
||||
|
||||
if ( get_site_option('rsssl_uploads_htaccess_error') ) {
|
||||
delete_site_option( 'rsssl_uploads_htaccess_error' );
|
||||
delete_site_option( 'rsssl_uploads_htaccess_rules' );
|
||||
}
|
||||
|
||||
if ( get_option('rsssl_updating_htaccess') ) {
|
||||
return;
|
||||
}
|
||||
|
||||
update_option('rsssl_updating_htaccess', true, false );
|
||||
|
||||
$start = '#Begin Really Simple Security';
|
||||
$end = "\n" . '#End Really Simple Security' . "\n";
|
||||
$pattern_content = '/'.$start.'(.*?)'.$end.'/is';
|
||||
|
||||
$pattern = '/'.$start.'.*?'.$end.'/is';
|
||||
/**
|
||||
* htaccess in uploads dir
|
||||
*/
|
||||
$rules_uploads = apply_filters( 'rsssl_htaccess_security_rules_uploads', []);
|
||||
$upload_dir = wp_get_upload_dir();
|
||||
$htaccess_file_uploads = trailingslashit( $upload_dir['basedir']).'.htaccess';
|
||||
|
||||
if ( ! file_exists( $htaccess_file_uploads ) && count($rules_uploads)>0 ) {
|
||||
if ( is_writable(trailingslashit( $upload_dir['basedir'])) ) {
|
||||
file_put_contents($htaccess_file_uploads, '');
|
||||
} else {
|
||||
update_site_option( 'rsssl_uploads_htaccess_error', 'not-writable' );
|
||||
$rules_uploads_result = implode( '', array_column( $rules_uploads, 'rules' ) );
|
||||
update_site_option( 'rsssl_uploads_htaccess_rules', $rules_uploads_result );
|
||||
}
|
||||
}
|
||||
|
||||
if ( file_exists( $htaccess_file_uploads ) ) {
|
||||
$content_htaccess_uploads = file_exists( $htaccess_file_uploads ) ? file_get_contents( $htaccess_file_uploads ) : '';
|
||||
preg_match( $pattern_content, $content_htaccess_uploads, $matches );
|
||||
if ( ( ! empty( $matches[1] ) && empty( $rules_uploads ) ) || ! empty( $rules_uploads ) ) {
|
||||
$rules_uploads_result = '';
|
||||
foreach ( $rules_uploads as $rule_uploads ) {
|
||||
//check if the rule exists outside RSSSL, but not within
|
||||
if ( strpos($content_htaccess_uploads, $rule_uploads['identifier'])!==false && !preg_match('/#Begin Really Simple Security.*?('.preg_quote($rule_uploads['identifier'],'/').').*?#End Really Simple Security/is', $content_htaccess_uploads, $matches) ) {
|
||||
continue;
|
||||
}
|
||||
$rules_uploads_result .= $rule_uploads['rules'];
|
||||
}
|
||||
//We differ between missing rules, and a complete set. As we don't want the replace all rules with just the missing set.
|
||||
|
||||
//should replace if rules is not empty, OR if rules is empty and htaccess is not.
|
||||
$htaccess_has_rsssl_rules = preg_match( '/#Begin Really Simple Security(.*?)#End Really Simple Security/is', $content_htaccess_uploads, $matches);
|
||||
if ( ! empty( $rules_uploads_result ) || $htaccess_has_rsssl_rules ) {
|
||||
if ( ! file_exists( $htaccess_file_uploads ) ) {
|
||||
file_put_contents( $htaccess_file_uploads, '' );
|
||||
}
|
||||
|
||||
$new_rules = empty($rules_uploads_result) ? '' : $start . $rules_uploads_result . $end;
|
||||
if ( ! is_writable( $htaccess_file_uploads ) ) {
|
||||
update_site_option( 'rsssl_uploads_htaccess_error', 'not-writable' );
|
||||
update_site_option( 'rsssl_uploads_htaccess_rules', $rules_uploads_result );
|
||||
} else {
|
||||
delete_site_option( 'rsssl_uploads_htaccess_error' );
|
||||
delete_site_option( 'rsssl_uploads_htaccess_rules' );
|
||||
//remove current rules
|
||||
$content_htaccess_uploads = preg_replace( $pattern, '', $content_htaccess_uploads );
|
||||
//add rules as new block
|
||||
$new_htaccess = $content_htaccess_uploads . "\n" . $new_rules;
|
||||
#clean up
|
||||
if (strpos($new_htaccess, "\n" ."\n" . "\n" )!==false) {
|
||||
$new_htaccess = str_replace("\n" . "\n" . "\n", "\n" ."\n", $new_htaccess);
|
||||
}
|
||||
file_put_contents( $htaccess_file_uploads, $new_htaccess );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* htaccess in root dir
|
||||
*/
|
||||
$rules = apply_filters( 'rsssl_htaccess_security_rules', [] );
|
||||
$htaccess_file = RSSSL()->admin->htaccess_file();
|
||||
|
||||
if ( !file_exists( $htaccess_file ) && count($rules)>0 ) {
|
||||
update_site_option('rsssl_htaccess_error', 'not-exists');
|
||||
$rules_result = implode('',array_column($rules, 'rules'));
|
||||
update_site_option('rsssl_htaccess_rules', $rules_result);
|
||||
}
|
||||
|
||||
if ( file_exists( $htaccess_file ) ) {
|
||||
$content_htaccess = file_get_contents( $htaccess_file );
|
||||
|
||||
//remove old style rules
|
||||
//we do this beforehand, so we don't accidentally assume redirects are already in place
|
||||
$content_htaccess = preg_replace(
|
||||
[
|
||||
"/#\s?BEGIN\s?rlrssslReallySimpleSSL.*?#\s?END\s?rlrssslReallySimpleSSL/s",
|
||||
"/#\s?BEGIN\s?Really Simple Security Redirect.*?#\s?END\s?Really Simple Security Redirect/s"
|
||||
], "", $content_htaccess);
|
||||
preg_match( $pattern_content, $content_htaccess, $matches );
|
||||
|
||||
if ( ( ! empty( $matches[1] ) && empty( $rules ) ) || ! empty( $rules ) ) {
|
||||
$rules_result = '';
|
||||
foreach ( $rules as $rule ) {
|
||||
//check if the rule exists outside RSSSL, but not within
|
||||
if ( strpos($content_htaccess, $rule['identifier'])!==false && !preg_match('/#Begin Really Simple Security.*?('.preg_quote($rule['identifier'],'/').').*?#End Really Simple Security/is', $content_htaccess, $matches) ) {
|
||||
continue;
|
||||
}
|
||||
$rules_result .= $rule['rules'];
|
||||
}
|
||||
//should replace if rules is not empty, OR if rules is empty and htaccess is not.
|
||||
$htaccess_has_rsssl_rules = preg_match( '/#Begin Really Simple Security(.*?)#End Really Simple Security/is', $content_htaccess, $matches );
|
||||
if ( ! empty( $rules_result ) || $htaccess_has_rsssl_rules ) {
|
||||
if ( ! is_writable( $htaccess_file ) ) {
|
||||
update_site_option( 'rsssl_htaccess_error', 'not-writable' );
|
||||
update_site_option( 'rsssl_htaccess_rules', get_site_option( 'rsssl_htaccess_rules' ) . $rules_result );
|
||||
} else {
|
||||
delete_site_option( 'rsssl_htaccess_error' );
|
||||
delete_site_option( 'rsssl_htaccess_rules' );
|
||||
$new_rules = empty($rules_result) ? '' : $start . $rules_result . $end;
|
||||
|
||||
//remove current rules
|
||||
$content_htaccess = preg_replace( $pattern, '', $content_htaccess );
|
||||
|
||||
//add rules as new block
|
||||
if ( strpos($content_htaccess, '# BEGIN WordPress')!==false ) {
|
||||
$new_htaccess = str_replace('# BEGIN WordPress', "\n" . $new_rules.'# BEGIN WordPress', $content_htaccess);
|
||||
} else {
|
||||
$new_htaccess = "\n" . $new_rules . $content_htaccess;
|
||||
}
|
||||
|
||||
#clean up
|
||||
if (strpos($new_htaccess, "\n" ."\n" . "\n" )!==false) {
|
||||
$new_htaccess = str_replace("\n" . "\n" . "\n", "\n" ."\n", $new_htaccess);
|
||||
}
|
||||
|
||||
file_put_contents( $htaccess_file, $new_htaccess );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delete_option('rsssl_updating_htaccess');
|
||||
}
|
||||
add_action('admin_init', 'rsssl_wrap_htaccess' );
|
||||
add_action('rsssl_after_saved_fields', 'rsssl_wrap_htaccess', 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store warning blocks for later use in the mailer
|
||||
*
|
||||
* @param array $changed_fields
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rsssl_gather_warning_blocks_for_mail( array $changed_fields ){
|
||||
if (!rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !rsssl_get_option('send_notifications_email') ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = array_filter($changed_fields, static function($field) {
|
||||
// Check if email_condition exists and call the function, else assume true
|
||||
if ( !isset($field['email']['condition']) ) {
|
||||
$email_condition_result = true;
|
||||
} else if (is_array($field['email']['condition'])) {
|
||||
//rsssl option check
|
||||
$fieldname = array_key_first($field['email']['condition']);
|
||||
$value = $field['email']['condition'][$fieldname];
|
||||
$email_condition_result = rsssl_get_option($fieldname) === $value;
|
||||
} else {
|
||||
//function check
|
||||
$function = $field['email']['condition'];
|
||||
$email_condition_result = function_exists($function) && $function();
|
||||
}
|
||||
return isset($field['email']['message']) && $field['value'] && $email_condition_result;
|
||||
});
|
||||
|
||||
if ( count($fields)===0 ) {
|
||||
return;
|
||||
}
|
||||
$current_fields = get_option('rsssl_email_warning_fields', []);
|
||||
//if it's empty, we start counting time. 30 mins later we send a mail.
|
||||
update_option('rsssl_email_warning_fields_saved', time(), false );
|
||||
|
||||
$current_ids = array_column($current_fields, 'id');
|
||||
foreach ($fields as $field){
|
||||
if ( !in_array( $field['id'], $current_ids, true ) ) {
|
||||
$current_fields[] = $field;
|
||||
}
|
||||
}
|
||||
update_option('rsssl_email_warning_fields', $current_fields, false);
|
||||
}
|
||||
add_action('rsssl_after_saved_fields', 'rsssl_gather_warning_blocks_for_mail', 40);
|
||||
|
||||
/**
|
||||
* Check if server uses .htaccess
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_uses_htaccess() {
|
||||
//when using WP CLI, the get_server check does not work, so we assume .htaccess is being used
|
||||
//and rely on the file exists check to catch if not.
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
return true;
|
||||
}
|
||||
return rsssl_get_server() === 'apache' || rsssl_get_server() === 'litespeed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get htaccess status
|
||||
* @return string | bool
|
||||
*/
|
||||
function rsssl_htaccess_status(){
|
||||
if ( empty(get_site_option('rsssl_htaccess_rules','')) ) {
|
||||
return false;
|
||||
}
|
||||
return get_site_option('rsssl_htaccess_error');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get htaccess status
|
||||
* @return string | bool
|
||||
*/
|
||||
|
||||
function rsssl_uploads_htaccess_status(){
|
||||
if ( empty(get_site_option('rsssl_uploads_htaccess_rules','')) ) {
|
||||
return false;
|
||||
}
|
||||
return get_site_option('rsssl_uploads_htaccess_error');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* Get the wp-config.php path
|
||||
*/
|
||||
function rsssl_find_wp_config_path() {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Allow the wp-config.php path to be overridden via a filter.
|
||||
$filtered_path = apply_filters( 'rsssl_wpconfig_path', '' );
|
||||
|
||||
// If a filtered path is provided, validate it.
|
||||
if ( ! empty( $filtered_path ) ) {
|
||||
$directory = dirname( $filtered_path );
|
||||
|
||||
// Ensure the directory exists before checking for the file.
|
||||
if ( is_dir( $directory ) && file_exists( $filtered_path ) ) {
|
||||
return $filtered_path;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit number of iterations to 10
|
||||
$i = 0;
|
||||
$dir = __DIR__;
|
||||
do {
|
||||
$i ++;
|
||||
if ( file_exists( $dir . "/wp-config.php" ) ) {
|
||||
return $dir . "/wp-config.php";
|
||||
}
|
||||
} while ( ( $dir = realpath( "$dir/.." ) ) && ( $i < 10 ) );
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the server type of the plugin user.
|
||||
*
|
||||
* @return string|bool server type the user is using of false if undetectable.
|
||||
*/
|
||||
|
||||
function rsssl_get_server() {
|
||||
//Allows to override server authentication for testing or other reasons.
|
||||
if ( defined( 'RSSSL_SERVER_OVERRIDE' ) ) {
|
||||
return RSSSL_SERVER_OVERRIDE;
|
||||
}
|
||||
|
||||
$server_raw = strtolower( htmlspecialchars( $_SERVER['SERVER_SOFTWARE'] ) );
|
||||
|
||||
//figure out what server they're using
|
||||
if ( strpos( $server_raw, 'apache' ) !== false ) {
|
||||
return 'apache';
|
||||
} elseif ( strpos( $server_raw, 'nginx' ) !== false ) {
|
||||
return 'nginx';
|
||||
} elseif ( strpos( $server_raw, 'litespeed' ) !== false ) {
|
||||
return 'litespeed';
|
||||
} else { //unsupported server
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* Generate a random prefix
|
||||
*/
|
||||
|
||||
function rsssl_generate_random_string($length) {
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$randomString = '';
|
||||
|
||||
for ( $i = 0; $i < $length; $i++ ) {
|
||||
$index = rand(0, strlen($characters) - 1);
|
||||
$randomString .= $characters[$index];
|
||||
}
|
||||
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*
|
||||
* Get users as string to display
|
||||
*/
|
||||
function rsssl_list_users_where_display_name_is_login_name() {
|
||||
|
||||
if ( !rsssl_user_can_manage() ) {
|
||||
return '';
|
||||
}
|
||||
$users = rsssl_get_users_where_display_name_is_login( true );
|
||||
if ( is_array( $users ) ) {
|
||||
$ext = count($users)>=10 ? '...' : '';
|
||||
$users = array_slice($users, 0, 10);
|
||||
return implode( ', ', $users ).$ext;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user e-mail is verified
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_is_email_verified() {
|
||||
$verificationStatus = get_option('rsssl_email_verification_status');
|
||||
if (rsssl_user_can_manage() && $verificationStatus == 'completed') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// User cannot manage or status is ['started', 'email_changed']
|
||||
return false;
|
||||
}
|
||||
|
||||
function rsssl_remove_prefix_from_version($version) {
|
||||
return preg_replace('/^[^\d]*(?=\d)/', '', $version);
|
||||
}
|
||||
function rsssl_version_compare($version, $compare_to, $operator = null) {
|
||||
$version = rsssl_remove_prefix_from_version($version);
|
||||
$compare_to = rsssl_remove_prefix_from_version($compare_to);
|
||||
return version_compare($version, $compare_to, $operator);
|
||||
}
|
||||
|
||||
function rsssl_maybe_disable_404_blocking() {
|
||||
$option_value = get_option( 'rsssl_homepage_contains_404_resources', false );
|
||||
// Explicitly check for boolean true or string "true"
|
||||
return $option_value === true || $option_value === "true";
|
||||
}
|
||||
|
||||
function rsssl_lock_file_exists() {
|
||||
if ( file_exists( trailingslashit( WP_CONTENT_DIR ) . 'rsssl-safe-mode.lock' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
defined('ABSPATH') or die();
|
||||
class rsssl_hardening {
|
||||
private static $_this;
|
||||
public $risk_naming;
|
||||
function __construct()
|
||||
{
|
||||
if (isset(self::$_this))
|
||||
wp_die(sprintf(__('%s is a singleton class and you cannot create a second instance.', 'really-simple-ssl'), get_class($this)));
|
||||
add_filter( 'rsssl_do_action', array($this, 'hardening_data'), 10, 3 );
|
||||
|
||||
add_action("admin_init", array($this, "load_translations"));
|
||||
self::$_this = $this;
|
||||
}
|
||||
|
||||
public function load_translations(){
|
||||
$this->risk_naming = [
|
||||
'l' => __('low-risk', 'really-simple-ssl'),
|
||||
'm' => __('medium-risk', 'really-simple-ssl'),
|
||||
'h' => __('high-risk', 'really-simple-ssl'),
|
||||
'c' => __('critical', 'really-simple-ssl'),
|
||||
];
|
||||
}
|
||||
|
||||
function hardening_data( array $response, string $action, $data ): array {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return $response;
|
||||
}
|
||||
if ($action === 'hardening_data') {
|
||||
$response = $this->get_stats( $data );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
static function this()
|
||||
{
|
||||
return self::$_this;
|
||||
}
|
||||
|
||||
/* Public Section 2: DataGathering */
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_stats($data): array
|
||||
{
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$vulEnabled = rsssl_get_option('enable_vulnerability_scanner');
|
||||
//now we fetch all plugins that have an update available.
|
||||
|
||||
$stats = [
|
||||
'updates' => $this->getAllUpdatesCount(),
|
||||
'lastChecked' => time(),
|
||||
'riskNaming' => $this->risk_naming,
|
||||
'vulEnabled' => $vulEnabled,
|
||||
];
|
||||
|
||||
$repsonse = [
|
||||
"request_success" => true,
|
||||
'data' => apply_filters('rsssl_vulnerability_data', $stats),
|
||||
];
|
||||
return $repsonse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of all available updates for core, plugins, and themes.
|
||||
*
|
||||
* @return int The count of all available updates.
|
||||
*/
|
||||
public function getAllUpdatesCount(): int
|
||||
{
|
||||
$updatesData = wp_get_update_data();
|
||||
// Checks if the 'counts' key exists in the array and it's an array itself.
|
||||
if (isset($updatesData['counts']) && is_array($updatesData['counts'])) {
|
||||
//we only want core, plugins and themes.
|
||||
$updatesCounts = array_slice($updatesData['counts'], 0, 3);
|
||||
return array_sum($updatesCounts);
|
||||
}
|
||||
// Fallback return in case there's no 'counts' key or it's not an array.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
namespace RSSSL\Security\Includes\Check404;
|
||||
|
||||
class Rsssl_Simple_404_Interceptor {
|
||||
|
||||
private $attempts = 10; // Default attempts threshold
|
||||
private $time_span = 5; // Time span in seconds (5 seconds)
|
||||
private $option_name = 'rsssl_404_cache';
|
||||
private $notice_option = 'rsssl_404_notice_shown';
|
||||
|
||||
public function __construct() {
|
||||
// Load the 404 test class only if the firewall has been enabled
|
||||
if ( rsssl_get_option('enable_firewall') == '1' ) {
|
||||
add_action( 'admin_init', array( $this, 'maybe_load_class_404_test' ), 20, 4 );
|
||||
}
|
||||
|
||||
add_filter( 'rsssl_notices', array( $this, 'show_help_notices' ) );
|
||||
|
||||
if ( defined( 'rsssl_pro' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action( 'template_redirect', array( $this, 'detect_404' ) );
|
||||
}
|
||||
/**
|
||||
* Detect and handle 404 errors.
|
||||
*/
|
||||
public function detect_404(): void {
|
||||
if (is_404()) {
|
||||
if ( get_option( $this->notice_option ) ) {
|
||||
return;
|
||||
}
|
||||
$ip_address = $this->get_ip_address();
|
||||
$current_time = time();
|
||||
|
||||
// Prevent the option from becoming too large
|
||||
$cache = get_option($this->option_name, []);
|
||||
|
||||
if (!isset($cache[$ip_address])) {
|
||||
$cache[$ip_address] = [];
|
||||
}
|
||||
|
||||
$cache[$ip_address][] = $current_time;
|
||||
$cache[$ip_address] = $this->clean_up_old_entries($cache[$ip_address]);
|
||||
|
||||
if (count($cache[$ip_address]) > $this->attempts && !get_option($this->notice_option)) {
|
||||
update_option($this->notice_option, true, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
update_option($this->option_name, $cache, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up old entries based on the given timestamps.
|
||||
*
|
||||
* This method filters the given timestamps array and only keeps the entries where the difference between the current time
|
||||
* and the timestamp is less than the specified time span.
|
||||
*
|
||||
* @param array $timestamps An array of timestamps.
|
||||
*
|
||||
* @return array The cleaned up timestamps array.
|
||||
*/
|
||||
private function clean_up_old_entries($timestamps): array {
|
||||
$current_time = time();
|
||||
return array_filter($timestamps, function($timestamp) use ($current_time) {
|
||||
return ($current_time - $timestamp) < $this->time_span;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the IP address of the client.
|
||||
*
|
||||
* This method checks for the IP address in the following order:
|
||||
* 1. HTTP_CLIENT_IP: Represents the IP address of the client if the client is a shared internet device.
|
||||
* 2. HTTP_X_FORWARDED_FOR: Represents the IP address of the client if the client is accessing the server through a proxy server.
|
||||
* 3. REMOTE_ADDR: Represents the IP address of the client if the client is accessing the server directly.
|
||||
*
|
||||
* @return string The IP address of the client.
|
||||
*/
|
||||
private function get_ip_address(): string {
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
return $_SERVER['HTTP_CLIENT_IP'];
|
||||
}
|
||||
|
||||
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
return $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
}
|
||||
|
||||
if (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
return $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a help notice for 404 detection warning.
|
||||
*
|
||||
* @param array $notices The existing notices array.
|
||||
*
|
||||
* @return array Updated notices array with 404 detection warning notice.
|
||||
*/
|
||||
public function show_help_notices(array $notices): array {
|
||||
if (get_option($this->notice_option)) {
|
||||
$message = __('We detected suspected bots triggering large numbers of 404 errors on your site.', 'really-simple-ssl');
|
||||
$notice = [
|
||||
'callback' => '_true_',
|
||||
'score' => 1,
|
||||
'show_with_options' => ['enable_404_detection'],
|
||||
'output' => [
|
||||
'true' => [
|
||||
'msg' => $message,
|
||||
'icon' => 'warning',
|
||||
'type' => 'warning',
|
||||
'dismissible' => true,
|
||||
'admin_notice' => false,
|
||||
'highlight_field_id' => 'enable_firewall',
|
||||
'plusone' => true,
|
||||
'url' => 'https://really-simple-ssl.com/suspected-bots-causing-404-errors/',
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$notices['404_detection_warning'] = $notice;
|
||||
}
|
||||
return $notices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $field
|
||||
* @param $value
|
||||
* @param $old_value
|
||||
* @param $option_name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_load_class_404_test() {
|
||||
if ( ! get_option( 'rsssl_homepage_contains_404_resources' ) ) {
|
||||
Rsssl_Test_404::get_instance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Rsssl_Simple_404_Interceptor();
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
namespace RSSSL\Security\Includes\Check404;
|
||||
|
||||
class Rsssl_Test_404 {
|
||||
// Static instance property
|
||||
public static $instance = null;
|
||||
|
||||
// Private constructor to prevent direct instantiation
|
||||
private function __construct() {
|
||||
// Immediately check if there are resources to process and handle them
|
||||
$resources = get_option( 'rsssl_404_resources_to_check' );
|
||||
$found_404_option_value = get_option( 'rsssl_homepage_contains_404_resources', false );
|
||||
$found_404s = $found_404_option_value === true || $found_404_option_value === "true";
|
||||
|
||||
if ( ! empty( $resources ) && ! $found_404s ) {
|
||||
// Trigger chunk processing if resources are pending
|
||||
$this->process_404_resources_chunk();
|
||||
}
|
||||
|
||||
$this->fetch_and_check_homepage_resources();
|
||||
|
||||
}
|
||||
|
||||
// Static method to get the single instance of the class
|
||||
public static function get_instance() {
|
||||
if ( self::$instance === null ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
// Process resources in chunks
|
||||
public function process_404_resources_chunk() {
|
||||
$resources = get_option( 'rsssl_404_resources_to_check' );
|
||||
if ( empty( $resources ) ) {
|
||||
update_option( 'rsssl_homepage_contains_404_resources', 'false' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process a chunk of the resources (e.g., 2 at a time)
|
||||
$chunk_size = 2;
|
||||
$resources_chunk = array_splice( $resources, 0, $chunk_size );
|
||||
|
||||
$result = $this->process_404_resources( $resources_chunk );
|
||||
|
||||
// Update the remaining resources back to the option
|
||||
if ( ! empty( $resources ) ) {
|
||||
update_option( 'rsssl_404_resources_to_check', $resources );
|
||||
return 'processing';
|
||||
} else {
|
||||
// All resources have been processed
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check homepage and handle 404s
|
||||
public static function homepage_contains_404_resources() {
|
||||
$found_404_option_value = get_option( 'rsssl_homepage_contains_404_resources', false );
|
||||
if ( $found_404_option_value === true || $found_404_option_value === "true" ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$resources = get_option( 'rsssl_404_resources_to_check' );
|
||||
if ( ! empty( $resources ) ) {
|
||||
// If resources are available to check, process them immediately
|
||||
return self::get_instance()->process_404_resources_chunk();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Function to fetch homepage resources and check for 404 errors
|
||||
public function fetch_and_check_homepage_resources() {
|
||||
|
||||
if ( get_option('rsssl_homepage_contains_404_resources') ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$site_url = trailingslashit( site_url() );
|
||||
|
||||
$response = wp_remote_get( $site_url );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
update_option( 'rsssl_homepage_contains_404_resources', false );
|
||||
return false;
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
if ( $status_code == 404 ) {
|
||||
update_option( 'rsssl_homepage_contains_404_resources', true );
|
||||
return true;
|
||||
}
|
||||
|
||||
// Patterns to match img, script, link tags
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$patterns = array(
|
||||
'/<img[^>]+src=([\'"])?((.*?)\1)/i',
|
||||
'/<script[^>]+src=([\'"])?((.*?)\1)/i',
|
||||
'/<link[^>]+href=([\'"])?((.*?)\1)/i'
|
||||
);
|
||||
|
||||
$resources = array();
|
||||
foreach ( $patterns as $pattern ) {
|
||||
if ( preg_match_all( $pattern, $body, $matches ) ) {
|
||||
foreach ( $matches[2] as $resource_url ) {
|
||||
$resource_url = esc_url_raw( $resource_url );
|
||||
if ( strpos( $resource_url, $site_url ) !== false ) {
|
||||
$resources[] = $resource_url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $resources ) > 2 ) {
|
||||
update_option( 'rsssl_404_resources_to_check', $resources );
|
||||
return $this->process_404_resources_chunk();
|
||||
} else {
|
||||
if ( empty( $resources ) ) {
|
||||
update_option( 'rsssl_homepage_contains_404_resources', 'false' );
|
||||
return false;
|
||||
}
|
||||
|
||||
update_option( 'rsssl_404_resources_to_check', $resources );
|
||||
// Process all resources if fewer than 5
|
||||
return $this->process_404_resources( $resources );
|
||||
}
|
||||
}
|
||||
|
||||
// Function to process a list of resources and check for 404 errors
|
||||
private function process_404_resources( $resources ) {
|
||||
$not_found_resources = array();
|
||||
|
||||
foreach ( $resources as $resource_url ) {
|
||||
$resource_response = wp_remote_head( $resource_url );
|
||||
if ( is_wp_error( $resource_response ) ) {
|
||||
$not_found_resources[] = $resource_url . ' (Error: ' . $resource_response->get_error_message() . ')';
|
||||
} else {
|
||||
$resource_status = wp_remote_retrieve_response_code( $resource_response );
|
||||
if ( $resource_status == 404 ) {
|
||||
$not_found_resources[] = $resource_url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $not_found_resources ) ) {
|
||||
update_option( 'rsssl_homepage_contains_404_resources', 'false' );
|
||||
return false;
|
||||
} else {
|
||||
update_option( 'rsssl_homepage_contains_404_resources', 'true' );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php // You don't belong here.
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) or die();
|
||||
global $rsssl_integrations_list;
|
||||
$rsssl_integrations_list = apply_filters( 'rsssl_integrations', array(
|
||||
'user-registration' => array(
|
||||
'folder' => 'wordpress',
|
||||
'option_id' => 'disable_anyone_can_register',
|
||||
),
|
||||
|
||||
'file-editing' => array(
|
||||
'folder' => 'wordpress',
|
||||
'option_id' => 'disable_file_editing',
|
||||
),
|
||||
|
||||
'hide-wp-version' => array(
|
||||
'folder' => 'wordpress',
|
||||
'option_id' => 'hide_wordpress_version',
|
||||
),
|
||||
|
||||
'user-enumeration' => array(
|
||||
'folder' => 'wordpress',
|
||||
'option_id' => 'disable_user_enumeration',
|
||||
),
|
||||
|
||||
'block-code-execution-uploads' => array(
|
||||
'folder' => 'wordpress',
|
||||
'impact' => 'medium',
|
||||
'risk' => 'low',
|
||||
'option_id' => 'block_code_execution_uploads',
|
||||
),
|
||||
|
||||
'prevent-login-info-leakage' => array(
|
||||
'folder' => 'wordpress',
|
||||
'option_id' => 'disable_login_feedback',
|
||||
),
|
||||
'disable-indexing' => array(
|
||||
'folder' => 'server',
|
||||
'option_id' => 'disable_indexing',
|
||||
'has_deactivation' => true,
|
||||
),
|
||||
|
||||
'rename-admin-user' => array(
|
||||
'folder' => 'wordpress',
|
||||
'option_id' => 'rename_admin_user',
|
||||
),
|
||||
'display-name-is-login-name' => array(
|
||||
'folder' => 'wordpress',
|
||||
'option_id' => 'block_display_is_login',
|
||||
),
|
||||
|
||||
'disable-xmlrpc' => array(
|
||||
'folder' => 'wordpress',
|
||||
'option_id' => 'disable_xmlrpc',
|
||||
'always_include' => false,
|
||||
),
|
||||
'vulnerabilities' => array(
|
||||
'folder' => 'wordpress',
|
||||
'option_id' => 'enable_vulnerability_scanner',
|
||||
'admin_only' => true,
|
||||
),
|
||||
'class-rsssl-two-factor' => array(
|
||||
'folder' => 'wordpress/two-fa',
|
||||
'option_id' => 'login_protection_enabled',
|
||||
'always_include' => false,
|
||||
),
|
||||
) );
|
||||
|
||||
/**
|
||||
* Check if this plugin's integration is enabled
|
||||
* @param string $plugin
|
||||
* @param array $details
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
if ( ! function_exists('rsssl_is_integration_enabled') ) {
|
||||
function rsssl_is_integration_enabled( $plugin, $details ) {
|
||||
global $rsssl_integrations_list;
|
||||
if ( ! array_key_exists( $plugin, $rsssl_integrations_list ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( $details['always_include'] ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//if an integration was just enabled, we keep it enabled until it removes itself from the list.
|
||||
//only for admin users
|
||||
if ( rsssl_is_in_deactivation_list( $plugin ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$field_id = $details['option_id'] ?? false;
|
||||
if ( ! $field_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$field_value = $details['option_value'] ?? false;
|
||||
$stored_value = rsssl_get_option( $field_id );
|
||||
if ( $field_value ) {
|
||||
$invert = false;
|
||||
$condition_met = false;
|
||||
if (strpos($field_value, 'NOT') === 0) {
|
||||
$invert = true;
|
||||
$field_value = str_replace( 'NOT ', '', $field_value);
|
||||
}
|
||||
if ( $stored_value === $field_value ) {
|
||||
$condition_met = true;
|
||||
}
|
||||
if ( $invert ) {
|
||||
$condition_met = !$condition_met;
|
||||
}
|
||||
return $condition_met;
|
||||
} else if ( $stored_value ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* code loaded without privileges to allow integrations between plugins and services, when enabled.
|
||||
*/
|
||||
if ( ! function_exists('rsssl_integrations') ) {
|
||||
function rsssl_integrations() {
|
||||
|
||||
$safe_mode = defined( 'RSSSL_SAFE_MODE' ) && RSSSL_SAFE_MODE;
|
||||
|
||||
global $rsssl_integrations_list;
|
||||
foreach ( $rsssl_integrations_list as $plugin => $details ) {
|
||||
$details = wp_parse_args( $details,
|
||||
[
|
||||
'option_id' => false,
|
||||
'always_include' => false,
|
||||
'folder' => false,
|
||||
'admin_only' => false,
|
||||
'is_pro' => false,
|
||||
]
|
||||
);
|
||||
|
||||
if ( $details['admin_only'] && ! rsssl_admin_logged_in() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( rsssl_is_integration_enabled( $plugin, $details ) ) {
|
||||
$path = apply_filters( 'rsssl_integrations_path', rsssl_path, $plugin, $details );
|
||||
|
||||
$file = $path . 'security/' . $details['folder'] . "/" . $plugin . '.php';
|
||||
if ( ! file_exists( $file ) && $safe_mode ) {
|
||||
continue;
|
||||
}
|
||||
require_once( $file );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action( 'plugins_loaded', 'rsssl_integrations', 10 );
|
||||
add_action( 'rsssl_after_saved_fields', 'rsssl_integrations', 20 );
|
||||
|
||||
/**
|
||||
* Check if a plugin is on the deactivation list
|
||||
*
|
||||
* @param string $plugin
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
if ( ! function_exists('rsssl_is_in_deactivation_list') ) {
|
||||
function rsssl_is_in_deactivation_list( string $plugin ): bool {
|
||||
if ( ! is_admin() || ! is_user_logged_in() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! is_array( get_option( 'rsssl_deactivate_list', [] ) ) ) {
|
||||
delete_option( 'rsssl_deactivate_list' );
|
||||
}
|
||||
|
||||
return in_array( $plugin, get_option( 'rsssl_deactivate_list', [] ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php defined( 'ABSPATH' ) or die();
|
||||
/**
|
||||
* Convert htaccess rules to html friendly layout
|
||||
*
|
||||
* @param string $code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function rsssl_parse_htaccess_to_html( string $code): string {
|
||||
if ( strpos($code, "\n")===0 ) {
|
||||
$code = preg_replace('/\n/', '', $code, 1);
|
||||
}
|
||||
//split into linebreak separated array, so we can run esc_html on the result
|
||||
$code = preg_replace('/\n/', '--br--', $code, 1);
|
||||
$code = preg_replace('/<br>/', '--br--', $code, 1);
|
||||
$code_arr = explode('--br--', $code);
|
||||
$code_arr = array_map('esc_html', $code_arr);
|
||||
$code = implode('<br>', $code_arr);
|
||||
return '<br><code>' . $code . '</code><br>';
|
||||
}
|
||||
|
||||
function rsssl_general_security_notices( $notices ) {
|
||||
$code = rsssl_parse_htaccess_to_html( get_site_option( 'rsssl_htaccess_rules', '' ) );
|
||||
$uploads_code = rsssl_parse_htaccess_to_html( get_site_option( 'rsssl_uploads_htaccess_rules', '' ) );
|
||||
$open_hardening_count = rsssl_count_open_hardening_features();
|
||||
|
||||
$notices['htaccess_status'] = array(
|
||||
'callback' => 'rsssl_htaccess_status',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'not-writable' => array(
|
||||
'title' => __( ".htaccess not writable", "really-simple-ssl" ),
|
||||
'msg' => __( "An option that requires the .htaccess file is enabled, but the file is not writable.", "really-simple-ssl" ) . ' ' . __( "Please add the following lines to your .htaccess, or set it to writable:", "really-simple-ssl" ) . $code,
|
||||
'icon' => 'warning',
|
||||
'dismissible' => true,
|
||||
'plusone' => true,
|
||||
'url' => 'manual/editing-htaccess/',
|
||||
),
|
||||
'not-exists' => array(
|
||||
'title' => __( ".htaccess does not exist", "really-simple-ssl" ),
|
||||
'msg' => __( "An option that requires the .htaccess file is enabled, but the file does not exist.", "really-simple-ssl" ) . ' ' . __( "Please add the following lines to your .htaccess, or set it to writable:", "really-simple-ssl" ) . $code,
|
||||
'icon' => 'warning',
|
||||
'dismissible' => true,
|
||||
'plusone' => true,
|
||||
'url' => 'manual/editing-htaccess/',
|
||||
),
|
||||
),
|
||||
'show_with_options' => [
|
||||
'disable_indexing',
|
||||
'redirect'
|
||||
]
|
||||
);
|
||||
|
||||
$notices['htaccess_status_uploads'] = array(
|
||||
'callback' => 'rsssl_uploads_htaccess_status',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'not-writable' => array(
|
||||
'title' => __( ".htaccess in uploads not writable", "really-simple-ssl" ),
|
||||
'msg' => __( "An option that requires the .htaccess file in the uploads directory is enabled, but the file is not writable.", "really-simple-ssl" ) . ' ' . __( "Please add the following lines to your .htaccess, or set it to writable:", "really-simple-ssl" ) . $uploads_code,
|
||||
'icon' => 'warning',
|
||||
'dismissible' => true,
|
||||
'plusone' => true,
|
||||
'url' => 'manual/editing-htaccess/',
|
||||
),
|
||||
),
|
||||
'show_with_options' => [
|
||||
'block_code_execution_uploads',
|
||||
]
|
||||
);
|
||||
|
||||
$notices['display_name_is_login_exists'] = array(
|
||||
'condition' => [ 'rsssl_get_users_where_display_name_is_login' ],
|
||||
'callback' => '_true_',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'true' => array(
|
||||
'url' => 'manual/login-and-display-names-should-be-different-for-wordpress/',
|
||||
'msg' => __( "We have detected administrator roles where the login and display names are the same.", "really-simple-ssl" ) . " <b>" . rsssl_list_users_where_display_name_is_login_name() . "</b>",
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$notices['new_username_empty'] = array(
|
||||
'condition' => [ 'rsssl_has_admin_user', 'option_rename_admin_user', 'NOT rsssl_new_username_valid' ],
|
||||
'callback' => '_true_',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'true' => array(
|
||||
'highlight_field_id' => 'rename_admin_user',
|
||||
'title' => __( "Username", "really-simple-ssl" ),
|
||||
'msg' => __( "Rename admin user enabled: Please choose a new username of at least 3 characters, which is not in use yet.", "really-simple-ssl" ),
|
||||
'icon' => 'warning',
|
||||
'dismissible' => true,
|
||||
),
|
||||
),
|
||||
'show_with_options' => [
|
||||
'new_admin_user_login',
|
||||
],
|
||||
);
|
||||
|
||||
$notices['enable_vulnerability_scanner'] = array(
|
||||
'callback' => 'option_enable_vulnerability_scanner',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'false' => array(
|
||||
'highlight_field_id' => 'enable_vulnerability_scanner',
|
||||
'msg' => __( "Enable the Vulnerability scan to detect possible vulnerabilities.", 'really-simple-ssl' ),
|
||||
'icon' => 'open',
|
||||
'admin_notice' => false,
|
||||
'dismissible' => true,
|
||||
'plusone' => false,
|
||||
),
|
||||
'true' => array(
|
||||
'msg' => __( "Vulnerability scanning is enabled.", 'really-simple-ssl' ),
|
||||
'icon' => 'success',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$notices['count_open_hardening_features'] = array(
|
||||
'callback' => 'rsssl_has_open_hardening_features',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'true' => array(
|
||||
'highlight_field_id' => 'disable_anyone_can_register',
|
||||
'msg' => sprintf(
|
||||
_n(
|
||||
"You have %s open hardening feature.",
|
||||
"You have %s open hardening features.",
|
||||
$open_hardening_count,
|
||||
"really-simple-ssl"
|
||||
),
|
||||
$open_hardening_count
|
||||
),
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
),
|
||||
'false' => array(
|
||||
'msg' => __( "All recommended hardening features enabled.", "really-simple-ssl" ),
|
||||
'icon' => 'success',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$notices['lock_file_exists'] = array(
|
||||
'callback' => 'rsssl_lock_file_exists',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'true' => array(
|
||||
'msg' => __( 'The Firewall, LLA and 2FA are currently inactive, as you have activated Safe Mode with the rsssl-safe-mode.lock file. Remove the file from your /wp-content folder after you have finished debugging.', 'really-simple-ssl' ),
|
||||
'icon' => 'warning',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $notices;
|
||||
}
|
||||
add_filter('rsssl_notices', 'rsssl_general_security_notices');
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
defined('ABSPATH') or die();
|
||||
class REALLY_SIMPLE_SECURITY
|
||||
{
|
||||
private static $instance;
|
||||
public $firewall_manager;
|
||||
public $hardening;
|
||||
/**
|
||||
* Components array, so we can access singleton classes which are dynamically added, from anywhere.
|
||||
* @var
|
||||
*/
|
||||
public $components;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
if (!defined('RSSSL_SAFE_MODE') && file_exists(trailingslashit(WP_CONTENT_DIR) . 'rsssl-safe-mode.lock')) {
|
||||
define('RSSSL_SAFE_MODE', true);
|
||||
}
|
||||
}
|
||||
|
||||
public static function instance()
|
||||
{
|
||||
if (!isset(self::$instance) && !(self::$instance instanceof REALLY_SIMPLE_SECURITY)) {
|
||||
self::$instance = new REALLY_SIMPLE_SECURITY;
|
||||
self::$instance->includes();
|
||||
if ( rsssl_admin_logged_in() ) {
|
||||
self::$instance->firewall_manager = new rsssl_firewall_manager();
|
||||
self::$instance->hardening = new rsssl_hardening();
|
||||
}
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function includes()
|
||||
{
|
||||
|
||||
$path = rsssl_path.'security/';
|
||||
require_once( $path . 'integrations.php' );
|
||||
require_once( $path . 'hardening.php' );
|
||||
require_once( $path . 'cron.php' );
|
||||
require_once( $path . 'includes/check404/class-rsssl-simple-404-interceptor.php' );
|
||||
|
||||
/**
|
||||
* Load only on back-end
|
||||
*/
|
||||
if ( rsssl_admin_logged_in() ) {
|
||||
require_once( $path . 'functions.php' );
|
||||
require_once( $path . 'deactivate-integration.php' );
|
||||
require_once( $path . 'firewall-manager.php' );
|
||||
require_once( $path . 'tests.php' );
|
||||
require_once( $path . 'notices.php' );
|
||||
require_once( $path . 'sync-settings.php' );
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function RSSSL_SECURITY()
|
||||
{
|
||||
return REALLY_SIMPLE_SECURITY::instance();
|
||||
}
|
||||
add_action('plugins_loaded', 'RSSSL_SECURITY', 9);
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) or die();
|
||||
if ( rsssl_is_in_deactivation_list('disable-indexing') ){
|
||||
rsssl_remove_from_deactivation_list('disable-indexing');
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable indexing
|
||||
* @param array $rules
|
||||
* @return []
|
||||
*/
|
||||
|
||||
function rsssl_disable_indexing_rules( $rules ) {
|
||||
$rules[] = ['rules' => "\n" . 'Options -Indexes', 'identifier' => 'Options -Indexes'];
|
||||
return $rules;
|
||||
}
|
||||
add_filter('rsssl_htaccess_security_rules', 'rsssl_disable_indexing_rules');
|
||||
|
||||
/**
|
||||
* Dropped suggestions for indexing in NGINX as indexing in NGINX is by default disabled.
|
||||
*/
|
||||
@@ -0,0 +1 @@
|
||||
<?php // You don't belong here.
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
defined('ABSPATH') or die();
|
||||
/**
|
||||
* Conditionally we can decide to disable fields, add comments, and manipulate the value here
|
||||
* @param array $field
|
||||
* @param string $field_id
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
|
||||
function rsssl_disable_fields( $field, $field_id ) {
|
||||
/**
|
||||
* If a feature is already enabled, but not by RSSSL, we can simply check for that feature, and if the option in RSSSL is active.
|
||||
* We set is as true, but disabled. Because our React interface only updates changed option, and this option never changes, this won't get set to true in the database.
|
||||
*/
|
||||
if ( $field_id === 'change_debug_log_location' ) {
|
||||
if ( ! rsssl_debug_log_file_exists_in_default_location() ) {
|
||||
if ( ! rsssl_is_debugging_enabled() ) {
|
||||
if ( ! $field['value'] ) {
|
||||
$field['value'] = true;
|
||||
$field['disabled'] = true;
|
||||
}
|
||||
} else if ( ! rsssl_debug_log_value_is_default() ) {
|
||||
if ( ! $field['value'] ) {
|
||||
$field['value'] = true;
|
||||
$field['disabled'] = true;
|
||||
}
|
||||
}
|
||||
//if not the default location
|
||||
$location = strstr( rsssl_get_debug_log_value(), 'wp-content' );
|
||||
if ( ! empty( $location ) && rsssl_is_debugging_enabled() && ! rsssl_debug_log_value_is_default() ) {
|
||||
$field['help'] = [
|
||||
'label' => 'default',
|
||||
'title' => __( "Debug.log", 'really-simple-ssl' ),
|
||||
'text' => __( "Changed debug.log location to:", 'really-simple-ssl' ) . $location,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( $field_id === 'disable_indexing' ) {
|
||||
if ( ! rsssl_directory_indexing_allowed() && ! ( $field['value'] ?? false ) ) {
|
||||
$field['value'] = true;
|
||||
$field['disabled'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $field_id === 'disable_anyone_can_register' ) {
|
||||
if ( ! get_option( 'users_can_register' ) && ! ( $field['value'] ?? false ) ) {
|
||||
$field['value'] = true;
|
||||
$field['disabled'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $field_id === 'disable_http_methods' ) {
|
||||
if ( ! rsssl_http_methods_allowed() && ! ( $field['value'] ?? false ) ) {
|
||||
$field['value'] = true;
|
||||
$field['disabled'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $field_id === 'disable_file_editing' ) {
|
||||
if ( defined( 'DISALLOW_FILE_EDIT' ) && DISALLOW_FILE_EDIT && ! ( $field['value'] ?? false ) ) {
|
||||
$field['value'] = true;
|
||||
$field['disabled'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $field_id === 'block_code_execution_uploads' ) {
|
||||
if ( ! rsssl_code_execution_allowed() && ! ( $field['value'] ?? false ) ) {
|
||||
$field['value'] = true;
|
||||
$field['disabled'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $field_id === 'disable_xmlrpc' ) {
|
||||
if ( ! rsssl_xmlrpc_enabled() && ! ( $field['value'] ?? false ) ) {
|
||||
$field['value'] = true;
|
||||
$field['disabled'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $field_id === 'rename_db_prefix' ) {
|
||||
if ( ! rsssl_is_default_wp_prefix() && ! ( $field['value'] ?? false ) ) {
|
||||
$field['value'] = true;
|
||||
$field['disabled'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $field;
|
||||
}
|
||||
add_filter('rsssl_field', 'rsssl_disable_fields', 10, 2);
|
||||
@@ -0,0 +1,507 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) or die();
|
||||
|
||||
/**
|
||||
* Check if XML-RPC requests are allowed on this site
|
||||
* POST a request, if the request returns a 200 response code the request is allowed
|
||||
*/
|
||||
function rsssl_xmlrpc_allowed()
|
||||
{
|
||||
$allowed = get_transient( 'rsssl_xmlrpc_allowed' );
|
||||
if ( !$allowed ) {
|
||||
$allowed = 'allowed';
|
||||
if ( function_exists( 'curl_init' ) ) {
|
||||
//set a default, in case of time out
|
||||
set_transient( 'rsssl_xmlrpc_allowed', 'no-response', DAY_IN_SECONDS );
|
||||
$url = site_url() . '/xmlrpc.php';
|
||||
$ch = curl_init($url);
|
||||
// XML-RPC listMethods call
|
||||
// Valid XML-RPC request
|
||||
$xmlstring = '<?xml version="1.0" encoding="utf-8"?>
|
||||
<methodCall>
|
||||
<methodName>system.listMethods</methodName>
|
||||
<params></params>
|
||||
</methodCall>';
|
||||
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
// Post string
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $xmlstring );
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 3); //timeout in seconds
|
||||
curl_exec($ch);
|
||||
$response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($response_code === 200) {
|
||||
$allowed = 'allowed';
|
||||
} else {
|
||||
$allowed = 'not-allowed';
|
||||
}
|
||||
}
|
||||
set_transient( 'rsssl_xmlrpc_allowed', $allowed, DAY_IN_SECONDS );
|
||||
}
|
||||
return $allowed === 'allowed';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* Test if HTTP methods are allowed
|
||||
*/
|
||||
function rsssl_http_methods_allowed()
|
||||
{
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$methods = [
|
||||
'GET',
|
||||
'POST',
|
||||
'PUT',
|
||||
'DELETE',
|
||||
'HEAD',
|
||||
'OPTIONS',
|
||||
'CONNECT',
|
||||
'TRACE',
|
||||
'TRACK',
|
||||
'PATCH',
|
||||
'COPY',
|
||||
'LINK',
|
||||
'UNLINK',
|
||||
'PURGE',
|
||||
'LOCK',
|
||||
'UNLOCK',
|
||||
'PROPFIND',
|
||||
'VIEW',
|
||||
];
|
||||
$tested = get_option( 'rsssl_http_methods_allowed' );
|
||||
|
||||
#if the option was reset, start couting from 0
|
||||
if ( !$tested ){
|
||||
delete_option('rsssl_last_tested_http_method');
|
||||
}
|
||||
$last_tested = get_option('rsssl_last_tested_http_method', -1);
|
||||
|
||||
$nr_of_tests_on_batch = 4;
|
||||
if ( !$tested || ( $last_tested < count($methods)-1 ) ) {
|
||||
$tested = get_option( 'rsssl_http_methods_allowed', [] );
|
||||
$next_test = $last_tested+1;
|
||||
|
||||
$test_methods = array_slice($methods, $next_test, $nr_of_tests_on_batch, true);
|
||||
update_option('rsssl_last_tested_http_method', $last_tested+$nr_of_tests_on_batch, false);
|
||||
|
||||
foreach ( $test_methods as $method ) {
|
||||
#set a default, in case a timeout occurs
|
||||
$tested['not-allowed'][] = $method;
|
||||
update_option( 'rsssl_http_methods_allowed', $tested, false );
|
||||
|
||||
if ( function_exists( 'curl_init' ) ) {
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt( $ch, CURLOPT_URL, site_url() );
|
||||
curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $method );
|
||||
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
|
||||
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
|
||||
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
|
||||
curl_setopt( $ch, CURLOPT_HEADER, true );
|
||||
curl_setopt( $ch, CURLOPT_NOBODY, true );
|
||||
curl_setopt( $ch, CURLOPT_VERBOSE, true );
|
||||
curl_setopt( $ch, CURLOPT_TIMEOUT, 3 ); //timeout in seconds
|
||||
curl_exec( $ch );
|
||||
|
||||
#if there are no errors, the request is allowed
|
||||
if ( ! curl_errno( $ch ) ) {
|
||||
//remove the not allowed entry
|
||||
$not_allowed_index = array_search( $method, $tested['not-allowed'], true );
|
||||
if ( $not_allowed_index !== false ) {
|
||||
unset( $tested['not-allowed'][ $not_allowed_index ] );
|
||||
}
|
||||
$tested['allowed'][] = $method;
|
||||
}
|
||||
curl_close( $ch );
|
||||
update_option( 'rsssl_http_methods_allowed', $tested, false );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ( !empty($tested['allowed'])) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*
|
||||
* Check if DB has default wp_ prefix
|
||||
*/
|
||||
|
||||
function rsssl_is_default_wp_prefix() {
|
||||
global $wpdb;
|
||||
if ( $wpdb->prefix === 'wp_' ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function rsssl_xmlrpc_enabled(){
|
||||
return apply_filters('xmlrpc_enabled', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*
|
||||
* Check if user admin exists
|
||||
*/
|
||||
|
||||
function rsssl_has_admin_user() {
|
||||
if ( !rsssl_user_can_manage() ) {
|
||||
return false;
|
||||
}
|
||||
//transient is more persistent then wp cache set
|
||||
$count = get_transient('rsssl_admin_user_count');
|
||||
//get from cache, but not on settings page
|
||||
if ( $count === false || RSSSL()->admin->is_settings_page() ){
|
||||
//use wp_cache_get to prevent duplicate queries in one pageload
|
||||
$count = wp_cache_get('rsssl_admin_user_count', 'really-simple-ssl');
|
||||
if ( $count === false ) {
|
||||
global $wpdb;
|
||||
$count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->base_prefix}users WHERE user_login = 'admin'" );
|
||||
wp_cache_set('rsssl_admin_user_count', $count, 'really-simple-ssl', HOUR_IN_SECONDS );
|
||||
}
|
||||
set_transient('rsssl_admin_user_count', $count, HOUR_IN_SECONDS);
|
||||
}
|
||||
|
||||
return $count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if username is valid for use
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_new_username_valid(): bool {
|
||||
|
||||
$new_user_login = trim(sanitize_user(rsssl_get_option('new_admin_user_login')));
|
||||
if ( $new_user_login === 'admin' ) {
|
||||
return false;
|
||||
}
|
||||
$user_exists = get_user_by('login', $new_user_login);
|
||||
if ( $user_exists ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_string($new_user_login) && strlen($new_user_login)>2;
|
||||
}
|
||||
|
||||
/**
|
||||
* For backward compatibility we need to wrap this function, as older versions do not have this function (<5.6)
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_wp_is_application_passwords_available(){
|
||||
if ( function_exists('wp_is_application_passwords_available') ) {
|
||||
return wp_is_application_passwords_available();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users where display name is the same as login
|
||||
*
|
||||
* @param bool $return_users
|
||||
*
|
||||
* @return bool | array
|
||||
*
|
||||
*/
|
||||
|
||||
function rsssl_get_users_where_display_name_is_login( $return_users=false ) {
|
||||
$found_users = [];
|
||||
$users = get_transient('rsssl_admin_users');
|
||||
if ( !$users ){
|
||||
$args = array(
|
||||
'role' => 'administrator',
|
||||
);
|
||||
$users = get_users( $args );
|
||||
set_transient('rsssl_admin_users', $users, HOUR_IN_SECONDS);
|
||||
}
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
if ($user->display_name === $user->user_login) {
|
||||
$found_users[] = $user->user_login;
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe return users in integration
|
||||
if ( $return_users ) {
|
||||
return $found_users;
|
||||
}
|
||||
|
||||
if ( count($found_users) > 0 ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if debugging in WordPress is enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_is_debugging_enabled() {
|
||||
return ( defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG );
|
||||
}
|
||||
|
||||
function rsssl_debug_log_value_is_default(){
|
||||
$value = rsssl_get_debug_log_value();
|
||||
|
||||
return (string) $value === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value of debug_log constant
|
||||
* Please note that for a value 'true', you should check for the string value === 'true'
|
||||
* @return bool|string
|
||||
*/
|
||||
|
||||
function rsssl_get_debug_log_value(){
|
||||
if ( !defined('WP_DEBUG_LOG')) {
|
||||
return false;
|
||||
}
|
||||
$wpconfig_path = rsssl_find_wp_config_path();
|
||||
|
||||
if ( !$wpconfig_path ) {
|
||||
return false;
|
||||
}
|
||||
$wpconfig = file_get_contents( $wpconfig_path );
|
||||
|
||||
// Get WP_DEBUG_LOG declaration
|
||||
$regex = "/^\s*define\([ ]{0,2}[\'|\"]WP_DEBUG_LOG[\'|\"][ ]{0,2},[ ]{0,2}(.*)[ ]{0,2}\);/m";
|
||||
preg_match( $regex, $wpconfig, $matches );
|
||||
if ($matches && isset($matches[1]) ){
|
||||
return trim($matches[1]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the debug log file exists in the default location, and if it contains our bogus info
|
||||
* @return bool
|
||||
*
|
||||
*/
|
||||
function rsssl_debug_log_file_exists_in_default_location(){
|
||||
$default_file = trailingslashit(WP_CONTENT_DIR).'debug.log';
|
||||
if ( !file_exists($default_file) ) {
|
||||
return false;
|
||||
}
|
||||
//limit max length of string to 500
|
||||
$content = file_get_contents($default_file, false, null, 0, 500 );
|
||||
return trim( $content ) !== 'Access denied';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* Test if code execution is allowed in /uploads folder
|
||||
*/
|
||||
function rsssl_code_execution_allowed()
|
||||
{
|
||||
$code_execution_allowed = get_transient('rsssl_code_execution_allowed_status');
|
||||
if ( !$code_execution_allowed ) {
|
||||
$upload_dir = wp_get_upload_dir();
|
||||
//set a default, in case of timeouts
|
||||
$code_execution_allowed = 'not-allowed';
|
||||
set_transient( 'rsssl_code_execution_allowed_status', $code_execution_allowed, DAY_IN_SECONDS );
|
||||
|
||||
$test_file = $upload_dir['basedir'] . '/' . 'code-execution.php';
|
||||
if ( is_writable($upload_dir['basedir'] ) && ! file_exists( $test_file ) ) {
|
||||
try {
|
||||
copy( rsssl_path . 'security/tests/code-execution.php', $test_file );
|
||||
} catch (Exception $e) {
|
||||
$code_execution_allowed = 'not-allowed';
|
||||
}
|
||||
}
|
||||
|
||||
if ( file_exists( $test_file ) ) {
|
||||
$uploads = wp_upload_dir();
|
||||
$upload_url = trailingslashit($uploads['baseurl']).'code-execution.php';
|
||||
$response = wp_remote_get($upload_url);
|
||||
if ( !is_wp_error($response) ) {
|
||||
if ( is_array( $response ) ) {
|
||||
$status = wp_remote_retrieve_response_code( $response );
|
||||
$web_source = wp_remote_retrieve_body( $response );
|
||||
}
|
||||
|
||||
if ( $status != 200 ) {
|
||||
//Could not connect to website
|
||||
$code_execution_allowed = 'not-allowed';
|
||||
} elseif ( strpos( $web_source, "RSSSL CODE EXECUTION MARKER" ) === false ) {
|
||||
//Mixed content fixer marker not found in the websource
|
||||
$code_execution_allowed = 'not-allowed';
|
||||
} else {
|
||||
$code_execution_allowed = 'allowed';
|
||||
}
|
||||
} else {
|
||||
$code_execution_allowed = 'not-allowed';
|
||||
}
|
||||
}
|
||||
|
||||
//clean up file again
|
||||
if ( file_exists($test_file) ) {
|
||||
unlink($test_file);
|
||||
}
|
||||
set_transient('rsssl_code_execution_allowed_status', $code_execution_allowed, DAY_IN_SECONDS);
|
||||
}
|
||||
|
||||
return $code_execution_allowed === 'allowed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if directory indexing is allowed
|
||||
* We assume allowed if test is not possible due to restrictions. Only an explicity 403 on the response results in "forbidden".
|
||||
* On non htaccess servers, the default is non indexing, so we return forbidden.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_directory_indexing_allowed() {
|
||||
$status = get_transient('rsssl_directory_indexing_status');
|
||||
if ( !$status ) {
|
||||
if ( !rsssl_uses_htaccess() ) {
|
||||
$status = 'forbidden';
|
||||
} else {
|
||||
$status = 'allowed';
|
||||
//set a default, in case of timeouts
|
||||
set_transient( 'rsssl_directory_indexing_status', $status, DAY_IN_SECONDS );
|
||||
|
||||
try {
|
||||
$test_folder = 'indexing-test';
|
||||
$test_dir = trailingslashit(ABSPATH) . $test_folder;
|
||||
if ( ! is_dir( $test_dir ) ) {
|
||||
mkdir( $test_dir, 0755 );
|
||||
}
|
||||
|
||||
$response = wp_remote_get(trailingslashit( site_url($test_folder) ) );
|
||||
if ( is_dir( $test_dir ) ) {
|
||||
rmdir( $test_dir );
|
||||
}
|
||||
|
||||
// WP_Error doesn't contain response code, return false
|
||||
if ( !is_wp_error( $response ) ) {
|
||||
$response_code = $response['response']['code'];
|
||||
if ( $response_code === 403 ) {
|
||||
$status = 'forbidden';
|
||||
}
|
||||
}
|
||||
} catch( Exception $e ) {
|
||||
|
||||
}
|
||||
}
|
||||
set_transient('rsssl_directory_indexing_status', $status, DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $status !== 'forbidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file editing is allowed
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_file_editing_allowed()
|
||||
{
|
||||
if ( function_exists('wp_is_block_theme') && wp_is_block_theme() ) {
|
||||
return false;
|
||||
}
|
||||
return !defined('DISALLOW_FILE_EDIT' ) || !DISALLOW_FILE_EDIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user registration is allowed
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_user_registration_allowed()
|
||||
{
|
||||
return get_option( 'users_can_register' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if page source contains WordPress version information
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
function rsssl_src_contains_wp_version() {
|
||||
$result = get_option('rsssl_wp_version_detected' );
|
||||
if ( $result===false ) {
|
||||
$result = 'no-response';
|
||||
update_option( 'rsssl_wp_version_detected', 'no-response', false );
|
||||
try {
|
||||
$wp_version = get_bloginfo( 'version' );
|
||||
$web_source = "";
|
||||
$response = wp_remote_get( home_url() );
|
||||
if ( ! is_wp_error( $response ) ) {
|
||||
if ( is_array( $response ) ) {
|
||||
$status = wp_remote_retrieve_response_code( $response );
|
||||
$web_source = wp_remote_retrieve_body( $response );
|
||||
}
|
||||
|
||||
if ( $status != 200 ) {
|
||||
$result = 'no-response';
|
||||
} elseif ( strpos( $web_source, 'ver='.$wp_version ) === false ) {
|
||||
$result = 'not-found';
|
||||
} else {
|
||||
$result = 'found';
|
||||
}
|
||||
}
|
||||
update_option( 'rsssl_wp_version_detected', $result, false );
|
||||
} catch(Exception $e) {
|
||||
update_option( 'rsssl_wp_version_detected', 'no-response', false );
|
||||
}
|
||||
}
|
||||
return $result==='found';
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of open hardening features
|
||||
* @return int
|
||||
*/
|
||||
function rsssl_count_open_hardening_features() {
|
||||
$open = 0;
|
||||
$fields = rsssl_fields( false );
|
||||
|
||||
// Filter out unused fields
|
||||
$recommended_hardening_fields = array_filter($fields, function($field){
|
||||
return isset($field['recommended']) && $field['recommended'];
|
||||
});
|
||||
|
||||
// Create $hardening_options dynamically based on recommended field IDs
|
||||
$hardening_options = array_map(function($field) {
|
||||
return $field['id'];
|
||||
}, $recommended_hardening_fields);
|
||||
|
||||
foreach ( $hardening_options as $option ) {
|
||||
|
||||
// Get the field
|
||||
$field = array_filter( $fields, function ( $f ) use ( $option ) {
|
||||
return $f['id'] === $option;
|
||||
} );
|
||||
|
||||
if ( ! empty( $field ) ) {
|
||||
$field = reset( $field );
|
||||
// Apply the rsssl_disable_fields filter
|
||||
$field = apply_filters( 'rsssl_field', $field, $field['id'] );
|
||||
|
||||
// Check if the option is not set to true and the field is not disabled
|
||||
if ( rsssl_get_option( $option ) !== true &&
|
||||
( ! isset( $field['disabled'] ) || $field['disabled'] !== true ) &&
|
||||
( ! isset( $field['value'] ) || $field['value'] !== true ) ) {
|
||||
$open ++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $open;
|
||||
}
|
||||
|
||||
function rsssl_has_open_hardening_features() {
|
||||
return rsssl_count_open_hardening_features() > 0;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* Test file for Really Simple Security to check if uploads directory has code execution permissions
|
||||
*
|
||||
*/
|
||||
|
||||
echo "RSSSL CODE EXECUTION MARKER";
|
||||
@@ -0,0 +1 @@
|
||||
<?php // You don't belong here. ?>
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php defined( 'ABSPATH' ) or die();
|
||||
|
||||
/**
|
||||
* @param $notices
|
||||
* @return mixed
|
||||
* Notice function
|
||||
*/
|
||||
function rsssl_code_execution_errors_notice( $notices ) {
|
||||
$notices['code-execution-uploads'] = array(
|
||||
'callback' => 'rsssl_code_execution_allowed',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'file-not-found' => array(
|
||||
'msg' => __("Could not find code execution test file.", "really-simple-ssl"),
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
),
|
||||
'uploads-folder-not-writable' => array(
|
||||
'msg' => __("Uploads folder not writable.", "really-simple-ssl"),
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
),
|
||||
'could-not-create-test-file' => array(
|
||||
'msg' => __("Could not copy code execution test file.", "really-simple-ssl"),
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if ( rsssl_get_server() === 'nginx') {
|
||||
$notices['code-execution-uploads-nginx'] = array(
|
||||
'callback' => 'rsssl_code_execution_allowed',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'true' => array(
|
||||
'msg' => __("The code to block code execution in the uploads folder cannot be added automatically on nginx. Add the following code to your nginx.conf file:", "really-simple-ssl")
|
||||
. "<br>" . rsssl_get_nginx_code_code_execution_uploads(),
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return $notices;
|
||||
}
|
||||
add_filter('rsssl_notices', 'rsssl_code_execution_errors_notice');
|
||||
|
||||
|
||||
/**
|
||||
* Block code execution
|
||||
* @param array $rules
|
||||
*
|
||||
* @return []
|
||||
*
|
||||
*/
|
||||
function rsssl_disable_code_execution_rules($rules)
|
||||
{
|
||||
if ( !rsssl_get_option('block_code_execution_uploads')) {
|
||||
return $rules;
|
||||
}
|
||||
|
||||
if ( RSSSL()->server->apache_version_min_24() ) {
|
||||
$rule = "\n" ."<Files *.php>";
|
||||
$rule .= "\n" . "Require all denied";
|
||||
$rule .= "\n" . "</Files>";
|
||||
} else {
|
||||
$rule = "\n" ."<Files *.php>";
|
||||
$rule .= "\n" . "deny from all";
|
||||
$rule .= "\n" . "</Files>";
|
||||
}
|
||||
|
||||
$rules[] = ['rules' => $rule, 'identifier' => 'deny from all'];
|
||||
return $rules;
|
||||
}
|
||||
add_filter('rsssl_htaccess_security_rules_uploads', 'rsssl_disable_code_execution_rules');
|
||||
|
||||
|
||||
|
||||
function rsssl_get_nginx_code_code_execution_uploads() {
|
||||
$code = '<code>location ~* /uploads/.*\.php$ {' . "<br>";
|
||||
$code .= ' return 503;' . "<br>";
|
||||
$code .= '}</code>' . "<br>";
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) or die( "you do not have access to this page!" );
|
||||
|
||||
/**
|
||||
* Disable XMLRPC when this integration is activated
|
||||
*/
|
||||
|
||||
add_filter('xmlrpc_enabled', '__return_false');
|
||||
/**
|
||||
* Remove html link
|
||||
*/
|
||||
remove_action( 'wp_head', 'rsd_link' );
|
||||
/**
|
||||
* stop all requests to xmlrpc.php for RSD per XML-RPC:
|
||||
*/
|
||||
if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
|
||||
exit;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) or die();
|
||||
|
||||
/**
|
||||
* Add javascript to make first and last name fields required
|
||||
*/
|
||||
function rsssl_disable_registration_js() {
|
||||
if ( !isset($_SERVER['REQUEST_URI']) || (strpos($_SERVER['REQUEST_URI'], 'user-new.php')===false && strpos($_SERVER['REQUEST_URI'], 'profile.php')===false) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<script>
|
||||
window.addEventListener('load', () => {
|
||||
document.getElementById('first_name').closest('tr').classList.add("form-required");
|
||||
document.getElementById('last_name').closest('tr').classList.add("form-required");
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
add_action( 'admin_print_footer_scripts', 'rsssl_disable_registration_js' );
|
||||
|
||||
/**
|
||||
* Add javascript to make first and last name fields required
|
||||
*/
|
||||
function rsssl_strip_userlogin() {
|
||||
if ( !isset($_SERVER['REQUEST_URI']) || strpos($_SERVER['REQUEST_URI'], 'profile.php')===false ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<script>
|
||||
let rsssl_user_login = document.querySelector('input[name=user_login]');
|
||||
let rsssl_display_name = document.querySelector('select[name=display_name]');
|
||||
if ( rsssl_display_name.options.length>1) {
|
||||
for (let i = rsssl_display_name.options.length-1; i >= 0; i--) {
|
||||
if ( rsssl_user_login.value.toLowerCase() === rsssl_display_name.options[i].value.toLowerCase() ) {
|
||||
rsssl_display_name.removeChild(rsssl_display_name.options[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
add_action( 'admin_print_footer_scripts', 'rsssl_strip_userlogin' );
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) or die();
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
* Disable file editing
|
||||
*/
|
||||
function rsssl_disable_file_editing() {
|
||||
if ( ! defined('DISALLOW_FILE_EDIT' ) ) {
|
||||
define('DISALLOW_FILE_EDIT', true );
|
||||
}
|
||||
}
|
||||
add_action("init", "rsssl_disable_file_editing");
|
||||
|
||||
|
||||
/**
|
||||
* Username 'admin' changed notice
|
||||
* @return array
|
||||
*/
|
||||
function rsssl_disable_file_editing_notice( $notices ) {
|
||||
$notices['disallow_file_edit_false'] = array(
|
||||
'condition' => ['rsssl_file_editing_defined_but_disabled'],
|
||||
'callback' => '_true_',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'true' => array(
|
||||
'msg' => __("The DISALLOW_FILE_EDIT constant is defined and set to false. You can remove it from your wp-config.php.", "really-simple-ssl"),
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
'url' => 'disallow_file_edit-defined-set-to-false'
|
||||
),
|
||||
),
|
||||
);
|
||||
return $notices;
|
||||
}
|
||||
add_filter('rsssl_notices', 'rsssl_disable_file_editing_notice');
|
||||
|
||||
/**
|
||||
* Check if the constant is defined, AND set to false. In that case the plugin cannot override it anymore
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_file_editing_defined_but_disabled(){
|
||||
return defined( 'DISALLOW_FILE_EDIT' ) && ! DISALLOW_FILE_EDIT;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
defined( 'ABSPATH' ) or die();
|
||||
if ( ! class_exists( 'rsssl_hide_wp_version' ) ) {
|
||||
class rsssl_hide_wp_version {
|
||||
private static $_this;
|
||||
public $new_version = false;
|
||||
function __construct() {
|
||||
if ( isset( self::$_this ) ) {
|
||||
wp_die( "you cannot create a second instance of a singleton class" );
|
||||
}
|
||||
|
||||
self::$_this = $this;
|
||||
add_action( 'init', array($this, 'remove_wp_version') );
|
||||
add_filter( 'rsssl_fixer_output', array( $this, 'replace_wp_version') );
|
||||
}
|
||||
|
||||
static function this() {
|
||||
return self::$_this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove WordPress version info from page source
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function remove_wp_version() {
|
||||
// remove <meta name="generator" content="WordPress VERSION" />
|
||||
add_filter( 'the_generator', function () {
|
||||
return '';
|
||||
} );
|
||||
// remove WP ?ver=5.X.X from css/js
|
||||
add_filter( 'style_loader_src', array( $this, 'remove_css_js_version' ), 9999 );
|
||||
add_filter( 'script_loader_src', array ($this, 'remove_css_js_version'), 9999 );
|
||||
remove_action( 'wp_head', 'wp_generator' ); // remove wordpress version
|
||||
remove_action( 'wp_head', 'index_rel_link' ); // remove link to index page
|
||||
remove_action( 'wp_head', 'wlwmanifest_link' ); // remove wlwmanifest.xml (needed to support windows live writer)
|
||||
remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 ); // Remove shortlink
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random version number
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generate_rand_version() {
|
||||
if ( !$this->new_version) {
|
||||
$wp_version = get_bloginfo( 'version' );
|
||||
$token = get_option( 'rsssl_wp_version_token' );
|
||||
if ( ! $token ) {
|
||||
$token = str_shuffle( time() );
|
||||
update_option( 'rsssl_wp_version_token', $token );
|
||||
}
|
||||
|
||||
$this->new_version = hash( 'md5', $token );
|
||||
}
|
||||
return $this->new_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
public function replace_wp_version( $html ) {
|
||||
$wp_version = get_bloginfo( 'version' );
|
||||
$new_version = $this->generate_rand_version();
|
||||
|
||||
return str_replace( '?ver=' . $wp_version, '?ver=' . $new_version, $html );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $src
|
||||
*
|
||||
* @return mixed|string
|
||||
* Remove WordPress version from css and js strings
|
||||
*/
|
||||
public function remove_css_js_version( $src ) {
|
||||
if ( empty($src) ) {
|
||||
return $src;
|
||||
}
|
||||
|
||||
if ( strpos( $src, '?ver=' ) && strpos( $src, 'wp-includes' ) ) {
|
||||
$wp_version = get_bloginfo( 'version' );
|
||||
$new_version = $this->generate_rand_version();
|
||||
$src = str_replace( '?ver=' . $wp_version, '?ver=' . $new_version, $src );
|
||||
}
|
||||
|
||||
return $src;
|
||||
}
|
||||
}
|
||||
}
|
||||
RSSSL_SECURITY()->components['hide-wp-version'] = new rsssl_hide_wp_version();
|
||||
@@ -0,0 +1 @@
|
||||
<?php // You don't belong here. ?>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
defined('ABSPATH') or die();
|
||||
/**
|
||||
* Override default login error message
|
||||
* @return string|void
|
||||
**/
|
||||
function rsssl_no_wp_login_errors()
|
||||
{
|
||||
return __("Invalid login details.", "really-simple-ssl");
|
||||
}
|
||||
add_filter( 'login_errors', 'rsssl_no_wp_login_errors' );
|
||||
|
||||
/**
|
||||
* Hide feedback entirely on password reset (no filter available).
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
*/
|
||||
function rsssl_hide_pw_reset_error() {
|
||||
?>
|
||||
<style>
|
||||
.login-action-lostpassword #login_error{
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<?php
|
||||
}
|
||||
add_action( 'login_enqueue_scripts', 'rsssl_hide_pw_reset_error' );
|
||||
|
||||
/**
|
||||
*
|
||||
* Clear username when username is valid but password is incorrect
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function rsssl_clear_username_on_correct_username() {
|
||||
?>
|
||||
<script>
|
||||
if ( document.getElementById('login_error') ) {
|
||||
document.getElementById('user_login').value = '';
|
||||
}
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
add_action( 'login_footer', 'rsssl_clear_username_on_correct_username' );
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
defined('ABSPATH') or die();
|
||||
|
||||
/**
|
||||
* Username 'admin' changed notice
|
||||
* @return array
|
||||
*/
|
||||
function rsssl_admin_username_changed( $notices ) {
|
||||
$notices['username_admin_changed'] = array(
|
||||
'condition' => ['rsssl_username_admin_changed'],
|
||||
'callback' => '_true_',
|
||||
'score' => 5,
|
||||
'output' => array(
|
||||
'true' => array(
|
||||
'msg' => sprintf(__("Username 'admin' has been changed to %s", "really-simple-ssl"),esc_html(get_site_transient('rsssl_username_admin_changed')) ),
|
||||
'icon' => 'open',
|
||||
'dismissible' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
return $notices;
|
||||
}
|
||||
add_filter('rsssl_notices', 'rsssl_admin_username_changed');
|
||||
|
||||
/**
|
||||
* Add admin as not allowed username
|
||||
* @param array $illegal_user_logins
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function rsssl_prevent_admin_user_add(array $illegal_user_logins){
|
||||
$illegal_user_logins[] = 'admin';
|
||||
$illegal_user_logins[] = 'administrator';
|
||||
return $illegal_user_logins;
|
||||
}
|
||||
add_filter( 'illegal_user_logins', 'rsssl_prevent_admin_user_add' );
|
||||
|
||||
/**
|
||||
* Rename admin user
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_rename_admin_user() {
|
||||
if ( !rsssl_user_can_manage() ) {
|
||||
return false;
|
||||
}
|
||||
//to be able to update the admin user email, we need to disable this filter temporarily
|
||||
remove_filter( 'illegal_user_logins', 'rsssl_prevent_admin_user_add' );
|
||||
|
||||
// Get user data for login admin
|
||||
$admin_user = get_user_by('login','admin');
|
||||
if ( $admin_user ) {
|
||||
// Get the new user login
|
||||
$new_user_login = trim(sanitize_user(rsssl_get_option('new_admin_user_login')));
|
||||
if ( rsssl_new_username_valid() ) {
|
||||
$admin_user_id = $admin_user->data->ID;
|
||||
$admin_userdata = get_userdata( $admin_user_id );
|
||||
$admin_email = $admin_userdata->data->user_email;
|
||||
global $wpdb;
|
||||
//get current user hash
|
||||
$user_hash = $wpdb->get_var($wpdb->prepare("select user_pass from {$wpdb->base_prefix}users where ID = %s", $admin_user_id) );
|
||||
//create temp email address
|
||||
$domain = site_url();
|
||||
$parse = parse_url( $domain );
|
||||
$host = $parse['host'] ?? 'example.com';
|
||||
$email = "$new_user_login@$host";
|
||||
|
||||
// Do not send an e-mail with this temporary e-mail address
|
||||
add_filter('send_email_change_email', '__return_false');
|
||||
|
||||
// update e-mail for existing user. Cannot have two accounts connected to the same e-mail address
|
||||
$success = wp_update_user( array(
|
||||
'ID' => $admin_user_id,
|
||||
'user_email' => $email,
|
||||
) );
|
||||
|
||||
if ( ! $success ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Populate the new user data. Use current 'admin' userdata wherever available
|
||||
$new_userdata = array(
|
||||
'user_pass' => wp_generate_password( 12 ), //temp, overwrite with actual hash later.
|
||||
//(string) The plain-text user password.
|
||||
'user_login' => $new_user_login,
|
||||
//(string) The user's login username.
|
||||
'user_nicename' => isset( $admin_user->data->user_nicename ) ? $admin_user->data->user_nicename : '',
|
||||
//(string) The URL-friendly user name.
|
||||
'user_url' => isset( $admin_user->data->user_url ) ? $admin_user->data->user_url : '',
|
||||
//(string) The user URL.
|
||||
'user_email' => isset( $admin_email ) ? $admin_email : '',
|
||||
//(string) The user email address.
|
||||
'display_name' => isset( $admin_user->data->display_name ) ? $admin_user->data->display_name : '',
|
||||
//(string) The user's display name. Default is the user's username.
|
||||
'nickname' => isset( $admin_user->data->nickname ) ? $admin_user->data->nickname : '',
|
||||
//(string) The user's nickname. Default is the user's username.
|
||||
'first_name' => isset( $admin_user->data->user_firstname ) ? $admin_user->data->user_firstname : '',
|
||||
//(string) The user's first name. For new users, will be used to build the first part of the user's display name if $display_name is not specified.
|
||||
'last_name' => isset( $admin_user->data->user_lastname ) ? $admin_user->data->user_lastname : '',
|
||||
//(string) The user's last name. For new users, will be used to build the second part of the user's display name if $display_name is not specified.
|
||||
'description' => isset( $admin_user->data->description ) ? $admin_user->data->description : '',
|
||||
//(string) The user's biographical description.
|
||||
'rich_editing' => isset( $admin_user->data->rich_editing ) ? $admin_user->data->rich_editing : '',
|
||||
//(string|bool) Whether to enable the rich-editor for the user. False if not empty.
|
||||
'syntax_highlighting' => isset( $admin_user->data->syntax_highlighting ) ? $admin_user->data->syntax_highlighting : '',
|
||||
//(string|bool) Whether to enable the rich code editor for the user. False if not empty.
|
||||
'comment_shortcuts' => isset( $admin_user->data->comment_shortcuts ) ? $admin_user->data->comment_shortcuts : '',
|
||||
//(string|bool) Whether to enable comment moderation keyboard shortcuts for the user. Default false.
|
||||
'admin_color' => isset( $admin_user->data->admin_color ) ? $admin_user->data->admin_color : '',
|
||||
//(string) Admin color scheme for the user. Default 'fresh'.
|
||||
'use_ssl' => isset( $admin_user->data->use_ssl ) ? $admin_user->data->use_ssl : '',
|
||||
//(bool) Whether the user should always access the admin over https. Default false.
|
||||
'user_registered' => isset( $admin_user->data->user_registered ) ? $admin_user->data->user_registered : '',
|
||||
//(string) Date the user registered. Format is 'Y-m-d H:i:s'.
|
||||
'show_admin_bar_front' => isset( $admin_user->data->show_admin_bar_front ) ? $admin_user->data->show_admin_bar_front : '',
|
||||
//(string|bool) Whether to display the Admin Bar for the user on the site's front end. Default true.
|
||||
'role' => isset( $admin_user->roles[0] ) ? $admin_user->roles[0] : '',
|
||||
//(string) User's role.
|
||||
'locale' => isset( $admin_user->data->locale ) ? $admin_user->data->locale : '',
|
||||
//(string) User's locale. Default empty.
|
||||
);
|
||||
|
||||
// Create new admin user
|
||||
$new_user_id = wp_insert_user( $new_userdata );
|
||||
if ( ! $new_user_id || is_wp_error($new_user_id) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//store original user hash in this user.
|
||||
$wpdb->update(
|
||||
$wpdb->base_prefix.'users',
|
||||
['user_pass' => $user_hash ],
|
||||
['ID' => $new_user_id]
|
||||
);
|
||||
|
||||
require_once( ABSPATH . 'wp-admin/includes/user.php' );
|
||||
wp_delete_user( $admin_user_id, $new_user_id );
|
||||
|
||||
// On multisite we have to update the $wpdb->prefix . sitemeta -> meta_key -> site_admins -> meta_value to the new username
|
||||
if ( is_multisite() ) {
|
||||
global $wpdb;
|
||||
$site_admins = $wpdb->get_var( "SELECT meta_value FROM {$wpdb->base_prefix}sitemeta WHERE meta_key = 'site_admins'" );
|
||||
if ( is_serialized( $site_admins ) ) {
|
||||
$unserialized = unserialize( $site_admins );
|
||||
foreach ( $unserialized as $index => $site_admin ) {
|
||||
if ( $site_admin === 'admin' ) {
|
||||
$unserialized[ $index ] = $new_user_login;
|
||||
}
|
||||
}
|
||||
$site_admins = serialize( $unserialized );
|
||||
}
|
||||
$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->base_prefix}sitemeta SET meta_value = %s WHERE meta_key = 'site_admins'", $site_admins ) );
|
||||
}
|
||||
|
||||
set_site_transient( 'rsssl_username_admin_changed', $new_user_login, DAY_IN_SECONDS );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
add_action('rsssl_after_saved_fields','rsssl_rename_admin_user', 30);
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*
|
||||
* Notice condition
|
||||
*/
|
||||
function rsssl_username_admin_changed() {
|
||||
if ( get_site_transient('rsssl_username_admin_changed') ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
defined('ABSPATH') or die();
|
||||
/**
|
||||
* @param $response
|
||||
* @param $handler
|
||||
* @param WP_REST_Request $request
|
||||
* @return mixed|WP_Error
|
||||
*
|
||||
* Hook into REST API requests
|
||||
*/
|
||||
function authorize_rest_api_requests( $response, $handler, WP_REST_Request $request ) {
|
||||
// allowed routes, whitelist option?
|
||||
// $routes = array(
|
||||
// '/wp/v2/csp etc',
|
||||
// );
|
||||
|
||||
// Check if authorization header is set
|
||||
if ( ! $request->get_header( 'authorization' ) ) {
|
||||
return new WP_Error( 'authorization', 'Unauthorized access.', array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
// if ( rsssl_get_networkwide_option('rsssl_restrict_rest_api') === 'restrict-roles' ) {
|
||||
// Check for certain role and allowed route
|
||||
if ( ! in_array( 'administrator', wp_get_current_user()->roles ) ) {
|
||||
return new WP_Error( 'forbidden', 'Access forbidden.', array( 'status' => 403 ) );
|
||||
}
|
||||
// }
|
||||
|
||||
// if ( rsssl_get_networkwide_option('rsssl_restrict_rest_api') === 'logged-in-users' ) {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return new WP_Error( 'forbidden', 'Access forbidden to non-logged in users.', array( 'status' => 403 ) );
|
||||
}
|
||||
// }
|
||||
|
||||
// if ( rsssl_get_networkwide_option('rsssl_restrict_rest_api') === 'application-passwords' ) {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return new WP_Error( 'forbidden', 'Access forbidden to non-logged in users.', array( 'status' => 403 ) );
|
||||
}
|
||||
// }
|
||||
|
||||
return $response;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* Disable REST API
|
||||
*/
|
||||
function rsssl_disable_rest_api() {
|
||||
add_filter('json_enabled', '__return_false');
|
||||
add_filter('json_jsonp_enabled', '__return_false');
|
||||
}
|
||||
|
||||
add_filter( 'rest_request_before_callbacks', 'authorize_rest_api_requests', 10, 3 );
|
||||
@@ -0,0 +1,59 @@
|
||||
#two-factor-qr-code {
|
||||
display: flex; /* Enables Flexbox */
|
||||
justify-content: left; /* Centers horizontally */
|
||||
align-items: center; /* Centers vertically */
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
#qr-code-container {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
//right: 0;
|
||||
}
|
||||
|
||||
#two-factor-totp-authcode {
|
||||
width: 100%;
|
||||
}
|
||||
tr.rsssl_verify_email {
|
||||
display: none;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
span.rsssl-backup-codes {
|
||||
padding: 5px;
|
||||
background: #fbebed;
|
||||
border-radius: 8px;
|
||||
box-shadow: rgba(0,0,0,0.1) 0 4px 6px -1px;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
#totp-key {
|
||||
cursor: pointer;
|
||||
display: flex; /* Enables Flexbox */
|
||||
justify-content: center; /* Centers horizontally */
|
||||
align-items: center; /* Centers vertically */
|
||||
}
|
||||
|
||||
table.rsssl-table-two-fa {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.rsssl-methods-tag {
|
||||
padding: 2px 5px;
|
||||
border: 1px solid #000;
|
||||
color: #000;
|
||||
margin-left: 5px;
|
||||
background: dimgrey;
|
||||
&.active {
|
||||
background: darkgreen;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/* Style radio inputs */
|
||||
.radio-input {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin-left: 10px; /* Adjust this value to your preferred spacing */
|
||||
vertical-align: middle;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
/* Style radio labels */
|
||||
.radio-label {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-left: 10px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.badge-default {
|
||||
background-color: #e5e5e5;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.badge-enabled {
|
||||
background-color: #fbc43e;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following styles are for the onboarding form
|
||||
*/
|
||||
#two_fa_onboarding_form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#two_fa_onboarding_form div {
|
||||
transition: height 0.5s;
|
||||
}
|
||||
|
||||
#skip_onboarding {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.skip_container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.totp-submit {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
div.rsssl_step_one_onboarding {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.rsssl_step_two_onboarding {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.rsssl_step_three_onboarding {
|
||||
margin-top: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#two-factor-qr-code {
|
||||
display: flex; /* Enables Flexbox */
|
||||
justify-content: center; /* Centers horizontally */
|
||||
align-items: center; /* Centers vertically */
|
||||
min-width: 205px;
|
||||
min-height: 205px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
#totp-key {
|
||||
cursor: pointer;
|
||||
display: flex; /* Enables Flexbox */
|
||||
justify-content: center; /* Centers horizontally */
|
||||
align-items: center; /* Centers vertically */
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@import "profile-settings.scss";
|
||||
@import "two-fa-onboarding.scss";
|
||||
@@ -0,0 +1,119 @@
|
||||
class BaseAuth {
|
||||
constructor(root, settings) {
|
||||
this.root = root;
|
||||
this.settings = settings;
|
||||
this.translatableStrings = {
|
||||
keyCopied: this.settings.translatables.keyCopied,
|
||||
// ... add more strings as needed
|
||||
};
|
||||
}
|
||||
getElement = (id) => document.getElementById(id);
|
||||
|
||||
getCheckedInputValue = (name) => document.querySelector(`input[name="${name}"]:checked`).value;
|
||||
|
||||
/**
|
||||
* Performs a fetch operation.
|
||||
*
|
||||
* @param {string} urlExtension - The URL extension to perform the fetch operation on.
|
||||
* @param {Object} data - The data to be sent in the fetch operation.
|
||||
* @param {string} [method='POST'] - The HTTP method to be used in the fetch operation. Defaults to 'POST'.
|
||||
* @returns {Promise} - A Promise that resolves with the response of the fetch operation.
|
||||
*/
|
||||
performFetchOp = (urlExtension, data, method = 'POST') => {
|
||||
let url = this.root + urlExtension;
|
||||
let fetchParams = {
|
||||
method: method,
|
||||
headers: {'Content-Type': 'application/json',},
|
||||
};
|
||||
if (method === 'POST') {
|
||||
fetchParams.body = JSON.stringify(data);
|
||||
}
|
||||
return fetch(url, fetchParams);
|
||||
};
|
||||
|
||||
assignClickListener = (id, callback) => {
|
||||
const element = this.getElement(id);
|
||||
if (element) {
|
||||
element.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
logFetchError = (error) => console.error('There has been a problem with your fetch operation:', error);
|
||||
|
||||
/**
|
||||
* Generates a QR code for Two-Factor Authentication using the TOTP URL.
|
||||
* If the TOTP URL is not available, nothing will be generated.
|
||||
*
|
||||
* @function qr_generator
|
||||
* @returns {void} Nothing is returned.
|
||||
*/
|
||||
qr_generator = () => {
|
||||
const totp_url = this.settings.totp_data.totp_url;
|
||||
if (!totp_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
let qr = qrcode(0, 'L');
|
||||
qr.addData(totp_url);
|
||||
qr.make();
|
||||
let qrElem = document.querySelector('#two-factor-qr-code a');
|
||||
if (qrElem != null) {
|
||||
qrElem.innerHTML = qr.createSvgTag(5);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Downloads backup codes as a text file.
|
||||
*
|
||||
* @function download_codes
|
||||
*/
|
||||
download_codes = () => {
|
||||
let TextToCode = this.settings.totp_data.backup_codes;
|
||||
let TextToCodeString = '';
|
||||
TextToCode.forEach(function (item) {
|
||||
TextToCodeString += item + '\n';
|
||||
});
|
||||
let downloadLink = document.createElement('a');
|
||||
downloadLink.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(TextToCodeString));
|
||||
downloadLink.setAttribute('download', 'backup_codes.txt');
|
||||
downloadLink.style.display = 'none';
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function copies the text from the `totp_data.key` property of the `settings` object
|
||||
* using the Clipboard API. It then shows a success message and reverts back to the original display
|
||||
* after a specified timeout.
|
||||
*
|
||||
* @function copyTextAndShowMessage
|
||||
* @memberof BaseAuth
|
||||
*/
|
||||
copyTextAndShowMessage = () => {
|
||||
let text = this.settings.totp_data.key; // Get the text to be copied
|
||||
|
||||
// Use Clipboard API to copy the text
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// Change the display of the key
|
||||
let originalText = this.getElement('totp-key').innerText;
|
||||
this.getElement('totp-key').innerText = this.translatableStrings.keyCopied;
|
||||
this.getElement('totp-key').style.color = 'green';
|
||||
|
||||
// Revert back to original text after a timeout
|
||||
setTimeout(() => {
|
||||
this.getElement('totp-key').innerText = originalText;
|
||||
this.getElement('totp-key').style.color = ''; // Reset the color
|
||||
}, 2000); // Adjust timeout as needed
|
||||
|
||||
}, function (err) {
|
||||
console.error(this.settings.translatables.keyCopiedFailed, err);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* The Global rsssl_onboard object is defined in the PHP file that enqueues this script.
|
||||
* @global rsssl_onboard
|
||||
* It contains the following properties:
|
||||
* @typedef {Object} rsssl_onboard
|
||||
* @property {string} root - The root URL of the site.
|
||||
* @property {string} redirect_to - The URL to redirect to after the onboarding process is complete.
|
||||
* @property {string} user_id - The ID of the user.
|
||||
* @property {string} login_nonce - The nonce for the login.
|
||||
* @property {string} totp_data - The data for the TOTP.
|
||||
* @property {string} totp_data.totp_url - The URL for the TOTP.
|
||||
* @property {string} totp_data.backup_codes - The backup codes for the TOTP.
|
||||
* @property {string} totp_data.key - The key for the TOTP.
|
||||
* @property {string} totp_data.authcode - The authcode for the TOTP.
|
||||
* @property {string} totp_data.provider - The provider for the TOTP.
|
||||
* @property {string} totp_data.redirect_to - The URL to redirect to after the TOTP process is complete.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Global rsssl_profile object is defined in the PHP file that enqueues this script.
|
||||
* @global rsssl_profile
|
||||
* It contains the following properties:
|
||||
* @typedef {Object} rsssl_profile
|
||||
* @property {string} root - The root URL of the site.
|
||||
* @property {string} redirect_to - The URL to redirect to after the profile process is complete.
|
||||
* @property {string} user_id - The ID of the user.
|
||||
* @property {string} login_nonce - The nonce for the login.
|
||||
* @property {string} totp_data - The data for the TOTP.
|
||||
* @property {string} totp_data.totp_url - The URL for the TOTP.
|
||||
* @property {string} totp_data.backup_codes - The backup codes for the TOTP.
|
||||
* @property {string} totp_data.key - The key for the TOTP.
|
||||
* @property {string} totp_data.authcode - The authcode for the TOTP.
|
||||
* @property {string} totp_data.provider - The provider for the TOTP.
|
||||
* @property {string} totp_data.redirect_to - The URL to redirect to after the TOTP process is complete.
|
||||
* @property {string} totp_data.email - The email for the TOTP.
|
||||
* @property {array} translatables - The translatable strings for the profile.
|
||||
* @property {string} translatables.keyCopied - The message to display when the key is copied.
|
||||
* @property {string} translatables.keyCopiedFailed - The error message to display.
|
||||
*/
|
||||
|
||||
window.onload = function() {
|
||||
if(typeof rsssl_onboard !== 'undefined') {
|
||||
let onboarding = new Onboarding(rsssl_onboard.root, rsssl_onboard);
|
||||
onboarding.init();
|
||||
}
|
||||
|
||||
if (typeof rsssl_profile !== 'undefined') {
|
||||
let profile = new Profile(rsssl_profile.root, rsssl_profile);
|
||||
profile.init();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
class Onboarding extends BaseAuth {
|
||||
|
||||
init() {
|
||||
const translatableStrings = {
|
||||
keyCopied: 'Key copied',
|
||||
};
|
||||
|
||||
let endpoints = ['do_not_ask_again', 'skip_onboarding'];
|
||||
let that = this;
|
||||
|
||||
endpoints.forEach(endpoint => {
|
||||
let endpointsElement = this.getElement(endpoint);
|
||||
if (endpointsElement !== null) {
|
||||
endpointsElement.addEventListener('click', (event) => { // Use arrow function here
|
||||
event.preventDefault();
|
||||
// we call the performFetchOp method and then log the response
|
||||
this.performFetchOp(`/${endpoint}`, this.settings)
|
||||
.then(response => response.json())
|
||||
// We log the data and redirect to the redirect_to URL
|
||||
.then(data => window
|
||||
.location
|
||||
.href = data.redirect_to)
|
||||
// We catch any errors and log them
|
||||
.catch(this.logFetchError);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let endpointElem = this.getElement('rsssl_continue_onboarding');
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
let urlExtension = '';
|
||||
let selectedProvider = this.getCheckedInputValue('preferred_method');
|
||||
if (selectedProvider === 'email') {
|
||||
let data = {
|
||||
provider: selectedProvider,
|
||||
redirect_to: this.settings.redirect_to,
|
||||
user_id: this.settings.user_id,
|
||||
login_nonce: this.settings.login_nonce
|
||||
};
|
||||
urlExtension = '/save_default_method_email';
|
||||
this.performFetchOp(urlExtension, data)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.getElement('rsssl_step_one_onboarding').style.display = 'none';
|
||||
const validation_check = document.getElementById("rsssl_step_three_onboarding");
|
||||
validation_check.style.display = "block";
|
||||
// Removing the 'click' event listener from the rsssl_continue_onboarding id button
|
||||
endpointElem.addEventListener('click', (event) => handleValidation(event, data));
|
||||
endpointElem.removeEventListener('click', handleClick);
|
||||
|
||||
})
|
||||
.catch(that.logFetchError);
|
||||
} else if (selectedProvider === 'totp') {
|
||||
// Hiding step one and showing step two
|
||||
this.getElement('rsssl_step_one_onboarding').style.display = 'none';
|
||||
// We hide this element
|
||||
endpointElem.style.display = 'none';
|
||||
this.getElement('rsssl_step_two_onboarding').style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidation = async (event, data) => {
|
||||
event.preventDefault();
|
||||
let selectedProvider = this.getCheckedInputValue('preferred_method');
|
||||
let urlExtension = '/' + data.validation_action;
|
||||
let sendData = {
|
||||
user_id: this.settings.user_id,
|
||||
login_nonce: this.settings.login_nonce,
|
||||
redirect_to: this.settings.redirect_to,
|
||||
token: document.getElementById('rsssl-authcode').value,
|
||||
provider: selectedProvider
|
||||
};
|
||||
let response;
|
||||
try {
|
||||
response = await this.performFetchOp(urlExtension, sendData);
|
||||
} catch (err) {
|
||||
console.log('Fetch Error: ', err);
|
||||
}
|
||||
if (response && !response.ok) {
|
||||
let error = await response.json();
|
||||
this.displayTwoFaOnboardingError(error.error);
|
||||
}
|
||||
if (response && response.ok) {
|
||||
let data = await response.json();
|
||||
window.location.href = data.redirect_to;
|
||||
}
|
||||
};
|
||||
|
||||
if (endpointElem !== null) {
|
||||
endpointElem.addEventListener('click', handleClick);
|
||||
}
|
||||
|
||||
let totpSubmit = this.getElement('two-factor-totp-submit');
|
||||
if (totpSubmit !== null) {
|
||||
totpSubmit.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
let authCode = document.getElementById('two-factor-totp-authcode').value;
|
||||
let key = this.settings.totp_data.key;
|
||||
let selectedProvider = this.getCheckedInputValue('preferred_method');
|
||||
let sendData = {
|
||||
'two-factor-totp-authcode': authCode,
|
||||
provider: selectedProvider,
|
||||
key: key,
|
||||
redirect_to: this.settings.redirect_to,
|
||||
user_id: this.settings.user_id,
|
||||
login_nonce: this.settings.login_nonce
|
||||
};
|
||||
try {
|
||||
let response = await this.performFetchOp('/save_default_method_totp', sendData);
|
||||
if (!response.ok) {
|
||||
let error = await response.json();
|
||||
this.displayTwoFaOnboardingError(error.error);
|
||||
} else {
|
||||
let data = await response.json();
|
||||
window.location.href = data.redirect_to;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logFetchError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let resendButton = this.getElement('rsssl-two-factor-email-code-resend');
|
||||
if(resendButton !== null) {
|
||||
resendButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
let data = {
|
||||
user_id: this.settings.user_id,
|
||||
login_nonce: this.settings.login_nonce,
|
||||
provider: 'email'
|
||||
};
|
||||
this.performFetchOp('/resend_email_code', data)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.displayTwoFaOnboardingError(data.message);
|
||||
})
|
||||
.catch(this.logFetchError);
|
||||
});
|
||||
}
|
||||
|
||||
let downloadButton = this.getElement('download_codes');
|
||||
|
||||
downloadButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.download_codes();
|
||||
});
|
||||
|
||||
this.getElement('two-factor-qr-code').addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
that.copyTextAndShowMessage();
|
||||
});
|
||||
|
||||
this.getElement('totp-key').addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
that.copyTextAndShowMessage();
|
||||
});
|
||||
if (document.readyState === 'complete') {
|
||||
this.qr_generator();
|
||||
|
||||
} else {
|
||||
this.qr_generator();
|
||||
}
|
||||
}
|
||||
|
||||
displayTwoFaOnboardingError(error) {
|
||||
let loginForm = document.getElementById('two_fa_onboarding_form');
|
||||
if (loginForm) {
|
||||
let errorDiv = document.getElementById('login-message');
|
||||
if(!errorDiv) {
|
||||
errorDiv = document.createElement('div');
|
||||
errorDiv.id = 'login-message';
|
||||
errorDiv.className = 'notice notice-error message';
|
||||
loginForm.insertAdjacentElement('beforebegin', errorDiv);
|
||||
}
|
||||
errorDiv.innerHTML = `<p>${error}</p>`;
|
||||
setTimeout(() => {
|
||||
// removing the error box from the loginForm
|
||||
errorDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
class Profile extends BaseAuth {
|
||||
init() {
|
||||
|
||||
this.assignClickListener('download_codes', this.download_codes);
|
||||
this.assignClickListener('two-factor-qr-code', this.copyTextAndShowMessage);
|
||||
this.assignClickListener('totp-key', this.copyTextAndShowMessage);
|
||||
|
||||
const qrCodeContainer = this.getElement('qr-code-container');
|
||||
const enableCheckbox = this.getElement('two-factor-authentication');
|
||||
const tableRowSelection = this.getElement('selection_two_fa');
|
||||
const methodSelection = document.querySelectorAll('input[name="preferred_method"]');
|
||||
const validationEmail = document.getElementById('rsssl_verify_email');
|
||||
const change2faConfig = this.getElement('change_2fa_config');
|
||||
let that = this;
|
||||
if (qrCodeContainer) {
|
||||
qrCodeContainer.style.display = "none";
|
||||
if (!enableCheckbox.checked) {
|
||||
tableRowSelection.style.display = "none";
|
||||
qrCodeContainer.style.display = "none";
|
||||
}
|
||||
}
|
||||
if(enableCheckbox) {
|
||||
let parent = this;
|
||||
enableCheckbox.addEventListener("change", function () {
|
||||
if (this.checked) {
|
||||
tableRowSelection.style.display = "table-row";
|
||||
let selectedMethod = document.querySelector('input[name="preferred_method"]:checked');
|
||||
if (selectedMethod && selectedMethod.value === "totp") {
|
||||
qrCodeContainer.style.display = "block";
|
||||
parent.qr_generator();
|
||||
} else {
|
||||
qrCodeContainer.style.display = "none";
|
||||
}
|
||||
} else {
|
||||
tableRowSelection.style.display = "none";
|
||||
qrCodeContainer.style.display = "none";
|
||||
let selectedMethod = document.querySelector('input[name="preferred_method"]:checked');
|
||||
selectedMethod.value = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(methodSelection.length > 0 ) {
|
||||
let parent = this;
|
||||
methodSelection.forEach(function (element) {
|
||||
element.addEventListener("change", function () {
|
||||
let selectedMethod = document.querySelector('input[name="preferred_method"]:checked').value;
|
||||
if (selectedMethod === "totp") {
|
||||
if(validationEmail) {
|
||||
validationEmail.style.display = "none";
|
||||
}
|
||||
qrCodeContainer.style.display = "block";
|
||||
parent.qr_generator();
|
||||
} else if(selectedMethod === "email") {
|
||||
qrCodeContainer.style.display = "none";
|
||||
if(validationEmail) {
|
||||
validationEmail.style.display = "table-row";
|
||||
}
|
||||
let data = {
|
||||
action: 'change_method_to_email',
|
||||
provider: selectedMethod,
|
||||
user_id: rsssl_profile.user_id,
|
||||
login_nonce: document.getElementById('rsssl_two_fa_nonce').value,
|
||||
redirect_to: rsssl_profile.redirect_to,
|
||||
profile: true
|
||||
};
|
||||
fetch(rsssl_profile.ajax_url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
},
|
||||
body: new URLSearchParams(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(responseData => {
|
||||
// Expected structure: { success: true, data: { message: "Verification code sent", token: ... } }
|
||||
let errorDiv = document.getElementById('login-message');
|
||||
let inPutField = document.getElementById('rsssl-two-factor-email-code');
|
||||
if (inPutField) {
|
||||
if (!errorDiv) {
|
||||
errorDiv = document.createElement('p');
|
||||
errorDiv.classList.add('notice', 'notice-success');
|
||||
inPutField.insertAdjacentElement('afterend', errorDiv);
|
||||
}
|
||||
// Use the message returned from your PHP callback
|
||||
if (responseData.data.message) {
|
||||
errorDiv.innerHTML = `<p>${responseData.data.message}</p>`;
|
||||
} else {
|
||||
console.error('No message returned from the server.');
|
||||
}
|
||||
// Optionally, do something with responseData.data.token if needed.
|
||||
setTimeout(() => {
|
||||
errorDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
})
|
||||
.catch(that.logFetchError);
|
||||
} else {
|
||||
qrCodeContainer.style.display = "none";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let resendButton = this.getElement('rsssl_resend_code_action');
|
||||
if(resendButton !== null) {
|
||||
resendButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
let data = {
|
||||
action: 'resend_email_code_profile',
|
||||
user_id: this.settings.user_id,
|
||||
login_nonce: document.getElementById('rsssl_two_fa_nonce').value,
|
||||
provider: 'email',
|
||||
profile: true
|
||||
};
|
||||
let ajaxUrl = rsssl_profile.ajax_url;
|
||||
fetch(ajaxUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
},
|
||||
body: new URLSearchParams(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(responseData => {
|
||||
// responseData will have the structure: { success: true, data: { message: "..." } }
|
||||
let errorDiv = document.getElementById('login-message');
|
||||
let inPutField = document.getElementById('rsssl-two-factor-email-code');
|
||||
if (inPutField) {
|
||||
if (!errorDiv) {
|
||||
errorDiv = document.createElement('p');
|
||||
errorDiv.classList.add('notice', 'notice-success');
|
||||
inPutField.insertAdjacentElement('afterend', errorDiv);
|
||||
}
|
||||
errorDiv.innerHTML = `<p>${responseData.data.message}</p>`;
|
||||
// Fade out the message after 5 seconds.
|
||||
setTimeout(() => {
|
||||
errorDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
})
|
||||
.catch(this.logFetchError);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (change2faConfig) {
|
||||
change2faConfig.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
let inputField = document.createElement('input');
|
||||
inputField.setAttribute('type', 'hidden');
|
||||
inputField.setAttribute('name', 'change_2fa_config_field');
|
||||
inputField.setAttribute('value', 'true');
|
||||
document.getElementById('change_2fa_config').insertAdjacentElement('afterend', inputField);
|
||||
// we uncheck Enable Two-Factor Authentication
|
||||
let enableCheckbox = document.getElementById("two-factor-authentication");
|
||||
enableCheckbox.checked = false;
|
||||
let profileForm = document.getElementById('your-profile');
|
||||
if (profileForm) {
|
||||
profileForm.requestSubmit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
/**
|
||||
* Holds the request parameters for a specific action.
|
||||
* This class holds the request parameters for a specific action.
|
||||
* It is used to store the parameters and pass them to the functions.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* Holds the request parameters for a specific action.
|
||||
* This class holds the request parameters for a specific action.
|
||||
* It is used to store the parameters and pass them to the functions.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
class Rsssl_Parameter_Validation
|
||||
{
|
||||
|
||||
/**
|
||||
* Validates a user ID.
|
||||
*
|
||||
* @param int $user_id The user ID to be validated.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function validate_user_id(int $user_id): void
|
||||
{
|
||||
if (!is_numeric($user_id)) {
|
||||
// Create an error message for the profile page.
|
||||
add_settings_error(
|
||||
'two-factor-authentication',
|
||||
'rsssl-two-factor-authentication-error',
|
||||
__('The user ID is not valid.', 'really-simple-ssl')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates post data.
|
||||
*
|
||||
* @param array $post_data The post data to validate.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function validate_post_data(array $post_data): void
|
||||
{
|
||||
if (!isset($post_data['preferred_method'])) {
|
||||
// Create an error message for the profile page.
|
||||
add_settings_error(
|
||||
'two-factor-authentication',
|
||||
'rsssl-two-factor-authentication-error',
|
||||
__('The preferred method is not set.', 'really-simple-ssl')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user object.
|
||||
*
|
||||
* @param mixed $user The user object to validate.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function validate_user($user): void
|
||||
{
|
||||
if (!$user instanceof WP_User) {
|
||||
// Create an error message for the profile page.
|
||||
add_settings_error(
|
||||
'two-factor-authentication',
|
||||
'rsssl-two-factor-authentication-error',
|
||||
__('The user object is not valid.', 'really-simple-ssl')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the selected provider.
|
||||
*
|
||||
* @param string $selected_provider The selected provider to validate.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function validate_selected_provider(string $selected_provider): void
|
||||
{
|
||||
if (!in_array($selected_provider, array('totp', 'email', 'none'), true)) {
|
||||
// Create an error message for the profile page.
|
||||
add_settings_error(
|
||||
'two-factor-authentication',
|
||||
'rsssl-two-factor-authentication-error',
|
||||
__('The selected provider is not valid.', 'really-simple-ssl')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an authentication code.
|
||||
*
|
||||
* @param mixed $auth_code The authentication code to validate.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function validate_auth_code($auth_code): void
|
||||
{
|
||||
if (!is_numeric($auth_code)) {
|
||||
// Create an error message for the profile page.
|
||||
add_settings_error(
|
||||
'two-factor-authentication',
|
||||
'rsssl-two-factor-authentication-error',
|
||||
__('The authentication code is not valid.', 'really-simple-ssl')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a given key.
|
||||
*
|
||||
* @param mixed $key The key to validate.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function validate_key($key): void
|
||||
{
|
||||
if (!is_string($key)) {
|
||||
// Create an error message for the profile page.
|
||||
add_settings_error(
|
||||
'two-factor-authentication',
|
||||
'rsssl-two-factor-authentication-error',
|
||||
__('The key is not valid.', 'really-simple-ssl')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the current errors for a user in a transient.
|
||||
*
|
||||
* @param int $user_id The ID of the user.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function cache_errors(int $user_id): void
|
||||
{
|
||||
// Put the current errors in a transient.
|
||||
set_transient('rsssl_two_factor_auth_error_' . $user_id, get_settings_errors(), 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves cached errors for a specific user.
|
||||
*
|
||||
* @param int $user_id The ID of the user to retrieve the errors for.
|
||||
*
|
||||
* @return mixed|null An array of errors if found, null otherwise.
|
||||
*/
|
||||
public static function get_cached_errors(int $user_id)
|
||||
{
|
||||
// Get the errors from the transient.
|
||||
$errors = get_transient('rsssl_two_factor_auth_error_' . $user_id);
|
||||
// Delete the transient.
|
||||
delete_transient('rsssl_two_factor_auth_error_' . $user_id);
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes cached errors for a specific user.
|
||||
*
|
||||
* @param int $user_id The ID of the user to delete the errors for.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function delete_cached_errors(int $user_id): void
|
||||
{
|
||||
delete_transient('rsssl_two_factor_auth_error_' . $user_id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* This file contains the Rsssl_Provider_Loader class.
|
||||
* This class is responsible for loading and managing Two-Factor authentication providers.
|
||||
*
|
||||
* @package RSSSL\Pro\Security\WordPress\Two_Fa
|
||||
* @subpackage Providers
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use Exception;
|
||||
use WP_User;
|
||||
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Totp;
|
||||
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Email;
|
||||
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Status;
|
||||
|
||||
/**
|
||||
* Class Rsssl_Provider_Loader
|
||||
*
|
||||
* This class is responsible for loading and managing Two-Factor authentication providers.
|
||||
*
|
||||
* @package RSSSL\Pro\Security\WordPress\Two_Fa
|
||||
* @subpackage Providers
|
||||
*/
|
||||
class Rsssl_Provider_Loader {
|
||||
|
||||
public const METHODS = array( 'totp', 'email' ); // This is a list of all available methods.
|
||||
|
||||
/**
|
||||
* For each provider, include it and then instantiate it.
|
||||
*
|
||||
* @return array
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public static function get_providers(): array {
|
||||
$providers = array(
|
||||
Rsssl_Two_Factor_Email::class => __DIR__ . '/class-rsssl-two-factor-email.php',
|
||||
Rsssl_Two_Factor_Totp::class => __DIR__ . '/class-rsssl-two-factor-totp.php',
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the supplied providers.
|
||||
*
|
||||
* This lets third-parties either remove providers (such as Email), or
|
||||
* add their own providers (such as text message or Clef).
|
||||
*
|
||||
* @param array $providers A key-value array where the key is the class name, and
|
||||
* the value is the path to the file containing the class.
|
||||
*/
|
||||
$providers = apply_filters( 'rsssl_two_factor_providers', $providers );
|
||||
|
||||
/**
|
||||
* For each filtered provider,
|
||||
*/
|
||||
foreach ( $providers as $class => $path ) {
|
||||
include_once $path;
|
||||
|
||||
/**
|
||||
* Confirm that it's been successfully included before instantiating.
|
||||
*/
|
||||
if ( class_exists( $class ) ) {
|
||||
try {
|
||||
$providers[ $class ] = call_user_func( array( $class, 'get_instance' ) );
|
||||
} catch ( Exception $e ) {
|
||||
unset( $providers[ $class ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Two-Factor Auth providers that are enabled for the specified|current user.
|
||||
*
|
||||
* @param WP_User $user Optional. User ID, or WP_User object of the user. Defaults to current user.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_enabled_providers_for_user( WP_User $user ): array {
|
||||
|
||||
$enabled_providers = self::get_user_enabled_providers( $user );
|
||||
$statuses = Rsssl_Two_Fa_Status::get_user_two_fa_status( $user );
|
||||
|
||||
$forced_providers = array();
|
||||
|
||||
foreach ( $statuses as $method => $status ) {
|
||||
/**
|
||||
* Check if the provider is forced for the user.
|
||||
*
|
||||
* @var Rsssl_Two_Factor_Provider_Interface $provider_class
|
||||
*/
|
||||
$provider_class = 'RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_' . ucfirst( $method );
|
||||
|
||||
|
||||
if ( in_array( $status, array( 'active', 'open', 'disabled' ), true ) && $provider_class::is_enabled( $user ) ) {
|
||||
$forced_providers[] = $provider_class;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $forced_providers ) ) {
|
||||
$enabled_providers = $forced_providers;
|
||||
} else {
|
||||
foreach ( $enabled_providers as $key => $enabled_provider ) {
|
||||
/**
|
||||
* Check if the provider is optional for the user.
|
||||
*
|
||||
* @var Rsssl_Two_Factor_Provider_Interface $enabled_provider
|
||||
*/
|
||||
if ( ! $enabled_provider::is_optional( $user ) ) {
|
||||
unset( $enabled_providers[ $key ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
return $enabled_providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* This isn't currently set anywhere, but allows to add more providers in the future.
|
||||
*
|
||||
* @param WP_User $user The user to check.
|
||||
*
|
||||
* @return array|string[]
|
||||
*/
|
||||
public static function get_user_enabled_providers( WP_User $user ): array {
|
||||
$enabled_providers = array();
|
||||
if ( true === Rsssl_Two_Factor_Totp::is_enabled( $user ) ) {
|
||||
$enabled_providers[] = Rsssl_Two_Factor_Totp::class;
|
||||
}
|
||||
if ( true === Rsssl_Two_Factor_Email::is_enabled( $user ) ) {
|
||||
$enabled_providers[] = Rsssl_Two_Factor_Email::class;
|
||||
}
|
||||
|
||||
return $enabled_providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enabled providers for the user's roles.
|
||||
*
|
||||
* @param WP_User $user The user object.
|
||||
*
|
||||
* @return array The enabled providers.
|
||||
*/
|
||||
public static function get_enabled_providers_for_roles( WP_User $user ): array {
|
||||
// First get all the providers that are enabled for the user's role.
|
||||
$totp = Rsssl_Two_Factor_Totp::is_enabled( $user );
|
||||
$email = Rsssl_Two_Factor_Email::is_enabled( $user );
|
||||
|
||||
// Put the enabled providers in an array.
|
||||
$enabled_providers = array();
|
||||
if ( $totp ) {
|
||||
$enabled_providers[] = 'totp';
|
||||
}
|
||||
if ( $email ) {
|
||||
$enabled_providers[] = 'email';
|
||||
}
|
||||
|
||||
// Return the enabled providers.
|
||||
return $enabled_providers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* Holds the request parameters for a specific action.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use WP_REST_Request;
|
||||
|
||||
/**
|
||||
* Holds the request parameters for a specific action.
|
||||
* This class holds the request parameters for a specific action.
|
||||
* It is used to store the parameters and pass them to the functions.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
class Rsssl_Request_Parameters {
|
||||
/**
|
||||
* User ID.
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
public $user_id;
|
||||
|
||||
/**
|
||||
* Login nonce.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $login_nonce;
|
||||
|
||||
/**
|
||||
* User.
|
||||
*
|
||||
* @var WP_User
|
||||
*/
|
||||
public $user;
|
||||
|
||||
/**
|
||||
* Service provider.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
public $provider;
|
||||
|
||||
/**
|
||||
* Redirect to URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $redirect_to;
|
||||
|
||||
/**
|
||||
* The code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $code;
|
||||
|
||||
/**
|
||||
* The key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $key;
|
||||
|
||||
/**
|
||||
* The nonce.
|
||||
*
|
||||
* @var mixed|null
|
||||
*/
|
||||
public $nonce;
|
||||
/**
|
||||
* @var array|string
|
||||
*/
|
||||
public $token;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $profile;
|
||||
|
||||
/**
|
||||
* Constructor for the class.
|
||||
*
|
||||
* @param WP_REST_Request $request The WordPress REST request object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( WP_REST_Request $request ) {
|
||||
$this->user_id = $request->get_param( 'user_id' );
|
||||
$this->login_nonce = $request->get_param( 'login_nonce' );
|
||||
$this->nonce = $request->get_header( 'X-WP-Nonce' );
|
||||
$this->user = get_user_by( 'id', $this->user_id );
|
||||
$this->provider = $request->get_param( 'provider' );
|
||||
$this->redirect_to = $request->get_param( 'redirect_to' )?? admin_url();
|
||||
if ( 'totp' === $this->provider ) {
|
||||
$this->code = wp_unslash( $request->get_param( 'two-factor-totp-authcode' ) );
|
||||
$this->key = wp_unslash( $request->get_param( 'key' ) );
|
||||
}
|
||||
if ('email' === $this->provider) {
|
||||
$this->token = wp_unslash($request->get_param('token'));
|
||||
$this->profile = wp_unslash($request->get_param('profile')?? false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
/**
|
||||
* Two-Factor Authentication.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class Rsssl_Two_Fa_Authentication
|
||||
*
|
||||
* Represents the two-factor authentication functionality.
|
||||
*/
|
||||
class Rsssl_Two_Fa_Authentication {
|
||||
|
||||
/**
|
||||
* The user meta nonce key.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
public const RSSSL_USER_META_NONCE_KEY = '_rsssl_two_factor_nonce';
|
||||
|
||||
/**
|
||||
* Verify a login nonce for a user.
|
||||
*
|
||||
* @param int $user_id The ID of the user.
|
||||
* @param string $nonce The login nonce to verify.
|
||||
*
|
||||
* @return bool True if the nonce is valid and has not expired, false otherwise.
|
||||
*/
|
||||
public static function verify_login_nonce( int $user_id, string $nonce ): bool {
|
||||
$login_nonce = get_user_meta( $user_id, self::RSSSL_USER_META_NONCE_KEY, true );
|
||||
|
||||
if ( ! $login_nonce || empty( $login_nonce['rsssl_key'] ) || empty( $login_nonce['rsssl_expiration'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$unverified_nonce = array(
|
||||
'rsssl_user_id' => $user_id,
|
||||
'rsssl_expiration' => $login_nonce['rsssl_expiration'],
|
||||
'rsssl_key' => $nonce,
|
||||
);
|
||||
|
||||
$unverified_hash = self::hash_login_nonce( $unverified_nonce );
|
||||
$hashes_match = $unverified_hash && hash_equals( $login_nonce['rsssl_key'], $unverified_hash );
|
||||
|
||||
if ( $hashes_match && time() < $login_nonce['rsssl_expiration'] ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Require a fresh nonce if verification fails.
|
||||
self::delete_login_nonce( $user_id );
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a login nonce for a user.
|
||||
*
|
||||
* @param int $user_id The ID of the user.
|
||||
*
|
||||
* @return array|false The login nonce array if successfully created and stored, false otherwise.
|
||||
*/
|
||||
public static function create_login_nonce( int $user_id ) {
|
||||
$login_nonce = array(
|
||||
'rsssl_user_id' => $user_id,
|
||||
'rsssl_expiration' => time() + ( 15 * MINUTE_IN_SECONDS ),
|
||||
);
|
||||
|
||||
try {
|
||||
$login_nonce['rsssl_key'] = bin2hex( random_bytes( 32 ) );
|
||||
} catch ( Exception $ex ) {
|
||||
$login_nonce['rsssl_key'] = wp_hash( $user_id . wp_rand() . microtime(), 'nonce' );
|
||||
}
|
||||
|
||||
// Store the nonce hashed to avoid leaking it via database access.
|
||||
$hashed_key = self::hash_login_nonce( $login_nonce );
|
||||
|
||||
if ( $hashed_key ) {
|
||||
$login_nonce_stored = array(
|
||||
'rsssl_expiration' => $login_nonce['rsssl_expiration'],
|
||||
'rsssl_key' => $hashed_key,
|
||||
);
|
||||
|
||||
if ( update_user_meta( $user_id, self::RSSSL_USER_META_NONCE_KEY, $login_nonce_stored ) ) {
|
||||
return $login_nonce;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the login nonce.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return bool
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public static function delete_login_nonce( int $user_id ): bool {
|
||||
return delete_user_meta( $user_id, self::RSSSL_USER_META_NONCE_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash of a nonce for storage and comparison.
|
||||
*
|
||||
* @param array $nonce Nonce array to be hashed. ⚠️ This must contain user ID and expiration,
|
||||
* to guarantee the nonce only works for the intended user during the
|
||||
* intended time window.
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
protected static function hash_login_nonce( array $nonce ) {
|
||||
$message = wp_json_encode( $nonce );
|
||||
|
||||
if ( ! $message ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return wp_hash( $message, 'nonce' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
/**
|
||||
* Two-Factor Authentication Data Parameters helper.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
/**
|
||||
* Class Rsssl_Two_FA_Data_Parameters
|
||||
*
|
||||
* Represents the data parameters for the Two FA data.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
class Rsssl_Two_FA_Data_Parameters {
|
||||
|
||||
/**
|
||||
* The current page name.
|
||||
*
|
||||
* @var string $page The current page name.
|
||||
*/
|
||||
public $page;
|
||||
/**
|
||||
* The number of items to display per page.
|
||||
*
|
||||
* @var int $page_size The number of items to display per page.
|
||||
*/
|
||||
public $page_size;
|
||||
/**
|
||||
* The search term entered by the user.
|
||||
*
|
||||
* @var string $search_term The search term entered by the user
|
||||
*/
|
||||
public $search_term;
|
||||
/**
|
||||
* The value used for filtering.
|
||||
*
|
||||
* @var string|null $filter_value This variable stores the value used for filtering.
|
||||
*/
|
||||
public $filter_value;
|
||||
/**
|
||||
* The column used for filtering.
|
||||
*
|
||||
* @var string|null $filter_column This variable stores the column used for filtering.
|
||||
*/
|
||||
public $filter_column;
|
||||
/**
|
||||
* The column used for sorting.
|
||||
*
|
||||
* @var string|null $sort_column This variable stores the column used for sorting.
|
||||
*/
|
||||
public $sort_column;
|
||||
/**
|
||||
* The direction of the sorting, can be 'asc' or 'desc'.
|
||||
*
|
||||
* @var string $sort_direction The direction of the sorting, can be 'asc' or 'desc'
|
||||
*/
|
||||
public $sort_direction;
|
||||
/**
|
||||
* The HTTP method used for the current request, can be 'GET', 'POST', 'PUT', 'DELETE', etc.
|
||||
*
|
||||
* @var string $method The HTTP method used for the current request, can be 'GET', 'POST', 'PUT', 'DELETE', etc.
|
||||
*/
|
||||
public $method;
|
||||
|
||||
/**
|
||||
* The allowed filters.
|
||||
*
|
||||
* @var array $allowed_filters The allowed filters.
|
||||
*/
|
||||
private const allowed_filters = array( 'all', 'open', 'disabled', 'active', 'expired' );
|
||||
|
||||
/**
|
||||
* Constructs a new object with given data.
|
||||
*
|
||||
* @param array $data The data array.
|
||||
*/
|
||||
public function __construct( array $data ) {
|
||||
$this->page = isset( $data['currentPage'] ) ? (int) $data['currentPage'] : 1;
|
||||
$this->page_size = isset( $data['currentRowsPerPage'] ) ? (int) $data['currentRowsPerPage'] : 5;
|
||||
$this->search_term = isset( $data['search'] ) ? sanitize_text_field( $data['search'] ) : '';
|
||||
$this->filter_value = in_array( $data['filterValue'] ?? 'all', self::allowed_filters, true ) ? sanitize_text_field( $data['filterValue'] ?? 'all') : 'all';
|
||||
$this->sort_direction = in_array( strtoupper( $data['sortDirection'] ?? 'DESC' ), array( 'ASC', 'DESC' ), true ) ? strtoupper( sanitize_text_field( $data['sortDirection'] ?? 'DESC')) : 'DESC';
|
||||
$this->filter_column = isset( $data['filterColumn'] ) ? sanitize_text_field( $data['filterColumn'] ) : 'rsssl_two_fa_status';
|
||||
$this->sort_column = isset( $data['sortColumn'] ) ? sanitize_text_field( $data['sortColumn'] ) : 'user';
|
||||
$this->method = isset( $data['method'] ) ? Rsssl_Two_Factor_Settings::sanitize_method( $data['method'] ) : 'email';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* Two-Factor Authentication.
|
||||
* Status class.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use RSSSL\Security\WordPress\Two_Fa\Traits\Rsssl_Two_Fa_Helper;
|
||||
use WP_User;
|
||||
|
||||
|
||||
/**
|
||||
* Class Rsssl_Two_Fa_Status
|
||||
*
|
||||
* Represents the two-factor authentication status.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
class Rsssl_Two_Fa_Status {
|
||||
|
||||
use Rsssl_Two_Fa_Helper;
|
||||
|
||||
public const STATUSES = array( 'disabled', 'open', 'active' ); // This is a list of all available statuses.
|
||||
|
||||
/**
|
||||
* Get the status of two-factor authentication for a user.
|
||||
*
|
||||
* @param WP_User|null $user (optional) The user for which to retrieve the status. Defaults to current user.
|
||||
*
|
||||
* @return array An associative array where the method names are the keys and the status values are the values.
|
||||
* The status can be one of the following: 'disabled' if the method is disabled for the user,
|
||||
* 'enabled' if the method is enabled for the user, or 'unknown' if the status could not be determined.
|
||||
*/
|
||||
public static function get_user_two_fa_status( $user = null ): array {
|
||||
$methods = Rsssl_Provider_Loader::METHODS; // Assume this function returns all available methods.
|
||||
$statuses = array();
|
||||
|
||||
foreach ( $methods as $method ) {
|
||||
$status = self::get_user_status( $method, $user->ID );
|
||||
$statuses[ $method ] = $status ? $status : 'disabled';
|
||||
}
|
||||
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's two-factor authentication status.
|
||||
*
|
||||
* @param string $method The authentication method used by the user.
|
||||
* @param int $user_id The ID of the user.
|
||||
*
|
||||
* @return string The user's two-factor authentication status (enabled or disabled).
|
||||
*/
|
||||
public static function get_user_status( string $method, int $user_id ): string {
|
||||
$activated = $method === 'email' ? '_email' : '_' . self::sanitize_method( $method );
|
||||
|
||||
// Check the roles per method if they are enabled.
|
||||
$enabled_roles = rsssl_get_option( 'two_fa_enabled_roles'.$activated, array());
|
||||
|
||||
if ( empty( $enabled_roles ) && self::is_user_role_enabled( $user_id, $enabled_roles )) {
|
||||
return 'disabled';
|
||||
}
|
||||
|
||||
$status = get_user_meta( $user_id, "rsssl_two_fa_status_$method", true );
|
||||
|
||||
return self::sanitize_status( $status );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete two-factor authentication metadata for a user.
|
||||
*
|
||||
* @param WP_User $user The user object for whom to delete the metadata.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
public static function delete_two_fa_meta( $user ): void {
|
||||
if( is_object($user) ){
|
||||
$user = $user->ID;
|
||||
}
|
||||
delete_user_meta( $user, '_rsssl_two_factor_totp_last_successful_login' );
|
||||
delete_user_meta( $user, '_rsssl_two_factor_nonce' );
|
||||
delete_user_meta( $user, 'rsssl_two_fa_status' );
|
||||
delete_user_meta( $user, 'rsssl_two_fa_status_email' );
|
||||
delete_user_meta( $user, 'rsssl_two_fa_status_totp' );
|
||||
delete_user_meta( $user, '_rsssl_two_factor_totp_key' );
|
||||
delete_user_meta( $user, '_rsssl_two_factor_backup_codes' );
|
||||
delete_user_meta( $user, 'rsssl_activation_date' );
|
||||
delete_user_meta( $user, 'rsssl_two_fa_last_login' );
|
||||
delete_user_meta( $user, 'rsssl_two_fa_skip_token' );
|
||||
delete_user_meta( $user, '_rsssl_factor_email_token_timestamp' );
|
||||
delete_user_meta( $user, '_rsssl_factor_email_token' );
|
||||
delete_user_meta( $user, 'rsssl_two_fa_reminder_sent' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user has any of the enabled roles.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
* @param array $enabled_roles The enabled roles to check against.
|
||||
*
|
||||
* @return bool Returns true if the user has any of the enabled roles, false otherwise.
|
||||
*/
|
||||
private static function is_user_role_enabled( int $user_id, array $enabled_roles ):bool {
|
||||
$user = get_userdata( $user_id );
|
||||
|
||||
if ( ! $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user_roles = $user->roles;
|
||||
|
||||
foreach ( $user_roles as $role ) {
|
||||
if ( in_array( $role, $enabled_roles, true ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
<?php
|
||||
/**
|
||||
* This file contains the Rsssl_Two_Factor_Admin class.
|
||||
*
|
||||
* The Rsssl_Two_Factor_Admin class is responsible for handling the administrative
|
||||
* aspects of the two-factor authentication feature in the Really Simple SSL plugin.
|
||||
* It includes methods for displaying the two-factor authentication settings in the
|
||||
* admin area, handling user input, and managing user roles and capabilities related
|
||||
* to two-factor authentication.
|
||||
*
|
||||
* PHP version 7.2
|
||||
*
|
||||
* @category Security
|
||||
* @package Really_Simple_SSL
|
||||
* @author Really Simple SSL
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* The Rsssl_Two_Factor_Admin class is responsible for handling the administrative
|
||||
* aspects of the two-factor authentication feature in the Really Simple SSL plugin.
|
||||
* It includes methods for displaying the two-factor authentication settings in the
|
||||
* admin area, handling user input, and managing user roles and capabilities related
|
||||
* to two-factor authentication.
|
||||
*
|
||||
* @category Security
|
||||
* @package Really_Simple_SSL
|
||||
* @subpackage Two_Factor
|
||||
*/
|
||||
class Rsssl_Two_Factor_Admin
|
||||
{
|
||||
/**
|
||||
* The Rsssl_Two_Factor_Admin instance.
|
||||
*
|
||||
* @var Rsssl_Two_Factor_Settings $instance The settings object.
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// if the user is not logged in, it don't need to do anything.
|
||||
if (!rsssl_admin_logged_in()) {
|
||||
return;
|
||||
}
|
||||
if (isset(self::$instance)) {
|
||||
wp_die();
|
||||
}
|
||||
self::$instance = $this;
|
||||
add_filter('rsssl_do_action', array($this, 'two_fa_table'), 10, 3);
|
||||
add_filter('rsssl_after_save_field', array($this, 'maybe_reset_two_fa'), 20, 2);
|
||||
add_filter('rsssl_after_save_field', array($this, 'change_disabled_users_when_forced'), 20, 3);
|
||||
add_action('process_user_batch_event', [$this, 'process_user_batch'], 10 , 5);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles server-side processing for two-factor authentication data.
|
||||
*
|
||||
* @param Rsssl_Two_FA_Data_Parameters $data_parameters The data parameters for the request.
|
||||
*
|
||||
* @return array The response array containing the request success status, data, total records, and executed query.
|
||||
*/
|
||||
private function server_side_handler(Rsssl_Two_FA_Data_Parameters $data_parameters): array {
|
||||
global $wpdb;
|
||||
|
||||
$days_threshold = rsssl_get_option('two_fa_grace_period', 30);
|
||||
$filter_value = $data_parameters->filter_value;
|
||||
|
||||
$enabled_roles = array_unique(array_merge(
|
||||
defined('rsssl_pro') ? rsssl_get_option('two_fa_enabled_roles_totp', array()) : array(),
|
||||
rsssl_get_option('two_fa_enabled_roles_email', array())
|
||||
));
|
||||
|
||||
$forced_roles = rsssl_get_option('two_fa_forced_roles', array());
|
||||
|
||||
$fields = ['id', 'user', 'status_for_user', 'rsssl_two_fa_providers', 'user_role']; // Example fields
|
||||
$enabled_roles_placeholders = implode(',', array_map(function($role) { return "'$role'"; }, $enabled_roles));
|
||||
$forced_roles_placeholder = implode(',', array_map(function($role) { return "'$role'"; }, $forced_roles));
|
||||
$query = self::generate_query($fields, $enabled_roles_placeholders, $forced_roles_placeholder, $forced_roles);
|
||||
|
||||
if ($filter_value !== 'all') {
|
||||
$query .= $wpdb->prepare(" HAVING status_for_user = %s", $filter_value);
|
||||
}
|
||||
$prepared_query = $wpdb->prepare($query, array_merge(
|
||||
// Use array_map to generate the thresholds for each forced role
|
||||
array_fill(0, count($forced_roles), $days_threshold)
|
||||
));
|
||||
|
||||
// only execute query if there are enabled roles to show
|
||||
if (empty($enabled_roles)) {
|
||||
return array(
|
||||
'request_success' => true,
|
||||
'data' => [],
|
||||
'totalRecords' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
$results = $wpdb->get_results($prepared_query);
|
||||
|
||||
return array(
|
||||
'request_success' => true,
|
||||
'data' => is_array($results) ? array_values($results) : [],
|
||||
'totalRecords' => is_array($results) ? count($results) : 0,
|
||||
// 'executed_query' => $prepared_query,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the SELECT clause for the SQL query.
|
||||
*
|
||||
* @param array $fields The fields to include in the SELECT clause.
|
||||
* @return string The generated SELECT clause.
|
||||
*/
|
||||
public static function generate_select_clause(array $fields, array $forced_roles): string
|
||||
{
|
||||
$select_parts = [];
|
||||
|
||||
if ( in_array( 'id', $fields, true ) ) {
|
||||
$select_parts[] = 'DISTINCT (u.ID) as id';
|
||||
}
|
||||
|
||||
if ( in_array( 'user', $fields, true ) ) {
|
||||
$select_parts[] = 'u.user_login as user';
|
||||
}
|
||||
|
||||
// Status for User Field
|
||||
if (in_array('status_for_user', $fields, true)) {
|
||||
// Create placeholders for forced roles
|
||||
$forced_roles_placeholders = implode(',', array_fill(0, count($forced_roles), '%s'));
|
||||
|
||||
// Check if forced_roles is empty or not
|
||||
if (empty($forced_roles_placeholders)) {
|
||||
// No forced roles, basic status handling
|
||||
$select_parts[] = "
|
||||
CASE
|
||||
WHEN COALESCE(um_totp.meta_value, 'open') = 'open' OR COALESCE(um_email.meta_value, 'open') = 'open' THEN 'open'
|
||||
WHEN COALESCE(um_totp.meta_value, 'disabled') = 'active' OR COALESCE(um_email.meta_value, 'disabled') = 'active' THEN 'active'
|
||||
ELSE COALESCE(um_totp.meta_value, um_email.meta_value)
|
||||
END AS status_for_user
|
||||
";
|
||||
} else {
|
||||
// Initialize the CASE statement parts for status_for_user
|
||||
$status_cases = [];
|
||||
|
||||
// First condition: Check if TOTP or Email is active (this is common for all roles)
|
||||
$status_cases[] = "WHEN COALESCE(um_totp.meta_value, 'disabled') = 'active' OR COALESCE(um_email.meta_value, 'disabled') = 'active' THEN 'active'";
|
||||
|
||||
// Loop through forced roles and apply expiration logic
|
||||
foreach ($forced_roles as $role) {
|
||||
// Check if an expiration threshold is defined for the current role
|
||||
$status_cases[] = "WHEN SUBSTRING_INDEX(SUBSTRING_INDEX(ur.meta_value, '\"', 2), '\"', -1) = '$role'
|
||||
AND DATEDIFF(NOW(), um_last_login.meta_value) > %d THEN 'expired'";
|
||||
}
|
||||
|
||||
// Fallback: If no other conditions match, default to 'open'
|
||||
$status_cases[] = "ELSE COALESCE(um_totp.meta_value, um_email.meta_value, 'open')";
|
||||
|
||||
// Combine the conditions into a CASE clause
|
||||
$select_parts[] = "CASE " . implode(' ', $status_cases) . " END AS status_for_user";
|
||||
}
|
||||
}
|
||||
|
||||
if ( in_array( 'user_role', $fields, true ) ) {
|
||||
$select_parts[] = "SUBSTRING_INDEX(SUBSTRING_INDEX(ur.meta_value, '\"', 2), '\"', -1) AS user_role";
|
||||
}
|
||||
|
||||
if ( in_array( 'rsssl_two_fa_providers', $fields, true ) ) {
|
||||
$select_parts[] = "
|
||||
CASE
|
||||
WHEN COALESCE(um_totp.meta_value, um_email.meta_value, 'open') = 'open' THEN ''
|
||||
WHEN um_totp.meta_value = 'active' THEN 'totp'
|
||||
WHEN um_email.meta_value = 'active' THEN 'email'
|
||||
ELSE 'none'
|
||||
END AS rsssl_two_fa_providers
|
||||
";
|
||||
}
|
||||
|
||||
return implode(', ', $select_parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the full SQL query.
|
||||
*
|
||||
* @param array $fields The fields to include in the SELECT clause.
|
||||
* @param string $enabled_roles_placeholders The placeholders for enabled roles.
|
||||
* @param string|null $forced_roles_placeholder The placeholders for forced roles.
|
||||
* @return string The generated SQL query.
|
||||
*/
|
||||
public static function generate_query(array $fields, string $enabled_roles_placeholders, string $forced_roles_placeholder = '', $forced_roles = array() ): string
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$select_clause = self::generate_select_clause($fields, $forced_roles);
|
||||
|
||||
$where_clause = "SUBSTRING_INDEX(SUBSTRING_INDEX(ur.meta_value, '\"', 2), '\"', -1) in ($enabled_roles_placeholders)";
|
||||
// if (!empty($forced_roles_placeholder)) {
|
||||
// $where_clause = "SUBSTRING_INDEX(SUBSTRING_INDEX(ur.meta_value, '\"', 2), '\"', -1) in ($forced_roles_placeholder)";
|
||||
// }
|
||||
|
||||
$sql = "
|
||||
SELECT $select_clause
|
||||
FROM {$wpdb->users} u
|
||||
LEFT JOIN {$wpdb->usermeta} um_totp ON u.ID = um_totp.user_id AND um_totp.meta_key = 'rsssl_two_fa_status_totp'
|
||||
LEFT JOIN {$wpdb->usermeta} um_email ON u.ID = um_email.user_id AND um_email.meta_key = 'rsssl_two_fa_status_email'
|
||||
";
|
||||
if (is_multisite()) {
|
||||
$sites = get_sites();
|
||||
$conditions = [];
|
||||
foreach ($sites as $site) {
|
||||
$conditions[] = "ur.meta_key = '{$wpdb->get_blog_prefix($site->blog_id)}capabilities'";
|
||||
}
|
||||
$sql .= "LEFT JOIN {$wpdb->usermeta} ur ON u.ID = ur.user_id AND (" . implode(' OR ', $conditions) . ")";
|
||||
} else {
|
||||
$sql .= "LEFT JOIN {$wpdb->usermeta} ur ON u.ID = ur.user_id AND ur.meta_key = '{$wpdb->base_prefix}capabilities'";
|
||||
}
|
||||
|
||||
|
||||
$sql .="LEFT JOIN {$wpdb->usermeta} la ON u.ID = la.user_id AND la.meta_key = 'rsssl_two_fa_login_action'
|
||||
LEFT JOIN {$wpdb->usermeta} um_last_login ON u.ID = um_last_login.user_id AND um_last_login.meta_key = 'rsssl_two_fa_last_login'
|
||||
WHERE $where_clause
|
||||
";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private static function user_count(): ?string {
|
||||
global $wpdb;
|
||||
return $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->users");
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the disabled status of users when forced.
|
||||
*
|
||||
* @param string $field_id The ID of the field being changed.
|
||||
* @param mixed $new_value The new value of the field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function change_disabled_users_when_forced( string $field_id, $new_value, $prev_value = [] ): void
|
||||
{
|
||||
if ('two_fa_forced_roles' === $field_id && !empty($new_value)) {
|
||||
global $wpdb;
|
||||
$forced_roles = $new_value;
|
||||
if (empty($prev_value)) {
|
||||
$prev_value = [];
|
||||
}
|
||||
$added_roles = array_diff($forced_roles, $prev_value);
|
||||
|
||||
$forced_roles = $added_roles;
|
||||
if(empty($forced_roles)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetching the users that have the forced roles.
|
||||
$fields = ['id', 'status_for_user'];
|
||||
$enabled_roles = array_unique(array_merge(
|
||||
defined('rsssl_pro') ? rsssl_get_option('two_fa_enabled_roles_totp', array()) : array(),
|
||||
rsssl_get_option('two_fa_enabled_roles_email', array())
|
||||
));
|
||||
//This line is forcefully setting the forced roles to the enabled roles. Because we only impact enforced users with this action.
|
||||
$enabled_roles_placeholders = implode(',', array_map(function($role) { return "'$role'"; }, $forced_roles));
|
||||
$forced_roles_placeholder = implode(',', array_map(function($role) { return "'$role'"; }, $forced_roles));
|
||||
$query = self::generate_query($fields, $enabled_roles_placeholders, $forced_roles_placeholder, $forced_roles);
|
||||
|
||||
$batch_size = 1000;
|
||||
$offset = 0;
|
||||
|
||||
$this->process_user_batch($query, $enabled_roles, $forced_roles, $batch_size, $offset);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a batch of users.
|
||||
*
|
||||
* @param string $query The base query to fetch users.
|
||||
* @param array $enabled_roles The enabled roles.
|
||||
* @param array $forced_roles The forced roles.
|
||||
* @param int $batch_size The size of each batch.
|
||||
* @param int $offset The offset for the current batch.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function process_user_batch(string $query, array $enabled_roles, array $forced_roles, int $batch_size, int $offset): void
|
||||
{
|
||||
global $wpdb;
|
||||
$paged_query = $query . " LIMIT %d OFFSET %d";
|
||||
$forced_roles_placeholder = implode(',', $forced_roles);
|
||||
$enabled_roles_placeholders = implode(',', $enabled_roles);
|
||||
$prepared_query = $wpdb->prepare($paged_query, $forced_roles_placeholder, $batch_size, $offset);
|
||||
$users = $wpdb->get_results($prepared_query);
|
||||
|
||||
if (empty($users)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($users as $user) {
|
||||
// if there is an active or open method, We do nothing.
|
||||
if ('active' === $user->status_for_user ) {
|
||||
continue;
|
||||
}
|
||||
if ('open' === $user->status_for_user) {
|
||||
// if the user has no meta_key rsssl_two_fa_last_login, we set it to now.
|
||||
if (!get_user_meta((int)$user->id, 'rsssl_two_fa_last_login', true)) {
|
||||
update_user_meta((int)$user->id, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// now we reset the user.
|
||||
Rsssl_Two_Fa_Status::delete_two_fa_meta((int)$user->id);
|
||||
// Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa.
|
||||
update_user_meta((int)$user->id, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
// Schedule the next batch
|
||||
wp_schedule_single_event(time() + 60, 'process_user_batch_event', [$query, $enabled_roles, $forced_roles, $batch_size, $offset + $batch_size]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user can use two-factor authentication (2FA).
|
||||
*
|
||||
* @return bool Returns true if the user can use 2FA, false otherwise.
|
||||
*/
|
||||
public function can_i_use_2fa(): bool
|
||||
{
|
||||
return rsssl_get_option('login_protection_enabled');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a captcha notice array.
|
||||
*
|
||||
* This method creates and returns an array representing a captcha notice.
|
||||
*
|
||||
* @param string $title The title of the notice.
|
||||
* @param string $msg The message of the notice.
|
||||
*
|
||||
* @return array The captcha notice array.
|
||||
*/
|
||||
private function create_2fa_notice( string $title, string $msg ): array {
|
||||
return array(
|
||||
'callback' => '_true_',
|
||||
'score' => 1,
|
||||
'show_with_options' => array( 'login_protection_enabled' ),
|
||||
'output' => array(
|
||||
'true' => array(
|
||||
'title' => $title,
|
||||
'msg' => $msg,
|
||||
'icon' => 'warning',
|
||||
'type' => 'open',
|
||||
'dismissible' => true,
|
||||
'admin_notice' => false,
|
||||
'plusone' => true,
|
||||
'highlight_field_id' => 'two_fa_enabled_roles',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a user role is removed, it needs to reset this role for all users
|
||||
*
|
||||
* @param string $field_id The field ID.
|
||||
* @param mixed $new_value The new value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function maybe_reset_two_fa( string $field_id, $new_value ): void {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the two-factor authentication for a user.
|
||||
*
|
||||
* @param array $response The response array.
|
||||
* @param string $action The action being performed.
|
||||
* @param array $data The data array.
|
||||
*
|
||||
* @return array The updated response array.
|
||||
*/
|
||||
public static function reset_user_two_fa( array $response, string $action, array $data ): array {
|
||||
if ( ! rsssl_user_can_manage() ) {
|
||||
return $response;
|
||||
}
|
||||
if ( 'two_fa_table' === $action ) {
|
||||
// if the user has been disabled, it needs to reset the two-factor authentication.
|
||||
$user = get_user_by( 'id', $data['user_id'] );
|
||||
if ( $user ) {
|
||||
// Delete all 2fa related user meta.
|
||||
self::delete_two_fa_meta( $user );
|
||||
// Set the last login to now, so the user will be forced to use 2fa.
|
||||
update_user_meta( $user->ID, 'rsssl_two_fa_last_login', gmdate( 'Y-m-d H:i:s' ) );
|
||||
}
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users based on arguments and method.
|
||||
*
|
||||
* @param array $args The arguments to retrieve users.
|
||||
* @param string $method The method to retrieve users.
|
||||
*
|
||||
* @return array The list of users matching the arguments and method.
|
||||
*/
|
||||
protected static function get_users( array $args, string $method ): array {
|
||||
if ( ! is_multisite() ) {
|
||||
return get_users( $args );
|
||||
}
|
||||
|
||||
$users = self::get_multisite_users( $args );
|
||||
if( $method !== 'two_fa_forced_roles' ) {
|
||||
$users = self::filter_users_by_role( $users, $args, $method );
|
||||
}
|
||||
|
||||
return self::slice_users_by_offset_and_number( $users, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all multisite users from all sites.
|
||||
*
|
||||
* @param array $args {
|
||||
* Optional. Arguments for filtering the users.
|
||||
*
|
||||
* @type int $offset Offset for pagination. Default is 0.
|
||||
* @type int $number Maximum number of users to retrieve. Default is 0 (retrieve all users).
|
||||
* ... Additional arguments for filtering the user query.
|
||||
* }
|
||||
*
|
||||
* @return array Array of users.
|
||||
*/
|
||||
private static function get_multisite_users( array $args ): array {
|
||||
$sites = get_sites();
|
||||
$users = array();
|
||||
|
||||
unset( $args['offset'], $args['number'] );
|
||||
|
||||
foreach ( $sites as $site ) {
|
||||
switch_to_blog( $site->blog_id );
|
||||
$site_users = get_users( $args );
|
||||
foreach ( $site_users as $user ) {
|
||||
$user_roles = get_userdata( $user->ID )->roles;
|
||||
if ( ! isset( $users[ $user->ID ] ) ) {
|
||||
$users[ $user->ID ] = $user;
|
||||
}
|
||||
$users_roles[ $user->ID ] = array_unique( $users_roles[ $user->ID ] ?? array() + $user_roles );
|
||||
}
|
||||
restore_current_blog();
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter users by role.
|
||||
*
|
||||
* @param array $users The array of users.
|
||||
* @param array $args The array of filter arguments.
|
||||
* @param string $method The method name.
|
||||
*
|
||||
* @return array The filtered array of users.
|
||||
*/
|
||||
private static function filter_users_by_role( array $users, array $args, string $method ): array {
|
||||
if ( ! isset( $args['role'] ) ) {
|
||||
return $users;
|
||||
}
|
||||
|
||||
$filter_role = $args['role'];
|
||||
$filter_role_is_forced = Rsssl_Two_Factor_Settings::role_is_of_type( $method, $filter_role, 'forced' );
|
||||
|
||||
return array_filter(
|
||||
$users,
|
||||
static function ( $user_id, $user_roles ) use ( $filter_role_is_forced, $method ) {
|
||||
return ! ( ! $filter_role_is_forced && Rsssl_Two_Factor_Settings::contains_role_of_type( $method, (array) $user_roles, 'forced' ) );
|
||||
},
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice users by offset and number.
|
||||
*
|
||||
* This function takes an array of users and an array of arguments
|
||||
* and applies the offset and number values to the users array.
|
||||
* It returns a new array with the specified offset and number of users.
|
||||
*
|
||||
* @param array $users The array of users.
|
||||
* @param array $args The array of arguments containing the offset and number values.
|
||||
*
|
||||
* @return array The new array of users with the specified offset and number.
|
||||
*/
|
||||
private static function slice_users_by_offset_and_number( array $users, array $args ): array {
|
||||
// Apply the 'offset' to the combined result.
|
||||
if ( 0 !== ( $args['offset'] ?? 0 ) ) {
|
||||
$users = array_slice( $users, $args['offset'] );
|
||||
}
|
||||
// Ensure the final result does not exceed the specified 'number'.
|
||||
if ( 0 !== ( $args['number'] ?? 0 ) ) {
|
||||
$users = array_slice( $users, 0, $args['number'] );
|
||||
}
|
||||
|
||||
// To reset array keys.
|
||||
return array_values( $users );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates the two-factor authentication table data based on the action and data parameters.
|
||||
*
|
||||
* @param array $response The initial response data.
|
||||
* @param string $action The action to perform.
|
||||
* @param array $data The data needed for the action.
|
||||
*
|
||||
* @return array The updated response data.
|
||||
*/
|
||||
public function two_fa_table(array $response, string $action, array $data): array
|
||||
{
|
||||
$new_response = $response;
|
||||
if (rsssl_user_can_manage()) {
|
||||
$data_parameters = new Rsssl_Two_FA_Data_Parameters($data);
|
||||
|
||||
switch ($action) {
|
||||
case 'two_fa_table':
|
||||
return $this->server_side_handler($data_parameters);
|
||||
case 'two_fa_reset_user':
|
||||
// if the user has been disabled, it needs to reset the two-factor authentication.
|
||||
$user = get_user_by('id', $data['id']);
|
||||
|
||||
if ($user) {
|
||||
// Delete all 2fa related user meta.
|
||||
Rsssl_Two_Fa_Status::delete_two_fa_meta($user);
|
||||
// Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa.
|
||||
update_user_meta($user->ID, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
|
||||
}
|
||||
if (!$user) {
|
||||
$new_response['request_success'] = false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Default case if no action matches.
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $new_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset two-factor authentication for a user if the user has been disabled.
|
||||
*
|
||||
* @param string $method The method to reset.
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function check_status_and_return(string $method, int $user_id): ?array
|
||||
{
|
||||
$status = Rsssl_Two_Factor_Settings::get_user_status($method, $user_id);
|
||||
if (in_array($status, array('active', 'open', 'disabled'), true)) {
|
||||
return array($method, $status, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status for a given user ID, by method.
|
||||
*
|
||||
* @param int $user_id The user ID to get the status for.
|
||||
*
|
||||
* @return array The status for the given user ID, by method.
|
||||
*/
|
||||
public function get_status_by_method(int $user_id): array
|
||||
{
|
||||
$user_id = absint($user_id);
|
||||
if (defined('rsssl_pro') && rsssl_pro) {
|
||||
$result = $this->get_status_for_method('totp', $user_id);
|
||||
}
|
||||
|
||||
if (!isset($result)) {
|
||||
$result = $this->get_status_for_method('email', $user_id);
|
||||
} else {
|
||||
if ($result[0] === 'empty' || 'disabled' === $result[1]) {
|
||||
$result = $this->get_status_for_method('email', $user_id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (empty($result) || 'disabled' === $result[1]) {
|
||||
$result = array('disabled', 'disabled');
|
||||
}
|
||||
|
||||
if (empty($result)) {
|
||||
$enabled_roles = Rsssl_Two_Factor_Settings::get_enabled_roles($user_id) ?? array();
|
||||
$enabled_method = Rsssl_Two_Factor_Settings::get_enabled_method($user_id);
|
||||
|
||||
$result = empty($enabled_roles)
|
||||
? array($enabled_method, 'disabled')
|
||||
: array($enabled_method, 'open');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status for a given method and user ID.
|
||||
*
|
||||
* @param string $method The method to get the status for.
|
||||
* @param int $user_id The user ID to get the status for.
|
||||
*
|
||||
* @return array|null The status for the given method and user ID, or null if not found.
|
||||
*/
|
||||
public function get_status_for_method(string $method, int $user_id): ?array
|
||||
{
|
||||
$role_status = Rsssl_Two_Factor_Settings::get_role_status($method, $user_id);
|
||||
$user_status = Rsssl_Two_Factor_Settings::get_user_status($method, $user_id);
|
||||
|
||||
if ('empty' !== $role_status && 'open' === $user_status) {
|
||||
$result = $this->check_status_and_return($method, $user_id);
|
||||
if ('active' === $user_status) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
return array($role_status, $user_status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for creating a backup codes provider.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* Class for creating a backup codes provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
class Rsssl_Two_Factor_Backup_Codes extends Rsssl_Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* The user meta backup codes key.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
public const BACKUP_CODES_META_KEY = '_rsssl_two_factor_backup_codes';
|
||||
|
||||
/**
|
||||
* The number backup codes.
|
||||
*
|
||||
* @type int
|
||||
*/
|
||||
public const NUMBER_OF_CODES = 10;
|
||||
|
||||
/**
|
||||
* Ensures only one instance of this class exists in memory at any one time.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public static function get_instance() {
|
||||
static $instance;
|
||||
$class = __CLASS__;
|
||||
if ( ! is_a( $instance, $class ) ) {
|
||||
$instance = new $class();
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
protected function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
|
||||
add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
|
||||
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all backup codes for a user.
|
||||
*
|
||||
* @param int $id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function delete_backup_codes( int $id ): void {
|
||||
delete_user_meta( $id, self::BACKUP_CODES_META_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the rest-api endpoints required for this provider.
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function register_rest_routes(): void {
|
||||
register_rest_route(
|
||||
Rsssl_Two_Factor::REST_NAMESPACE,
|
||||
'/generate-backup-codes',
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'rest_generate_codes' ),
|
||||
'permission_callback' => function ( $request ) {
|
||||
return current_user_can( 'edit_user', $request['user_id'] );
|
||||
},
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
),
|
||||
'enable_provider' => array(
|
||||
'required' => false,
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an admin notice when backup codes have run out.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function admin_notices(): void {
|
||||
$user = wp_get_current_user();
|
||||
|
||||
// Return if the provider is not enabled.
|
||||
if ( ! in_array( __CLASS__, Rsssl_Two_Factor::get_enabled_providers_for_user( $user->ID ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return if not out of codes.
|
||||
if ( $this->is_available_for_user( $user ) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="error">
|
||||
<p>
|
||||
<span>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
sprintf(
|
||||
/* translators: %s: URL for code regeneration */
|
||||
__( 'Two-Factor: You are out of backup codes and need to <a href="%s">regenerate!</a>', 'really-simple-ssl' ),
|
||||
esc_url( get_edit_user_link( $user->ID ) . '#two-factor-backup-codes' )
|
||||
),
|
||||
array( 'a' => array( 'href' => true ) )
|
||||
);
|
||||
?>
|
||||
<span>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function get_label(): ?string {
|
||||
return _x( 'Backup Verification Codes (Single Use)', 'Provider Label', 'really-simple-ssl' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and codes are available for the user specified.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available_for_user( $user ): bool {
|
||||
// Does this user have available codes?
|
||||
if ( 0 < self::codes_remaining_for_user( $user ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts markup at the end of the user profile field for this provider.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function user_options( WP_User $user ): void {
|
||||
wp_enqueue_script( 'wp-api-request' );
|
||||
wp_enqueue_script( 'jquery' );
|
||||
|
||||
$count = self::codes_remaining_for_user( $user );
|
||||
?>
|
||||
<p id="two-factor-backup-codes">
|
||||
<button type="button" class="button button-two-factor-backup-codes-generate button-secondary hide-if-no-js">
|
||||
<?php esc_html_e( 'Generate Verification Codes', 'really-simple-ssl' ); ?>
|
||||
</button>
|
||||
<span class="two-factor-backup-codes-count">
|
||||
<?php
|
||||
echo esc_html(
|
||||
sprintf(
|
||||
/* translators: %s: count */
|
||||
_n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'really-simple-ssl' ),
|
||||
$count
|
||||
)
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
<div class="two-factor-backup-codes-wrapper" style="display:none;">
|
||||
<ol class="two-factor-backup-codes-unused-codes"></ol>
|
||||
<p class="description"><?php esc_html_e( 'Write these down! Once you navigate away from this page, you will not be able to view these codes again.', 'really-simple-ssl' ); ?></p>
|
||||
<p>
|
||||
<a class="button button-two-factor-backup-codes-download button-secondary hide-if-no-js" href="javascript:void(0);" id="two-factor-backup-codes-download-link" download="two-factor-backup-codes.txt"><?php esc_html_e( 'Download Codes', 'really-simple-ssl' ); ?></a>
|
||||
<p>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
( function( $ ) {
|
||||
$( '.button-two-factor-backup-codes-generate' ).click( function() {
|
||||
wp.apiRequest( {
|
||||
method: 'POST',
|
||||
path: <?php echo wp_json_encode( Rsssl_Two_Factor::REST_NAMESPACE . '/generate-backup-codes' ); ?>,
|
||||
data: {
|
||||
user_id: <?php echo wp_json_encode( $user->ID ); ?>
|
||||
}
|
||||
} ).then( function( response ) {
|
||||
var $codesList = $( '.two-factor-backup-codes-unused-codes' );
|
||||
|
||||
$( '.two-factor-backup-codes-wrapper' ).show();
|
||||
$codesList.html( '' );
|
||||
|
||||
// Append the codes.
|
||||
for ( i = 0; i < response.codes.length; i++ ) {
|
||||
$codesList.append( '<li>' + response.codes[ i ] + '</li>' );
|
||||
}
|
||||
|
||||
// Update counter.
|
||||
$( '.two-factor-backup-codes-count' ).html( response.i18n.count );
|
||||
$( '#two-factor-backup-codes-download-link' ).attr( 'href', response.download_link );
|
||||
} );
|
||||
} );
|
||||
} )( jQuery );
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates backup codes & updates the user meta.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @param array $args Optional arguments for assigning new codes.
|
||||
*
|
||||
* @return array
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public static function generate_codes( WP_User $user, array $args = array() ): array {
|
||||
$codes = array();
|
||||
$codes_hashed = array();
|
||||
|
||||
// Check for arguments.
|
||||
if ( isset( $args['number'] ) ) {
|
||||
$num_codes = (int) $args['number'];
|
||||
} else {
|
||||
$num_codes = self::NUMBER_OF_CODES;
|
||||
}
|
||||
|
||||
// Append or replace (default).
|
||||
if ( isset( $args['method'] ) && 'append' === $args['method'] ) {
|
||||
$codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
}
|
||||
|
||||
for ( $i = 0; $i < $num_codes; $i++ ) {
|
||||
$code = self::get_code();
|
||||
$codes_hashed[] = wp_hash_password( $code );
|
||||
$codes[] = $code;
|
||||
unset( $code );
|
||||
}
|
||||
if ( isset( $args['cached'] ) && $args['cached'] ) {
|
||||
// Place the $codes in a transient for 5 minutes.
|
||||
set_transient( 'rsssl_two_factor_backup_codes_' . $user->ID, $codes, 300 );
|
||||
}
|
||||
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $codes_hashed );
|
||||
|
||||
// Unhashed.
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates Backup Codes for returning through the WordPress Rest API.
|
||||
*
|
||||
* @param WP_REST_Request $request The WordPress REST request object.
|
||||
*
|
||||
* @since 0.8.0
|
||||
*/
|
||||
public function rest_generate_codes( WP_REST_Request $request ) {
|
||||
$user_id = $request['user_id'];
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
|
||||
// Hardcode these, the user shouldn't be able to choose them.
|
||||
$args = array(
|
||||
'number' => self::NUMBER_OF_CODES,
|
||||
'method' => 'replace',
|
||||
);
|
||||
|
||||
// Setup the return data.
|
||||
$codes = $this->generate_codes( $user, $args );
|
||||
$count = self::codes_remaining_for_user( $user );
|
||||
$title = sprintf(
|
||||
/* translators: %s: the site's domain */
|
||||
__( 'Two-Factor Backup Codes for %s', 'really-simple-ssl' ),
|
||||
home_url( '/' )
|
||||
);
|
||||
|
||||
// Generate download content.
|
||||
$download_link = 'data:application/text;charset=utf-8,';
|
||||
$download_link .= rawurlencode( "{$title}\r\n\r\n" );
|
||||
|
||||
$i = 1;
|
||||
foreach ( $codes as $code ) {
|
||||
$download_link .= rawurlencode( "{$i}. {$code}\r\n" );
|
||||
++$i;
|
||||
}
|
||||
|
||||
$i18n = array(
|
||||
/* translators: %s: count */
|
||||
'count' => esc_html( sprintf( _n( '%s unused code remaining.', '%s unused codes remaining.', $count, 'really-simple-ssl' ), $count ) ),
|
||||
);
|
||||
|
||||
if ( $request->get_param( 'enable_provider' ) && ! Rsssl_Two_Factor::enable_provider_for_user( $user_id, 'Two_Factor_Backup_Codes' ) ) {
|
||||
return new WP_Error( 'db_error', __( 'Unable to enable Backup Codes provider for this user.', 'really-simple-ssl' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
return array(
|
||||
'codes' => $codes,
|
||||
'download_link' => $download_link,
|
||||
'remaining' => $count,
|
||||
'i18n' => $i18n,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of unused codes for the specified user
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return int $int The number of unused codes remaining
|
||||
*/
|
||||
public static function codes_remaining_for_user( $user ) {
|
||||
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
|
||||
return count( $backup_codes );
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function authentication_page( WP_User $user ) {
|
||||
require_once ABSPATH . '/wp-admin/includes/template.php';
|
||||
?>
|
||||
<p class="two-factor-prompt"><?php esc_html_e( 'Enter a backup verification code.', 'really-simple-ssl' ); ?></p>
|
||||
<p>
|
||||
<label for="authcode"><?php esc_html_e( 'Verification Code:', 'really-simple-ssl' ); ?></label>
|
||||
<input type="text" inputmode="numeric" name="two-factor-backup-code" id="authcode" class="input authcode" value="" size="20" pattern="[0-9 ]*" placeholder="1234 5678" data-digits="8" />
|
||||
</p>
|
||||
<?php
|
||||
submit_button( __( 'Submit', 'really-simple-ssl' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* In this class just return true.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_authentication( $user ) {
|
||||
$backup_code = self::sanitize_code_from_request( 'two-factor-backup-code' );
|
||||
if ( ! $backup_code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::validate_code( $user, $backup_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a backup code.
|
||||
*
|
||||
* Backup Codes are single use and are deleted upon a successful validation.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @param int $code The backup code.
|
||||
* @return boolean
|
||||
*@since 0.1-dev
|
||||
*
|
||||
*/
|
||||
public static function validate_code(WP_User $user, int $code, bool $delete = true): bool {
|
||||
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
|
||||
if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
|
||||
foreach ( $backup_codes as $code_index => $code_hashed ) {
|
||||
if ( wp_check_password( $code, $code_hashed, $user->ID ) ) {
|
||||
if ( $delete ) {
|
||||
self::delete_code( $user, $code_hashed );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a backup code.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @param string $code_hashed The hashed the backup code.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public static function delete_code( WP_User $user, string $code_hashed ): void {
|
||||
$backup_codes = get_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, true );
|
||||
|
||||
// Delete the current code from the list since it's been used.
|
||||
$backup_codes = array_flip( $backup_codes );
|
||||
unset( $backup_codes[ $code_hashed ] );
|
||||
$backup_codes = array_values( array_flip( $backup_codes ) );
|
||||
|
||||
// Update the backup code master list.
|
||||
update_user_meta( $user->ID, self::BACKUP_CODES_META_KEY, $backup_codes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a user is selectable for a specific authentication method.
|
||||
*
|
||||
* @param WP_User $user The user object to check.
|
||||
*
|
||||
* @return bool Returns true if the user is selectable for the authentication method, false otherwise.
|
||||
*/
|
||||
public static function is_selectable_for_user( WP_User $user ): bool {
|
||||
// TODO: Logic for when backup codes are needed (for now only totp).
|
||||
if ( Rsssl_Two_Factor_Totp::is_selectable_for_user( $user ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* A compatibility layer for some of the most popular plugins.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
/**
|
||||
* A compatibility layer for some of the most popular plugins.
|
||||
*
|
||||
* Should be used with care because ideally we wouldn't need
|
||||
* any integration specific code for this plugin. Everything should
|
||||
* be handled through clever use of hooks and best practices.
|
||||
*/
|
||||
class Rsssl_Two_Factor_Compat {
|
||||
/**
|
||||
* Initialize all the custom hooks as necessary.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
/**
|
||||
* Jetpack
|
||||
*
|
||||
* @see https://wordpress.org/plugins/jetpack/
|
||||
*/
|
||||
add_filter( 'rsssl_two_factor_rememberme', array( $this, 'jetpack_rememberme' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Jetpack single sign-on wants long-lived sessions for users.
|
||||
*
|
||||
* @param boolean $rememberme Current state of the "remember me" toggle.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function jetpack_rememberme( $rememberme ) {
|
||||
$action = filter_input( INPUT_GET, 'action', FILTER_CALLBACK, array( 'options' => 'sanitize_key' ) );
|
||||
|
||||
if ( 'jetpack-sso' === $action && $this->jetpack_is_sso_active() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $rememberme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to detect the presence of the active SSO module.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function jetpack_is_sso_active() {
|
||||
return ( method_exists( '\Jetpack', 'is_module_active' ) && \Jetpack::is_module_active( 'sso' ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
<?php
|
||||
/**
|
||||
* Class for creating an email provider.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
/**
|
||||
* Class for creating an email provider.
|
||||
*
|
||||
* @since 7.0.6
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
//require_once __DIR__ . '/class-rsssl-two-factor-provider.php';
|
||||
//require_once __DIR__ . '/interface-rsssl-provider-interface.php';
|
||||
require_once rsssl_path . 'mailer/class-mail.php';
|
||||
|
||||
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Settings;
|
||||
use rsssl_mailer;
|
||||
use Exception;
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* Generate and email the user token.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @return void
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
class Rsssl_Two_Factor_Email extends Rsssl_Two_Factor_Provider implements Rsssl_Two_Factor_Provider_Interface {
|
||||
|
||||
/**
|
||||
* The user meta token key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const RSSSL_TOKEN_META_KEY = '_rsssl_factor_email_token';
|
||||
|
||||
/**
|
||||
* Store the timestamp when the token was generated.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const RSSSL_TOKEN_META_KEY_TIMESTAMP = '_rsssl_factor_email_token_timestamp';
|
||||
|
||||
/**
|
||||
* Name of the input field used for code resend.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const RSSSL_INPUT_NAME_RESEND_CODE = 'rsssl-two-factor-email-code-resend';
|
||||
public const SECRET_META_KEY = 'rsssl_two_fa_email_enabled';
|
||||
|
||||
public const METHOD = 'email';
|
||||
|
||||
public const NAME = 'Email';
|
||||
|
||||
/**
|
||||
* Ensures only one instance of this class exists in memory at any one time.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public static function get_instance() {
|
||||
static $instance;
|
||||
$class = __CLASS__;
|
||||
if ( ! is_a( $instance, $class ) ) {
|
||||
$instance = new $class();
|
||||
}
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
protected function __construct() {
|
||||
add_action( 'rsssl_two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) );
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function get_label(): string {
|
||||
return _x( 'Email', 'Provider Label', 'really-simple-ssl' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the user token.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return string
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function generate_token( int $user_id ): string {
|
||||
$token = self::get_code();
|
||||
|
||||
update_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY_TIMESTAMP, time() );
|
||||
update_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY, wp_hash( $token ) );
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has a valid token already.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return boolean If user has a valid email token.
|
||||
*/
|
||||
public function user_has_token( int $user_id ): bool {
|
||||
$hashed_token = $this->get_user_token( $user_id );
|
||||
|
||||
if ( ! empty( $hashed_token ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the user token validity timestamp expired.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function user_token_has_expired( int $user_id ): bool {
|
||||
|
||||
$token_lifetime = $this->user_token_lifetime( $user_id );
|
||||
$token_ttl = $this->user_token_ttl( $user_id );
|
||||
|
||||
// Invalid token lifetime is considered an expired token.
|
||||
return ! ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lifetime of a user token in seconds.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @return integer|null Return `null` if the lifetime can't be measured.
|
||||
*/
|
||||
public function user_token_lifetime( $user_id ) {
|
||||
$timestamp = (int) get_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY_TIMESTAMP, true );
|
||||
|
||||
if ( ! empty( $timestamp ) ) {
|
||||
return time() - $timestamp;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the token time-to-live for a user.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function user_token_ttl( int $user_id ): int {
|
||||
$token_ttl = 15 * MINUTE_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Number of seconds the token is considered valid
|
||||
* after the generation.
|
||||
*
|
||||
* @param integer $token_ttl Token time-to-live in seconds.
|
||||
* @param integer $user_id User ID.
|
||||
*/
|
||||
return (int) apply_filters( 'rsssl_two_factor_token_ttl', $token_ttl, $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication token for the user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return string|boolean User token or `false` if no token found.
|
||||
*/
|
||||
public function get_user_token( int $user_id ) {
|
||||
$hashed_token = get_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY, true );
|
||||
|
||||
if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
|
||||
return $hashed_token;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the user token.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $token User token.
|
||||
*
|
||||
* @return boolean
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function validate_token( int $user_id, string $token ): bool {
|
||||
|
||||
$hashed_token = $this->get_user_token( $user_id );
|
||||
|
||||
// Bail if token is empty or it doesn't match.
|
||||
if ( empty( $hashed_token ) || ! hash_equals( wp_hash( $token ), $hashed_token ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->user_token_has_expired( $user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the token can be used only once.
|
||||
$this->delete_token( $user_id );
|
||||
|
||||
update_user_meta( $user_id, 'rsssl_two_fa_status_email', 'active' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user token.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function delete_token( int $user_id ): void {
|
||||
delete_user_meta( $user_id, self::RSSSL_TOKEN_META_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate and email the user token.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @return void
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function generate_and_email_token( WP_User $user, $profile = false ): void {
|
||||
$token = $this->generate_token( $user->ID );
|
||||
|
||||
$skip_two_fa_url = Rsssl_Two_Factor_Settings::rsssl_one_time_login_url( $user->ID, false, $profile );
|
||||
// Add skip button to email content.
|
||||
$skip_button_html = sprintf(
|
||||
'<a href="%s" class="button" style="padding: 10px 30px; background: #2A7ABF; border-color: #2A7ABF; color: #fff; text-decoration: none; text-shadow: none; display: inline-block; margin-top: 15px; font-size: 0.8125rem; font-weight: 300; transition: all .3s ease; min-height: 10px;">' . __( 'Continue', 'really-simple-ssl' ) . '</a>',
|
||||
esc_url( $skip_two_fa_url )
|
||||
);
|
||||
|
||||
/* translators: %s: site name */
|
||||
$subject = wp_strip_all_tags( sprintf( __( 'Your login confirmation code for %s', 'really-simple-ssl' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) );
|
||||
/* translators: %s: token */
|
||||
$token_cleaned = wp_strip_all_tags( $token );
|
||||
|
||||
// insert whitespace after four characters in the $token, for readability.
|
||||
$token_cleaned = preg_replace( '/(.{4})/', '$1 ', $token_cleaned );
|
||||
|
||||
$token_html = sprintf(
|
||||
'
|
||||
<table cellspacing="0" cellpadding="0" border="0" width="100%%" style="margin-top: 25px;background-color:white; box-shadow: 1px 3px 0 1px rgba(211, 211, 211, 0.3); height: 180px;"> <!-- Further increased height for white box -->
|
||||
<tr>
|
||||
<td style="padding: 45px 10px 10px 10px; vertical-align: middle; font-size: 18px; font-weight:700; text-align: center;">%s</td> <!-- Increased padding for top and bottom -->
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 10px 20px 45px 20px; vertical-align: middle; text-align: center;">%s</td> <!-- Increased padding for bottom -->
|
||||
</tr>
|
||||
</table>',
|
||||
$token_cleaned,
|
||||
$skip_button_html
|
||||
);
|
||||
|
||||
if($profile) {
|
||||
$message = sprintf(
|
||||
__( "Below you'll find the email activation code for %1\$s. It's valid for 15 minutes. %2\$s", 'really-simple-ssl' ),
|
||||
site_url(),
|
||||
$token_html
|
||||
);
|
||||
} else {
|
||||
$message = sprintf(
|
||||
__( "Below you will find your login code for %1\$s. It's valid for 15 minutes. %2\$s", 'really-simple-ssl' ),
|
||||
site_url(),
|
||||
$token_html
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter the token email subject.
|
||||
*
|
||||
* @param string $subject The email subject line.
|
||||
* @param int $user_id The ID of the user.
|
||||
*/
|
||||
$subject = apply_filters( 'rsssl_two_factor_token_email_subject', $subject, $user->ID );
|
||||
|
||||
/**
|
||||
* Filter the token email message.
|
||||
*
|
||||
* @param string $message The email message.
|
||||
* @param string $token The token.
|
||||
* @param int $user_id The ID of the user.
|
||||
*/
|
||||
$message = apply_filters( 'rsssl_two_factor_token_email_message', $message, $token, $user->ID );
|
||||
|
||||
if ( ! class_exists( 'rsssl_mailer' ) ) {
|
||||
require_once rsssl_path . 'mailer/class-mail.php';
|
||||
}
|
||||
|
||||
$mailer = new rsssl_mailer();
|
||||
$mailer->subject = $subject;
|
||||
$mailer->branded = false;
|
||||
/* translators: %s is replaced with the site url */
|
||||
$mailer->sent_by_text = "<b>" . sprintf( __( 'Notification by %s', 'really-simple-ssl' ), site_url() ) . "</b>";
|
||||
$mailer->template_filename = apply_filters( 'rsssl_email_template', rsssl_path . '/mailer/templates/email-unbranded.html' );
|
||||
$mailer->to = $user->user_email;
|
||||
$mailer->title = __( 'Hi', 'really-simple-ssl' ) . ' ' . $user->display_name . ',';
|
||||
$mailer->message = $message;
|
||||
$mailer->send_mail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function authentication_page( WP_User $user ): void {
|
||||
|
||||
if ( ! $user ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) {
|
||||
$this->generate_and_email_token( $user );
|
||||
}
|
||||
|
||||
require_once ABSPATH . '/wp-admin/includes/template.php';
|
||||
?>
|
||||
<p class="two-factor-prompt"><?php esc_html_e( 'A verification code has been sent to the email address associated with your account.', 'really-simple-ssl' ); ?></p>
|
||||
<p>
|
||||
<label for="rsssl-authcode"><?php esc_html_e( 'Verification Code:', 'really-simple-ssl' ); ?></label>
|
||||
<input type="text" inputmode="numeric" name="rsssl-two-factor-email-code" id="rsssl-authcode" class="input rsssl-authcode" value="" size="20" pattern="[0-9 ]*" placeholder="1234 5678" data-digits="8" />
|
||||
<?php submit_button( __( 'Log In', 'really-simple-ssl' ) ); ?>
|
||||
</p>
|
||||
<p class="rsssl-two-factor-email-resend">
|
||||
<input type="submit" class="button" name="<?php echo esc_attr( self::RSSSL_INPUT_NAME_RESEND_CODE ); ?>" value="<?php esc_attr_e( 'Resend Code', 'really-simple-ssl' ); ?>" />
|
||||
</p>
|
||||
<script type="text/javascript">
|
||||
setTimeout( function(){
|
||||
var d;
|
||||
try{
|
||||
d = document.getElementById('rsssl-authcode');
|
||||
d.value = '';
|
||||
d.focus();
|
||||
} catch(e){}
|
||||
}, 200);
|
||||
</script>
|
||||
<?php
|
||||
$provider = get_user_meta( $user->ID, 'rsssl_two_fa_status_email', true );
|
||||
|
||||
foreach ( $user->roles as $role ) {
|
||||
// Never show the skip link if a role is a forced role.
|
||||
$two_fa_forced_roles = is_array(rsssl_get_option('two_fa_forced_roles'))
|
||||
? rsssl_get_option('two_fa_forced_roles')
|
||||
: [];
|
||||
|
||||
if (in_array($role, $two_fa_forced_roles, true)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If optional and open, allow the user to skip 2FA for now.
|
||||
if ( 'open' === $provider && in_array( $role, rsssl_get_option( 'two_fa_enabled_roles_email', array() ), true ) ) {
|
||||
|
||||
$skip_two_fa_url = Rsssl_Two_Factor_Settings::rsssl_one_time_login_url( $user->ID, true );
|
||||
|
||||
?>
|
||||
<a class="rsssl-skip-link" href="<?php echo esc_url( $skip_two_fa_url ); ?>" style="display: flex; justify-content: center; margin: 15px 20px 0 0;">
|
||||
<?php esc_html_e( "Don't use Two-Factor Authentication", 'really-simple-ssl' ); ?>
|
||||
</a>
|
||||
<?php
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the email code if missing or requested. Stop the authentication
|
||||
* validation if a new token has been generated and sent.
|
||||
*
|
||||
* @param WP_USer $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function pre_process_authentication( $user ): bool {
|
||||
if ( isset( $user->ID ) && isset( $_REQUEST[ self::RSSSL_INPUT_NAME_RESEND_CODE ] ) ) {
|
||||
$this->generate_and_email_token( $user );
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function validate_authentication( $user ): bool {
|
||||
|
||||
$code = self::sanitize_code_from_request( 'rsssl-two-factor-email-code' );
|
||||
if ( ! isset( $user->ID ) || ! $code ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->validate_token( $user->ID, $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and available for the user specified.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_available_for_user( $user ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts markup at the end of the user profile field for this provider.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function user_options( WP_User $user ): void {
|
||||
$email = $user->user_email;
|
||||
?>
|
||||
<div>
|
||||
<?php
|
||||
echo esc_html(
|
||||
sprintf(
|
||||
/* translators: %s: email address */
|
||||
__( 'Authentication codes will be sent to %s.', 'really-simple-ssl' ),
|
||||
$email
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is forced to use two-factor authentication.
|
||||
*
|
||||
* @param WP_User $user The user object.
|
||||
*
|
||||
* @return bool Whether the user is forced to use two-factor authentication.
|
||||
*/
|
||||
public static function is_forced( WP_User $user ): bool {
|
||||
// If there is no user logged in, it can't check if the user is forced.
|
||||
if ( ! $user->exists() ) {
|
||||
return false;
|
||||
}
|
||||
return Rsssl_Two_Factor_Settings::get_role_status( 'email', $user->ID ) === 'forced';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is Optional.
|
||||
*
|
||||
* @param WP_User $user The user object.
|
||||
*
|
||||
* @return bool Whether the user is optional or not.
|
||||
*/
|
||||
public static function is_optional( WP_User $user ): bool {
|
||||
if ( ! $user->exists() ) {
|
||||
return false;
|
||||
}
|
||||
$user_roles = $user->roles;
|
||||
$optional_roles = rsssl_get_option( 'two_fa_enabled_roles_email' );
|
||||
if ( empty( $optional_roles ) ) {
|
||||
$optional_roles = array();
|
||||
}
|
||||
if ( 'disabled' === Rsssl_Two_Factor_Settings::get_user_status( 'email', $user->ID ) ) {
|
||||
return false;
|
||||
}
|
||||
return in_array( $user_roles[0], $optional_roles, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user status for two-factor authentication.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $status The status to set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function set_user_status( int $user_id, string $status ): void {
|
||||
update_user_meta( $user_id, 'rsssl_two_fa_status_email', $status );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the HTML for the selection option.
|
||||
*
|
||||
* @param WP_User $user The user object.
|
||||
* @param bool $checked Whether the option is checked or not.
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception Throws an exception if the template file is not found.
|
||||
*/
|
||||
public static function get_selection_option( $user, bool $checked = false ): void {
|
||||
// Get the preferred method meta, which could be a string or an array.
|
||||
$preferred_method_meta = get_user_meta( $user->ID, 'rsssl_two_fa_set_provider', true );
|
||||
// Normalize the preferred method to always be an array.
|
||||
$preferred_methods = is_array( $preferred_method_meta ) ? $preferred_method_meta : (array) $preferred_method_meta;
|
||||
// Check if 'Rsssl_Two_Factor_Email' is the preferred method.
|
||||
$is_preferred = in_array( 'Rsssl_Two_Factor_Email', $preferred_methods, true );
|
||||
$is_enabled = (bool) get_user_meta( $user->ID, self::SECRET_META_KEY, true );
|
||||
$badge_class = $is_enabled ? 'badge-enabled' : 'badge-default';
|
||||
$enabled_text = $is_enabled ? esc_html__( 'Enabled', 'really-simple-ssl' ) : esc_html__( 'Disabled', 'really-simple-ssl' );
|
||||
$checked_attribute = $checked ? 'checked' : '';
|
||||
$title = esc_html__( 'Email', 'really-simple-ssl' );
|
||||
$description = esc_html__( 'Receive a code by email', 'really-simple-ssl' );
|
||||
|
||||
// Load the template.
|
||||
rsssl_load_template(
|
||||
'selectable-option.php',
|
||||
array(
|
||||
'badge_class' => $badge_class,
|
||||
'enabled_text' => $enabled_text,
|
||||
'checked_attribute' => $checked_attribute,
|
||||
'title' => $title,
|
||||
'type' => 'email', // Used this to identify the provider.
|
||||
'forcible' => in_array( $user->roles[0], (array) rsssl_get_option( 'two_fa_forced_roles' ), true ),
|
||||
'description' => $description,
|
||||
'user' => $user,
|
||||
),
|
||||
rsssl_path . 'assets/templates/two_fa'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is enabled based on their role.
|
||||
*
|
||||
* @param WP_User $user The user object to check.
|
||||
*
|
||||
* @return bool Whether the user is enabled or not.
|
||||
*/
|
||||
public static function is_enabled( WP_User $user ): bool {
|
||||
// todo - Do we need to check for a pro version here too?
|
||||
if ( ! $user->exists() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get and normalize the user roles.
|
||||
$user_roles = $user->roles;
|
||||
if ( ! is_array( $user_roles ) ) {
|
||||
$user_roles = array();
|
||||
}
|
||||
|
||||
if ( is_multisite() ) {
|
||||
$user_roles = Rsssl_Two_Factor_Settings::get_strictest_role_across_sites($user->ID, ['email']);
|
||||
}
|
||||
|
||||
// Get and normalize enabled roles.
|
||||
$enabled_roles = rsssl_get_option( 'two_fa_enabled_roles_email' );
|
||||
if ( ! is_array( $enabled_roles ) ) {
|
||||
$enabled_roles = array();
|
||||
}
|
||||
|
||||
// Return true if one of the user roles is in the enabled roles.
|
||||
if ( (count($user_roles) > 1) || is_multisite() ) {
|
||||
return count(array_intersect($user_roles, $enabled_roles)) > 0;
|
||||
}
|
||||
|
||||
// If the user role is in the enabled roles, return true.
|
||||
$firstKey = ( empty($user_roles) ? 0 : array_key_first($user_roles) );
|
||||
$firstFoundRole = $user_roles[$firstKey];
|
||||
return in_array( $firstFoundRole, $enabled_roles, true );
|
||||
}
|
||||
|
||||
public static function is_configured( WP_User $user ): bool {
|
||||
$status = get_user_meta( $user->ID, 'rsssl_two_fa_status_email', true );
|
||||
return 'active' === $status;
|
||||
}
|
||||
|
||||
public static function get_status( WP_User $user ): string {
|
||||
return Rsssl_Two_Factor_Settings::get_user_status( 'email', $user->ID );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles the API routes for the two-factor authentication onboarding process.
|
||||
* This class is responsible for handling the API routes for the two-factor authentication onboarding process.
|
||||
* It registers the routes and handles the requests.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
* @subpackage Security\WordPress\Two_Fa
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use Exception;
|
||||
use RSSSL\Pro\Security\WordPress\Limitlogin\Rsssl_IP_Fetcher;
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* Registers API routes for the application.
|
||||
* This class is responsible for registering the API routes for the two-factor authentication onboarding process.
|
||||
* It registers the routes and handles the requests.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
* @subpackage Security\WordPress\Two_Fa
|
||||
*/
|
||||
class Rsssl_Two_Factor_On_Board_Api
|
||||
{
|
||||
/**
|
||||
* Initializes the object and registers API routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_api_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the requested namespace matches our specific namespace and bypasses authentication.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request object.
|
||||
*/
|
||||
private function check_custom_validation( WP_REST_Request $request ): bool {
|
||||
// first check if the $-REQUEST['rest_route'] is set.
|
||||
$params = new Rsssl_Request_Parameters( $request );
|
||||
if ( ! isset( $params->login_nonce ) ) {
|
||||
return false;
|
||||
}
|
||||
return Rsssl_Two_Fa_Authentication::verify_login_nonce( $params->user_id, $params->login_nonce );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a login nonce, gets user by the user id, and returns an error response if any steps fail.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function check_login_and_get_user( int $user_id, string $login_nonce ): WP_User {
|
||||
if ( ! Rsssl_Two_Fa_Authentication::verify_login_nonce( $user_id, $login_nonce ) ) {
|
||||
// We throw an error
|
||||
wp_die();
|
||||
}
|
||||
/**
|
||||
* Get the user by the user ID.
|
||||
*
|
||||
* @var WP_User $user
|
||||
*/
|
||||
$user = get_user_by('id', $user_id);
|
||||
if (!$user) {
|
||||
throw new Exception('User not found');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the authentication cookie and returns a success response.
|
||||
*
|
||||
*/
|
||||
private function authenticate_and_redirect( int $user_id, string $redirect_to = '' ): WP_REST_Response {
|
||||
// Okay checked the provider now authenticate the user.
|
||||
wp_set_auth_cookie( $user_id, true );
|
||||
// Finally redirect the user to the redirect_to page or to the home page if the redirect_to is not set.
|
||||
$redirect_to = $redirect_to ?: home_url();
|
||||
return new WP_REST_Response( array( 'redirect_to' => $redirect_to ), 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the process of email validation for a user.
|
||||
*
|
||||
* @param int $user_id The ID of the user for whom the email validation process needs to be started.
|
||||
* @param string $redirect_to The URL to redirect the user after the email validation process. Default is an empty string.
|
||||
*
|
||||
* @return WP_REST_Response The REST response object.
|
||||
*/
|
||||
private function start_email_validation(int $user_id, string $redirect_to = '', $profile = false): WP_REST_Response
|
||||
{
|
||||
$redirect_to = $redirect_to ?: home_url();
|
||||
$user = get_user_by('id', $user_id);
|
||||
// Sending the email with the code.
|
||||
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, $profile);
|
||||
$token = get_user_meta( $user_id, Rsssl_Two_Factor_Email::RSSSL_TOKEN_META_KEY, true );
|
||||
if ( $redirect_to === 'profile') {
|
||||
return new WP_REST_Response( array( 'token' => $token, 'validation_action' => 'validate_email_setup' ), 200 );
|
||||
}
|
||||
return new WP_REST_Response( array( 'token' => $token, 'redirect_to' => $redirect_to, 'validation_action' => 'validate_email_setup' ), 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user provider as email and redirects the user to the specified page.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request object.
|
||||
*
|
||||
* @return WP_REST_Response The REST response object if user is not logged in or provider is invalid.
|
||||
*/
|
||||
public function set_as_email( WP_REST_Request $request ): WP_REST_Response {
|
||||
$parameters = new Rsssl_Request_Parameters($request);
|
||||
try {
|
||||
$this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce);
|
||||
} catch (Exception $e) {
|
||||
return new WP_REST_Response(['error' => $e->getMessage()], 403);
|
||||
}
|
||||
if ('email' !== $parameters->provider) {
|
||||
return new WP_REST_Response(['error' => 'Invalid provider'], 401);
|
||||
}
|
||||
|
||||
return $this->start_email_validation($parameters->user_id, $parameters->redirect_to, $parameters->profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the profile email for a user.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request object.
|
||||
*
|
||||
* @return WP_REST_Response The REST response object.
|
||||
*/
|
||||
public function set_profile_email(WP_REST_Request $request ): WP_REST_Response {
|
||||
$parameters = new Rsssl_Request_Parameters($request);
|
||||
try {
|
||||
$this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce);
|
||||
} catch (Exception $e) {
|
||||
return new WP_REST_Response(['error' => $e->getMessage()], 403);
|
||||
}
|
||||
if ('email' !== $parameters->provider) {
|
||||
return new WP_REST_Response(['error' => 'Invalid provider'], 401);
|
||||
}
|
||||
|
||||
return $this->start_email_validation($parameters->user_id, $parameters->redirect_to, $parameters->profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the email setup for a user.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request object.
|
||||
*
|
||||
* @return WP_REST_Response The REST response object.
|
||||
*/
|
||||
public function validate_email_setup(WP_REST_Request $request ): WP_REST_Response {
|
||||
$parameters = new Rsssl_Request_Parameters($request);
|
||||
|
||||
if ('email' !== $parameters->provider) {
|
||||
return new WP_REST_Response(['error' => 'Invalid provider'], 401);
|
||||
}
|
||||
|
||||
/*
|
||||
* This will result in a wp_die otherwise it will return the user.
|
||||
*/
|
||||
$user = $this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce);
|
||||
|
||||
if (!Rsssl_Two_Factor_Email::get_instance()->validate_token($user->ID, self::sanitize_token($parameters->token))) {
|
||||
Rsssl_Two_Factor_Email::set_user_status($user->ID, 'open');
|
||||
Rsssl_Two_Factor_Totp::set_user_status($user->ID, 'open');
|
||||
wp_logout();
|
||||
return new WP_REST_Response(['error' => __('Code was invalid, try "Resend Code"', 'really-simple.ssl')], 401);
|
||||
}
|
||||
|
||||
Rsssl_Two_Factor_Email::set_user_status($user->ID, 'active');
|
||||
Rsssl_Two_Factor_Totp::set_user_status($user->ID, 'disabled');
|
||||
self::set_other_providers_inactive($user->ID, 'email');
|
||||
|
||||
return $this->authenticate_and_redirect($user->ID, $parameters->redirect_to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends the email code for a user.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request object.
|
||||
*
|
||||
* @return WP_REST_Response The REST response object.
|
||||
*/
|
||||
public function resend_email_code( WP_REST_Request $request ): WP_REST_Response {
|
||||
$parameters = new Rsssl_Request_Parameters( $request );
|
||||
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($parameters->user, $parameters->profile);
|
||||
return new WP_REST_Response( array( 'message' => __('Verification code re-sent', 'really-simple.ssl') ), 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the 2FA code for TOTP.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request object.
|
||||
*
|
||||
* @return WP_REST_Response The REST response object.
|
||||
*/
|
||||
public function verify_2fa_code_totp( WP_REST_Request $request ): WP_REST_Response {
|
||||
$parameters = new Rsssl_Request_Parameters( $request );
|
||||
$user = $this->check_login_and_get_user( $parameters->user_id, $parameters->login_nonce );
|
||||
// Check if the provider.
|
||||
if ( 'totp' !== $parameters->provider ) {
|
||||
$response = new WP_REST_Response( array( 'error' => __('Invalid provider', 'really-simple-ssl') ), 400 );
|
||||
}
|
||||
|
||||
//This is an extra check so someone who thinks to use backup codes can't use them.
|
||||
$code_backup = Rsssl_Two_Factor_Backup_Codes::sanitize_code_from_request( 'authcode', 8 );
|
||||
if ( $code_backup && Rsssl_Two_Factor_Backup_Codes::validate_code( $user, $code_backup, false ) ) {
|
||||
$error_message = __('Invalid Two Factor Authentication code.', 'really-simple-ssl');
|
||||
return new WP_REST_Response( array( 'error' => $error_message ), 400 );
|
||||
}
|
||||
|
||||
if ( Rsssl_Two_Factor_Totp::setup_totp( $user, $parameters->key, $parameters->code ) ) {
|
||||
Rsssl_Two_Factor_Totp::set_user_status( $user->ID, 'active' );
|
||||
Rsssl_Two_Factor_Email::set_user_status( $user->ID, 'disabled' );
|
||||
// Mark all other statuses as inactive.
|
||||
self::set_other_providers_inactive( $user->ID, 'totp' );
|
||||
// Finally we redirect the user to the redirect_to page.
|
||||
return $this->authenticate_and_redirect( $parameters->user_id, $parameters->redirect_to );
|
||||
}
|
||||
|
||||
// We get the error message from the setup_totp function.
|
||||
$error_message = get_transient( 'rsssl_error_message_' . $user->ID );
|
||||
// We delete the transient.
|
||||
delete_transient( 'rsssl_error_message_' . $user->ID );
|
||||
return new WP_REST_Response( array( 'error' => $error_message ), 400 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables two-factor authentication for the user.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request object.
|
||||
*
|
||||
* @return WP_REST_Response The REST response object.
|
||||
*/
|
||||
public function disable_two_fa_for_user( WP_REST_Request $request ): WP_REST_Response {
|
||||
$parameters = new Rsssl_Request_Parameters($request);
|
||||
try {
|
||||
$user = $this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce);
|
||||
} catch (Exception $e) {
|
||||
return new WP_REST_Response(['error' => $e->getMessage()], 403);
|
||||
}
|
||||
|
||||
$user_available_providers = Rsssl_Provider_Loader::get_providers();
|
||||
foreach ($user_available_providers as $provider) {
|
||||
$provider::set_user_status($user->ID, 'disabled');
|
||||
}
|
||||
|
||||
return $this->authenticate_and_redirect($parameters->user_id, $parameters->redirect_to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the onboarding process for the user.
|
||||
*
|
||||
* @param WP_REST_Request $request The REST request object.
|
||||
*
|
||||
* @return WP_REST_Response The REST response object.
|
||||
*/
|
||||
public function skip_onboarding( WP_REST_Request $request ): WP_REST_Response {
|
||||
$parameters = new Rsssl_Request_Parameters( $request );
|
||||
// As a double we check the user_id with the login nonce.
|
||||
try {
|
||||
$this->check_login_and_get_user($parameters->user_id, $parameters->login_nonce);
|
||||
} catch (Exception $e) {
|
||||
return new WP_REST_Response(['error' => $e->getMessage()], 403);
|
||||
}
|
||||
return $this->authenticate_and_redirect( $parameters->user_id, $parameters->redirect_to );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers API routes for the application.
|
||||
*/
|
||||
public function register_api_routes(): void {
|
||||
register_rest_route(
|
||||
Rsssl_Two_Factor::REST_NAMESPACE,
|
||||
'/save_default_method_email',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'set_as_email' ),
|
||||
'permission_callback' => function ( WP_REST_Request $request ) {
|
||||
return true; // Allow all requests; handle auth in the callback.
|
||||
},
|
||||
'args' => array(
|
||||
'provider' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
),
|
||||
'login_nonce' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
Rsssl_Two_Factor::REST_NAMESPACE,
|
||||
'/save_default_method_email_profile',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'set_profile_email' ),
|
||||
'permission_callback' => function ( WP_REST_Request $request ) {
|
||||
return true; // Allow all requests; handle auth in the callback.
|
||||
},
|
||||
'args' => array(
|
||||
'provider' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
),
|
||||
'login_nonce' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
Rsssl_Two_Factor::REST_NAMESPACE,
|
||||
'/validate_email_setup',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'validate_email_setup' ),
|
||||
'permission_callback' => function (WP_REST_Request $request) {
|
||||
$login_actions = array('onboarding', 'email'); // Define allowed login actions here
|
||||
return $this->permission_callback_login_actions($request, $login_actions);
|
||||
},
|
||||
'args' => array(
|
||||
'provider' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
),
|
||||
'login_nonce' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
'redirect_to' => array(
|
||||
'required' => false,
|
||||
'type' => 'string',
|
||||
),
|
||||
'token' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
Rsssl_Two_Factor::REST_NAMESPACE,
|
||||
'/resend_email_code',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'resend_email_code' ),
|
||||
'permission_callback' => function (WP_REST_Request $request) {
|
||||
$login_actions = array('onboarding', 'email'); // Define allowed login actions here
|
||||
return $this->permission_callback_login_actions($request, $login_actions);
|
||||
},
|
||||
'args' => array(
|
||||
'provider' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
),
|
||||
'login_nonce' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
Rsssl_Two_Factor::REST_NAMESPACE,
|
||||
'/save_default_method_totp',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'verify_2fa_code_totp' ),
|
||||
'permission_callback' => function ( WP_REST_Request $request ) {
|
||||
return true; // Allow all requests; handle auth in the callback.
|
||||
},
|
||||
'args' => array(
|
||||
'two-factor-totp-authcode' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
'provider' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
'key' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
'redirect_to' => array(
|
||||
'required' => false,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
Rsssl_Two_Factor::REST_NAMESPACE,
|
||||
'do_not_ask_again',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'disable_two_fa_for_user' ),
|
||||
'permission_callback' => function (WP_REST_Request $request) {
|
||||
$login_actions = array('onboarding'); // Define allowed login actions here
|
||||
return $this->permission_callback_login_actions($request, $login_actions);
|
||||
},
|
||||
'args' => array(
|
||||
'redirect_to' => array(
|
||||
'required' => false,
|
||||
'type' => 'string',
|
||||
),
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
),
|
||||
'login_nonce' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
Rsssl_Two_Factor::REST_NAMESPACE,
|
||||
'skip_onboarding',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'skip_onboarding' ),
|
||||
'permission_callback' => function (WP_REST_Request $request) {
|
||||
$login_actions = array('onboarding'); // Define allowed login actions here
|
||||
return $this->permission_callback_login_actions($request, $login_actions);
|
||||
},
|
||||
'args' => array(
|
||||
'redirect_to' => array(
|
||||
'required' => false,
|
||||
'type' => 'string',
|
||||
),
|
||||
'user_id' => array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
),
|
||||
'login_nonce' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all other providers to inactive.
|
||||
*
|
||||
* @param int $id The user ID.
|
||||
* @param string $allowed_method The allowed method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function set_other_providers_inactive( int $id, string $allowed_method ): void {
|
||||
// First we get all the available providers for the user.
|
||||
// We get the user from the id.
|
||||
$user_available_providers = Rsssl_Provider_Loader::get_enabled_providers_for_user( get_user_by( 'id', $id ) );
|
||||
foreach ( $user_available_providers as $provider ) {
|
||||
$namespace_parts = explode( '\\', $provider );
|
||||
$last_key = end( $namespace_parts );
|
||||
// we explode the last key to get the provider name.
|
||||
$provider_name = explode( '_', $last_key );
|
||||
$provider_name = end( $provider_name );
|
||||
if ( ucfirst( $allowed_method ) !== $provider_name ) {
|
||||
$provider::set_user_status( $id, 'disabled' );
|
||||
}
|
||||
if ( 'email' === $allowed_method ) {
|
||||
//We delete backup codes if email is the provider.
|
||||
Rsssl_Two_Factor_Backup_Codes::delete_backup_codes( $id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a token.
|
||||
*
|
||||
* @param string $token The token to sanitize.
|
||||
* @param int $length The expected length of the token. Default is 0.
|
||||
*
|
||||
* @return string|false The sanitized token, or false if the length is invalid.
|
||||
*/
|
||||
public static function sanitize_token(string $token, int $length = 0 ) {
|
||||
$code = wp_unslash( $token );
|
||||
$code = preg_replace( '/\s+/', '', $code );
|
||||
|
||||
// Maybe validate the length.
|
||||
if ( $length && strlen( $code ) !== $length ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string) $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission callback for routes that need to:
|
||||
* 1) Verify a login nonce
|
||||
* 2) Ensure the user exists
|
||||
* 3) Check that "email" is the active login action (as an example)
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public function permission_callback_login_actions( WP_REST_Request $request, array $login_actions ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$login_nonce = $request->get_param( 'login_nonce' );
|
||||
|
||||
// we check if the login nonce is a string.
|
||||
if ( ! is_string( $login_nonce ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
esc_html__( 'Access denied.', 'really-simple-ssl' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: Incompatible with free vesion currently needs the following branch: get-ip-functionality
|
||||
// for now we will skip if the Rsssl_IP_Fetcher does not exist.
|
||||
if( class_exists( 'RSSSL\Pro\Security\WordPress\Limitlogin\Rsssl_IP_Fetcher' ) ) {
|
||||
$ip_array_found = (new Rsssl_IP_Fetcher)->get_ip_address();
|
||||
$ip_address = $ip_array_found[0] ?? $_SERVER['REMOTE_ADDR'];
|
||||
} else {
|
||||
// As a temporary solution we will get the ip from the remote header
|
||||
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
|
||||
// Check if ip is a valid ip address.
|
||||
if ( ! filter_var( $ip_address, FILTER_VALIDATE_IP ) ) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
esc_html__( 'Access denied.', 'really-simple-ssl' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
$route = $request->get_route();
|
||||
$transient_key = 'rsssl_rate_limit_' . md5($ip_address . $route );
|
||||
$attempts = get_transient( $transient_key );
|
||||
|
||||
if ($attempts === false) {
|
||||
$attempts = 0;
|
||||
}
|
||||
|
||||
if ($attempts >= 5) {
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
esc_html__('Too many attempts. Please try again later.', 'really-simple-ssl'),
|
||||
array('status' => 429)
|
||||
);
|
||||
}
|
||||
|
||||
// Perform all checks silently
|
||||
$current_login_action_is_allowed_for_user = in_array(
|
||||
Rsssl_Two_Factor_Settings::get_login_action( $user_id ),
|
||||
$login_actions,
|
||||
true );
|
||||
if (
|
||||
! $current_login_action_is_allowed_for_user ||
|
||||
! Rsssl_Two_Fa_Authentication::verify_login_nonce( $user_id, $login_nonce ) ||
|
||||
! get_user_by( 'id', $user_id )
|
||||
) {
|
||||
// Increment the attempts count
|
||||
set_transient( $transient_key, $attempts + 1, 10 * MINUTE_IN_SECONDS );
|
||||
// Use a short, generic message so the user doesn't know *why* it failed
|
||||
return new WP_Error(
|
||||
'rest_forbidden',
|
||||
esc_html__( 'Access denied.', 'really-simple-ssl' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
// Reset the attempts count on successful validation
|
||||
delete_transient($transient_key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
<?php
|
||||
/**
|
||||
* Holds the logic for the profile page.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
//require_once __DIR__ . '/class-rsssl-provider-loader.php';
|
||||
//require_once __DIR__ . '/class-rsssl-parameter-validation.php';
|
||||
//require_once __DIR__ . '/class-rsssl-parameter-validation.php';
|
||||
//require_once __DIR__ . '/class-rsssl-two-factor-on-board-api.php';
|
||||
ob_start();
|
||||
|
||||
use Exception;
|
||||
use WP_User;
|
||||
|
||||
if (!class_exists('Rsssl_Two_Factor_Profile_Settings')) {
|
||||
/**
|
||||
* Class Rsssl_Two_Factor_Profile_Settings
|
||||
*
|
||||
* This class is responsible for handling the Two-Factor Authentication settings on the user profile page.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
class Rsssl_Two_Factor_Profile_Settings
|
||||
{
|
||||
|
||||
/**
|
||||
* The available providers.
|
||||
*
|
||||
* @var array $available_providers An array to store the available providers.
|
||||
*/
|
||||
private $available_providers = array();
|
||||
|
||||
/**
|
||||
* The forced Two-Factor Authentication roles.
|
||||
*
|
||||
* @var array $forced_two_fa An array to store the forced Two-Factor Authentication roles.
|
||||
*/
|
||||
private $forced_two_fa = array();
|
||||
|
||||
/**
|
||||
* Constructor for the class.
|
||||
*
|
||||
* If the user is logged in, retrieve the user object and check if two-factor authentication is turned on for the user.
|
||||
* If two-factor authentication is enabled, add the necessary hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if (is_user_logged_in()) {
|
||||
$user_id = get_current_user_id();
|
||||
$user = get_user_by('ID', $user_id);
|
||||
global $pagenow;
|
||||
|
||||
if ('profile.php' === $pagenow || ('user-edit.php' === $pagenow && isset($_GET['user_id']))) {
|
||||
if ($this->validate_two_turned_on_for_user($user)) {
|
||||
add_action('admin_init', array($this, 'add_hooks'));
|
||||
}
|
||||
}
|
||||
add_action( 'wp_ajax_resend_email_code_profile', [$this, 'resend_email_code_profile_callback'] );
|
||||
add_action( 'wp_ajax_change_method_to_email', [$this, 'start_email_validation_callback'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add hooks for user profile page.
|
||||
*
|
||||
* This method adds hooks to display the Two-Factor Authentication settings on user profile pages.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_hooks(): void
|
||||
{
|
||||
if (is_user_logged_in()) {
|
||||
$errors = Rsssl_Parameter_Validation::get_cached_errors(get_current_user_id());
|
||||
if (!empty($errors)) {
|
||||
// We display the errors.
|
||||
foreach ($errors as $error) {
|
||||
add_settings_error(
|
||||
'two-factor-authentication',
|
||||
'rsssl-two-factor-authentication-error',
|
||||
$error['message'],
|
||||
$error['type']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$error = Rsssl_Parameter_Validation::get_cached_errors(get_current_user_id());
|
||||
add_action('show_user_profile', array($this, 'show_user_profile'));
|
||||
add_action('edit_user_profile', array($this, 'show_user_profile'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||
add_action('admin_enqueue_scripts', array($this, 'enqueue_styles'));
|
||||
add_action('personal_options_update', array($this, 'save_user_profile'));
|
||||
add_action('edit_user_profile_update', array($this, 'save_user_profile'));
|
||||
|
||||
if (isset($_GET['profile'], $_GET['_wpnonce'])) {
|
||||
$profile = rest_sanitize_boolean(wp_unslash($_GET['profile']));
|
||||
if ($profile) {
|
||||
Rsssl_Two_Factor_Email::set_user_status(get_current_user_id(), 'active');
|
||||
Rsssl_Two_Factor_Totp::set_user_status(get_current_user_id(), 'disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend the email code for the user.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function resend_email_code_profile_callback(): void
|
||||
{
|
||||
// Check for nonce (make sure your nonce name and action match what you output to the page)
|
||||
if ( ! isset( $_POST['login_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['login_nonce'] ) ), 'update_user_two_fa_settings' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'really-simple-ssl' ) ), 403 );
|
||||
}
|
||||
|
||||
// Ensure the user is logged in.
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'User not logged in.', 'really-simple-ssl' ) ), 401 );
|
||||
}
|
||||
|
||||
// Get the user ID.
|
||||
$user_id = get_current_user_id();
|
||||
$user = get_user_by( 'ID', $user_id );
|
||||
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, true);
|
||||
wp_send_json_success( array( 'message' => __('Verification code re-sent', 'really-simple.ssl') ), 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the process of email validation for a user.
|
||||
*
|
||||
*/
|
||||
public function start_email_validation_callback(): void
|
||||
{
|
||||
if(!is_user_logged_in()) {
|
||||
wp_send_json_error( array( 'message' => __( 'User not logged in.', 'really-simple-ssl' ) ), 401 );
|
||||
}
|
||||
$user = get_user_by('id', get_current_user_id());
|
||||
// Sending the email with the code.
|
||||
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, true);
|
||||
$token = get_user_meta( $user->ID, Rsssl_Two_Factor_Email::RSSSL_TOKEN_META_KEY, true );
|
||||
wp_send_json_success( array( 'message' => __('Verification code sent', 'really-simple-ssl'), 'token' => $token ), 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the Two-Factor Authentication settings for the user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save_user_profile(int $user_id): void
|
||||
{
|
||||
// We check if the user owns the profile.
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($_POST['rsssl_two_fa_nonce']) && !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['rsssl_two_fa_nonce'])), 'update_user_two_fa_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($_POST['change_2fa_config_field'])) {
|
||||
// We sanitize the input needs to be a boolean.
|
||||
$reset_input = filter_var($_POST['change_2fa_config_field'], FILTER_VALIDATE_BOOLEAN);
|
||||
$this->maybe_the_user_resets_config($user_id, $reset_input);
|
||||
return;
|
||||
}
|
||||
|
||||
$params = new Rsssl_Parameter_Validation();
|
||||
$params::validate_user_id($user_id);
|
||||
$user = get_user_by('ID', $user_id);
|
||||
$params::validate_user($user);
|
||||
|
||||
if (!isset($_POST['two-factor-authentication'])) {
|
||||
// reset the user's 2fa settings.
|
||||
// Delete all 2fa related user meta.
|
||||
Rsssl_Two_Fa_Status::delete_two_fa_meta($user);
|
||||
// Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa.
|
||||
update_user_meta($user->ID, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
|
||||
// also make sure no lingering errpr messages are shown.
|
||||
Rsssl_Parameter_Validation::delete_cached_errors($user_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($_POST['preferred_method'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// now we check witch provider is selected from the $_POST.
|
||||
$params::validate_selected_provider($this->sanitize_method(sanitize_text_field(wp_unslash($_POST['preferred_method']))));
|
||||
$selected_provider = $this->sanitize_method(sanitize_text_field(wp_unslash($_POST['preferred_method'])));
|
||||
|
||||
// if the selected provider is not then return.
|
||||
if (!$selected_provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($selected_provider) {
|
||||
case 'totp':
|
||||
$current_status = Rsssl_Two_Factor_Settings::get_user_status('totp', $user_id);
|
||||
if ('active' === $current_status) {
|
||||
return;
|
||||
}
|
||||
if ((empty($_POST['two-factor-totp-authcode'])) || !isset($_POST['two-factor-totp-key'])) {
|
||||
add_settings_error(
|
||||
'two-factor-authentication',
|
||||
'rsssl-two-factor-authentication-error',
|
||||
__('Two-Factor Authentication for TOTP failed. No Authentication code provided, please try again.', 'really-simple-ssl'),
|
||||
'error'
|
||||
);
|
||||
$params::cache_errors($user_id);
|
||||
return;
|
||||
}
|
||||
|
||||
$params::validate_auth_code(absint(wp_unslash($_POST['two-factor-totp-authcode'])));
|
||||
$params::validate_key(sanitize_text_field(wp_unslash($_POST['two-factor-totp-key'])));
|
||||
$auth_code = sanitize_text_field(wp_unslash($_POST['two-factor-totp-authcode']));
|
||||
$key = sanitize_text_field(wp_unslash($_POST['two-factor-totp-key']));
|
||||
if (Rsssl_Two_Factor_Totp::setup_totp($user, $key, $auth_code)) {
|
||||
Rsssl_Two_Factor_Totp::set_user_status($user_id, 'active');
|
||||
// We disable the email.
|
||||
Rsssl_Two_Factor_Email::set_user_status($user_id, 'disabled');
|
||||
// We generate the backup codes.
|
||||
Rsssl_Two_Factor_Backup_Codes::generate_codes(
|
||||
$user,
|
||||
array(
|
||||
'cached' => true,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
add_settings_error(
|
||||
'two-factor-authentication',
|
||||
'rsssl-two-factor-authentication-error',
|
||||
__('The Two-Factor Authentication setup for TOTP failed. Please try again.', 'really-simple-ssl'),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
// We cache the errors.
|
||||
$params::cache_errors($user_id);
|
||||
break;
|
||||
case 'email':
|
||||
$current_status = Rsssl_Two_Factor_Settings::get_user_status('email', $user_id);
|
||||
if ('active' === $current_status) {
|
||||
return;
|
||||
}
|
||||
$user = get_user_by('ID', $user_id);
|
||||
// fetch current status of the user for the email method.
|
||||
$status = Rsssl_Two_Factor_Settings::get_user_status('email', $user->ID);
|
||||
if ('active' === $status) {
|
||||
return;
|
||||
}
|
||||
if (Rsssl_Two_Factor_Email::get_instance()->validate_authentication($user)) {
|
||||
// We set the user status to active.
|
||||
Rsssl_Two_Factor_Email::set_user_status($user_id, 'active');
|
||||
// We disable the TOTP.
|
||||
Rsssl_Two_Factor_Totp::set_user_status($user_id, 'disabled');
|
||||
} else {
|
||||
add_settings_error(
|
||||
'two-factor-authentication',
|
||||
'rsssl-two-factor-authentication-error',
|
||||
__('The Two-Factor Authentication setup for email failed. Please try again.', 'really-simple-ssl'),
|
||||
'error'
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'none':
|
||||
// We disable the Two-Factor Authentication.
|
||||
Rsssl_Two_Fa_Status::delete_two_fa_meta($user);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
$params::cache_errors($user_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the input method.
|
||||
*
|
||||
* @param string $method The input method.
|
||||
*
|
||||
* @return string The sanitized input method. Defaults to 'email' if not found in the allowed methods.
|
||||
*/
|
||||
private function sanitize_method(string $method): string
|
||||
{
|
||||
$methods = array('totp', 'email', 'none');
|
||||
|
||||
return in_array($method, $methods, true) ? sanitize_text_field($method) : 'email';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the user profile with Two-Factor Authentication settings.
|
||||
*
|
||||
* @param WP_User $user The user object.
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception Throws an exception if the template file is not found.
|
||||
*/
|
||||
public function show_user_profile(WP_User $user): void
|
||||
{
|
||||
if ($user->ID !== get_current_user_id()) {
|
||||
return;
|
||||
}
|
||||
settings_errors('two-factor-authentication');
|
||||
settings_errors('rsssl-two-factor-authentication-error');
|
||||
Rsssl_Two_Factor_Totp::enqueue_qrcode_script();
|
||||
|
||||
$available_providers = $this->available_providers;
|
||||
$forced = !empty(array_intersect($user->roles, $this->forced_two_fa));
|
||||
$one_enabled = 'onboarding' !== Rsssl_Two_Factor_Settings::get_login_action($user->ID);
|
||||
$providers = Rsssl_Provider_Loader::get_user_enabled_providers($user);
|
||||
$selected_provider = '';
|
||||
if ($one_enabled) {
|
||||
$selected_provider = Rsssl_Two_Factor_Settings::get_configured_provider($user->ID);
|
||||
}
|
||||
|
||||
$backup_codes = Rsssl_Two_Factor_Settings::get_backup_codes($user->ID);
|
||||
$key = Rsssl_Two_Factor_Totp::generate_key();
|
||||
$totp_url = Rsssl_Two_Factor_Totp::generate_qr_code_url($user, $key);
|
||||
|
||||
wp_nonce_field('update_user_two_fa_settings', 'rsssl_two_fa_nonce');
|
||||
|
||||
$data = array(
|
||||
'key' => $key,
|
||||
'totp_url' => $totp_url,
|
||||
'backup_codes' => $backup_codes,
|
||||
'selected_provider' => $selected_provider,
|
||||
'one_enabled' => $one_enabled,
|
||||
'forced' => $forced,
|
||||
'available_providers' => $available_providers,
|
||||
'user' => $user,
|
||||
);
|
||||
|
||||
$data_js = 'rsssl_profile.totp_data = ' . json_encode($data) . ';';
|
||||
|
||||
wp_add_inline_script('rsssl-profile-settings', $data_js, 'after');
|
||||
|
||||
// We load the needed template for the Two-Factor Authentication settings.
|
||||
rsssl_load_template(
|
||||
'profile-settings.php',
|
||||
compact(
|
||||
'user',
|
||||
'available_providers',
|
||||
'providers',
|
||||
'forced',
|
||||
'one_enabled',
|
||||
'selected_provider',
|
||||
'backup_codes',
|
||||
'totp_url',
|
||||
'key'
|
||||
),
|
||||
rsssl_path . 'assets/templates/two_fa/'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the Two-Factor Authentication is turned on for the user.
|
||||
*
|
||||
* @param WP_User $user The user object.
|
||||
*
|
||||
* @return bool Returns true if Two-Factor Authentication is turned on for the user, false otherwise.
|
||||
*/
|
||||
private function validate_two_turned_on_for_user(WP_User $user): bool
|
||||
{
|
||||
// Get the setting for the system to check if it is turned on.
|
||||
$enabled_two_fa = rsssl_get_option('login_protection_enabled');
|
||||
$providers = Rsssl_Provider_Loader::get_enabled_providers_for_user($user);
|
||||
$option = rsssl_get_option('two_fa_forced_roles');
|
||||
$this->forced_two_fa = $option !== false ? $option : array();
|
||||
$this->available_providers = $providers;
|
||||
|
||||
return $enabled_two_fa && !empty($providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the RSSSL profile settings script.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_scripts(): void
|
||||
{
|
||||
$uri = trailingslashit(rsssl_url) . 'assets/two-fa/rtl/two-fa-assets.min.js';
|
||||
$backup_codes = Rsssl_Two_Factor_Settings::get_backup_codes(get_current_user_id());
|
||||
// We check if the backup codes are available.
|
||||
wp_enqueue_script('rsssl-profile-settings', $uri, array(), rsssl_version, true);
|
||||
wp_localize_script('rsssl-profile-settings', 'rsssl_profile', array(
|
||||
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||
'backup_codes' => $backup_codes,
|
||||
'root' => esc_url_raw(rest_url(Rsssl_Two_Factor::REST_NAMESPACE)),
|
||||
'user_id' => get_current_user_id(),
|
||||
'redirect_to' => 'profile', //added this for comparison in the json output.
|
||||
'translatables' => [
|
||||
'download_codes' => esc_html__('Download Backup Codes', 'really-simple-ssl'),
|
||||
'keyCopied' => __('Key copied', 'really-simple-ssl'),
|
||||
'keyCopiedFailed' => __('Could not copy text: ', 'really-simple-ssl')
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the RSSSL profile settings stylesheet.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_styles(): void
|
||||
{
|
||||
$uri = trailingslashit(rsssl_url) . 'assets/two-fa/rtl/two-fa-assets.min.css';
|
||||
wp_enqueue_style('rsssl-profile-settings', $uri, array(), rsssl_version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user resets the configuration and actually reset everything.
|
||||
*
|
||||
* @param int $user_id The ID of the user.
|
||||
* @param $reset_input
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function maybe_the_user_resets_config(int $user_id, $reset_input): bool
|
||||
{
|
||||
$user = get_user_by('ID', $user_id);
|
||||
// If the reset is true, we do the reset.
|
||||
if ($reset_input && $user) {
|
||||
// We reset the user's Two-Factor Authentication settings.
|
||||
Rsssl_Two_Fa_Status::delete_two_fa_meta($user);
|
||||
}
|
||||
|
||||
return $reset_input;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract class for creating two factor authentication providers.
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* Abstract class for creating two-factor authentication providers.
|
||||
*
|
||||
* @since 7.0.6
|
||||
*
|
||||
* @package Two_Factor
|
||||
*/
|
||||
abstract class Rsssl_Two_Factor_Provider {
|
||||
|
||||
/**
|
||||
* The instance of the provider.
|
||||
*
|
||||
* @var Rsssl_Two_Factor_Provider
|
||||
*/
|
||||
public $instance;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
protected function __construct() {
|
||||
$this->instance = $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function get_label();
|
||||
|
||||
/**
|
||||
* Prints the name of the provider.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
public function print_label() {
|
||||
echo esc_html( $this->get_label() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the form that prompts the user to authenticate.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*/
|
||||
abstract public function authentication_page( WP_User $user );
|
||||
|
||||
/**
|
||||
* Allow providers to do extra processing before the authentication.
|
||||
* Return `true` to prevent the authentication and render the
|
||||
* authentication page.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
public function pre_process_authentication( $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the users input token.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function validate_authentication( $user );
|
||||
|
||||
/**
|
||||
* Whether this Two Factor provider is configured and available for the user specified.
|
||||
*
|
||||
* @param WP_User $user WP_User object of the logged-in user.
|
||||
* @return boolean
|
||||
*/
|
||||
abstract public function is_available_for_user( $user );
|
||||
|
||||
/**
|
||||
* Generate a random eight-digit string to send out as an auth code.
|
||||
*
|
||||
* @since 0.1-dev
|
||||
*
|
||||
* @param int $length The code length.
|
||||
* @param string|array $chars Valid auth code characters.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_code( $length = 8, $chars = '1234567890' ): string {
|
||||
$code = '';
|
||||
if ( is_array( $chars ) ) {
|
||||
$chars = implode( '', $chars );
|
||||
}
|
||||
for ( $i = 0; $i < $length; $i++ ) {
|
||||
$code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a numeric code to be used as an auth code.
|
||||
*
|
||||
* @param string $field The _REQUEST field to check for the code.
|
||||
* @param int $length The valid expected length of the field.
|
||||
*
|
||||
* @return false|string Auth code on success, false if the field is not set or not expected length.
|
||||
*/
|
||||
public static function sanitize_code_from_request( string $field, int $length = 0 ) {
|
||||
if ( empty( $_REQUEST[ $field ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$code = wp_unslash( $_REQUEST[ $field ] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, handled by the core method already.
|
||||
$code = preg_replace( '/\s+/', '', $code );
|
||||
|
||||
// Maybe validate the length.
|
||||
if ( $length && strlen( $code ) !== $length ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string) $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user status.
|
||||
*
|
||||
* This function updates the 'rsssl_two_fa_status' user meta key with the provided status.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $status The user status.
|
||||
*
|
||||
* @return void
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function set_user_status( int $user_id, string $status ): void {
|
||||
update_user_meta( $user_id, 'rsssl_two_fa_status', $status );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_FA;
|
||||
|
||||
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Admin;
|
||||
use rsssl_mailer;
|
||||
|
||||
class RSSSL_Two_Factor_Reset_Factory {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
add_action('rsssl_process_batched_users', [self::class, 'batched_process'], 10, 3);
|
||||
}
|
||||
|
||||
public static function reset_fix(): void
|
||||
{
|
||||
$self = new self();
|
||||
$forced_roles = rsssl_get_option('two_fa_forced_roles', array());
|
||||
if(empty($forced_roles)) {
|
||||
update_option('rsssl_reset_fix', false, false);
|
||||
return;
|
||||
}
|
||||
$user_count = (int)$self->get_count_expired_users($self->get_expired_users_query());
|
||||
|
||||
if ($user_count > 0 ) {
|
||||
$batch_size = 1000;
|
||||
wp_schedule_single_event(time()+ 20, 'rsssl_process_batched_users', [$self->get_expired_users_query(), $user_count, $batch_size]);
|
||||
} else {
|
||||
update_option('rsssl_reset_fix', false, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function get_expired_users_query()
|
||||
{
|
||||
global $wpdb;
|
||||
$days_threshold = rsssl_get_option('two_fa_grace_period', 30);
|
||||
$filter_value = 'expired';
|
||||
|
||||
$enabled_roles = array_unique(array_merge(
|
||||
defined('rsssl_pro') ? rsssl_get_option('two_fa_enabled_roles_totp', array()) : array(),
|
||||
rsssl_get_option('two_fa_enabled_roles_email', array())
|
||||
));
|
||||
|
||||
$forced_roles = rsssl_get_option('two_fa_forced_roles', array());
|
||||
|
||||
$fields = ['id', 'user', 'status_for_user', 'rsssl_two_fa_providers', 'user_role']; // Example fields
|
||||
$enabled_roles_placeholders = implode(',', array_map(function($role) { return "'$role'"; }, $enabled_roles));
|
||||
$forced_roles_placeholder = implode(',', array_map(function($role) { return "'$role'"; }, $forced_roles));
|
||||
$query = Rsssl_Two_Factor_Admin::generate_query($fields, $enabled_roles_placeholders, $forced_roles_placeholder, $forced_roles);
|
||||
|
||||
if ($filter_value !== 'all') {
|
||||
$query .= $wpdb->prepare(" HAVING status_for_user = %s", $filter_value);
|
||||
}
|
||||
$prepared_query = $wpdb->prepare($query, array_merge(
|
||||
array_fill(0, count($forced_roles), $days_threshold)
|
||||
));
|
||||
|
||||
return $prepared_query;
|
||||
}
|
||||
|
||||
public function get_count_expired_users($query): ?string
|
||||
{
|
||||
global $wpdb;
|
||||
$count_query = "SELECT COUNT(*) FROM ($query) AS count_table";
|
||||
return $wpdb->get_var($count_query);
|
||||
}
|
||||
|
||||
public static function batched_process($query, $user_count, $batch_size = 500): void
|
||||
{
|
||||
global $wpdb;
|
||||
$paged_query = $query . " LIMIT %d";
|
||||
|
||||
while ($user_count > 0) {
|
||||
$current_query = $wpdb->prepare($paged_query, $batch_size);
|
||||
$users = $wpdb->get_results($current_query);
|
||||
|
||||
foreach ($users as $user) {
|
||||
Rsssl_Two_Fa_Status::delete_two_fa_meta((int)$user->id);
|
||||
// Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa.
|
||||
update_user_meta((int)$user->id, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
|
||||
}
|
||||
$user_count -= $batch_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,765 @@
|
||||
<?php
|
||||
/**
|
||||
* Holds the request parameters for a specific action.
|
||||
* This class holds the request parameters for a specific action.
|
||||
* It is used to store the parameters and pass them to the functions.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* Class Rsssl_Two_Factor_Settings
|
||||
*
|
||||
* This class handles the settings for the Two-Factor Authentication plugin.
|
||||
*/
|
||||
class Rsssl_Two_Factor_Settings {
|
||||
/**
|
||||
* The class instance.
|
||||
*
|
||||
* @var Rsssl_Two_Factor_Settings
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* The forced roles for 2FA.
|
||||
*
|
||||
* @var array $forced_roles
|
||||
*/
|
||||
public static $forced_roles;
|
||||
|
||||
/**
|
||||
* The enabled roles for TOTP.
|
||||
*
|
||||
* @var $enabled_roles_totp
|
||||
*/
|
||||
public static $enabled_roles_totp;
|
||||
|
||||
/**
|
||||
* The forced roles for TOTP dynamically generated by logic.
|
||||
*
|
||||
* @var $forced_roles_totp
|
||||
*/
|
||||
public static $forced_roles_totp; // @codingStandardsIgnoreLine It is dynamically generated by logic.
|
||||
|
||||
/**
|
||||
* The enabled roles for Email dynamically generated by logic.
|
||||
*
|
||||
* @var $enabled_roles_totp
|
||||
*/
|
||||
public static $forced_roles_email; // @codingStandardsIgnoreLine It is dynamically generated by logic.
|
||||
|
||||
/**
|
||||
* The enabled roles for Email.
|
||||
*
|
||||
* @var array $enabled_roles_email
|
||||
*/
|
||||
public static $enabled_roles_email;
|
||||
|
||||
/**
|
||||
* If the previous roles variables are loaded or not.
|
||||
*
|
||||
* @var bool $roles_loaded
|
||||
*/
|
||||
private static $roles_loaded = false;
|
||||
|
||||
/**
|
||||
* The user meta enabled providers key.
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
const RSSSL_ENABLED_PROVIDERS_USER_META_KEY = 'rsssl_two_fa_providers';
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* Checks if the class instance has already been initialized. If so, returns
|
||||
* immediately. Otherwise, assigns the class instance to the static variable
|
||||
* "self::$instance".
|
||||
*/
|
||||
public function __construct() {
|
||||
if ( isset( self::$instance ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$instance = $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get user roles for a user, cross multisite.
|
||||
*
|
||||
* @param int $user_id //the user id to get the roles for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_user_roles( int $user_id ): array {
|
||||
if ( is_multisite() ) {
|
||||
return array_values(self::get_strictest_role_across_sites($user_id, ['totp', 'email']));
|
||||
}
|
||||
|
||||
$user = get_userdata( $user_id );
|
||||
$roles = $user->roles;
|
||||
if ( ! is_array( $roles ) ) {
|
||||
$roles = array();
|
||||
}
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a one-time login URL for a user.
|
||||
*
|
||||
* @param int $user_id //the user ID.
|
||||
* @param bool $disable_two_fa //whether to disable two-factor authentication.
|
||||
*
|
||||
* @return string //the generated URL.
|
||||
*/
|
||||
public static function rsssl_one_time_login_url( int $user_id, bool $disable_two_fa = false, $profile = false ): string {
|
||||
|
||||
$token = bin2hex( openssl_random_pseudo_bytes( 16 ) ); // 16 bytes * 8 bits/byte = 128 bits.
|
||||
set_transient( 'skip_two_fa_token_' . $user_id, $token, 2 * MINUTE_IN_SECONDS );
|
||||
|
||||
$obfuscated_user_id = self::obfuscate_user_id( $user_id );
|
||||
|
||||
$nonce = wp_create_nonce( 'one_time_login_' . $user_id );
|
||||
if(!$profile) {
|
||||
$args = array(
|
||||
'rsssl_one_time_login' => $obfuscated_user_id,
|
||||
'token' => $token,
|
||||
'_wpnonce' => $nonce,
|
||||
);
|
||||
} else {
|
||||
$args = array(
|
||||
'_wpnonce' => $nonce,
|
||||
'profile' => $profile,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ( $disable_two_fa ) {
|
||||
$args['rsssl_two_fa_disable'] = true;
|
||||
}
|
||||
|
||||
// Return the URL with the added query arguments.
|
||||
return add_query_arg( $args, $profile? get_edit_profile_url( $user_id ):admin_url() );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the {method}_role_status. The role with the most weighing status will be returned. empty, optional or forded. Where forced is the most weighing.
|
||||
*
|
||||
* @param string $method //the method to check.
|
||||
* @param int $user_id //the user id to get the roles for.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_role_status( string $method, int $user_id ): string {
|
||||
if (is_multisite()) {
|
||||
$roles = self::get_strictest_role_across_sites($user_id, [$method]);
|
||||
$roles = array_values($roles); // Flatten the array to get the strictest role
|
||||
} else {
|
||||
$roles = self::get_user_roles($user_id);
|
||||
}
|
||||
|
||||
$provider = 'email' === $method ? '_email' : '_' . self::sanitize_method( $method );
|
||||
|
||||
// Check if the method is enabled.
|
||||
$enabled = rsssl_get_option( "two_fa_enabled_roles$provider" );
|
||||
|
||||
$forced = false;
|
||||
|
||||
if ( ! $enabled ) {
|
||||
$return = 'empty';
|
||||
}
|
||||
|
||||
// if the role is forced, return forced.
|
||||
if ( self::contains_role_of_type( $method, $roles, 'forced' ) ) {
|
||||
$return = 'forced';
|
||||
$forced = true;
|
||||
}
|
||||
// if the role is enabled, return optional.
|
||||
if ( self::contains_role_of_type( $method, $roles, 'enabled' ) && ! $forced ) {
|
||||
$return = 'optional';
|
||||
}
|
||||
|
||||
if ( empty( $return ) ) {
|
||||
$return = 'empty';
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required 2fa action for a user.
|
||||
*
|
||||
* @param int|null $user_id //the user id to get the roles for.
|
||||
* @return string //email, totp, onboarding or login
|
||||
*/
|
||||
public static function get_login_action( int $user_id = null ): string {
|
||||
if ( null === $user_id ) {
|
||||
$user_id = get_current_user_id();
|
||||
}
|
||||
|
||||
$user = get_userdata( $user_id );
|
||||
|
||||
$totp = Rsssl_Two_Factor_Totp::get_instance();
|
||||
|
||||
if ( $totp::is_enabled( $user ) ) {
|
||||
// first, check TOTP.
|
||||
$user_status = self::get_user_status( 'totp', $user_id );
|
||||
$role_status = self::get_role_status( 'totp', $user_id );
|
||||
|
||||
// if it's active, it's simple: the user should enter the code.
|
||||
if ( 'active' === $user_status ) {
|
||||
// Check the role status, in case the admin has disabled this for this role.
|
||||
if ( 'forced' === $role_status || 'optional' === $role_status ) {
|
||||
return 'totp';
|
||||
}
|
||||
}
|
||||
if ( 'open' === $user_status ) {
|
||||
// if the status is open, the user should get onboarding if the role status either forced or optional.
|
||||
if ( 'forced' === $role_status || 'optional' === $role_status ) {
|
||||
// The role is forced. So check if the grace period is over.
|
||||
$grace_period = self::is_user_in_grace_period( $user );
|
||||
|
||||
if ( $grace_period > 0 && 'forced' === $role_status ) {
|
||||
return 'onboarding';
|
||||
}
|
||||
|
||||
if ('optional' === $role_status) {
|
||||
return 'onboarding';
|
||||
}
|
||||
|
||||
return 'expired';
|
||||
}
|
||||
// if empty, nothing is currently activated for this role, so check if there's an email method enabled.
|
||||
return self::get_email_method_action( $user_id );
|
||||
}
|
||||
|
||||
// Check if the role_status is not 'forced' currently. If so, show onboarding TODO: test this code below.
|
||||
$role_status = self::get_role_status( 'totp', $user_id );
|
||||
if ( 'forced' === $role_status && 'disabled' !== $user_status ) {
|
||||
return 'onboarding';
|
||||
}
|
||||
}
|
||||
|
||||
return self::get_email_method_action( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required action for the email 2fa method.
|
||||
*
|
||||
* @param int $user_id //the user id to get the roles for.
|
||||
*
|
||||
* @return string //email, onboarding or login
|
||||
*/
|
||||
public static function get_email_method_action( int $user_id ): string {
|
||||
$email = Rsssl_Two_Factor_Email::get_instance();
|
||||
$grace_period = self::is_user_in_grace_period( get_userdata( $user_id ) );
|
||||
$return = 'login';
|
||||
|
||||
if ( $email::is_enabled( get_userdata( $user_id ) ) ) {
|
||||
|
||||
$user_status = self::get_user_status( 'email', $user_id );
|
||||
$role_status = self::get_role_status( 'email', $user_id );
|
||||
|
||||
if ( 'active' === $user_status ) {
|
||||
// Also check the role status, in case the admin has disabled this for this role.
|
||||
if ( 'forced' === $role_status || 'optional' === $role_status ) {
|
||||
$return = 'email';
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'open' === $user_status ) {
|
||||
// if the role status is forced or optional, we show onboarding.
|
||||
if ( 'forced' === $role_status || 'optional' === $role_status ) {
|
||||
|
||||
// The role is forced. So check if the grace period is over.
|
||||
if ( $grace_period > 0 && 'forced' === $role_status ) {
|
||||
return 'onboarding';
|
||||
}
|
||||
|
||||
if ('optional' === $role_status) {
|
||||
return 'onboarding';
|
||||
}
|
||||
|
||||
return 'expired';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we're here, the email method is not enabled, so we show login.
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if the role status and user status are valid.
|
||||
*
|
||||
* @param string $role_status // The role status to check.
|
||||
* @param string $user_status // The user status to check.
|
||||
*
|
||||
* @return bool // Returns true if the role status and user status are valid, otherwise false.
|
||||
*/
|
||||
public static function is_role_and_user_status_valid( string $role_status, string $user_status ): bool {
|
||||
return ( 'forced' === $role_status || 'optional' === $role_status ) && ( 'active' === $user_status || 'open' === $user_status );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status for a user, based on the method.
|
||||
*
|
||||
* @param string $method //the method to check.
|
||||
* @param int $user_id //the user id to get the roles for.
|
||||
*
|
||||
* @return string //open, active or disabled
|
||||
*/
|
||||
public static function get_user_status( string $method, int $user_id ): string {
|
||||
$method = 'email' === $method ? '_email' : '_' . self::sanitize_method( $method );
|
||||
|
||||
// first check if a user meta rsssl_two_fa_status is set.
|
||||
$status = get_user_meta( $user_id, "rsssl_two_fa_status$method", true );
|
||||
|
||||
return self::sanitize_status( $status );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles for a user, based on the method and type.
|
||||
*
|
||||
* @param string $method //the method to check.
|
||||
* @param string $type //the type to check.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function get_dynamic_roles_variable( string $method, string $type ): array {
|
||||
// store these roles, as this function can be used in large loops.
|
||||
if ( ! self::$roles_loaded ) {
|
||||
// if the option is a boolean we convert it to an array.
|
||||
self::$enabled_roles_totp = rsssl_get_option( 'two_fa_enabled_roles_totp', [] );
|
||||
self::$enabled_roles_email = rsssl_get_option( 'two_fa_enabled_roles_email', [] );
|
||||
self::$forced_roles = rsssl_get_option( 'two_fa_forced_roles', [] );
|
||||
self::$roles_loaded = true;
|
||||
}
|
||||
|
||||
$method = 'email' === $method ? '_email' : '_' . self::sanitize_method( $method );
|
||||
$type = 'enabled' === $type ? 'enabled' : 'forced';
|
||||
|
||||
$name = $type . '_roles' . $method;
|
||||
$roles_to_check = 'enabled_roles' . $method;
|
||||
|
||||
// if the type is forced, use the forced roles.
|
||||
if ( 'forced' === $type ) {
|
||||
// Intersect the roles with the enabled roles.
|
||||
self::$$name = array_intersect( self::$forced_roles, self::$$roles_to_check );
|
||||
if ( property_exists( self::class, $name ) ) {
|
||||
$roles = self::$$name;
|
||||
if ( ! is_array( $roles ) ) {
|
||||
$roles = array();
|
||||
}
|
||||
return $roles;
|
||||
}
|
||||
}
|
||||
|
||||
// if the type is enabled, use the enabled roles.
|
||||
if ( 'enabled' === $type ) {
|
||||
self::$$name = array_merge( self::$$roles_to_check );
|
||||
if ( property_exists( self::class, $name ) ) {
|
||||
$roles = self::$$name;
|
||||
if ( ! is_array( $roles ) ) {
|
||||
$roles = array();
|
||||
}
|
||||
return $roles;
|
||||
}
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the array of roles contains a role of type $type, forced or optional.
|
||||
*
|
||||
* @param string $method //the method to check.
|
||||
* @param array $roles //the roles to check.
|
||||
* @param string $type //the type to check.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function contains_role_of_type( string $method, array $roles, string $type ): bool {
|
||||
$roles_to_check = self::get_dynamic_roles_variable( $method, $type );
|
||||
foreach ( $roles as $role ) {
|
||||
if ( in_array( $role, $roles_to_check, true ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a role is of a certain type, optional or forced
|
||||
*
|
||||
* @param string $method //the method to check.
|
||||
* @param string $role //the role to check.
|
||||
* @param string $type //the type to check.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function role_is_of_type( string $method, string $role, string $type ): bool {
|
||||
return self::contains_role_of_type( $method, array( $role ), $type );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the user meta enabled providers key.
|
||||
*
|
||||
* @param string $status //the status to filter by.
|
||||
*
|
||||
* @return string //the user meta key.
|
||||
*/
|
||||
protected static function sanitize_status( string $status ): string {
|
||||
return in_array( $status, array( 'open', 'active', 'disabled' ), true ) ? $status : 'open';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user meta enabled providers key.
|
||||
*
|
||||
* @param string $method //the method to sanitize.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function sanitize_method( string $method ): string {
|
||||
return in_array( $method, array( 'email', 'totp' ), true ) ? $method : 'email';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is forced to use 2FA based on their roles.
|
||||
*
|
||||
* @param int $user_id // the ID of the user to check.
|
||||
*
|
||||
* @return bool // true if the user is forced to use 2FA, false otherwise.
|
||||
*/
|
||||
public static function is_user_forced_to_use_2fa( int $user_id ): bool {
|
||||
$roles = self::get_user_roles( $user_id );
|
||||
$forced_roles = rsssl_get_option( 'two_fa_forced_roles', [] );
|
||||
foreach ( $roles as $role ) {
|
||||
if ( in_array( $role, $forced_roles, true ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is in the grace period for two-factor authentication.
|
||||
*
|
||||
* @param WP_User $user The user to check.
|
||||
*
|
||||
* @return int|false The number of days remaining in the grace period, or false if the user is not in the grace period.
|
||||
*/
|
||||
public static function is_user_in_grace_period( WP_User $user ) {
|
||||
|
||||
$grace_period = rsssl_get_option( 'two_fa_grace_period');
|
||||
|
||||
// if the grace period is not set, return false.
|
||||
if ( ! self::is_user_forced_to_use_2fa( $user->ID ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$last_login = get_user_meta( $user->ID, 'rsssl_two_fa_last_login', true );
|
||||
|
||||
|
||||
if ( $last_login ) {
|
||||
$last_login = strtotime( $last_login );
|
||||
$now = time();
|
||||
$diff = $now - $last_login;
|
||||
$days = floor( $diff / ( 60 * 60 * 24 ) );
|
||||
|
||||
if ( $days < $grace_period ) {
|
||||
$end_date = gmdate( 'Y-m-d', $last_login );
|
||||
// We add the grace period to the last login date.
|
||||
$end_date = date( 'Y-m-d', strtotime( $end_date . ' + ' . $grace_period . ' days' ) );
|
||||
$today = gmdate('Y-m-d', $now);
|
||||
// If the end date is today, return 1.
|
||||
if ($end_date === $today) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return $grace_period - $days;
|
||||
}
|
||||
// it is now equal or greater, so return false.
|
||||
return false;
|
||||
}
|
||||
// if the last login is not set, return the grace period. but also set the user meta.
|
||||
update_user_meta( $user->ID, 'rsssl_two_fa_last_login', gmdate( 'Y-m-d H:i:s' ) );
|
||||
|
||||
return $grace_period;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enabled roles for a user.
|
||||
*
|
||||
* @param int $user_id // The ID of the user.
|
||||
*
|
||||
* @return array // The array of enabled roles for the user.
|
||||
*/
|
||||
public static function get_enabled_roles( int $user_id ): array {
|
||||
$roles = self::get_user_roles( $user_id );
|
||||
if(defined('rsssl_pro') && rsssl_pro ) {
|
||||
$totp = rsssl_get_option( 'two_fa_enabled_roles_totp', [] );
|
||||
} else {
|
||||
$totp = [];
|
||||
}
|
||||
|
||||
$email = rsssl_get_option( 'two_fa_enabled_roles_email', [] );
|
||||
$enabled_roles = array_merge( $totp, $email );
|
||||
return array_intersect( $roles, $enabled_roles );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enabled roles for a user.
|
||||
* This function is used to get the roles that are enabled for a user.
|
||||
*
|
||||
* @param int $user_id //the user ID to obfuscate.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function obfuscate_user_id( int $user_id ): string {
|
||||
// Convert the user ID to a string with some noise.
|
||||
$obfuscated = 'user-' . $user_id . '-id';
|
||||
// Encode the string using base64.
|
||||
return base64_encode( $obfuscated );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deobfuscate the user ID for use in URL.
|
||||
*
|
||||
* @param string $data //the data to deobfuscate.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public static function deobfuscate_user_id( string $data ): ?string {
|
||||
// Decode from base64.
|
||||
$decoded = base64_decode( $data );
|
||||
// Remove the noise to get the user ID.
|
||||
if ( preg_match( '/user-(\d+)-id/', $decoded, $matches ) ) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the roles enabled return the method for the current user.
|
||||
* If both methods are enabled, return the string not set.
|
||||
* If only one method is enabled, return that method as a string.
|
||||
* If no method is enabled, return the string None.
|
||||
*
|
||||
* @param int $user_id //the user ID to get the roles for.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_enabled_method( int $user_id ): string {
|
||||
$user_id = absint( $user_id ); // make sure an integer and not a float, negative value.
|
||||
$enabled_roles = self::get_enabled_roles( $user_id ) ?? array();
|
||||
$enabled_totp = rsssl_get_option( 'two_fa_enabled_roles_totp', [] );
|
||||
$enabled_email = rsssl_get_option( 'two_fa_enabled_roles_email', [] );
|
||||
|
||||
$totp = array_intersect( $enabled_roles, $enabled_totp );
|
||||
$email = array_intersect( $enabled_roles, $enabled_email );
|
||||
|
||||
if ( ! empty( $totp ) && ! empty( $email ) ) {
|
||||
$enabled_method = __( 'not set', 'really-simple-ssl' );
|
||||
}
|
||||
|
||||
if ( ! empty( $totp ) ) {
|
||||
$enabled_method = __( 'Authenticator App', 'really-simple-ssl' );
|
||||
}
|
||||
|
||||
if ( ! empty( $email ) ) {
|
||||
$enabled_method = __( 'Email', 'really-simple-ssl' );
|
||||
}
|
||||
|
||||
if ( ! isset( $enabled_method ) ) {
|
||||
$enabled_method = __( 'None', 'really-simple-ssl' );
|
||||
}
|
||||
|
||||
return $enabled_method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured provider for a user based on their ID.
|
||||
*
|
||||
* @param int $user_id The ID of the user.
|
||||
*
|
||||
* @return string The configured provider.
|
||||
*/
|
||||
public static function get_configured_provider( int $user_id ): string {
|
||||
// With 2 providers, TOTP and Email we check both options and get the one that is not disabled.
|
||||
$totp_meta = get_user_meta( $user_id, 'rsssl_two_fa_status_totp', true );
|
||||
$email_meta = get_user_meta( $user_id, 'rsssl_two_fa_status_email', true );
|
||||
$provider = __( 'None', 'really-simple-ssl' );
|
||||
// if the status is active, return the method.
|
||||
if ( 'active' === $totp_meta ) {
|
||||
$provider = Rsssl_Two_Factor_Totp::NAME;
|
||||
}
|
||||
if ( 'active' === $email_meta ) {
|
||||
$provider = Rsssl_Two_Factor_Email::NAME;
|
||||
}
|
||||
return $provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the backup codes for a user.
|
||||
*
|
||||
* @param int $user_id // The user ID.
|
||||
*
|
||||
* @return array // An array of backup codes.
|
||||
*/
|
||||
public static function get_backup_codes( int $user_id ): array {
|
||||
$codes = get_transient( 'rsssl_two_factor_backup_codes_' . $user_id );
|
||||
if ( ! is_array( $codes ) ) {
|
||||
$codes = array();
|
||||
}
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the last login date for a user is today.
|
||||
*
|
||||
* @param WP_User $user //the user.
|
||||
*
|
||||
* @return bool //true if last login date is today, false otherwise.
|
||||
*/
|
||||
public static function is_today( WP_User $user ): bool {
|
||||
return (1 === (int) self::is_user_in_grace_period( $user ));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure that the default roles are first in the array
|
||||
*
|
||||
* @param array $roles
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function sort_roles_by_default_first( array $roles ): array {
|
||||
$default_roles = array( 'administrator', 'editor', 'author', 'contributor', 'subscriber' );
|
||||
$sorted_roles = array();
|
||||
foreach ( $default_roles as $default_role ) {
|
||||
if ( in_array( $default_role, $roles, true ) ) {
|
||||
$sorted_roles[] = $default_role;
|
||||
}
|
||||
}
|
||||
foreach ( $roles as $role ) {
|
||||
if ( ! in_array( $role, $sorted_roles, true ) ) {
|
||||
$sorted_roles[] = $role;
|
||||
}
|
||||
}
|
||||
return $sorted_roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the strictest role across all sites for a given user
|
||||
*
|
||||
* @param int $user_id //the ID of the user.
|
||||
*
|
||||
* @return array|null //returns the strictest role or null if no roles found.
|
||||
*/
|
||||
public static function get_strictest_role_across_sites(int $user_id, $methods ): ?array
|
||||
{
|
||||
$sites = get_sites();
|
||||
$all_roles = [];
|
||||
|
||||
foreach ($sites as $site) {
|
||||
switch_to_blog($site->blog_id);
|
||||
$user = get_userdata($user_id);
|
||||
if ($user) {
|
||||
foreach($user->roles as $role){
|
||||
$all_roles[] = $role;
|
||||
}
|
||||
}
|
||||
|
||||
restore_current_blog();
|
||||
}
|
||||
$all_roles = array_unique($all_roles);
|
||||
|
||||
return self::get_strictest_role($methods, $all_roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the strictest role from a list of roles
|
||||
*
|
||||
* @param array $methods
|
||||
* @param array $roles // The list of roles
|
||||
*
|
||||
* @return array // The strictest role
|
||||
*/
|
||||
protected static function get_strictest_role(array $methods, array $roles): array
|
||||
{
|
||||
$result = [];
|
||||
if (is_multisite()) {
|
||||
$roles = self::sort_roles_by_default_first($roles);
|
||||
$forced_roles = rsssl_get_option('two_fa_forced_roles', []);
|
||||
// if there are forced roles, prioritize them by removing all other roles
|
||||
if (!empty($forced_roles) && array_intersect($roles, $forced_roles)) {
|
||||
$roles = array_intersect($roles, $forced_roles);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($methods as $method) {
|
||||
// First, prioritize forced roles using the default-first sorting method
|
||||
if (self::contains_role_of_type($method, $roles, 'forced')) {
|
||||
foreach ($roles as $role) {
|
||||
if (self::role_is_of_type($method, $role, 'forced')) {
|
||||
// If forced role is found, assign it to the method and continue to the next method
|
||||
$result[$method] = $role;
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no forced role, check for optional roles
|
||||
if (self::contains_role_of_type($method, $roles, 'enabled')) {
|
||||
foreach ($roles as $role) {
|
||||
if (self::role_is_of_type($method, $role, 'enabled')) {
|
||||
// If optional role is found, assign it to the method and continue to the next method
|
||||
$result[$method] = $role;
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no role was found, assign an empty string
|
||||
$result[$method] = '';
|
||||
}
|
||||
//remove empty values
|
||||
return array_values(array_unique(array_filter($result)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user status per method
|
||||
*
|
||||
* @param int $user_id // The ID of the user.
|
||||
*
|
||||
* @return array // The user status per method
|
||||
*/
|
||||
public static function get_user_status_per_method(int $user_id): array
|
||||
{
|
||||
$methods = self::get_available_methods();
|
||||
$result = [];
|
||||
foreach ($methods as $method) {
|
||||
$result[$method] = self::get_user_status($method, $user_id);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function get_available_methods(): array
|
||||
{
|
||||
if(defined('rsssl_pro') && !rsssl_pro ) {
|
||||
return ['totp', 'email'];
|
||||
}
|
||||
return ['email'];
|
||||
}
|
||||
}
|
||||
|
||||
new Rsssl_Two_Factor_Settings();
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Extracted from wp-login.php since that file also loads WP core which already have.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
|
||||
/**
|
||||
* Outputs the footer for the login page.
|
||||
*
|
||||
* @param string $input_id Which input to auto-focus.
|
||||
*
|
||||
* @global bool|string $interim_login Whether interim login modal is being displayed. String 'success'
|
||||
* upon successful login.
|
||||
*
|
||||
* @since 3.1.0
|
||||
*/
|
||||
function login_footer( string $input_id = '' ) {
|
||||
global $interim_login;
|
||||
|
||||
// Don't allow interim logins to navigate away from the page.
|
||||
if ( ! $interim_login ) {
|
||||
?>
|
||||
<p id="backtoblog">
|
||||
<?php
|
||||
$html_link = sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
esc_url( home_url( '/' ) ),
|
||||
sprintf(
|
||||
/* translators: %s: Site title. */
|
||||
_x( '← Go to %s', 'site' ),
|
||||
get_bloginfo( 'title', 'display' )
|
||||
)
|
||||
);
|
||||
/**
|
||||
* Filter the "Go to site" link displayed in the login page footer.
|
||||
*
|
||||
* @since 5.7.0
|
||||
*
|
||||
* @param string $link HTML link to the home URL of the current site.
|
||||
*/
|
||||
echo esc_url( apply_filters( 'login_site_html_link', $html_link ) );
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
|
||||
the_privacy_policy_link( '<div class="privacy-policy-page-link">', '</div>' );
|
||||
}
|
||||
|
||||
?>
|
||||
</div><?php // End of <div id="login">. ?>
|
||||
|
||||
<?php
|
||||
|
||||
if ( ! empty( $input_id ) ) {
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
try{document.getElementById('<?php echo esc_html( $input_id ); ?>').focus();}catch(e){}
|
||||
if(typeof wpOnload==='function')wpOnload();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires in the login page footer.
|
||||
*
|
||||
* @since 3.1.0
|
||||
*/
|
||||
do_action( 'login_footer' );
|
||||
|
||||
?>
|
||||
<div class="clear"></div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the JavaScript to handle the form shaking on the login page.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
function wp_shake_js() {
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
document.querySelector('form').classList.add('shake');
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
/**
|
||||
* Extracted from wp-login.php since that file also loads WP core which already have.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
|
||||
/**
|
||||
* Output the login page header.
|
||||
*
|
||||
* @param string $title Optional. WordPress login Page title to display in the `<title>` element.
|
||||
* Default 'Log In'.
|
||||
* @param string $message Optional. Message to display in header. Default empty.
|
||||
* @param WP_Error|null $wp_error Optional. The error to pass. Default is a WP_Error instance.
|
||||
*
|
||||
* @global string $action The action that brought the visitor to the login page.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*
|
||||
* @global string $error Login error message set by deprecated pluggable wp_login() function
|
||||
* or plugins replacing it.
|
||||
* @global bool|string $interim_login Whether interim login modal is being displayed. String 'success'
|
||||
* upon successful login.
|
||||
*/
|
||||
function login_header( string $title = 'Log In', string $message = '', WP_Error $wp_error = null ) {
|
||||
global $error, $interim_login, $action;
|
||||
|
||||
// Don't index any of these forms.
|
||||
add_filter( 'wp_robots', 'wp_robots_sensitive_page' );
|
||||
add_action( 'login_head', 'wp_strict_cross_origin_referrer' );
|
||||
|
||||
add_action( 'login_head', 'wp_login_viewport_meta' );
|
||||
|
||||
if ( ! is_wp_error( $wp_error ) ) {
|
||||
$wp_error = new WP_Error();
|
||||
}
|
||||
|
||||
// Shake it!
|
||||
$shake_error_codes = array( 'empty_password', 'empty_email', 'invalid_email', 'invalidcombo', 'empty_username', 'invalid_username', 'incorrect_password', 'retrieve_password_email_failure' );
|
||||
/**
|
||||
* Filters the error codes array for shaking the login form.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param array $shake_error_codes Error codes that shake the login form.
|
||||
*/
|
||||
$shake_error_codes = apply_filters( 'shake_error_codes', $shake_error_codes );
|
||||
|
||||
if ( $shake_error_codes && $wp_error->has_errors() && in_array( $wp_error->get_error_code(), $shake_error_codes, true ) ) {
|
||||
add_action( 'login_footer', 'wp_shake_js', 12 );
|
||||
}
|
||||
|
||||
$login_title = get_bloginfo( 'name', 'display' );
|
||||
|
||||
/* translators: Login screen title. 1: Login screen name, 2: Network or site name. */
|
||||
$login_title = sprintf( __( '%1$s ‹ %2$s — WordPress' ), $title, $login_title );
|
||||
|
||||
if ( wp_is_recovery_mode() ) {
|
||||
/* translators: %s: Login screen title. */
|
||||
$login_title = sprintf( __( 'Recovery Mode — %s' ), $login_title );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the title tag content for login page.
|
||||
*
|
||||
* @since 4.9.0
|
||||
*
|
||||
* @param string $login_title The page title, with extra context added.
|
||||
* @param string $title The original page title.
|
||||
*/
|
||||
$login_title = apply_filters( 'login_title', $login_title, $title );
|
||||
|
||||
?><!DOCTYPE html>
|
||||
<html <?php language_attributes(); ?>>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="<?php bloginfo( 'html_type' ); ?>; charset=<?php bloginfo( 'charset' ); ?>" />
|
||||
<title><?php echo esc_html( $login_title ); ?></title>
|
||||
<?php
|
||||
|
||||
wp_enqueue_style( 'login' );
|
||||
|
||||
/*
|
||||
* Remove all stored post data on logging out.
|
||||
* This could be added by add_action('login_head'...) like wp_shake_js(),
|
||||
* but maybe better if it's not removable by plugins.
|
||||
*/
|
||||
if ( 'loggedout' === $wp_error->get_error_code() ) {
|
||||
?>
|
||||
<script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts and styles for the login page.
|
||||
*
|
||||
* @since 3.1.0
|
||||
*/
|
||||
do_action( 'login_enqueue_scripts' );
|
||||
|
||||
/**
|
||||
* Fires in the login page header after scripts are enqueued.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*/
|
||||
do_action( 'login_head' );
|
||||
|
||||
$login_header_url = __( 'https://wordpress.org/' );
|
||||
|
||||
/**
|
||||
* Filters link URL of the header logo above login form.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*
|
||||
* @param string $login_header_url Login header logo URL.
|
||||
*/
|
||||
$login_header_url = apply_filters( 'login_headerurl', $login_header_url );
|
||||
|
||||
$login_header_title = '';
|
||||
|
||||
/**
|
||||
* Filters the title attribute of the header logo above login form.
|
||||
*
|
||||
* @since 2.1.0
|
||||
* @deprecated 5.2.0 Use {@see 'login_headertext'} instead.
|
||||
*
|
||||
* @param string $login_header_title Login header logo title attribute.
|
||||
*/
|
||||
$login_header_title = apply_filters_deprecated(
|
||||
'login_headertitle',
|
||||
array( $login_header_title ),
|
||||
'5.2.0',
|
||||
'login_headertext',
|
||||
__( 'Usage of the title attribute on the login logo is not recommended for accessibility reasons. Use the link text instead.' )
|
||||
);
|
||||
|
||||
$login_header_text = empty( $login_header_title ) ? __( 'Powered by WordPress' ) : $login_header_title;
|
||||
|
||||
/**
|
||||
* Filters the link text of the header logo above the login form.
|
||||
*
|
||||
* @since 5.2.0
|
||||
*
|
||||
* @param string $login_header_text The login header logo link text.
|
||||
*/
|
||||
$login_header_text = apply_filters( 'login_headertext', $login_header_text );
|
||||
|
||||
$classes = array( 'login-action-' . $action, 'wp-core-ui' );
|
||||
|
||||
if ( is_rtl() ) {
|
||||
$classes[] = 'rtl';
|
||||
}
|
||||
|
||||
if ( $interim_login ) {
|
||||
$classes[] = 'interim-login';
|
||||
|
||||
?>
|
||||
<style type="text/css">html{background-color: transparent;}</style>
|
||||
<?php
|
||||
|
||||
if ( 'success' === $interim_login ) {
|
||||
$classes[] = 'interim-login-success';
|
||||
}
|
||||
}
|
||||
|
||||
$classes[] = ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) );
|
||||
|
||||
/**
|
||||
* Filters the login page body classes.
|
||||
*
|
||||
* @since 3.5.0
|
||||
*
|
||||
* @param array $classes An array of body classes.
|
||||
* @param string $action The action that brought the visitor to the login page.
|
||||
*/
|
||||
$classes = apply_filters( 'login_body_class', $classes, $action );
|
||||
|
||||
?>
|
||||
</head>
|
||||
<body class="login no-js <?php echo esc_attr( implode( ' ', $classes ) ); ?>">
|
||||
<script type="text/javascript">
|
||||
document.body.className = document.body.className.replace('no-js','js');
|
||||
</script>
|
||||
<?php
|
||||
/**
|
||||
* Fires in the login page header after the body tag is opened.
|
||||
*
|
||||
* @since 4.6.0
|
||||
*/
|
||||
do_action( 'login_header' );
|
||||
|
||||
?>
|
||||
<div id="login">
|
||||
<h1><a href="<?php echo esc_url( $login_header_url ); ?>"><?php echo esc_html( $login_header_text ); ?></a></h1>
|
||||
<?php
|
||||
/**
|
||||
* Filters the message to display above the login form.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*
|
||||
* @param string $message Login message text.
|
||||
*/
|
||||
$message = apply_filters( 'login_message', $message );
|
||||
|
||||
if ( ! empty( $message ) ) {
|
||||
echo esc_html( $message ) . "\n";
|
||||
}
|
||||
|
||||
// In case a plugin uses $error rather than the $wp_errors object.
|
||||
if ( ! empty( $error ) ) {
|
||||
$wp_error->add( 'error', $error );
|
||||
unset( $error );
|
||||
}
|
||||
|
||||
if ( $wp_error->has_errors() ) {
|
||||
$errors = '';
|
||||
$messages = '';
|
||||
|
||||
foreach ( $wp_error->get_error_codes() as $code ) {
|
||||
$severity = $wp_error->get_error_data( $code );
|
||||
foreach ( $wp_error->get_error_messages( $code ) as $error_message ) {
|
||||
if ( 'message' === $severity ) {
|
||||
$messages .= ' ' . $error_message . "<br />\n";
|
||||
} else {
|
||||
$errors .= ' ' . $error_message . "<br />\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
/**
|
||||
* Filters the error messages displayed above the login form.
|
||||
*
|
||||
* @since 2.1.0
|
||||
*
|
||||
* @param string $errors Login error message.
|
||||
*/
|
||||
echo '<div id="login_error">' . esc_html( apply_filters( 'login_errors', $errors ) ) . "</div>\n";
|
||||
}
|
||||
|
||||
if ( ! empty( $messages ) ) {
|
||||
/**
|
||||
* Filters instructional messages displayed above the login form.
|
||||
*
|
||||
* @since 2.5.0
|
||||
*
|
||||
* @param string $messages Login messages.
|
||||
*/
|
||||
echo '<p class="message">' . esc_html( apply_filters( 'login_messages', $messages ) ) . "</p>\n";
|
||||
}
|
||||
}
|
||||
} // End of login_header().
|
||||
|
||||
/**
|
||||
* Outputs the viewport meta tag for the login page.
|
||||
*
|
||||
* @since 3.7.0
|
||||
*/
|
||||
function wp_login_viewport_meta() {
|
||||
?>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Holds the request parameters for a specific action.
|
||||
*
|
||||
* @package REALLY_SIMPLE_SSL
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa;
|
||||
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* Check if a user is forced.
|
||||
*
|
||||
* @param WP_User $user The user to check.
|
||||
*
|
||||
* @return bool True if the user is forced, false otherwise.
|
||||
*/
|
||||
interface Rsssl_Two_Factor_Provider_Interface {
|
||||
/**
|
||||
* Check if a user is forced.
|
||||
*
|
||||
* @param WP_User $user The user to check.
|
||||
*
|
||||
* @return bool True if the user is forced, false otherwise.
|
||||
*/
|
||||
public static function is_forced( WP_User $user ): bool;
|
||||
|
||||
/**
|
||||
* Check if a method is enabled within the roles of the user.
|
||||
*
|
||||
* @param WP_User $user The user to check.
|
||||
*
|
||||
* @return bool True if the user is enabled, false otherwise.
|
||||
*/
|
||||
public static function is_enabled( WP_User $user ): bool;
|
||||
|
||||
public static function is_optional( WP_User $user ): bool;
|
||||
|
||||
public static function is_configured( WP_User $user ): bool;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
/**
|
||||
* Trait for sending emails related to two-factor authentication.
|
||||
*
|
||||
* @package RSSSL\Pro\Security\WordPress\Two_Fa\Traits
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa\Traits;
|
||||
|
||||
use rsssl_mailer;
|
||||
use WP_User;
|
||||
|
||||
/**
|
||||
* Trait Rsssl_Email_Trait
|
||||
*
|
||||
* This trait handles email notifications related to password reset and compromised passwords.
|
||||
*/
|
||||
trait Rsssl_Email_Trait {
|
||||
|
||||
/**
|
||||
* Notify the user that their password has been compromised and reset.
|
||||
*
|
||||
* @param WP_User $user The user to notify.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function notify_user_password_reset( WP_User $user ): void {
|
||||
$subject = __( 'Your password was compromised and has been reset', 'really-simple-ssl' );
|
||||
$message = self::create_user_message( $user );
|
||||
|
||||
if ( ! class_exists( 'rsssl_mailer' ) ) {
|
||||
require_once rsssl_path . 'mailer/class-mail.php';
|
||||
}
|
||||
|
||||
$mailer = self::initialize_mailer( $subject, $message, $user );
|
||||
$mailer->send_mail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a user message for failed login attempts.
|
||||
*
|
||||
* @param WP_User $user The user object.
|
||||
*
|
||||
* @return string The user message.
|
||||
*/
|
||||
private static function create_user_message( WP_User $user ): string {
|
||||
$message = sprintf(
|
||||
/* translators: %1$s: user login, %2$s: site url, %3$s: password best practices link, %4$s: lost password url */
|
||||
__(
|
||||
'Hello %1$s, an unusually high number of failed login attempts have been detected on your account at %2$s.
|
||||
|
||||
These attempts successfully entered your password, and were only blocked because they failed to enter your second authentication factor. Despite not being able to access your account, this behavior indicates that the attackers have compromised your password. The most common reasons for this are that your password was easy to guess, or was reused on another site which has been compromised.
|
||||
|
||||
To protect your account, your password has been reset, and you will need to create a new one. For advice on setting a strong password, please read %3$s
|
||||
|
||||
To pick a new password, please visit %4$s
|
||||
|
||||
This is an automated notification. If you would like to speak to a site administrator, please contact them directly.',
|
||||
'really-simple-ssl'
|
||||
),
|
||||
esc_html( $user->user_login ),
|
||||
home_url(),
|
||||
'https://wordpress.org/documentation/article/password-best-practices/',
|
||||
esc_url( add_query_arg( 'action', 'lostpassword', rsssl_wp_login_url() ) )
|
||||
);
|
||||
|
||||
return str_replace( "\t", '', $message );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Notify the admin that a user's password was compromised and reset.
|
||||
*
|
||||
* @param WP_User $user The user whose password was reset.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function notify_admin_user_password_reset( WP_User $user ): void {
|
||||
if ( ! class_exists( 'rsssl_mailer' ) ) {
|
||||
require_once rsssl_path . 'mailer/class-mail.php';
|
||||
}
|
||||
|
||||
$subject = self::create_subject( $user );
|
||||
$message = self::create_message( $user );
|
||||
|
||||
$mailer = self::initialize_mailer( $subject, $message, $user );
|
||||
|
||||
$mailer->send_mail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create subject for the compromised password reset email.
|
||||
*
|
||||
* @param WP_User $user The user object.
|
||||
*
|
||||
* @return string The subject of the email.
|
||||
*/
|
||||
private static function create_subject( WP_User $user ): string {
|
||||
/* translators: %s: user login */
|
||||
return sprintf(
|
||||
__( 'Compromised password for %s has been reset', 'really-simple-ssl' ),
|
||||
esc_html( $user->user_login )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a message for notifying the user about a high number of failed login attempts.
|
||||
*
|
||||
* @param WP_User $user The user for whom the message is created.
|
||||
*
|
||||
* @return string The generated message.
|
||||
*/
|
||||
private static function create_message( WP_User $user ): string {
|
||||
$documentation_url = 'https://developer.wordpress.org/plugins/hooks/';
|
||||
|
||||
return str_replace(
|
||||
"\t",
|
||||
'',
|
||||
// translators: %1$s: user login, %2$d: user ID, %3$s: documentation URL.
|
||||
sprintf(
|
||||
__( 'Hello, this is a notice from your website to inform you that an unusually high number of failed login attempts have been detected on the %1$s account (ID %2$d). Those attempts successfully entered the user\'s password, and were only blocked because they entered invalid second authentication factors. To protect their account, the password has automatically been reset, and they have been notified that they will need to create a new one. If you do not wish to receive these notifications, you can disable them with the `two_factor_notify_admin_user_password_reset` filter. See %3$s for more information. Thank you', 'really-simple-ssl' ),
|
||||
esc_html( $user->user_login ),
|
||||
$user->ID,
|
||||
$documentation_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the mailer for sending a notification email.
|
||||
*
|
||||
* @param string $subject The subject of the email.
|
||||
* @param string $message The message content of the email.
|
||||
* @param WP_User $user The user object to send the email to.
|
||||
*
|
||||
* @return rsssl_mailer The initialized mailer object.
|
||||
*/
|
||||
private static function initialize_mailer( string $subject, string $message, WP_User $user ): rsssl_mailer {
|
||||
$mailer = new rsssl_mailer();
|
||||
$mailer->subject = $subject;
|
||||
$mailer->branded = false;
|
||||
$mailer->sent_by_text = "<b>" . sprintf( __( 'Notification by %s', 'really-simple-ssl' ), site_url() ) . "</b>";
|
||||
$mailer->template_filename = apply_filters( 'rsssl_email_template', rsssl_path . '/mailer/templates/email-unbranded.html' );
|
||||
$mailer->to = $user->user_email;
|
||||
$mailer->title = __( 'Compromised password reset', 'really-simple-ssl' );
|
||||
$mailer->message = $message;
|
||||
|
||||
return $mailer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* A helper trait for sanitizing status and method values.
|
||||
*
|
||||
* @package really-simple-ssl
|
||||
*/
|
||||
|
||||
namespace RSSSL\Security\WordPress\Two_Fa\Traits;
|
||||
|
||||
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Provider_Loader;
|
||||
use RSSSL\Security\WordPress\Two_Fa\Rsssl_Two_Fa_Status;
|
||||
|
||||
/**
|
||||
* A helper trait for sanitizing status and method values.
|
||||
*/
|
||||
trait Rsssl_Two_Fa_Helper {
|
||||
/**
|
||||
* Sanitize the given status.
|
||||
*
|
||||
* @param string $status The status to sanitize.
|
||||
*
|
||||
* @return string The sanitized status.
|
||||
*/
|
||||
private static function sanitize_status( string $status ): string {
|
||||
$statuses_available = Rsssl_Two_Fa_Status::STATUSES;
|
||||
|
||||
if ( empty( $status ) ) {
|
||||
return 'open';
|
||||
}
|
||||
// Check if the $status is in the array of available statuses.
|
||||
if ( ! in_array( $status, $statuses_available, true ) ) {
|
||||
// if not, set it to 'disabled'.
|
||||
$status = 'disabled';
|
||||
}
|
||||
|
||||
return sanitize_text_field( $status );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a given method.
|
||||
*
|
||||
* @param string $method The method to sanitize.
|
||||
*
|
||||
* @return string The sanitized method.
|
||||
*/
|
||||
private static function sanitize_method( string $method ): string {
|
||||
$methods_available = Rsssl_Provider_Loader::METHODS;
|
||||
// Check if the $method is in the array of available methods.
|
||||
if ( ! in_array( $method, $methods_available, true ) ) {
|
||||
// if not, set it to 'disabled'.
|
||||
$method = 'disabled';
|
||||
}
|
||||
|
||||
return sanitize_text_field( $method );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
defined('ABSPATH') or die();
|
||||
/**
|
||||
* Prevent User Enumeration
|
||||
* @return void
|
||||
*/
|
||||
function rsssl_check_user_enumeration() {
|
||||
if ( ! is_user_logged_in() && isset( $_REQUEST['author'] ) ) {
|
||||
if ( preg_match( '/\\d/', $_REQUEST['author'] ) > 0 ) {
|
||||
wp_die( sprintf(__( 'forbidden - number in author name not allowed = %s', 'really-simple-ssl' ), esc_html( $_REQUEST['author'] ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action('init', 'rsssl_check_user_enumeration');
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* Remove author from Yoast sitemap
|
||||
*/
|
||||
function rsssl_remove_author_from_yoast_sitemap( $users ) {
|
||||
return false;
|
||||
}
|
||||
add_filter('wpseo_sitemap_exclude_author', 'rsssl_remove_author_from_yoast_sitemap', 10, 1 );
|
||||
|
||||
/**
|
||||
* Prevent WP JSON API User Enumeration
|
||||
* Do not disable in when logged in, preventing issues in the Gutenberg Editor
|
||||
*/
|
||||
|
||||
if ( !is_user_logged_in() || !current_user_can('edit_posts') ) {
|
||||
add_filter( 'rest_endpoints', function ( $endpoints ) {
|
||||
if ( isset( $endpoints['/wp/v2/users'] ) ) {
|
||||
unset( $endpoints['/wp/v2/users'] );
|
||||
}
|
||||
if ( isset( $endpoints['/wp/v2/users/(?P[\d]+)'] ) ) {
|
||||
unset( $endpoints['/wp/v2/users/(?P[\d]+)'] );
|
||||
}
|
||||
return $endpoints;
|
||||
} );
|
||||
}
|
||||
|
||||
//prevent xml site map user enumeration
|
||||
add_filter(
|
||||
'wp_sitemaps_add_provider',
|
||||
function( $provider, $name ) {
|
||||
if ( 'users' === $name ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $provider;
|
||||
},
|
||||
10,
|
||||
2
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
defined('ABSPATH') or die();
|
||||
/**
|
||||
* Action to disable user registration
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function rsssl_users_can_register($value, $option) {
|
||||
return false;
|
||||
}
|
||||
add_filter( "option_users_can_register", 'rsssl_users_can_register', 999, 2 );
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace security\wordpress\vulnerabilities;
|
||||
defined('ABSPATH') or die();
|
||||
class FileStorage
|
||||
{
|
||||
private $hash;
|
||||
|
||||
/**
|
||||
* FileStorage constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//Fetching the key from the database
|
||||
$this->generateHashKey();
|
||||
}
|
||||
|
||||
public Static function StoreFile($file, $data)
|
||||
{
|
||||
$storage = new FileStorage();
|
||||
$storage->set($data, $file);
|
||||
}
|
||||
|
||||
public Static function GetFile($file)
|
||||
{
|
||||
$storage = new FileStorage();
|
||||
return $storage->get($file);
|
||||
}
|
||||
|
||||
/** Get the data from the file
|
||||
* @param $file
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function get($file)
|
||||
{
|
||||
if (file_exists($file)) {
|
||||
$data = file_get_contents($file);
|
||||
$data = $this->Decode64WithHash($data);
|
||||
return json_decode($data);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Save the data to the file
|
||||
* @param $data
|
||||
* @param $file
|
||||
*/
|
||||
public function set($data, $file)
|
||||
{
|
||||
$data = $this->Encode64WithHash(json_encode($data));
|
||||
file_put_contents($file, $data);
|
||||
}
|
||||
|
||||
/** encode the data with a hash
|
||||
* @param $data
|
||||
* @return string
|
||||
*/
|
||||
private function Encode64WithHash($data): string
|
||||
{
|
||||
//we create a simple encoding, using the hashkey as a salt
|
||||
$data = base64_encode($data);
|
||||
return base64_encode($data . $this->hash);
|
||||
}
|
||||
|
||||
/** decode the data with a hash
|
||||
* @param $data
|
||||
* @return string
|
||||
*/
|
||||
private function Decode64WithHash($data): string
|
||||
{
|
||||
//we create a simple decoding, using the hashkey as a salt
|
||||
$data = base64_decode($data);
|
||||
$data = substr($data, 0, -strlen($this->hash));
|
||||
return base64_decode($data);
|
||||
}
|
||||
|
||||
/** Generate a hashkey and store it in the database
|
||||
* @return void
|
||||
*/
|
||||
private function generateHashKey(): void
|
||||
{
|
||||
if (get_option('rsssl_hashkey') && get_option('rsssl_hashkey') !== "") {
|
||||
$this->hash = get_option('rsssl_hashkey');
|
||||
} else {
|
||||
$this->hash = md5(uniqid(rand(), true));
|
||||
update_option('rsssl_hashkey', $this->hash, false);
|
||||
}
|
||||
}
|
||||
|
||||
public static function GetDate(string $file)
|
||||
{
|
||||
if (file_exists($file)) {
|
||||
return filemtime($file);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function DeleteAll()
|
||||
{
|
||||
//we get the upload folder
|
||||
$upload_dir = wp_upload_dir();
|
||||
|
||||
//we get the really-simple-ssl folder
|
||||
$rsssl_dir = $upload_dir['basedir'] . '/really-simple-ssl';
|
||||
|
||||
//then we delete the following files from that folder: manifest.json, components.json and core.json
|
||||
$files = array('manifest.json', 'components.json', 'core.json');
|
||||
foreach ($files as $file) {
|
||||
//we delete the file
|
||||
$file = $rsssl_dir . '/' . $file;
|
||||
if (file_exists($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace security\wordpress\vulnerabilities;
|
||||
|
||||
defined( 'ABSPATH' ) or die();
|
||||
require_once rsssl_path . 'lib/admin/class-encryption.php';
|
||||
require_once 'class-rsssl-folder-name.php';
|
||||
|
||||
use RSSSL\lib\admin\Encryption;
|
||||
|
||||
class Rsssl_File_Storage {
|
||||
use Encryption;
|
||||
public $folder; //for the folder name
|
||||
|
||||
/**
|
||||
* Rsssl_File_Storage constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
//Fetching the key from the database
|
||||
$upload_dir = wp_upload_dir();
|
||||
$this->folder = $upload_dir['basedir'] . '/' . Rsssl_Folder_Name::getFolderName();
|
||||
}
|
||||
|
||||
public static function StoreFile( $file, $data ): void {
|
||||
$storage = new Rsssl_File_Storage();
|
||||
//first we check if the storage folder is already in the $file string
|
||||
if ( strpos( $file, $storage->folder ) !== false ) {
|
||||
$file = str_replace( $storage->folder . '/', '', $file );
|
||||
}
|
||||
$storage->set( $data, $storage->folder . '/' . $file );
|
||||
}
|
||||
|
||||
public static function GetFile( $file ) {
|
||||
$storage = new Rsssl_File_Storage();
|
||||
|
||||
//first we check if the storage folder is already in the $file string
|
||||
if ( strpos( $file, $storage->folder ) !== false ) {
|
||||
$file = str_replace( $storage->folder . '/', '', $file );
|
||||
}
|
||||
|
||||
return $storage->get( $storage->folder . '/' . $file );
|
||||
}
|
||||
|
||||
/** Get the data from the file
|
||||
*
|
||||
* @param $file
|
||||
*
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function get( $file ) {
|
||||
if ( file_exists( $file ) ) {
|
||||
$data = file_get_contents( $file );
|
||||
$data = $this->decrypt( $data );
|
||||
return json_decode( $data );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Save the data to the file
|
||||
*
|
||||
* @param $data
|
||||
* @param $file
|
||||
*/
|
||||
public function set( $data, $file ) {
|
||||
if ( ! is_dir( $this->folder ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! is_writable( $this->folder ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = $this->encrypt( json_encode( $data ) );
|
||||
//first we check if the storage folder is already in the $file string
|
||||
if ( strpos( $file, $this->folder ) !== false ) {
|
||||
$file = str_replace( $this->folder . '/', '', $file );
|
||||
}
|
||||
|
||||
file_put_contents( $this->folder . '/' . $file, $data );
|
||||
}
|
||||
|
||||
public static function GetDate( string $file ) {
|
||||
if ( file_exists( $file ) ) {
|
||||
return filemtime( $file );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function get_upload_dir() {
|
||||
return ( new Rsssl_File_Storage() )->folder;
|
||||
}
|
||||
|
||||
public static function validateFile( string $file ): bool {
|
||||
$storage = new Rsssl_File_Storage();
|
||||
$file = $storage->folder . '/' . $file;
|
||||
if ( file_exists( $file ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all files in the storage folder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function DeleteAll(): void {
|
||||
$storage = new Rsssl_File_Storage();
|
||||
//we get the really-simple-ssl folder
|
||||
$rsssl_dir = $storage->folder;
|
||||
//then we delete the following files from that folder: manifest.json, components.json and core.json
|
||||
$files = array( 'manifest.json', 'components.json', 'core.json' );
|
||||
foreach ( $files as $file ) {
|
||||
//we delete the file
|
||||
$file = $rsssl_dir . '/' . $file;
|
||||
if ( file_exists( $file ) ) {
|
||||
unlink( $file );
|
||||
}
|
||||
}
|
||||
//we delete the folder
|
||||
if ( file_exists( $rsssl_dir ) ) {
|
||||
self::DeleteFolder($rsssl_dir);
|
||||
//we delete the option
|
||||
delete_option( 'rsssl_folder_name' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete a folder and its contents.
|
||||
*
|
||||
* @param string $dir The path to the folder to be deleted.
|
||||
*
|
||||
* @return bool Returns true if the folder was successfully deleted, false otherwise.
|
||||
*/
|
||||
public static function DeleteFolder($dir): bool {
|
||||
if (substr($dir, strlen($dir) - 1, 1) != '/')
|
||||
$dir .= '/';
|
||||
|
||||
if ($handle = opendir($dir)) {
|
||||
while ($obj = readdir($handle)) {
|
||||
if ($obj != '.' && $obj != '..') {
|
||||
if (is_dir($dir.$obj)) {
|
||||
if (!self::DeleteFolder($dir.$obj))
|
||||
return false;
|
||||
}
|
||||
elseif (is_file($dir.$obj)) {
|
||||
if (!unlink($dir.$obj))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir($handle);
|
||||
|
||||
if (!rmdir($dir))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all files in the storage folder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function DeleteOldFiles(): void {
|
||||
$rsssl_dir = wp_upload_dir()['basedir'] . '/really-simple-ssl';
|
||||
//then we delete the following files from that folder: manifest.json, components.json and core.json
|
||||
$files = array( 'manifest.json', 'components.json', 'core.json' );
|
||||
foreach ( $files as $file ) {
|
||||
//we delete the file
|
||||
$file = $rsssl_dir . '/' . $file;
|
||||
if ( file_exists( $file ) ) {
|
||||
unlink( $file );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace security\wordpress\vulnerabilities;
|
||||
require_once rsssl_path . '/lib/admin/class-helper.php';
|
||||
use RSSSL\lib\admin\Helper;
|
||||
class Rsssl_Folder_Name {
|
||||
use Helper;
|
||||
public $folderName;
|
||||
|
||||
private function __construct() {
|
||||
$this->initializeFolderName();
|
||||
$this->verifyAndCreateFolder();
|
||||
}
|
||||
|
||||
private function initializeFolderName(): void {
|
||||
$rsssl_folder = get_option( 'rsssl_folder_name' );
|
||||
|
||||
if ( $rsssl_folder ) {
|
||||
$this->folderName = $this->folderName( $rsssl_folder );
|
||||
} else {
|
||||
$newFolderName = 'really-simple-ssl/' . md5( uniqid( mt_rand(), true ) );
|
||||
$this->folderName = $this->folderName( $newFolderName );
|
||||
|
||||
require_once 'class-rsssl-file-storage.php';
|
||||
Rsssl_File_Storage::DeleteOldFiles();
|
||||
update_option( 'rsssl_folder_name', $this->folderName );
|
||||
}
|
||||
}
|
||||
|
||||
private function folderName( $name ): string {
|
||||
return $name;
|
||||
}
|
||||
|
||||
private function verifyAndCreateFolder(): void {
|
||||
$upload_dir = wp_upload_dir();
|
||||
if ( ! file_exists( $upload_dir['basedir'] . '/' . $this->folderName ) ) {
|
||||
$this->createFolder();
|
||||
}
|
||||
}
|
||||
|
||||
public function createFolder(): void {
|
||||
$upload_dir = wp_upload_dir();
|
||||
$folder_path = $upload_dir['basedir'] . '/' . $this->folderName;
|
||||
|
||||
if ( ! file_exists( $folder_path ) && is_writable($upload_dir['basedir'] ) ) {
|
||||
if ( ! mkdir( $folder_path, 0755, true ) && ! is_dir( $folder_path ) ) {
|
||||
$this->log( sprintf( 'Really Simple Security: Directory "%s" was not created', $folder_path ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new folder name and saves it in the settings
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getFolderName(): string
|
||||
{
|
||||
return (new Rsssl_Folder_Name())->folderName;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user