335 lines
7.8 KiB
PHP
335 lines
7.8 KiB
PHP
<?php
|
|
/**
|
|
* Core plugin bootstrap.
|
|
*
|
|
* @package Robotstxt_2FA
|
|
*/
|
|
|
|
namespace Robotstxt\TwoFA;
|
|
|
|
use Robotstxt\TwoFA\Admin\Dashboard_Page;
|
|
use Robotstxt\TwoFA\Admin\Manager_Notice;
|
|
use Robotstxt\TwoFA\Admin\Migration_Page;
|
|
use Robotstxt\TwoFA\Admin\Settings_Page;
|
|
use Robotstxt\TwoFA\Cli\Cli_Command;
|
|
use Robotstxt\TwoFA\Login\Login_Form_Manager;
|
|
use Robotstxt\TwoFA\Rest\Rest_Controller;
|
|
use Robotstxt\TwoFA\User\Frontend_Profile;
|
|
use Robotstxt\TwoFA\User\Grace_Period;
|
|
use Robotstxt\TwoFA\User\Profile_Settings;
|
|
use Robotstxt\TwoFA\User\Recovery_Codes;
|
|
use Robotstxt\TwoFA\User\Trusted_Devices;
|
|
use Robotstxt\TwoFA\User\Two_Factor_Config;
|
|
use Robotstxt\TwoFA\User\User_Settings_Repository;
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Main plugin controller.
|
|
*/
|
|
class Plugin {
|
|
/**
|
|
* Plugin instance.
|
|
*
|
|
* @var Plugin|null
|
|
*/
|
|
private static ?Plugin $instance = null;
|
|
|
|
/**
|
|
* Login form manager.
|
|
*
|
|
* @var Login_Form_Manager
|
|
*/
|
|
private Login_Form_Manager $login_manager;
|
|
|
|
/**
|
|
* Profile settings manager.
|
|
*
|
|
* @var Profile_Settings
|
|
*/
|
|
private Profile_Settings $profile_settings;
|
|
|
|
/**
|
|
* Recovery codes manager.
|
|
*
|
|
* @var Recovery_Codes
|
|
*/
|
|
private Recovery_Codes $recovery_codes;
|
|
|
|
/**
|
|
* Settings page handler.
|
|
*
|
|
* @var Settings_Page
|
|
*/
|
|
private Settings_Page $settings_page;
|
|
|
|
/**
|
|
* Frontend profile shortcode handler.
|
|
*
|
|
* @var Frontend_Profile
|
|
*/
|
|
private Frontend_Profile $frontend_profile;
|
|
|
|
/**
|
|
* Trusted devices manager.
|
|
*
|
|
* @var Trusted_Devices
|
|
*/
|
|
private Trusted_Devices $trusted_devices;
|
|
|
|
/**
|
|
* Grace period manager.
|
|
*
|
|
* @var Grace_Period
|
|
*/
|
|
private Grace_Period $grace_period;
|
|
|
|
/**
|
|
* Failed attempts log.
|
|
*
|
|
* @var Attempts_Log
|
|
*/
|
|
private Attempts_Log $attempts_log;
|
|
|
|
/**
|
|
* Email notifications handler.
|
|
*
|
|
* @var Email_Notifications
|
|
*/
|
|
private Email_Notifications $email_notifications;
|
|
|
|
/**
|
|
* IP restrictions handler.
|
|
*
|
|
* @var IP_Restrictions
|
|
*/
|
|
private IP_Restrictions $ip_restrictions;
|
|
|
|
/**
|
|
* GeoIP country restrictions handler.
|
|
*
|
|
* @var Geo_Restrictions
|
|
*/
|
|
private Geo_Restrictions $geo_restrictions;
|
|
|
|
/**
|
|
* GeoIP database auto-download handler.
|
|
*
|
|
* @var GeoIP_Updater
|
|
*/
|
|
private GeoIP_Updater $geoip_updater;
|
|
|
|
/**
|
|
* Audit dashboard page.
|
|
*
|
|
* @var Dashboard_Page
|
|
*/
|
|
private Dashboard_Page $dashboard_page;
|
|
|
|
/**
|
|
* Migration / import page.
|
|
*
|
|
* @var Migration_Page
|
|
*/
|
|
private Migration_Page $migration_page;
|
|
|
|
/**
|
|
* Manager plugin dependency notice.
|
|
*
|
|
* @var Manager_Notice
|
|
*/
|
|
private Manager_Notice $manager_notice;
|
|
|
|
/**
|
|
* REST API controller.
|
|
*
|
|
* @var Rest_Controller
|
|
*/
|
|
private Rest_Controller $rest_controller;
|
|
|
|
/**
|
|
* Get singleton instance.
|
|
*
|
|
* @return Plugin
|
|
*/
|
|
public static function get_instance(): Plugin {
|
|
if ( null === self::$instance ) {
|
|
self::$instance = new self();
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct() {
|
|
$config = new Two_Factor_Config();
|
|
$repository = new User_Settings_Repository();
|
|
$this->attempts_log = new Attempts_Log();
|
|
$this->login_manager = new Login_Form_Manager();
|
|
$this->profile_settings = new Profile_Settings();
|
|
$this->recovery_codes = new Recovery_Codes();
|
|
$this->settings_page = new Settings_Page();
|
|
$this->frontend_profile = new Frontend_Profile();
|
|
$this->trusted_devices = new Trusted_Devices();
|
|
$this->grace_period = new Grace_Period();
|
|
$this->email_notifications = new Email_Notifications( $config );
|
|
$this->ip_restrictions = new IP_Restrictions( $config );
|
|
$this->geo_restrictions = new Geo_Restrictions( $config );
|
|
$this->geoip_updater = new GeoIP_Updater( $config );
|
|
$this->dashboard_page = new Dashboard_Page( $this->attempts_log, $config, $repository );
|
|
$this->migration_page = new Migration_Page();
|
|
$this->manager_notice = new Manager_Notice();
|
|
$this->rest_controller = new Rest_Controller();
|
|
}
|
|
|
|
/**
|
|
* Plugin initialization.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function init(): void {
|
|
$this->maybe_load_textdomain();
|
|
$this->attempts_log->register_hooks();
|
|
$this->email_notifications->register_hooks();
|
|
$this->ip_restrictions->register_hooks();
|
|
$this->geo_restrictions->register_hooks();
|
|
$this->geoip_updater->register_hooks();
|
|
$this->dashboard_page->register_hooks();
|
|
$this->migration_page->register_hooks();
|
|
$this->manager_notice->register_hooks();
|
|
$this->rest_controller->register_hooks();
|
|
$this->login_manager->register_hooks();
|
|
$this->profile_settings->register_hooks();
|
|
$this->recovery_codes->register_hooks();
|
|
$this->settings_page->register_hooks();
|
|
$this->frontend_profile->register_hooks();
|
|
$this->trusted_devices->register_hooks();
|
|
$this->grace_period->register_hooks();
|
|
|
|
add_filter( 'rest_pre_dispatch', array( $this, 'maybe_block_app_password_creation' ), 10, 3 );
|
|
|
|
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
|
add_action( 'cli_init', array( $this, 'register_cli_commands' ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Block Application Password creation when the user has not recently verified 2FA.
|
|
*
|
|
* @since 1.5.0
|
|
*
|
|
* @param mixed $result Current result (null = not yet dispatched).
|
|
* @param \WP_REST_Server $server REST server instance.
|
|
* @param \WP_REST_Request $request Current request.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function maybe_block_app_password_creation( mixed $result, \WP_REST_Server $server, \WP_REST_Request $request ): mixed {
|
|
unset( $server );
|
|
|
|
if ( null !== $result ) {
|
|
return $result;
|
|
}
|
|
|
|
if ( 'POST' !== $request->get_method() ) {
|
|
return $result;
|
|
}
|
|
|
|
$config = new Two_Factor_Config();
|
|
|
|
if ( ! $config->is_app_passwords_require_2fa_creation() ) {
|
|
return $result;
|
|
}
|
|
|
|
if ( ! preg_match( '#^/wp/v2/users/(?:\d+|me)/application-passwords$#', $request->get_route() ) ) {
|
|
return $result;
|
|
}
|
|
|
|
$user = wp_get_current_user();
|
|
|
|
if ( ! $user->exists() ) {
|
|
return $result;
|
|
}
|
|
|
|
$verified_at = 0;
|
|
$raw = get_user_meta( $user->ID, 'robotstxt_2fa_last_verifications', true );
|
|
|
|
if ( is_array( $raw ) ) {
|
|
foreach ( $raw as $method_data ) {
|
|
if ( is_array( $method_data ) ) {
|
|
foreach ( $method_data as $context_data ) {
|
|
if ( is_array( $context_data ) && isset( $context_data['verified_at'] ) && is_int( $context_data['verified_at'] ) ) {
|
|
$verified_at = max( $verified_at, $context_data['verified_at'] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filter the 2FA recency window (in seconds) required before creating an Application Password.
|
|
*
|
|
* @since 1.5.0
|
|
*
|
|
* @param int $window Seconds since last 2FA verification. Default 900 (15 minutes).
|
|
*/
|
|
$window = (int) apply_filters( 'robotstxt_2fa_app_password_verification_window', 900 );
|
|
if ( $window <= 0 ) {
|
|
$window = 900;
|
|
}
|
|
|
|
if ( $verified_at > 0 && ( time() - $verified_at ) <= $window ) {
|
|
return $result;
|
|
}
|
|
|
|
return new \WP_Error(
|
|
'robotstxt_2fa_required',
|
|
__( 'A recent two-factor authentication verification is required to create an Application Password. Please log in and complete 2FA, then try again.', 'robotstxt-2fa' ),
|
|
array( 'status' => 401 )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register WP-CLI commands.
|
|
*
|
|
* Called only when WP_CLI is active.
|
|
*
|
|
* @since 1.3.0
|
|
*
|
|
* @return void
|
|
*/
|
|
public function register_cli_commands(): void {
|
|
\WP_CLI::add_command( '2fa', new Cli_Command() );
|
|
}
|
|
|
|
/**
|
|
* Runs on activation.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function activate(): void {
|
|
// Placeholder for activation tasks (option creation, migrations, etc.).
|
|
}
|
|
|
|
/**
|
|
* Runs on deactivation.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function deactivate(): void {
|
|
// Placeholder for deactivation cleanup logic.
|
|
}
|
|
|
|
/**
|
|
* Load text domain for translations.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function maybe_load_textdomain(): void {
|
|
load_plugin_textdomain( 'robotstxt-2fa', false, dirname( plugin_basename( ROBOTSTXT_2FA_FILE ) ) . '/languages/' );
|
|
}
|
|
}
|