HEX
Server: Apache/2.4.58 (Ubuntu)
System: Linux bsx-1-dev 6.8.0-101-generic #101-Ubuntu SMP PREEMPT_DYNAMIC Mon Feb 9 10:15:05 UTC 2026 x86_64
User: www-data (33)
PHP: 8.3.6
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/memberpress/app/controllers/MeprMathCaptchaCtrl.php
<?php

declare(strict_types=1);

defined('ABSPATH') || exit;

class MeprMathCaptchaCtrl extends MeprBaseCtrl
{
    /**
     * Loads the hooks.
     */
    public function load_hooks(): void
    {
        add_action('after_setup_theme', [$this, 'enable']);
    }

    /**
     * Enables the math captcha functionality.
     *
     * This method is called after the theme setup, so that we can safely fetch the options.
     */
    public function enable(): void
    {
        $options = MeprOptions::fetch();

        if (is_plugin_active('memberpress-math-captcha/main.php')) {
            $options->enable_math_captcha = true;
            $options->store(false);

            deactivate_plugins('memberpress-math-captcha/main.php', true);
        }

        if ($options->enable_math_captcha) {
            add_action('mepr-checkout-before-coupon-field', [$this, 'render_field'], 12); // Higher priority to ensure it shows up below the strength meter.
            add_action('mepr-forgot-password-form', [$this, 'render_field']);
            add_action('mepr-login-form-before-submit', [$this, 'render_field']);
            add_filter('mepr-validate-signup', [$this, 'validate_answer']);
            add_filter('mepr-validate-forgot-password', [$this, 'validate_answer']);
            add_filter('mepr-validate-login', [$this, 'validate_answer']);
        }
    }

    /**
     * Generates, stores and returns a unique key.
     *
     * This key is used to create HMACs for the math captcha data.
     */
    public function get_unique_key(): string
    {
        $key = get_option('mepr_math_captcha_key', false);

        if (!$key || 64 !== strlen($key)) {
            // Generate a cryptographically secure random key.
            $key = bin2hex(random_bytes(32));
            update_option('mepr_math_captcha_key', $key);
        }

        return $key;
    }

    /**
     * Creates an HMAC-SHA256 hash of the answer for secure verification.
     *
     * @param integer $answer The answer to secure, which is the sum of two random numbers.
     *
     * @return string Base64 encoded HMAC-SHA256 of the answer with the unique key.
     */
    public function hash_data(int $answer): string
    {
        $key = $this->get_unique_key();

        // Use HMAC for secure message authentication.
        $hmac = hash_hmac('sha256', (string) $answer, $key, true);

        return base64_encode($hmac);
    }

    /**
     * Verifies if the provided answer matches the stored HMAC hash.
     *
     * @param integer $answer The user's answer to the math challenge.
     * @param string  $data   The stored data (Base64 encoded HMAC).
     *
     * @return boolean True if the answer matches the stored data, false otherwise.
     */
    public function verify_data(int $answer, string $data): bool
    {
        $expected = $this->hash_data($answer);

        // Use hash_equals to prevent timing attacks.
        return hash_equals($expected, $data);
    }

    /**
     * Generates a random number based on the specified size.
     *
     * @param string $size The size of the random number to generate ('small', 'medium', 'large'). Default is 'small'.
     *
     * @return integer The generated random number.
     */
    public function generate_random_number(string $size = 'small'): int
    {
        if ('large' === $size) {
            return random_int(16, 30);
        }

        if ('medium' === $size) {
            return random_int(6, 15);
        }

        return random_int(1, 5);
    }

    /**
     * Displays the math captcha field in the forms.
     */
    public function render_field(): void
    {
        if (MeprUtils::is_user_logged_in()) {
            return;
        }

        $num1 = $this->generate_random_number('medium');
        $num2 = $this->generate_random_number('small');
        $data = $this->hash_data($num1 + $num2);

        MeprView::render('shared/math_captcha', compact('num1', 'num2', 'data'));
    }

    /**
     * Validates the math captcha answer.
     *
     * @param array $errors The array of errors.
     *
     * @return array The updated array of errors, if any.
     */
    public function validate_answer(array $errors): array
    {
        if (MeprUtils::is_user_logged_in()) {
            return $errors;
        }

        if (empty($_POST['mepr_math_quiz'])) {
            $errors[] = __('You must fill out the Math Quiz correctly.', 'memberpress');
            return $errors;
        }

        if (empty($_POST['mepr_math_data'])) {
            $errors[] = __('You must fill out the Math Quiz correctly.', 'memberpress');
            return $errors;
        }

        $answer = (int) $_POST['mepr_math_quiz'];
        $data   = sanitize_text_field(wp_unslash($_POST['mepr_math_data']));

        if (!$this->verify_data($answer, $data)) {
            $errors[] = __('You must fill out the Math Quiz correctly.', 'memberpress');
        }

        return $errors;
    }
}