<?php
/**
 * Handles Captcha related actions.
 *
 * @package WP_Defender\Controller
 */

namespace WP_Defender\Controller;

use WP_User;
use WP_Error;
use WP_Defender\Event;
use Calotes\Component\Request;
use Calotes\Component\Response;
use WP_Defender\Component\Crypt;
use WP_Defender\Behavior\WPMUDEV;
use WP_Defender\Traits\Hummingbird;
use WP_Defender\Integrations\Buddypress;
use WP_Defender\Integrations\Woocommerce;
use WP_Defender\Component\Config\Config_Hub_Helper;
use WP_Defender\Component\Captcha as Captcha_Component;
use WP_Defender\Model\Setting\Captcha as Captcha_Model;

/**
 * Handles Captcha related actions.
 *
 * @since 2.5.4
 */
class Captcha extends Event {

	use Hummingbird;

	/**
	 * Accepted values: v2_checkbox, v2_invisible, v3_recaptcha, turnstile.
	 *
	 * @var string
	 */
	private $recaptcha_type;

	/**
	 * Accepted values: light and dark.
	 *
	 * @var string
	 */
	private $recaptcha_theme;

	/**
	 * Accepted values: normal and compact.
	 *
	 * @var string
	 */
	private $recaptcha_size;

	/**
	 * Recaptcha API public key.
	 *
	 * @var string
	 */
	private $public_key;

	/**
	 * Recaptcha API private key.
	 *
	 * @var string
	 */
	private $private_key;

	/**
	 * Language for the reCAPTCHA.
	 *
	 * @var string
	 */
	private $language;

	/**
	 * Default message for the reCAPTCHA.
	 *
	 * @var string
	 */
	private $default_msg;

	/**
	 * The model for handling the data.
	 *
	 * @var Captcha_Model
	 */
	public $model;

	/**
	 * Service for handling logic.
	 *
	 * @var Captcha_Component
	 */
	protected $service;

	/**
	 * Is Woo activated.
	 *
	 * @var bool
	 */
	private $is_woo_activated;

	/**
	 * Is BuddyPress activated.
	 *
	 * @var bool
	 */
	private $is_buddypress_activated;

	/**
	 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
	 */
	public function __construct() {
		$this->model   = wd_di()->get( Captcha_Model::class );
		$this->service = new Captcha_Component( $this->model );
		// Use default msg to avoid empty message error.
		$default_values    = $this->model->get_default_values();
		$this->default_msg = $default_values['message'];
		$this->register_routes();
		$this->is_woo_activated        = wd_di()->get( Woocommerce::class )->is_activated();
		$this->is_buddypress_activated = wd_di()->get( Buddypress::class )->is_activated();
		add_filter( 'wp_defender_advanced_tools_data', array( $this, 'script_data' ) );

		if (
			$this->model->is_active()
			// No need the check by Woo and Buddypress are activated because we use this below.
			&& $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated )
			&& ! $this->service->exclude_captcha_for_requests()
		) {
			$this->declare_variables();
			$this->add_actions();

			add_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ), 10, 2 );
		}
	}

	/**
	 * Declares the necessary variables for the reCAPTCHA functionality.
	 *
	 * @return void
	 */
	protected function declare_variables(): void {
		$this->recaptcha_type  = $this->model->active_type;
		$this->recaptcha_theme = 'light';
		$this->recaptcha_size  = 'invisible';
		$this->language        = '' !== $this->model->language && 'automatic' !== $this->model->language
			? $this->model->language
			: get_locale();

		// Add the CAPTCHA keys depending on the CAPTCHA type.
		if ( 'v2_checkbox' === $this->recaptcha_type ) {
			$this->public_key      = $this->model->data_v2_checkbox['key'];
			$this->private_key     = $this->model->data_v2_checkbox['secret'];
			$this->recaptcha_theme = $this->model->data_v2_checkbox['style'];
			$this->recaptcha_size  = $this->model->data_v2_checkbox['size'];
		} elseif ( 'v2_invisible' === $this->recaptcha_type ) {
			$this->public_key  = $this->model->data_v2_invisible['key'];
			$this->private_key = $this->model->data_v2_invisible['secret'];
		} elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
			$this->public_key  = $this->model->data_v3_recaptcha['key'];
			$this->private_key = $this->model->data_v3_recaptcha['secret'];
		} elseif ( 'turnstile' === $this->recaptcha_type ) {
			$this->public_key      = $this->model->data_turnstile['key'];
			$this->private_key     = $this->model->data_turnstile['secret'];
			$this->recaptcha_theme = $this->model->data_turnstile['style'] ?? 'auto';
			$this->recaptcha_size  = $this->model->data_turnstile['size'] ?? 'normal';
			$this->language        = $this->model->data_turnstile['language'] ?? 'auto';
		}
	}

	/**
	 * Is it Defender's CAPTCHA page?
	 *
	 * @return bool
	 */
	protected function is_captcha_settings(): bool {
		$view = defender_get_data_from_request( 'view', 'g' );
		return 'wdf-advanced-tools' === defender_get_current_page() && 'captcha' === $view;
	}

	/**
	 * Add actions for CAPTCHA.
	 *
	 * @return void
	 */
	protected function add_actions() {
		$extra_conditions = is_admin() && ! ( defined( 'DOING_AJAX' ) && $this->is_captcha_settings() );
		// Since v5.7.0.
		do_action( 'wd_captcha_before_actions', $extra_conditions );

		do_action_deprecated(
			'wd_recaptcha_before_actions',
			array( $extra_conditions ),
			'5.7.0',
			'wd_captcha_before_actions',
			__( 'This hook is deprecated and will be removed in future versions.', 'defender-security' )
		);
		if ( $extra_conditions ) {
			return;
		}

		$display_for_known_users = $this->model->display_for_known_users();
		$locations               = $this->model->locations;
		if (
				in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true )
				|| in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true )
				|| in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true )
		) {
			add_filter( 'cfturnstile_widget_disable', '__return_true' );
			add_filter( 'easy_cloudflare_turnstile_render_list', '__return_empty_array' );
			add_filter( 'easy_cloudflare_turnstile_verify_list', '__return_empty_array' );
			add_action( 'login_enqueue_scripts', array( $this->service, 'remove_duplicate_captcha_scripts' ), PHP_INT_MAX );
		}
		// Default login form.
		if ( in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true ) ) {
			add_filter( 'authenticate', array( $this, 'validate_login_captcha' ), 9999 );
			add_action( 'login_form', array( $this, 'display_login_captcha' ) );
			add_filter( 'wp_authenticate_user', array( $this, 'validate_captcha_field_on_login' ), 8 );
		}
		// Default register form.
		if ( in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true ) ) {
			if ( ! is_multisite() ) {
				add_action( 'register_form', array( $this, 'display_login_captcha' ) );
				add_filter( 'registration_errors', array( $this, 'validate_captcha_field_on_registration' ), 10 );
			} else {
				add_action( 'signup_extra_fields', array( $this, 'display_signup_captcha' ) );
				add_action( 'signup_blogform', array( $this, 'display_signup_captcha' ) );
				add_filter(
					'wpmu_validate_user_signup',
					array(
						$this,
						'validate_captcha_field_on_wpmu_registration',
					),
					10
				);
			}
		}
		// Default lost password form.
		if ( in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true ) ) {
			add_action( 'lostpassword_form', array( $this, 'display_login_captcha' ) );
			if ( $this->maybe_validate_captcha_for_lostpassword() ) {
				add_action( 'lostpassword_post', array( $this, 'validate_captcha_field_on_lostpassword' ) );
			}
		}
		// Default comment form.
		if ( $display_for_known_users && in_array( Captcha_Component::DEFAULT_COMMENT_FORM, $locations, true ) ) {
			// @since v3.4.0 Change from 'comment_form_after_fields' to 'comment_form_defaults'.
			add_filter( 'comment_form_defaults', array( $this, 'comment_form_defaults' ), 10 );
			add_action( 'pre_comment_on_post', array( $this, 'validate_captcha_field_on_comment' ) );
			// When comments are loaded via Hummingbird's lazy load feature.
			if ( $this->is_lazy_load_comments_enabled() ) {
				add_action( 'wp_footer', array( $this, 'add_scripts_for_lazy_load' ) );
			}
		}
		// Todo: move code to related class.
		// Only Recaptcha type has Integration fields in v5.7.0.
		// For Woo forms. Mandatory check for the activated Woo before.
		if ( $this->service->is_recaptcha_active_provider() && $this->model->check_woo_locations( $this->is_woo_activated ) ) {
			$woo_locations = $this->model->woo_checked_locations;
			// Woo login form.
			if ( in_array( Woocommerce::WOO_LOGIN_FORM, $woo_locations, true ) ) {
				add_action( 'woocommerce_login_form', array( $this, 'display_login_captcha' ) );
				add_filter(
					'woocommerce_process_login_errors',
					array(
						$this,
						'validate_captcha_field_on_woo_login',
					),
					10
				);
			}
			// Woo register form.
			if ( in_array( Woocommerce::WOO_REGISTER_FORM, $woo_locations, true ) ) {
				add_action( 'woocommerce_register_form', array( $this, 'display_login_captcha' ) );
				add_filter(
					'woocommerce_registration_errors',
					array(
						$this,
						'validate_captcha_field_on_woo_registration',
					),
					10
				);
			}
			// Woo lost password form.
			if ( in_array( Woocommerce::WOO_LOST_PASSWORD_FORM, $woo_locations, true ) ) {
				add_action( 'woocommerce_lostpassword_form', array( $this, 'display_login_captcha' ) );
				// Use default WP hook because Woo doesn't have own hook, so there's the extra check for Woo form.
				$post_data = defender_get_data_from_request( null, 'p' );
				if ( isset( $post_data['wc_reset_password'], $post_data['user_login'] ) ) {
					add_action( 'lostpassword_post', array( $this, 'validate_captcha_field_on_lostpassword' ) );
				}
			}
			// Woo checkout form.
			if ( $display_for_known_users && in_array( Woocommerce::WOO_CHECKOUT_FORM, $woo_locations, true ) ) {
				add_action( 'woocommerce_after_checkout_billing_form', array( $this, 'display_login_captcha' ) );
				add_action(
					'woocommerce_after_checkout_validation',
					array(
						$this,
						'validate_captcha_field_on_woo_checkout',
					),
					10,
					2
				);
			}
		}
		// Todo: move code to related class.
		// Only Recaptcha type has Integration fields in v5.7.0.
		// For BuddyPress forms. Mandatory check for the activated BuddyPress before.
		if ( $this->service->is_recaptcha_active_provider() && $this->model->check_buddypress_locations( $this->is_buddypress_activated ) ) {
			$buddypress_locations = $this->model->buddypress_checked_locations;
			// Register form.
			if ( in_array( Buddypress::REGISTER_FORM, $buddypress_locations, true ) ) {
				add_action( 'bp_before_registration_submit_buttons', array( $this, 'display_buddypress_recaptcha' ) );
				add_filter(
					'bp_signup_validate',
					array(
						$this,
						'validate_captcha_field_on_buddypress_registration',
					),
					10
				);
			}
			// Group form.
			if ( $display_for_known_users && in_array( Buddypress::NEW_GROUP_FORM, $buddypress_locations, true ) ) {
				add_action( 'bp_after_group_details_creation_step', array( $this, 'display_login_captcha' ) );
				add_action( 'groups_group_before_save', array( $this, 'validate_captcha_field_on_buddypress_group' ) );
			}
		}
		// Since v5.7.0.
		do_action( 'wd_captcha_after_actions', $display_for_known_users );

		do_action_deprecated(
			'wd_recaptcha_after_actions',
			array( $display_for_known_users ),
			'5.7.0',
			'wd_captcha_after_actions',
			__( 'This hook is deprecated and will be removed in a future version.', 'defender-security' )
		);
	}

	/**
	 * Validates the CAPTCHA response for the login form.
	 *
	 * @param  null|WP_Error $error  WP_Error object if validation fails, else null.
	 *
	 * @return null|WP_Error WP_Error object if validation fails else null.
	 */
	public function validate_login_captcha( $error ) {
		$post_data = defender_get_data_from_request( null, 'p' );
		if ( array() === $post_data ) {
			return $error;
		}
		// Skip turnstile verification for current sprint plan.
		if ( $this->service->should_skip_turnstile_check() ) {
			return $error;
		}
		// Check if the $_POST array is not empty and if 'g-recaptcha-response' key is also empty.
		$response_field   = 'turnstile' === $this->recaptcha_type ? 'wpdef-turnstile-response' : 'g-recaptcha-response';
		$captcha_response = stripslashes( defender_get_data_from_request( $response_field, 'p' ) );
		if ( '' === $captcha_response ) {
			$code    = 'captcha_error';
			$message = __( 'Please verify that you are not a robot.', 'defender-security' );

			if ( is_wp_error( $error ) ) {
				$error->add( $code, $message );
			} else {
				// Replace $user with a new WP_Error object with an error message.
				$error = new WP_Error( $code, $message );
			}
		}

		// Return the $error variable.
		return $error;
	}

	/**
	 * Modifies the script loader tag for the 'wpdef_captcha_api' handle.
	 *
	 * @param  string $tag  The original script loader tag.
	 * @param  string $handle  The handle being loaded.
	 *
	 * @return string The modified script loader tag.
	 */
	public function script_loader_tag( string $tag, string $handle ) {
		if ( 'wpdef_captcha_api' === $handle ) {
			$tag = str_replace( ' src', ' data-cfasync="false" async="async" defer="defer" src', $tag );
		}

		return $tag;
	}

	/**
	 * Returns the API URL for reCAPTCHA based on the recaptcha_type property.
	 *
	 * @return string The API URL for reCAPTCHA. Returns an empty string if the recaptcha_type is not set.
	 */
	protected function get_api_url(): string {
		if ( isset( $this->recaptcha_type ) ) {
			if ( 'v3_recaptcha' === $this->recaptcha_type ) {
				return sprintf( 'https://www.google.com/recaptcha/api.js?hl=%s&render=%s', $this->language, $this->public_key );
			} elseif ( 'turnstile' === $this->recaptcha_type ) {
				return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=wpdefTurnstileCallback';
			} elseif ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true ) ) {
				return sprintf( 'https://www.google.com/recaptcha/api.js?hl=%s&render=explicit', $this->language );
			}
		}

		return '';
	}

	/**
	 * Enqueues the necessary scripts for the reCAPTCHA frontend.
	 *
	 * @return void
	 */
	public function add_captcha_scripts(): void {
		if ( isset( $this->recaptcha_type ) ) {
			$this->service->remove_duplicate_captcha_scripts();
		}
		if ( 'turnstile' === $this->recaptcha_type ) {
			$handle  = 'wpdef_turnstile_script';
			$src_url = 'assets/js/turnstile.js';
		} else {
			$handle  = 'wpdef_recaptcha_script';
			$src_url = 'assets/js/recaptcha_frontend.js';
		}
		if ( ! wp_script_is( $handle, 'enqueued' ) ) {
			wp_enqueue_script(
				$handle,
				plugins_url( $src_url, WP_DEFENDER_FILE ),
				array(
					'jquery',
					'wpdef_captcha_api',
				),
				DEFENDER_VERSION,
				true
			);
		}

		// @since 2.5.6
		do_action( 'wd_recaptcha_extra_assets' );

		$error_text =
			sprintf(
				/* translators: 1: Captcha Type, 2: Captcha Type */
				esc_html__( 'More than one %1$s has been found in the current form. Please remove all unnecessary %2$s fields to make it work properly.', 'defender-security' ),
				'turnstile' === $this->recaptcha_type ? 'turnstile' : 'reCAPTCHA',
				'turnstile' === $this->recaptcha_type ? 'turnstile' : 'reCAPTCHA'
			);
		$options = array(
			'size'    => $this->recaptcha_size,
			'sitekey' => $this->public_key,
			'error'   => sprintf( '<strong>%s</strong>:&nbsp;%s', esc_html__( 'Warning', 'defender-security' ), $error_text ),
			// For default comment form.
			'disable' => '',
		);
		if ( 'turnstile' === $this->recaptcha_type ) {
			$options['theme'] = $this->recaptcha_theme;
			$options['lang']  = $this->language;
		} else {
			$options['version'] = $this->recaptcha_type;
			$options['hl']      = $this->language;
			if ( 'v2_checkbox' === $this->recaptcha_type ) {
				$options['theme'] = $this->recaptcha_theme;
			}
		}

		wp_localize_script(
			$handle,
			'WPDEF',
			array(
				'options' => $options,
				'vars'    => array(
					'visibility' => ( 'login_footer' === current_filter() ),
				),
			)
		);
	}

	/**
	 * Add scripts when comments are lazy loaded.
	 *
	 * @return void
	 * @since 2.6.1
	 */
	public function add_scripts_for_lazy_load(): void {
		if (
			in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true )
			&& ( is_single() || is_page() )
			&& comments_open()
		) {
			if ( ! wp_script_is( 'wpdef_captcha_api', 'registered' ) ) {
				$api_url = $this->get_api_url();
				$deps    = array( 'jquery' );
				wp_register_script( 'wpdef_captcha_api', $api_url, $deps, DEFENDER_VERSION, true );
			}

			$this->add_captcha_scripts();
		}
	}

	/**
	 * Display the CAPTCHA field.
	 *
	 * @return void
	 */
	public function display_login_captcha(): void {
		if ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'turnstile' ), true ) ) {
			$from_width = 302; ?>
			<style media="screen">
				.login-action-login #loginform,
				.login-action-lostpassword #lostpasswordform,
				.login-action-register #registerform {
					width: <?php echo esc_attr( $from_width ); ?>px !important;
				}

				#login_error,
				.message {
					width: <?php echo esc_attr( $from_width + 20 ); ?>px !important;
				}

				.login-action-login #loginform .captcha_wrap,
				.login-action-lostpassword #lostpasswordform .captcha_wrap,
				.login-action-register #registerform .captcha_wrap {
					margin-bottom: 10px;
				}

				#group-create-body .captcha_wrap {
					margin-top: 15px;
				}
			</style>
			<?php
		} elseif ( 'v2_invisible' === $this->recaptcha_type ) {
			?>
			<style>
				.login-action-lostpassword #lostpasswordform .captcha_wrap,
				.login-action-login #loginform .captcha_wrap,
				.login-action-register #registerform .captcha_wrap {
					margin-bottom: 10px;
				}

				#signup-content .captcha_wrap,
				#group-create-body .captcha_wrap {
					margin-top: 10px;
				}
			</style>
			<?php
		}
		echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
	}

	/**
	 * Display the output of the recaptcha.
	 *
	 * @return string
	 */
	protected function display_recaptcha(): string {
		$deps    = null;
		$content = '<div class="captcha_wrap wpdef_captcha_' . $this->recaptcha_type . '">';
		if ( ! $this->private_key || ! $this->public_key || '' === $this->recaptcha_type ) {
			// Display nothing.
			$content .= '</div>';

			return $content;
		}

		$api_url = $this->get_api_url();

		// Generate random id value if there's content with pagination plugin for not getting duplicate id values.
		$id = Crypt::random_int( 0, mt_getrandmax() );
		if ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true ) ) {
			$content .= '<div id="wpdef_recaptcha_' . $id . '" class="wpdef_recaptcha"></div>
			<noscript>
				<div style="width: 302px;">
					<div style="width: 302px; height: 422px; position: relative;">
						<div style="width: 302px; height: 422px; position: absolute;">
							<iframe src="https://www.google.com/recaptcha/api/fallback?k=' . esc_attr( $this->public_key ) . '" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
						</div>
					</div>
					<div style="border-style: none; bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px; background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px; height: 60px; width: 300px;">
						<textarea name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px !important; height: 40px !important; border: 1px solid #c1c1c1 !important; margin: 10px 25px !important; padding: 0px !important; resize: none !important;"></textarea>
					</div>
				</div>
			</noscript>';
			$deps     = array( 'jquery' );
		} elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
			$content .= '<input type="hidden" class="g-recaptcha-response" name="g-recaptcha-response" />';
		} elseif ( 'turnstile' === $this->recaptcha_type ) {
			$content .= sprintf( '<div id="wpdef_turnstile_%s"></div>', $id );
			$content .= '<noscript>';
			$content .= '<div style="padding: 10px; border: 1px solid #cccccc; background-color: #f9f9f9;margin-top: 10px;">';
			$content .= sprintf( '<p>%s</p>', esc_html__( 'Please enable JavaScript to complete the security verification.', 'defender-security' ) );
			$content .= '</div>';
			$content .= '</noscript>';
		}
		$content .= '</div>';

		// Register reCAPTCHA script.
		$locations = $this->model->locations;
		if ( ! wp_script_is( 'wpdef_captcha_api', 'registered' ) ) {

			if ( 'v3_recaptcha' === $this->recaptcha_type ) {
				wp_register_script( 'wpdef_captcha_api', $api_url, array(), DEFENDER_VERSION, false );
			} else {
				wp_register_script( 'wpdef_captcha_api', $api_url, $deps, $this->service->is_recaptcha_active_provider() ? DEFENDER_VERSION : null, true );
			}
			add_action( 'wp_footer', array( $this, 'add_captcha_scripts' ) );
			if (
				in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true )
				|| in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true )
				|| in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true )
			) {
				add_action( 'login_footer', array( $this, 'add_captcha_scripts' ) );
			}
		}

		return $content;
	}

	/**
	 * Check the current page from is from the Woo plugin.
	 *
	 * @return bool
	 */
	protected function is_woocommerce_page(): bool {
		if ( ! $this->is_woo_activated ) {
			return false;
		}

		$traces = debug_backtrace(); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
		foreach ( $traces as $trace ) {
			if ( isset( $trace['file'] ) && false !== strpos( $trace['file'], 'woocommerce' ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Verify the captcha code on the Login page.
	 *
	 * @param  WP_User|WP_Error $user  WP_User or WP_Error object if a previous callback failed authentication.
	 *
	 * @return WP_Error|WP_User
	 */
	public function validate_captcha_field_on_login( $user ) {
		// Only Recaptcha type has Integration fields in v5.7.0.
		if ( $this->service->is_recaptcha_active_provider() && $this->is_woocommerce_page() ) {
			return $user;
		}
		// Skip check if connecting to XMLRPC.
		if ( defined( 'XMLRPC_REQUEST' ) ) {
			return $user;
		}
		// Skip turnstile verification for current sprint plan.
		if ( $this->service->should_skip_turnstile_check() ) {
			return $user;
		}
		// Is Recaptcha-request from 'Ultimate Member' plugin?
		$um_request = defender_get_data_from_request( 'um_request', 'p' );
		if ( $this->service->is_recaptcha_active_provider() &&
				is_string( $um_request ) &&
				'' !== $um_request &&
				function_exists( 'um_recaptcha_validate' )
		) {
			return $user;
		}

		if ( ! $this->captcha_response( 'default_login' ) ) {
			if ( is_wp_error( $user ) ) {
				$user->add( 'invalid_captcha', $this->service->error_message() );

				return $user;
			}
			return new WP_Error( 'invalid_captcha', $this->service->error_message() );
		}

		return $user;
	}

	/**
	 * Verify the recaptcha code on the Registration page.
	 *
	 * @param  WP_Error $errors  A WP_Error object containing any errors encountered during registration.
	 *
	 * @return WP_Error
	 */
	public function validate_captcha_field_on_registration( WP_Error $errors ) {
		// Skip check if connecting to XMLRPC.
		if ( defined( 'XMLRPC_REQUEST' ) ) {
			return $errors;
		}

		if ( ! $this->captcha_response( 'default_registration' ) ) {
			$errors->add( 'invalid_captcha', $this->service->error_message() );
		}
		$_POST['g-recaptcha-response-check'] = true;

		return $errors;
	}

	/**
	 * Add captcha to the multisite signup form.
	 *
	 * @param  WP_Error $errors  A WP_Error object possibly containing 'blogname' or 'blog_title' errors.
	 *
	 * @return void
	 */
	public function display_signup_captcha( WP_Error $errors ): void {
		$error_message = $errors->get_error_message( 'invalid_captcha' );
		if ( is_string( $error_message ) && '' !== $error_message ) {
			printf( '<p class="error">%s</p>', wp_kses_post( $error_message ) );
		}
		echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
	}

	/**
	 * Verify the recaptcha code on the multisite signup page.
	 *
	 * @param  array $result  An array of errors.
	 *
	 * @return array
	 */
	public function validate_captcha_field_on_wpmu_registration( array $result ): array {
		global $current_user;
		if ( is_admin() && ! defined( 'DOING_AJAX' ) && isset( $current_user->data->ID ) && 0 < (int) $current_user->data->ID ) {
			return $result;
		}

		// Skip if BuddyPress is handling the registration.
		if (
			$this->is_buddypress_activated &&
			$this->service->is_recaptcha_active_provider() &&
			in_array( Buddypress::REGISTER_FORM, $this->model->buddypress_checked_locations, true )
		) {
			return $result;
		}

		if ( ! $this->captcha_response( 'wpmu_registration' ) ) {
			if ( isset( $result['errors'] ) && $result['errors'] instanceof \WP_Error ) {
				$errors = $result['errors'];
			} else {
				$errors = new WP_Error();
			}
			$errors->add( 'invalid_captcha', $this->service->error_message() );
			$result['errors'] = $errors;

			return $result;
		}

		return $result;
	}

	/**
	 * Verify the recaptcha code on Woo login page.
	 *
	 * @param  WP_Error $errors  A WP_Error object containing any errors encountered during login.
	 *
	 * @return WP_Error
	 */
	public function validate_captcha_field_on_woo_login( WP_Error $errors ) {
		// Skip check if connecting to XMLRPC.
		if ( defined( 'XMLRPC_REQUEST' ) ) {
			return $errors;
		}

		if ( ! $this->captcha_response( 'woo_login' ) ) {
			// Remove 'Error: ' because Woo has it by default.
			$message = str_replace(
				sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
				'',
				$this->service->error_message()
			);
			$errors->add( 'invalid_captcha', $message );
		}

		return $errors;
	}

	/**
	 * Check recaptcha on Woo registration form.
	 *
	 * @param  WP_Error $errors  A WP_Error object containing any errors encountered during registration.
	 *
	 * @return WP_Error
	 */
	public function validate_captcha_field_on_woo_registration( WP_Error $errors ) {
		if ( defined( 'WOOCOMMERCE_CHECKOUT' ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
			return $errors;
		}
		if ( ! $this->captcha_response( 'woo_registration' ) ) {
			// Remove 'Error: ' because Woo has it by default.
			$message = str_replace(
				sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
				'',
				$this->service->error_message()
			);
			$errors->add( 'invalid_captcha', $message );
		}

		return $errors;
	}

	/**
	 * Fires before errors are returned from a password reset request.
	 * Without 2nd `$user_data` parameter because it's since WP 5.4.0.
	 *
	 * @param  WP_Error $errors  A WP_Error object containing any errors encountered during password reset.
	 *
	 * @return void
	 */
	public function validate_captcha_field_on_lostpassword( WP_Error $errors ): void {
		if ( ! $this->captcha_response( 'default_lost_password' ) ) {
			$errors->add( 'invalid_captcha', $this->service->error_message() );
		}
	}

	/**
	 * Validates the reCAPTCHA field on the WooCommerce checkout form.
	 *
	 * @param  array    $fields  The fields of the checkout form.
	 * @param  WP_Error $errors  The errors encountered during the checkout process.
	 *
	 * @return WP_Error The updated errors with the reCAPTCHA validation result.
	 */
	public function validate_captcha_field_on_woo_checkout( $fields, $errors ): WP_Error {
		if ( ! $this->captcha_response( 'woo_checkout' ) ) {
			// Remove 'Error: ' because Woo has it by default.
			$message = str_replace(
				sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
				'',
				$this->service->error_message()
			);
			$errors->add( 'invalid_captcha', $message );
		}

		return $errors;
	}

	/**
	 * Display google recaptcha on comments form.
	 *
	 * @param  array $defaults  The default comment form arguments.
	 *
	 * @return array
	 */
	public function comment_form_defaults( array $defaults ) {
		$defaults['comment_notes_after'] .= '<p>' . $this->display_recaptcha() . '</p>';

		return $defaults;
	}

	/**
	 * Check JS enabled for comment form.
	 *
	 * @param  int $comment_post_id  Post ID.
	 *
	 * @return void
	 */
	public function validate_captcha_field_on_comment( $comment_post_id ) {
		if ( $this->service->exclude_captcha_for_requests() ) {
			return;
		}
		// Skip if it's from WooCommerce review form.
		if ( 'product' === get_post_type( $comment_post_id ) ) {
			return;
		}

		if ( ! $this->captcha_response( 'default_comments' ) ) {
			// @since v2.5.6
			wp_die(
				wp_kses_post(
					apply_filters(
						'wd_recaptcha_require_valid_comment',
						$this->service->error_message()
					)
				)
			);
		}
	}

	/**
	 * Validates the reCAPTCHA response for a given form.
	 *
	 * @param  string $form  The form being verified.
	 *
	 * @return bool The result of the reCAPTCHA verification.
	 * @since 2.5.6
	 */
	protected function captcha_response( string $form ): bool {
		$response_field = 'turnstile' === $this->recaptcha_type ? 'wpdef-turnstile-response' : 'g-recaptcha-response';
		$response       = stripslashes( defender_get_data_from_request( $response_field, 'p' ) );
		if ( ! is_string( $this->private_key ) || '' === $this->private_key || '' === $response ) {
			return false;
		}
		// reCAPTCHA response post data.
		$remote_ip = filter_var( defender_get_data_from_request( 'REMOTE_ADDR', 's' ), FILTER_VALIDATE_IP );
		/**
		 * Filters to Get the remote IP address.
		 *
		 * @param  mixed  $remote_ip  The remote IP address.
		 *
		 * @since 2.5.6
		 */
		$remote_ip = (string) apply_filters( 'wd_recaptcha_remote_ip', $remote_ip );

		$post_body = array(
			'secret'   => $this->private_key,
			'response' => $response,
			'remoteip' => $remote_ip,
		);

		$result = $this->service->captcha_post_request( $post_body );

		/**
		 * Filters to check the result of a reCAPTCHA verification.
		 *
		 * @param  mixed  $result  The result of the reCAPTCHA verification.
		 * @param  mixed  $form  The form being verified.
		 *
		 * @since 2.5.6
		 */
		return apply_filters( 'wd_recaptcha_check_result', $result, $form );
	}

	/**
	 * Display the BuddyPress reCAPTCHA.
	 *
	 * @return void
	 */
	public function display_buddypress_recaptcha(): void {
		$error = '';
		$obj   = buddypress();
		if ( isset( $obj->signup->errors['failed_recaptcha_verification'] ) && '' !== $obj->signup->errors['failed_recaptcha_verification'] ) {
			$error = $obj->signup->errors['failed_recaptcha_verification'];
		}

		if ( '' !== $error ) {
			$output  = '<div class="error">';
			$output .= $error;
			$output .= '</div>';

			echo wp_kses_post( $output );
		}

		echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
	}

	/**
	 * Validates the reCAPTCHA field on the BuddyPress registration form.
	 *
	 * @return void
	 */
	public function validate_captcha_field_on_buddypress_registration(): void {
		if ( ! $this->captcha_response( 'buddypress_registration' ) ) {
			buddypress()->signup->errors['failed_recaptcha_verification'] = $this->service->error_message();
		}
	}

	/**
	 * Verify BuddyPress group form captcha.
	 *
	 * @return bool|void
	 */
	public function validate_captcha_field_on_buddypress_group() {
		if ( ! bp_is_group_creation_step( 'group-details' ) ) {
			return false;
		}

		if ( ! $this->captcha_response( 'buddypress_create_group' ) ) {
			bp_core_add_message( $this->service->error_message(), 'error' );
			bp_core_redirect( bp_get_root_domain() . '/' . bp_get_groups_root_slug() . '/create/step/group-details/' );
		} else {
			return false;
		}
	}

	/**
	 * Provide data to the frontend via localized script.
	 *
	 * @param  array $data  Data collection is ready to passed.
	 *
	 * @return array Modified data array with added this controller data.
	 */
	public function script_data( array $data ): array {
		$data['captcha'] = $this->data_frontend();

		return $data;
	}

	/**
	 * Save settings.
	 *
	 * @param  Request $request  The request object containing new settings data.
	 *
	 * @return Response
	 * @defender_route
	 */
	public function save_settings( Request $request ): Response {
		$data = $request->get_data_by_model( $this->model );
		$this->model->import( $data );
		if ( ! $this->service->is_recaptcha_active_provider() ) {
			$valid_token = $request->get_data(
				array(
					'token' => array(
						'type' => 'string',
					),
				)
			);
			$token       = $valid_token['token'] ?? '';
			$is_valid    = $this->service->verify_turnstile_key( $token );
			if ( $is_valid ) {
				$this->model->active_type = 'turnstile';
			} else {
				return new Response(
					false,
					// Merge stored data to avoid errors.
						array_merge(
							array(
								'message'    => __( 'Please make sure your Cloudflare API keys are correct and that Preview Widget verification is successful.', 'defender-security' ),
								'error_keys' => array( 'data_turnstile.secret' ),
							),
							$this->data_frontend()
						)
				);
			}
		}
		if ( $this->model->validate() ) {
			$this->model->save();
			Config_Hub_Helper::set_clear_active_flag();

			return new Response(
				true,
				array_merge(
					array(
						'message'    => esc_html__( 'Settings saved successfully!', 'defender-security' ),
						'auto_close' => true,
					),
					$this->data_frontend()
				)
			);
		}

		return new Response(
			false,
			// Merge stored data to avoid errors.
			array_merge(
				array(
					'message'    => $this->model->get_formatted_errors(),
					'error_keys' => $this->model->get_error_keys(),
				),
				$this->data_frontend()
			)
		);
	}

	/**
	 * Load the CAPTCHA preview based on the provided request data.
	 *
	 * @param  Request $request  The request object containing the data needed to load the preview.
	 *
	 * @return Response The response object with the preview HTML and status.
	 * @defender_route
	 */
	public function load_captcha_preview( Request $request ): Response {
		$onload               = null;
		$js                   = null;
		$data                 = $request->get_data(
			array(
				'captcha_type' => array(
					'type' => 'string',
				),
			)
		);
		$this->recaptcha_type = $data['captcha_type'];

		$model = $this->model;

		if ( 'turnstile' === $this->recaptcha_type ) {
			return new Response(
				$model->is_active(),
				array(
					'preview' => $model->is_active(),
				)
			);
		}
		$language = '' !== $model->language && 'automatic' !== $model->language ? $model->language : get_locale();

		$notice  = '<div class="sui-notice sui-notice-default">';
		$notice .= '<div class="sui-notice-content">';
		$notice .= '<div class="sui-notice-message">';
		$notice .= '<i class="sui-notice-icon sui-icon-info sui-md" aria-hidden="true"></i>';
		$notice .= '<p>' . esc_html__( 'Save your API keys to load the reCAPTCHA preview.', 'defender-security' ) . '</p>';
		$notice .= '</div>';
		$notice .= '</div>';
		$notice .= '</div>';

		$theme        = 'light';
		$captcha_size = 'invisible';
		$data_args    = '';
		if ( 'v2_checkbox' === $this->recaptcha_type ) {
			$this->public_key = $model->data_v2_checkbox['key'];
			$theme            = $model->data_v2_checkbox['style'];
			$captcha_size     = $model->data_v2_checkbox['size'];
			$onload           = 'defender_render_admin_captcha_v2';
			// Onload method for Recaptcha works only for js 'var'.
			$js = "<script>var defender_render_admin_captcha_v2 = function () {
			setTimeout( function () {
			var captcha_v2 = jQuery( '.defender-g-recaptcha-v2_checkbox' ),
				sitekey_v2 = captcha_v2.data('sitekey'),
				theme_v2 = captcha_v2.data('theme'),
				size_v2 = captcha_v2.data('size');
				if (captcha_v2[0] && captcha_v2[0].hasChildNodes()) {
					return;
				}
			window.grecaptcha.render( captcha_v2[0], {
				sitekey: sitekey_v2,
				theme: theme_v2,
				size: size_v2,
				'error-callback': function() {
					jQuery('#v2_checkbox_notice_1').hide();
					jQuery('#v2_checkbox_notice_2').show();
				}
			} );
			}, 100 );
			};</script>";
		} elseif ( 'v2_invisible' === $this->recaptcha_type ) {
			$this->public_key = $model->data_v2_invisible['key'];
			$onload           = 'defender_render_admin_captcha_v2_invisible';
			$data_args        = 'data-badge="inline" data-callback="setResponse"';
			$js               = "<script>var defender_render_admin_captcha_v2_invisible = function () {
			setTimeout( function () {
				var captcha = jQuery( '.defender-g-recaptcha-v2_invisible' ),
					sitekey = captcha.data('sitekey'),
					theme = captcha.data('theme'),
					size = captcha.data('size');
					if (captcha[0] && captcha[0].hasChildNodes()) {
						return;
					}
				window.grecaptcha.render( captcha[0], {
					sitekey: sitekey,
					theme: theme,
					size: size,
					badge: 'inline',
					'error-callback': function() {
						jQuery('#v2_invisible_notice_1').hide();
						jQuery('#v2_invisible_notice_2').show();
					}
				} );
			}, 100 );
			};</script>";
		} elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
			$this->public_key = $model->data_v3_recaptcha['key'];
			$onload           = 'defender_render_admin_captcha_v3';
			$js               = "<script>var defender_render_admin_captcha_v3 = function () {
			setTimeout( function () {
				var captcha = jQuery( '.defender-g-recaptcha-v3_recaptcha' ),
					sitekey = captcha.data('sitekey'),
					theme = captcha.data('theme'),
					size = captcha.data('size');
					if (captcha[0] && captcha[0].hasChildNodes()) {
						return;
					}
				window.grecaptcha.render( captcha[0], {
					sitekey: sitekey,
					theme: theme,
					size: size,
					badge: 'inline',
					'error-callback': function() {
						jQuery('#v3_recaptcha_notice_1').hide();
						jQuery('#v3_recaptcha_notice_2').show();
					}
				} );
			}, 100 );
			};</script>";
		}

		$html = '';
		if ( isset( $this->recaptcha_type ) && '' !== $this->public_key ) {
			// Google reCAPTCHA script must be connected with dynamic parameters (onload, render, etc.).
			$html .= '<script src="https://www.google.com/recaptcha/api.js?hl=' . esc_attr( $language ) . '&render=explicit&onload=' . $onload . '" async defer></script>' . $js; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript

			$html .= sprintf(
				'<div class="%s" data-sitekey="%s" data-theme="%s" data-size="%s" %s></div>',
				'defender-g-recaptcha-' . $this->recaptcha_type,
				esc_attr( $this->public_key ),
				esc_attr( $theme ),
				esc_attr( $captcha_size ),
				$data_args
			);
		} else {
			$html .= $notice;
		}

		return new Response(
			true,
			array(
				'preview' => true,
				'html'    => $html,
			)
		);
	}

	/**
	 * Removes settings for all submodules.
	 */
	public function remove_settings() {
	}

	/**
	 * Delete all the data & the cache.
	 */
	public function remove_data(): void {
		$this->model->delete();
	}

	/**
	 * Converts the current object state to an array.
	 *
	 * @return array The array representation of the object.
	 */
	public function to_array(): array {
		return array();
	}

	/**
	 * Provides data for the frontend.
	 *
	 * @return array An array of data for the frontend.
	 */
	public function data_frontend(): array {
		$model     = $this->model;
		$is_active = $model->is_active();
		/**
		 * Different cases for entered keys and locations:
		 * success - one default, Woo or BuddyPress location is checked at least,
		 * warning - default, Woo and BuddyPress locations are unchecked,
		 * warning - default location is unchecked, also Woo and BuddyPress is deactivated,
		 * warning - non-entered keys.
		 */
		if ( $is_active ) {
			if ( $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) ) {
				$notice_type = 'success';
				$notice_text = false;
			} elseif ( ! $this->is_woo_activated && ! $this->is_buddypress_activated && ! $model->enable_default_location() ) {
				$notice_type = 'warning';
				$notice_text = sprintf(
					/* translators: 1. Captcha Provider name. 2. Captcha Type */
					esc_html__( '%1$s is currently inactive for all forms. You can deploy %2$s for specific forms in the reCAPTCHA Locations below.', 'defender-security' ),
					'turnstile' === $model->provider ? esc_html__( 'Cloudflare Turnstile', 'defender-security' ) : esc_html__( 'Google reCAPTCHA', 'defender-security' ),
					'turnstile' === $model->provider ? esc_html__( 'Turnstile', 'defender-security' ) : esc_html__( 'reCAPTCHA', 'defender-security' ),
				);
			} elseif (
				! $model->enable_default_location()
				&& (
					( $this->is_woo_activated && ! $model->enable_woo_location() )
					|| ( $this->is_buddypress_activated && ! $model->enable_buddypress_location() )
				)
			) {
				$notice_type = 'warning';
				$notice_text = sprintf(
				/* translators: 1. reCAPTCHA & Woo location. 2. BuddyPress location. */
					esc_html__( 'Google reCAPTCHA is currently inactive for all forms. You can deploy reCAPTCHA for specific forms in the %1$s or %2$s settings below.', 'defender-security' ),
					'<b>' . esc_html__( 'reCAPTCHA Locations', 'defender-security' ) . '</b>, <b>' . esc_html__( 'WooCommerce', 'defender-security' ) . '</b>',
					'<b>' . esc_html__( 'BuddyPress', 'defender-security' ) . '</b>'
				);
			}
		} else {
			// Inactive case.
			$notice_type = 'error';
			$notice_text = esc_html__(
				'Configure a CAPTCHA provider to verify users and prevent automated bots. Both reCAPTCHA and Cloudflare Turnstile are supported.',
				'defender-security'
			);
		}

		/**
		 * Cases:
		 * Invalid domain for Site Key,
		 * Google ReCAPTCHA is in localhost,
		 * Cannot contact reCAPTCHA. Check your connection.
		 */
		$ticket_text = esc_html__(
			'If you see any errors in the preview, make sure the keys you’ve entered are valid, and you\'ve listed your domain name while generating the keys.',
			'defender-security'
		);

		if ( ( new WPMUDEV() )->show_support_links() ) {
			$ticket_text .= defender_support_ticket_text();
		}

		return array_merge(
			array(
				'model'                     => $model->export(),
				'is_active'                 => $is_active,
				'default_message'           => $this->default_msg,
				'default_turnstile_message' => $this->model->get_default_values()['turnstile_message'],
				'default_locations'         => Captcha_Component::get_forms(),
				'notice_type'               => $notice_type,
				'notice_text'               => $notice_text,
				'ticket_text'               => $ticket_text,
				'is_woo_active'             => $this->is_woo_activated,
				'woo_locations'             => Woocommerce::get_forms(),
				'is_buddypress_active'      => $this->is_buddypress_activated,
				'buddypress_locations'      => Buddypress::get_forms(),
			),
			$this->dump_routes_and_nonces()
		);
	}

	/**
	 * Provides data for the dashboard widget.
	 *
	 * @return array An array of dashboard widget data.
	 */
	public function dashboard_widget(): array {
		$model       = $this->model;
		$notice_type = ( $model->is_active()
						&& $this->service->enable_any_location(
							$this->is_woo_activated,
							$this->is_buddypress_activated
						)
		)
			? 'success'
			: 'warning';

		return array(
			'model'       => $model->export(),
			'notice_type' => $notice_type,
		);
	}

	/**
	 * Imports data into the model.
	 *
	 * @param  array $data  Data to be imported into the model.
	 */
	public function import_data( array $data ) {
		$model = $this->model;

		$model->import( $data );
		if ( $model->validate() ) {
			$model->save();
		}
	}

	/**
	 * Exports strings.
	 *
	 * @return array An array of strings.
	 */
	public function export_strings(): array {
		return array( $this->model->is_active() ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ) );
	}

	/**
	 * Maybe validate reCaptcha for lost password.
	 *
	 * @return bool
	 * @since 3.2.0
	 */
	protected function maybe_validate_captcha_for_lostpassword(): bool {
		$post_data = defender_get_data_from_request( null, 'p' );
		$action    = $post_data['action'] ?? '';

		return ! $this->is_woocommerce_page() &&
				! isset( $post_data['wc_reset_password'], $post_data['user_login'] ) &&
				! ( is_admin() && 'send-password-reset' === $action ) &&
				'pp_ajax_passwordreset' !== $action;
	}

	/**
	 * Enable/disable module.
	 *
	 * @param  Request $request  The request object.
	 *
	 * @return Response
	 * @defender_route
	 * @since 3.12.0
	 */
	public function toggle_module( Request $request ): Response {
		$data                 = $request->get_data(
			array(
				'enabled' => array(
					'type' => 'boolean',
				),
			)
		);
		$prev_state           = $this->model->enabled;
		$this->model->enabled = $data['enabled'];
		$this->model->save();
		$message = esc_html__( 'Settings saved successfully!', 'defender-security' );
		if ( $prev_state !== $data['enabled'] ) {
			if ( $data['enabled'] ) {
				$message = esc_html__( 'CAPTCHA module is enabled successfully!', 'defender-security' );
			} else {
				$message = esc_html__( 'CAPTCHA module is disabled successfully!', 'defender-security' );
			}
		}

		Config_Hub_Helper::set_clear_active_flag();

		return new Response(
			true,
			array_merge(
				array(
					'message'    => $message,
					'auto_close' => true,
				),
				$this->data_frontend()
			)
		);
	}

	/**
	 * Get allowed HTML tags and attributes for reCaptcha output sanitization.
	 *
	 * @return array
	 */
	private function get_allowed_html(): array {
		return array(
			'div'      => array(
				'class' => array(),
				'id'    => array(),
				'style' => array(),
			),
			'iframe'   => array(
				'src'         => array(),
				'frameborder' => array(),
				'scrolling'   => array(),
				'style'       => array(),
			),
			'noscript' => array(),
			'textarea' => array(
				'name'  => array( 'g-recaptcha-response' ),
				'class' => array( 'g-recaptcha-response' ),
				'style' => array(),
			),
			'input'    => array(
				'type'  => array( 'hidden' ),
				'class' => array( 'g-recaptcha-response' ),
				'name'  => array( 'g-recaptcha-response' ),
			),
		);
	}
}
