robotstxt-2fa/includes/class-plugin.php
2026-08-17 15:55:46 +00:00

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/' );
}
}