Newer
Older
tonitalia-pluginsoci / includes / class-captcha.php
<?php
/**
 * Built-in CAPTCHA (honeypot + math + rate limit).
 *
 * @package TonItaliaRegistration
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class TON_Reg_Captcha
 */
class TON_Reg_Captcha {

	const NONCE_ACTION = 'ton_reg_form';
	const FIELD_MATH    = 'ton_reg_captcha_answer';
	const FIELD_TOKEN   = 'ton_reg_form_token';
	const FIELD_HONEYPOT = 'ton_reg_website';

	/**
	 * @return bool
	 */
	public static function is_enabled() {
		return '1' === get_option( 'ton_reg_captcha_enabled', '1' );
	}

	/**
	 * @return string
	 */
	public static function create_token() {
		$token = wp_generate_password( 32, false, false );
		set_transient( self::token_key( $token ), time(), 30 * MINUTE_IN_SECONDS );
		return $token;
	}

	/**
	 * @param string $token Token.
	 * @return string
	 */
	private static function token_key( $token ) {
		return 'ton_reg_tok_' . md5( $token );
	}

	/**
	 * @param string $token Form token.
	 * @return array{a:int,b:int,question:string}
	 */
	public static function generate_challenge( $token ) {
		$a  = wp_rand( 2, 12 );
		$b  = wp_rand( 2, 12 );
		$answer = $a + $b;

		set_transient(
			'ton_reg_cap_' . md5( $token ),
			$answer,
			30 * MINUTE_IN_SECONDS
		);

		return array(
			'a'        => $a,
			'b'        => $b,
			'question' => sprintf(
				/* translators: 1: number, 2: number */
				__( 'Quanto fa %1$d + %2$d?', 'ton-italia-registration' ),
				$a,
				$b
			),
		);
	}

	/**
	 * @return bool
	 */
	public static function check_rate_limit() {
		$ip     = self::get_client_ip();
		$key    = 'ton_reg_rl_' . md5( $ip );
		$max    = max( 1, (int) get_option( 'ton_reg_rate_limit_max', 5 ) );
		$window = max( 60, (int) get_option( 'ton_reg_rate_limit_window', 900 ) );
		$count  = (int) get_transient( $key );

		if ( $count >= $max ) {
			return false;
		}

		set_transient( $key, $count + 1, $window );
		return true;
	}

	/**
	 * @param array<string,mixed> $post POST data.
	 * @return true|WP_Error
	 */
	public static function validate( $post ) {
		if ( ! self::is_enabled() ) {
			return true;
		}

		if ( ! empty( $post[ self::FIELD_HONEYPOT ] ) ) {
			return new WP_Error( 'ton_reg_spam', __( 'Invio non valido.', 'ton-italia-registration' ) );
		}

		$token = isset( $post[ self::FIELD_TOKEN ] ) ? sanitize_text_field( wp_unslash( $post[ self::FIELD_TOKEN ] ) ) : '';
		if ( ! $token || ! get_transient( self::token_key( $token ) ) ) {
			return new WP_Error( 'ton_reg_token', __( 'Sessione scaduta. Ricarica la pagina e riprova.', 'ton-italia-registration' ) );
		}

		$issued = (int) get_transient( self::token_key( $token ) );
		if ( time() - $issued < 3 ) {
			return new WP_Error( 'ton_reg_fast', __( 'Attendi qualche secondo prima di inviare.', 'ton-italia-registration' ) );
		}

		$expected = get_transient( 'ton_reg_cap_' . md5( $token ) );
		$answer   = isset( $post[ self::FIELD_MATH ] ) ? (int) $post[ self::FIELD_MATH ] : -1;

		if ( false === $expected || (int) $expected !== $answer ) {
			return new WP_Error( 'ton_reg_captcha', __( 'Risposta di verifica non corretta.', 'ton-italia-registration' ) );
		}

		delete_transient( 'ton_reg_cap_' . md5( $token ) );
		delete_transient( self::token_key( $token ) );

		return true;
	}

	/**
	 * @return string
	 */
	public static function get_client_ip() {
		$ip = '';
		if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
			$parts = explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) );
			$ip    = trim( $parts[0] );
		} elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
			$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
		}
		return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '0.0.0.0';
	}
}