Initial commit.

This commit is contained in:
2025-11-24 21:33:55 +00:00
parent 14b7ade051
commit d6e9d316bc
8974 changed files with 1423277 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
<?php
namespace {
return array('dependencies' => array(), 'version' => '8944fa12c33f0c6e9c9f');
}

View File

@@ -0,0 +1 @@
!function(e){"use strict";const t=window.ppcpRecaptchaSettings||{};let o=null,n=null,c=null,r=!1;function a(){return new Promise(function(e){"undefined"!=typeof grecaptcha&&grecaptcha.execute?(console.log("PPCP reCAPTCHA: reCAPTCHA v3 loaded and ready"),e()):(console.log("PPCP reCAPTCHA: Waiting for reCAPTCHA v3 to load..."),setTimeout(function(){a().then(e)},100))})}function i(o,n){if(t.isBlocks)return;const c=t.isSingleProduct?"form.cart":t.isCheckout?"form.checkout":null;if(!c)return;const r=e(c);0!==r.length?(r.find('input[name="ppcp_recaptcha_token"]').remove(),r.find('input[name="ppcp_recaptcha_version"]').remove(),r.append('<input type="hidden" name="ppcp_recaptcha_token" value="'+o+'">'),r.append('<input type="hidden" name="ppcp_recaptcha_version" value="'+n+'">')):console.error("PPCP reCAPTCHA: Form not found for token update")}function s(e){console.log("PPCP reCAPTCHA: Checkout error detected");const o=e&&("string"==typeof e?e.toLowerCase().includes("captcha"):e.some(e=>e.content&&e.content.toLowerCase().includes("captcha")));r?null!==c&&"undefined"!=typeof grecaptcha&&grecaptcha.reset&&(console.log("PPCP reCAPTCHA: Resetting v2 widget"),grecaptcha.reset(c),n=null):o?(console.log("PPCP reCAPTCHA: v3 failed on checkout, rendering v2"),d()):t.siteKeyV3&&(console.log("PPCP reCAPTCHA: Regenerating v3 token"),l())}async function l(){if("undefined"!=typeof grecaptcha&&grecaptcha.execute)try{o=await grecaptcha.execute(t.siteKeyV3,{action:"ppcp"}),console.log("PPCP reCAPTCHA: New v3 token generated"),i(o,"v3"),t.isBlocks&&window.wp&&window.wp.data&&window.wp.data.dispatch("wc/store/checkout").__internalSetExtensionData("ppcp_recaptcha",{token:o,version:"v3"})}catch(e){console.error("PPCP reCAPTCHA: Failed to generate v3 token",e),o=null}else console.error("PPCP reCAPTCHA: grecaptcha v3 not loaded")}function d(){if(r||!t.siteKeyV2)return;const e=document.getElementById(t.v2ContainerId);if(!e)return void console.error("PPCP reCAPTCHA: v2 container not found");if("undefined"==typeof grecaptcha||!grecaptcha.render)return void console.error("PPCP reCAPTCHA: grecaptcha v2 not loaded");e.innerHTML="";const o=document.createElement("div");o.className="g-recaptcha",o.setAttribute("data-sitekey",t.siteKeyV2),o.setAttribute("data-theme",t.theme),e.appendChild(o),c=grecaptcha.render(o,{sitekey:t.siteKeyV2,theme:t.theme,callback(e){n=e,console.log("PPCP reCAPTCHA: v2 verified"),i(e,"v2"),t.isBlocks&&window.wp&&window.wp.data&&window.wp.data.dispatch("wc/store/checkout").__internalSetExtensionData("ppcp_recaptcha",{token:e,version:"v2"})},"expired-callback"(){n=null,i("","v2"),t.isBlocks&&window.wp&&window.wp.data&&window.wp.data.dispatch("wc/store/checkout").__internalSetExtensionData("ppcp_recaptcha",{token:"",version:"v2"})}}),r=!0}const p=window.fetch;if(window.fetch=async function(e,a={}){const i="string"==typeof e?e:e.url;if(!i||!i.includes("ppc-create-order"))return p.call(this,e,a);let s,l;if(console.log("PPCP reCAPTCHA: Intercepting AJAX",i),r&&n?(s=n,l="v2"):(s=o,l="v3"),!s)return console.error("PPCP reCAPTCHA: No token available"),Promise.reject(new Error("Missing reCAPTCHA token"));try{const e=JSON.parse(a.body);e.ppcp_recaptcha_token=s,e.ppcp_recaptcha_version=l,a.body=JSON.stringify(e),console.log("PPCP reCAPTCHA: Token injected",l)}catch(e){console.error("PPCP reCAPTCHA: Failed to inject token",e)}return p.call(this,e,a).then(function(e){return 403!==e.status&&400!==e.status||e.clone().json().then(function(e){e.data.code===t.errorCodeVerificationFailed&&(r?(console.log("PPCP reCAPTCHA: v2 verification failed, resetting v2 widget"),grecaptcha.reset(c),n=null):(console.log("PPCP reCAPTCHA: v3 failed, rendering v2"),d()))}),e})},e(document).ready(function(){t.siteKeyV3&&a().then(function(){console.log("PPCP reCAPTCHA: Pre-generating v3 token"),l(),setInterval(l,9e4)})}),!t.isBlocks&&t.isCheckout&&e(document.body).on("checkout_error",function(){s(e(".woocommerce-error, .woocommerce-NoticeGroup-checkout").text())}),t.isBlocks&&window.wp&&window.wp.data){const{subscribe:t,select:o}=window.wp.data;let n=!1;t(()=>{const t=o("wc/store/checkout");if(!t)return;const c=t.hasError();c&&!n&&(console.log("PPCP reCAPTCHA: Block checkout error detected"),setTimeout(function(){const t=e(".wc-block-components-notice-banner__content");if(t.length>0){const e=t.text();console.log("PPCP reCAPTCHA: Error message extracted:",e),s(e)}else console.error("PPCP reCAPTCHA: Error banner not found in DOM")},100)),n=c})}}(jQuery);

View File

@@ -0,0 +1,8 @@
<?php
declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\FraudProtection;
return static function (): \WooCommerce\PayPalCommerce\FraudProtection\FraudProtectionModule {
return new \WooCommerce\PayPalCommerce\FraudProtection\FraudProtectionModule();
};

View File

@@ -0,0 +1,20 @@
<?php
declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\FraudProtection;
use WooCommerce\PayPalCommerce\FraudProtection\Recaptcha\Recaptcha;
use WooCommerce\PayPalCommerce\FraudProtection\Recaptcha\RecaptchaIntegration;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
return array('fraud-protection.url' => static function (ContainerInterface $container): string {
return plugins_url('/modules/ppcp-fraud-protection/', $container->get('ppcp.path-to-plugin-main-file'));
}, 'fraud-protection.recaptcha' => static function (ContainerInterface $container): Recaptcha {
return new Recaptcha($container->get('fraud-protection.recaptcha.integration'), $container->get('fraud-protection.recaptcha.payment-methods'), $container->get('fraud-protection.url'), $container->get('ppcp.asset-version'), $container->get('woocommerce.logger.woocommerce'));
}, 'fraud-protection.recaptcha.integration' => static function (): RecaptchaIntegration {
return new RecaptchaIntegration();
}, 'fraud-protection.recaptcha.payment-methods' => static function (): array {
return apply_filters('woocommerce_paypal_payments_recaptcha_payment_methods', array(PayPalGateway::ID, CreditCardGateway::ID, CardButtonGateway::ID));
});

View File

@@ -0,0 +1,132 @@
<?php
declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\FraudProtection;
use WC_Order;
use WooCommerce\PayPalCommerce\FraudProtection\Recaptcha\Recaptcha;
use WooCommerce\PayPalCommerce\FraudProtection\Recaptcha\RecaptchaIntegration;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WP_Error;
class FraudProtectionModule implements ServiceModule, ExecutableModule
{
use ModuleClassNameIdTrait;
public function services(): array
{
return require __DIR__ . '/../services.php';
}
public function run(ContainerInterface $container): bool
{
$this->init_recaptcha($container);
return \true;
}
protected function init_recaptcha(ContainerInterface $container): void
{
add_filter(
'woocommerce_integrations',
/**
* @param array $integrations
* @returns array
* @psalm-suppress MissingClosureParamType
* @psalm-suppress MissingClosureReturnType
*/
static function ($integrations) use ($container) {
// WC always creates a new instance here.
$integrations[] = RecaptchaIntegration::class;
return $integrations;
}
);
add_action('wp_enqueue_scripts', static function () use ($container): void {
$recaptcha = $container->get('fraud-protection.recaptcha');
assert($recaptcha instanceof Recaptcha);
$recaptcha->enqueue_scripts();
});
foreach (array('woocommerce_review_order_before_submit' => 10, 'woocommerce_pay_order_before_submit' => 10, 'woocommerce_after_cart_totals' => 10, 'woocommerce_single_product_summary' => 32) as $hook => $priority) {
add_action($hook, static function () use ($container): void {
$recaptcha = $container->get('fraud-protection.recaptcha');
assert($recaptcha instanceof Recaptcha);
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $recaptcha->render_v2_container();
}, $priority);
}
foreach (array('render_block_woocommerce/checkout-express-payment-block', 'render_block_woocommerce/proceed-to-checkout-block') as $filter) {
add_filter(
$filter,
/**
* @param string $block_html
* @returns string
* @psalm-suppress MissingClosureParamType
* @psalm-suppress MissingClosureReturnType
*/
static function (string $block_html) use ($container) {
$recaptcha = $container->get('fraud-protection.recaptcha');
assert($recaptcha instanceof Recaptcha);
return $block_html . $recaptcha->render_v2_container();
}
);
}
add_action('woocommerce_paypal_payments_create_order_request_started', static function (array $data) use ($container): void {
$recaptcha = $container->get('fraud-protection.recaptcha');
assert($recaptcha instanceof Recaptcha);
$recaptcha->intercept_paypal_ajax($data);
});
foreach (array('woocommerce_checkout_process', 'woocommerce_before_pay_action') as $hook) {
add_action($hook, static function () use ($container): void {
$recaptcha = $container->get('fraud-protection.recaptcha');
assert($recaptcha instanceof Recaptcha);
$recaptcha->validate_classic_checkout();
});
}
add_action('woocommerce_blocks_loaded', function (): void {
$this->register_recaptcha_blocks_extension();
});
add_filter(
'rest_authentication_errors',
/**
* @param WP_Error|null|true $errors
* @return WP_Error|null|true WP_Error
* @psalm-suppress MissingClosureParamType
* @psalm-suppress MissingClosureReturnType
*/
static function ($errors) use ($container) {
$recaptcha = $container->get('fraud-protection.recaptcha');
assert($recaptcha instanceof Recaptcha);
return $recaptcha->validate_blocks_request($errors);
},
99
);
add_action(
'woocommerce_new_order',
/**
* @param int $order_id
* @param WC_Order $order
* @psalm-suppress MissingClosureParamType
* @psalm-suppress MissingClosureReturnType
*/
static function ($order_id, $order) use ($container): void {
$recaptcha = $container->get('fraud-protection.recaptcha');
assert($recaptcha instanceof Recaptcha);
$recaptcha->add_result_meta($order);
},
10,
2
);
add_action('add_meta_boxes', static function () use ($container): void {
$recaptcha = $container->get('fraud-protection.recaptcha');
assert($recaptcha instanceof Recaptcha);
$recaptcha->add_metabox();
});
}
private function register_recaptcha_blocks_extension(): void
{
if (!function_exists('woocommerce_store_api_register_endpoint_data')) {
return;
}
woocommerce_store_api_register_endpoint_data(array('endpoint' => 'checkout', 'namespace' => 'ppcp_recaptcha', 'schema_callback' => static function (): array {
return array('token' => array('description' => 'reCAPTCHA token', 'type' => 'string', 'readonly' => \false), 'version' => array('description' => 'reCAPTCHA version', 'type' => 'string', 'readonly' => \false));
}));
}
}

View File

@@ -0,0 +1,285 @@
<?php
declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\FraudProtection\Recaptcha;
use Automattic\WooCommerce\Utilities\OrderUtil;
use WooCommerce\PayPalCommerce\Vendor\Psr\Log\LoggerInterface;
use WC_Order;
use WP_Error;
class Recaptcha
{
private const V2_CONTAINER_ID = 'ppcp-recaptcha-v2-container';
private const ERROR_CODE_MISSING_TOKEN = 'ppcp_recaptcha_missing_token';
private const ERROR_CODE_VERIFICATION_FAILED = 'ppcp_recaptcha_verification_failed';
private const CAPTCHA_USAGE_LIMIT = 5;
private const CAPTCHA_RESULT_TRANSIENT_KEY = 'ppcp_recaptcha_result_';
private const CAPTCHA_RESULT_META_KEY = 'ppcp_recaptcha_captcha_result';
private \WooCommerce\PayPalCommerce\FraudProtection\Recaptcha\RecaptchaIntegration $integration;
/**
* The methods that require captcha.
*
* @var string[]
*/
private array $payment_methods;
private string $module_url;
private string $asset_version;
private LoggerInterface $logger;
/**
* @param RecaptchaIntegration $integration
* @param string[] $payment_methods The methods that require captcha.
* @param string $module_url
* @param string $asset_version
* @param LoggerInterface $logger
*/
public function __construct(\WooCommerce\PayPalCommerce\FraudProtection\Recaptcha\RecaptchaIntegration $integration, array $payment_methods, string $module_url, string $asset_version, LoggerInterface $logger)
{
$this->integration = $integration;
$this->payment_methods = $payment_methods;
$this->module_url = $module_url;
$this->asset_version = $asset_version;
$this->logger = $logger;
}
protected function should_use_recaptcha(): bool
{
if (!wc_string_to_bool($this->integration->enabled)) {
return \false;
}
if (wc_string_to_bool($this->integration->get_option('guest_only')) && is_user_logged_in()) {
return \false;
}
$has_v3 = !empty($this->integration->get_option('site_key_v3')) && !empty($this->integration->get_option('secret_key_v3'));
$has_v2 = !empty($this->integration->get_option('site_key_v2')) && !empty($this->integration->get_option('secret_key_v2'));
if (!$has_v3 || !$has_v2) {
return \false;
}
return \true;
}
public function enqueue_scripts(): void
{
if (!is_checkout() && !is_cart() && !is_product()) {
return;
}
if (!$this->should_use_recaptcha()) {
return;
}
$is_blocks = has_block('woocommerce/checkout') || has_block('woocommerce/cart');
wp_enqueue_script('ppcp-recaptcha', 'https://www.google.com/recaptcha/api.js?render=' . esc_attr($this->integration->get_option('site_key_v3')), array(), $this->asset_version, \true);
$dependencies = array('ppcp-recaptcha');
if ($is_blocks) {
$dependencies[] = 'wp-data';
}
wp_enqueue_script('ppcp-recaptcha-handler', untrailingslashit($this->module_url) . '/assets/recaptcha-handler.js', $dependencies, $this->asset_version, \true);
wp_localize_script('ppcp-recaptcha-handler', 'ppcpRecaptchaSettings', array('siteKeyV3' => $this->integration->get_option('site_key_v3'), 'siteKeyV2' => $this->integration->get_option('site_key_v2'), 'theme' => $this->integration->get_option('v2_theme', 'light'), 'isBlocks' => $is_blocks, 'isCheckout' => is_checkout(), 'isCart' => is_cart(), 'isSingleProduct' => is_product(), 'v2ContainerId' => self::V2_CONTAINER_ID, 'errorCodeMissingToken' => self::ERROR_CODE_MISSING_TOKEN, 'errorCodeVerificationFailed' => self::ERROR_CODE_VERIFICATION_FAILED));
}
public function render_v2_container(): string
{
return '<div id="' . esc_attr(self::V2_CONTAINER_ID) . '" style="margin:20px 0;"></div>';
}
public function intercept_paypal_ajax(array $request_data): void
{
if (!$this->should_use_recaptcha()) {
return;
}
$token = sanitize_text_field(wp_unslash($request_data['ppcp_recaptcha_token'] ?? ''));
$version = sanitize_text_field(wp_unslash($request_data['ppcp_recaptcha_version'] ?? ''));
if (empty($token)) {
wp_send_json_error(array('message' => __('Please complete the CAPTCHA verification.', 'woocommerce-paypal-payments'), 'code' => self::ERROR_CODE_MISSING_TOKEN), 400);
exit;
}
$success = $version === 'v3' ? $this->verify_v3($token, $this->integration->get_option('secret_key_v3'), $this->score_threshold()) : $this->verify_v2($token, $this->integration->get_option('secret_key_v2'));
if (!$success) {
wp_send_json_error(array('message' => __('CAPTCHA verification failed. Please try again.', 'woocommerce-paypal-payments'), 'code' => self::ERROR_CODE_VERIFICATION_FAILED), 403);
exit;
}
}
public function validate_classic_checkout(): void
{
if (!$this->should_use_recaptcha()) {
return;
}
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verification handled by WooCommerce before this hook fires
/** @psalm-suppress PossiblyInvalidCast */
$payment_method = sanitize_text_field(wp_unslash((string) ($_POST['payment_method'] ?? '')));
/** @psalm-suppress PossiblyInvalidCast */
$token = sanitize_text_field(wp_unslash((string) ($_POST['ppcp_recaptcha_token'] ?? '')));
/** @psalm-suppress PossiblyInvalidCast */
$version = sanitize_text_field(wp_unslash((string) ($_POST['ppcp_recaptcha_version'] ?? '')));
// phpcs:enable WordPress.Security.NonceVerification.Missing
if (!in_array($payment_method, $this->payment_methods, \true)) {
return;
}
if (empty($token)) {
wc_add_notice(__('Please complete the CAPTCHA verification.', 'woocommerce-paypal-payments'), 'error');
return;
}
$success = $version === 'v3' ? $this->verify_v3($token, $this->integration->get_option('secret_key_v3'), $this->score_threshold()) : $this->verify_v2($token, $this->integration->get_option('secret_key_v2'));
if (!$success) {
wc_add_notice(__('CAPTCHA verification failed. Please try again.', 'woocommerce-paypal-payments'), 'error');
}
}
/**
* @param WP_Error|null|true $errors
*
* @return WP_Error|null|true WP_Error
*/
public function validate_blocks_request($errors)
{
$request_uri = sanitize_url(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
if (!is_wp_error($errors) && strpos($request_uri, '/wc/store/v1/checkout') !== \false) {
if (!$this->should_use_recaptcha()) {
return $errors;
}
$request_body = file_get_contents('php://input');
if (!is_string($request_body)) {
return $errors;
}
$data = json_decode($request_body, \true);
// Not an order creation request.
if (!is_array($data) || !isset($data['billing_address'])) {
return $errors;
}
$ext_data = $data['extensions']['ppcp_recaptcha'] ?? null;
$payment_method = sanitize_text_field($data['payment_method'] ?? '');
if (!in_array($payment_method, $this->payment_methods, \true)) {
return $errors;
}
if (empty($ext_data) || empty($ext_data['token']) || empty($ext_data['version'])) {
return new WP_Error(self::ERROR_CODE_MISSING_TOKEN, __('Please complete the CAPTCHA verification.', 'woocommerce-paypal-payments'), array('status' => 400));
}
$token = sanitize_text_field($ext_data['token']);
$version = sanitize_text_field($ext_data['version']);
// Initialize WooCommerce session as it doesn't exist in REST API requests.
WC()->initialize_session();
$success = $version === 'v3' ? $this->verify_v3($token, $this->integration->get_option('secret_key_v3'), $this->score_threshold()) : $this->verify_v2($token, $this->integration->get_option('secret_key_v2'));
if (!$success) {
return new WP_Error(self::ERROR_CODE_VERIFICATION_FAILED, __('CAPTCHA verification failed. Please try again.', 'woocommerce-paypal-payments'), array('status' => 403));
}
}
return $errors;
}
public function add_result_meta(WC_Order $order): void
{
$customer_id = $this->customer_identifier();
if (!$customer_id) {
$this->logger->debug('Skipping reCAPTCHA meta addition: No customer identifier available', array('order_id' => $order->get_id(), 'backtrace' => \true));
return;
}
$result = get_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id);
if (!$result) {
return;
}
$order->update_meta_data(self::CAPTCHA_RESULT_META_KEY, $result);
$order->save();
delete_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id);
}
public function add_metabox(): void
{
if (!wc_string_to_bool($this->integration->get_option('show_metabox'))) {
return;
}
$screen = OrderUtil::custom_orders_table_usage_is_enabled() ? wc_get_page_screen_id('shop-order') : 'shop_order';
add_meta_box('ppcp_recaptcha_status', __('reCAPTCHA Status', 'woocommerce-paypal-payments'), function (WC_Order $order): void {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $this->render_metabox($order);
}, $screen, 'normal');
}
private function render_metabox(WC_Order $order): string
{
$captcha_result = $order->get_meta(self::CAPTCHA_RESULT_META_KEY);
if (empty($captcha_result)) {
return '<p>' . esc_html__('No reCAPTCHA data', 'woocommerce-paypal-payments') . '</p>';
}
// Truncate token to last 10 characters for display.
if (isset($captcha_result['token']) && is_string($captcha_result['token'])) {
if (strlen($captcha_result['token']) > 10) {
$captcha_result['token'] = '...' . (string) substr($captcha_result['token'], -10);
}
}
return '<pre>' . esc_html((string) wp_json_encode($captcha_result, \JSON_PRETTY_PRINT)) . '</pre>';
}
private function verify_v3(string $token, string $secret, float $threshold): bool
{
if ($this->check_cached_verification($token)) {
return \true;
}
$response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', array('body' => array('secret' => $secret, 'response' => $token, 'remoteip' => $this->customer_ip())));
if (is_wp_error($response)) {
$this->logger->error('reCAPTCHA v3 API error: ' . $response->get_error_message());
return \false;
}
$result = json_decode(wp_remote_retrieve_body($response), \true);
$score = isset($result['score']) ? floatval($result['score']) : 0;
$is_above_threshold = !empty($result['success']) && $score >= $threshold;
$is_valid = apply_filters('woocommerce_paypal_payments_recaptcha_verify_v3_result', $is_above_threshold, $threshold, $result);
if ($is_valid) {
$customer_id = $this->customer_identifier();
if ($customer_id) {
$cached_data = array('result' => $result, 'token' => $token, 'usage_count' => 1);
set_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id, $cached_data, 300);
} else {
$this->logger->debug('reCAPTCHA v3 verification successful but not cached: No customer identifier available', array('backtrace' => \true));
}
}
return $is_valid;
}
private function verify_v2(string $token, string $secret): bool
{
if ($this->check_cached_verification($token)) {
return \true;
}
$response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', array('body' => array('secret' => $secret, 'response' => $token, 'remoteip' => $this->customer_ip())));
if (is_wp_error($response)) {
$this->logger->error('reCAPTCHA v2 API error: ' . $response->get_error_message());
return \false;
}
$result = json_decode(wp_remote_retrieve_body($response), \true);
$is_valid = apply_filters('woocommerce_paypal_payments_recaptcha_verify_v2_result', $result['success'], $result);
if ($is_valid) {
$customer_id = $this->customer_identifier();
if ($customer_id) {
$cached_data = array('result' => $result, 'token' => $token, 'usage_count' => 1);
set_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id, $cached_data, 300);
} else {
$this->logger->debug('reCAPTCHA v2 verification successful but not cached: No customer identifier available', array('backtrace' => \true));
}
}
return $is_valid;
}
private function check_cached_verification(string $token): bool
{
$customer_id = $this->customer_identifier();
if (!$customer_id) {
$this->logger->debug('Skipping cached verification check: No customer identifier available', array('backtrace' => \true));
return \false;
}
$cached_data = get_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id);
if ($cached_data === \false || !isset($cached_data['usage_count'], $cached_data['token'])) {
return \false;
}
if ($cached_data['token'] === $token && $cached_data['usage_count'] < self::CAPTCHA_USAGE_LIMIT) {
++$cached_data['usage_count'];
set_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id, $cached_data, 300);
return \true;
}
if ($cached_data['usage_count'] >= self::CAPTCHA_USAGE_LIMIT) {
delete_transient(self::CAPTCHA_RESULT_TRANSIENT_KEY . $customer_id);
}
return \false;
}
private function customer_identifier(): ?string
{
if (!WC()->session || !WC()->session->get_customer_id()) {
return null;
}
return (string) WC()->session->get_customer_id();
}
private function customer_ip(): string
{
return filter_var(wp_unslash($_SERVER['REMOTE_ADDR'] ?? ''), \FILTER_VALIDATE_IP) ?: '';
}
private function score_threshold(): float
{
return floatval($this->integration->get_option('score_threshold', 0.5));
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\FraudProtection\Recaptcha;
use WC_Integration;
class RecaptchaIntegration extends WC_Integration
{
public const ID = 'ppcp-recaptcha';
public function __construct()
{
$this->id = self::ID;
$this->method_title = 'WooCommerce PayPal Payments reCAPTCHA';
$this->method_description = 'Protects PayPal for WooCommerce checkout and cart with Google reCAPTCHA v3 (primary) and v2 (fallback).';
$this->init_form_fields();
$this->init_settings();
add_action('woocommerce_update_options_integration_' . $this->id, array($this, 'process_admin_options'));
}
public function init_form_fields()
{
$this->form_fields = array('enabled' => array('title' => 'Enable/Disable', 'type' => 'checkbox', 'label' => 'Enable reCAPTCHA protection', 'default' => 'no'), 'v3_title' => array('title' => 'reCAPTCHA v3 Settings', 'type' => 'title', 'description' => sprintf('Primary invisible protection. To get the keys go to <a href="%s" target="_blank">Google reCAPTCHA Admin</a> and create a site with <b>Score based (v3)</b> reCAPTCHA type.', 'https://www.google.com/recaptcha/admin')), 'site_key_v3' => array('title' => 'v3 Site Key', 'type' => 'text', 'desc_tip' => \true, 'description' => 'Your reCAPTCHA v3 site key'), 'secret_key_v3' => array('title' => 'v3 Secret Key', 'type' => 'password', 'desc_tip' => \true, 'description' => 'Your reCAPTCHA v3 secret key'), 'score_threshold' => array('title' => 'Score Threshold', 'type' => 'number', 'default' => '0.5', 'custom_attributes' => array('min' => '0', 'max' => '1', 'step' => '0.1'), 'desc_tip' => \true, 'description' => 'Minimum score to pass (0.01.0). Lower scores trigger v2 fallback. Recommended: 0.5'), 'v2_title' => array('title' => 'reCAPTCHA v2 Settings', 'type' => 'title', 'description' => sprintf('Fallback visible checkbox when v3 score is below threshold. To get the keys go to <a href="%s" target="_blank">Google reCAPTCHA Admin</a> and create a site with <b>Challenge (v2) -> "I\'m not a robot" Checkbox</b> reCAPTCHA type.', 'https://www.google.com/recaptcha/admin')), 'site_key_v2' => array('title' => 'v2 Site Key', 'type' => 'text', 'desc_tip' => \true, 'description' => 'Your reCAPTCHA v2 (checkbox) site key'), 'secret_key_v2' => array('title' => 'v2 Secret Key', 'type' => 'password', 'desc_tip' => \true, 'description' => 'Your reCAPTCHA v2 secret key'), 'v2_theme' => array('title' => 'v2 Theme', 'type' => 'select', 'default' => 'light', 'options' => array('light' => 'Light', 'dark' => 'Dark'), 'desc_tip' => \true, 'description' => 'Color theme for the v2 checkbox'), 'scope_title' => array('title' => 'Protection Scope', 'type' => 'title', 'description' => 'Configure where reCAPTCHA protection is applied'), 'guest_only' => array('title' => 'Guest Orders Only', 'type' => 'checkbox', 'label' => 'Only verify for non-logged-in users', 'default' => 'yes', 'description' => 'Skip reCAPTCHA for logged-in customers'), 'advanced_title' => array('title' => 'Advanced Options', 'type' => 'title'), 'show_metabox' => array('title' => 'Order Metabox', 'type' => 'checkbox', 'label' => 'Show reCAPTCHA status metabox on order pages', 'default' => 'no', 'description' => 'Display reCAPTCHA verification details in a metabox on order edit pages'));
}
}