wikimedia/mediawiki-extensions-DonationInterface

View on GitHub
extras/custom_filters/custom_filters.body.php

Summary

Maintainability
A
50 mins
Test Coverage
<?php

use SmashPig\PaymentData\ValidationAction;

class Gateway_Extras_CustomFilters extends FraudFilter {

    // filter list to run on adapter construction
    const PHASE_INITIAL = 'GatewayInitialFilter';

    // filter list to run before making processor API calls
    const PHASE_VALIDATE = 'GatewayCustomFilter';

    /**
     * A value for tracking the 'riskiness' of a transaction
     *
     * The action to take based on a transaction's riskScore is determined by
     * $action_ranges.  This is built assuming a range of possible risk scores
     * as 0-100, although you can probably bend this as needed.
     * Due to the increased complexity introduced by custom filters, $risk_score
     * will now be represented as an array of scores, with the name of the
     * score's source in the keys, to promote our ability to tell what the heck
     * is going on.
     * @var array
     */
    private $risk_score;

    /**
     * Define the action to take for a given $risk_score
     * @var array
     */
    protected $action_ranges;

    /**
     * A container for an instance of self
     * @var self
     */
    protected static $instance;

    protected function __construct( GatewayType $gateway_adapter ) {
        parent::__construct( $gateway_adapter ); // gateway_adapter is set in there.

        // load user action ranges and risk score
        $this->action_ranges = $this->gateway_adapter->getGlobal( 'CustomFiltersActionRanges' );
        $this->risk_score = WmfFramework::getSessionValue( 'risk_scores' );
        if ( !$this->risk_score ) {
            $this->risk_score = [];
        } else {
            $unnecessarily_escaped_session_contents = addslashes( json_encode( $this->risk_score ) );
            $this->fraud_logger->info( '"Loaded from session" ' . $unnecessarily_escaped_session_contents );
        }
        $this->risk_score['initial'] = $this->gateway_adapter->getGlobal( 'CustomFiltersRiskScore' );
    }

    /**
     * Determine the action to take for a transaction based on its $risk_score
     *
     * @return string The action to take
     */
    protected function determineAction() {
        $risk_score = $this->getRiskScore();
        // possible risk scores are between 0 and 100
        if ( $risk_score < 0 ) {
            $risk_score = 0;
        }
        if ( $risk_score > 100 ) {
            $risk_score = 100;
        }
        foreach ( $this->action_ranges as $action => $range ) {
            if ( $risk_score >= $range[0] && $risk_score <= $range[1] ) {
                return $action;
            }
        }
    }

    /**
     * Add a component to the array of risk scores
     * @param float $score Score calculated by the indicated filter
     * @param string $source Name of the risk filter
     */
    public function addRiskScore( $score, $source ) {
        if ( !is_numeric( $score ) ) {
            throw new InvalidArgumentException( __FUNCTION__ . " Cannot add $score to risk score (not numeric). Source: $source" );
        }
        if ( !is_array( $this->risk_score ) ) {
            if ( is_numeric( $this->risk_score ) ) {
                // @phan-suppress-next-line PhanTypeMismatchDimAssignment
                $this->risk_score['unknown'] = (int)$this->risk_score;
            } else {
                $this->risk_score = [];
            }
        }

        $log_message = "\"$source added a score of $score\"";
        $this->fraud_logger->info( '"addRiskScore" ' . $log_message );
        $this->risk_score[$source] = $score;
    }

    /**
     * Add up the risk scores in an array, by default $this->risk_score
     * @param array|null $scoreArray
     * @return float total risk score
     */
    public function getRiskScore( $scoreArray = null ) {
        if ( $scoreArray === null ) {
            $scoreArray = $this->risk_score;
        }

        if ( is_numeric( $scoreArray ) ) {
            return $scoreArray;
        } elseif ( is_array( $scoreArray ) ) {
            $total = 0;
            foreach ( $scoreArray as $score ) {
                $total += $score;
            }
            return $total;

        } else {
            // TODO: We should catch this during setRiskScore.
            throw new InvalidArgumentException(
                __FUNCTION__ . " risk_score is neither numeric, nor an array."
                . print_r( $scoreArray, true )
            );
        }
    }

    /**
     * Run the transaction through the custom filters
     * @param string $phase Run custom filters attached for this phase
     * @return bool
     */
    protected function validate( $phase ) {
        $this->runFilters( $phase );
        $score = $this->getRiskScore();
        $this->gateway_adapter->setRiskScore( $score );
        $localAction = $this->determineAction();
        $this->gateway_adapter->setValidationAction( $localAction );

        $user_ip = $this->gateway_adapter->getData_Unstaged_Escaped( 'user_ip' );
        $log_message = '"' . $localAction . '" "' . $score . '" ' . $user_ip;

        $this->fraud_logger->info( '"Filtered" ' . $log_message );

        $log_message = '"' . addslashes( json_encode( $this->risk_score ) ) . '"';
        $this->fraud_logger->info( '"CustomFiltersScores" ' . $log_message );

        $utm = [
            'utm_campaign' => $this->gateway_adapter->getData_Unstaged_Escaped( 'utm_campaign' ),
            'utm_medium' => $this->gateway_adapter->getData_Unstaged_Escaped( 'utm_medium' ),
            'utm_source' => $this->gateway_adapter->getData_Unstaged_Escaped( 'utm_source' ),
            'utm_key' => $this->gateway_adapter->getData_Unstaged_Escaped( 'utm_key' ),
        ];
        $log_message = '"' . addslashes( json_encode( $utm ) ) . '"';
        $this->fraud_logger->info( '"utm" ' . $log_message );

        $this->sendAntifraudMessage( $localAction, $score, $this->risk_score );
        if ( $localAction !== ValidationAction::PROCESS ) {
            // Resets the ip velocity filter to ensure multiple suspicious attempts don't get a free pass.
            WmfFramework::setSessionValue( Gateway_Extras_CustomFilters_IP_Velocity::RAN_INITIAL, false );
        }
        // Always keep the stored scores up to date
        WmfFramework::setSessionValue( 'risk_scores', $this->risk_score );

        return true;
    }

    public static function onValidate( GatewayType $gateway_adapter ) {
        if ( !$gateway_adapter->getGlobal( 'EnableCustomFilters' ) ) {
            return true;
        }
        $gateway_adapter->debugarray[] = 'custom filters onValidate!';
        return self::singleton( $gateway_adapter )->validate( self::PHASE_VALIDATE );
    }

    public static function onGatewayReady( GatewayType $gateway_adapter ) {
        if ( !$gateway_adapter->getGlobal( 'EnableCustomFilters' ) ) {
            return true;
        }
        $gateway_adapter->debugarray[] = 'custom filters onGatewayReady!';
        return self::singleton( $gateway_adapter )->validate( self::PHASE_INITIAL );
    }

    public static function singleton( GatewayType $gateway_adapter ) {
        if ( !self::$instance ) {
            self::$instance = new self( $gateway_adapter );
        }
        return self::$instance;
    }

    /**
     * Gets the action calculated on the last filter run. If there are no
     * risk scores stored in session, throws a RuntimeException. Even if
     * all filters are disabled, we should have stored 'initial' => 0.
     *
     * @param GatewayType $gateway_adapter
     * @return string
     */
    public static function determineStoredAction( GatewayType $gateway_adapter ) {
        if (
            !WmfFramework::getSessionValue( 'risk_scores' )
        ) {
            throw new RuntimeException( 'No stored risk scores' );
        }
        return self::singleton( $gateway_adapter )->determineAction();
    }

    protected function runFilters( $phase ) {
        switch ( $phase ) {
            case self::PHASE_INITIAL:
                Gateway_Extras_CustomFilters_Referrer::onInitialFilter( $this->gateway_adapter, $this );
                Gateway_Extras_CustomFilters_Source::onInitialFilter( $this->gateway_adapter, $this );
                Gateway_Extras_CustomFilters_Functions::onInitialFilter( $this->gateway_adapter, $this );
                Gateway_Extras_CustomFilters_IP_Velocity::onInitialFilter( $this->gateway_adapter, $this );
                break;
            case self::PHASE_VALIDATE:
                Gateway_Extras_CustomFilters_Functions::onFilter( $this->gateway_adapter, $this );
                Gateway_Extras_CustomFilters_MinFraud::onFilter( $this->gateway_adapter, $this );
                Gateway_Extras_CustomFilters_IP_Velocity::onFilter( $this->gateway_adapter, $this );
                break;
        }
    }
}