init
This commit is contained in:
+92
@@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
+177
@@ -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 );
|
||||
}
|
||||
}
|
||||
+398
@@ -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 ) ] );
|
||||
}
|
||||
}
|
||||
+267
@@ -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 & 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',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
+223
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
+154
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+19
@@ -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 );
|
||||
Reference in New Issue
Block a user