HEX
Server: nginx
System: Linux pool195-106-36.bur.atomicsites.net 6.12.57+deb12-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1~bpo12+1 (2025-11-17) x86_64
User: (0)
PHP: 8.3.32
Disabled: pcntl_fork
Upload Files
File: //wordpress/plugins/woocommerce-fraud-protection/0.1.5/src/FraudProtection/SessionVerifier.php
<?php
/**
 * SessionVerifier class file.
 */

declare( strict_types=1 );

namespace Automattic\WooCommerce\FraudProtection;

use Automattic\WooCommerce\FraudProtection\Schemas\FraudDecision;
use Automattic\WooCommerce\Internal\FraudProtectionPlugin\ApiClient;
use Automattic\WooCommerce\Internal\FraudProtectionPlugin\DecisionHandler;
use Automattic\WooCommerce\Internal\FraudProtectionPlugin\FraudProtectionController;
use Automattic\WooCommerce\Internal\FraudProtectionPlugin\PaymentDataResolver;
use Automattic\WooCommerce\Internal\FraudProtectionPlugin\Sessions\SessionDataCollector;

defined( 'ABSPATH' ) || exit;

/**
 * Encapsulates the resolve-payment → collect → verify → apply-decision flow.
 *
 * Each checkout/payment handler (blocks checkout, shortcode checkout,
 * add-payment-method, etc.) delegates to this class for the common
 * verification sequence, and only handles the context-specific response
 * to the returned decision (e.g. throw RouteException, add wc_notice).
 *
 * Fail-open: verify_session() never throws. All internal errors (payment
 * data resolution, API call, decision handler) result in an ALLOW decision.
 */
class SessionVerifier {

	/**
	 * Order meta key for storing the Blackbox session ID.
	 *
	 * Persisted so the report integration can correlate order outcomes
	 * (e.g. chargebacks, refunds) back to the original fraud-check session.
	 */
	public const ORDER_BLACKBOX_SESSION_ID_KEY = '_wc_fraud_protection_session_id';

	/**
	 * Session data collector instance.
	 *
	 * @var SessionDataCollector
	 */
	private SessionDataCollector $data_collector;

	/**
	 * API client instance.
	 *
	 * @var ApiClient
	 */
	private ApiClient $api_client;

	/**
	 * Decision handler instance.
	 *
	 * @var DecisionHandler
	 */
	private DecisionHandler $decision_handler;

	/**
	 * Payment data resolver instance.
	 *
	 * @var PaymentDataResolver
	 */
	private PaymentDataResolver $payment_data_resolver;

	/**
	 * Initialize with dependencies.
	 *
	 * @internal
	 *
	 * @param SessionDataCollector $data_collector        The session data collector instance.
	 * @param ApiClient            $api_client            The API client instance.
	 * @param DecisionHandler      $decision_handler      The decision handler instance.
	 * @param PaymentDataResolver  $payment_data_resolver The payment data resolver instance.
	 */
	final public function init(
		SessionDataCollector $data_collector,
		ApiClient $api_client,
		DecisionHandler $decision_handler,
		PaymentDataResolver $payment_data_resolver
	): void {
		$this->data_collector        = $data_collector;
		$this->api_client            = $api_client;
		$this->decision_handler      = $decision_handler;
		$this->payment_data_resolver = $payment_data_resolver;
	}

	/**
	 * Register hooks for deferred session ID persistence.
	 *
	 * In shortcode checkout the order does not exist at verification time
	 * (order_id = 0). This hook copies the session ID from the WC session
	 * to order meta once the order is created.
	 */
	public function register(): void {
		add_action( 'woocommerce_checkout_order_created', array( $this, 'persist_session_id_to_order' ) );
	}

	/**
	 * Copy the Blackbox session ID from the WC session to order meta.
	 *
	 * Hooked to `woocommerce_checkout_order_created` for flows where the
	 * order did not exist at verification time.
	 *
	 * @internal
	 *
	 * @param \WC_Order $order The newly created order.
	 */
	public function persist_session_id_to_order( \WC_Order $order ): void {
		$session_id = $this->get_session_id_from_session();
		if ( '' === $session_id ) {
			return;
		}

		$order->update_meta_data( self::ORDER_BLACKBOX_SESSION_ID_KEY, $session_id );
		$order->save_meta_data();

		FraudProtectionController::log(
			'info',
			sprintf(
				'Persisted session ID to order meta (deferred): order=%d',
				$order->get_id()
			)
		);
	}

	/**
	 * Verify a session and return the final decision.
	 *
	 * Resolves payment data, collects session/order data, calls the Blackbox
	 * verify API, and applies the decision.
	 *
	 * Fail-open: Never throws. Returns ALLOW on any internal error.
	 *
	 * @param string $session_id   The Blackbox session ID from collect().
	 * @param string $source       Identifies the caller (e.g. 'blocks_checkout').
	 * @param int    $order_id     The WooCommerce order ID (0 for pre-order flows).
	 * @param array  $request_data Request data containing payment_method and payment_data.
	 * @return FraudDecision The final decision: Allow or Block.
	 */
	public function verify_session( string $session_id, string $source, int $order_id = 0, array $request_data = array() ): FraudDecision {
		try {
			/**
			 * Filters whether to skip session verification.
			 *
			 * Extensions that handle their own verification (e.g. PayPal express
			 * checkout via PayPalCompat) can return true to skip the redundant
			 * verify call from standard protectors.
			 *
			 * Fail-open: truthy values skip verification (session is allowed).
			 *
			 * @since 0.1.0
			 *
			 * @param bool   $skip         Whether to skip verification. Default false.
			 * @param string $source       Source identifier (e.g. 'blocks_checkout').
			 * @param array  $request_data Request data with payment_method, payment_data, etc.
			 * @param string $session_id   The Blackbox session ID being verified.
			 */
			$skip = (bool) apply_filters( 'woocommerce_fraud_protection_skip_session_verify', false, $source, $request_data, $session_id );

			if ( $skip ) {
				FraudProtectionController::log(
					'info',
					sprintf( 'Session verification skipped by `woocommerce_fraud_protection_skip_session_verify` filter for source: %s', $source )
				);
				return FraudDecision::Allow;
			}
		} catch ( \Throwable $e ) {
			FraudProtectionController::log(
				'warning',
				'`woocommerce_fraud_protection_skip_session_verify` filter threw',
				array(
					'event_source'      => $source,
					'session_id'        => $session_id,
					'filter'            => 'woocommerce_fraud_protection_skip_session_verify',
					'exception'         => $e,
					'exception_class'   => $e::class,
					'exception_message' => $e->getMessage(),
					'exception_file'    => $e->getFile(),
					'exception_line'    => $e->getLine(),
				),
				true
			);
		}

		// Resolve payment data (fail-open).
		$payment_data = null;
		try {
			$payment_data = $this->payment_data_resolver->resolve(
				$request_data['payment_method'] ?? '',
				$request_data['payment_data'] ?? array()
			);
		} catch ( \Throwable $e ) {
			FraudProtectionController::log(
				'warning',
				'Payment data resolution failed',
				array(
					'event_source'      => $source,
					'session_id'        => $session_id,
					'order_id'          => $order_id,
					'payment_type'      => $request_data['payment_method'] ?? '',
					'hook'              => 'payment_data_resolution',
					'exception'         => $e,
					'exception_class'   => $e::class,
					'exception_message' => $e->getMessage(),
					'exception_file'    => $e->getFile(),
					'exception_line'    => $e->getLine(),
					'verify_context'    => array(
						'source'       => $source,
						'session_id'   => $session_id,
						'order_id'     => $order_id,
						'request_data' => $request_data,
					),
				),
				true
			);
		}

		// Collect data, call API, apply decision (fail-open).
		try {
			$payload = $this->data_collector->get_collected_data( $order_id );

			$payload['source']  = $source;
			$payload['payment'] = $payment_data?->to_array();

			$result   = $this->api_client->verify( $session_id, $payload );
			$decision = $this->decision_handler->apply_decision( $result->decision, $payload );

			// No collect session: persist the ID Blackbox generated so /report can attach the outcome.
			$effective_session_id = '' === $session_id ? $result->session_id : $session_id;
			$this->persist_session_id( $effective_session_id, $order_id );
		} catch ( \Throwable $e ) {
			FraudProtectionController::log(
				'error',
				'Session verification failed, allowing',
				array(
					'event_source'      => $source,
					'session_id'        => $session_id,
					'order_id'          => $order_id,
					'hook'              => 'session_verify',
					'exception'         => $e,
					'exception_class'   => $e::class,
					'exception_message' => $e->getMessage(),
					'exception_file'    => $e->getFile(),
					'exception_line'    => $e->getLine(),
					'verify_context'    => array(
						'source'       => $source,
						'session_id'   => $session_id,
						'order_id'     => $order_id,
						'request_data' => $request_data,
					),
				),
				true
			);
			return FraudDecision::Allow;
		}

		return $decision;
	}

	/**
	 * Persist the Blackbox session ID to order meta and WC session.
	 *
	 * Saves to order meta when an order already exists (blocks checkout,
	 * pay-for-order). Always saves to WC session so the deferred hook
	 * can pick it up for shortcode checkout.
	 *
	 * @param string $session_id The Blackbox session ID.
	 * @param int    $order_id   The WooCommerce order ID (0 for pre-order flows).
	 */
	private function persist_session_id( string $session_id, int $order_id ): void {
		$this->store_session_id_in_session( $session_id );

		if ( $order_id > 0 ) {
			$order = wc_get_order( $order_id );
			if ( $order instanceof \WC_Order ) {
				$order->update_meta_data( self::ORDER_BLACKBOX_SESSION_ID_KEY, $session_id );
				$order->save_meta_data();
			}
		}
	}

	/**
	 * Store the Blackbox session ID in the WC session.
	 *
	 * @param string $session_id The Blackbox session ID.
	 */
	private function store_session_id_in_session( string $session_id ): void {
		if ( ! function_exists( 'WC' ) || ! WC()->session instanceof \WC_Session ) {
			return;
		}

		WC()->session->set( self::ORDER_BLACKBOX_SESSION_ID_KEY, $session_id );
	}

	/**
	 * Retrieve the Blackbox session ID from the WC session.
	 *
	 * @return string The session ID, or empty string if unavailable.
	 */
	private function get_session_id_from_session(): string {
		if ( ! function_exists( 'WC' ) || ! WC()->session instanceof \WC_Session ) {
			return '';
		}

		$session_id = WC()->session->get( self::ORDER_BLACKBOX_SESSION_ID_KEY, '' );

		return is_string( $session_id ) ? $session_id : '';
	}
}