File "SessionClearanceManager.php"

Full Path: /home/shadsolw/public_html/wp-content/plugins/woocommerce/src/Internal/FraudProtection/SessionClearanceManager.php
File size: 5.8 KB
MIME-type: text/x-php
Charset: utf-8

<?php
/**
 * SessionClearanceManager class file.
 */

declare( strict_types=1 );

namespace Automattic\WooCommerce\Internal\FraudProtection;

defined( 'ABSPATH' ) || exit;

/**
 * Manages session clearance state for fraud protection.
 *
 * This class handles the session status tracking for fraud protection decisions,
 * managing three possible states: pending, allowed, and blocked. It integrates
 * with WooCommerce sessions and uses the FraudProtectionController logging helper
 * to maintain consistent audit logs.
 *
 * @since 10.5.0
 * @internal This class is part of the internal API and is subject to change without notice.
 */
class SessionClearanceManager {

	/**
	 * Session key for storing clearance status.
	 */
	private const SESSION_KEY = '_fraud_protection_clearance_status';

	/**
	 * Session status: pending clearance.
	 */
	public const STATUS_PENDING = 'pending';

	/**
	 * Session status: allowed.
	 */
	public const STATUS_ALLOWED = 'allowed';

	/**
	 * Session status: blocked.
	 */
	public const STATUS_BLOCKED = 'blocked';

	/**
	 * Default session status.
	 */
	public const DEFAULT_STATUS = self::STATUS_ALLOWED;

	/**
	 * Check if the current session is allowed.
	 *
	 * @return bool True if session is allowed, false otherwise.
	 */
	public function is_session_allowed(): bool {
		$status = $this->get_session_status();
		return self::STATUS_ALLOWED === $status;
	}

	/**
	 * Check if the current session is blocked.
	 *
	 * @return bool True if session is blocked, false otherwise.
	 */
	public function is_session_blocked(): bool {
		$status = $this->get_session_status();
		return self::STATUS_BLOCKED === $status;
	}

	/**
	 * Mark the current session as allowed.
	 *
	 * @return void
	 */
	public function allow_session(): void {
		$this->set_session_status( self::STATUS_ALLOWED );
		$this->log_session_update_event( 'allowed' );
	}

	/**
	 * Mark the current session as pending (challenge required).
	 *
	 * @return void
	 */
	public function challenge_session(): void {
		$this->set_session_status( self::STATUS_PENDING );
		$this->log_session_update_event( 'challenged' );
	}

	/**
	 * Mark the current session as blocked.
	 *
	 * @return void
	 */
	public function block_session(): void {
		$this->set_session_status( self::STATUS_BLOCKED );
		$this->log_session_update_event( 'blocked' );
		$this->empty_cart();
	}

	/**
	 * Get the current session clearance status.
	 *
	 * @return string One of: pending, allowed, blocked.
	 */
	public function get_session_status(): string {
		if ( ! $this->is_session_available() ) {
			return self::DEFAULT_STATUS;
		}

		$status = WC()->session->get( self::SESSION_KEY, self::DEFAULT_STATUS );

		// Validate status value - return default for invalid values.
		if ( ! in_array( $status, array( self::STATUS_PENDING, self::STATUS_ALLOWED, self::STATUS_BLOCKED ), true ) ) {
			return self::DEFAULT_STATUS;
		}

		return $status;
	}

	/**
	 * Set the session clearance status.
	 *
	 * @param string $status One of: pending, allowed, blocked.
	 * @return void
	 */
	private function set_session_status( string $status ): void {
		if ( ! $this->is_session_available() ) {
			return;
		}

		WC()->session->set( self::SESSION_KEY, $status );

		// Ensure session cookie is set so the session persists across page loads.
		// This is important because fraud protection may set session status before
		// any cart action triggers the cookie to be set.
		// Skip cookie setting if headers have already been sent (e.g., in test environment).
		if ( WC()->session instanceof \WC_Session_Handler ) {
			WC()->session->set_customer_session_cookie( true );
		}
	}

	/**
	 * Reset the session clearance status to default (allowed).
	 *
	 * @return void
	 */
	public function reset_session(): void {
		$this->set_session_status( self::DEFAULT_STATUS );
	}

	/**
	 * Ensure cart and session are available.
	 *
	 * Loads cart if not already loaded, which initializes session for both
	 * traditional (cookie) and Store API (token) flows.
	 *
	 * @return void
	 */
	public function ensure_cart_loaded(): void {
		if ( ! did_action( 'woocommerce_load_cart_from_session' ) && function_exists( 'wc_load_cart' ) ) {
			WC()->call_function( 'wc_load_cart' );
		}
	}

	/**
	 * Check if WooCommerce session is available.
	 *
	 * @return bool True if session is available.
	 */
	private function is_session_available(): bool {
		$this->ensure_cart_loaded();
		return WC()->session instanceof \WC_Session;
	}

	/**
	 * Get a unique identifier for the current session.
	 *
	 * @return string Session identifier.
	 */
	public function get_session_id(): string {
		if ( ! $this->is_session_available() ) {
			return 'no-session';
		}

		// Use or generate a stable session ID for tracking consistency.
		$fraud_customer_session_id = WC()->session->get( '_fraud_protection_customer_session_id' );
		if ( ! $fraud_customer_session_id ) {
			$fraud_customer_session_id = WC()->call_function( 'wc_rand_hash', 'customer_', 30 );
			WC()->session->set( '_fraud_protection_customer_session_id', $fraud_customer_session_id );
		}
		return $fraud_customer_session_id;
	}

	/**
	 * Empty the cart.
	 *
	 * @return void
	 */
	private function empty_cart(): void {
		if ( function_exists( 'WC' ) && WC()->cart ) {
			WC()->cart->empty_cart();
		}
	}

	/**
	 * Log a session update event using FraudProtectionController's logging helper.
	 *
	 * @param string $action The action taken (allowed, challenged, or blocked).
	 * @return void
	 */
	private function log_session_update_event( string $action ): void {
		$session_id = $this->get_session_id();
		$user_id    = get_current_user_id();
		$user_info  = $user_id ? "User: {$user_id}" : 'User: guest';
		$timestamp  = current_time( 'mysql' );

		$message = sprintf(
			'Session updated: %s | %s | Action: %s | Timestamp: %s',
			$session_id,
			$user_info,
			$action,
			$timestamp
		);

		FraudProtectionController::log( 'info', $message );
	}
}