> */ public static function providers() { return array( 'turnstile' => array( 'label' => 'Cloudflare Turnstile', 'verify' => 'https://challenges.cloudflare.com/turnstile/v0/siteverify', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Server-side token verification, documented under External services. 'script' => 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Official captcha widget script, documented under External services. 'api' => 'turnstile', ), 'hcaptcha' => array( 'label' => 'hCaptcha', 'verify' => 'https://api.hcaptcha.com/siteverify', 'script' => 'https://js.hcaptcha.com/1/api.js?render=explicit', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Official captcha widget script, documented under External services. 'api' => 'hcaptcha', ), 'recaptcha' => array( 'label' => 'Google reCAPTCHA v2', 'verify' => 'https://www.google.com/recaptcha/api/siteverify', 'script' => 'https://www.google.com/recaptcha/api.js?render=explicit', 'api' => 'grecaptcha', ), ); } /** * Settings fields for the admin screen. * * @return array> */ public static function fields() { $choices = array(); foreach ( self::providers() as $id => $provider ) { $choices[ $id ] = $provider['label']; } return array( 'provider' => array( 'label' => __( 'Provider', 'aslansheed-login' ), 'type' => 'select', 'default' => 'turnstile', 'choices' => $choices, ), 'site_key' => array( 'label' => __( 'Site key', 'aslansheed-login' ), 'type' => 'text', ), 'secret_key' => array( 'label' => __( 'Secret key', 'aslansheed-login' ), 'type' => 'password', 'help' => __( 'Stored encrypted. Leave empty to keep the saved key.', 'aslansheed-login' ), ), ); } /** * Returns the saved settings, decrypted. * * @return array */ public static function settings() { return Component_Settings::get( self::KEY, self::fields() ); } /** * Tells whether the captcha is switched on and fully configured. * * @return bool */ public static function is_enabled() { $settings = self::settings(); return ! empty( $settings['_enabled'] ) && isset( self::providers()[ (string) $settings['provider'] ] ) && '' !== trim( (string) $settings['site_key'] ) && '' !== trim( (string) $settings['secret_key'] ); } /** * Returns what the browser needs to draw the widget, or an empty array. * * @return array{api?: string, site_key?: string, script?: string} */ public static function client_config() { if ( ! self::is_enabled() ) { return array(); } $settings = self::settings(); $provider = self::providers()[ (string) $settings['provider'] ]; return array( 'api' => $provider['api'], 'site_key' => trim( (string) $settings['site_key'] ), 'script' => $provider['script'], ); } /** * Verifies a widget token and returns a pass for one identifier. * * @param string $token Token from the widget. * @param string $identifier Normalized identifier. * @return string|WP_Error */ public static function issue_pass( $token, $identifier ) { $failed = new WP_Error( 'aslansheed_captcha', __( 'Please complete the security check and try again.', 'aslansheed-login' ) ); $token = (string) $token; if ( '' === $token || strlen( $token ) > 4096 ) { return $failed; } $settings = self::settings(); $provider = self::providers()[ (string) $settings['provider'] ]; $response = Http::request( 'POST', $provider['verify'], array( 'body' => array( 'secret' => (string) $settings['secret_key'], 'response' => $token, 'remoteip' => Request::ip(), ), ) ); if ( ! $response['ok'] || ! is_array( $response['json'] ) || empty( $response['json']['success'] ) ) { Audit_Log::log( 'captcha_failed' ); return $failed; } $pass = wp_generate_password( 32, false ); set_transient( self::PREFIX . hash( 'sha256', $pass ), array( 'identifier' => hash( 'sha256', (string) $identifier ), 'uses' => self::PASS_USES, ), self::PASS_TTL ); return $pass; } /** * Checks a pass for an identifier and uses one of its allowed uses. * * Passes when no captcha is configured. * * @param string $pass Pass from issue_pass(). * @param string $identifier Normalized identifier. * @return true|WP_Error */ public static function check_pass( $pass, $identifier ) { if ( ! self::is_enabled() ) { return true; } $failed = new WP_Error( 'aslansheed_captcha', __( 'Please complete the security check and try again.', 'aslansheed-login' ) ); $pass = (string) $pass; if ( '' === $pass ) { return $failed; } $key = self::PREFIX . hash( 'sha256', $pass ); $stored = get_transient( $key ); if ( ! is_array( $stored ) || ! hash_equals( (string) $stored['identifier'], hash( 'sha256', (string) $identifier ) ) || (int) $stored['uses'] < 1 ) { return $failed; } --$stored['uses']; if ( $stored['uses'] < 1 ) { delete_transient( $key ); } else { set_transient( $key, $stored, self::PASS_TTL ); } return true; } }