This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,92 @@
<?php
/**
* Class Buyer_Fingerprinting_Service
*
* @package WCPay\Fraud_Prevention
*/
namespace WCPay\Fraud_Prevention;
use WC_Geolocation;
/**
* Class Buyer_Fingerprinting_Service
*/
class Buyer_Fingerprinting_Service {
/**
* Singleton instance.
*
* @var Buyer_Fingerprinting_Service
*/
private static $instance;
/**
* Returns singleton instance.
*
* @return Buyer_Fingerprinting_Service
*/
public static function get_instance(): self {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Sets a instance to be used in request cycle.
* Introduced primarily for supporting unit tests.
*
* @param Buyer_Fingerprinting_Service|null $instance Instance of self.
*/
public static function set_instance( ?self $instance = null ) {
self::$instance = $instance;
}
/**
* Hashes customer data for the fraud prevention.
*
* @param string $data The data you want to hash.
*
* @return string Hashed data.
*/
public function hash_data_for_fraud_prevention( string $data ): string {
return hash( 'sha512', $data, false );
}
/**
* Returns fraud prevention data for an order.
*
* @param string $fingerprint User fingerprint.
*
* @return array An array of hashed data for an order.
*/
public function get_hashed_data_for_customer( $fingerprint ): array {
global $wp;
$order_items_count = WC()->cart ? intval( WC()->cart->get_cart_contents_count() ) : null;
$order_id = null;
if ( isset( $wp->query_vars['order-pay'] ) ) {
$order_id = absint( $wp->query_vars['order-pay'] );
} elseif ( isset( $_POST['wcpay_order_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$order_id = absint( $_POST['wcpay_order_id'] ); // phpcs:ignore WordPress.Security.NonceVerification
}
if ( ! $order_items_count && 0 < $order_id ) {
$order = wc_get_order( $order_id );
if ( $order ) {
$order_items_count = $order->get_item_count();
}
}
// According to https://www.php.net/manual/en/function.array-filter.php#111091
// Applying "strlen" as the callback function will remove `false`, `null` and empty strings, but not "0" values.
return array_filter(
[
'fraud_prevention_data_shopper_ip_hash' => $this->hash_data_for_fraud_prevention( WC_Geolocation::get_ip_address() ),
'fraud_prevention_data_shopper_ua_hash' => $fingerprint,
'fraud_prevention_data_ip_country' => WC_Geolocation::geolocate_ip( '', true )['country'],
'fraud_prevention_data_cart_contents' => $order_items_count,
],
'strlen'
);
}
}
@@ -0,0 +1,177 @@
<?php
/**
* Class Fraud_Prevention_Service
*
* @package WCPay\Fraud_Prevention
*/
namespace WCPay\Fraud_Prevention;
use WC_Payment_Gateway_WCPay;
use WC_Payments;
/**
* Class Fraud_Prevention_Service
*/
class Fraud_Prevention_Service {
const TOKEN_NAME = 'wcpay-fraud-prevention-token';
/**
* Singleton instance.
*
* @var Fraud_Prevention_Service
*/
private static $instance;
/**
* Session instance.
*
* @var \WC_Session
*/
private $session;
/**
* Instance of WC_Payment_Gateway_WCPay.
*
* @var WC_Payment_Gateway_WCPay
*/
private $wcpay_gateway;
/**
* Fraud_Prevention_Service constructor.
*
* @param \WC_Session $session Session instance.
* @param WC_Payment_Gateway_WCPay $wcpay_gateway Instance of WC_Payment_Gateway_WCPay.
*/
public function __construct( \WC_Session $session, WC_Payment_Gateway_WCPay $wcpay_gateway ) {
$this->session = $session;
$this->wcpay_gateway = $wcpay_gateway;
}
/**
* Returns singleton instance.
*
* @param null $session Session instance.
* @param null $gateway WC_Payment_Gateway_WCPay instance.
* @return Fraud_Prevention_Service
*/
public static function get_instance( $session = null, $gateway = null ): self {
if ( null === self::$instance ) {
self::$instance = new self( $session ?? WC()->session, $gateway ?? WC_Payments::get_gateway() );
}
return self::$instance;
}
/**
* Appends the fraud prevention token to the JS context if the protection is enabled, and a session exists.
* This token will also be used by express checkouts.
*
* @return void
*/
public static function maybe_append_fraud_prevention_token() {
if ( wp_script_is( self::TOKEN_NAME, 'enqueued' ) ) {
return;
}
// Check session first before trying to append the token.
if ( ! WC()->session ) {
return;
}
$instance = self::get_instance();
// Don't add the token if the prevention is not enabled.
if ( ! $instance->is_enabled() ) {
return;
}
// Don't add the token if the user isn't on the cart, checkout, product or pay for order page.
// Checking the product and cart page too because the user can pay quickly via the payment buttons on that page.
if ( ! is_checkout() && ! has_block( 'woocommerce/checkout' ) && ! is_cart() && ! is_product() && ! $instance->is_pay_for_order_page() ) {
return;
}
wp_register_script( self::TOKEN_NAME, '', [], time(), true );
wp_enqueue_script( self::TOKEN_NAME );
// Add the fraud prevention token to the checkout configuration.
wp_add_inline_script(
self::TOKEN_NAME,
"window.wcpayFraudPreventionToken = '" . esc_js( $instance->get_token() ) . "';",
'after'
);
}
/**
* Checks if this is the Pay for Order page.
*
* @return bool
*/
public function is_pay_for_order_page() {
return is_checkout() && isset( $_GET['pay_for_order'] ); // phpcs:ignore WordPress.Security.NonceVerification
}
/**
* Sets a instance to be used in request cycle.
* Introduced primarily for supporting unit tests.
*
* @param Fraud_Prevention_Service|null $instance Instance of self.
*/
public static function set_instance( ?self $instance = null ) {
self::$instance = $instance;
}
/**
* Checks if fraud prevention feature is enabled for the account.
*
* @return bool
*/
public function is_enabled(): bool {
return $this->wcpay_gateway->is_card_testing_protection_eligible();
}
/**
* Returns current valid token.
*
* For the first page load generates the token,
* for consecutive loads - takes from session.
*
* @return string|mixed
*/
public function get_token(): string {
$fraud_prevention_token = $this->session->get( self::TOKEN_NAME );
if ( ! $fraud_prevention_token ) {
$fraud_prevention_token = $this->regenerate_token();
}
return $fraud_prevention_token;
}
/**
* Generates a new token, persists in session and returns for immediate use.
*
* @return string
*/
public function regenerate_token(): string {
$token = wp_generate_password( 16, false );
$this->session->set( self::TOKEN_NAME, $token );
return $token;
}
/**
* Verifies the token against POST data.
*
* @param string|null $token Token sent in request.
* @return bool
*/
public function verify_token( ?string $token = null ): bool {
$session_token = $this->session->get( self::TOKEN_NAME );
// Check if the tokens are both strings.
if ( ! is_string( $session_token ) || ! is_string( $token ) ) {
return false;
}
// Compare the hashes to check request validity.
return hash_equals( $session_token, $token );
}
}
@@ -0,0 +1,398 @@
<?php
/**
* Class Fraud_Risk_Tools
*
* @package WooCommerce\Payments\Fraud_Risk_Tools
*/
namespace WCPay\Fraud_Prevention;
require_once __DIR__ . '/models/class-check.php';
require_once __DIR__ . '/models/class-rule.php';
use WC_Payments;
use WC_Payments_Account;
use WC_Payments_Features;
use WCPay\Fraud_Prevention\Models\Check;
use WCPay\Fraud_Prevention\Models\Rule;
use WCPay\Constants\Currency_Code;
defined( 'ABSPATH' ) || exit;
/**
* Class that controls Fraud and Risk tools functionality.
*/
class Fraud_Risk_Tools {
/**
* The single instance of the class.
*
* @var ?Fraud_Risk_Tools
*/
protected static $instance = null;
/**
* Instance of WC_Payments_Account.
*
* @var WC_Payments_Account
*/
private $payments_account;
/**
* Main Fraud_Risk_Tools Instance.
*
* Ensures only one instance of Fraud_Risk_Tools is loaded or can be loaded.
*
* @static
* @return Fraud_Risk_Tools - Main instance.
*/
public static function instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self( WC_Payments::get_account_service() );
self::$instance->init_hooks();
}
return self::$instance;
}
// Rule names.
const RULE_ADDRESS_MISMATCH = 'address_mismatch';
const RULE_INTERNATIONAL_IP_ADDRESS = 'international_ip_address';
const RULE_IP_ADDRESS_MISMATCH = 'ip_address_mismatch';
const RULE_ORDER_ITEMS_THRESHOLD = 'order_items_threshold';
const RULE_PURCHASE_PRICE_THRESHOLD = 'purchase_price_threshold';
/**
* Class constructor.
*
* @param WC_Payments_Account $payments_account WC_Payments_Account instance.
*/
public function __construct( WC_Payments_Account $payments_account ) {
$this->payments_account = $payments_account;
}
/**
* Initializes this class's WP hooks.
*
* @return void
*/
public function init_hooks() {
if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) {
add_action( 'admin_menu', [ $this, 'init_advanced_settings_page' ] );
}
}
/**
* Initialize the Fraud & Risk Tools Advanced Settings Page.
*
* @return void
*/
public function init_advanced_settings_page() {
// Settings page generation on the incoming CLI and async job calls.
if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( defined( 'WPCOM_JOBS' ) && WPCOM_JOBS ) ) {
return;
}
if ( ! $this->payments_account->is_stripe_account_valid() ) {
return;
}
if ( ! function_exists( 'wc_admin_register_page' ) ) {
return;
}
wc_admin_register_page(
[
'id' => 'wc-payments-fraud-protection',
'title' => __( 'Fraud protection', 'woocommerce-payments' ),
'parent' => 'wc-payments',
'path' => '/payments/fraud-protection',
'nav_args' => [
'parent' => 'wc-payments',
'order' => 50,
],
]
);
remove_submenu_page( 'wc-admin&path=/payments/overview', 'wc-admin&path=/payments/fraud-protection' );
}
/**
* Returns the basic protection rules.
*
* @return array
*/
public static function get_basic_protection_settings() {
$rules = [];
return self::get_ruleset_array( $rules );
}
/**
* Validates the array to see if it's a valid ruleset.
*
* @param array $array The array to validate.
*
* @return bool Whether if the given array is a ruleset, or not.
*/
public static function is_valid_ruleset_array( array $array ) {
foreach ( $array as $rule ) {
if ( ! Rule::validate_array( $rule ) ) {
return false;
}
}
return true;
}
/**
* Returns the international IP address rule.
*
* @return Rule International IP address rule object.
*/
public static function get_international_ip_address_rule() {
return new Rule(
self::RULE_INTERNATIONAL_IP_ADDRESS,
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
Check::check(
'ip_country',
self::get_selling_locations_type_operator(),
self::get_selling_locations_string()
)
);
}
/**
* Returns the standard protection rules.
*
* @return array
*/
public static function get_standard_protection_settings() {
$rules = [
// REVIEW An order originates from an IP address outside your country.
self::get_international_ip_address_rule(),
// REVIEW An order exceeds $1,000.00 or 10 items.
new Rule(
self::RULE_ORDER_ITEMS_THRESHOLD,
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
Check::check(
'item_count',
Check::OPERATOR_GT,
10
)
),
// REVIEW An order exceeds $1,000.00 or 10 items.
new Rule(
self::RULE_PURCHASE_PRICE_THRESHOLD,
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
Check::check(
'order_total',
Check::OPERATOR_GT,
self::get_formatted_converted_amount( 1000 * 100, strtolower( Currency_Code::UNITED_STATES_DOLLAR ) )
)
),
// REVIEW An order is originated from a different country than the shipping country.
new Rule(
self::RULE_IP_ADDRESS_MISMATCH,
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
Check::check(
'ip_billing_country_same',
Check::OPERATOR_EQUALS,
false
)
),
];
return self::get_ruleset_array( $rules );
}
/**
* Returns the default protection settings.
*
* @return array
*/
public static function get_high_protection_settings() {
$rules = [
// BLOCK An order originates from an IP address outside your country.
new Rule(
self::RULE_INTERNATIONAL_IP_ADDRESS,
Rule::FRAUD_OUTCOME_BLOCK,
Check::check(
'ip_country',
self::get_selling_locations_type_operator(),
self::get_selling_locations_string()
)
),
// BLOCK An order exceeds $1,000.00.
new Rule(
self::RULE_PURCHASE_PRICE_THRESHOLD,
Rule::FRAUD_OUTCOME_BLOCK,
Check::check(
'order_total',
Check::OPERATOR_GT,
self::get_formatted_converted_amount( 1000 * 100, strtolower( Currency_Code::UNITED_STATES_DOLLAR ) )
)
),
// REVIEW An order has less than 2 items or more than 10 items.
new Rule(
self::RULE_ORDER_ITEMS_THRESHOLD,
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
Check::list(
Check::LIST_OPERATOR_OR,
[
Check::check( 'item_count', Check::OPERATOR_LT, 2 ),
Check::check( 'item_count', Check::OPERATOR_GT, 10 ),
]
)
),
// REVIEW The shipping and billing address don't match.
new Rule(
self::RULE_ADDRESS_MISMATCH,
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
Check::check(
'billing_shipping_address_same',
Check::OPERATOR_EQUALS,
false
)
),
// REVIEW An order is originated from a different country than the shipping country.
new Rule(
self::RULE_IP_ADDRESS_MISMATCH,
WC_Payments_Features::is_frt_review_feature_active() ? Rule::FRAUD_OUTCOME_REVIEW : Rule::FRAUD_OUTCOME_BLOCK,
Check::check(
'ip_billing_country_same',
Check::OPERATOR_EQUALS,
false
)
),
];
return self::get_ruleset_array( $rules );
}
/**
* Returns the matching predef for a given ruleset array, if nothing matches, returns "advanced".
*
* @param array $fraud_ruleset The ruleset config to match to.
*
* @return string The matching protection level.
*/
public static function get_matching_protection_level( $fraud_ruleset ) {
// Check if the ruleset contains the basic protection config.
$target_ruleset = self::get_basic_protection_settings();
if ( $target_ruleset === $fraud_ruleset ) {
return 'basic';
}
// Check if the ruleset contains the standard protection config.
$target_ruleset = self::get_standard_protection_settings();
if ( $target_ruleset === $fraud_ruleset ) {
return 'standard';
}
// Check if the ruleset contains the high protection config.
$target_ruleset = self::get_high_protection_settings();
if ( $target_ruleset === $fraud_ruleset ) {
return 'high';
}
// The ruleset contains custom configuration.
return 'advanced';
}
/**
* Returns the array representation of ruleset.
*
* @param array $array The array of Rule objects.
*
* @return array
*/
private static function get_ruleset_array( $array ) {
return array_map(
function ( Rule $rule ) {
return $rule->to_array();
},
$array
);
}
/**
* Returns the check operator for international checks according to the WC Core selling locations setting.
*
* @return string The related operator.
*/
private static function get_selling_locations_type_operator() {
$selling_locations_type = get_option( 'woocommerce_allowed_countries', 'all' );
if ( 'specific' === $selling_locations_type ) {
return Check::OPERATOR_NOT_IN;
}
return Check::OPERATOR_IN;
}
/**
* Returns the countries to sell to, or not, as a | delimited string array.
*
* @return string The array imploded with | character.
*/
private static function get_selling_locations_string() {
$selling_locations_type = get_option( 'woocommerce_allowed_countries', 'all' );
switch ( $selling_locations_type ) {
case 'specific':
return strtolower( implode( '|', get_option( 'woocommerce_specific_allowed_countries', [] ) ) );
case 'all_except':
return strtolower( implode( '|', get_option( 'woocommerce_all_except_countries', [] ) ) );
case 'all':
return '';
default:
return '';
}
}
/**
* Returns the converted amount from a given currency to the default currency.
*
* @param int $amount The amount to be converted.
* @param string $from The currency to be converted from.
* @param string $to The currency to be converted to.
*
* @return int
*/
private static function get_converted_amount( $amount, $from, $to ) {
$to_currency = strtoupper( $to );
$from_currency = strtoupper( $from );
$enabled_currencies = WC_Payments_Multi_Currency()->get_enabled_currencies();
if ( empty( $enabled_currencies ) || $to_currency === $from_currency ) {
return $amount;
}
if ( array_key_exists( $from_currency, $enabled_currencies ) ) {
$currency = $enabled_currencies[ $from_currency ];
$amount = (int) round( $amount * ( 1 / (float) $currency->get_rate() ) );
}
return $amount;
}
/**
* Returns the formatted converted amount from a given currency to the default currency.
* The final format is "AMOUNT|CURRENCY".
*
* @param int $amount The amount to be converted.
* @param string $base_currency The currency to be converted from.
*
* @return string
*/
private static function get_formatted_converted_amount( $amount, $base_currency ) {
$default_currency = $base_currency;
$target_currency = $base_currency;
if ( function_exists( 'WC_Payments_Multi_Currency' ) ) {
$default_currency = WC_Payments_Multi_Currency()->get_default_currency();
if ( ! empty( $default_currency ) ) {
$target_currency = $default_currency->get_code();
$amount = self::get_converted_amount( $amount, $base_currency, $target_currency );
}
}
return implode( '|', [ $amount, strtolower( $target_currency ) ] );
}
}
@@ -0,0 +1,267 @@
<?php
/**
* Class Order_Fraud_And_Risk_Meta_Box
*
* @package WCPay\Fraud_Prevention
*/
namespace WCPay\Fraud_Prevention;
use WC_Payments_Admin_Settings;
use WC_Payments_Order_Service;
use WC_Payments_Utils;
use WCPay\Constants\Fraud_Meta_Box_Type;
use WCPay\Fraud_Prevention\Models\Rule;
/**
* Class Order_Fraud_And_Risk_Meta_Box
*/
class Order_Fraud_And_Risk_Meta_Box {
/**
* The Order Service.
*
* @var WC_Payments_Order_Service
*/
private $order_service;
/**
* Constructor.
*
* @param WC_Payments_Order_Service $order_service The order service.
*/
public function __construct( WC_Payments_Order_Service $order_service ) {
$this->order_service = $order_service;
}
/**
* Initializes this class's WP hooks.
*
* @return void
*/
public function init_hooks() {
add_action( 'add_meta_boxes', [ $this, 'maybe_add_meta_box' ] );
}
/**
* Maybe add the meta box.
*/
public function maybe_add_meta_box() {
// If we cannot get the screen ID, exit.
if ( ! function_exists( '\wc_get_page_screen_id' ) ) {
return;
}
// Get the order edit screen to be able to add the meta box to.
$wc_screen_id = \wc_get_page_screen_id( 'shop-order' );
add_meta_box( 'wcpay-order-fraud-and-risk-meta-box', __( 'Fraud &amp; Risk', 'woocommerce-payments' ), [ $this, 'display_order_fraud_and_risk_meta_box_message' ], $wc_screen_id, 'side', 'default' );
}
/**
* Displays the contents of the Fraud & Risk meta box.
*
* @param \WC_Order $order The order we are working with.
*
* @return void
*/
public function display_order_fraud_and_risk_meta_box_message( $order ) {
$order = wc_get_order( $order );
if ( ! $order ) {
return;
}
$intent_id = $this->order_service->get_intent_id_for_order( $order );
$charge_id = $this->order_service->get_charge_id_for_order( $order );
$meta_box_type = $this->order_service->get_fraud_meta_box_type_for_order( $order );
$risk_level = $this->order_service->get_charge_risk_level_for_order( $order );
$payment_method = $order->get_payment_method();
if ( strstr( $payment_method, 'woocommerce_payments_' ) ) {
$meta_box_type = Fraud_Meta_Box_Type::NOT_CARD;
} elseif ( 'woocommerce_payments' !== $payment_method ) {
$meta_box_type = Fraud_Meta_Box_Type::NOT_WCPAY;
}
$icons = [
'green_check_mark' => [
'url' => plugins_url( 'assets/images/icons/check-green.svg', WCPAY_PLUGIN_FILE ),
'alt' => __( 'Green check mark', 'woocommerce-payments' ),
],
'orange_shield' => [
'url' => plugins_url( 'assets/images/icons/shield-stroke-orange.svg', WCPAY_PLUGIN_FILE ),
'alt' => __( 'Orange shield outline', 'woocommerce-payments' ),
],
'red_shield' => [
'url' => plugins_url( 'assets/images/icons/shield-stroke-red.svg', WCPAY_PLUGIN_FILE ),
'alt' => __( 'Red shield outline', 'woocommerce-payments' ),
],
];
$statuses = [
'blocked' => __( 'Blocked', 'woocommerce-payments' ),
'approved' => __( 'Approved', 'woocommerce-payments' ),
'held_for_review' => __( 'Held for review', 'woocommerce-payments' ),
'no_action_taken' => __( 'No action taken', 'woocommerce-payments' ),
];
$risk_filters_callout = __( 'Adjust risk filters', 'woocommerce-payments' );
$risk_filters_url = WC_Payments_Admin_Settings::get_settings_url( [ 'anchor' => '%23fp-settings' ] );
$show_adjust_risk_filters_link = true;
$this->maybe_print_risk_level_block( $risk_level );
echo '<div class="wcpay-fraud-risk-action">';
switch ( $meta_box_type ) {
case Fraud_Meta_Box_Type::ALLOW:
$description = __( 'The payment for this order passed your risk filtering.', 'woocommerce-payments' );
echo '<p class="wcpay-fraud-risk-meta-allow"><img src="' . esc_url( $icons['green_check_mark']['url'] ) . '" alt="' . esc_html( $icons['green_check_mark']['alt'] ) . '"> ' . esc_html( $statuses['no_action_taken'] ) . '</p><p>' . esc_html( $description ) . '</p>';
break;
case Fraud_Meta_Box_Type::BLOCK:
$description = __( 'The payment for this order was blocked by your risk filtering. There is no pending authorization, and the order can be cancelled to reduce any held stock.', 'woocommerce-payments' );
$callout = __( 'View more details', 'woocommerce-payments' );
$transaction_url = $this->compose_transaction_url_with_tracking( $order->get_id(), '', Rule::FRAUD_OUTCOME_BLOCK );
echo '<p class="wcpay-fraud-risk-meta-blocked"><img src="' . esc_url( $icons['red_shield']['url'] ) . '" alt="' . esc_html( $icons['red_shield']['alt'] ) . '"> ' . esc_html( $statuses['blocked'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;
case Fraud_Meta_Box_Type::NOT_CARD:
case Fraud_Meta_Box_Type::NOT_WCPAY:
$payment_method_title = $order->get_payment_method_title();
$show_adjust_risk_filters_link = false;
if ( ! empty( $payment_method_title ) && 'Popular payment methods' !== $payment_method_title ) {
$description = sprintf(
/* translators: %1: WooPayments, %2: Payment method title */
__( 'Risk filtering is only available for orders processed using credit cards with %1$s. This order was processed with %2$s.', 'woocommerce-payments' ),
'WooPayments',
$payment_method_title
);
} else {
$description = sprintf(
/* translators: %s: WooPayments */
__( 'Risk filtering is only available for orders processed using credit cards with %s.', 'woocommerce-payments' ),
'WooPayments'
);
}
$callout = __( 'Learn more', 'woocommerce-payments' );
$callout_url = 'https://woocommerce.com/document/woopayments/fraud-and-disputes/fraud-protection/';
$callout_url = add_query_arg( 'status_is', 'fraud-meta-box-not-wcpay-learn-more', $callout_url );
echo '<p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $callout_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;
case Fraud_Meta_Box_Type::PAYMENT_STARTED:
$description = __( 'The payment for this order has not yet been passed to the fraud and risk filters to determine its outcome status.', 'woocommerce-payments' );
echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['no_action_taken'] ) . '</p><p>' . esc_html( $description ) . '</p>';
break;
case Fraud_Meta_Box_Type::REVIEW:
$description = __( 'The payment for this order was held for review by your risk filtering. You can review the details and determine whether to approve or block the payment.', 'woocommerce-payments' );
$callout = __( 'Review payment', 'woocommerce-payments' );
$transaction_url = $this->compose_transaction_url_with_tracking( $intent_id, $charge_id, Rule::FRAUD_OUTCOME_REVIEW );
echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;
case Fraud_Meta_Box_Type::REVIEW_ALLOWED:
$description = __( 'The payment for this order was held for review by your risk filtering and manually approved.', 'woocommerce-payments' );
echo '<p class="wcpay-fraud-risk-meta-allow"><img src="' . esc_url( $icons['green_check_mark']['url'] ) . '" alt="' . esc_html( $icons['green_check_mark']['alt'] ) . '"> ' . esc_html( $statuses['approved'] ) . '</p><p>' . esc_html( $description ) . '</p>';
break;
case Fraud_Meta_Box_Type::REVIEW_BLOCKED:
$description = __( 'This transaction was held for review by your risk filters, and the charge was manually blocked after review.', 'woocommerce-payments' );
$callout = __( 'Review payment', 'woocommerce-payments' );
$transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id );
echo '<p class="wcpay-fraud-risk-meta-blocked"><img src="' . esc_url( $icons['red_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;
case Fraud_Meta_Box_Type::REVIEW_EXPIRED:
$description = __( 'The payment for this order was held for review by your risk filtering. The authorization for the charge appears to have expired.', 'woocommerce-payments' );
$callout = __( 'Review payment', 'woocommerce-payments' );
$transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id );
echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;
case Fraud_Meta_Box_Type::REVIEW_FAILED:
$description = __( 'The payment for this order was held for review by your risk filtering. The authorization for the charge appears to have failed.', 'woocommerce-payments' );
$callout = __( 'Review payment', 'woocommerce-payments' );
$transaction_url = WC_Payments_Utils::compose_transaction_url( $intent_id, $charge_id );
echo '<p class="wcpay-fraud-risk-meta-review"><img src="' . esc_url( $icons['orange_shield']['url'] ) . '" alt="' . esc_html( $icons['orange_shield']['alt'] ) . '"> ' . esc_html( $statuses['held_for_review'] ) . '</p><p>' . esc_html( $description ) . '</p><p><a href="' . esc_url( $transaction_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $callout ) . '</a></p>';
break;
case Fraud_Meta_Box_Type::TERMINAL_PAYMENT:
$description = __( 'The payment for this order was done in person and has bypassed your risk filtering.', 'woocommerce-payments' );
echo '<p class="wcpay-fraud-risk-meta-allow"><img src="' . esc_url( $icons['green_check_mark']['url'] ) . '" alt="' . esc_html( $icons['green_check_mark']['alt'] ) . '"> ' . esc_html( $statuses['no_action_taken'] ) . '</p><p>' . esc_html( $description ) . '</p>';
break;
default:
$description = sprintf(
/* translators: %s: WooPayments */
__( 'Risk filtering through %s was not found on this order, it may have been created while filtering was not enabled.', 'woocommerce-payments' ),
'WooPayments'
);
echo '<p>' . esc_html( $description ) . '</p>';
break;
}
if ( $show_adjust_risk_filters_link ) {
echo '<p><a href="' . esc_url( $risk_filters_url ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $risk_filters_callout ) . '</a></p>';
}
echo '</div>';
}
/**
* Prints the risk level block.
*
* @param string $risk_level The risk level to display.
*
* @return void
*/
private function maybe_print_risk_level_block( $risk_level ) {
$valid_risk_levels = [ 'normal', 'elevated', 'highest' ];
if ( ! in_array( $risk_level, $valid_risk_levels, true ) ) {
return;
}
$titles = [
'normal' => __( 'Normal', 'woocommerce-payments' ),
'elevated' => __( 'Elevated', 'woocommerce-payments' ),
'highest' => __( 'High', 'woocommerce-payments' ),
];
$descriptions = [
'normal' => __( 'This payment shows a lower than normal risk of fraudulent activity.', 'woocommerce-payments' ),
'elevated' => __( 'This order has a moderate risk of being fraudulent. We suggest contacting the customer to confirm their details before fulfilling it.', 'woocommerce-payments' ),
'highest' => __( 'This order has a high risk of being fraudulent. We suggest contacting the customer to confirm their details before fulfilling it.', 'woocommerce-payments' ),
];
echo '<div class="wcpay-fraud-risk-level wcpay-fraud-risk-level--' . esc_attr( $risk_level ) . '">';
echo '<p class="wcpay-fraud-risk-level__title">' . esc_html( $titles[ $risk_level ] ) . '</p>';
echo '<div class="wcpay-fraud-risk-level__bar"></div>';
echo '<p>' . esc_html( $descriptions[ $risk_level ] ) . '</p>';
echo '</div>';
}
/**
* Composes url for transaction details page.
*
* @param string $primary_id Usually the Payment Intent ID, but can be an order ID.
* @param string $fallback_id Usually the Charge ID.
* @param string $status The status we're wanting to add to the meta box tracking.
*
* @return string Transaction details page url with tracking.
*/
private function compose_transaction_url_with_tracking( $primary_id, $fallback_id, $status ) {
return WC_Payments_Utils::compose_transaction_url(
$primary_id,
$fallback_id,
[
'status_is' => $status,
'type_is' => 'meta_box',
]
);
}
}
@@ -0,0 +1,223 @@
<?php
/**
* The check model class.
*
* @package WCPay\Fraud_Prevention\Models
*/
namespace WCPay\Fraud_Prevention\Models;
use WCPay\Exceptions\Fraud_Ruleset_Exception;
/**
* Check model.
*/
class Check {
// Check operators.
const OPERATOR_EQUALS = 'equals';
const OPERATOR_NOT_EQUALS = 'not_equals';
const OPERATOR_GTE = 'greater_or_equal';
const OPERATOR_GT = 'greater_than';
const OPERATOR_LTE = 'less_or_equal';
const OPERATOR_LT = 'less_than';
const OPERATOR_IN = 'in';
const OPERATOR_NOT_IN = 'not_in';
// Checklist operators.
const LIST_OPERATOR_AND = 'and';
const LIST_OPERATOR_OR = 'or';
/**
* List of check operators.
*
* @var array
*/
private static $check_operators = [
self::OPERATOR_EQUALS,
self::OPERATOR_NOT_EQUALS,
self::OPERATOR_GT,
self::OPERATOR_GTE,
self::OPERATOR_LT,
self::OPERATOR_LTE,
self::OPERATOR_IN,
self::OPERATOR_NOT_IN,
];
/**
* List of checklist operators.
*
* @var array
*/
private static $list_operators = [
self::LIST_OPERATOR_AND,
self::LIST_OPERATOR_OR,
];
/**
* Operator for the Check.
*
* @var string
*/
public $operator = null;
/**
* The key of the source which contains the data. Is mapped to a real data to compare with the value on the Fraud_Ruleset_Service.
*
* @var string
*/
public $key = null;
/**
* Value to check against the source.
*
* @var mixed
*/
public $value = null;
/**
* Subchecks array that when filled, indicates this is a checklist.
*
* @var array
*/
public $checks = [];
/**
* Creates a Check instance from an array.
*
* @param array $array The Check configuration.
*
* @return Check
* @throws Fraud_Ruleset_Exception When the array validation fails.
*/
public static function from_array( array $array ): Check {
// Check if this is a valid candidate for a rule. Rules should have keys, outcomes, and checks defined and not empty.
if ( ! self::validate_array( $array ) ) {
throw new Fraud_Ruleset_Exception( 'Check definition not valid.' );
}
$check = new self();
$check->key = $array['key'] ?? null;
$check->operator = $array['operator'];
$check->value = $array['value'] ?? null;
if ( isset( $array['checks'] ) ) {
foreach ( $array['checks'] as $check_definition ) {
$check->checks[] = self::from_array( $check_definition );
}
}
return $check;
}
/**
* Validates the given array if it's structured to become a Check object.
*
* @param array $array The array to validate.
*
* @return bool Whether it is a valid Check array.
*/
public static function validate_array( array $array ): bool {
// Check if this array contains an operator. In all cases it should have an operator field.
if ( ! isset( $array['operator'] ) ) {
return false;
}
if ( in_array( $array['operator'], self::$list_operators, true ) ) {
// This should be a checklist, and should have checks.
if ( ! isset( $array['checks'] ) || empty( $array['checks'] ) ) {
return false;
}
// Validate child checks.
foreach ( $array['checks'] as $check ) {
if ( ! self::validate_array( $check ) ) {
return false;
}
}
} elseif ( in_array( $array['operator'], self::$check_operators, true ) ) {
// This should be a single check, and should have key and value.
if ( ! isset( $array['value'] ) ) {
return false;
}
if ( ! isset( $array['key'] ) ) {
return false;
}
} else {
return false;
}
return true;
}
/**
* Creates a list type of check with the given parameters.
*
* @param string $operator The checklist operator.
* @param array $checks The child checks array.
*
* @return Check
* @throws Fraud_Ruleset_Exception When the validation fails.
*/
public static function list( string $operator, array $checks ) {
if ( ! in_array( $operator, self::$list_operators, true ) ) {
// $operator is a predefined constant, no need to escape.
// phpcs:ignore WordPress.Security.EscapeOutput
throw new Fraud_Ruleset_Exception( 'Operator for the check is invalid: ' . $operator );
}
if ( 0 < count(
array_filter(
$checks,
function ( $check ) {
return ! ( $check instanceof Check ); }
)
) ) {
throw new Fraud_Ruleset_Exception( 'The checklist checks should only contain Check objects.' );
}
$checklist = new Check();
$checklist->operator = $operator;
$checklist->checks = $checks;
return $checklist;
}
/**
* Creates a list type of check with the given parameters.
*
* @param string $key The key of the check.
* @param string $operator The check operator.
* @param mixed $value The value to compare against.
*
* @return Check
* @throws Fraud_Ruleset_Exception When the validation fails.
*/
public static function check( string $key, string $operator, $value ) {
if ( ! in_array( $operator, self::$check_operators, true ) ) {
// $operator is a predefined constant, no need to escape.
// phpcs:ignore WordPress.Security.EscapeOutput
throw new Fraud_Ruleset_Exception( 'Operator for the check is invalid: ' . $operator );
}
$check = new Check();
$check->operator = $operator;
$check->key = $key;
$check->value = $value;
return $check;
}
/**
* Converts the class to it's array representation for transmission.
*
* @return array
*/
public function to_array() {
if ( ! empty( $this->checks ) ) {
return [
'operator' => $this->operator,
'checks' => array_map(
function ( Check $check ) {
return $check->to_array();
},
$this->checks
),
];
}
return [
'key' => $this->key,
'operator' => $this->operator,
'value' => $this->value,
];
}
}
@@ -0,0 +1,154 @@
<?php
/**
* The rule model class.
*
* @package WCPay\Fraud_Prevention\Models
*/
namespace WCPay\Fraud_Prevention\Models;
use WCPay\Exceptions\Fraud_Ruleset_Exception;
/**
* Rule model.
*/
class Rule {
/**
* Constants that define the outcome of the rule.
*
* @var string
*/
const FRAUD_OUTCOME_ALLOW = 'allow';
const FRAUD_OUTCOME_REVIEW = 'review';
const FRAUD_OUTCOME_BLOCK = 'block';
/**
* Rule key.
*
* @var string
*/
public $key;
/**
* The action to take when the rule is successful.
*
* @var string
*/
public $outcome;
/**
* The check or checklist that defines the rule clause.
*
* @var Check
*/
public $check;
/**
* Class constructor.
*
* @param string $key The rule key.
* @param string $outcome The rule outcome.
* @param Check $check The single check, or the wrapper checklist.
*
* @return void
* @throws Fraud_Ruleset_Exception When the outcome validation fails.
*/
public function __construct( string $key, string $outcome, Check $check ) {
if ( ! in_array(
$outcome,
[ self::FRAUD_OUTCOME_ALLOW, self::FRAUD_OUTCOME_BLOCK, self::FRAUD_OUTCOME_REVIEW ],
true
) ) {
throw new Fraud_Ruleset_Exception( 'Given rule outcome is invalid.' );
}
$this->key = $key;
$this->outcome = $outcome;
$this->check = $check;
}
/**
* Creates a Rule instance from a Fraud_Ruleset rule_config field.
*
* @param array $array The rule array retrieved from parsing Fraud_Ruleset::rules_config.
*
* @return Rule
* @throws Fraud_Ruleset_Exception
*/
public static function from_array( array $array ): Rule {
// Check if this is a valid candidate for a rule. Rules should have keys, outcomes, and checks defined and not empty.
if ( ! self::validate_array( $array ) ) {
throw new Fraud_Ruleset_Exception( 'Rule definition not valid.' );
}
return new self(
$array['key'],
$array['outcome'],
Check::from_array( $array['check'] )
);
}
/**
* Validates the given array if it's structured to become a Rule object.
*
* @param array $array The array to validate.
*
* @return bool Whether it is a valid Rule array.
*/
public static function validate_array( array $array ) {
if ( ! isset( $array['key'], $array['check'], $array['outcome'] )
|| ! is_array( $array['check'] )
|| empty( $array['check'] )
|| ! in_array(
$array['outcome'],
[
self::FRAUD_OUTCOME_BLOCK,
self::FRAUD_OUTCOME_REVIEW,
self::FRAUD_OUTCOME_ALLOW,
],
true
)
) {
return false;
}
// Validate child checks.
if ( ! Check::validate_array( $array['check'] ) ) {
return false;
}
return true;
}
/**
* Validates the given string to see if it's a valid fraud outcome status.
*
* @param string $outcome The array to validate.
*
* @return bool Whether it is a valid Rule array.
*/
public static function is_valid_fraud_outcome_status( string $outcome ): bool {
return in_array(
$outcome,
[
self::FRAUD_OUTCOME_BLOCK,
self::FRAUD_OUTCOME_REVIEW,
self::FRAUD_OUTCOME_ALLOW,
],
true
);
}
/**
* Converts the class to it's array representation for transmission.
*
* @return array
*/
public function to_array() {
return [
'key' => $this->key,
'outcome' => $this->outcome,
'check' => $this->check->to_array(),
];
}
}
@@ -0,0 +1,19 @@
<?php
/**
* Main functions to start Fraud & Risk tools class.
*
* @package WooCommerce\Payments
*/
defined( 'ABSPATH' ) || exit;
/**
* Returns the main instance of Fraud and Risk tools.
*
* @return WCPay\Fraud_Prevention\Fraud_Risk_Tools
*/
function WC_Payments_Fraud_Risk_Tools() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
return WCPay\Fraud_Prevention\Fraud_Risk_Tools::instance();
}
add_action( 'plugins_loaded', 'WC_Payments_Fraud_Risk_Tools', 12 );