diff --git a/DonationInterface.php b/DonationInterface.php index b77d5949..7ee05f40 100644 --- a/DonationInterface.php +++ b/DonationInterface.php @@ -1,1047 +1,1024 @@ 'Donation Interface', 'author' => array( 'Elliott Eggleston', 'Katie Horn', 'Ryan Kaldari' , 'Arthur Richards', 'Sherah Smith', 'Matt Walker', 'Adam Wight', 'Peter Gehres', 'Jeremy Postlethwaite' ), 'version' => '2.1.0', 'descriptionmsg' => 'donationinterface-desc', 'url' => 'https://www.mediawiki.org/wiki/Extension:DonationInterface', ); // Test mode (not for production!) // Set it if not defined if ( !isset( $wgDonationInterfaceTestMode) || $wgDonationInterfaceTestMode !== true ) { $wgDonationInterfaceTestMode = false; } /** * CLASSES */ $wgAutoloadClasses['CurrencyRates'] = __DIR__ . '/gateway_common/CurrencyRates.php'; $wgAutoloadClasses['CurrencyRatesModule'] = __DIR__ . '/modules/CurrencyRatesModule.php'; $wgAutoloadClasses['CyclicalArray'] = __DIR__ . '/globalcollect_gateway/CyclicalArray.php'; $wgAutoloadClasses['DonationData'] = __DIR__ . '/gateway_common/DonationData.php'; $wgAutoloadClasses['DonationLoggerFactory'] = __DIR__ . '/gateway_common/DonationLoggerFactory.php'; $wgAutoloadClasses['DonationLogProcessor'] = __DIR__ . '/gateway_common/DonationLogProcessor.php'; $wgAutoloadClasses['DonationQueue'] = __DIR__ . '/gateway_common/DonationQueue.php'; $wgAutoloadClasses['EncodingMangler'] = __DIR__ . '/gateway_common/EncodingMangler.php'; $wgAutoloadClasses['FinalStatus'] = __DIR__ . '/gateway_common/FinalStatus.php'; $wgAutoloadClasses['GatewayAdapter'] = __DIR__ . '/gateway_common/gateway.adapter.php'; $wgAutoloadClasses['GatewayPage'] = __DIR__ . '/gateway_common/GatewayPage.php'; $wgAutoloadClasses['GatewayType'] = __DIR__ . '/gateway_common/gateway.adapter.php'; $wgAutoloadClasses['DataValidator'] = __DIR__ . '/gateway_common/DataValidator.php'; $wgAutoloadClasses['LogPrefixProvider'] = __DIR__ . '/gateway_common/gateway.adapter.php'; $wgAutoloadClasses['MessageUtils'] = __DIR__ . '/gateway_common/MessageUtils.php'; $wgAutoloadClasses['NationalCurrencies'] = __DIR__ . '/gateway_common/NationalCurrencies.php'; $wgAutoloadClasses['PaymentMethod'] = __DIR__ . '/gateway_common/PaymentMethod.php'; $wgAutoloadClasses['PaymentResult'] = __DIR__ . '/gateway_common/PaymentResult.php'; $wgAutoloadClasses['PaymentTransactionResponse'] = __DIR__ . '/gateway_common/PaymentTransactionResponse.php'; $wgAutoloadClasses['ResponseCodes'] = __DIR__ . '/gateway_common/ResponseCodes.php'; $wgAutoloadClasses['ResponseProcessingException'] = __DIR__ . '/gateway_common/ResponseProcessingException.php'; $wgAutoloadClasses['WmfFramework_Mediawiki'] = __DIR__ . '/gateway_common/WmfFramework.mediawiki.php'; $wgAutoloadClasses['WmfFrameworkLogHandler'] = __DIR__ . '/gateway_common/WmfFrameworkLogHandler.php'; //load all possible form classes $wgAutoloadClasses['Gateway_Form'] = __DIR__ . '/gateway_forms/Form.php'; $wgAutoloadClasses['Gateway_Form_Mustache'] = __DIR__ . '/gateway_forms/Mustache.php'; $wgAutoloadClasses['Gateway_Form_RapidHtml'] = __DIR__ . '/gateway_forms/RapidHtml.php'; $wgAutoloadClasses['CountryCodes'] = __DIR__ . '/gateway_forms/includes/CountryCodes.php'; $wgAutoloadClasses['ProvinceAbbreviations'] = __DIR__ . '/gateway_forms/includes/ProvinceAbbreviations.php'; $wgAutoloadClasses['StateAbbreviations'] = __DIR__ . '/gateway_forms/includes/StateAbbreviations.php'; //GlobalCollect gateway classes $wgAutoloadClasses['GlobalCollectGateway'] = __DIR__ . '/globalcollect_gateway/globalcollect_gateway.body.php'; $wgAutoloadClasses['GlobalCollectGatewayResult'] = __DIR__ . '/globalcollect_gateway/globalcollect_resultswitcher.body.php'; $wgAutoloadClasses['GlobalCollectAdapter'] = __DIR__ . '/globalcollect_gateway/globalcollect.adapter.php'; $wgAutoloadClasses['GlobalCollectOrphanAdapter'] = __DIR__ . '/globalcollect_gateway/scripts/orphan_adapter.php'; $wgAutoloadClasses['GlobalCollectOrphanRectifier'] = __DIR__ . '/globalcollect_gateway/scripts/orphans.php'; // Amazon $wgAutoloadClasses['AmazonGateway'] = __DIR__ . '/amazon_gateway/amazon_gateway.body.php'; $wgAutoloadClasses['AmazonAdapter'] = __DIR__ . '/amazon_gateway/amazon.adapter.php'; //Adyen $wgAutoloadClasses['AdyenGateway'] = __DIR__ . '/adyen_gateway/adyen_gateway.body.php'; $wgAutoloadClasses['AdyenGatewayResult'] = __DIR__ . '/adyen_gateway/adyen_resultswitcher.body.php'; $wgAutoloadClasses['AdyenAdapter'] = __DIR__ . '/adyen_gateway/adyen.adapter.php'; // Astropay $wgAutoloadClasses['AstropayGateway'] = __DIR__ . '/astropay_gateway/astropay_gateway.body.php'; $wgAutoloadClasses['AstropayGatewayResult'] = __DIR__ . '/astropay_gateway/astropay_resultswitcher.body.php'; $wgAutoloadClasses['AstropayAdapter'] = __DIR__ . '/astropay_gateway/astropay.adapter.php'; // Paypal $wgAutoloadClasses['PaypalGateway'] = __DIR__ . '/paypal_gateway/paypal_gateway.body.php'; $wgAutoloadClasses['PaypalGatewayResult'] = __DIR__ . '/paypal_gateway/paypal_resultswitcher.body.php'; $wgAutoloadClasses['PaypalAdapter'] = __DIR__ . '/paypal_gateway/paypal.adapter.php'; // Worldpay $wgAutoloadClasses['WorldpayGateway'] = __DIR__ . '/worldpay_gateway/worldpay_gateway.body.php'; $wgAutoloadClasses['WorldpayAdapter'] = __DIR__ . '/worldpay_gateway/worldpay.adapter.php'; $wgAPIModules['di_wp_validate'] = 'WorldpayValidateApi'; $wgAutoloadClasses['WorldpayValidateApi'] = __DIR__ . '/worldpay_gateway/worldpay.api.php'; //Extras classes - required for ANY optional class that is considered an "extra". $wgAutoloadClasses['Gateway_Extras'] = __DIR__ . '/extras/extras.body.php'; //Custom Filters classes $wgAutoloadClasses['Gateway_Extras_CustomFilters'] = __DIR__ . '/extras/custom_filters/custom_filters.body.php'; //Conversion Log classes $wgAutoloadClasses['Gateway_Extras_ConversionLog'] = __DIR__ . '/extras/conversion_log/conversion_log.body.php'; $wgAutoloadClasses['Gateway_Extras_CustomFilters_MinFraud'] = __DIR__ . '/extras/custom_filters/filters/minfraud/minfraud.body.php'; $wgAutoloadClasses['Gateway_Extras_CustomFilters_Referrer'] = __DIR__ . '/extras/custom_filters/filters/referrer/referrer.body.php'; $wgAutoloadClasses['Gateway_Extras_CustomFilters_Source'] = __DIR__ . '/extras/custom_filters/filters/source/source.body.php'; $wgAutoloadClasses['Gateway_Extras_CustomFilters_Functions'] = __DIR__ . '/extras/custom_filters/filters/functions/functions.body.php'; $wgAutoloadClasses['Gateway_Extras_CustomFilters_IP_Velocity'] = __DIR__ . '/extras/custom_filters/filters/ip_velocity/ip_velocity.body.php'; $wgAutoloadClasses['Gateway_Extras_SessionVelocityFilter'] = __DIR__ . '/extras/session_velocity/session_velocity.body.php'; $wgAutoloadClasses['GatewayFormChooser'] = __DIR__ . '/special/GatewayFormChooser.php'; $wgAutoloadClasses['SystemStatus'] = __DIR__ . '/special/SystemStatus.php'; /** * GLOBALS */ /** * Global form dir */ $wgDonationInterfaceHtmlFormDir = __DIR__ . '/gateway_forms/rapidhtml/html'; $wgDonationInterfaceTest = false; /** * Default top-level template file. */ $wgDonationInterfaceTemplate = __DIR__ . '/gateway_forms/mustache/index.html.mustache'; /** * Title to transclude in form template as {{{ appeal_text }}}. * $appeal and $language will be substituted before transclusion */ $wgDonationInterfaceAppealWikiTemplate = 'LanguageSwitch|2011FR/$appeal/text|$language'; // Email address used when donor enters nothing $wgDonationInterfaceDefaultEmail = 'nobody@wikimedia.org'; //all of the following variables make sense to override directly, //or change "DonationInterface" to the gateway's id to override just for that gateway. //for instance: To override $wgDonationInterfaceUseSyslog just for GlobalCollect, add // $wgGlobalCollectGatewayUseSyslog = true // to LocalSettings. // $wgDonationInterfaceDisplayDebug = false; $wgDonationInterfaceUseSyslog = false; $wgDonationInterfaceSaveCommStats = false; $wgDonationInterfaceCSSVersion = 1; $wgDonationInterfaceTimeout = 5; $wgDonationInterfaceDefaultForm = 'RapidHtml'; /** * If set to a currency code, gateway forms will try to convert amounts * in unsupported currencies to the fallback instead of just showing * an unsupported currency error. */ $wgDonationInterfaceFallbackCurrency = false; /** * When this is true and an unsupported currency has been converted to the * fallback (see above), we show an interstitial page notifying the user * of the conversion before sending the donation to the gateway. */ $wgDonationInterfaceNotifyOnConvert = true; /** * A string or array of strings for making tokens more secure * * Please set this! If you do not, tokens are easy to get around, which can * potentially leave you and your users vulnerable to CSRF or other forms of * attack. */ $wgDonationInterfaceSalt = $wgSecretKey; /** * A string that can contain wikitext to display at the head of the credit card form * * This string gets run like so: $wg->addHtml( $wg->Parse( $wgGlobalCollectGatewayHeader )) * You can use '@language' as a placeholder token to extract the user's language. * */ $wgDonationInterfaceHeader = NULL; /** * A string containing full URL for Javascript-disabled credit card form redirect */ $wgDonationInterfaceNoScriptRedirect = null; /** * Configure price ceiling and floor for valid contribution amount. Values * should be in USD. */ $wgDonationInterfacePriceFloor = 1.00; $wgDonationInterfacePriceCeiling = 10000.00; /** * Default Thank You and Fail pages for all of donationinterface - language will be calc'd and appended at runtime. */ //$wgDonationInterfaceThankYouPage = 'https://wikimediafoundation.org/wiki/Thank_You'; $wgDonationInterfaceThankYouPage = 'Donate-thanks'; $wgDonationInterfaceFailPage = 'Donate-error'; /** * Retry Loop Count - If there's a place where the API can choose to loop on some retry behavior, do it this number of times. */ $wgDonationInterfaceRetryLoopCount = 3; /** * Orphan Cron settings global */ $wgDonationInterfaceOrphanCron = array( 'enable' => true, 'target_execute_time' => 300, 'max_per_execute' => '', ); /** * Forbidden countries. No donations will be allowed to come in from countries * in this list. * All should be represented as all-caps ISO 3166-1 alpha-2 * This one global shouldn't ever be overridden per gateway. As it's probably * going to only conatin countries forbidden by law, there's no reason * to override by gateway and as such it's always referenced directly. */ $wgDonationInterfaceForbiddenCountries = array(); /** * 3D Secure enabled currencies (and countries) for Credit Card. * An array in the form of currency => array of countries * (all-caps ISO 3166-1 alpha-2), or an empty array for all transactions in that * currency regardless of country of origin. * As this is a mandatroy check for all INR transactions, that rule made it into * the default. */ $wgDonationInterface3DSRules = array( 'INR' => array(), //all countries ); //GlobalCollect gateway globals $wgGlobalCollectGatewayURL = 'https://ps.gcsip.nl/wdl/wdl'; $wgGlobalCollectGatewayTestingURL = 'https://'; // GlobalCollect testing URL # $wgGlobalCollectGatewayAccountInfo['example'] = array( # 'MerchantID' => '', // GlobalCollect ID # ); $wgGlobalCollectGatewayHtmlFormDir = __DIR__ . '/globalcollect_gateway/forms/html'; $wgGlobalCollectGatewayCvvMap = array( 'M' => true, //CVV check performed and valid value. 'N' => false, //CVV checked and no match. 'P' => true, //CVV check not performed, not requested 'S' => false, //Card holder claims no CVV-code on card, issuer states CVV-code should be on card. 'U' => true, //? //Issuer not certified for CVV2. 'Y' => false, //Server provider did not respond. '0' => true, //No service available. '' => false, //No code returned. All the points. ); $wgGlobalCollectGatewayAvsMap = array( 'A' => 50, //Address (Street) matches, Zip does not. 'B' => 50, //Street address match for international transactions. Postal code not verified due to incompatible formats. 'C' => 50, //Street address and postal code not verified for international transaction due to incompatible formats. 'D' => 0, //Street address and postal codes match for international transaction. 'E' => 100, //AVS Error. 'F' => 0, //Address does match and five digit ZIP code does match (UK only). 'G' => 50, //Address information is unavailable; international transaction; non-AVS participant. 'I' => 50, //Address information not verified for international transaction. 'M' => 0, //Street address and postal codes match for international transaction. 'N' => 100, //No Match on Address (Street) or Zip. 'P' => 50, //Postal codes match for international transaction. Street address not verified due to incompatible formats. 'R' => 100, //Retry, System unavailable or Timed out. 'S' => 50, //Service not supported by issuer. 'U' => 50, //Address information is unavailable. 'W' => 50, //9 digit Zip matches, Address (Street) does not. 'X' => 0, //Exact AVS Match. 'Y' => 0, //Address (Street) and 5 digit Zip match. 'Z' => 50, //5 digit Zip matches, Address (Street) does not. '0' => 25, //No service available. '' => 100, //No code returned. All the points. ); //n.b. "-Testing-" urls are not wired to anything, they're just here for // your copy n paste pleasure. $wgAmazonGatewayURL = "https://authorize.payments.amazon.com/pba/paypipeline"; $wgAmazonGatewayTestingURL = "https://authorize.payments-sandbox.amazon.com/pba/paypipeline"; $wgAmazonGatewayFpsURL = "https://fps.amazonaws.com/"; $wgAmazonGatewayFpsTestingURL = "https://fps.sandbox.amazonaws.com/"; # $wgAmazonGatewayAccountInfo['example'] = array( # 'AccessKey' => "", # 'SecretKey' => "", # # // the long one, not the AWS account ID # 'PaymentsAccountID' => "", # ); // e.g. http://payments.wikimedia.org/index.php/Special:AmazonGateway -- // does NOT accept unroutable development names, use the number instead // even if it's 127.0.0.1 $wgAmazonGatewayReturnURL = ""; $wgAmazonGatewayHtmlFormDir = __DIR__ . '/amazon_gateway/forms/html'; $wgPaypalGatewayURL = 'https://www.paypal.com/cgi-bin/webscr'; $wgPaypalGatewayTestingURL = 'https://www.sandbox.paypal.com/cgi-bin/webscr'; $wgPaypalGatewayReturnURL = ''; //'http://127.0.0.1/index.php/Special:PaypalGatewayResult'; $wgPaypalGatewayRecurringLength = '0'; // 0 should mean forever $wgPaypalGatewayHtmlFormDir = __DIR__ . '/paypal_gateway/forms/html'; $wgPaypalGatewayXclickCountries = array(); # $wgPaypalGatewayAccountInfo['example'] = array( # 'AccountEmail' => "", # ); $wgAdyenGatewayHtmlFormDir = __DIR__ . '/adyen_gateway/forms/html'; $wgAdyenGatewayBaseURL = 'https://live.adyen.com'; $wgAdyenGatewayBaseTestingURL = 'https://test.adyen.com'; // unused # $wgAdyenGatewayAccountInfo['example'] = array( # 'AccountName' => ''; // account identifier, not login name # 'SharedSecret' => ''; // entered in the skin editor # 'SkinCode' => ''; # ); $wgAstropayGatewayHtmlFormDir = __DIR__ . '/astropay_gateway/forms/html'; // Set base URLs here. Individual transactions have their own paths $wgAstropayGatewayURL = 'https://astropaycard.com/'; $wgAstropayGatewayTestingURL = 'https://sandbox.astropaycard.com/'; # $wgAstropayGatewayAccountInfo['example'] = array( # 'Create' => array( // For creating invoices # 'Login' => '', # 'Password' => '', # ), # 'Status' => array( // For checking payment status # 'Login' => '', # 'Password' => '', # ), # 'SecretKey' => '', // For signing requests and verifying responses # ); $wgWorldpayGatewayHtmlFormDir = __DIR__ . '/worldpay_gateway/forms/html'; $wgWorldpayGatewayURL = 'https://some.url.here'; /** * Set this to true if fraud checks should be disabled for integration testing */ $wgWorldpayGatewayNoFraudIntegrationTest = false; /* $wgWorldpayGatewayAccountInfo['default'] = array( 'Test' => 1, 'MerchantId' => 00000, 'Username' => 'suchuser', 'Password' => 'suchsecret', 'DefaultCurrency' => CURRENCY 'StoreIDs' => array( CURRENCY => StoreID ), ); */ $wgWorldpayGatewayCvvMap = array ( '0' => false, //No Match '1' => true, //Match '2' => false, //Not Checked '3' => false, //Issuer is Not Certified or Unregistered '4' => false, //Should have CVV2 on card - ?? '5' => false, //CVC1 Incorrect '6' => false, //No service available. '7' => false, //No code returned. All the points. '8' => false, //No code returned. All the points. '9' => false, //Not Performed //(occurs when CVN value was not present in the STN string //or when transaction was not sent to the acquiring bank) '' => false, //No code returned. All the points. ); $wgWorldpayGatewayAvsAddressMap = array ( '0' => 50, //No Match '1' => 0, //Match '2' => 12, //Not Checked/Not Available '3' => 50, //Issuer is Not Certified or Unregistered '4' => 12, //Not Supported '9' => 12, //Not Performed (occurs when Address1, Address2 and Address3 values were not present in the STN string or when transaction was not sent to the acquiring bank) '' => 50, //No code returned. All the points. ); $wgWorldpayGatewayAvsZipMap = array ( '0' => 50, //No Match '1' => 0, //Match '2' => 12, //Not Checked/Not Available '3' => 0, //9 digit zipcode match '4' => 0, //5 digit zipcode match '5' => 12, //Not Supported '9' => 12, //Not Performed (occurs when ZipCode value was not present in the STN string or when transaction was not sent to the acquiring bank) '' => 50, //No code returned. All the points. ); -$wgStompServer = ""; - -// In this array, 'default', 'pending', and 'limbo' are required keys for those categories of -// transactions. The value is the name of the queue. To single out a transaction type, ie: -// credit cards, prepend 'cc-' to the base key name. -// -// If the resultant queue name evaluates to false, the message will not be queued on the server. -$wgStompQueueNames = array( - 'default' => 'test-default', // Previously known as $wgStompQueueName - 'pending' => 'test-pending', // Previously known as $wgPendingStompQueueName - 'limbo' => 'test-limbo', // Previously known as $wgLimboStompQueueName - 'payments-antifraud' => 'payments-antifraud', //noncritical: Basically shoving the fraud log into a database. - 'payments-init' => 'payments-init', //noncritical: same as above with the payments-initial log -); - /** * @global array $wgDonationInterfaceDefaultQueueServer * * Common development defaults for the queue server. * TODO: Default to a builtin backend such as PDO? * FIXME: Note that this must be an instance of FifoQueueStore. */ $wgDonationInterfaceDefaultQueueServer = array( 'type' => 'PHPQueue\Backend\Stomp', 'uri' => 'tcp://localhost:61613', 'read_timeout' => '1', 'expiry' => '30 days', ); /** * @global array $wgDonationInterfaceQueues * * This is a mapping from queue name to attributes. It's not necessary to * list queues here, but the built-in queues are listed for convenience. * * Default values are taken from $wgDonationInterfaceDefaultQueueServer, and * values given here will override the defaults. * * The array key is the queue name as it is referred to from code, although the * actual queue name used in the backend may be overridden, see below. * * Unrecognized options will be passed along to the queue backend constructor, * but the following have special meaning to DonationQueue: * type - Class name of the queue backend. * expiry - The default lifespan of messages in this queue (days). * name - Backend can map to a named queue, rather than default to the * queue key as it appears in the $wgDonationInterfaceQueues array. */ $wgDonationInterfaceQueues = array( // Incoming donations that we think have been paid for. 'completed' => array(), // So-called limbo queue for GlobalCollect, where we store donor personal // information while waiting for the donor to return from iframe or a // redirect. It's very important that this data is not stored anywhere // permanent such as logs or the database, until we know this person // finished making a donation. // FIXME: Note that this must be an instance of KeyValueStore. // // Example of a PCI-compliant queue configuration: // // 'globalcollect-cc-limbo' => array( // 'type' => 'PHPQueue\Backend\Predis', // # Note that servers cannot be an array, due to some incompatibility // # with aggregate connections. // 'servers' => 'tcp://payments1003.eqiad.net', // # 1 hour, in seconds // 'expiry' => 3600, // 'score_key' => 'date', // ), - // 'limbo' => ... + // + // Example of aliasing a queue + // + // 'globalcollect-cc-limbo' => array( + // # Point at the main CC limbo queue. + // 'queue' => 'cc-limbo', + // ), // Transactions still needing action before they are settled. // FIXME: who reads from this queue? 'pending' => array(), // Non-critical queues // These messages will be shoved into the fraud database (see // crm/modules/fredge). 'payments-antifraud' => array(), // These are shoved into the payments-initial database. 'payments-init' => array(), ); //Custom Filters globals //Define the action to take for a given $risk_score $wgDonationInterfaceCustomFiltersActionRanges = array( 'process' => array( 0, 100 ), 'review' => array( -1, -1 ), 'challenge' => array( -1, -1 ), 'reject' => array( -1, -1 ), ); /** * 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. */ $wgDonationInterfaceCustomFiltersRiskScore = 0; //Minfraud globals /** * Your minFraud license key. */ $wgMinFraudLicenseKey = ''; /** * Set the risk score ranges that will cause a particular 'action' * * The keys to the array are the 'actions' to be taken (eg 'process'). * The value for one of these keys is an array representing the lower * and upper bounds for that action. For instance, * $wgDonationInterfaceMinFraudActionRanges = array( * 'process' => array( 0, 100) * ... * ); * means that any transaction with a risk score greather than or equal * to 0 and less than or equal to 100 will be given the 'process' action. * * These are evauluated on a >= or <= basis. Please refer to minFraud * documentation for a thorough explanation of the 'riskScore'. */ $wgDonationInterfaceMinFraudActionRanges = array( 'process' => array( 0, 100 ), 'review' => array( -1, -1 ), 'challenge' => array( -1, -1 ), 'reject' => array( -1, -1 ) ); /** * This allows setting where to point the minFraud servers. * * As of February 21st, 2012 minfraud.maxmind.com will route to the east or * west server, depending on you location. * * minfraud-us-east.maxmind.com: 174.36.207.186 * minfraud-us-west.maxmind.com: 50.97.220.226 * * The minFraud API requires an array of servers. * * You do not have to specify a server. * * @see CreditCardFraudDetection::$server */ $wgDonationInterfaceMinFraudServers = array(); // Timeout in seconds for communicating with MaxMind $wgMinFraudTimeout = 2; /** * When to send an email to $wgEmergencyContact that we're * running low on minfraud queries. Will continue to send * once per day until the limit is once again over the limit. */ $wgDonationInterfaceMinFraudAlarmLimit = 25000; //Referrer Filter globals $wgDonationInterfaceCustomFiltersRefRules = array(); //Source Filter globals $wgDonationInterfaceCustomFiltersSrcRules = array(); //Functions Filter globals $wgDonationInterfaceCustomFiltersFunctions = array(); //IP velocity filter globals $wgDonationInterfaceMemcacheHost = 'localhost'; $wgDonationInterfaceMemcachePort = '11211'; $wgDonationInterfaceIPVelocityFailScore = 100; $wgDonationInterfaceIPVelocityTimeout = 60 * 5; //5 minutes in seconds $wgDonationInterfaceIPVelocityThreshhold = 3; //3 transactions per timeout //$wgDonationInterfaceIPVelocityToxicDuration can be set to penalize IP addresses //that attempt to use cards reported stolen. //$wgDonationInterfaceIPVelocityFailDuration is also something you can set... //If you leave it blank, it will use the VelocityTimeout as a default. // Session velocity filter globals $wgDonationInterfaceSessionVelocity_HitScore = 10; // How much to add to the score per API hit $wgDonationInterfaceSessionVelocity_DecayRate = 1; // Linear decay rate; pts / sec $wgDonationInterfaceSessionVelocity_Threshold = 50; // Above this score, we deny users the page /** * $wgDonationInterfaceCountryMap * * A score of 0 for a country means no risk. * A score of 100 means this country is extremely risky for fraud. * * The score for a country has the following range: * * 0 <= $score <= 100 * * To enable this filter add this to your LocalSettings.php: * * @code * 100, * ); * * $wgDonationInterfaceCountryMap = array( * 'CA' => 1, * 'US' => 5, * ); * ?> * @endcode */ $wgDonationInterfaceCountryMap = array(); /** * $wgDonationInterfaceEmailDomainMap * * A score of 0 for an email domain means no risk. * A score of 100 means this email domain is extremely risky for fraud. * Scores may be negative. * * To enable this filter add this to your LocalSettings.php: * * @code * 100, * ); * * $wgDonationInterfaceEmailDomainMap = array( * 'gmail.com' => 5, * 'wikimedia.org' => 0, * ); * ?> * @endcode */ $wgDonationInterfaceEmailDomainMap = array(); /** * $wgDonationInterfaceUtmCampaignMap * * A score of 0 for utm_campaign means no risk. * A score of 100 means this utm_campaign is extremely risky for fraud. * Scores may be negative * * To enable this filter add this to your LocalSettings.php: * * @code * 100, * ); * * $wgDonationInterfaceUtmCampaignMap = array( * '' => 20, * 'some-odd-string' => 100, * ); * ?> * @endcode */ $wgDonationInterfaceUtmCampaignMap = array(); /** * $wgDonationInterfaceUtmMediumMap * * A score of 0 for utm_medium means no risk. * A score of 100 means this utm_medium is extremely risky for fraud. * Scores may be negative * * To enable this filter add this to your LocalSettings.php: * * @code * 100, * ); * * $wgDonationInterfaceUtmMediumMap = array( * '' => 20, * 'some-odd-string' => 100, * ); * ?> * @endcode */ $wgDonationInterfaceUtmMediumMap = array(); /** * $wgDonationInterfaceUtmSourceMap * * A score of 0 for utm_source means no risk. * A score of 100 means this utm_source is extremely risky for fraud. * Scores may be negative * * To enable this filter add this to your LocalSettings.php: * * @code * 100, * ); * * $wgDonationInterfaceUtmSourceMap = array( * '' => 20, * 'some-odd-string' => 100, * ); * ?> * @endcode */ $wgDonationInterfaceUtmSourceMap = array(); -$wgDonationInterfaceEnableStomp = false; $wgDonationInterfaceEnableQueue = false; $wgDonationInterfaceEnableConversionLog = false; //this is definitely an Extra $wgDonationInterfaceEnableMinfraud = false; //this is definitely an Extra $wgGlobalCollectGatewayEnabled = false; $wgAmazonGatewayEnabled = false; $wgAdyenGatewayEnabled = false; $wgAstropayGatewayEnabled = false; $wgPaypalGatewayEnabled = false; $wgWorldpayGatewayEnabled = false; /** * @global boolean Set to false to disable all filters, or set a gateway- * specific value such as $wgPaypalGatewayEnableCustomFilters = false. */ $wgDonationInterfaceEnableCustomFilters = true; $wgDonationInterfaceEnableFormChooser = false; $wgDonationInterfaceEnableReferrerFilter = false; //extra $wgDonationInterfaceEnableSourceFilter = false; //extra $wgDonationInterfaceEnableFunctionsFilter = false; //extra $wgDonationInterfaceEnableIPVelocityFilter = false; //extra $wgDonationInterfaceEnableSessionVelocityFilter = false; //extra $wgDonationInterfaceEnableSystemStatus = false; //extra $wgSpecialPages['GatewayFormChooser'] = 'GatewayFormChooser'; $wgSpecialPages['SystemStatus'] = 'SystemStatus'; $wgSpecialPages['GlobalCollectGateway'] = 'GlobalCollectGateway'; $wgSpecialPages['GlobalCollectGatewayResult'] = 'GlobalCollectGatewayResult'; $wgDonationInterfaceGatewayAdapters[] = 'GlobalCollectAdapter'; $wgSpecialPages['AmazonGateway'] = 'AmazonGateway'; $wgDonationInterfaceGatewayAdapters[] = 'AmazonAdapter'; $wgSpecialPages['AdyenGateway'] = 'AdyenGateway'; $wgSpecialPages['AdyenGatewayResult'] = 'AdyenGatewayResult'; $wgDonationInterfaceGatewayAdapters[] = 'AdyenAdapter'; $wgSpecialPages['AstropayGateway'] = 'AstropayGateway'; $wgSpecialPages['AstropayGatewayResult'] = 'AstropayGatewayResult'; $wgDonationInterfaceGatewayAdapters[] = 'AstropayAdapter'; $wgSpecialPages['PaypalGateway'] = 'PaypalGateway'; $wgSpecialPages['PaypalGatewayResult'] = 'PaypalGatewayResult'; $wgDonationInterfaceGatewayAdapters[] = 'PaypalAdapter'; $wgSpecialPages['WorldpayGateway'] = 'WorldpayGateway'; $wgDonationInterfaceGatewayAdapters[] = 'WorldpayAdapter'; -//Stomp hooks -// FIXME: There's no point in using hooks any more, since we're switching -// behavior inside the callbacks and not via conditional hooking. -$wgHooks['ParserFirstCallInit'][] = 'efStompSetup'; -$wgHooks['gwStomp'][] = 'sendSTOMP'; -$wgHooks['gwPendingStomp'][] = 'sendPendingSTOMP'; -$wgHooks['gwFreeformStomp'][] = 'sendFreeformSTOMP'; - //Custom Filters hooks $wgHooks['GatewayValidate'][] = array( 'Gateway_Extras_CustomFilters::onValidate' ); $wgHooks['GatewayCustomFilter'][] = array( 'Gateway_Extras_CustomFilters_Referrer::onFilter' ); $wgHooks['GatewayCustomFilter'][] = array( 'Gateway_Extras_CustomFilters_Source::onFilter' ); $wgHooks['GatewayCustomFilter'][] = array( 'Gateway_Extras_CustomFilters_Functions::onFilter' ); $wgHooks['GatewayCustomFilter'][] = array( 'Gateway_Extras_CustomFilters_MinFraud::onFilter' ); $wgHooks['GatewayCustomFilter'][] = array( 'Gateway_Extras_CustomFilters_IP_Velocity::onFilter' ); $wgHooks['GatewayPostProcess'][] = array( 'Gateway_Extras_CustomFilters_IP_Velocity::onPostProcess' ); $wgHooks['DonationInterfaceCurlInit'][] = array( 'Gateway_Extras_SessionVelocityFilter::onCurlInit' ); //Conversion Log hooks // Sets the 'conversion log' as logger for post-processing $wgHooks['GatewayPostProcess'][] = array( 'Gateway_Extras_ConversionLog::onPostProcess' ); //Unit tests $wgHooks['UnitTestsList'][] = 'efDonationInterfaceUnitTests'; /** * APIS */ // enable the API $wgAPIModules['donate'] = 'DonationApi'; $wgAutoloadClasses['DonationApi'] = __DIR__ . '/gateway_common/donation.api.php'; /** * ADDITIONAL MAGICAL GLOBALS */ // Resource modules $wgResourceTemplate = array( 'localBasePath' => __DIR__ . '/modules', 'remoteExtPath' => 'DonationInterface/modules', ); $wgResourceModules['iframe.liberator'] = array( 'scripts' => 'iframe.liberator.js', 'position' => 'top' ) + $wgResourceTemplate; $wgResourceModules['donationInterface.skinOverride'] = array( 'scripts' => 'js/skinOverride.js', 'styles' => array( 'css/gateway.css', 'css/skinOverride.css', ), 'position' => 'top' ) + $wgResourceTemplate; $wgResourceModules['donationInterface.test.rapidhtml'] = array( 'scripts' => 'tests/modules/gc.testinterface.js', 'dependencies' => array( 'mediawiki.Uri', 'gc.normalinterface' ) ) + $wgResourceTemplate; $wgResourceModules['jquery.payment'] = array( 'scripts' => 'jquery.payment/jquery.payment.js', ) + $wgResourceTemplate; //Forms $wgResourceModules['ext.donationinterface.mustache.styles'] = array ( 'styles' => array( 'forms.css' ), 'localBasePath' => __DIR__ . '/gateway_forms/mustache', 'remoteExtPath' => 'DonationInterface/gateway_forms/mustache', 'position' => 'top', ); $wgResourceModules['ext.donationinterface.mustache.scripts'] = array ( 'scripts' => 'forms.js', 'dependencies' => 'di.form.core.validate', 'localBasePath' => __DIR__ . '/gateway_forms/mustache', 'remoteExtPath' => 'DonationInterface/gateway_forms/mustache' ); // load any rapidhtml related resources require_once( __DIR__ . '/gateway_forms/rapidhtml/RapidHtmlResources.php' ); $wgResourceTemplate = array( 'localBasePath' => __DIR__ . '/gateway_forms', 'remoteExtPath' => 'DonationInterface/gateway_forms', ); $wgResourceModules[ 'ext.donationInterface.errorMessages' ] = array( 'messages' => array( 'donate_interface-noscript-msg', 'donate_interface-noscript-redirect-msg', 'donate_interface-error-msg-general', 'donate_interface-error-msg', 'donate_interface-error-msg-js', 'donate_interface-error-msg-validation', 'donate_interface-error-msg-invalid-amount', 'donate_interface-error-msg-email', 'donate_interface-error-msg-card-num', 'donate_interface-error-msg-amex', 'donate_interface-error-msg-mc', 'donate_interface-error-msg-visa', 'donate_interface-error-msg-discover', 'donate_interface-error-msg-amount', 'donate_interface-error-msg-emailAdd', 'donate_interface-error-msg-fname', 'donate_interface-error-msg-lname', 'donate_interface-error-msg-street', 'donate_interface-error-msg-city', 'donate_interface-error-msg-state', 'donate_interface-error-msg-zip', 'donate_interface-error-msg-postal', 'donate_interface-error-msg-country', 'donate_interface-error-msg-card_type', 'donate_interface-error-msg-card_num', 'donate_interface-error-msg-expiration', 'donate_interface-error-msg-cvv', 'donate_interface-error-msg-fiscal_number', 'donate_interface-error-msg-captcha', 'donate_interface-error-msg-captcha-please', 'donate_interface-error-msg-cookies', 'donate_interface-error-msg-account_name', 'donate_interface-error-msg-account_number', 'donate_interface-error-msg-authorization_id', 'donate_interface-error-msg-bank_check_digit', 'donate_interface-error-msg-bank_code', 'donate_interface-error-msg-branch_code', 'donate_interface-smallamount-error', 'donate_interface-donor-fname', 'donate_interface-donor-lname', 'donate_interface-donor-street', 'donate_interface-donor-city', 'donate_interface-donor-state', 'donate_interface-donor-zip', 'donate_interface-donor-postal', 'donate_interface-donor-country', 'donate_interface-donor-emailAdd', 'donate_interface-state-province', 'donate_interface-cvv-explain', ) ); // minimum amounts for all currencies $wgResourceModules[ 'di.form.core.minimums' ] = array( 'class' => 'CurrencyRatesModule', ); // form validation resource $wgResourceModules[ 'di.form.core.validate' ] = array( 'scripts' => 'validate_input.js', 'dependencies' => array( 'di.form.core.minimums', 'ext.donationInterface.errorMessages' ), 'localBasePath' => __DIR__ . '/modules', 'remoteExtPath' => 'DonationInterface/modules' ); // Load the interface messages that are shared across multiple gateways $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/gateway_common/i18n/interface'; $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/gateway_common/i18n/country-specific'; $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/gateway_common/i18n/countries'; $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/gateway_common/i18n/us-states'; $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/gateway_common/i18n/canada-provinces'; $wgExtensionMessagesFiles['GatewayAliases'] = __DIR__ . '/DonationInterface.alias.php'; $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/amazon_gateway/i18n'; $wgExtensionMessagesFiles['AmazonGatewayAlias'] = __DIR__ . '/amazon_gateway/amazon_gateway.alias.php'; //GlobalCollect gateway magical globals // @todo All the bits where we make the i18n make sense for multiple gateways. This is clearly less than ideal. $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/globalcollect_gateway/i18n'; $wgExtensionMessagesFiles['GlobalCollectGatewayAlias'] = __DIR__ . '/globalcollect_gateway/globalcollect_gateway.alias.php'; $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/adyen_gateway/i18n'; $wgExtensionMessagesFiles['AdyenGatewayAlias'] = __DIR__ . '/adyen_gateway/adyen_gateway.alias.php'; $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/astropay_gateway/i18n'; $wgExtensionMessagesFiles['AstropayGatewayAlias'] = __DIR__ . '/astropay_gateway/astropay_gateway.alias.php'; $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/paypal_gateway/i18n'; $wgExtensionMessagesFiles['PaypalGatewayAlias'] = __DIR__ . '/paypal_gateway/paypal_gateway.alias.php'; $wgMessagesDirs['DonationInterface'][] = __DIR__ . '/worldpay_gateway/i18n'; $wgExtensionMessagesFiles['WorldpayGatewayAlias'] = __DIR__ . '/worldpay_gateway/worldpay_gateway.alias.php'; /** * See default values in DonationInterfaceFormSettings.php. Note that any values * set in LocalSettings.php are array_merged into the defaults, which allows you * to override specific forms. Please completely specify forms when overriding, * or disable by setting to an empty array or false. */ $wgDonationInterfaceAllowedHtmlForms = array(); /** * Base directories for each gateway's form templates. */ $wgDonationInterfaceFormDirs = array( 'adyen' => $wgAdyenGatewayHtmlFormDir, 'amazon' => $wgAmazonGatewayHtmlFormDir, 'default' => $wgDonationInterfaceHtmlFormDir, 'gc' => $wgGlobalCollectGatewayHtmlFormDir, 'paypal' => $wgPaypalGatewayHtmlFormDir, 'worldpay' => $wgWorldpayGatewayHtmlFormDir, ); // Load the default form settings. require_once __DIR__ . '/DonationInterfaceFormSettings.php'; /** * FUNCTIONS */ -//---Stomp functions--- -// TODO: Encapsulate in a class, or deprecate. -require_once( __DIR__ . '/activemq_stomp/activemq_stomp.php' ); -$wgAutoloadClasses['Stomp'] = __DIR__ . '/activemq_stomp/Stomp.php'; - function efDonationInterfaceUnitTests( &$files ) { global $wgAutoloadClasses; $testDir = __DIR__ . '/tests/'; $files[] = $testDir . 'AllTests.php'; $wgAutoloadClasses['DonationInterfaceTestCase'] = $testDir . 'DonationInterfaceTestCase.php'; $wgAutoloadClasses['TestingQueue'] = $testDir . 'includes/TestingQueue.php'; $wgAutoloadClasses['TestingAdyenAdapter'] = $testDir . 'includes/test_gateway/TestingAdyenAdapter.php'; $wgAutoloadClasses['TestingAmazonAdapter'] = $testDir . 'includes/test_gateway/TestingAmazonAdapter.php'; $wgAutoloadClasses['TestingAstropayAdapter'] = $testDir . 'includes/test_gateway/TestingAstropayAdapter.php'; $wgAutoloadClasses['TestingAmazonGateway'] = $testDir . 'includes/test_page/TestingAmazonGateway.php'; $wgAutoloadClasses['TestingDonationLogger'] = $testDir . 'includes/TestingDonationLogger.php'; $wgAutoloadClasses['TestingGatewayPage'] = $testDir . 'includes/TestingGatewayPage.php'; $wgAutoloadClasses['TestingGenericAdapter'] = $testDir . 'includes/test_gateway/TestingGenericAdapter.php'; $wgAutoloadClasses['TestingGlobalCollectAdapter'] = $testDir . 'includes/test_gateway/TestingGlobalCollectAdapter.php'; $wgAutoloadClasses['TestingGlobalCollectGateway'] = $testDir . 'includes/test_page/TestingGlobalCollectGateway.php'; $wgAutoloadClasses['TestingGlobalCollectOrphanAdapter'] = $testDir . 'includes/test_gateway/TestingGlobalCollectOrphanAdapter.php'; $wgAutoloadClasses['TestingPaypalAdapter'] = $testDir . 'includes/test_gateway/TestingPaypalAdapter.php'; $wgAutoloadClasses['TestingWorldpayAdapter'] = $testDir . 'includes/test_gateway/TestingWorldpayAdapter.php'; $wgAutoloadClasses['TestingWorldpayGateway'] = $testDir . 'includes/test_page/TestingWorldpayGateway.php'; $wgAutoloadClasses['TestingLanguage'] = $testDir . 'includes/test_language/test.language.php'; $wgAutoloadClasses['TestingRequest'] = $testDir . 'includes/test_request/test.request.php'; return true; } // Include composer's autoload if the vendor directory exists. If we have been // included via Composer, our dependencies should already be autoloaded at the // top level. // Note that in WMF's continuous integration, we can still only use stuff from // Composer if it is already in Mediawiki's vendor directory, such as monolog $vendorAutoload = __DIR__ . '/vendor/autoload.php'; if ( file_exists( $vendorAutoload ) ) { require_once ( $vendorAutoload ); } else { require_once ( 'gateway_common/WmfFramework.php' ); } diff --git a/activemq_stomp/Stomp.php b/activemq_stomp/Stomp.php deleted file mode 100644 index bc8c0fc2..00000000 --- a/activemq_stomp/Stomp.php +++ /dev/null @@ -1,606 +0,0 @@ - - * @author Dejan Bosanac - * @author Michael Caplan - * @version $Revision: 43 $ - */ -class Stomp -{ - /** - * Perform request synchronously - * - * @var boolean - */ - public $sync = false; - - /** - * Default prefetch size - * - * @var int - */ - public $prefetchSize = 1; - - /** - * Client id used for durable subscriptions - * - * @var string - */ - public $clientId = null; - - protected $_brokerUri = null; - protected $_socket = null; - protected $_hosts = array(); - protected $_params = array(); - protected $_subscriptions = array(); - protected $_defaultPort = 61613; - protected $_currentHost = - 1; - protected $_attempts = 10; - protected $_username = ''; - protected $_password = ''; - protected $_sessionId; - protected $_read_timeout_seconds = 60; - protected $_read_timeout_milliseconds = 0; - - /** - * Constructor - * - * @param string $brokerUri Broker URL - * @throws Stomp_Exception - */ - public function __construct ( $brokerUri ) - { - $this->_brokerUri = $brokerUri; - $this->_init(); - } - /** - * Initialize connection - * - * @throws Stomp_Exception - */ - protected function _init () - { - $pattern = "|^(([a-zA-Z]+)://)+\(*([a-zA-Z0-9\.:/i,-]+)\)*\??([a-zA-Z0-9=]*)$|i"; - if ( preg_match( $pattern, $this->_brokerUri, $regs ) ) { - $scheme = $regs[2]; - $hosts = $regs[3]; - $params = $regs[4]; - if ( $scheme != "failover" ) { - $this->_processUrl( $this->_brokerUri ); - } else { - $urls = explode( ",", $hosts ); - foreach ( $urls as $url ) { - $this->_processUrl( $url ); - } - } - if ( $params != null ) { - parse_str( $params, $this->_params ); - } - } else { - require_once 'Stomp/Exception.php'; - throw new Stomp_Exception( "Bad Broker URL {$this->_brokerUri}" ); - } - } - /** - * Process broker URL - * - * @param string $url Broker URL - * @throws Stomp_Exception - * @return boolean - */ - protected function _processUrl ( $url ) - { - $parsed = parse_url( $url ); - if ( $parsed ) { - $parsed['port'] = isset( $parsed['port'] ) ? $parsed['port'] : null; - array_push( $this->_hosts, array( $parsed['host'] , $parsed['port'] , $parsed['scheme'] ) ); - } else { - require_once 'Stomp/Exception.php'; - throw new Stomp_Exception( "Bad Broker URL $url" ); - } - } - /** - * Make socket connection to the server - * - * @throws Stomp_Exception - */ - protected function _makeConnection () - { - if ( count( $this->_hosts ) == 0 ) { - require_once 'Stomp/Exception.php'; - throw new Stomp_Exception( "No broker defined" ); - } - - // force disconnect, if previous established connection exists - $this->disconnect(); - - $i = $this->_currentHost; - $att = 0; - $connected = false; - while ( ! $connected && $att ++ < $this->_attempts ) { - if ( isset( $this->_params['randomize'] ) && $this->_params['randomize'] == 'true' ) { - $i = rand( 0, count( $this->_hosts ) - 1 ); - } else { - $i = ( $i + 1 ) % count( $this->_hosts ); - } - $broker = $this->_hosts[$i]; - $host = $broker[0]; - $port = $broker[1]; - $scheme = $broker[2]; - if ( $port == null ) { - $port = $this->_defaultPort; - } - if ( $this->_socket != null ) { - fclose( $this->_socket ); - $this->_socket = null; - } - $this->_socket = @fsockopen( $scheme . '://' . $host, $port ); - if ( !is_resource( $this->_socket ) && $att >= $this->_attempts && !array_key_exists( $i + 1, $this->_hosts ) ) { - require_once 'Stomp/Exception.php'; - throw new Stomp_Exception( "Could not connect to $host:$port ($att/{$this->_attempts})" ); - } elseif ( is_resource( $this->_socket ) ) { - $connected = true; - $this->_currentHost = $i; - break; - } - } - if ( ! $connected ) { - require_once 'Stomp/Exception.php'; - throw new Stomp_Exception( "Could not connect to a broker" ); - } - } - /** - * Connect to server - * - * @param string $username - * @param string $password - * @return boolean - * @throws Stomp_Exception - */ - public function connect ( $username = '', $password = '' ) - { - $this->_makeConnection(); - if ( $username != '' ) { - $this->_username = $username; - } - if ( $password != '' ) { - $this->_password = $password; - } - $headers = array( 'login' => $this->_username , 'passcode' => $this->_password ); - if ( $this->clientId != null ) { - $headers["client-id"] = $this->clientId; - } - $frame = new Stomp_Frame( "CONNECT", $headers ); - $this->_writeFrame( $frame ); - $frame = $this->readFrame(); - if ( $frame instanceof Stomp_Frame && $frame->command == 'CONNECTED' ) { - $this->_sessionId = $frame->headers["session"]; - return true; - } else { - require_once 'Stomp/Exception.php'; - if ( $frame instanceof Stomp_Frame ) { - throw new Stomp_Exception( "Unexpected command: {$frame->command}", 0, $frame->body ); - } else { - throw new Stomp_Exception( "Connection not acknowledged" ); - } - } - } - - /** - * Check if client session has ben established - * - * @return boolean - */ - public function isConnected () - { - return !empty( $this->_sessionId ) && is_resource( $this->_socket ); - } - /** - * Current stomp session ID - * - * @return string - */ - public function getSessionId() - { - return $this->_sessionId; - } - /** - * Send a message to a destination in the messaging system - * - * @param string $destination Destination queue - * @param string|Stomp_Frame $msg Message - * @param array $properties - * @param boolean $sync Perform request synchronously - * @return boolean - */ - public function send ( $destination, $msg, $properties = null, $sync = null ) - { - if ( $msg instanceof Stomp_Frame ) { - $msg->headers['destination'] = $destination; - $msg->headers = array_merge( $msg->headers, $properties ); - $frame = $msg; - } else { - $headers = $properties; - $headers['destination'] = $destination; - $frame = new Stomp_Frame( 'SEND', $headers, $msg ); - } - $this->_prepareReceipt( $frame, $sync ); - $this->_writeFrame( $frame ); - return $this->_waitForReceipt( $frame, $sync ); - } - /** - * Prepair frame receipt - * - * @param Stomp_Frame $frame - * @param boolean $sync - */ - protected function _prepareReceipt ( Stomp_Frame $frame, $sync ) - { - $receive = $this->sync; - if ( $sync !== null ) { - $receive = $sync; - } - if ( $receive == true ) { - $frame->headers['receipt'] = md5( microtime() ); - } - } - /** - * Wait for receipt - * - * @param Stomp_Frame $frame - * @param boolean $sync - * @return boolean - * @throws Stomp_Exception - */ - protected function _waitForReceipt ( Stomp_Frame $frame, $sync ) - { - - $receive = $this->sync; - if ( $sync !== null ) { - $receive = $sync; - } - if ( $receive == true ) { - $id = ( isset( $frame->headers['receipt'] ) ) ? $frame->headers['receipt'] : null; - if ( $id == null ) { - return true; - } - $frame = $this->readFrame(); - if ( $frame instanceof Stomp_Frame && $frame->command == 'RECEIPT' ) { - if ( $frame->headers['receipt-id'] == $id ) { - return true; - } else { - require_once 'Stomp/Exception.php'; - throw new Stomp_Exception( "Unexpected receipt id {$frame->headers['receipt-id']}", 0, $frame->body ); - } - } else { - require_once 'Stomp/Exception.php'; - if ( $frame instanceof Stomp_Frame ) { - throw new Stomp_Exception( "Unexpected command {$frame->command}", 0, $frame->body ); - } else { - throw new Stomp_Exception( "Receipt not received" ); - } - } - } - return true; - } - /** - * Register to listen to a given destination - * - * @param string $destination Destination queue - * @param array $properties - * @param boolean $sync Perform request synchronously - * @return boolean - * @throws Stomp_Exception - */ - public function subscribe ( $destination, $properties = null, $sync = null ) - { - $headers = array( 'ack' => 'client' ); - $headers['activemq.prefetchSize'] = $this->prefetchSize; - if ( $this->clientId != null ) { - $headers["activemq.subcriptionName"] = $this->clientId; - } - if ( isset( $properties ) ) { - foreach ( $properties as $name => $value ) { - $headers[$name] = $value; - } - } - $headers['destination'] = $destination; - $frame = new Stomp_Frame( 'SUBSCRIBE', $headers ); - $this->_prepareReceipt( $frame, $sync ); - $this->_writeFrame( $frame ); - if ( $this->_waitForReceipt( $frame, $sync ) == true ) { - $this->_subscriptions[$destination] = $properties; - return true; - } else { - return false; - } - } - /** - * Remove an existing subscription - * - * @param string $destination - * @param array $properties - * @param boolean $sync Perform request synchronously - * @return boolean - * @throws Stomp_Exception - */ - public function unsubscribe ( $destination, $properties = null, $sync = null ) - { - $headers = array(); - if ( isset( $properties ) ) { - foreach ( $properties as $name => $value ) { - $headers[$name] = $value; - } - } - $headers['destination'] = $destination; - $frame = new Stomp_Frame( 'UNSUBSCRIBE', $headers ); - $this->_prepareReceipt( $frame, $sync ); - $this->_writeFrame( $frame ); - if ( $this->_waitForReceipt( $frame, $sync ) == true ) { - unset( $this->_subscriptions[$destination] ); - return true; - } else { - return false; - } - } - /** - * Start a transaction - * - * @param string $transactionId - * @param boolean $sync Perform request synchronously - * @return boolean - * @throws Stomp_Exception - */ - public function begin ( $transactionId = null, $sync = null ) - { - $headers = array(); - if ( isset( $transactionId ) ) { - $headers['transaction'] = $transactionId; - } - $frame = new Stomp_Frame( 'BEGIN', $headers ); - $this->_prepareReceipt( $frame, $sync ); - $this->_writeFrame( $frame ); - return $this->_waitForReceipt( $frame, $sync ); - } - /** - * Commit a transaction in progress - * - * @param string $transactionId - * @param boolean $sync Perform request synchronously - * @return boolean - * @throws Stomp_Exception - */ - public function commit ( $transactionId = null, $sync = null ) - { - $headers = array(); - if ( isset( $transactionId ) ) { - $headers['transaction'] = $transactionId; - } - $frame = new Stomp_Frame( 'COMMIT', $headers ); - $this->_prepareReceipt( $frame, $sync ); - $this->_writeFrame( $frame ); - return $this->_waitForReceipt( $frame, $sync ); - } - /** - * Roll back a transaction in progress - * - * @param string $transactionId - * @param boolean $sync Perform request synchronously - */ - public function abort ( $transactionId = null, $sync = null ) - { - $headers = array(); - if ( isset( $transactionId ) ) { - $headers['transaction'] = $transactionId; - } - $frame = new Stomp_Frame( 'ABORT', $headers ); - $this->_prepareReceipt( $frame, $sync ); - $this->_writeFrame( $frame ); - return $this->_waitForReceipt( $frame, $sync ); - } - /** - * Acknowledge consumption of a message from a subscription - * Note: This operation is always asynchronous - * - * @param string|Stomp_Frame $message Message ID - * @param string $transactionId - * @return boolean - * @throws Stomp_Exception - */ - public function ack ( $message, $transactionId = null ) - { - if ( $message instanceof Stomp_Frame ) { - $frame = new Stomp_Frame( 'ACK', $message->headers ); - $this->_writeFrame( $frame ); - return true; - } else { - $headers = array(); - if ( isset( $transactionId ) ) { - $headers['transaction'] = $transactionId; - } - $headers['message-id'] = $message; - $frame = new Stomp_Frame( 'ACK', $headers ); - $this->_writeFrame( $frame ); - return true; - } - } - /** - * Graceful disconnect from the server - * - */ - public function disconnect () - { - if ( $this->clientId != null ) { - $headers["client-id"] = $this->clientId; - } else { - $headers = array(); - } - - if ( is_resource( $this->_socket ) ) { - $this->_writeFrame( new Stomp_Frame( 'DISCONNECT', $headers ) ); - fclose( $this->_socket ); - } - $this->_socket = null; - $this->_sessionId = null; - $this->_currentHost = -1; - $this->_subscriptions = array(); - $this->_username = ''; - $this->_password = ''; - } - - /** - * Write frame to server - * - * @param Stomp_Frame $stompFrame - * @throws Stomp_Exception - */ - protected function _writeFrame ( Stomp_Frame $stompFrame ) - { - if ( !is_resource( $this->_socket ) ) { - require_once 'Stomp/Exception.php'; - throw new Stomp_Exception( 'Socket connection hasn\'t been established' ); - } - - $data = $stompFrame->__toString(); - - $r = fwrite( $this->_socket, $data, strlen( $data ) ); - if ( $r === false || $r == 0 ) { - $this->_reconnect(); - $this->_writeFrame( $stompFrame ); - } - } - - /** - * Set timeout to wait for content to read - * - * @param int $seconds Seconds to wait for a frame - * @param int $milliseconds Milliseconds to wait for a frame - */ - public function setReadTimeout( $seconds, $milliseconds = 0 ) - { - $this->_read_timeout_seconds = $seconds; - $this->_read_timeout_milliseconds = $milliseconds; - } - - /** - * Read responce frame from server - * - * @return Stomp_Frame|Stomp_Message_Map|boolean False when no frame to read - */ - public function readFrame () - { - if ( !$this->hasFrameToRead() ) { - return false; - } - - stream_set_timeout( $this->_socket, 5 ); - $rb = 1024; - $data = ''; - do { - $read = fgets( $this->_socket, $rb ); - $info = stream_get_meta_data( $this->_socket ); - if ( $info['timed_out'] ) { - return FALSE; - } - // if ($read === false) { - // $this->_reconnect(); - // return $this->readFrame(); - // } - $data .= $read; - $len = strlen( $data ); - } while ( $read && ( $len < 2 || ! ( $data[$len - 2] == "\x00" && $data[$len - 1] == "\n" ) ) ); - - list ( $header, $body ) = explode( "\n\n", $data, 2 ); - $header = explode( "\n", $header ); - $headers = array(); - $command = null; - foreach ( $header as $v ) { - if ( isset( $command ) ) { - list ( $name, $value ) = explode( ':', $v, 2 ); - $headers[$name] = $value; - } else { - $command = $v; - } - } - $frame = new Stomp_Frame( $command, $headers, trim( $body ) ); - - if ( isset( $frame->headers['amq-msg-type'] ) && $frame->headers['amq-msg-type'] == 'MapMessage' ) { - require_once 'Stomp/Message/Map.php'; - return new Stomp_Message_Map( $frame ); - } else { - return $frame; - } - } - - /** - * Check if there is a frame to read - * - * @return boolean - */ - public function hasFrameToRead() - { - return true; // http://bugs.php.net/bug.php?id=46024 - - /*$read = array($this->_socket); - $write = null; - $except = null; - - $has_frame_to_read = stream_select($read, $write, $except, $this->_read_timeout_seconds, $this->_read_timeout_milliseconds); - - if ($has_frame_to_read === false) { - throw new Stomp_Exception('Check failed to determin if the socket is readable'); - } elseif ($has_frame_to_read > 0) { - return true; - } else { - return false; - }*/ - } - - /** - * Reconnects and renews subscriptions (if there were any) - * Call this method when you detect connection problems - */ - protected function _reconnect () - { - $subscriptions = $this->_subscriptions; - - $this->connect( $this->_username, $this->_password ); - foreach ( $subscriptions as $dest => $properties ) { - $this->subscribe( $dest, $properties ); - } - } - /** - * Graceful object desruction - * - */ - public function __destruct() - { - $this->disconnect(); - } -} -?> diff --git a/activemq_stomp/Stomp/Exception.php b/activemq_stomp/Stomp/Exception.php deleted file mode 100644 index 09607561..00000000 --- a/activemq_stomp/Stomp/Exception.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @version $Revision: 23 $ - */ -class Stomp_Exception extends Exception -{ - protected $_details; - - /** - * Constructor - * - * @param string $message Error message - * @param int $code Error code - * @param string $details Stomp server error details - */ - public function __construct( $message = null, $code = 0, $details = '' ) - { - $this->_details = $details; - //$message = "Stomp Error. Check host connection. Details suppressed for security."; - parent::__construct( $message, $code ); - } - - /** - * Stomp server error details - * - * @return string - */ - public function getDetails() - { - return $this->_details; - - } - -} -?> diff --git a/activemq_stomp/Stomp/Frame.php b/activemq_stomp/Stomp/Frame.php deleted file mode 100644 index efe8116e..00000000 --- a/activemq_stomp/Stomp/Frame.php +++ /dev/null @@ -1,80 +0,0 @@ - - * @author Dejan Bosanac - * @author Michael Caplan - * @version $Revision: 36 $ - */ -class Stomp_Frame -{ - public $command; - public $headers = array(); - public $body; - - /** - * Constructor - * - * @param string $command - * @param array $headers - * @param string $body - */ - public function __construct ( $command = null, $headers = null, $body = null ) - { - $this->_init( $command, $headers, $body ); - } - - protected function _init ( $command = null, $headers = null, $body = null ) - { - $this->command = $command; - if ( $headers != null ) { - $this->headers = $headers; - } - $this->body = $body; - - if ( $this->command == 'ERROR' ) { - require_once 'Exception.php'; - throw new Stomp_Exception( $this->headers['message'], 0, $this->body ); - } - } - - /** - * Convert frame to transportable string - * - * @return string - */ - public function __toString() - { - $data = $this->command . "\n"; - - foreach ( $this->headers as $name => $value ) { - $data .= $name . ": " . $value . "\n"; - } - - $data .= "\n"; - $data .= $this->body; - return $data . "\x00\n"; - } -} -?> diff --git a/activemq_stomp/Stomp/Message.php b/activemq_stomp/Stomp/Message.php deleted file mode 100644 index 5686b8ae..00000000 --- a/activemq_stomp/Stomp/Message.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @version $Revision: 23 $ - */ -class Stomp_Message extends Stomp_Frame -{ - public function __construct ( $body, $headers = null ) - { - $this->_init( "SEND", $headers, $body ); - } -} -?> diff --git a/activemq_stomp/Stomp/Message/Bytes.php b/activemq_stomp/Stomp/Message/Bytes.php deleted file mode 100644 index f580a783..00000000 --- a/activemq_stomp/Stomp/Message/Bytes.php +++ /dev/null @@ -1,47 +0,0 @@ - - * @version $Revision: 23 $ - */ -class Stomp_Message_Bytes extends Stomp_Message -{ - /** - * Constructor - * - * @param string $body - * @param array $headers - */ - function __construct ( $body, $headers = null ) - { - $this->_init( "SEND", $headers, $body ); - if ( $this->headers == null ) { - $this->headers = array(); - } - $this->headers['content-length'] = count( $body ); - } -} -?> diff --git a/activemq_stomp/Stomp/Message/Map.php b/activemq_stomp/Stomp/Message/Map.php deleted file mode 100644 index dd61e6d2..00000000 --- a/activemq_stomp/Stomp/Message/Map.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @version $Revision: 23 $ - */ -class Stomp_Message_Map extends Stomp_Message -{ - public $map; - - /** - * Constructor - * - * @param Stomp_Frame|string $msg - * @param array $headers - */ - function __construct ( $msg, $headers = null ) - { - if ( $msg instanceof Stomp_Frame ) { - $this->_init( $msg->command, $msg->headers, $msg->body ); - $this->map = json_decode( $msg->body ); - } else { - $this->_init( "SEND", $headers, $msg ); - if ( $this->headers == null ) { - $this->headers = array(); - } - $this->headers['amq-msg-type'] = 'MapMessage'; - $this->body = json_encode( $msg ); - } - } -} -?> diff --git a/activemq_stomp/activemq_stomp.php b/activemq_stomp/activemq_stomp.php deleted file mode 100644 index 4bfd4add..00000000 --- a/activemq_stomp/activemq_stomp.php +++ /dev/null @@ -1,360 +0,0 @@ - tag to include landing page donation form - */ - -function efStompSetup( &$parser ) { - global $wgDonationInterfaceEnableStomp; - - if ( $wgDonationInterfaceEnableStomp !== true ) { - return true; - } - - // redundant and causes Fatal Error - // $parser->disableCache(); - - $parser->setHook( 'stomp', 'efStompTest' ); - - return true; -} - -function efStompTest( $input, $args, &$parser ) { - $parser->disableCache(); - - $output = "STOMP Test page"; - - WmfFramework::runHooks( 'gwStomp', array( &$transaction ) ); - - return $output; -} - -/** - * Hook to send complete transaction information to ActiveMQ server - * - * @global string $wgStompServer ActiveMQ server name. - * @global string $wgStompQueueNames Array containing names of queues. Will use entry either on key - * of the strcat(payment_method '-$queue'), or '$queue' - * - * @param array $transaction Key-value array of staged and ready donation data. - * @param string $queue Name of the queue to use, ie: 'limbo' or 'pending' - * - * @return bool Just returns true all the time. Presumably an indication that - * nothing exploded big enough to kill the whole thing. - * @throws RuntimeException - */ -function sendSTOMP( $transaction, $queue = 'default' ) { - global $IP, $wgStompServer, $wgStompQueueNames, - $wgDonationInterfaceEnableStomp; - - if ( $wgDonationInterfaceEnableStomp !== true ) { - return true; - } - - // Find the queue name - if ( array_key_exists( $transaction['payment_method'] . "-$queue", $wgStompQueueNames ) ) { - $queueName = $wgStompQueueNames[$transaction['payment_method'] . "-$queue"]; - } elseif ( array_key_exists( $queue, $wgStompQueueNames ) ) { - $queueName = $wgStompQueueNames[$queue]; - } else { - // Sane default... - $queueName = "test-$queue"; - WmfFramework::debugLog( 'stomp', "We should consider adding a queue name entry for $queue" ); - } - - // If it turns out the queue name is false or empty, we don't actually want to use this queue - if ( $queueName == false ) { - return true; - } - - static $sourceRevision = null; - if ( !$sourceRevision ) { - $versionStampPath = "$IP/.version-stamp"; - if ( file_exists( $versionStampPath ) ) { - $sourceRevision = trim( file_get_contents( $versionStampPath ) ); - } else { - $sourceRevision = 'unknown'; - } - } - - // Create the message and associated properties - $properties = array( - 'persistent' => 'true', - 'payment_method' => $transaction['payment_method'], - 'php-message-class' => $transaction['php-message-class'], - 'gateway' => $transaction['gateway'], - 'source_name' => 'DonationInterface', - 'source_type' => 'payments', - 'source_host' => WmfFramework::getHostname(), - 'source_run_id' => getmypid(), - 'source_version' => $sourceRevision, - 'source_enqueued_time' => time(), - ); - - if ( array_key_exists( 'antimessage', $transaction ) ) { - $message = ''; - $properties['antimessage'] = 'true'; - } else { - if ( array_key_exists( 'freeform', $transaction ) ) { - $message = $transaction; - unset( $message['freeform'] ); - } else { - $message = createQueueMessage( $transaction ); - } - $message = json_encode( $message ); - } - - if ( array_key_exists( 'correlation-id', $transaction ) ) { - $properties['correlation-id'] = $transaction['correlation-id']; - } - - // make a connection - $con = new Stomp( $wgStompServer ); - $con->connect(); - - // send a message to the queue - $result = $con->send( "/queue/$queueName", $message, $properties ); - - if ( !$result ) { - throw new RuntimeException( "Send to $queueName failed for this message: $message" ); - } - - $con->disconnect(); - - return true; -} - -/** - * Hook to send transaction information to ActiveMQ server - * @deprecated Use sendSTOMP with $queue = 'pending' instead - * - * @param array $transaction Key-value array of staged and ready donation data. - * @return bool Just returns true all the time. Presumably an indication that - * nothing exploded big enough to kill the whole thing. - */ -function sendPendingSTOMP( $transaction ) { - global $wgDonationInterfaceEnableStomp; - - if ( $wgDonationInterfaceEnableStomp !== true ) { - return true; - } - - return sendSTOMP( $transaction, 'pending' ); -} - -/** - * Hook to send transaction information to ActiveMQ server - * @deprecated Use sendSTOMP with $queue = 'limbo' instead - * - * @param array $transaction Key-value array of staged and ready donation data. - * @return bool Just returns true all the time. Presumably an indication that - * nothing exploded big enough to kill the whole thing. - */ -function sendLimboSTOMP( $transaction ) { - return sendSTOMP( $transaction, 'limbo' ); -} - -/** - * Hook to send transaction information to ActiveMQ server - * @deprecated Use sendSTOMP with $queue = 'limbo' instead - * - * @param array $transaction Key-value array of staged and ready donation data. - * @return bool Just returns true all the time. Presumably an indication that - * nothing exploded big enough to kill the whole thing. - */ -function sendFreeformSTOMP( $transaction, $queue ) { - global $wgDonationInterfaceEnableStomp; - - if ( $wgDonationInterfaceEnableStomp !== true ) { - return true; - } - - $transaction['freeform'] = true; - return sendSTOMP( $transaction, $queue ); -} - -/** - * Assign correct values to the array of data to be sent to the ActiveMQ server - * TODO: Probably something else. I don't like the way this works and neither do you. - * - * Older notes follow: - * Currency in receiving module has currency set to USD, should take passed variable for these - * PAssed both ISO and country code, no need to look up - * 'gateway' = globalcollect, e.g. - * 'date' is sent as $date("r") so it can be translated with strtotime like Paypal transactions (correct?) - * Processor txn ID sent in the transaction response is assigned to 'gateway_txn_id' (PNREF) - * Order ID (generated with transaction) is assigned to 'contribution_tracking_id'? - * Response from processor is assigned to 'response' - */ -function createQueueMessage( $transaction ) { - // specifically designed to match the CiviCRM API that will handle it - // edit this array to include/ignore transaction data sent to the server - $message = array( - 'contribution_tracking_id' => $transaction['contribution_tracking_id'], - 'utm_source' => $transaction['utm_source'], - 'language' => $transaction['language'], - 'referrer' => $transaction['referrer'], - 'email' => $transaction['email'], - 'first_name' => $transaction['fname'], - 'last_name' => $transaction['lname'], - 'street_address' => $transaction['street'], - 'city' => $transaction['city'], - 'state_province' => $transaction['state'], - 'country' => $transaction['country'], - 'postal_code' => $transaction['zip'], - 'gateway' => $transaction['gateway'], - 'gateway_account' => $transaction['gateway_account'], - 'gateway_txn_id' => $transaction['gateway_txn_id'], - 'payment_method' => $transaction['payment_method'], - 'payment_submethod' => $transaction['payment_submethod'], - 'response' => $transaction['response'], - 'currency' => $transaction['currency_code'], - 'fee' => '0', - 'gross' => $transaction['amount'], - 'user_ip' => $transaction['user_ip'], - //the following int casting fixes an issue that is more in Drupal/CiviCRM than here. - //The code there should also be fixed. - 'date' => ( int ) $transaction['date'], - ); - - //optional keys - $optional_keys = array( - 'recurring', - 'optout', - 'anonymous', - 'street_supplemental', - 'utm_campaign', - 'utm_medium', - ); - foreach ( $optional_keys as $key ) { - if ( isset( $transaction[ $key ] ) ) { - $message[ $key ] = $transaction[ $key ]; - } - } - - //as this is just the one thing, I can't think of a way to do this that isn't actually more annoying. :/ - if ( isset( $message['street_supplemental'] ) ) { - $message['supplemental_address_1'] = $message['street_supplemental']; - unset( $message['street_supplemental'] ); - } - - return $message; -} - -/** - * Called by the orphan rectifier to change a queue message back into a gateway - * transaction array, basically undoing the mappings from createQueueMessage - * - * @param array $transaction STOMP message - * - * @return array message with queue keys remapped to gateway keys - */ -function unCreateQueueMessage( $transaction ) { - // For now, this function assumes that we have a complete queue message. - // TODO: Something more robust and programmatic, as time allows. This whole file is just terrible. - - $rekey = array( - 'first_name' => 'fname', - 'last_name' => 'lname', - 'street_address' => 'street', - 'state_province' => 'state', - 'postal_code' => 'zip', - 'currency' => 'currency_code', - 'gross' => 'amount', - ); - - foreach ( $rekey as $stomp => $di ){ - if ( isset( $transaction[$stomp] ) ){ - $transaction[$di] = $transaction[$stomp]; - unset($transaction[$stomp]); - }; - } - - return $transaction; -} - - -/** - * Fetches all the messages in a queue that match the supplies selector. - * Limiting to a completely arbitrary 50, just in case something goes amiss somewhere. - * @param string $queue The target queue from which we would like to fetch things. - * To simplify things, specify either 'verified', 'pending', or 'limbo'. - * @param string $selector Could be anything that STOMP will regard as a valid selector. For our purposes, we will probably do things like: - * $selector = "JMSCorrelationID = 'globalcollect-6214814668'", or - * $selector = "payment_method = 'cc'"; - * @param int $limit The maximum number of messages we would like to pull off of the queue at one time. - * @return array an array of stomp messages, with a count of up to $limit. - */ -function stompFetchMessages( $queue, $selector = null, $limit = 50 ){ - global $wgStompQueueNames; - - static $selector_last = null; - if ( !is_null( $selector_last ) && $selector_last != $selector ){ - $renew = true; - } else { - $renew = false; - } - $selector_last = $selector; - - // Get the actual name of the queue - if ( array_key_exists( $queue, $wgStompQueueNames ) ) { - $queue = $wgStompQueueNames[$queue]; - } else { - $queue = $wgStompQueueNames['default']; - } - - //This needs to be renewed every time we change the selectors. - $stomp = getDIStompConnection( $renew ); - - $properties = array( 'ack' => 'client' ); - if ( !is_null( $selector ) ){ - $properties['selector'] = $selector; - } - - $stomp->subscribe( '/queue/' . $queue, $properties ); - $message = $stomp->readFrame(); - - $return = array(); - - while ( !empty( $message ) && count( $return ) < $limit ) { - $return[] = $message; - $stomp->subscribe( '/queue/' . $queue, $properties ); - $message = $stomp->readFrame(); - } - - return $return; -} - - -/** - * Ack all of the messages in the array, thereby removing them from the queue. - * @param array $messages - */ -function stompAckMessages( $messages = array() ){ - $stomp = getDIStompConnection(); - foreach ($messages as $message){ - if (!array_key_exists('redelivered', $message->headers)) { - $message->headers['redelivered'] = 'true'; - } - $stomp->ack($message); - } -} - -function getDIStompConnection( $renew = false ){ - global $wgStompServer; - static $conn = null; - if ( $conn === null || !$conn->isConnected() || $renew ) { - if ( $conn !== null && $conn->isConnected() ){ - $conn->disconnect(); //just to be safe. - } - // make a connection - $conn = new Stomp( $wgStompServer ); - $conn->connect(); - } - return $conn; -} - -function closeDIStompConnection(){ - $conn = getDIStompConnection(); - $conn->disconnect(); -} diff --git a/adyen_gateway/adyen.adapter.php b/adyen_gateway/adyen.adapter.php index 218caab3..672fc623 100644 --- a/adyen_gateway/adyen.adapter.php +++ b/adyen_gateway/adyen.adapter.php @@ -1,616 +1,614 @@ accountInfo = array( 'merchantAccount' => $this->account_config[ 'AccountName' ], 'skinCode' => $this->account_config[ 'SkinCode' ], 'hashSecret' => $this->account_config[ 'SharedSecret' ], ); } function defineDataConstraints() { } function defineErrorMap() { $this->error_map = array( 'internal-0000' => 'donate_interface-processing-error', // Failed failed pre-process checks. ); } function defineStagedVars() { $this->staged_vars = array( 'amount', 'street', 'zip', 'billing_signature', 'hpp_signature', 'fraud_score', ); } /** * Define var_map */ function defineVarMap() { $this->var_map = array( 'allowedMethods' => 'allowed_methods', 'billingAddress.city' => 'city', 'billingAddress.country' => 'country', 'billingAddress.postalCode' => 'zip', 'billingAddressSig' => 'billing_signature', 'billingAddress.stateOrProvince' => 'state', 'billingAddress.street' => 'street', 'billingAddressType' => 'billing_address_type', 'blockedMethods' => 'blocked_methods', 'currencyCode' => 'currency_code', 'deliveryAddressType' => 'delivery_address_type', 'merchantAccount' => 'merchant_account', 'merchantReference' => 'order_id', 'merchantReturnData' => 'return_data', 'merchantSig' => 'hpp_signature', 'offset' => 'risk_score', 'orderData' => 'order_data', 'paymentAmount' => 'amount', 'pspReference' => 'gateway_txn_id', 'recurringContract' => 'recurring_type', 'sessionValidity' => 'session_expiration', 'shipBeforeDate' => 'expiration', 'shopperEmail' => 'email', 'shopperLocale' => 'language', 'shopperReference' => 'customer_id', 'shopperStatement' => 'statement_template', 'skinCode' => 'skin_code', ); } function defineReturnValueMap() { $this->return_value_map = array( 'authResult' => 'result', 'merchantReference' => 'order_id', 'merchantReturnData' => 'return_data', 'pspReference' => 'gateway_txn_id', 'skinCode' => 'skin_code', ); } /** * Sets up the $order_id_meta array. * Should contain the following keys/values: * 'alt_locations' => array( $dataset_name, $dataset_key ) //ordered * 'type' => numeric, or alphanumeric * 'length' => $max_charlen */ public function defineOrderIDMeta() { $this->order_id_meta = array ( 'alt_locations' => array ( '_GET' => 'merchantReference' ), 'generate' => TRUE, ); } function setGatewayDefaults() {} /** * Define transactions */ function defineTransactions() { $this->transactions = array( ); $this->transactions[ 'donate' ] = array( 'request' => array( 'allowedMethods', 'billingAddress.street', 'billingAddress.city', 'billingAddress.postalCode', 'billingAddress.stateOrProvince', 'billingAddress.country', 'billingAddressSig', 'billingAddressType', 'currencyCode', 'merchantAccount', 'merchantReference', 'merchantSig', 'offset', 'paymentAmount', 'sessionValidity', 'shipBeforeDate', 'skinCode', 'shopperLocale', 'shopperEmail', // TODO more fields we might want to send to Adyen //'shopperReference', //'recurringContract', //'blockedMethods', //'shopperStatement', //'merchantReturnData', //'deliveryAddressType', ), 'values' => array( 'allowedMethods' => implode( ',', $this->getAllowedPaymentMethods() ), 'billingAddressType' => 2, // hide billing UI fields 'merchantAccount' => $this->accountInfo[ 'merchantAccount' ], 'sessionValidity' => date( 'c', strtotime( '+2 days' ) ), 'shipBeforeDate' => date( 'Y-M-d', strtotime( '+2 days' ) ), 'skinCode' => $this->accountInfo[ 'skinCode' ], //'shopperLocale' => language _ country ), 'iframe' => TRUE, ); } public function definePaymentMethods() { $this->payment_methods = array( 'cc' => array(), ); } protected function getAllowedPaymentMethods() { return array( 'card', ); } function doPayment() { return PaymentResult::fromResults( $this->do_transaction( 'donate' ), $this->getFinalStatus() ); } /** * FIXME: I can't help but feel like it's bad that the parent's do_transaction * is never used at all. */ function do_transaction( $transaction ) { $this->session_addDonorData(); $this->setCurrentTransaction( $transaction ); $this->transaction_response = new PaymentTransactionResponse(); if ( $this->transaction_option( 'iframe' ) ) { // slightly different than other gateways' iframe method, // we don't have to make the round-trip, instead just // stage the variables and return the iframe url in formaction. switch ( $transaction ) { case 'donate': $formaction = $this->getGlobal( 'BaseURL' ) . '/hpp/pay.shtml'; $this->runAntifraudHooks(); $this->addRequestData( array ( 'risk_score' => $this->risk_score ) ); //this will also fire off staging again. if ( $this->getValidationAction() != 'process' ) { // copied from base class. $this->logger->info( "Failed pre-process checks for transaction type $transaction." ); $message = $this->getErrorMapByCodeAndTranslate( 'internal-0000' ); $this->transaction_response->setCommunicationStatus( false ); $this->transaction_response->setMessage( $message ); $this->transaction_response->setErrors( array( 'internal-0000' => array( 'message' => $message, 'debugInfo' => "Failed pre-process checks for transaction type $transaction.", 'logLevel' => LogLevel::INFO ), ) ); break; } $this->stageData(); $requestParams = $this->buildRequestParams(); $this->transaction_response->setData( array( 'FORMACTION' => $formaction, 'gateway_params' => $requestParams, ) ); $this->logger->info( "launching external iframe request: " . print_r( $requestParams, true ) ); - $this->doLimboStompTransaction(); $this->setLimboMessage(); break; } } return $this->transaction_response; } static function getCurrencies() { // See http://www.adyen.com/platform/all-countries-all-currencies/ // This should be the list of all global "acceptance currencies". Not // finding that list, I've used everything for which we keep // conversion rates. $currencies = array( 'ADF', // Andorran Franc 'ADP', // Andorran Peseta 'AED', // Utd. Arab Emir. Dirham 'AFA', // Afghanistan Afghani 'AFN', // Afghanistan Afghani 'ALL', // Albanian Lek 'AMD', // Armenian Dram 'ANG', // NL Antillian Guilder 'AOA', // Angolan Kwanza 'AON', // Angolan Old Kwanza 'ARS', // Argentinian peso 'ATS', // Austrian Schilling 'AUD', // Australian Dollar 'AWG', // Aruban Florin 'AZM', // Azerbaijan Old Manat 'AZN', // Azerbaijan New Manat 'BAM', // Bosnian Mark 'BBD', // Barbadian dollar 'BDT', // Bangladeshi Taka 'BEF', // Belgian Franc 'BGL', // Bulgarian Old Lev 'BGN', // Bulgarian Lev 'BHD', // Bahraini Dinar 'BIF', // Burundi Franc 'BMD', // Bermudian Dollar 'BND', // Brunei Dollar 'BOB', // Bolivian Boliviano 'BRL', // Brazilian Real 'BSD', // Bahamian Dollar 'BTN', // Bhutan Ngultrum 'BWP', // Botswana Pula 'BYR', // Belarusian Ruble 'BZD', // Belize Dollar 'CAD', // Canadian Dollar 'CDF', // Congolese Franc 'CHF', // Swiss Franc 'CLP', // Chilean Peso 'CNY', // Chinese Yuan Renminbi 'COP', // Colombian Peso 'CRC', // Costa Rican Colon 'CUC', // Cuban Convertible Peso 'CUP', // Cuban Peso 'CVE', // Cape Verde Escudo 'CYP', // Cyprus Pound 'CZK', // Czech Koruna 'DEM', // German Mark 'DJF', // Djibouti Franc 'DKK', // Danish Krone 'DOP', // Dominican R. Peso 'DZD', // Algerian Dinar 'ECS', // Ecuador Sucre 'EEK', // Estonian Kroon 'EGP', // Egyptian Pound 'ESP', // Spanish Peseta 'ETB', // Ethiopian Birr 'EUR', // Euro 'FIM', // Finnish Markka 'FJD', // Fiji Dollar 'FKP', // Falkland Islands Pound 'FRF', // French Franc 'GBP', // British Pound 'GEL', // Georgian Lari 'GHC', // Ghanaian Cedi 'GHS', // Ghanaian New Cedi 'GIP', // Gibraltar Pound 'GMD', // Gambian Dalasi 'GNF', // Guinea Franc 'GRD', // Greek Drachma 'GTQ', // Guatemalan Quetzal 'GYD', // Guyanese Dollar 'HKD', // Hong Kong Dollar 'HNL', // Honduran Lempira 'HRK', // Croatian Kuna 'HTG', // Haitian Gourde 'HUF', // Hungarian Forint 'IDR', // Indonesian Rupiah 'IEP', // Irish Punt 'ILS', // Israeli New Shekel 'INR', // Indian Rupee 'IQD', // Iraqi Dinar 'IRR', // Iranian Rial 'ISK', // Iceland Krona 'ITL', // Italian Lira 'JMD', // Jamaican Dollar 'JOD', // Jordanian Dinar 'JPY', // Japanese Yen 'KES', // Kenyan Shilling 'KGS', // Kyrgyzstanian Som 'KHR', // Cambodian Riel 'KMF', // Comoros Franc 'KPW', // North Korean Won 'KRW', // South Korean won 'KWD', // Kuwaiti Dinar 'KYD', // Cayman Islands Dollar 'KZT', // Kazakhstani Tenge 'LAK', // Lao Kip 'LBP', // Lebanese Pound 'LKR', // Sri Lankan Rupee 'LRD', // Liberian Dollar 'LSL', // Lesotho Loti 'LTL', // Lithuanian Litas 'LUF', // Luxembourg Franc 'LVL', // Latvian Lats 'LYD', // Libyan Dinar 'MAD', // Moroccan Dirham 'MDL', // Moldovan Leu 'MGA', // Malagasy Ariary 'MGF', // Malagasy Franc 'MKD', // Macedonian Denar 'MMK', // Myanmar Kyat 'MNT', // Mongolian Tugrik 'MOP', // Macau Pataca 'MRO', // Mauritanian Ouguiya 'MTL', // Maltese Lira 'MUR', // Mauritius Rupee 'MVR', // Maldive Rufiyaa 'MWK', // Malawi Kwacha 'MXN', // Mexican Peso 'MYR', // Malaysian Ringgit 'MZM', // Mozambique Metical 'MZN', // Mozambique New Metical 'NAD', // Namibia Dollar 'NGN', // Nigerian Naira 'NIO', // Nicaraguan Cordoba Oro 'NLG', // Dutch Guilder 'NOK', // Norwegian Kroner 'NPR', // Nepalese Rupee 'NZD', // New Zealand Dollar 'OMR', // Omani Rial 'PAB', // Panamanian Balboa 'PEN', // Peruvian Nuevo Sol 'PGK', // Papua New Guinea Kina 'PHP', // Philippine Peso 'PKR', // Pakistani Rupee 'PLN', // Polish Złoty 'PTE', // Portuguese Escudo 'PYG', // Paraguay Guarani 'QAR', // Qatari Rial 'ROL', // Romanian Lei 'RON', // Romanian New Lei 'RSD', // Serbian Dinar 'RUB', // Russian Rouble 'RWF', // Rwandan Franc 'SAR', // Saudi Riyal 'SBD', // Solomon Islands Dollar 'SCR', // Seychelles Rupee 'SDD', // Sudanese Dinar 'SDG', // Sudanese Pound 'SDP', // Sudanese Old Pound 'SEK', // Swedish Krona 'SGD', // Singapore Dollar 'SHP', // St. Helena Pound 'SIT', // Slovenian Tolar 'SKK', // Slovak Koruna 'SLL', // Sierra Leone Leone 'SOS', // Somali Shilling 'SRD', // Suriname Dollar 'SRG', // Suriname Guilder 'STD', // Sao Tome/Principe Dobra 'SVC', // El Salvador Colon 'SYP', // Syrian Pound 'SZL', // Swaziland Lilangeni 'THB', // Thai Baht 'TJS', // Tajikistani Somoni 'TMM', // Turkmenistan Manat 'TMT', // Turkmenistan New Manat 'TND', // Tunisian Dinar 'TOP', // Tonga Pa'anga 'TRL', // Turkish Old Lira 'TRY', // Turkish Lira 'TTD', // Trinidad/Tobago Dollar 'TWD', // New Taiwan dollar 'TZS', // Tanzanian Shilling 'UAH', // Ukrainian hryvnia 'UGX', // Uganda Shilling 'USD', // U.S. dollar 'UYU', // Uruguayan Peso 'UZS', // Uzbekistan Som 'VEB', // Venezuelan Bolivar 'VEF', // Venezuelan Bolivar Fuerte 'VND', // Vietnamese Dong 'VUV', // Vanuatu Vatu 'WST', // Samoan Tala 'XAF', // Central African CFA franc 'XAG', // Silver (oz.) 'XAU', // Gold (oz.) 'XCD', // East Caribbean Dollar 'XEU', // ECU 'XOF', // West African CFA franc 'XPD', // Palladium (oz.) 'XPF', // CFP Franc 'XPT', // Platinum (oz.) 'YER', // Yemeni Rial 'YUN', // Yugoslav Dinar 'ZAR', // South African Rand 'ZMK', // Zambian Kwacha 'ZWD', // Zimbabwe Dollar ); return $currencies; } //@TODO: Determine why this is being overloaded here. //This looks like a var-renamed copy of the parent. :[ protected function buildRequestParams() { // Look up the request structure for our current transaction type in the transactions array $structure = $this->getTransactionRequestStructure(); if ( !is_array( $structure ) ) { return FALSE; } $queryvals = array(); foreach ( $structure as $fieldname ) { $fieldvalue = $this->getTransactionSpecificValue( $fieldname ); if ( $fieldvalue !== '' && $fieldvalue !== false ) { $queryvals[$fieldname] = $fieldvalue; } } return $queryvals; } /** * For Adyen, we only call this on the donor's return to the ResultSwitcher * @param array $response GET/POST params from request * @throws ResponseProcessingException */ public function processResponse( $response ) { // Always called outside do_transaction, so just make a new response object $this->transaction_response = new PaymentTransactionResponse(); if ( empty( $response ) ) { $this->logger->info( "No response from gateway" ); throw new ResponseProcessingException( 'No response from gateway', ResponseCodes::NO_RESPONSE ); } $this->logger->info( "Processing user return data: " . print_r( $response, TRUE ) ); if ( !$this->checkResponseSignature( $response ) ) { $this->logger->info( "Bad signature in response" ); throw new ResponseProcessingException( 'Bad signature in response', ResponseCodes::BAD_SIGNATURE ); } $this->logger->debug( 'Good signature' ); $gateway_txn_id = isset( $response['pspReference'] ) ? $response['pspReference'] : ''; $result_code = isset( $response['authResult'] ) ? $response['authResult'] : ''; if ( $result_code == 'PENDING' || $result_code == 'AUTHORISED' ) { // Both of these are listed as pending because we have to submit a capture // request on 'AUTHORIZATION' ipn message receipt. $this->logger->info( "User came back as pending or authorised, placing in pending queue" ); $this->finalizeInternalStatus( FinalStatus::PENDING ); } else { $this->finalizeInternalStatus( FinalStatus::FAILED ); $this->logger->info( "Negative response from gateway. Full response: " . print_r( $response, TRUE ) ); throw new ResponseProcessingException( "Negative response from gateway. Full response: " . print_r( $response, TRUE ), ResponseCodes::UNKNOWN ); } $this->transaction_response->setGatewayTransactionId( $gateway_txn_id ); // FIXME: Why put that two places in transaction_response? $this->transaction_response->setTxnMessage( $this->getFinalStatus() ); $this->runPostProcessHooks(); $this->deleteLimboMessage(); - $this->doLimboStompTransaction( TRUE ); // TODO: stop mirroring to stomp } /** * TODO do we want to stage the country code for language variants? protected function stage_language( $type = 'request' ) { */ protected function stage_risk_score() { //This isn't smart enough to grab a new value here; //Late-arriving values have to trigger a restage via addData or //this will always equil the risk_score at the time of object //construction. Still need the formatting, though. $this->staged_data['risk_score'] = ( string ) round( $this->unstaged_data['risk_score'] ); } protected function stage_hpp_signature() { $keys = array( 'amount', 'currency_code', 'expiration', 'order_id', 'skin_code', 'merchant_account', 'session_expiration', 'email', 'customer_id', 'recurring_type', 'allowed_methods', 'blocked_methods', 'statement_template', 'return_data', 'billing_address_type', 'delivery_address_type', 'risk_score', ); $sig_values = $this->getStagedValues( $this->getGatewayKeys( $keys ) ); $this->staged_data['hpp_signature'] = $this->calculateSignature( $sig_values ); } protected function stage_billing_signature() { $keys = array( 'street', 'city', 'zip', 'state', 'country', ); $sig_values = $this->getStagedValues( $this->getGatewayKeys( $keys ) ); $this->staged_data['billing_signature'] = $this->calculateSignature( $sig_values ); } // TODO: make the signature code more reusable. Generalize the idea of // mapping keys and fetching matching values. protected function getGatewayKeys( $keys ) { $staged = array(); $staging_map = array_flip( $this->var_map ); foreach ( $keys as $normal_form_key ) { $staged[] = $staging_map[ $normal_form_key ]; } return $staged; } protected function getStagedValues( $keys ) { $values = array(); foreach ( $keys as $key ) { $s = $this->getTransactionSpecificValue( $key ); if ( $s !== NULL ) { $values[] = $s; } } return $values; } function checkResponseSignature( $request_vars ) { $normal_form_keys = array( 'result', 'gateway_txn_id', 'order_id', 'skin_code', 'return_data' ); $unstage_map = array_flip( $this->return_value_map ); $keys = array(); foreach ( $normal_form_keys as $normal_key ) { $keys[] = $unstage_map[ $normal_key ]; } $sig_values = array(); foreach ( $keys as $key ) { $sig_values[] = ( array_key_exists( $key, $request_vars ) ? $request_vars[ $key ] : "" ); } $calculated_sig = $this->calculateSignature( $sig_values ); return ( $calculated_sig === $request_vars[ 'merchantSig' ] ); } protected function calculateSignature( $values ) { $joined = implode( '', $values ); return base64_encode( hash_hmac( 'sha1', $joined, $this->accountInfo[ 'hashSecret' ], TRUE ) ); } } diff --git a/amazon_gateway/amazon.adapter.php b/amazon_gateway/amazon.adapter.php index 4fb7a5a77..118cfadf 100644 --- a/amazon_gateway/amazon.adapter.php +++ b/amazon_gateway/amazon.adapter.php @@ -1,553 +1,552 @@ getData_Unstaged_Escaped( 'payment_method' ) == null ) { $this->addRequestData( array( 'payment_method' => 'amazon' ) ); } } public function getCommunicationType() { if ( $this->transaction_option( 'redirect' ) ) { return 'redirect'; } return 'xml'; } function defineStagedVars() {} function defineVarMap() { $this->var_map = array( "amount" => "amount", "transactionAmount" => "amount", "transactionId" => "gateway_txn_id", "status" => "gateway_status", "buyerEmail" => "email", "transactionDate" => "date_collect", "buyerName" => "fname", // This is dealt with in addDataFromURI() "errorMessage" => "error_message", "paymentMethod" => "payment_submethod", "referenceId" => "contribution_tracking_id", ); } function defineAccountInfo() { //XXX since this class actually accesses two different endpoints, // the usefulness of this function is uncertain. In other words, // account info is transaction-specific. We use account_config // instead $this->accountInfo = array(); } function defineReturnValueMap() {} function defineDataConstraints() {} function defineOrderIDMeta() { $this->order_id_meta = array ( 'generate' => TRUE, ); } function setGatewayDefaults() {} public function defineErrorMap() { $this->error_map = array( // Internal messages 'internal-0000' => 'donate_interface-processing-error', // Failed failed pre-process checks. 'internal-0001' => 'donate_interface-processing-error', // Transaction could not be processed due to an internal error. 'internal-0002' => 'donate_interface-processing-error', // Communication failure ); } function defineTransactions() { $this->transactions = array(); $this->transactions[ 'Donate' ] = array( 'request' => array( 'accessKey', 'amount', 'collectShippingAddress', 'description', 'immediateReturn', 'ipnUrl', 'returnUrl', 'isDonationWidget', 'processImmediate', 'referenceId', //'signature', 'signatureMethod', 'signatureVersion', ), 'values' => array( 'accessKey' => $this->account_config[ 'AccessKey' ], 'collectShippingAddress' => '0', 'description' => WmfFramework::formatMessage( 'donate_interface-donation-description' ), 'immediateReturn' => '1', 'ipnUrl' => $this->account_config['IpnOverride'], 'isDonationWidget' => '1', 'processImmediate' => '1', 'signatureMethod' => 'HmacSHA256', 'signatureVersion' => '2', ), 'redirect' => TRUE, ); $this->transactions[ 'DonateMonthly' ] = array( 'request' => array( 'accessKey', 'amount', 'collectShippingAddress', 'description', 'immediateReturn', 'ipnUrl', 'processImmediate', 'recurringFrequency', 'referenceId', 'returnUrl', //'signature', 'signatureMethod', 'signatureVersion', //'subscriptionPeriod', ), 'values' => array( // FIXME: There is magick available if the names match. 'accessKey' => $this->account_config[ 'AccessKey' ], 'collectShippingAddress' => '0', 'description' => WmfFramework::formatMessage( 'donate_interface-monthly-donation-description' ), 'immediateReturn' => '1', 'ipnUrl' => $this->account_config['IpnOverride'], 'processImmediate' => '1', 'recurringFrequency' => "1 month", 'signatureMethod' => "HmacSHA256", 'signatureVersion' => "2", // FIXME: this is the documented default, but passing it explicitly is buggy //'subscriptionPeriod' => "forever", ), 'redirect' => TRUE, ); $this->transactions[ 'VerifySignature' ] = array( 'request' => array( 'Action', 'HttpParameters', 'UrlEndPoint', 'Version', //'Signature', 'SignatureMethod', 'SignatureVersion', 'AWSAccessKeyId', 'Timestamp', ), 'values' => array( 'Action' => "VerifySignature", 'AWSAccessKeyId' => $this->account_config[ 'AccessKey' ], 'UrlEndPoint' => $this->getGlobal( "ReturnURL" ), 'Version' => "2010-08-28", 'SignatureMethod' => "HmacSHA256", 'SignatureVersion' => "2", 'Timestamp' => date( 'c' ), ), 'url' => $this->getGlobal( "FpsURL" ), ); $this->transactions[ 'ProcessAmazonReturn' ] = array( 'request' => array(), 'values' => array(), ); } public function definePaymentMethods() { $this->payment_methods = array( 'amazon' => array(), ); $this->payment_submethods = array( 'amazon_cc' => array(), 'amazon_wallet' => array(), ); } protected function buildRequestParams() { $queryparams = parent::buildRequestParams(); ksort( $queryparams ); return $queryparams; } public function doPayment() { if ( $this->getData_Unstaged_Escaped( 'recurring' ) ) { $resultData = $this->do_transaction( 'DonateMonthly' ); } else { $resultData = $this->do_transaction( 'Donate' ); } return PaymentResult::fromResults( $resultData, $this->getFinalStatus() ); } function do_transaction( $transaction ) { global $wgRequest, $wgOut; $this->session_addDonorData(); $this->setCurrentTransaction( $transaction ); $this->transaction_response = new PaymentTransactionResponse(); $override_url = $this->transaction_option( 'url' ); if ( !empty( $override_url ) ) { $this->url = $override_url; } else { $this->url = $this->getGlobal( "URL" ); } switch ( $transaction ) { case 'Donate': case 'DonateMonthly': $return_url = $this->getGlobal( 'ReturnURL' ); //check if ReturnURL already has a query string $return_query = parse_url( $return_url, PHP_URL_QUERY ); $return_url .= ( $return_query ? '&' : '?' ); $return_url .= "ffname=amazon&order_id={$this->getData_Unstaged_Escaped( 'order_id' )}"; $this->transactions[ $transaction ][ 'values' ][ 'returnUrl' ] = $return_url; break; case 'VerifySignature': $request_params = $wgRequest->getValues(); unset( $request_params[ 'title' ] ); $incoming = http_build_query( $request_params, '', '&' ); $this->transactions[ $transaction ][ 'values' ][ 'HttpParameters' ] = $incoming; $this->logger->debug( "received callback from amazon with: $incoming" ); break; } // TODO this will move to a staging function once FR#507 is deployed $query = $this->buildRequestParams(); $parsed_uri = parse_url( $this->url ); $signature = $this->signRequest( $parsed_uri[ 'host' ], $parsed_uri[ 'path' ], $query ); switch ( $transaction ) { case 'Donate': case 'DonateMonthly': $query_str = $this->encodeQuery( $query ); $this->logger->debug( "At $transaction, redirecting with query string: $query_str" ); //always have to do this before a redirect. $this->dataObj->saveContributionTrackingData(); //@TODO: This shouldn't be happening here. Oh Amazon... Why can't you be more like PayPalAdapter? $wgOut->redirect("{$this->getGlobal( "URL" )}?{$query_str}&signature={$signature}"); break; case 'VerifySignature': // We don't currently use this. In fact we just ignore the return URL signature. // However, it's perfectly good code and we may go back to using it at some point // so I didn't want to remove it. $query_str = $this->encodeQuery( $query ); $this->url .= "?{$query_str}&Signature={$signature}"; $this->logger->debug( "At $transaction, query string: $query_str" ); parent::do_transaction( $transaction ); if ( $this->getFinalStatus() === FinalStatus::COMPLETE ) { $this->unstaged_data = $this->dataObj->getDataEscaped(); // XXX not cool. $this->runPostProcessHooks(); - $this->doLimboStompTransaction( true ); $this->deleteLimboMessage(); } break; case 'ProcessAmazonReturn': // What we need to do here is make sure THE WHAT // FIXME: This is resultswitcher logic. $this->addDataFromURI(); $this->analyzeReturnStatus(); break; default: $this->logger->critical( "At $transaction; THIS IS NOT DEFINED!" ); $this->finalizeInternalStatus( FinalStatus::FAILED ); } return $this->transaction_response; } static function getCurrencies() { // See https://payments.amazon.com/sdui/sdui/about?nodeId=73479#feat_countries return array( 'USD', ); } /** * Looks at the 'status' variable in the amazon return URL get string and places the data * in the appropriate Final Status and sends to STOMP. */ protected function analyzeReturnStatus() { // We only want to analyze this if we don't already have a Final Status... Therefore we // won't overwrite things. if ( $this->getFinalStatus() === false ) { $txnid = $this->dataObj->getVal_Escaped( 'gateway_txn_id' ); $this->transaction_response->setGatewayTransactionId( $txnid ); // Second make sure that the inbound request had a matching outbound session. If it // doesn't we drop it. if ( !self::session_hasDonorData( 'order_id', $this->getData_Unstaged_Escaped( 'order_id' ) ) ) { // We will however log it if we have a seemingly valid transaction id if ( $txnid != null ) { $ctid = $this->getData_Unstaged_Escaped( 'contribution_tracking_id' ); $this->logger->alert( "$ctid failed orderid verification but has txnid '$txnid'. Investigation required." ); if ( $this->getGlobal( 'UseOrderIdValidation' ) ) { $this->finalizeInternalStatus( FinalStatus::FAILED ); return; } } else { $this->finalizeInternalStatus( FinalStatus::FAILED ); return; } } // Third: we did have an outbound request; so let's look at what amazon is telling us // about the transaction. // todo: lots of other statuses we can interpret // see: http://docs.amazonwebservices.com/AmazonSimplePay/latest/ASPAdvancedUserGuide/ReturnValueStatusCodes.html $this->logger->info( "Transaction $txnid returned with status " . $this->dataObj->getVal_Escaped( 'gateway_status' ) ); switch ( $this->dataObj->getVal_Escaped( 'gateway_status' ) ) { case 'PS': // Payment success $this->finalizeInternalStatus( FinalStatus::COMPLETE ); $this->doStompTransaction(); break; case 'PI': // Payment initiated, it will complete later $this->finalizeInternalStatus( FinalStatus::PENDING ); $this->doStompTransaction(); break; case 'SS': // Subscription success -- processing handled by the IPN listener $this->finalizeInternalStatus( FinalStatus::COMPLETE ); break; case 'SI': // Subscription initiated -- processing handled by the IPN listener $this->finalizeInternalStatus( FinalStatus::PENDING ); break; case 'PF': // Payment failed case 'SF': // Subscription failed case 'SE': // This one is interesting; service failure... can we do something here? default: // All other errorz $status = $this->dataObj->getVal_Escaped( 'gateway_status' ); $errString = $this->dataObj->getVal_Escaped( 'error_message' ); $this->logger->info( "Transaction $txnid failed with ($status) $errString" ); $this->finalizeInternalStatus( FinalStatus::FAILED ); break; } } else { $this->logger->error( 'Apparently we attempted to process a transaction that already had a final status... Odd' ); } } /** * Adds translated data from the URI string into donation data * FIXME: This should be done by unstaging functions. */ function addDataFromURI() { global $wgRequest; // Obtain data parameters for STOMP message injection //n.b. these request vars were from the _previous_ api call $add_data = array(); foreach ( $this->var_map as $gateway_key => $normal_key ) { $value = $wgRequest->getVal( $gateway_key, null ); if ( !empty( $value ) ) { // Deal with some fun special cases switch ( $gateway_key ) { case 'transactionAmount': list ($currency, $amount) = explode( ' ', $value ); $add_data['currency'] = $currency; $add_data['amount'] = $amount; break; case 'buyerName': list ($fname, $lname) = explode( ' ', $value, 2 ); $add_data['fname'] = $fname; $add_data['lname'] = $lname; break; case 'paymentMethod': $submethods = array ( 'Credit Card' => 'amazon_cc', 'Amazon Payments Balance' => 'amazon_wallet', ); if ( array_key_exists( $value, $submethods ) ) { $add_data['payment_submethod'] = $submethods[$value]; } else { //We don't rely on this anywhere serious, but I want to know about it anyway. $this->logger->error( "Amazon just coughed up a surprise payment submethod of '$value'." ); $add_data['payment_submethod'] = 'unknown'; } break; default: $add_data[ $normal_key ] = $value; break; } } } //TODO: consider prioritizing the session vars $this->addResponseData( $add_data ); //using the gateway's addData function restages everything $txnid = $this->dataObj->getVal_Escaped( 'gateway_txn_id' ); $email = $this->dataObj->getVal_Escaped( 'email' ); $this->logger->info( "Added data to session for txnid $txnid. Now serving email $email." ); } /** * We would call this function for the VerifySignature transaction, if we * ever used that. * @param DomDocument $response * @throws ResponseProcessingException */ public function processResponse( $response ) { $this->transaction_response->setErrors( $this->parseResponseErrors( $response ) ); if ( $this->getCurrentTransaction() !== 'VerifySignature' ) { return; } $statuses = $response->getElementsByTagName( 'VerificationStatus' ); $verified = false; $commStatus = false; foreach ( $statuses as $node ) { $commStatus = true; if ( strtolower( $node->nodeValue ) == 'success' ) { $verified = true; } } $this->transaction_response->setCommunicationStatus( $commStatus ); if ( !$verified ) { $this->logger->info( "Transaction failed in response data verification." ); $this->finalizeInternalStatus( FinalStatus::FAILED ); } } function encodeQuery( $params ) { ksort( $params ); $query = array(); foreach ( $params as $key => $value ) { $encoded = str_replace( "%7E", "~", rawurlencode( $value ) ); $query[] = $key . "=" . $encoded; } return implode( "&", $query ); } function signRequest( $host, $path, &$params ) { unset( $params['signature'] ); $secret_key = $this->account_config[ "SecretKey" ]; $query_str = $this->encodeQuery( $params ); $path_encoded = str_replace( "%2F", "/", rawurlencode( $path ) ); $message = "GET\n{$host}\n{$path_encoded}\n{$query_str}"; return rawurlencode( base64_encode( hash_hmac( 'sha256', $message, $secret_key, TRUE ) ) ); } /** * We're never POST'ing, just send a Content-type that won't confuse Amazon. */ function getCurlBaseHeaders() { $headers = array( 'Content-Type: text/html; charset=utf-8', ); return $headers; } function getCurlBaseOpts() { $opts = parent::getCurlBaseOpts(); $opts[CURLOPT_SSL_VERIFYPEER] = true; $opts[CURLOPT_SSL_VERIFYHOST] = 2; $opts[CURLOPT_CAINFO] = __DIR__ . "/ca-bundle.crt"; $opts[CURLOPT_CAPATH] = __DIR__ . "/ca-bundle.crt"; return $opts; } function parseResponseCommunicationStatus( $response ) { $aok = false; if ( $this->getCurrentTransaction() == 'VerifySignature' ) { foreach ( $response->getElementsByTagName( 'VerifySignatureResult' ) as $node ) { // All we care about is that the node exists $aok = true; } } return $aok; } // @todo FIXME: This doesn't go anywhere. function parseResponseErrors( $response ) { $errors = array( ); foreach ( $response->getElementsByTagName( 'Error' ) as $node ) { $code = ''; $message = ''; foreach ( $node->childNodes as $childnode ) { if ( $childnode->nodeName === "Code" ) { $code = $childnode->nodeValue; } if ( $childnode->nodeName === "Message" ) { $message = $childnode->nodeValue; } // TODO: Convert to internal codes and translate. // $errors[$code] = $message; } } return $errors; } /** * For the Amazon adapter this is a huge hack! Because we build the transaction differently. * Amazon expectings things to them in the query string, and back via XML. Go figure. * * In any case; do_transaction() does the heavy lifting. And this does nothing; which is * required because otherwise we throw a bunch of silly XML at Amazon that it just ignores. * * @return string|void Nothing :) */ protected function buildRequestXML( $rootElement = 'XML', $encoding = 'UTF-8' ) { return ''; } /** * Amount is returned as a dollar amount, so override base class division by 100. */ protected function unstage_amount() { $this->unstaged_data['amount'] = $this->getData_Staged( 'amount' ); } } diff --git a/extras/custom_filters/custom_filters.body.php b/extras/custom_filters/custom_filters.body.php index e6089af7..c424c9ed 100644 --- a/extras/custom_filters/custom_filters.body.php +++ b/extras/custom_filters/custom_filters.body.php @@ -1,171 +1,171 @@ action_ranges = $this->gateway_adapter->getGlobal( 'CustomFiltersActionRanges' ); $this->risk_score['initial'] = $this->gateway_adapter->getGlobal( 'CustomFiltersRiskScore' ); $this->fraud_logger = DonationLoggerFactory::getLogger( $this->gateway_adapter, '_fraud' ); } /** * Determine the action to take for a transaction based on its $risk_score * * @return string The action to take */ public 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; } } } /** * @throws InvalidArgumentException */ 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 ) ){ $this->risk_score['unknown'] = (int)$this->risk_score; } else { $this->risk_score = array(); } } $log_message = "\"$source added a score of $score\""; $this->fraud_logger->info( '"addRiskScore" ' . $log_message ); $this->risk_score[$source] = $score; $this->gateway_adapter->addRiskScore( $score ); } /** * @throws InvalidArgumentException */ public function getRiskScore() { if ( is_numeric( $this->risk_score ) ) { return $this->risk_score; } elseif ( is_array( $this->risk_score) ) { $total = 0; foreach ( $this->risk_score 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( $this->risk_score, true ) ); } } /** * Run the transaction through the custom filters */ public function validate() { // expose a hook for custom filters WmfFramework::runHooks( 'GatewayCustomFilter', array( &$this->gateway_adapter, &$this ) ); $localAction = $this->determineAction(); $this->gateway_adapter->setValidationAction( $localAction ); $log_message = '"' . $localAction . "\"\t\"" . $this->getRiskScore() . "\""; $this->fraud_logger->info( '"Filtered" ' . $log_message ); $log_message = '"' . addslashes( json_encode( $this->risk_score ) ) . '"'; $this->fraud_logger->info( '"CustomFiltersScores" ' . $log_message ); $utm = array( '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' ), ); $log_message = '"' . addslashes( json_encode( $utm ) ) . '"'; $this->fraud_logger->info( '"utm" ' . $log_message ); //add a message to the fraud stats queue, so we can shovel it into the fredge. $stomp_msg = array ( 'validation_action' => $localAction, 'risk_score' => $this->getRiskScore(), 'score_breakdown' => $this->risk_score, 'php-message-class' => 'SmashPig\CrmLink\Messages\DonationInterfaceAntifraud', 'user_ip' => $this->gateway_adapter->getData_Unstaged_Escaped( 'user_ip' ), ); //If we need much more here to help combat fraud, we could just //start stuffing the whole maxmind query in the fredge, too. //Legal said ok... but this seems a bit excessive to me at the //moment. - $stomp_msg = $this->gateway_adapter->makeFreeformStompTransaction( $stomp_msg ); + $transaction = $this->gateway_adapter->makeFreeformStompTransaction( $stomp_msg ); try { - WmfFramework::runHooks( 'gwFreeformStomp', array ( $stomp_msg, 'payments-antifraud' ) ); + $this->fraud_logger->info( 'Pushing transaction to payments-antifraud queue.' ); + DonationQueue::instance()->push( $transaction, 'payments-antifraud' ); } catch ( Exception $e ) { - $this->log( 'Unable to send payments-antifraud message', LogLevel::ERROR ); + $this->fraud_logger->error( 'Unable to send payments-antifraud message' ); } return TRUE; } static function onValidate( &$gateway_adapter ) { if ( !$gateway_adapter->getGlobal( 'EnableCustomFilters' ) ){ return true; } $gateway_adapter->debugarray[] = 'custom filters onValidate hook!'; return self::singleton( $gateway_adapter )->validate(); } static function singleton( &$gateway_adapter ) { if ( !self::$instance || $gateway_adapter->isBatchProcessor() ) { self::$instance = new self( $gateway_adapter ); } return self::$instance; } } diff --git a/gateway_common/gateway.adapter.php b/gateway_common/gateway.adapter.php index 5aceef17..c4ec307c 100644 --- a/gateway_common/gateway.adapter.php +++ b/gateway_common/gateway.adapter.php @@ -1,3925 +1,3885 @@ getResponseType * @throws ResponseProcessingException with an actionable error code and any * variables to retry */ public function processResponse( $response ); /** * Should be a list of our variables that need special staging. * @see $this->staged_vars */ function defineStagedVars(); /** * defineTransactions will define the $transactions array. * The array will contain everything we need to know about the request structure for all the transactions we care about, * for the current gateway. * First array key: Some way for us to id the transaction. Doesn't actually have to be the gateway's name for it, but I'm going with that until I have a reason not to. * Second array key: * 'request' contains the structure of that request. Leaves in the array tree will eventually be mapped to actual values of ours, * according to the precidence established in the getTransactionSpecificValue function. * 'values' contains default values for the transaction. Things that are typically not overridden should go here. */ function defineTransactions(); /** * Define the message keys used to display errors to the user. Should set * @see $this->error_map to an array whose keys are error codes and whose * values are i18n keys. * Any unmapped error code will use 'donate_interface-processing-error' */ function defineErrorMap(); /** * defineVarMap needs to set up the $var_map array. * Keys = the name (or node name) value in the gateway transaction * Values = the mediawiki field name for the corresponding piece of data. */ function defineVarMap(); /** */ function defineDataConstraints(); /** * defineAccountInfo needs to set up the $accountInfo array. * Keys = the name (or node name) value in the gateway transaction * Values = The actual values for those keys. Probably have to access a global or two. (use getGlobal()!) */ function defineAccountInfo(); /** * defineReturnValueMap sets up the $return_value_map array. * Keys = The different constants that may be contained as values in the gateway's response. * Values = what that string constant means to mediawiki. */ function defineReturnValueMap(); /** * Sets up the $payment_methods array. * Keys = unique name for this method * Values = metadata about the method */ function definePaymentMethods(); /** * Sets up the $order_id_meta array. * @TODO: Data Item Class. There should be a class that keeps track of * the metadata for every field we use (everything that currently comes * back from DonationData), that can be overridden per gateway. Revisit * this in a more universal way when that time comes. * * In general, $order_id_meta contains default data about how we * handle/create/gather order_id, which needs to be defined on a * per-gateway basis. Once $order_id_meta has been used to decide the * order_id for the current request, it will also be used to keep * information about the origin and state of the order_id data. * * Should contain the following keys/values: * 'alt_locations' => array( $dataset_name, $dataset_key ) * ** alt_locations is intended to contain a list of arrays that * are always available (or should be), from which we can pull the * order_id. * ** Examples of valid things to throw in $dataset_name are $_GET, * $_POST, $_SESSION (though they should be expressed in the arary * without the dollar prefix) * ** $dataset_key : The key in the associated dataset that is * expected to contain the order_id. Probably going to be order_id * if we are generating the dataset internally. Probably something * else if a gateway is posting or getting back to us in a * resultswitcher situation. * ** These should be expressed in $order_id_meta in order of * preference / authority. * 'generate' => boolean value. True if we will be generating our own * order IDs, false if we are deferring order_id generation to the * gateway. * 'ct_id' => boolean value. If True, when generating order ID use * the contribution tracking ID with the sequence number appended * * Will eventually contain the following keys/values: * 'final'=> The value that we have chosen as the valid order ID for * this request. * 'final_source' => Where we ultimately decided to grab the value we * chose to stuff in 'final'. */ function defineOrderIDMeta(); /** * Called in the constructor, this function should be used to define * pieces of default data particular to the gateway. It will be up to * the child class to poke the data through to the data object * (probably with $this->addRequestData()). * DO NOT set default payment information here (or anywhere, really). * That would be naughty. */ function setGatewayDefaults(); /** * @return array of ISO 4217 currency codes supported by this adapter */ static function getCurrencies(); /** * Attempt the default transaction for the current DonationData * * @return PaymentResult hints for the next donor interaction */ function doPayment(); /** * Data format for outgoing requests to the processor. * Must be one of 'xml', 'namevalue' (for POST), or 'redirect'. * May depend on current transaction. * * @return string */ function getCommunicationType(); /** * Data format for responses coming back from the processor. * Should be 'xml' // TODO: json * * @return string */ function getResponseType(); } interface LogPrefixProvider { function getLogMessagePrefix(); } /** * GatewayAdapter * */ abstract class GatewayAdapter implements GatewayType, LogPrefixProvider { /** * $dataConstraints provides information on how to handle variables. * * * 'account_holder' => array( 'type' => 'alphanumeric', 'length' => 50, ) * * * @var array $dataConstraints */ protected $dataConstraints = array(); /** * $error_map maps gateway errors to client errors * * The key of each error should map to a i18n message key. * By convention, the following three keys have these meanings: * 'internal-0000' => 'message-key-1', // Failed failed pre-process checks. * 'internal-0001' => 'message-key-2', // Transaction could not be processed due to an internal error. * 'internal-0002' => 'message-key-3', // Communication failure * Any undefined key will map to 'donate_interface-processing-error' * * @var array $error_map */ protected $error_map = array(); /** * @see GlobalCollectAdapter::defineGoToThankYouOn() * * @var array $goToThankYouOn */ protected $goToThankYouOn = array(); /** * $var_map maps gateway variables to client variables * * @var array $var_map */ protected $var_map = array(); protected $account_name; protected $account_config; protected $accountInfo; protected $url; protected $transactions; /** * $payment_methods will be defined by the adapter. * * @var array $payment_methods */ protected $payment_methods = array(); /** * $payment_submethods will be defined by the adapter. * * @var array $payment_submethods */ protected $payment_submethods = array(); /** * Staged variables. This is affected by the transaction type. * * @var array $staged_vars */ protected $staged_vars = array(); protected $return_value_map; protected $staged_data; protected $unstaged_data; /** * For gateways that speak XML, we use this variable to hold the document * while we build the outgoing request. TODO: move XML functions out of the * main gateway classes. * @var DomDocument */ protected $xmlDoc; /** * @var DonationData */ protected $dataObj; /** * Standard logger, logs to {type}_gateway * @var \Psr\Log\LoggerInterface */ protected $logger; /** * Logs to {type}_gateway_commstats * @var \Psr\Log\LoggerInterface */ protected $commstats_logger; /** * Logs to {type}_gateway_payment_init * @var \Psr\Log\LoggerInterface */ protected $payment_init_logger; /** * $transaction_response is the member var that keeps track of the results of * the latest discrete transaction with the gateway. * @var PaymentTransactionResponse */ protected $transaction_response; /** * @var string When the smoke clears, this should be set to one of the * constants defined in @see FinalStatus */ protected $final_status; protected $validation_errors; protected $manual_errors = array(); /** * Name of the current transaction. Set via @see setCurrentTransaction * @var string */ protected $current_transaction; protected $action; protected $risk_score = 0; public $debugarray; /** * A boolean that will tell us if we've posted to ourselves. A little more telling than * $wgRequest->wasPosted(), as something else could have posted to us. * @var boolean */ public $posted = false; protected $batch = false; protected $api_request = false; /** * Holds the global values we've already looked up. Used in getGlobal. * @staticvar array */ protected static $globalsCache = array ( ); //ALL OF THESE need to be redefined in the children. Much voodoo depends on the accuracy of these constants. const GATEWAY_NAME = 'Donation Gateway'; const IDENTIFIER = 'donation'; const GLOBAL_PREFIX = 'wgDonationGateway'; //...for example. public $log_outbound = FALSE; //This should be set to true for gateways that don't return the request in the response. @see buildLogXML() /** * Default response type to be the same as communication type. * @return string */ public function getResponseType() { return $this->getCommunicationType(); } /** * Get @see GatewayAdapter::$goToThankYouOn */ public function getGoToThankYouOn() { return $this->goToThankYouOn; } /** * Constructor * * @param array $options * OPTIONAL - You may set options for testing * - external_data - array, data from unusual sources (such as test fixture) * - api_request - Boolean, this is an api request, do not perform UI actions * * @see DonationData */ public function __construct( $options = array() ) { global $wgRequest; $defaults = array( 'external_data' => null, 'api_request' => false, ); $options = array_merge( $defaults, $options ); if ( array_key_exists( 'batch_mode', $options ) ) { $this->batch = $options['batch_mode']; unset( $options['batch_mode'] ); } $this->logger = DonationLoggerFactory::getLogger( $this ); $this->commstats_logger = DonationLoggerFactory::getLogger( $this, '_commstats' ); $this->payment_init_logger = DonationLoggerFactory::getLogger( $this, '_payment_init' ); if ( !self::getGlobal( 'Test' ) ) { $this->url = self::getGlobal( 'URL' ); } else { $this->url = self::getGlobal( 'TestingURL' ); } //so we know we can skip all the visual stuff. if ( $options['api_request'] ) { $this->setApiRequest(); } // The following needs to be set up before we initialize DonationData. // TODO: move the rest of the initialization here $this->defineOrderIDMeta(); $this->defineDataConstraints(); $this->definePaymentMethods(); $this->session_resetOnGatewaySwitch(); //clear out the old stuff before DD snarfs it. $this->dataObj = new DonationData( $this, $options['external_data'] ); $this->setValidationErrors( $this->getOriginalValidationErrors() ); $this->unstaged_data = $this->dataObj->getDataEscaped(); $this->staged_data = $this->unstaged_data; //checking to see if we have an edit token in the request... $this->posted = ( $this->dataObj->wasPosted() && (!is_null( $wgRequest->getVal( 'token', null ) ) ) ); $this->findAccount(); $this->defineAccountInfo(); $this->defineTransactions(); $this->defineErrorMap(); $this->defineVarMap(); $this->defineReturnValueMap(); $this->setValidForm(); $this->setGatewayDefaults( $options ); $this->stageData(); } /** * Determine which account to use for this session */ protected function findAccount() { $acctConfig = self::getGlobal( 'AccountInfo' ); //this is causing warns in Special:SpecialPages if ( !$acctConfig ) { return; } //TODO crazy logic to determine which account we want $accounts = array_keys( $acctConfig ); $this->account_name = array_shift( $accounts ); $this->account_config = $acctConfig[ $this->account_name ]; $this->addRequestData( array( 'gateway_account' => $this->account_name, ) ); } /** * Get the log message prefix: * $contribution_tracking_id . ':' . $order_id . ' ' * * Now, going to the DonationData object to handle this, because it will * always have less stale data (and we need messages to come out of * there before data exists here) * * @return string */ public function getLogMessagePrefix() { if ( !is_object( $this->dataObj ) ) { //please avoid exploding; It's just a log line. return 'Constructing!'; } return $this->dataObj->getLogMessagePrefix(); } /** * getThankYouPage should either return a full page url, or false. * @return mixed Page URL in string format, or false if none is set. */ public function getThankYouPage() { $page = self::getGlobal( "ThankYouPage" ); if ( $page ) { $page = $this->appendLanguageAndMakeURL( $page ); } return $page; } /** * getFailPage should either return a full page url, or false. * @return mixed Page URL in string format, or false if none is set. */ public function getFailPage() { //Prefer RapidFail. if ( self::getGlobal( "RapidFailPage" ) ) { $data = $this->getData_Unstaged_Escaped(); //choose which fail page to go for. try { $fail_ffname = GatewayFormChooser::getBestErrorForm( $data['gateway'], $data['payment_method'], $data['payment_submethod'] ); } catch ( Exception $e ) { $this->logger->error( 'Cannot determine best error form. ' . $e->getMessage() ); } return GatewayFormChooser::buildPaymentsFormURL( $fail_ffname, $this->getRetryData() ); } $page = self::getGlobal( "FailPage" ); if ( $page ) { $language = $this->getData_Unstaged_Escaped( 'language' ); $page .= '?uselang=' . $language; } return $page; } /** * For pages we intend to redirect to. This function will take either a full * URL or a page title, and turn it into a URL with the appropriate language * appended onto the end. * @param string $url Either a wiki page title, or a URL to an external wiki * page title. * @return string A URL */ protected function appendLanguageAndMakeURL( $url ){ $language = $this->getData_Unstaged_Escaped( 'language' ); //make sure we don't already have the language in there... $dirs = explode('/', $url); if ( !is_array($dirs) || !in_array( $language, $dirs ) ){ $url = $url . "/$language"; } if ( strpos( $url, 'http' ) === 0) { return $url; } else { //this isn't a url yet. $returnTitle = Title::newFromText( $url ); $url = $returnTitle->getFullURL(); return $url; } } /** * Checks the edit tokens in the user's session against the one gathered * from populated form data. * Adds a string to the debugarray, to make it a little easier to tell what * happened if we turn the debug results on. * Only called from the .body pages * @return boolean true if match, else false. */ public function checkTokens() { $checkResult = $this->token_checkTokens(); if ( $checkResult ) { $this->debugarray[] = 'Token Match'; } else { $this->debugarray[] = 'Token MISMATCH'; } $this->refreshGatewayValueFromSource( 'token' ); return $checkResult; } /** * Returns staged data from the adapter object, or null if a key was * specified and no value exsits. * @param string $val An optional specific key you want returned. * @return mixed All the staged data held by the adapter, or if a key was * set, the staged value for that key. */ protected function getData_Staged( $val = '' ) { if ( $val === '' ) { return $this->staged_data; } else { if ( array_key_exists( $val, $this->staged_data ) ) { return $this->staged_data[$val]; } else { return null; } } } /** * A helper function to let us stash extra data after the form has been submitted. * * @param array $dataArray An associative array of data. */ public function addRequestData( $dataArray ) { $this->dataObj->addData( $dataArray ); $calculated_fields = $this->dataObj->getCalculatedFields(); $data_fields = array_keys( $dataArray ); $data_fields = array_merge( $data_fields, $calculated_fields ); foreach ( $data_fields as $value ) { $this->refreshGatewayValueFromSource( $value ); } //and now check to see if you have to re-stage. //I'd fire off individual staging functions by value, but that's a //really bad idea, as multiple staged vars could be used in any staging //function, to calculate any other staged var. $changed_staged_vars = array_intersect( $this->staged_vars, $data_fields ); if ( count( $changed_staged_vars ) ) { $this->stageData(); } } /** * Add data from the processor to staged_data and run any unstaging functions. * * @param array $dataArray An associative array of data. */ public function addResponseData( $dataArray ) { foreach ( $dataArray as $key => $value ) { $this->staged_data[$key] = $value; } $this->unstageData( $dataArray ); // Only copy the affected values back into the normalized data. $newlyUnstagedData = array(); foreach ( $dataArray as $key => $stagedValue ) { if ( array_key_exists( $key, $this->unstaged_data ) ) { $newlyUnstagedData[$key] = $this->unstaged_data[$key]; } } $this->dataObj->addData( $newlyUnstagedData ); } /** * This is the ONLY getData type function anything should be using * outside the adapter. * Short explanation of the data population up to now: * *) When the gateway adapter is constructed, it constructs a DonationData * object. * *) On construction, the DonationData object pulls donation data from an * appropriate source, and normalizes the entire data set for storage. * *) The gateway adapter pulls normalized, html escaped data out of the * DonationData object, as the base of its own data set. * @param string $val The specific key you're looking for (if any) * @return mixed An array of all the raw, unstaged (but normalized and * sanitized) data sent to the adapter, or if $val was set, either the * specific value held for $val, or null if none exists. */ public function getData_Unstaged_Escaped( $val = '' ) { if ( $val === '' ) { return $this->unstaged_data; } else { if ( array_key_exists( $val, $this->unstaged_data ) ) { return $this->unstaged_data[$val]; } else { return null; } } } /** * This function is important. * All the globals in Donation Interface should be accessed in this manner * if they are meant to have a default value, but can be overridden by any * of the gateways. It will check to see if a gateway-specific global * exists, and if one is not set, it will pull the default from the * wgDonationInterface definitions. Through this function, it is no longer * necessary to define gateway-specific globals in LocalSettings unless you * wish to override the default value for all gateways. * If the variable exists in {prefix}AccountInfo[currentAccountName], * that value will override the default settings. * Caches found values in self::$globalsCache * * @param string $varname The global value we're looking for. It will first * look for a global named for the instantiated gateway's GLOBAL_PREFIX, * plus the $varname value. If that doesn't come up with anything that has * been set, it will use the default value for all of donation interface, * stored in $wgDonationInterface . $varname. * @return mixed The configured value for that gateway if it exists. If not, * the configured value for Donation Interface if it exists or not. */ static function getGlobal( $varname ) { //adding another layer of depth here, in case you're working with two gateways in the same request. //That does, in fact, ruin everything. :/ if ( !array_key_exists( self::getGlobalPrefix(), self::$globalsCache ) ) { self::$globalsCache[self::getGlobalPrefix()] = array ( ); } if ( !array_key_exists( $varname, self::$globalsCache[self::getGlobalPrefix()] ) ) { $globalname = self::getGlobalPrefix() . $varname; global $$globalname; if ( !isset( $$globalname ) ) { $globalname = "wgDonationInterface" . $varname; global $$globalname; //set or not. This is fine. } self::$globalsCache[self::getGlobalPrefix()][$varname] = $$globalname; } return self::$globalsCache[self::getGlobalPrefix()][$varname]; } /** * getErrorMap * * This will also return an error message if a $code is passed. * * If the error code does not exist, the default message will be returned. * * A default message should always exist with an index of 0. * * NOTE: This method will check to see if the message exists in translation * and use that message instead of the default. This would override error_map. * * @param string $code The error code to look up in the map * @param array $options * @return array|string Returns @see GatewayAdapter::$error_map */ public function getErrorMap( $code = null, $options = array() ) { if ( is_null( $code ) ) { return $this->error_map; } $defaults = array( 'translate' => false, ); $options = array_merge( $defaults, $options ); $response_message = $this->getIdentifier() . '_gateway-response-' . $code; $translatedMessage = WmfFramework::formatMessage( $response_message ); // FIXME: don't do this. // Check to see if an error message exists in translation if ( substr( $translatedMessage, 0, 3 ) !== '<' ) { // Message does not exist $translatedMessage = ''; } // If the $code does not exist, use the default message if ( isset( $this->error_map[ $code ] ) ) { $messageKey = $this->error_map[ $code ]; } else { $messageKey = 'donate_interface-processing-error'; } $translatedMessage = ( $options['translate'] && empty( $translatedMessage ) ) ? WmfFramework::formatMessage( $messageKey ) : $translatedMessage; // Check to see if we return the translated message. $message = ( $options['translate'] ) ? $translatedMessage : $messageKey; return $message; } /** * getErrorMapByCodeAndTranslate * * This will take an error code and translate the message. * * @param string $code The error code to look up in the map * * @return string Returns the translated message from @see GatewayAdapter::$error_map */ public function getErrorMapByCodeAndTranslate( $code ) { return $this->getErrorMap( $code, array( 'translate' => true, ) ); } /** * This function is used exclusively by the two functions that build * requests to be sent directly to external payment gateway servers. Those * two functions are buildRequestNameValueString, and (perhaps less * obviously) buildRequestXML. As such, unless a valid current transaction * has already been set, this will error out rather hard. * In other words: In all likelihood, this is not the function you're * looking for. * @param string $gateway_field_name The GATEWAY's field name that we are * hoping to populate. Probably not even remotely the way we name the same * data internally. * @param boolean $token This is a throwback to a road we nearly went down, * with ajax and client-side token replacement. The idea was, if this was * set to true, we would simply pass the fully-formed transaction structure * with our tokenized var names in the spots where form values would usually * go, so we could fetch the structure and have some client-side voodoo * populate the transaction so we wouldn't have to touch the data at all. * At this point, very likely cruft that can be removed, but as I'm not 100% * on that point, I'm keeping it for now. If we do kill off this param, we * should also get rid of the function buildTransactionFormat and anything * that calls it. * @throws LogicException * @return mixed The value we want to send directly to the gateway, for the * specified gateway field name. */ protected function getTransactionSpecificValue( $gateway_field_name, $token = false ) { if ( empty( $this->transactions ) ) { $msg = self::getGatewayName() . ': Transactions structure is empty! No transaction can be constructed.'; $this->logger->critical( $msg ); throw new LogicException( $msg ); } //Ensures we are using the correct transaction structure for our various lookups. $transaction = $this->getCurrentTransaction(); if ( !$transaction ){ return null; } //If there's a hard-coded value in the transaction definition, use that. if ( !empty( $transaction ) ) { if ( array_key_exists( $transaction, $this->transactions ) && is_array( $this->transactions[$transaction] ) && array_key_exists( 'values', $this->transactions[$transaction] ) && array_key_exists( $gateway_field_name, $this->transactions[$transaction]['values'] ) ) { return $this->transactions[$transaction]['values'][$gateway_field_name]; } } //if it's account info, use that. //$this->accountInfo; if ( array_key_exists( $gateway_field_name, $this->accountInfo ) ) { return $this->accountInfo[$gateway_field_name]; } //If there's a value in the post data (name-translated by the var_map), use that. if ( array_key_exists( $gateway_field_name, $this->var_map ) ) { if ( $token === true ) { //we just want the field name to use, so short-circuit all that mess. return '@' . $this->var_map[$gateway_field_name]; } $staged = $this->getData_Staged( $this->var_map[$gateway_field_name] ); if ( !is_null( $staged ) ) { //if it was sent, use that. return $staged; } else { //return blank string return ''; } } //not in the map, or hard coded. What then? //Complain furiously, for your code is faulty. $msg = self::getGatewayName() . ': Requested value ' . $gateway_field_name . ' cannot be found in the transactions structure.'; $this->logger->critical( $msg ); throw new LogicException( $msg ); } /** * Returns the current transaction request structure if it exists, otherwise * returns false. * Fails nicely if the current transaction is simply not set yet. * @throws LogicException if the transaction is set, but no structure is defined. * @return mixed current transaction's structure as an array, or false */ protected function getTransactionRequestStructure(){ $transaction = $this->getCurrentTransaction(); if ( !$transaction ){ return false; } if ( empty( $this->transactions ) || !array_key_exists( $transaction, $this->transactions ) || !array_key_exists( 'request', $this->transactions[$transaction] ) ) { $msg = self::getGatewayName() . ": $transaction request structure is empty! No transaction can be constructed."; $this->logger->critical( $msg ); throw new LogicException( $msg ); } return $this->transactions[$transaction]['request']; } /** * Builds a set of transaction data in name/value format * *)The current transaction must be set before you call this function. * *)Uses getTransactionSpecificValue to assign staged values to the * fields required by the gateway. Look there for more insight into the * heirarchy of all possible data sources. * @return string The raw transaction in name/value format, ready to be * curl'd off to the remote server. */ protected function buildRequestNameValueString() { // Look up the request structure for our current transaction type in the transactions array $structure = $this->getTransactionRequestStructure(); if ( !is_array( $structure ) ) { return ''; } $queryvals = array(); //we are going to assume a flat array, because... namevalue. foreach ( $structure as $fieldname ) { $fieldvalue = $this->getTransactionSpecificValue( $fieldname ); if ( $fieldvalue !== '' && $fieldvalue !== false ) { $queryvals[] = $fieldname . '=' . $fieldvalue; } } $ret = implode( '&', $queryvals ); return $ret; } /** * Builds a set of transaction data in XML format * *)The current transaction must be set before you call this function. * *)(eventually) uses getTransactionSpecificValue to assign staged * values to the fields required by the gateway. Look there for more insight * into the heirarchy of all possible data sources. * @return string The raw transaction in xml format, ready to be * curl'd off to the remote server. */ protected function buildRequestXML( $rootElement = 'XML', $encoding = 'UTF-8' ) { $this->xmlDoc = new DomDocument( '1.0', $encoding ); $node = $this->xmlDoc->createElement( $rootElement ); // Look up the request structure for our current transaction type in the transactions array $structure = $this->getTransactionRequestStructure(); if ( !is_array( $structure ) ) { return ''; } $this->buildTransactionNodes( $structure, $node ); $this->xmlDoc->appendChild( $node ); $return = $this->xmlDoc->saveXML(); if ( $this->log_outbound ) { $message = "Request XML: "; $full_structure = $this->transactions[$this->getCurrentTransaction()]; //if we've gotten this far, this exists. if ( array_key_exists( 'never_log', $full_structure ) ) { //Danger Zone! $message = "Cleaned $message"; //keep these totally separate. Do not want to risk sensitive information (like cvv) making it anywhere near the log. $this->xmlDoc = new DomDocument( '1.0' ); $log_node = $this->xmlDoc->createElement( $rootElement ); //remove all never_log nodes from the structure $log_structure = $this->cleanTransactionStructureForLogs( $structure, $full_structure['never_log'] ); $this->buildTransactionNodes( $log_structure, $log_node ); $this->xmlDoc->appendChild( $log_node ); $logme = $this->xmlDoc->saveXML(); } else { //...safe zone. $logme = $return; } $this->logger->info( $message . $logme ); } return $return; } /** * buildRequestXML helper function. * Builds the XML transaction by recursively crawling the transaction * structure and adding populated nodes by reference. * @param array $structure Current transaction's more leafward structure, * from the point of view of the current XML node. * @param xmlNode $node The current XML node. * @param bool $js More likely cruft relating back to buildTransactionFormat */ protected function buildTransactionNodes( $structure, &$node, $js = false ) { if ( !is_array( $structure ) ) { //this is a weird case that shouldn't ever happen. I'm just being... thorough. But, yeah: It's like... the base-1 case. $this->appendNodeIfValue( $structure, $node, $js ); } else { foreach ( $structure as $key => $value ) { if ( !is_array( $value ) ) { //do not use $key. $key is meaningless in this case. $this->appendNodeIfValue( $value, $node, $js ); } else { $keynode = $this->xmlDoc->createElement( $key ); $this->buildTransactionNodes( $value, $keynode, $js ); $node->appendChild( $keynode ); } } } //not actually returning anything. It's all side-effects. Because I suck like that. } /** * Recursively sink through a transaction structure array to remove all * nodes that we can't have showing up in the server logs. * Mostly for CVV: If we log those, we are all fired. * @param array $structure The transaction structure that we want to clean. * @param array $never_log An array of values we should never log. These values should be the gateway's transaciton nodes, rather than our normal values. * @return array $structure stripped of all references to the values in $never_log */ protected function cleanTransactionStructureForLogs( $structure, $never_log ) { foreach ( $structure as $node => $value ) { if ( is_array( $value ) ) { $structure[$node] = $this->cleanTransactionStructureForLogs( $value, $never_log ); } else { if ( in_array( $value, $never_log ) ) { unset( $structure[$node] ); } } } return $structure; } /** * appendNodeIfValue is a helper function for buildTransactionNodes, which * is used by buildRequestXML to construct an XML transaction. * This function will append an XML node to the transaction being built via * the passed-in parent node, only if the current node would have a * non-empty value. * @param string $value The GATEWAY's field name for the current node. * @param string $node The parent node this node will be contained in, if it * is determined to have a non-empty value. * @param bool $js Probably cruft at this point. This is connected to the * function buildTransactionFormat. */ protected function appendNodeIfValue( $value, &$node, $js = false ) { $nodevalue = $this->getTransactionSpecificValue( $value, $js ); if ( $nodevalue !== '' && $nodevalue !== false ) { $temp = $this->xmlDoc->createElement( $value, $nodevalue ); $node->appendChild( $temp ); } } /** * Performs a transaction through the gateway. Optionally may reattempt the transaction if * a recoverable gateway error occurred. * * This function provides all functionality to the external world to communicate with a * properly constructed gateway and handle all the return data in an appropriate manner. * -- Appropriateness is determined by the requested $transaction structure and definition/ * * @param string | $transaction The specific transaction type, like 'INSERT_ORDERWITHPAYMENT', * that maps to a first-level key in the $transactions array. * * @return PaymentTransactionResponse */ public function do_transaction( $transaction ) { $this->session_addDonorData(); if ( !$this->validatedOK() ){ //If the data didn't validate okay, prevent all data transmissions. $return = new PaymentTransactionResponse(); $return->setCommunicationStatus( false ); $return->setMessage( 'Failed data validation' ); foreach( $this->getAllErrors() as $code => $error ) { $return->addError( $code, array( 'message' => $error, 'logLevel' => LogLevel::INFO, 'debugInfo' => '' ) ); } // TODO: should we set $this->transaction_response ? $this->logger->info( "Failed Validation. Aborting $transaction " . print_r( $this->getValidationErrors(), true ) ); return $return; } $retryCount = 0; $loopCount = $this->getGlobal( 'RetryLoopCount' ); do { $retryVars = null; $retval = $this->do_transaction_internal( $transaction, $retryVars ); if ( !empty( $retryVars ) ) { // TODO: Add more intelligence here. Right now we just assume it's the order_id // and that it is totally OK to just reset it and reroll. $this->logger->info( "Repeating transaction on request for vars: " . implode( ',', $retryVars ) ); // Force regen of the order_id $this->regenerateOrderID(); // Pull anything changed from dataObj $this->unstaged_data = $this->dataObj->getDataEscaped(); $this->staged_data = $this->unstaged_data; $this->stageData(); } } while ( ( !empty( $retryVars ) ) && ( ++$retryCount < $loopCount ) ); if ( $retryCount >= $loopCount ) { $this->logger->error( "Transaction canceled after $retryCount retries." ); } return $retval; } /** * Called from do_transaction() in order to be able to deal with transactions that had * recoverable errors but that do require the entire transaction to be repeated. * * This function has the following extension hooks: * * pre_process_ * Called before the transaction is processed; intended to call setValidationAction() * if the transaction should not be performed. Anti-fraud can be performed in this * hook by calling $this->runAntifraudHooks(). * * * MediaWiki hook GatewayHandoff * Called if the gateway tranaction type is 'redirect' * * * post_process_ * * @param string $transaction Name of the transaction being performed * @param &string() $retryVars Reference to an array of variables that caused the * transaction to fail. * * @return PaymentTransactionResponse * @throws UnexpectedValueException */ final private function do_transaction_internal( $transaction, &$retryVars = null ) { $this->debugarray[] = __FUNCTION__ . " is doing a $transaction."; //reset, in case this isn't our first time. $this->transaction_response = new PaymentTransactionResponse(); $this->final_status = false; $this->setValidationAction( 'process', true ); $errCode = null; /* --- Build the transaction string for cURL --- */ try { $this->setCurrentTransaction( $transaction ); $this->executeIfFunctionExists( 'pre_process_' . $transaction ); if ( $this->getValidationAction() != 'process' ) { $this->logger->info( "Failed pre-process checks for transaction type $transaction." ); $this->transaction_response->setCommunicationStatus( false ); $this->transaction_response->setMessage( $this->getErrorMapByCodeAndTranslate( 'internal-0000' ) ); $this->transaction_response->setErrors( array( 'internal-0000' => array( 'debugInfo' => "Failed pre-process checks for transaction type $transaction.", 'message' => $this->getErrorMapByCodeAndTranslate( 'internal-0000' ), 'logLevel' => LogLevel::INFO ) ) ); return $this->transaction_response; } if ( !$this->isBatchProcessor() ) { //TODO: Maybe move this to the pre_process functions? $this->dataObj->saveContributionTrackingData(); } $commType = $this->getCommunicationType(); if ( $commType === 'redirect' ) { WmfFramework::runHooks( 'GatewayHandoff', array ( $this ) ); //in the event that we have a redirect transaction that never displays the form, //save this most recent one before we leave. $this->session_pushRapidHTMLForm( $this->getData_Unstaged_Escaped( 'ffname' ) ); $this->transaction_response->setCommunicationStatus( true ); $this->transaction_response->setRedirect( $this->url ); return $this->transaction_response; } elseif ( $commType === 'xml' ) { $this->getStopwatch( "buildRequestXML", true ); // begin profiling $curlme = $this->buildRequestXML(); // build the XML $this->saveCommunicationStats( "buildRequestXML", $transaction ); // save profiling data } elseif ( $commType === 'namevalue' ) { $this->getStopwatch( "buildRequestNameValueString", true ); // begin profiling $curlme = $this->buildRequestNameValueString(); // build the name/value pairs $this->saveCommunicationStats( "buildRequestNameValueString", $transaction ); // save profiling data } else { throw new UnexpectedValueException( "Communication type of '{$commType}' unknown" ); } } catch ( Exception $e ) { $this->logger->critical( 'Malformed gateway definition. Cannot continue: Aborting.\n' . $e->getMessage() ); $this->transaction_response->setCommunicationStatus( false ); $this->transaction_response->setMessage( $this->getErrorMapByCodeAndTranslate( 'internal-0001' ) ); $this->transaction_response->setErrors( array( 'internal-0001' => array( 'debugInfo' => 'Malformed gateway definition. Cannot continue: Aborting.\n' . $e->getMessage(), 'message' => $this->getErrorMapByCodeAndTranslate( 'internal-0001' ), 'logLevel' => LogLevel::CRITICAL ) ) ); return $this->transaction_response; } /* --- Do the cURL request --- */ $this->getStopwatch( __FUNCTION__, true ); $txn_ok = $this->curl_transaction( $curlme ); if ( $txn_ok === true ) { //We have something to slice and dice. $this->logger->info( "RETURNED FROM CURL:" . print_r( $this->transaction_response->getRawResponse(), true ) ); // Decode the response according to $this->getResponseType $formatted = $this->getFormattedResponse( $this->transaction_response->getRawResponse() ); // Process the formatted response. This will then drive the result action try{ $this->processResponse( $formatted ); } catch ( ResponseProcessingException $ex ) { $errCode = $ex->getErrorCode(); $retryVars = $ex->getRetryVars(); $this->transaction_response->addError( $errCode, array( 'message' => $this->getErrorMapByCodeAndTranslate( 'internal-0001' ), 'debugInfo' => $ex->getMessage(), 'logLevel' => LogLevel::ERROR ) ); } } elseif ( $txn_ok === false ) { //nothing to process, so we have to build it manually $logMessage = 'Transaction Communication failed' . print_r( $this->transaction_response, true ); $this->logger->error( $logMessage ); $this->transaction_response->setCommunicationStatus( false ); $this->transaction_response->setMessage( $this->getErrorMapByCodeAndTranslate( 'internal-0002' ) ); $this->transaction_response->setErrors( array( 'internal-0002' => array( 'debugInfo' => $logMessage, 'message' => $this->getErrorMapByCodeAndTranslate( 'internal-0002' ), 'logLevel' => LogLevel::ERROR ) ) ); } // Log out how much time it took for the cURL request $this->saveCommunicationStats( __FUNCTION__, $transaction ); if ( !empty( $retryVars ) ) { $this->logger->critical( "$transaction Communication failed (errcode $errCode), will reattempt!" ); // Set this by key so that the result object still has all the cURL data $this->transaction_response->setCommunicationStatus( false ); $this->transaction_response->setMessage( $this->getErrorMapByCodeAndTranslate( $errCode ) ); $this->transaction_response->setErrors( array( $errCode => array( 'debugInfo' => "$transaction Communication failed (errcode $errCode), will reattempt!", 'message' => $this->getErrorMapByCodeAndTranslate( $errCode ), 'logLevel' => LogLevel::CRITICAL ) ) ); } //if we have set errors by this point, the transaction is not okay $errors = $this->getTransactionErrors(); if ( !empty( $errors ) ) { $txn_ok = false; } //If we have any special post-process instructions for this //transaction, do 'em. //NOTE: If you want your transaction to fire off the post-process //hooks, you need to run $this->runPostProcessHooks in a function //called // 'post_process' . strtolower($transaction) //in the appropriate gateway object. if ( $txn_ok && empty( $retryVars ) ) { $this->executeIfFunctionExists( 'post_process_' . $transaction ); if ( $this->getValidationAction() != 'process' ) { $this->logger->info( "Failed post-process checks for transaction type $transaction." ); $this->transaction_response->setCommunicationStatus( false ); $this->transaction_response->setMessage( $this->getErrorMapByCodeAndTranslate( 'internal-0000' ) ); $this->transaction_response->setErrors( array( 'internal-0000' => array( 'debugInfo' => "Failed post-process checks for transaction type $transaction.", 'message' => $this->getErrorMapByCodeAndTranslate( 'internal-0000' ), 'logLevel' => LogLevel::INFO ) ) ); return $this->transaction_response; } } // log that the transaction is essentially complete $this->logger->info( 'Transaction complete.' ); $this->debugarray[] = 'numAttempt = ' . self::session_getData( 'numAttempt' ); return $this->transaction_response; } function getCurlBaseOpts() { //I chose to return this as a function so it's easy to override. //TODO: probably this for all the junk I currently have stashed in the constructor. //...maybe. $path = $this->transaction_option( 'path' ); if ( !$path ) { $path = ''; } $opts = array( CURLOPT_URL => $this->url . $path, CURLOPT_USERAGENT => WmfFramework::getUserAgent(), CURLOPT_HEADER => 1, CURLOPT_RETURNTRANSFER => 1, CURLOPT_TIMEOUT => self::getGlobal( 'Timeout' ), CURLOPT_FOLLOWLOCATION => 0, CURLOPT_SSL_VERIFYPEER => 1, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_FORBID_REUSE => true, CURLOPT_POST => 1, ); return $opts; } function getCurlBaseHeaders() { $content_type = 'application/x-www-form-urlencoded'; if ( $this->getCommunicationType() === 'xml' ) { $content_type = 'text/xml'; } $headers = array( 'Content-Type: ' . $content_type . '; charset=utf-8', 'X-VPS-Client-Timeout: 45', 'X-VPS-Request-ID:' . $this->getData_Staged( 'order_id' ), ); return $headers; } /** * Sets the transaction you are about to send to the payment gateway. This * will throw an exception if you try to set it to something that has no * transaction definition. * @param type $transaction_name This is a specific transaction type like * 'INSERT_ORDERWITHPAYMENT' (if you're GlobalCollect) that maps to a * first-level key in the $transactions array. * @throws UnexpectedValueException */ public function setCurrentTransaction( $transaction_name ){ if ( empty( $this->transactions ) || !is_array( $this->transactions ) || !array_key_exists( $transaction_name, $this->transactions ) ) { $msg = self::getGatewayName() . ': Transaction Name "' . $transaction_name . '" undefined for this gateway.'; $this->logger->alert( $msg ); throw new UnexpectedValueException( $msg ); } else { $this->current_transaction = $transaction_name; } // XXX WIP $override_options = array( 'url', ); foreach ( $override_options as $key ) { $override_val = $this->transaction_option( $key ); // XXX this hack should probably be pushed down to something // like "setTransactionOptions" so that we can override with // a NULL value when we need to if ( $override_val !== NULL ) { $this->$key = $override_val; } } } /** * Gets the currently set transaction name. This value should only ever be * set with setCurrentTransaction: A function that ensures the current * transaction maps to a first-level key that is known to exist in the * $transactions array, defined in the child gateway. * @return mixed The name of the properly set transaction, or false if none * has been set. */ public function getCurrentTransaction(){ if ( is_null( $this->current_transaction ) ) { return false; } else { return $this->current_transaction; } } /** * Get the payment method * * @return string */ public function getPaymentMethod() { //FIXME: this should return the final calculated method return $this->getData_Unstaged_Escaped('payment_method'); } /** * Define payment methods * * Not all payment methods are available within an adapter * * @return array Returns the available payment methods for the specific adapter */ public function getPaymentMethods() { return $this->payment_methods; } /** * Get the payment submethod * * @return string */ public function getPaymentSubmethod() { return $this->getData_Unstaged_Escaped('payment_submethod'); } /** * Define payment methods * * @todo * - this is not implemented in all adapters yet * * Not all payment submethods are available within an adapter * * @return array Returns the available payment submethods for the specific adapter */ public function getPaymentSubmethods() { return $this->payment_submethods; } /** * Sends a curl request to the gateway server, and gets a response. * Saves that response to the transaction_response's rawResponse; * @param string $data the raw data we want to curl up to a server somewhere. * Should have been constructed with either buildRequestNameValueString, or * buildRequestXML. * @return boolean true if the communication was successful and there is a * parseable response, false if there was a fundamental communication * problem. (timeout, bad URL, etc.) */ protected function curl_transaction( $data ) { $this->getStopwatch( __FUNCTION__, true ); // Basic variable init $retval = false; // By default return that we failed $gatewayName = self::getGatewayName(); $email = $this->getData_Unstaged_Escaped( 'email' ); /** * This log line is pretty important. Usually when a donor contacts us * saying that they have experienced problems donating, the first thing * we have to do is associate a gateway transaction ID and ctid with an * email address. If the cURL function fails, we lose the ability to do * that association outside of this log line. */ $this->logger->info( "Initiating cURL for donor $email" ); // Initialize cURL and construct operation (also run hook) $ch = curl_init(); $hookResult = WmfFramework::runHooks( 'DonationInterfaceCurlInit', array( &$this ) ); if ( $hookResult == false ) { $this->logger->info( 'cURL transaction aborted on hook DonationInterfaceCurlInit' ); $this->setValidationAction('reject'); return false; } // assign header data necessary for the curl_setopt() function $headers = $this->getCurlBaseHeaders(); $headers[] = 'Content-Length: ' . strlen( $data ); $curl_opts = $this->getCurlBaseOpts(); $curl_opts[CURLOPT_HTTPHEADER] = $headers; $curl_opts[CURLOPT_POSTFIELDS] = $data; curl_setopt_array( $ch, $curl_opts ); // As suggested in the PayPal developer forum sample code, try more than once to get a // response in case there is a general network issue $continue = true; $tries = 0; $curl_response = false; $loopCount = $this->getGlobal( 'RetryLoopCount' ); do { $this->logger->info( "Preparing to send {$this->getCurrentTransaction()} transaction to $gatewayName" ); // Execute the cURL operation $curl_response = $this->curl_exec( $ch ); if ( $curl_response !== false ) { // The cURL operation was at least successful, what happened in it? $headers = $this->curl_getinfo( $ch ); $httpCode = $headers['http_code']; switch ( $httpCode ) { case 200: // Everything is AWESOME $continue = false; $this->logger->debug( "Successful transaction to $gatewayName" ); $this->transaction_response->setRawResponse( $curl_response ); $retval = true; break; case 400: // Oh noes! Bad request.. BAD CODE, BAD BAD CODE! $continue = false; $this->logger->error( "$gatewayName returned (400) BAD REQUEST: $curl_response" ); // Even though there was an error, set the results. Amazon at least gives // us useful XML return $this->transaction_response->setRawResponse( $curl_response ); $retval = true; break; case 403: // Hmm, forbidden? Maybe if we ask it nicely again... $continue = true; $this->logger->alert( "$gatewayName returned (403) FORBIDDEN: $curl_response" ); break; default: // No clue what happened... break out and log it $continue = false; $this->logger->error( "$gatewayName failed remotely and returned ($httpCode): $curl_response" ); break; } } else { // Well the cURL transaction failed for some reason or another. Try again! $continue = true; $errno = $this->curl_errno( $ch ); $err = curl_error( $ch ); $this->logger->alert( "cURL transaction to $gatewayName failed: ($errno) $err" ); } $tries++; if ( $tries >= $loopCount ) { $continue = false; } if ( $continue ) { // If we're going to try again, log timing for this particular curl attempt and reset $this->saveCommunicationStats( __FUNCTION__, $this->getCurrentTransaction(), "cURL problems" ); $this->getStopwatch( __FUNCTION__, true ); } } while ( $continue ); // End while cURL transaction hasn't returned something useful // Clean up and return curl_close( $ch ); $log_results = array( 'result' => $curl_response, 'headers' => $headers, ); $this->saveCommunicationStats( __FUNCTION__, $this->getCurrentTransaction(), "Response: " . print_r( $log_results, true ) ); return $retval; } /** * Wrapper for the real curl_exec so we can override with magic for unit tests. * @param resource $ch curl handle (returned from curl_init) * @return mixed True or the result on success (depends if * CURLOPT_RETURNTRANSFER is set or not). False on total failure. */ protected function curl_exec( $ch ) { return curl_exec( $ch ); } /** * Wrapper for the real curl_getinfo so we can override with magic for unit tests. * @param resource $ch curl handle (returned from curl_init) * @return mixed an array, string, or false on total failure. */ protected function curl_getinfo( $ch ) { return curl_getinfo( $ch ); } /** * Wrapper for the real curl_errno so we can override with magic for unit tests. * @param resource $ch curl handle (returned from curl_init) * @return int the error number or 0 if none occurred */ protected function curl_errno( $ch ) { return curl_errno( $ch ); } /** * Check the response for general sanity - e.g. correct data format, keys exists * @return boolean true if response looks sane */ protected function parseResponseCommunicationStatus( $response ) { return true; } /** * Parse the response to get the errors in a format we can log and otherwise deal with. * @return array a key/value array of codes (if they exist) and messages. */ protected function parseResponseErrors( $response ) { return array(); } /** * This is only public for the orphan script. * Harvest the data we need back from the gateway. * @return array a key/value array */ public function parseResponseData( $response ) { return array(); } /** * Take the entire response string, and strip everything we don't care * about. For instance: If it's XML, we only want correctly-formatted XML. * Headers must be killed off. * @param string $rawResponse hot off the curl * @return string|DomDocument|array depending on $this->getResponseType * @throws InvalidArgumentException * @throws LogicException */ function getFormattedResponse( $rawResponse ) { $type = $this->getResponseType(); if ( $type === 'xml' ) { $xmlString = $this->stripXMLResponseHeaders( $rawResponse ); $displayXML = $this->formatXmlString( $xmlString ); $realXML = new DomDocument( '1.0' ); //DO NOT alter the line below unless you are prepared to also alter the GC audit scripts. //...and everything that references "Raw XML Response" //@TODO: All three of those things. $this->logger->info( "Raw XML Response:\n" . $displayXML ); //I am apparently a huge fibber. $realXML->loadXML( trim( $xmlString ) ); return $realXML; } // For anything else, delete all the headers and the blank line after $noHeaders = preg_replace( '/^.*?\n\r?\n/ms', '', $rawResponse, 1 ); $this->logger->info( "Raw Response:" . $noHeaders ); if ( $type === 'json' ) { return json_decode( $noHeaders, true ); } if ( $type === 'delimited' ) { $delimiter = $this->transaction_option( 'response_delimiter' ); $keys = $this->transaction_option( 'response_keys' ); if ( !$delimiter || !$keys ) { throw new LogicException( 'Delimited transactions must define both response_delimiter and response_keys options' ); } $values = explode( $delimiter, trim( $noHeaders ) ); $combined = array_combine( $keys, $values ); if ( $combined === FALSE ) { throw new InvalidArgumentException( 'Wrong number of values found in delimited response.'); } return $combined; } return $noHeaders; } function stripXMLResponseHeaders( $rawResponse ) { $xmlStart = strpos( $rawResponse, '... //...Weaken to almost no error checking. Buckle up! $xmlStart = strpos( $rawResponse, '<' ); } if ( $xmlStart === false ) { //Still false. Your Head Asplode. $this->logger->error( "Completely Mangled Response:\n" . $rawResponse ); return false; } $justXML = substr( $rawResponse, $xmlStart ); return $justXML; } //To avoid reinventing the wheel: taken from http://recursive-design.com/blog/2007/04/05/format-xml-with-php/ function formatXmlString( $xml ) { // add marker linefeeds to aid the pretty-tokeniser (adds a linefeed between all tag-end boundaries) $xml = preg_replace( '/(>)(<)(\/*)/', "$1\n$2$3", $xml ); // now indent the tags $token = strtok( $xml, "\n" ); $result = ''; // holds formatted version as it is built $pad = 0; // initial indent $matches = array(); // returns from preg_matches() // scan each line and adjust indent based on opening/closing tags while ( $token !== false ) : // test for the various tag states // 1. open and closing tags on same line - no change if ( preg_match( '/.+<\/\w[^>]*>$/', $token, $matches ) ) : $indent = 0; // 2. closing tag - outdent now elseif ( preg_match( '/^<\/\w/', $token, $matches ) ) : $pad--; // 3. opening tag - don't pad this one, only subsequent tags elseif ( preg_match( '/^<\w[^>]*[^\/]>.*$/', $token, $matches ) ) : $indent = 1; // 4. no indentation needed else : $indent = 0; endif; // pad the line with the required number of leading spaces $line = str_pad( $token, strlen( $token ) + $pad, ' ', STR_PAD_LEFT ); $result .= $line . "\n"; // add to the cumulative result, with linefeed $token = strtok( "\n" ); // get the next token $pad += $indent; // update the pad size for subsequent lines endwhile; return $result; } static function getGatewayName() { $c = get_called_class(); return $c::GATEWAY_NAME; } static function getGlobalPrefix() { $c = get_called_class(); return $c::GLOBAL_PREFIX; } static function getIdentifier() { $c = get_called_class(); return $c::IDENTIFIER; } static function getLogIdentifier() { return self::getIdentifier() . '_gateway'; } /** * getStopwatch keeps track of how long things take, for logging, * output, determining if we should loop on some method again... whatever. * @staticvar array $start The microtime at which a stopwatch was started. * @param string $string Some identifier for each stopwatch value we want to * keep. Each unique $string passed in will get its own value in $start. * @param bool $reset If this is set to true, it will reset any $start value * recorded for the $string identifier. * @return numeric The difference in microtime (rounded to 4 decimal places) * between the $start value, and now. */ public function getStopwatch( $string, $reset = false ) { static $start = array(); $now = microtime( true ); if ( empty( $start ) || !array_key_exists( $string, $start ) || $reset === true ) { $start[$string] = $now; } $clock = round( $now - $start[$string], 4 ); $this->logger->info( "Clock at $string: $clock ($now)" ); return $clock; } /** * @param string $function This is the function name that identifies the * stopwatch that should have already been started with the getStopwatch * function. * @param string $additional Additional information about the thing we're * currently timing. Meant to be easily searchable. * @param string $vars Intended to be particular values of any variables * that might be of interest. */ public function saveCommunicationStats( $function = '', $additional = '', $vars = '' ) { static $saveStats = null; static $saveDB = null; if ( $saveStats === null ){ $saveStats = self::getGlobal( 'SaveCommStats' ); } if ( !$saveStats ){ return; } if ( $saveDB === null && !$this->isBatchProcessor() ) { $db = ContributionTrackingProcessor::contributionTrackingConnection(); if ( $db->tableExists( 'communication_stats' ) ) { $saveDB = true; } else { $saveDB = false; } } $params = array( 'contribution_id' => $this->getData_Unstaged_Escaped( 'contribution_tracking_id' ), 'duration' => $this->getStopwatch( $function ), 'gateway' => self::getGatewayName(), 'function' => $function, 'vars' => $vars, 'additional' => $additional, ); if ( $saveDB ){ $db = ContributionTrackingProcessor::contributionTrackingConnection(); $params['ts'] = $db->timestamp(); $db->insert( 'communication_stats', $params ); } else { //save to syslog. But which syslog? $msg = ''; foreach ($params as $key=>$val){ $msg .= "$key:$val - "; } $this->commstats_logger->info( $msg ); } } function xmlChildrenToArray( $xml, $nodename ) { $data = array(); foreach ( $xml->getElementsByTagName( $nodename ) as $node ) { foreach ( $node->childNodes as $childnode ) { if ( trim( $childnode->nodeValue ) != '' ) { $data[$childnode->nodeName] = $childnode->nodeValue; } } } return $data; } /** * addCodeRange is used to define ranges of response codes for major * gateway transactions, that let us know what status bucket to sort * them into. * DO NOT DEFINE OVERLAPPING RANGES! * TODO: Make sure it won't let you add overlapping ranges. That would * probably necessitate the sort moving to here, too. * @param string $transaction The transaction these codes map to. * @param string $key The (incoming) field name containing the numeric codes * we're defining here. * @param string $action One of the constants defined in @see FinalStatus. * @param int $lower The integer value of the lower-bound in this code range. * @param int $upper Optional: The integer value of the upper-bound in the * code range. If omitted, it will make a range of one value: The lower bound. * @throws UnexpectedValueException * @return void */ protected function addCodeRange( $transaction, $key, $action, $lower, $upper = null ) { if ( $upper === null ) { $this->return_value_map[$transaction][$key][$lower] = $action; } else { $this->return_value_map[$transaction][$key][$upper] = array( 'action' => $action, 'lower' => $lower ); } } /** * findCodeAction * * @param string $transaction * @param string $key The key to lookup in the transaction such as STATUSID * @param integer|string $code This gets converted to an integer if the values is numeric. * FIXME: We should be pulling $code out of the current transaction fields, internally. * FIXME: Rename to reflect that these are Final Status values, not validation actions * @return null|string Returns the code action if a valid code is supplied. Otherwise, the return is null. */ public function findCodeAction( $transaction, $key, $code ) { $this->getStopwatch( __FUNCTION__, true ); // Do not allow anything that is not numeric if ( !is_numeric( $code ) ) { return null; } // Cast the code as an integer settype( $code, 'integer'); // Check to see if the transaction is defined if ( !array_key_exists( $transaction, $this->return_value_map ) ) { return null; } // Verify the key exists within the transaction if ( !array_key_exists( $key, $this->return_value_map[ $transaction ] ) || !is_array( $this->return_value_map[ $transaction ][ $key ] ) ) { return null; } //sort the array so we can do this quickly. ksort( $this->return_value_map[ $transaction ][ $key ], SORT_NUMERIC ); $ranges = $this->return_value_map[ $transaction ][ $key ]; //so, you have a code, which is a number. You also have a numerically sorted array. //loop through until you find an upper >= your code. //make sure it's in the range, and return the action. foreach ( $ranges as $upper => $val ) { if ( $upper >= $code ) { //you've arrived. It's either here or it's nowhere. if ( is_array( $val ) ) { if ( $val['lower'] <= $code ) { return $val['action']; } else { return null; } } else { if ( $upper === $code ) { return $val; } else { return null; } } } } //if we walk straight off the end... return null; } /** * Saves a stomp frame to the configured server and queue, based on the * outcome of our current transaction. * The big tricky thing here, is that we DO NOT SET a FinalStatus, * unless we have just learned what happened to a donation in progress, * through performing the current transaction. * To put it another way, getFinalStatus should always return * false, unless it's new data about a new transaction. In that case, the * outcome will be assigned and the proper queue selected. * * Probably called in runPostProcessHooks(), which is itself most likely to * be called through executeFunctionIfExists, later on in do_transaction. */ protected function doStompTransaction() { $status = $this->getFinalStatus(); switch ( $status ) { case FinalStatus::COMPLETE: $this->pushMessage( 'complete' ); break; case FinalStatus::PENDING: case FinalStatus::PENDING_POKE: // FIXME: I don't understand what the pending queue does. $this->pushMessage( 'pending' ); break; default: // No action $this->logger->info( "Not sending queue message for status {$status}." ); } } - /** - * Function that adds a stomp message to a special 'limbo' queue, for data - * that is either highly likely or completely guaranteed to be bifurcated by - * handing the ball to a third-party process. - * - * @param bool $antiMessage If TRUE message will be formatted to destroy a message in the limbo - * queue when the orphan slayer is run. - * - * @return null - */ - protected function doLimboStompTransaction( $antiMessage = false ) { - if ( !$this->getGlobal( 'EnableStomp' ) ){ - return; - } - - $this->debugarray[] = "Attempting Limbo Stomp Transaction!"; - - $transaction = $this->getStompTransaction( $antiMessage ); - - try { - WmfFramework::runHooks( 'gwStomp', array( $transaction, 'limbo' ) ); - } catch ( Exception $e ) { - $this->logger->critical( "STOMP ERROR. Could not add message to 'limbo' queue: {$e->getMessage()} " . json_encode( $transaction ) ); - } - } - /** * Formats an array in preparation for dispatch to a STOMP queue * - * @param bool $antiMessage If TRUE, message will be prepared to destroy - * @param bool $recoverTimestamp If TRUE the timestamp will be set to any recoverable timestamp - * from the transaction. If it cannot be recovered or this argument is false, it will take the - * current time. - * * @return array Pass this return array to STOMP :) * * TODO: Stop saying "STOMP". */ - protected function getStompTransaction( $antiMessage = false, $recoverTimestamp = false ) { + protected function getStompTransaction() { $transaction = array( 'gateway_txn_id' => $this->getTransactionGatewayTxnID(), 'payment_method' => $this->getData_Unstaged_Escaped( 'payment_method' ), 'response' => $this->getTransactionMessage(), // Can this be deprecated? 'correlation-id' => $this->getCorrelationID(), 'php-message-class' => 'SmashPig\CrmLink\Messages\DonationInterfaceMessage', 'gateway' => $this->getData_Unstaged_Escaped( 'gateway' ), ); - if ( $antiMessage == true ) { - // As anti-messages only exist to destroy messages all we need is the identifier - $transaction['antimessage'] = 'true'; - } else { - // Else we actually need the rest of the data - $stomp_data = array_intersect_key( - $this->getData_Unstaged_Escaped(), - array_flip( $this->dataObj->getStompMessageFields() ) - ); + // Else we actually need the rest of the data + $stomp_data = array_intersect_key( + $this->getData_Unstaged_Escaped(), + array_flip( $this->dataObj->getStompMessageFields() ) + ); - // The order here is important, values in $transaction are considered more definitive - // in case the transaction already had keys with those values - $transaction = array_merge( $stomp_data, $transaction ); - - // And now determine the date; which is annoyingly not as easy as one would like it - // if we're attempting to recover some data: ie: we're an orphan - $timestamp = null; - if ( $recoverTimestamp === true ) { - if ( !is_null( $this->getData_Unstaged_Escaped( 'date' ) ) ) { - $timestamp = $this->getData_Unstaged_Escaped( 'date' ); - } elseif ( !is_null( $this->getData_Unstaged_Escaped( 'ts' ) ) ) { - // That this works is mildly surprising - $timestamp = strtotime( $this->getData_Unstaged_Escaped( 'ts' ) ); - } - } - $transaction['date'] = ( $timestamp === null ) ? time() : $timestamp; - } + // The order here is important, values in $transaction are considered more definitive + // in case the transaction already had keys with those values + $transaction = array_merge( $stomp_data, $transaction ); + + // FIXME: Note that we're not using any existing date or ts fields. Why is that? + $transaction['date'] = time(); return $transaction; } /** * For making freeform stomp messages. * As these are all non-critical, we don't need to be as strict as we have been with the other stuff. * But, we've got to have some standards. * @param array $transaction The fields that we are interested in sending. * @return array The fields that will actually be sent. So, $transaction ++ some other things we think we're likely to always need. */ public function makeFreeformStompTransaction( $transaction ) { if ( !array_key_exists( 'php-message-class', $transaction ) ) { $this->logger->warning( "Trying to send a freeform STOMP message with no class defined. Bad programmer." ); $transaction['php-message-class'] = 'undefined-loser-message'; } //bascially, add all the stuff we have come to take for granted, because syslog. $transaction['gateway_txn_id'] = $this->getTransactionGatewayTxnID(); $transaction['correlation-id'] = $this->getCorrelationID(); $transaction['date'] = ( int ) time(); //I know this looks odd. Just trust me here. $transaction['server'] = WmfFramework::getHostname(); $these_too = array ( 'gateway', 'contribution_tracking_id', 'order_id', 'payment_method', //the stomp sender gets mad if we don't have this. @TODO: Stop being lazy someday. ); foreach ( $these_too as $field ) { $transaction[$field] = $this->getData_Unstaged_Escaped( $field ); } return $transaction; } protected function getCorrelationID(){ return $this->getIdentifier() . '-' . $this->getData_Unstaged_Escaped('order_id'); } /** * Executes the specified function in $this, if one exists. * NOTE: THIS WILL LCASE YOUR FUNCTION_NAME. * ...I like to keep the voodoo functions tidy. * @param string $function_name The name of the function you're hoping to * execute. * @param mixed $parameter That's right: For now you only get one. * @return bool True if a function was found and executed. */ function executeIfFunctionExists( $function_name, $parameter = null ) { $function_name = strtolower( $function_name ); //Because, that's why. if ( method_exists( $this, $function_name ) ) { $this->{$function_name}( $parameter ); return true; } else { return false; } } /** * Run any staging functions provided by the adapter */ protected function stageData() { // Copy data, the default is to not change the values. //reset from our normalized unstaged data so we never double-stage $this->staged_data = $this->unstaged_data; // This allows transactions to each stage different data. $this->defineStagedVars(); // Always stage email address first, to set default if missing array_unshift( $this->staged_vars, 'email' ); foreach ( $this->staged_vars as $field ) { $function_name = 'stage_' . $field; $this->executeIfFunctionExists( $function_name ); } // Format the staged data $this->formatStagedData(); } /** * Run any unstaging functions to decode processor responses * * @param array $data response data */ protected function unstageData( $data ) { foreach ( $data as $field => $value ) { // Run custom unstaging function if available. $function_name = 'unstage_' . $field; $isUnstaged = $this->executeIfFunctionExists( $function_name ); // Otherwise, copy the value directly. if ( !$isUnstaged ) { $this->unstaged_data[$field] = $this->staged_data[$field]; } } } /** * Format staged data * * Formatting: * - trim - all strings * - truncate - all strings to the maximum length permitted by the gateway */ public function formatStagedData() { foreach ( $this->staged_data as $field => $value ) { // Trim all values if they are a string $value = is_string( $value ) ? trim( $value ) : $value; if ( isset( $this->dataConstraints[ $field ] ) && is_string( $value ) ) { // Truncate the field if it has a length specified if ( isset( $this->dataConstraints[ $field ]['length'] ) ) { $length = (integer) $this->dataConstraints[ $field ]['length']; } else { $length = false; } if ( !empty( $length ) && !empty( $value ) ) { //Note: This is the very last resort. This should already have been dealt with thoroughly in staging. $value = substr( $value, 0, $length ); } } else { //$this->logger->debug( 'Field does not exist in $this->dataConstraints[ ' . ( string ) $field . ' ]' ); } $this->staged_data[ $field ] = $value; } } /** * Stage: amount * * For example: JPY 1000.05 get changed to 100005. This need to be 100000. * For example: JPY 1000.95 get changed to 100095. This need to be 100000. */ protected function stage_amount() { if ( !$this->getData_Unstaged_Escaped( 'amount' ) || !$this->getData_Unstaged_Escaped( 'currency_code' ) ) { //can't do anything with amounts at all. Just go home. unset( $this->staged_data['amount'] ); return; } $amount = $this->getData_Unstaged_Escaped( 'amount' ); if ( !DataValidator::is_fractional_currency( $this->getData_Unstaged_Escaped( 'currency_code' ) ) ) { $amount = floor( $amount ); } $this->staged_data['amount'] = $amount * 100; } protected function unstage_amount() { $this->unstaged_data['amount'] = $this->getData_Staged( 'amount' ) / 100; } /** * Stage the street address * * In the event that there isn't anything in there, we need to send * something along so that AVS checks get triggered at all. * * The zero is intentional: Allegedly, Some banks won't perform the check * if the address line contains no numerical data. */ protected function stage_street() { $street = ''; if ( isset( $this->unstaged_data['street'] ) ) { $street = trim( $this->unstaged_data['street'] ); } if ( !$street || !DataValidator::validate_not_just_punctuation( $street ) ) { $this->staged_data['street'] = 'N0NE PROVIDED'; //The zero is intentional. See function comment. } } /** * Stage the zip / postal code * * In the event that there isn't anything in there, we need to send * something along so that AVS checks get triggered at all. */ protected function stage_zip() { $zip = ''; if ( isset( $this->unstaged_data['zip'] ) ) { $zip = trim( $this->unstaged_data['zip'] ); } if ( strlen( $zip ) === 0 ) { //it would be nice to check for more here, but the world has some //straaaange postal codes... $this->staged_data['zip'] = '0'; } //country-based zip grooming to make AVS (marginally) happy switch ( $this->getData_Unstaged_Escaped( 'country' ) ) { case 'CA': //Canada goes "A0A 0A0" $this->staged_data['zip'] = strtoupper( $zip ); //In the event that they only forgot the space, help 'em out. $regex = '/[A-Z]\d[A-Z]\d[A-Z]\d/'; if ( strlen( $this->staged_data['zip'] ) === 6 && preg_match( $regex, $zip ) ) { $this->staged_data['zip'] = substr( $zip, 0, 3 ) . ' ' . substr( $zip, 3, 3 ); } break; } } protected function stage_email() { if ( empty( $this->staged_data['email'] ) ) { $this->staged_data['email'] = $this->getGlobal( 'DefaultEmail' ); } } protected function buildRequestParams() { // Look up the request structure for our current transaction type in the transactions array $structure = $this->getTransactionRequestStructure(); if ( !is_array( $structure ) ) { return ''; } $queryparams = array(); //we are going to assume a flat array, because... namevalue. foreach ( $structure as $fieldname ) { $fieldvalue = $this->getTransactionSpecificValue( $fieldname ); if ( $fieldvalue !== '' && $fieldvalue !== false ) { $queryparams[ $fieldname ] = $fieldvalue; } } return $queryparams; } /** * Public accessor to the $transaction_response variable * @return PaymentTransactionResponse */ public function getTransactionResponse() { return $this->transaction_response; } /** * Returns the transaction communication status, or false if not set * present. * @return mixed */ public function getTransactionStatus() { if ( $this->transaction_response && $this->transaction_response->getCommunicationStatus() ) { return $this->transaction_response->getCommunicationStatus(); } return false; } /** * If it has been set: returns the final payment status in the $final_status * member variable. This is the one we care about for switching * on overall behavior. Otherwise, returns false. * @return mixed Final Transaction results status, or false if not set. * Should be one of the constants defined in @see FinalStatus */ public function getFinalStatus() { if ( $this->final_status ) { return $this->final_status; } else { return false; } } /** * Sets the final payment status. This is the one we care about for * switching on behavior. * DO NOT SET THE FINAL STATUS unless you've just taken an entire donation * process to completion: This status being set at all, denotes the very end * of the donation process on our end. Further attempts by the same user * will be seen as starting over. * @param string $status The final status of one discrete donation attempt, * can be one of constants defined in @see FinalStatus * @throws UnexpectedValueException */ public function finalizeInternalStatus( $status ) { /** * Handle session stuff! * -Behavior- * * Always, always increment numAttempt. * * complete/pending/pending-poke: Reset for potential totally * new payment, but keep numAttempt and other antifraud things * (velocity data) around. * * failed: KEEP all donor data around unless numAttempt has * hit its max, but kill the ctid (in the likely case that it * was an honest mistake) */ $this->incrementNumAttempt(); $force = false; switch ( $status ) { case FinalStatus::COMPLETE: case FinalStatus::PENDING: case FinalStatus::PENDING_POKE: $force = true; break; case FinalStatus::FAILED: case FinalStatus::REVISED: $force = false; break; } $this->session_resetForNewAttempt( $force ); $this->logFinalStatus( $status ); $this->sendFinalStatusMessage( $status ); $this->final_status = $status; } /** * Easily-child-overridable log component of setting the final * transaction status, which will only ever be set at the very end of a * transaction workflow. * @param string $status one of the constants defined in @see FinalStatus */ public function logFinalStatus( $status ){ $action = $this->getValidationAction(); $msg = " FINAL STATUS: '$status:$action' - "; //what do we want in here? //Attempted payment type, country of origin, $status, amount... campaign? //error message if one exists. $keys = array( 'payment_submethod', 'payment_method', 'country', 'utm_campaign', 'amount', 'currency_code', ); foreach ($keys as $key){ $msg .= $this->getData_Unstaged_Escaped( $key ) . ', '; } $txn_message = $this->getTransactionMessage(); if ( $txn_message ){ $msg .= " $txn_message"; } $this->payment_init_logger->info( $msg ); } + /** + * Build and send a message to the payments-init queue, once the initial workflow is complete. + */ public function sendFinalStatusMessage( $status ) { $transaction = array ( 'php-message-class' => 'SmashPig\CrmLink\Messages\DonationInterfaceFinalStatus', 'validation_action' => $this->getValidationAction(), 'payments_final_status' => $status, ); //add more keys here if you want it in the db equivalent of the payments-init queue. //for now, though, just taking the ones that make it to the logs. $keys = array ( 'payment_submethod', 'payment_method', 'country', 'amount', 'currency_code', ); foreach ( $keys as $key ) { $transaction[$key] = $this->getData_Unstaged_Escaped( $key ); } $transaction = $this->makeFreeformStompTransaction( $transaction ); try { - WmfFramework::runHooks( 'gwFreeformStomp', array ( $transaction, 'payments-init' ) ); + // FIXME: Dispatch "freeform" messages transparently as well. + // TODO: write test + $this->logger->info( 'Pushing transaction to payments-init queue.' ); + DonationQueue::instance()->push( $transaction, 'payments-init' ); } catch ( Exception $e ) { $this->logger->error( 'Unable to send payments-init message' ); } } /** * @deprecated * @return string|boolean */ public function getTransactionMessage() { if ( $this->transaction_response && $this->transaction_response->getTxnMessage() ) { return $this->transaction_response->getTxnMessage(); } return false; } /** * @deprecated * @return string|boolean */ public function getTransactionGatewayTxnID() { if ( $this->transaction_response && $this->transaction_response->getGatewayTransactionId() ) { return $this->transaction_response->getGatewayTransactionId(); } return false; } /** * Returns the FORMATTED data harvested from the reply, or false if it is not set. * @return mixed An array of returned data, or false. */ public function getTransactionData() { if ( $this->transaction_response && $this->transaction_response->getData() ) { return $this->transaction_response->getData(); } return false; } /** * Returns an array of errors, in the format $error_code => $error_message. * This should be an empty array on transaction success. * * @deprecated * * @return array */ public function getTransactionErrors() { if ( $this->transaction_response && $this->transaction_response->getErrors() ) { $simplify = function( $error ) { return $error['message']; }; return array_map( $simplify, $this->transaction_response->getErrors() ); } else { return array(); } } public function getFormClass() { return 'Gateway_Form_RapidHtml'; } public function getGatewayAdapterClass() { return get_called_class(); } //only the gateway should be setting validation errors. Everybody else should set manual errors. protected function setValidationErrors( $errors ) { $this->validation_errors = $errors; } public function getValidationErrors() { if ( !empty( $this->validation_errors ) ) { return $this->validation_errors; } else { return false; } } public function addManualError( $errors, $reset = false ) { if ( $reset ){ $this->manual_errors = array(); return; } $this->manual_errors = array_merge( $this->manual_errors, $errors ); } public function getManualErrors() { if ( !empty( $this->manual_errors ) ) { return $this->manual_errors; } else { return false; } } public function getAllErrors(){ $validation = $this->getValidationErrors(); $manual = $this->getManualErrors(); $return = array(); if ( is_array( $validation ) ){ $return = array_merge( $return, $validation ); } if ( is_array( $manual ) ){ $return = array_merge( $return, $manual ); } return $return; } /** * Adds one to the 'numAttempt' field we use to keep track of how many * times a donor has attempted a payment, in a session. * When they first show up (or get their token/session reset), it should * be set to '0'. */ protected function incrementNumAttempt() { self::session_ensure(); $attempts = self::session_getData( 'numAttempt' ); //intentionally outside the 'Donor' key. if ( is_numeric( $attempts ) ) { $attempts += 1; } else { //assume garbage = 0, so... $attempts = 1; } $_SESSION['numAttempt'] = $attempts; } /** * Some payment gateways require a distinct identifier for each API call * or for each new payment attempt, even if retrying an attempt that failed * validation. This is slightly different from numAttempt, which is only * incremented when setting a final status for a payment attempt. * It is the child class's responsibility to increment this at the * appropriate time. */ protected function incrementSequenceNumber() { self::session_ensure(); $sequence = self::session_getData( 'sequence' ); //intentionally outside the 'Donor' key. if ( is_numeric( $sequence ) ) { $sequence += 1; } else { $sequence = 1; } $_SESSION['sequence'] = $sequence; } public function setHash( $hashval ) { $this->dataObj->setVal( 'data_hash', $hashval ); } public function unsetHash() { $this->dataObj->expunge( 'data_hash' ); } /** * Runs all the pre-process hooks that have been enabled and configured in * donationdata.php and/or LocalSettings.php * This function is most likely to be called through * executeFunctionIfExists, early on in do_transaction. */ function runAntifraudHooks() { //extra layer of Stop Doing This. $errors = $this->getTransactionErrors(); if ( !empty( $errors ) ) { $this->logger->info( 'Skipping antifraud hooks: Transaction is already in error' ); return; } // allow any external validators to have their way with the data $this->logger->info( 'Preparing to run custom filters' ); WmfFramework::runHooks( 'GatewayValidate', array( &$this ) ); $this->logger->info( 'Finished running custom filters' ); //DO NOT set some variable as getValidationAction() here, and keep //checking that. getValidationAction could change with each one of these //hooks, and this ought to cascade. // if the transaction was flagged for review if ( $this->getValidationAction() == 'review' ) { // expose a hook for external handling of trxns flagged for review WmfFramework::runHooks( 'GatewayReview', array( &$this ) ); } // if the transaction was flagged to be 'challenged' if ( $this->getValidationAction() == 'challenge' ) { // expose a hook for external handling of trxns flagged for challenge (eg captcha) WmfFramework::runHooks( 'GatewayChallenge', array( &$this ) ); } // if the transaction was flagged for rejection if ( $this->getValidationAction() == 'reject' ) { // expose a hook for external handling of trxns flagged for rejection WmfFramework::runHooks( 'GatewayReject', array( &$this ) ); } } /** * Runs all the post-process hooks that have been enabled and configured in * donationdata.php and/or LocalSettings.php, including the ActiveMQ/Stomp * hooks. * This function is most likely to be called through * executeFunctionIfExists, later on in do_transaction. */ protected function runPostProcessHooks() { // expose a hook for any post processing WmfFramework::runHooks( 'GatewayPostProcess', array( &$this ) ); $this->doStompTransaction(); } protected function pushMessage( $queue ) { $this->logger->info( "Pushing transaction to queue [$queue]" ); DonationQueue::instance()->push( $this->getStompTransaction(), $queue ); } protected function setLimboMessage( $queue = 'limbo' ) { // FIXME: log the key and raw queue name. $this->logger->info( "Setting transaction in limbo store [$queue]" ); DonationQueue::instance()->set( $this->getCorrelationID(), $this->getStompTransaction(), $queue ); } protected function deleteLimboMessage( $queue = 'limbo' ) { $this->logger->info( "Clearing transaction from limbo store [$queue]" ); try { DonationQueue::instance()->delete( $this->getCorrelationID(), $queue ); } catch( BadMethodCallException $ex ) { $this->logger->warning( "Backend for queue [$queue] does not support deletion. Hope your message had an expiration date!" ); } } /** * If there are things about a transaction that we need to stash in the * transaction's definition (defined in a local defineTransactions() ), we * can recall them here. Currently, this is only being used to determine if * we have a transaction whose transmission would require multiple attempts * to wait for a certain status (or set of statuses), but we could do more * with this mechanism if we need to. * @param string $option_value the name of the key we're looking for in the * transaction definition. * @return mixed the transaction's value for that key if it exists, or NULL. */ protected function transaction_option( $option_value ) { //ooo, ugly. $transaction = $this->getCurrentTransaction(); if ( !$transaction ){ return NULL; } if ( array_key_exists( $option_value, $this->transactions[$transaction] ) ) { return $this->transactions[$transaction][$option_value]; } return NULL; } /** * Instead of pulling all the DonationData back through to update one local * value, use this. It updates both staged_data (which is intended to be * staged and used _just_ by the gateway) and unstaged_data, which is actually * just normalized and sanitized form data as entered by the user. * * TODO: handle the cases where $val is listed in the gateway adapter's * staged_vars. * Not doing this right now, though, because it's not yet necessary for * anything we have at the moment. * * @param string $val The field name that we are looking to retrieve from * our DonationData object. */ function refreshGatewayValueFromSource( $val ) { $refreshed = $this->dataObj->getVal_Escaped( $val ); if ( !is_null($refreshed) ){ $this->staged_data[$val] = $refreshed; $this->unstaged_data[$val] = $refreshed; } else { unset( $this->staged_data[$val] ); unset( $this->unstaged_data[$val] ); } } /** * Allows us to send an initial fraud score offset with api calls */ public function addRiskScore( $score ) { $this->risk_score += $score; } /** * Sets the current validation action. This is meant to be used by the * process hooks, and as such, by default, only worse news than was already * being stored will be retained for the final result. * @param string $action the value you want to set as the action. * @param bool $reset set to true to do a hard set on the action value. * Otherwise, the status will only change if it fails harder than it already * was. * @throws UnexpectedValueException */ public function setValidationAction( $action, $reset = false ) { //our choices are: $actions = array( 'process' => 0, 'review' => 1, 'challenge' => 2, 'reject' => 3, ); if ( !isset( $actions[$action] ) ) { throw new UnexpectedValueException( "Action $action is invalid." ); } if ( $reset ) { $this->action = $action; return; } if ( ( int ) $actions[$action] > ( int ) $actions[$this->getValidationAction()] ) { $this->action = $action; } } /** * Returns the current validation action. * This will typically get set and altered by the various enabled process hooks. * @return string the current process action. */ public function getValidationAction() { if ( !isset( $this->action ) ) { $this->action = 'process'; } return $this->action; } /** * Lets the outside world (particularly hooks that accumulate points scores) * know if we are a batch processor. * @return type */ public function isBatchProcessor(){ return $this->batch; } /** * Tell the gateway that it is going to be used for an API request, so * it can bypass setting up all the visual components. * @param boolean $set True if this is an API request, false if not. */ public function setApiRequest( $set = true ) { $this->api_request = $set; } /** * Find out if we're an API request or not. * @return boolean true if we are, otherwise false. */ public function isApiRequest() { if ( !property_exists( $this, 'api_request' ) ) { return false; } else { return $this->api_request; } } public function getOriginalValidationErrors( ){ return $this->dataObj->getValidationErrors(); } /** * Build list of required fields * TODO: Determine if this ever needs to be overridden per gateway, or if * all the per-country / per-gateway cases can be expressed declaratively * in payment method / submethod metadata. If that's the case, move this * function (to DataValidator?) * @return array of field names (empty if no payment method set) */ public function getRequiredFields() { $required_fields = array(); if ( !$this->getPaymentMethod() ) { return $required_fields; } $methodMeta = $this->getPaymentMethodMeta(); $validation = isset( $methodMeta['validation'] ) ? $methodMeta['validation'] : array(); if ( $this->getPaymentSubmethod() ) { $submethodMeta = $this->getPaymentSubmethodMeta(); if ( isset( $submethodMeta['validation'] ) ) { // submethod validation can override method validation // TODO: child method anything should supercede parent method // anything, and PaymentMethod should handle that. $validation = $submethodMeta['validation'] + $validation; } } foreach ( $validation as $type => $enabled ) { if ( $enabled !== true ) { continue; } switch ( $type ) { case 'address' : $check_not_empty = array( 'street', 'city', 'state', 'country', 'zip', //this should really be added or removed, depending on the country and/or gateway requirements. //however, that's not happening in this class in the code I'm replacing, so... //TODO: Something clever in the DataValidator with data groups like these. ); break; case 'amount' : $check_not_empty = array( 'amount' ); break; case 'creditCard' : $check_not_empty = array( 'card_num', 'cvv', 'expiration', 'card_type' ); break; case 'email' : $check_not_empty = array( 'email' ); break; case 'name' : $check_not_empty = array( 'fname', 'lname' ); break; case 'fiscal_number' : $check_not_empty = array( 'fiscal_number' ); break; default: $this->logger->error( "bad required group name: {$type}" ); continue; } if ( $check_not_empty ) { $required_fields = array_unique( array_merge( $required_fields, $check_not_empty ) ); } } return $required_fields; } /** * Check donation data for validity * * @return boolean true if validation passes * * TODO: Maybe validate on $unstaged_data directly? */ public function revalidate() { $check_not_empty = $this->getRequiredFields(); $validation_errors = $this->dataObj->getValidationErrors( true, $check_not_empty ); $this->setValidationErrors( $validation_errors ); return $this->validatedOK(); } public function validatedOK(){ if ( $this->getValidationErrors() === false ){ return true; } return false; } /** * This custom filter function checks the global variable: * * CountryMap * * How the score is tabulated: * - If a country is not defined, a score of zero will be generated. * - Generates a score based on the defined value. * - Returns an integer: 0 <= $score <= 100 * * @see $wgDonationInterfaceCustomFiltersFunctions * @see $wgDonationInterfaceCountryMap * * @return integer */ public function getScoreCountryMap() { $score = 0; $country = $this->getData_Unstaged_Escaped( 'country' ); $countryMap = $this->getGlobal( 'CountryMap' ); $msg = self::getGatewayName() . ': Country map: ' . print_r( $countryMap, true ); $this->logger->debug( $msg ); // Lookup a score if it is defined if ( isset( $countryMap[ $country ] ) ) { $score = (integer) $countryMap[ $country ]; } // @see $wgDonationInterfaceDisplayDebug $this->debugarray[] = 'custom filters function: get country [ ' . $country . ' ] map score = ' . $score; return $score; } /** * This custom filter function checks the global variable: * * EmailDomainMap * * How the score is tabulated: * - If a emailDomain is not defined, a score of zero will be generated. * - Generates a score based on the defined value. * - Returns an integer: 0 <= $score <= 100 * * @see $wgDonationInterfaceCustomFiltersFunctions * @see $wgDonationInterfaceEmailDomainMap * * @return integer */ public function getScoreEmailDomainMap() { $score = 0; $email = $this->getData_Unstaged_Escaped( 'email' ); $emailDomain = substr( strstr( $email, '@' ), 1 ); $emailDomainMap = $this->getGlobal( 'EmailDomainMap' ); $msg = self::getGatewayName() . ': Email Domain map: ' . print_r( $emailDomainMap, true ); $this->logger->debug( $msg ); // Lookup a score if it is defined if ( isset( $emailDomainMap[ $emailDomain ] ) ) { $score = (integer) $emailDomainMap[ $emailDomain ]; } // @see $wgDonationInterfaceDisplayDebug $this->debugarray[] = 'custom filters function: get email domain [ ' . $emailDomain . ' ] map score = ' . $score; return $score; } /** * This custom filter function checks the global variable: * * UtmCampaignMap * * @TODO: All these regex map matching functions that are identical with * different internal var names are making me rilly mad. Collapse. * * How the score is tabulated: * - Add the score(value) associated with each regex(key) in the map var. * * @see $wgDonationInterfaceCustomFiltersFunctions * @see $wgDonationInterfaceUtmCampaignMap * * @return integer */ public function getScoreUtmCampaignMap() { $score = 0; $campaign = $this->getData_Unstaged_Escaped( 'utm_campaign' ); $campaignMap = $this->getGlobal( 'UtmCampaignMap' ); $msg = self::getGatewayName() . ': UTM Campaign map: ' . print_r( $campaignMap, true ); $this->logger->debug( $msg ); // If any of the defined regex patterns match, add the points. if ( is_array( $campaignMap ) && !empty( $campaignMap ) ){ foreach ( $campaignMap as $regex => $points ){ if ( preg_match( $regex, $campaign ) ) { $score = (integer) $points; } } } // @see $wgDonationInterfaceDisplayDebug $this->debugarray[] = 'custom filters function: get utm campaign [ ' . $campaign . ' ] score = ' . $score; return $score; } /** * This custom filter function checks the global variable: * * UtmMediumMap * * @TODO: Again. Regex map matching functions, identical, with minor * internal var names. Collapse. * * How the score is tabulated: * - Add the score(value) associated with each regex(key) in the map var. * * @see $wgDonationInterfaceCustomFiltersFunctions * @see $wgDonationInterfaceUtmMediumMap * * @return integer */ public function getScoreUtmMediumMap() { $score = 0; $medium = $this->getData_Unstaged_Escaped( 'utm_medium' ); $mediumMap = $this->getGlobal( 'UtmMediumMap' ); $msg = self::getGatewayName() . ': UTM Medium map: ' . print_r( $mediumMap, true ); $this->logger->debug( $msg ); // If any of the defined regex patterns match, add the points. if ( is_array( $mediumMap ) && !empty( $mediumMap ) ){ foreach ( $mediumMap as $regex => $points ){ if ( preg_match( $regex, $medium ) ) { $score = (integer) $points; } } } // @see $wgDonationInterfaceDisplayDebug $this->debugarray[] = 'custom filters function: get utm medium [ ' . $medium . ' ] score = ' . $score; return $score; } /** * This custom filter function checks the global variable: * * UtmSourceMap * * @TODO: Argharghargh, inflated code! Collapse! * * How the score is tabulated: * - Add the score(value) associated with each regex(key) in the map var. * * @see $wgDonationInterfaceCustomFiltersFunctions * @see $wgDonationInterfaceUtmSourceMap * * @return integer */ public function getScoreUtmSourceMap() { $score = 0; $source = $this->getData_Unstaged_Escaped( 'utm_source' ); $sourceMap = $this->getGlobal( 'UtmSourceMap' ); $msg = self::getGatewayName() . ': UTM Source map: ' . print_r( $sourceMap, true ); $this->logger->debug( $msg ); // If any of the defined regex patterns match, add the points. if ( is_array( $sourceMap ) && !empty( $sourceMap ) ){ foreach ( $sourceMap as $regex => $points ){ if ( preg_match( $regex, $source ) ) { $score = (integer) $points; } } } // @see $wgDonationInterfaceDisplayDebug $this->debugarray[] = 'custom filters function: get utm source [ ' . $source . ' ] score = ' . $score; return $score; } /** * For places that might need the merchant ID outside of the adapter */ public function getMerchantID() { return $this->account_config[ 'MerchantID' ]; } /** * Check to see if the session exists. */ public static function session_exists() { if ( session_id() ) { return true; } return false; } /** * session_ensure * Ensure that we have a session set for the current user. * If we do not have a session set for the current user, * start the session. */ public static function session_ensure() { // if the session is already started, do nothing if ( self::session_exists() ) { return; } // otherwise, fire it up using global mw function wfSetupSession WmfFramework::setupSession(); } /** * Retrieve data from the sesion if it's set, and null if it's not. * @param string $key The array key to return from the session. * @param string $subkey Optional: The subkey to return from the session. * Only really makes sense if $key is an array. * @return mixed The session value if present, or null if it is not set. */ public static function session_getData( $key, $subkey = null ) { if ( is_array( $_SESSION ) && array_key_exists( $key, $_SESSION ) ) { if ( is_null( $subkey ) ) { return $_SESSION[$key]; } else { if ( is_array( $_SESSION[$key] ) && array_key_exists( $subkey, $_SESSION[$key] ) ) { return $_SESSION[$key][$subkey]; } } } return null; } /** * Checks to see if we have donor data in our session. * This can be useful for determining if a user should be at a certain point * in the workflow for certain gateways. For example: This is used on the * outside of the adapter in GlobalCollect's resultswitcher page, to * determine if the user is actually in the process of making a credit card * transaction. * @param bool|string $key Optional: A particular key to check against the * donor data in session. * @param string $value Optional (unless $key is set): A value that the $key * should contain, in the donor session. * @return boolean true if the session contains donor data (and if the data * key matches, when key and value are set), and false if there is no donor * data (or if the key and value do not match) */ public static function session_hasDonorData( $key = false, $value = '' ) { if ( self::session_exists() && !is_null( self::session_getData( 'Donor' ) ) ) { if ( $key === false ) { return true; } if ( self::session_getData( 'Donor', $key ) === $value ) { return true; } } return false; } /** * Unsets the session data, in the case that we've saved it for gateways * like GlobalCollect that require it to persist over here through their * iframe experience. */ public static function session_unsetDonorData() { if ( self::session_hasDonorData() ) { unset( $_SESSION['Donor'] ); } } /** * Removes any old donor data from the session, and adds the current set. * This will be used internally every time we call do_transaction. */ public function session_addDonorData() { $this->logger->info( __FUNCTION__ . ': Refreshing all donor data' ); self::session_ensure(); $_SESSION['Donor'] = array ( ); $donordata = DonationData::getStompMessageFields(); $donordata[] = 'order_id'; foreach ( $donordata as $item ) { $_SESSION['Donor'][$item] = $this->getData_Unstaged_Escaped( $item ); } } /** * This should kill the session as hard as possible. * It will leave the cookie behind, but everything it could possibly * reference will be gone. */ public function session_killAllEverything() { //yes: We do need all of these things, to be sure we're killing the //correct session data everywhere it could possibly be. self::session_ensure(); //make sure we are killing the right thing. session_unset(); //frees all registered session variables. At this point, they can still be re-registered. session_destroy(); //killed on the server. } /** * Destroys the session completely. * ...including session velocity data, and the form stack. So, you * probably just shouldn't. Please consider session_reset instead. Please. * Note: This will leave the cookie behind! It just won't go to anything at * all. */ public function session_unsetAllData() { $this->session_killAllEverything(); $this->debugarray[] = 'Killed all the session everything.'; } /** * For those times you want to have the user functionally start over * without, you know, cutting your entire head off like you do with * session_unsetAllData(). * @param string $force Behavior Description: * $force = true: Reset for potential totally new payment, but keep * numAttempt and other antifraud things (velocity data) around. * $force = false: Keep all donor data around unless numAttempt has hit * its max, but kill the ctid (in the likely case that it was an honest * mistake) */ public function session_resetForNewAttempt( $force = false ) { $reset = $force; if ( self::session_getData( 'numAttempt' ) > 3 ) { $reset = true; $_SESSION['numAttempt'] = 0; } if ( $reset ) { $this->logger->info( __FUNCTION__ . ': Unsetting session donor data' ); $this->session_unsetDonorData(); //leave the payment forms and antifraud data alone. //but, under no circumstances should the gateway edit //token appear in the preserve array... $preserve_main = array ( 'DonationInterface_SessVelocity', 'PaymentForms', 'numAttempt', 'order_status', //for post-payment activities 'sequence', ); $msg = ''; foreach ( $_SESSION as $key => $value ) { if ( !in_array( $key, $preserve_main ) ) { $msg .= "$key, "; //always one extra comma; Don't care. unset( $_SESSION[$key] ); } } if ( $msg != '' ) { $this->logger->info( __FUNCTION__ . ": Unset the following session keys: $msg" ); } } else { //I'm sure we could put more here... $soft_reset = array ( 'order_id', ); foreach ( $soft_reset as $reset_me ) { unset( $_SESSION['Donor'][$reset_me] ); } $this->logger->info( __FUNCTION__ . ': Soft reset, order_id only' ); } } /** * Check to see if we've changed gateways, and throw out the garbage * from the old gateway if so. Prevents order_id leakage! */ protected function session_resetOnGatewaySwitch() { if ( !$this->session_exists() ) { return; } $old_gateway = $this->session_getData( 'Donor', 'gateway' ); if ( $old_gateway !== null && $old_gateway !== $this::IDENTIFIER ) { $this->session_resetForNewAttempt( true ); } } /** * Add a RapidHTML Form (ffname) to this abridged history of where we've * been in this session. This lets us do things like construct useful * "back" links that won't crush all session everything. * @param string $form_key The 'ffname' that RapidHTML uses to load a * payments form. Additional: ffname maps to a first-level key in * $wgDonationInterfaceAllowedHtmlForms */ public function session_pushRapidHTMLForm( $form_key ) { if ( !$form_key ) { return; } self::session_ensure(); if ( !is_array( self::session_getData( 'PaymentForms' ) ) ) { $_SESSION['PaymentForms'] = array ( ); } //don't want duplicates if ( $this->session_getLastRapidHTMLForm() != $form_key ) { $_SESSION['PaymentForms'][] = $form_key; } } /** * Get the 'ffname' of the last RapidHTML payment form that successfully * loaded for this session. * @return mixed ffname of the last valid payments form if there is one, * otherwise false. */ public function session_getLastRapidHTMLForm() { self::session_ensure(); if ( !is_array( self::session_getData( 'PaymentForms' ) ) ) { return false; } else { $ffname = end( $_SESSION['PaymentForms'] ); if ( !$ffname ) { return false; } $data = $this->getData_Unstaged_Escaped(); //have to check to see if the last loaded form is *still* valid. if ( GatewayFormChooser::isValidForm( $ffname, $data['country'], $data['currency_code'], $data['payment_method'], $data['payment_submethod'], $data['recurring'], $data['gateway'] ) ) { return $ffname; } else { return false; } } } /** * token_applyMD5AndSalt * Takes a clear-text token, and returns the MD5'd result of the token plus * the configured gateway salt. * @param string $clear_token The original, unsalted, unencoded edit token. * @return string The salted and MD5'd token. */ protected static function token_applyMD5AndSalt( $clear_token ) { $salt = self::getGlobal( 'Salt' ); if ( is_array( $salt ) ) { $salt = implode( "|", $salt ); } $salted = md5( $clear_token . $salt ) . User::EDIT_TOKEN_SUFFIX; return $salted; } /** * token_generateToken * Generate a random string to be used as an edit token. * @param string $padding A string with which we could pad out the random hex * further. * @return string */ public static function token_generateToken( $padding = '' ) { $token = dechex( mt_rand() ) . dechex( mt_rand() ); return md5( $token . $padding ); } /** * Establish an 'edit' token to help prevent CSRF, etc. * * We use this in place of $wgUser->editToken() b/c currently * $wgUser->editToken() is broken (apparently by design) for * anonymous users. Using $wgUser->editToken() currently exposes * a security risk for non-authenticated users. Until this is * resolved in $wgUser, we'll use our own methods for token * handling. * * Public so the api can get to it. * * @return string */ public static function token_getSaltedSessionToken() { // make sure we have a session open for tracking a CSRF-prevention token self::session_ensure(); $gateway_ident = self::getIdentifier(); if ( !isset( $_SESSION[$gateway_ident . 'EditToken'] ) ) { // generate unsalted token to place in the session $token = self::token_generateToken(); $_SESSION[$gateway_ident . 'EditToken'] = $token; } else { $token = $_SESSION[$gateway_ident . 'EditToken']; } return self::token_applyMD5AndSalt( $token ); } /** * token_refreshAllTokenEverything * In the case where we have an expired session (token mismatch), we go * ahead and fix it for 'em for their next post. We do this by refreshing * everything that has to do with the edit token. */ protected function token_refreshAllTokenEverything() { $unsalted = self::token_generateToken(); $gateway_ident = self::getIdentifier(); self::session_ensure(); $_SESSION[$gateway_ident . 'EditToken'] = $unsalted; $salted = $this->token_getSaltedSessionToken(); $this->addRequestData( array ( 'token' => $salted ) ); } /** * token_matchEditToken * Determine the validity of a token by checking it against the salted * version of the clear-text token we have already stored in the session. * On failure, it resets the edit token both in the session and in the form, * so they will match on the user's next load. * * @var string $val * @return bool */ protected function token_matchEditToken( $val ) { // When fetching the token from the URL (like we do for Worldpay), the last // portion may be mangled by + being substituted for ' '. Normally this is // valid URL unescaping, but not in this case. $val = str_replace( ' ', '+', $val ); // fetch a salted version of the session token $sessionSaltedToken = $this->token_getSaltedSessionToken(); if ( $val != $sessionSaltedToken ) { $this->logger->debug( __FUNCTION__ . ": broken session data\n" ); //and reset the token for next time. $this->token_refreshAllTokenEverything(); } return $val === $sessionSaltedToken; } /** * token_checkTokens * The main function to check the salted and MD5'd token we should have * saved and gathered from $wgRequest, against the clear-text token we * should have saved to the user's session. * token_getSaltedSessionToken() will start off the process if this is a * first load, and there's no saved token in the session yet. * @staticvar string $match * @return type */ protected function token_checkTokens() { static $match = null; //because we only want to do this once per load. if ( $match === null ) { // establish the edit token to prevent csrf $token = $this->token_getSaltedSessionToken(); $this->logger->debug( 'editToken: ' . $token ); // match token if ( !$this->dataObj->isSomething( 'token' ) ) { $this->addRequestData( array ( 'token' => $token ) ); } $token_check = $this->getData_Unstaged_Escaped( 'token' ); $match = $this->token_matchEditToken( $token_check ); if ( $this->dataObj->wasPosted() ) { $this->logger->debug( 'Submitted edit token: ' . $this->getData_Unstaged_Escaped( 'token' ) ); $this->logger->debug( 'Token match: ' . ($match ? 'true' : 'false' ) ); } } return $match; } /** * Retrieve the data we will need in order to retry a payment. * This is useful in the event that we have just killed a session before * the next retry. * @return array Data required for a payment retry. */ public function getRetryData() { $params = array ( ); foreach ( $this->dataObj->getRetryFields() as $field ) { $params[$field] = $this->getData_Unstaged_Escaped( $field ); } return $params; } /** * isValidSpecialForm: Tells us if the ffname supplied is a valid * special form for the current gateway. * @var string $ffname The form name we want to try * @return boolean True if this is a valid special form, otherwise false */ public function isValidSpecialForm( $ffname ){ $defn = GatewayFormChooser::getFormDefinition( $ffname ); if ( is_array( $defn ) && DataValidator::value_appears_in( $this->getIdentifier(), $defn['gateway'] ) && array_key_exists( 'special_type', $defn ) ){ return true; } return false; } /** * Make sure that we've got a valid ffname so we don't have to screw * around with this in RapidHTML when we try to load it and fail. */ public function setValidForm() { //do we even need the visual stuff? if ( $this->isApiRequest() || $this->isBatchProcessor() ) { return; } //check to see if the current ffname exists, and is loadable. $data = $this->getData_Unstaged_Escaped(); $ffname = null; if ( isset( $data['ffname'] ) ) { $ffname = $data['ffname']; //easy stuff first: if ( $this->isValidSpecialForm( $ffname ) ) { return; } } // 'country' might = 'XX' - CN does this when it's deeply confused. if ( !isset( $data['country'] ) || $data['country'] === 'XX' ) { $country = null; } else { $country = $data['country']; } //harumph. Remind me again why I hate @ suppression so much? $currency = isset( $data['currency_code'] ) ? $data['currency_code'] : null; $payment_method = isset( $data['payment_method'] ) ? $data['payment_method'] : null; $payment_submethod = isset( $data['payment_submethod'] ) ? $data['payment_submethod'] : null; $recurring = isset( $data['recurring'] ) ? $data['recurring'] : null; $gateway = isset( $data['gateway'] ) ? $data['gateway'] : null; //for the error messages $utm = isset( $data['utm_source'] ) ? $data['utm_source'] : null; $ref = isset( $data['referrer'] ) ? $data['referrer'] : null; //make it actually possible to debug this hot mess $this->logger->info( "Attempting to set a valid form for the combination: " . $this->getLogDebugJSON() ); if ( !is_null( $ffname ) && GatewayFormChooser::isValidForm( $ffname, $country, $currency, $payment_method, $payment_submethod, $recurring, $gateway ) ) { return; } else if ( $this->session_getLastRapidHTMLForm() ) { //This will take care of it if this is an ajax request, or a 3rd party return hit $new_ff = $this->session_getLastRapidHTMLForm(); $this->addRequestData( array ( 'ffname' => $new_ff ) ); //and debug log a little $this->logger->debug( "Setting form to last successful ('$new_ff')" ); } else if ( GatewayFormChooser::isValidForm( $ffname . "-$country", $country, $currency, $payment_method, $payment_submethod, $recurring, $gateway ) ) { //if the country-specific version exists, use that. $this->addRequestData( array ( 'ffname' => $ffname . "-$country" ) ); //I'm only doing this for serious legacy purposes. This mess needs to stop itself. To help with the mess-stopping... $message = "ffname '$ffname' was invalid, but the country-specific '$ffname-$country' works. utm_source = '$utm', referrer = '$ref'"; $this->logger->warning( $message ); } else { //Invalid form. Go get one that is valid, and squawk in the error logs. $new_ff = GatewayFormChooser::getOneValidForm( $country, $currency, $payment_method, $payment_submethod, $recurring, $gateway ); $this->addRequestData( array ( 'ffname' => $new_ff ) ); //now construct a useful error message $message = "ffname '{$ffname}' is invalid. Assigning ffname '{$new_ff}'. " . "I currently am choosing for: " . $this->getLogDebugJSON(); if ( empty( $ffname ) ) { // Gateway-specific link didn't specify a form, but we have a // default. Don't squawk too loud. $this->logger->warning( $message ); } else { $this->logger->error( $message ); } //Turn these off by setting the LogDebug global to false. $this->logger->debug( "GET: " . json_encode( $_GET ) ); $this->logger->debug( "POST: " . json_encode( $_POST ) ); $dontwannalog = array ( 'user_ip', 'server_ip', 'descriptor', 'account_name', 'account_number', 'authorization_id', 'bank_check_digit', 'bank_name', 'bank_code', 'branch_code', 'country_code_bank', 'date_collect', 'direct_debit_text', 'iban', 'fiscal_number', 'cvv', ); foreach ( $data as $key => $val ) { if ( in_array( $key, $dontwannalog ) ) { unset( $data[$key] ); } } $this->logger->debug( "Truncated DonationData: " . json_encode( $data ) ); } } /** * buildOrderIDSources: Uses the 'alt_locations' array in the order id * metadata, to build an array of all possible candidates for order_id. * This will also weed out candidates that do not meet the * gateway-specific data constraints for that field, and are therefore * invalid. * * @TODO: Data Item Class. There should be a class that keeps track of * the metadata for every field we use (everything that currently comes * back from DonationData), that can be overridden per gateway. Revisit * this in a more universal way when that time comes. */ public function buildOrderIDSources() { static $built = false; if ( $built && isset( $this->order_id_candidates ) ) { //once per request is plenty return; } //pull all order ids and variants from all their usual locations $locations = array ( '_GET' => 'order_id', '_POST' => 'order_id', '_SESSION' => array ( 'Donor' => 'order_id' ), ); $alt_locations = $this->getOrderIDMeta( 'alt_locations' ); if ( $alt_locations && is_array( $alt_locations ) ) { foreach ( $alt_locations as $var => $key ) { $locations[$var] = $key; } } //Now pull all the locations and populate the candidate array. $oid_candidates = array ( ); foreach ( $locations as $var => $key ) { //using a horribly redundant switch here until php supports superglobals with $$. Arglebarglefargle! switch ( $var ) { case "_GET" : if ( array_key_exists( $key, $_GET ) ) { $oid_candidates[$var] = $_GET[$key]; } break; case "_POST" : if ( array_key_exists( $key, $_POST ) ) { $oid_candidates[$var] = $_POST[$key]; } case "_SESSION" : if ( $this->session_exists() ) { if ( is_array( $key ) ) { foreach ( $key as $subkey => $subvalue ) { if ( array_key_exists( $subkey, $_SESSION ) && array_key_exists( $subvalue, $_SESSION[$subkey] ) ) { $oid_candidates['_SESSION' . $subkey . $subvalue] = $_SESSION[$subkey][$subvalue]; } } } else { if ( array_key_exists( $key, $_SESSION ) ) { $oid_candidates[$var] = $_SESSION[$key]; } } } break; default : if ( !is_array( $key ) && array_key_exists( $key, $$var ) ) { //simple case first. This is a direct key in $var. $oid_candidates[$var] = $$var[$key]; } if ( is_array( $key ) ) { foreach ( $key as $subkey => $subvalue ) { if ( array_key_exists( $subkey, $$var ) && array_key_exists( $subvalue, $$var[$subkey] ) ) { $oid_candidates[$var . $subkey . $subvalue] = $$var[$subkey][$subvalue]; } } } break; } } //unset every invalid candidate foreach ( $oid_candidates as $source => $value ) { if ( empty( $value ) || !$this->validateDataConstraintsMet( 'order_id', $value ) ) { unset( $oid_candidates[$source] ); } } $this->order_id_candidates = $oid_candidates; $built = true; } /** * Validates that the gateway-specific data constraints for this field * have been met. * @param string $field The field name we're checking * @param mixed $value The candidate value of the field we want to check * @return boolean True if it's a valid value for that field, false if it isn't. */ function validateDataConstraintsMet( $field, $value ) { $met = true; if ( is_array( $this->dataConstraints ) && array_key_exists( $field, $this->dataConstraints ) ) { $type = $this->dataConstraints[$field]['type']; $length = $this->dataConstraints[$field]['length']; switch ( $type ) { case 'numeric' : //@TODO: Determine why the DataValidator's type validation functions are protected. //There is no good answer, use those. //In fact, we should probably just port the whole thing over there. Derp. if ( !is_numeric( $value ) ) { $met = false; } elseif ( $field === 'order_id' && $this->getOrderIDMeta( 'disallow_decimals' ) ) { //haaaaaack... //it's a numeric string, so all the number functions (like is_float) always return false. Because, string. if ( strpos( $value, '.' ) !== false ) { //we don't want decimals. Something is wrong. Regen. $met = false; } } break; case 'alphanumeric' : //TODO: Something better here. break; default: //fail closed. $met = false; } if ( strlen( $value ) > $length ) { $met = false; } } return $met; } /** * This function is meant to be run by the DonationData class, both * before and after any communication has been done that might retrieve * an order ID. * To put it another way: If we are meant to be getting the OrderID from * a piece of gateway communication that hasn't been done yet, this * should return NULL. I think. * @param string $override The pre-determined value of order_id. * When you want to normalize an order_id to something you have already * sorted out (anything running in batch mode is a good candidate - you * have probably grabbed a preexisting order_id from some external data * source in that case), short-circuit the hunting process and just take * the override's word for order_id's final value. * Also used when receiving the order_id from external sources * (example: An API response) * * @param DonationData $dataObj Reference to the donation data object when * we're creating the order ID in the constructor of the object (and thus * do not yet have a reference to it.) * @return string The normalized value of order_id */ public function normalizeOrderID( $override = null, $dataObj = null ) { $selected = false; $source = null; $value = null; if ( !is_null( $override ) && $this->validateDataConstraintsMet( 'order_id', $override ) ) { //just do it. $selected = true; $source = 'override'; $value = $override; } else { //we are not overriding. Exit if we've been here before and decided something. if ( $this->getOrderIDMeta( 'final' ) ) { return $this->getOrderIDMeta( 'final' ); } } $this->buildOrderIDSources(); //make sure all possible preexisting data is ready to go //If there's anything in the candidate array, take it. It's already in default order of preference. if ( !$selected && is_array( $this->order_id_candidates ) && !empty( $this->order_id_candidates ) ) { $selected = true; reset( $this->order_id_candidates ); $source = key( $this->order_id_candidates ); $value = $this->order_id_candidates[$source]; } if ( !$selected && !array_key_exists( 'generated', $this->order_id_candidates ) && $this->getOrderIDMeta( 'generate' ) ) { $selected = true; $source = 'generated'; $value = $this->generateOrderID( $dataObj ); $this->order_id_candidates[$source] = $value; //so we don't regen accidentally } if ( $selected ) { $this->setOrderIDMeta( 'final', $value ); $this->setOrderIDMeta( 'final_source', $source ); return $value; } elseif ( $this->getOrderIDMeta( 'generate' ) ) { //I'd dump the whole oid meta array here, but it's pretty much guaranteed to be empty if we're here at all. $this->logger->error( __FUNCTION__ . ": Unable to determine what oid to use, in generate mode." ); } return null; } /** * Default orderID generation * This used to be done in DonationData, but gateways should control * the format here. Override this in child classes. * * @param DonationData $dataObj Reference to the donation data object * when we are forced to create the order ID during construction of it * and thus do not already have a reference. THIS IS A HACK! /me vomits * * @return int A freshly generated order ID */ public function generateOrderID( $dataObj = null ) { if ( $this->getOrderIDMeta( 'ct_id' ) ) { // This option means use the contribution tracking ID with the // sequence number tacked on to the end for uniqueness $dataObj = ( $dataObj ) ?: $this->dataObj; $ctid = $dataObj->getVal_Escaped( 'contribution_tracking_id' ); if ( !$ctid ) { $ctid = $dataObj->saveContributionTrackingData( true ); } $this->session_ensure(); $sequence = $this->session_getData( 'sequence' ) ?: 0; return "{$ctid}.{$sequence}"; } $order_id = ( string ) mt_rand( 1000, 9999999999 ); return $order_id; } public function regenerateOrderID() { $id = null; if ( $this->getOrderIDMeta( 'generate' ) ) { $id = $this->generateOrderID(); // should we pass $this->dataObj? $source = 'regenerated'; //This implies the try number is > 1. $this->order_id_candidates[$source] = $id; //alter the meta with the new data $this->setOrderIDMeta( 'final', $id ); $this->setOrderIDMeta( 'final_source', 'regenerated' ); } else { //we are not regenerating ourselves, but we need a new one... //so, blank it and wait. $this->order_id_candidates = array ( ); unset( $this->order_id_meta['final'] ); unset( $this->order_id_meta['final_source'] ); } //tell DonationData about it $this->addRequestData( array ( 'order_id' => $id ) ); return $id; } /** * returns the orderID Meta * @param string $key The key to retrieve. Optional. * @return mixed|false Data requested, or false if it is not set. */ public function getOrderIDMeta( $key = false ) { $data = $this->order_id_meta; if ( !is_array( $data ) ) { return false; } if ( $key ) { //just return the key if it exists if ( array_key_exists( $key, $data ) ) { return $data[$key]; } } else { return $data; } } /** * sets more orderID Meta, so we can remember things about what we chose * to go with in later logic. * @param string $key The key to set. * @param mixed $value The value to set. */ public function setOrderIDMeta( $key, $value ) { $this->order_id_meta[$key] = $value; } /** * Get payment method meta * * @param string|null $payment_method Defaults to the current payment method, if null. * * @throws OutOfBoundsException */ public function getPaymentMethodMeta( $payment_method = null ) { if ( $payment_method === null ) { $payment_method = $this->getPaymentMethod(); } if ( isset( $this->payment_methods[ $payment_method ] ) ) { return $this->payment_methods[ $payment_method ]; } else { $message = "The payment method [{$payment_method}] was not found."; throw new OutOfBoundsException( $message ); } } /** * Get payment submethod meta * * @param string|null $payment_submethod Payment submethods are mapped to paymentproductid * @throws OutOfBoundsException */ public function getPaymentSubmethodMeta( $payment_submethod = null ) { if ( is_null( $payment_submethod ) ) { $payment_submethod = $this->getPaymentSubmethod(); } if ( isset( $this->payment_submethods[ $payment_submethod ] ) ) { $this->logger->debug( 'Getting metadata for payment submethod: ' . ( string ) $payment_submethod ); // Ensure that the validation index is set. if ( !isset( $this->payment_submethods[ $payment_submethod ]['validation'] ) ) { $this->payment_submethods[ $payment_submethod ]['validation'] = array(); } return $this->payment_submethods[ $payment_submethod ]; } else { throw new OutOfBoundsException( "The payment submethod [{$payment_submethod}] was not found." ); } } /** * Get metadata for all available submethods, given current method / country * TODO: A PaymentMethod should be able to list its child options. Probably * still need some gateway-specific logic to prune the list by country and * currency. * TODO: Make it possible to override availability by currency and currency * in LocalSettings. Idea: same metadata array structure as used in * definePaymentMethods, overrides cascade from * methodMeta -> submethodMeta -> settingsMethodMeta -> settingsSubmethodMeta * @return array with available submethods * 'visa' => array( 'label' => 'Visa' ) */ function getAvailableSubmethods() { $method = $this->getPaymentMethod(); $submethods = array(); foreach( $this->payment_submethods as $key => $available_submethod ) { if ( $available_submethod['group'] !== $method ) { continue; // skip anything not part of the selected method } if ( $this->unstaged_data // need data for country filter && isset( $available_submethod['countries'] ) // if the list exists, the current country key needs to exist and have a true value && empty( $available_submethod['countries'][$this->getData_Unstaged_Escaped( 'country' )] ) ) { continue; // skip 'em if they're not allowed round here } $submethods[$key] = $available_submethod; } return $submethods; } /** * Returns some useful debugging JSON we can append to loglines for * increaded debugging happiness. * This is working pretty well for debugging FormChooser problems, so * let's use it other places. Still, this should probably still be used * sparingly... * @return string JSON-encoded donation data */ public function getLogDebugJSON() { $logObj = array ( 'ffname', 'country', 'currency_code', 'payment_method', 'payment_submethod', 'recurring', 'gateway', 'utm_source', 'referrer', ); foreach ( $logObj as $key => $value ) { $logObj[$value] = $this->getData_Unstaged_Escaped( $value ); unset( $logObj[$key] ); } return json_encode( $logObj ); } } diff --git a/globalcollect_gateway/globalcollect.adapter.php b/globalcollect_gateway/globalcollect.adapter.php index 8e9eedff..26238571 100644 --- a/globalcollect_gateway/globalcollect.adapter.php +++ b/globalcollect_gateway/globalcollect.adapter.php @@ -1,2515 +1,2509 @@ transactions['INSERT_ORDERWITHPAYMENT']['request']['REQUEST']['PARAMS'][ $key ][] = $value */ protected function addKeyToTransaction( $value, $type = 'PAYMENT' ) { if ( !in_array( $value, $this->transactions['INSERT_ORDERWITHPAYMENT']['request']['REQUEST']['PARAMS'][ $type ] ) ) { $this->transactions['INSERT_ORDERWITHPAYMENT']['request']['REQUEST']['PARAMS'][ $type ][] = $value; } } /** * Define accountInfo */ public function defineAccountInfo() { $this->accountInfo = array( 'MERCHANTID' => $this->account_config[ 'MerchantID' ], //'IPADDRESS' => '', //TODO: Not sure if this should be OUR ip, or the user's ip. Hurm. 'VERSION' => "1.0", ); } /** * Define dataConstraints */ public function defineDataConstraints() { $this->dataConstraints = array( // General fields //'ACCOUNTHOLDER' => 'account_holder', AN50 'account_holder' => array( 'type' => 'alphanumeric', 'length' => 50, ), //'ACCOUNTNAME' => 'account_name' AN35 'account_name' => array( 'type' => 'alphanumeric', 'length' => 35, ), //'ACCOUNTNUMBER' => 'account_number' AN30 'account_number' => array( 'type' => 'alphanumeric', 'length' => 30, ), //'ADDRESSLINE1E' => 'address_line_1e' AN35 'address_line_1e' => array( 'type' => 'alphanumeric', 'length' => 35, ), //'ADDRESSLINE2' => 'address_line_2' AN35 'address_line_2' => array( 'type' => 'alphanumeric', 'length' => 35, ), //'ADDRESSLINE3' => 'address_line_3' AN35 'address_line_3' => array( 'type' => 'alphanumeric', 'length' => 35, ), //'ADDRESSLINE4' => 'address_line_4' AN35 'address_line_4' => array( 'type' => 'alphanumeric', 'length' => 35, ), //'ATTEMPTID' => 'attempt_id' N5 'attempt_id' => array( 'type' => 'numeric', 'length' => 5, ), // Did not find this one //'AUTHORISATIONID' => 'authorization_id' AN18 'authorization_id' => array( 'type' => 'alphanumeric', 'length' => 18, ), //'AMOUNT' => 'amount' N12 'amount' => array( 'type' => 'numeric', 'length' => 12, ), //'BANKACCOUNTNUMBER' => 'bank_account_number' AN50 'bank_account_number' => array( 'type' => 'alphanumeric', 'length' => 50, ), //'BANKAGENZIA' => 'bank_agenzia' AN30 'bank_agenzia' => array( 'type' => 'alphanumeric', 'length' => 30, ), //'BANKCHECKDIGIT' => 'bank_check_digit' AN2 'bank_check_digit' => array( 'type' => 'alphanumeric', 'length' => 2, ), //'BANKCODE' => 'bank_code' N5 'bank_code' => array( 'type' => 'numeric', 'length' => 5, ), //'BANKFILIALE' => 'bank_filiale' AN30 'bank_filiale' => array( 'type' => 'alphanumeric', 'length' => 30, ), //'BANKNAME' => 'bank_name' AN40 'bank_name' => array( 'type' => 'alphanumeric', 'length' => 40, ), //'BRANCHCODE' => 'branch_code' N5 'branch_code' => array( 'type' => 'numeric', 'length' => 5, ), //'CITY' => 'city' AN40 'city' => array( 'type' => 'alphanumeric', 'length' => 40, ), //'COUNTRYCODE' => 'country' AN2 'country' => array( 'type' => 'alphanumeric', 'length' => 2, ), //'COUNTRYCODEBANK' => 'country_code_bank' AN2 'country_code_bank' => array( 'type' => 'alphanumeric', 'length' => 2, ), //'COUNTRYDESCRIPTION' => 'country_description' AN50 'country_description' => array( 'type' => 'alphanumeric', 'length' => 50, ), //'CUSTOMERBANKCITY' => 'customer_bank_city' AN50 'customer_bank_city' => array( 'type' => 'alphanumeric', 'length' => 50, ), //'CUSTOMERBANKSTREET' => 'customer_bank_street' AN30 'customer_bank_street' => array( 'type' => 'alphanumeric', 'length' => 30, ), //'CUSTOMERBANKNUMBER' => 'customer_bank_number' N5 'customer_bank_number' => array( 'type' => 'numeric', 'length' => 5, ), //'CUSTOMERBANKZIP' => 'customer_bank_zip' AN10 'customer_bank_zip' => array( 'type' => 'alphanumeric', 'length' => 10, ), //'CREDITCARDNUMBER' => 'card_num' N19 'card_num' => array( 'type' => 'numeric', 'length' => 19, ), //'CURRENCYCODE' => 'currency_code' AN3 'currency_code' => array( 'type' => 'alphanumeric', 'length' => 3, ), //'CVV' => 'cvv' N4 'cvv' => array( 'type' => 'numeric', 'length' => 4, ), //'DATECOLLECT' => 'date_collect' D8 YYYYMMDD 'date_collect' => array( 'type' => 'date', 'length' => 8, ), //'DIRECTDEBITTEXT' => 'direct_debit_text' AN50 'direct_debit_text' => array( 'type' => 'alphanumeric', 'length' => 50, ), //'DOMICILIO' => 'domicilio' AN30 'domicilio' => array( 'type' => 'alphanumeric', 'length' => 30, ), //'EFFORTID' => 'effort_id' N5 'effort_id' => array( 'type' => 'numeric', 'length' => 5, ), //'EMAIL' => 'email' AN70 'email' => array( 'type' => 'alphanumeric', 'length' => 70, ), //'EXPIRYDATE' => 'expiration' N4 MMYY 'expiration' => array( 'type' => 'numeric', 'length' => 4, ), //'FIRSTNAME' => 'fname' AN15 'fname' => array( 'type' => 'alphanumeric', 'length' => 15, ), //'IBAN' => 'iban' AN50 // IBAN is AN21 on direct debit 'iban' => array( 'type' => 'alphanumeric', 'length' => 50, ), //'IPADDRESS' => 'user_ip' AN32 'user_ip' => array( 'type' => 'alphanumeric', 'length' => 32, ), //'ISSUERID' => 'issuer_id' N4 'issuer_id' => array( 'type' => 'numeric', 'length' => 4, ), //'LANGUAGECODE' => 'language' AN2 'language' => array( 'type' => 'alphanumeric', 'length' => 2, ), //'ORDERID' => 'order_id' N10 'order_id' => array( 'type' => 'numeric', 'length' => 10, ), //PAYMENTPRODUCTID 'payment_product' => array( 'type' => 'numeric', 'length' => 5, ), //'PAYMENTREFERENCE' => 'payment_reference' AN20 'payment_reference' => array( 'type' => 'alphanumeric', 'length' => 20, ), //'PROVINCIA' => 'provincia' AN30 'provincia' => array( 'type' => 'alphanumeric', 'length' => 30, ), //'RETURNURL' => 'returnto' AN512 'returnto' => array( 'type' => 'alphanumeric', 'length' => 512, ), //'SPECIALID' => 'special_id' AN255 'special_id' => array( 'type' => 'alphanumeric', 'length' => 255, ), //'STATE' => 'state' AN35 'state' => array( 'type' => 'alphanumeric', 'length' => 35, ), //'STREET' => 'street' AN50 'street' => array( 'type' => 'alphanumeric', 'length' => 50, ), //'SURNAME' => 'lname' AN35 'lname' => array( 'type' => 'alphanumeric', 'length' => 35, ), //'SWIFTCODE' => 'swift_code' AN255 // This is AN11 for several payment types we are not dealing with yet. 'swift_code' => array( 'type' => 'alphanumeric', 'length' => 255, ), //'TRANSACTIONTYPE' => 'transaction_type' AN2 'transaction_type' => array( 'type' => 'alphanumeric', 'length' => 2, ), //'ZIP' => 'zip' AN10 'zip' => array( 'type' => 'alphanumeric', 'length' => 10, ), ); } /** * Define error_map * * @todo * - Add: Error messages */ public function defineErrorMap() { $this->error_map = array( 0 => 'globalcollect_gateway-response-default', 300620 => 'donate_interface-processing-error', // Order ID already used in a previous transaction 430452 => 'globalcollect_gateway-response-default', // Not authorised :: This message was generated when trying to attempt a direct debit transaction from Belgium. 430900 => 'globalcollect_gateway-response-default', // NO VALID PROVIDERS FOUND FOR COMBINATION MERCHANTID: NNNN, PAYMENTPRODUCT: NNN, COUNTRYCODE: XX, CURRENCYCODE: XXX // Errors where the suggested action is to try again 20205 => 'donate_interface-try-again', // COULD NOT START TRANSACTION 103000 => 'donate_interface-try-again', // ANOTHER_ACTION_IS_IN_PROCESS 400850 => 'donate_interface-try-again', // IDEAL_SYSTEM_MAINTENANCE 430150 => 'donate_interface-try-again', // READ_REQUEST_EXCEPTION 430160 => 'donate_interface-try-again', // Unable to authorize ALL_TERMINAL_IDS_FOR_MERCHANT_CURRENCY_IN_USE 430215 => 'donate_interface-try-again', // Unable to authorize COMMS_FAIL_101 430220 => 'donate_interface-try-again', // Unable to authorize COMMS_FAIL_103 430225 => 'donate_interface-try-again', // Unable to authorize COMMS_FAIL_111 430230 => 'donate_interface-try-again', // Unable to authorize COMMS_FAIL_113 430235 => 'donate_interface-try-again', // Unable to authorize COMMS_FAIL_183 430240 => 'donate_interface-try-again', // Unable to authorize COMMS_FAIL_601 430245 => 'donate_interface-try-again', // Unable to authorize COMMS_FAIL_605 430430 => 'donate_interface-try-again', // Unable to authorize TIMEOUT 430433 => 'donate_interface-try-again', // Unable to authorize TOO_MUCH_USAGE 430581 => 'donate_interface-try-again', // Not authorized SOFT_DECLINE_BUYER_HAS_ALTERNATE_FUNDING_SOURCE 485000 => 'donate_interface-try-again', // Unable to authorize NEW_ACCOUNT_INFO_AVAILABLE 485010 => 'donate_interface-try-again', // Unable to authorize TRY_AGAIN_LATER 4311130 => 'donate_interface-try-again', // PBS_SERVICE_NOT_AVAILABLE 4360025 => 'donate_interface-try-again', // ECARD SYSTEM ERROR 4500600 => 'donate_interface-try-again', // BOKU ERROR 4500700 => 'donate_interface-try-again', // SUB1 ERROR 22000045 => 'donate_interface-try-again', // COMMUNICATION ERROR 9999999999 => 'donate_interface-try-again', // ERROR_IN_PROCESSING_THE_REQUEST // Internal messages 'internal-0000' => 'donate_interface-processing-error', // Failed failed pre-process checks. 'internal-0001' => 'donate_interface-processing-error', // Transaction could not be processed due to an internal error. 'internal-0002' => 'donate_interface-processing-error', // Communication failure 'internal-0003' => 'donate_interface-processing-error', // Toxic card, don't retry on pain of $1000+ fine // Do bank validation messages //'dbv-50' => 'globalcollect_gateway-response-dbv-50', // Account number format incorrect //'dbv-80' => 'globalcollect_gateway-response-dbv-80', // Account details missing //'dbv-330' => 'globalcollect_gateway-response-dbv-330', // Check digit format is incorrect //'dbv-340' => 'globalcollect_gateway-response-dbv-340', // Branch code not submitted ); } /** * Define var_map * * @todo * - RETURNURL: Find out where the returnto URL is supposed to be coming from. */ public function defineVarMap() { $this->var_map = array( 'ACCOUNTHOLDER' => 'account_holder', 'ACCOUNTNAME' => 'account_name', 'ACCOUNTNUMBER' => 'account_number', 'ADDRESSLINE1E' => 'address_line_1e', //dd:CH 'ADDRESSLINE2' => 'address_line_2', //dd:CH 'ADDRESSLINE3' => 'address_line_3', //dd:CH 'ADDRESSLINE4' => 'address_line_4', //dd:CH 'ATTEMPTID' => 'attempt_id', 'AUTHORISATIONID' => 'authorization_id', 'AMOUNT' => 'amount', 'BANKACCOUNTNUMBER' => 'bank_account_number', 'BANKAGENZIA' => 'bank_agenzia', // dd:IT 'BANKCHECKDIGIT' => 'bank_check_digit', 'BANKCODE' => 'bank_code', 'BANKFILIALE' => 'bank_filiale', // dd:IT 'BANKNAME' => 'bank_name', 'BRANCHCODE' => 'branch_code', 'CITY' => 'city', 'COUNTRYCODE' => 'country', 'COUNTRYCODEBANK' => 'country_code_bank', 'COUNTRYDESCRIPTION'=> 'country_description', 'CUSTOMERBANKCITY' => 'customer_bank_city', // dd 'CUSTOMERBANKSTREET'=> 'customer_bank_street', // dd 'CUSTOMERBANKNUMBER'=> 'customer_bank_number', // dd 'CUSTOMERBANKZIP' => 'customer_bank_zip', // dd 'CREDITCARDNUMBER' => 'card_num', 'CURRENCYCODE' => 'currency_code', 'CVV' => 'cvv', 'DATECOLLECT' => 'date_collect', 'DESCRIPTOR' => 'descriptor', // eWallets 'DIRECTDEBITTEXT' => 'direct_debit_text', 'DOMICILIO' => 'domicilio', // dd:ES 'EFFORTID' => 'effort_id', 'EMAIL' => 'email', 'EXPIRYDATE' => 'expiration', 'FIRSTNAME' => 'fname', 'IBAN' => 'iban', 'IPADDRESS' => 'server_ip', 'IPADDRESSCUSTOMER' => 'user_ip', 'ISSUERID' => 'issuer_id', 'LANGUAGECODE' => 'language', 'MERCHANTREFERENCE' => 'contribution_tracking_id', //new as of Feb 2014. See also the staging function. 'ORDERID' => 'order_id', 'PAYMENTPRODUCTID' => 'payment_product', 'PAYMENTREFERENCE' => 'payment_reference', 'PROVINCIA' => 'provincia', // dd:ES 'RETURNURL' => 'returnto', 'SPECIALID' => 'special_id', 'STATE' => 'state', 'STREET' => 'street', 'SURNAME' => 'lname', 'SWIFTCODE' => 'swift_code', 'TRANSACTIONTYPE' => 'transaction_type', // dd:GB,NL 'ZIP' => 'zip', 'FISCALNUMBER' => 'fiscal_number', //Boletos ); } /** * Setting some GC-specific defaults. * @param array $options These get extracted in the parent. */ function setGatewayDefaults( $options = array ( ) ) { $returnTitle = isset( $options['returnTitle'] ) ? $options['returnTitle'] : Title::newFromText( 'Special:GlobalCollectGatewayResult' ); $returnTo = isset( $options['returnTo'] ) ? $options['returnTo'] : $returnTitle->getFullURL(); $defaults = array ( 'returnto' => $returnTo, 'attempt_id' => '1', 'effort_id' => '1', ); $this->addRequestData( $defaults ); } /** * Define return_value_map */ public function defineReturnValueMap() { $this->return_value_map = array( 'OK' => true, 'NOK' => false, ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::PENDING, 0, 70 ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::FAILED, 100, 180 ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::PENDING_POKE, 200 ); //The cardholder was successfully authenticated... but we have to DO_FINISHPAYMENT $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::FAILED, 220, 280 ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::PENDING, 300 ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::FAILED, 310, 350 ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::REVISED, 400 ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::PENDING_POKE, 525 ); //"The payment was challenged by your Fraud Ruleset and is pending" - we never see this. $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::PENDING, 550 ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::PENDING_POKE, 600 ); //Payments sit here until we SET_PAYMENT $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::PENDING, 625, 650 ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::COMPLETE, 800, 975 ); //these are all post-authorized, but technically pre-settled... $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::COMPLETE, 1000, 1050 ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::FAILED, 1100, 99999 ); $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::FAILED, 100000, 999999 ); // 102020 - ACTION 130 IS NOT ALLOWED FOR MERCHANT NNN, IPADDRESS NNN.NNN.NNN.NNN $this->defineGoToThankYouOn(); } /** * Sets up the $order_id_meta array. * Should contain the following keys/values: * 'alt_locations' => array( $dataset_name, $dataset_key ) //ordered * 'type' => numeric, or alphanumeric * 'length' => $max_charlen */ public function defineOrderIDMeta() { $this->order_id_meta = array ( 'alt_locations' => array ( '_GET' => 'order_id' ), 'generate' => TRUE, //freaking FINALLY. 'disallow_decimals' => true, //hacky hack hack... ); } /** * Define goToThankYouOn * * The statuses defined in @see GatewayAdapter::$goToThankYouOn will * allow a completed form to go to the Thank you page. * * Allowed: * - complete * - pending * - pending-poke * - revised * * Denied: * - failed * - Any thing else not defined @see FinalStatus * */ public function defineGoToThankYouOn() { $this->goToThankYouOn = array( FinalStatus::COMPLETE, FinalStatus::PENDING, FinalStatus::PENDING_POKE, FinalStatus::REVISED, ); } /** * Define transactions * * Please do not add more transactions to this array. * * @todo * - Does need IPADDRESS? What about the other transactions. Is this the user's IPA? * - Does DO_BANKVALIDATION need HOSTEDINDICATOR? * * This method should define: * - DO_BANKVALIDATION: used prior to INSERT_ORDERWITHPAYMENT for direct debit * - INSERT_ORDERWITHPAYMENT: used for payments * - TEST_CONNECTION: testing connections - is this still valid? * - GET_ORDERSTATUS */ public function defineTransactions() { $this->transactions = array( ); $this->transactions['DO_BANKVALIDATION'] = array( 'request' => array( 'REQUEST' => array( 'ACTION', 'META' => array( 'MERCHANTID', 'IPADDRESS', 'VERSION' ), 'PARAMS' => array( 'GENERAL' => array( 'ACCOUNTNAME', 'ACCOUNTNUMBER', 'AUTHORISATIONID', 'BANKCHECKDIGIT', 'BANKCODE', 'BANKNAME', 'BRANCHCODE', 'COUNTRYCODEBANK', 'DATECOLLECT', // YYYYMMDD 'DIRECTDEBITTEXT', 'IBAN', 'MERCHANTREFERENCE', 'TRANSACTIONTYPE', ), ) ) ), 'values' => array( 'ACTION' => 'DO_BANKVALIDATION', ), ); $this->transactions['INSERT_ORDERWITHPAYMENT'] = array( 'request' => array( 'REQUEST' => array( 'ACTION', 'META' => array( 'MERCHANTID', 'IPADDRESS', 'VERSION' ), 'PARAMS' => array( 'ORDER' => array( 'ORDERID', 'AMOUNT', 'CURRENCYCODE', 'LANGUAGECODE', 'COUNTRYCODE', 'MERCHANTREFERENCE', 'IPADDRESSCUSTOMER', 'EMAIL', ), 'PAYMENT' => array( 'PAYMENTPRODUCTID', 'AMOUNT', 'CURRENCYCODE', 'LANGUAGECODE', 'COUNTRYCODE', 'HOSTEDINDICATOR', 'RETURNURL', // 'CVV', // 'EXPIRYDATE', // 'CREDITCARDNUMBER', 'AUTHENTICATIONINDICATOR', 'FIRSTNAME', 'SURNAME', 'STREET', 'CITY', 'STATE', 'ZIP', 'EMAIL', ) ) ) ), 'values' => array( 'ACTION' => 'INSERT_ORDERWITHPAYMENT', 'HOSTEDINDICATOR' => '1', 'AUTHENTICATIONINDICATOR' => 0, //default to no 3DSecure ourselves ), ); $this->transactions['TEST_CONNECTION'] = array( 'request' => array( 'REQUEST' => array( 'ACTION', 'META' => array( 'MERCHANTID', 'IPADDRESS', 'VERSION' ), 'PARAMS' => array( ) ) ), 'values' => array( 'ACTION' => 'TEST_CONNECTION' ) ); $this->transactions['GET_ORDERSTATUS'] = array( 'request' => array( 'REQUEST' => array( 'ACTION', 'META' => array( 'MERCHANTID', 'IPADDRESS', 'VERSION' ), 'PARAMS' => array( 'ORDER' => array( 'ORDERID', 'EFFORTID', ), ) ) ), 'values' => array( 'ACTION' => 'GET_ORDERSTATUS', 'VERSION' => '2.0' ), ); $this->transactions['CANCEL_PAYMENT'] = array( 'request' => array( 'REQUEST' => array( 'ACTION', 'META' => array( 'MERCHANTID', 'IPADDRESS', 'VERSION' ), 'PARAMS' => array( 'PAYMENT' => array( 'ORDERID', 'EFFORTID', 'ATTEMPTID', ), ) ) ), 'values' => array( 'ACTION' => 'CANCEL_PAYMENT', 'VERSION' => '1.0' ), ); $this->transactions['SET_PAYMENT'] = array( 'request' => array( 'REQUEST' => array( 'ACTION', 'META' => array( 'MERCHANTID', 'IPADDRESS', 'VERSION' ), 'PARAMS' => array( 'PAYMENT' => array( 'ORDERID', 'EFFORTID', 'PAYMENTPRODUCTID', ), ) ) ), 'values' => array( 'ACTION' => 'SET_PAYMENT', 'VERSION' => '1.0' ), ); $this->transactions['DO_FINISHPAYMENT'] = array( 'request' => array( 'REQUEST' => array( 'ACTION', 'META' => array( 'MERCHANTID', 'IPADDRESS', 'VERSION' ), 'PARAMS' => array( 'PAYMENT' => array( 'ORDERID', 'EFFORTID', 'ATTEMPTID', ), ) ) ), 'values' => array( 'ACTION' => 'DO_FINISHPAYMENT', 'VERSION' => '1.0', ), ); $this->transactions['DO_PAYMENT'] = array( 'request' => array( 'REQUEST' => array( 'ACTION', 'META' => array( 'MERCHANTID', 'IPADDRESS', 'VERSION', ), 'PARAMS' => array( 'PAYMENT' => array( 'MERCHANTREFERENCE', 'ORDERID', 'EFFORTID', 'PAYMENTPRODUCTID', 'AMOUNT', 'CURRENCYCODE', 'HOSTEDINDICATOR', 'AUTHENTICATIONINDICATOR', ), ) ) ), 'values' => array( 'ACTION' => 'DO_PAYMENT', 'VERSION' => '1.0', 'HOSTEDINDICATOR' => '0', 'AUTHENTICATIONINDICATOR' => '0', ), ); } /** * Define payment methods * * The credit card group has a catchall for unspecified payment types. */ public function definePaymentMethods() { $this->payment_methods = array(); // Bank Transfers $this->payment_methods['bt'] = array( 'label' => 'Bank transfer', 'validation' => array( 'creditCard' => false, ), 'short_circuit_at' => 'first_iop', ); // Credit Cards $this->payment_methods['cc'] = array( 'label' => 'Credit Cards', ); // Direct Debit $this->payment_methods['dd'] = array( 'label' => 'Direct Debit', 'validation' => array( 'creditCard' => false, ), 'short_circuit_at' => 'first_iop', ); // eWallets $this->payment_methods['ew'] = array( 'label' => 'eWallets', 'validation' => array( 'address' => false, 'creditCard' => false, ), 'short_circuit_at' => 'first_iop', 'additional_success_status' => array( 20 ), ); // Bank Transfers $this->payment_methods['obt'] = array( 'label' => 'Online bank transfer', 'validation' => array( 'creditCard' => false, ), 'short_circuit_at' => 'first_iop', ); // Real Time Bank Transfers $this->payment_methods['rtbt'] = array( 'label' => 'Real time bank transfer', 'short_circuit_at' => 'first_iop', 'additional_success_status' => array( 20 ), ); // Cash payments $this->payment_methods['cash'] = array( 'label' => 'Cash payments', 'short_circuit_at' => 'first_iop', 'additional_success_status' => array( 55 ), //PENDING AT CUSTOMER - denotes they need to go to the bank, but we've done all we can. ); // *** Define payment submethods *** //TODO: deprecate submethod, everything is a first-class method. $this->payment_submethods = array(); /* * Default => Credit Card * * Every payment_method should have a payment_submethod. * This is just a catch-all to ensure some validation happens. * FIXME: I don't think this clause gets used. */ $this->payment_submethods[''] = array( 'paymentproductid' => 0, 'label' => 'Any', 'group' => 'cc', 'validation' => array( 'address' => true, 'amount' => true, 'email' => true, 'name' => true, ), 'keys' => array(), ); /* * Bank transfers */ // Bank Transfer $this->payment_submethods['bt'] = array( 'paymentproductid' => 11, 'label' => 'Bank Transfer', 'validation' => array(), 'keys' => array(), ); /* * Credit Card */ // Visa $this->payment_submethods['visa'] = array( 'paymentproductid' => 1, 'label' => 'Visa', 'group' => 'cc', 'validation' => array(), 'keys' => array(), ); // MasterCard $this->payment_submethods['mc'] = array( 'paymentproductid' => 3, 'label' => 'MasterCard', 'group' => 'cc', 'validation' => array(), 'keys' => array(), ); // American Express $this->payment_submethods['amex'] = array( 'paymentproductid' => 2, 'label' => 'American Express', 'group' => 'cc', 'validation' => array(), 'keys' => array(), ); // Maestro $this->payment_submethods['maestro'] = array( 'paymentproductid' => 117, 'label' => 'Maestro', 'group' => 'cc', 'validation' => array(), 'keys' => array(), ); // Solo $this->payment_submethods['solo'] = array( 'paymentproductid' => 118, 'label' => 'Solo', 'group' => 'cc', 'validation' => array(), 'keys' => array(), ); // Laser $this->payment_submethods['laser'] = array( 'paymentproductid' => 124, 'label' => 'Laser', 'group' => 'cc', 'validation' => array(), 'keys' => array(), ); // JCB $this->payment_submethods['jcb'] = array( 'paymentproductid' => 125, 'label' => 'JCB', 'group' => 'cc', 'validation' => array(), 'keys' => array(), ); // Discover $this->payment_submethods['discover'] = array( 'paymentproductid' => 128, 'label' => 'Discover', 'group' => 'cc', 'validation' => array(), 'keys' => array(), ); // CB $this->payment_submethods['cb'] = array( 'paymentproductid' => 130, 'label' => 'CB', // Carte Bancaire OR Carte Bleue 'group' => 'cc', 'validation' => array(), 'keys' => array(), ); /* * Direct debit * * See: WebCollect 7.1 Technical guide: Appendix H Country-specific direct debit keys * * - keys: These values, which can be found in $this->var_map, will only be put in the request, if they are populated from the form or staging. */ // Direct debit: AT $this->payment_submethods['dd_at'] = array( 'paymentproductid' => 703, 'label' => 'Direct debit: AT', 'group' => 'dd', 'validation' => array(), 'keys' => array( 'ACCOUNTNAME', 'ACCOUNTNUMBER', 'BANKCODE', /*'BANKNAME',*/ 'DIRECTDEBITTEXT', ), ); // Direct debit: BE $this->payment_submethods['dd_be'] = array( 'paymentproductid' => 706, 'label' => 'Direct debit: BE', 'group' => 'dd', 'validation' => array(), 'keys' => array( 'ACCOUNTNAME', 'ACCOUNTNUMBER', 'AUTHORISATIONID', 'BANKCHECKDIGIT', 'BANKCODE', 'BANKNAME', 'DIRECTDEBITTEXT', ), //'keys' => array( /*'ACCOUNTNAME',*/ 'ACCOUNTNUMBER', 'AUTHORISATIONID', /*'BANKCHECKDIGIT',*/ 'BANKCODE', /*'BANKNAME',*/ 'DIRECTDEBITTEXT', ), ); // Direct debit: CH $this->payment_submethods['dd_ch'] = array( 'paymentproductid' => 707, 'label' => 'Direct debit: CH', 'group' => 'dd', 'validation' => array(), 'keys' => array( 'ACCOUNTNAME', 'ACCOUNTNUMBER', 'ADDRESSLINE1E', 'ADDRESSLINE2', 'ADDRESSLINE3', 'ADDRESSLINE4', 'BANKCODE', /*'BANKNAME',*/ /*'CUSTOMERBANKCITY', 'CUSTOMERBANKNUMBER', 'CUSTOMERBANKSTREET', 'CUSTOMERBANKZIP',*/ 'DIRECTDEBITTEXT', 'IBAN', ), ); // Direct debit: DE $this->payment_submethods['dd_de'] = array( 'paymentproductid' => 702, 'label' => 'Direct debit: DE', 'group' => 'dd', 'validation' => array(), 'keys' => array( 'ACCOUNTNAME', 'ACCOUNTNUMBER', 'BANKCODE', /*'BANKNAME',*/ 'DIRECTDEBITTEXT', ), ); // Direct debit: ES $this->payment_submethods['dd_es'] = array( 'paymentproductid' => 709, 'label' => 'Direct debit: ES', 'group' => 'dd', 'validation' => array(), 'keys' => array( 'ACCOUNTNAME', 'ACCOUNTNUMBER', 'BANKCODE', /*'BANKNAME',*/ 'BRANCHCODE', 'BANKCHECKDIGIT', /*'CUSTOMERBANKCITY', 'CUSTOMERBANKSTREET', 'CUSTOMERBANKZIP',*/ 'DIRECTDEBITTEXT', /*'DOMICILIO', 'PROVINCIA',*/ ), ); // Direct debit: FR $this->payment_submethods['dd_fr'] = array( 'paymentproductid' => 704, 'label' => 'Direct debit: FR', 'group' => 'dd', 'validation' => array(), 'keys' => array( 'ACCOUNTNAME', 'ACCOUNTNUMBER', 'BANKCODE', /*'BANKNAME',*/ 'BRANCHCODE', 'BANKCHECKDIGIT', 'DIRECTDEBITTEXT', ), ); // Direct debit: GB $this->payment_submethods['dd_gb'] = array( 'paymentproductid' => 705, 'label' => 'Direct debit: GB', 'group' => 'dd', 'validation' => array(), 'keys' => array( 'ACCOUNTNAME', 'ACCOUNTNUMBER', 'AUTHORISATIONID', 'BANKCODE', /*'BANKNAME',*/ 'DIRECTDEBITTEXT', 'TRANSACTIONTYPE', ), ); // Direct debit: IT $this->payment_submethods['dd_it'] = array( 'paymentproductid' => 708, 'label' => 'Direct debit: IT', 'group' => 'dd', 'validation' => array(), 'keys' => array( 'ACCOUNTNAME', 'ACCOUNTNUMBER', 'BANKCODE', /*'BANKNAME',*/ 'BRANCHCODE', 'BANKAGENZIA', 'BANKCHECKDIGIT', /*'BANKFILIALE',*/ /*'CUSTOMERBANKCITY', 'CUSTOMERBANKNUMBER', 'CUSTOMERBANKSTREET', 'CUSTOMERBANKZIP',*/ 'DIRECTDEBITTEXT', ), ); // Direct debit: NL $this->payment_submethods['dd_nl'] = array( 'paymentproductid' => 701, 'label' => 'Direct debit: NL', 'group' => 'dd', 'validation' => array(), 'keys' => array( 'ACCOUNTNAME', 'ACCOUNTNUMBER', /*'BANKNAME',*/ 'DIRECTDEBITTEXT', 'TRANSACTIONTYPE', ), ); /* * eWallets */ // eWallets PayPal $this->payment_submethods['ew_paypal'] = array( 'paymentproductid' => 840, 'label' => 'eWallets: PayPal', 'group' => 'ew', 'validation' => array(), 'keys' => array(), ); // eWallets WebMoney $this->payment_submethods['ew_webmoney'] = array( 'paymentproductid' => 841, 'label' => 'eWallets: WebMoney', 'group' => 'ew', 'validation' => array(), 'keys' => array(), ); // eWallets Yandex $this->payment_submethods['ew_yandex'] = array( 'paymentproductid' => 849, 'label' => 'eWallets: Yandex', 'group' => 'ew', 'validation' => array(), 'keys' => array(), ); // eWallets Alipay $this->payment_submethods['ew_alipay'] = array( 'paymentproductid' => 861, 'label' => 'eWallets: Alipay', 'group' => 'ew', 'validation' => array(), 'keys' => array(), ); // eWallets Moneybookers $this->payment_submethods['ew_moneybookers'] = array( 'paymentproductid' => 843, 'label' => 'eWallets: Moneybookers', 'group' => 'ew', 'validation' => array(), 'keys' => array(), ); // eWallets cashU $this->payment_submethods['ew_cashu'] = array( 'paymentproductid' => 845, 'label' => 'eWallets: cashU', 'group' => 'ew', 'validation' => array(), 'keys' => array(), ); /* * Online bank transfers */ // Online Bank Transfer Bpay $this->payment_submethods['bpay'] = array( 'paymentproductid' => 500, 'label' => 'Online Bank Transfer: Bpay', 'group' => 'obt', 'validation' => array(), 'keys' => array(), ); /* * Real time bank transfers */ // Nordea (Sweden) $this->payment_submethods['rtbt_nordea_sweden'] = array( 'paymentproductid' => 805, 'label' => 'Nordea (Sweden)', 'group' => 'rtbt', 'validation' => array(), 'keys' => array(), ); // Ideal $this->payment_submethods['rtbt_ideal'] = array( 'paymentproductid' => 809, 'label' => 'Ideal', 'group' => 'rtbt', 'validation' => array(), 'keys' => array(), 'issuerids' => array( 771 => 'SNS Regio Bank', 161 => 'Van Lanschot Bankiers', 31 => 'ABN AMRO', 761 => 'ASN Bank', 21 => 'Rabobank', 511 => 'Triodos Bank', 721 => 'ING', 751 => 'SNS Bank', 91 => 'Friesland Bank', 801 => 'Knab', ) ); // eNETS $this->payment_submethods['rtbt_enets'] = array( 'paymentproductid' => 810, 'label' => 'eNETS', 'group' => 'rtbt', 'validation' => array(), 'keys' => array(), ); // Sofortuberweisung/DIRECTebanking $this->payment_submethods['rtbt_sofortuberweisung'] = array( 'paymentproductid' => 836, 'label' => 'Sofortuberweisung/DIRECTebanking', 'group' => 'rtbt', 'validation' => array(), 'keys' => array(), ); // eps Online-Überweisung $this->payment_submethods['rtbt_eps'] = array( 'paymentproductid' => 856, 'label' => 'eps Online-Überweisung', 'group' => 'rtbt', 'validation' => array(), 'keys' => array(), 'issuerids' => array( 824 => 'Bankhaus Spängler', 825 => 'Hypo Tirol Bank', 822 => 'NÖ HYPO', 823 => 'Voralberger HYPO', 828 => 'P.S.K.', 829 => 'Easy', 826 => 'Erste Bank und Sparkassen', 827 => 'BAWAG', 820 => 'Raifeissen', 821 => 'Volksbanken Gruppe', 831 => 'Sparda-Bank', ) ); // Cash Payments - Boletos $this->payment_submethods['cash_boleto'] = array( 'paymentproductid' => 1503, 'label' => 'Boleto Bancario Brazil', 'group' => 'cash', 'keys' => array(), ); } public function doPayment() { $payment_method = $this->getPaymentMethod(); // FIXME: this should happen during normalization, and before validatation. if ( $payment_method === 'dd' and !$this->getPaymentSubmethod() ) { // Synthesize a submethod based on the country. $country_code = strtolower( $this->getData_Unstaged_Escaped( 'country' ) ); $this->addRequestData( array( 'payment_submethod' => "dd_{$country_code}", ) ); } // Execute the proper transaction code: switch ( $payment_method ) { case 'cc': // FIXME: we don't actually use this code path, it's done from gc.cc.js instead. $this->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); // Display an iframe for credit cards return PaymentResult::newIframe( $this->getTransactionDataFormAction() ); case 'bt': case 'obt': $this->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); if ( in_array( $this->getFinalStatus(), $this->getGoToThankYouOn() ) ) { return PaymentResult::newForm( 'end-' . $payment_method ); } break; case 'dd': $this->do_transaction('Direct_Debit'); break; case 'ew': case 'rtbt': case 'cash': $this->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); $formAction = $this->getTransactionDataFormAction(); // Redirect to the bank if ( $formAction ) { return PaymentResult::newRedirect( $formAction ); } break; default: $this->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); } return PaymentResult::fromResults( $this->transaction_response, $this->getFinalStatus() ); } /** * Because GC has some processes that involve more than one do_transaction * chained together, we're catching those special ones in an overload and * letting the rest behave normally. * @return PaymentTransactionResponse */ public function do_transaction( $transaction ) { $this->session_addDonorData(); switch ( $transaction ){ case 'Confirm_CreditCard' : $this->getStopwatch( 'Confirm_CreditCard', true ); $result = $this->transactionConfirm_CreditCard(); $this->saveCommunicationStats( 'Confirm_CreditCard', $transaction ); return $result; case 'Direct_Debit' : $this->getStopwatch( 'Direct_Debit', true ); $result = $this->transactionDirect_Debit(); $this->saveCommunicationStats( 'Direct_Debit', $transaction ); return $result; case 'Recurring_Charge' : return $this->transactionRecurring_Charge(); default: return parent::do_transaction( $transaction ); } } /** * Either confirm or reject the payment * @global WebRequest $wgRequest * @return PaymentTransactionResponse */ private function transactionConfirm_CreditCard(){ global $wgRequest; //this is for pulling vars straight from the querystring $pull_vars = array( 'CVVRESULT' => 'cvv_result', 'AVSRESULT' => 'avs_result', ); // FIXME: Refactor as normal unstaging. $qsResults = array(); foreach ( $pull_vars as $theirkey => $ourkey) { $tmp = $wgRequest->getVal( $theirkey, null ); if ( !is_null( $tmp ) ) { $qsResults[$ourkey] = $tmp; } } $is_orphan = false; if ( count( $qsResults ) ){ // Nothing unusual here. Oh, except we are reading query parameters from // what we hope is a redirect back from the processor, caused by an earlier // transaction. $this->addResponseData( $qsResults ); $logmsg = 'CVV Result from querystring: ' . $this->getData_Unstaged_Escaped( 'cvv_result' ); $logmsg .= ', AVS Result from querystring: ' . $this->getData_Unstaged_Escaped( 'avs_result' ); $this->logger->info( $logmsg ); //add an antimessage for everything but orphans $this->logger->info( 'Adding Antimessage' ); $this->deleteLimboMessage( self::GC_CC_LIMBO_QUEUE ); - // TODO: Stop mirroring to STOMP - $this->doLimboStompTransaction( true ); } else { //this is an orphan transaction. $is_orphan = true; //have to change this code range: All these are usually "pending" and //that would still be true... //...aside from the fact that if the user has gotten this far, they left //the part where they could add more data. //By now, "incomplete" definitely means "failed" for 0-70. $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::FAILED, 0, 70 ); } $cancelflag = false; //this will denote the thing we're trying to do with the donation attempt $problemflag = false; //this will get set to true, if we can't continue and need to give up and just log the hell out of it. $problemmessage = ''; //to be used in conjunction with the flag. $problemseverity = LogLevel::ERROR; //to be used also in conjunction with the flag, to route the message to the appropriate log. Urf. $original_status_code = NULL; $ran_hooks = false; $loopcount = $this->getGlobal( 'RetryLoopCount' ); $loops = 0; for ( $loops = 0; $loops < $loopcount && !$cancelflag && !$problemflag; ++$loops ){ $gotCVV = false; $status_result = $this->do_transaction( 'GET_ORDERSTATUS' ); $validationAction = $this->getValidationAction(); if ( !$is_orphan ) { // live users get antifraud hooks run in this txn's pre-process $ran_hooks = true; } // FIXME: Refactor as normal unstaging. $xmlResults = array( 'cvv_result' => '', 'avs_result' => '' ); $data = $status_result->getData(); if ( !empty( $data ) ) { foreach ( $pull_vars as $theirkey => $ourkey) { if ( !array_key_exists( $theirkey, $data ) ) { continue; } $gotCVV = true; $xmlResults[$ourkey] = $data[$theirkey]; if ( array_key_exists( $ourkey, $qsResults ) && $qsResults[$ourkey] != $xmlResults[$ourkey] ) { $problemflag = true; $problemmessage = "$theirkey value '$qsResults[$ourkey]' from querystring does not match value '$xmlResults[$ourkey]' from GET_ORDERSTATUS XML"; } } } $this->addResponseData( $xmlResults ); $logmsg = 'CVV Result from XML: ' . $this->getData_Unstaged_Escaped( 'cvv_result' ); $logmsg .= ', AVS Result from XML: ' . $this->getData_Unstaged_Escaped( 'avs_result' ); $this->logger->info( $logmsg ); if ( $status_result->getForceCancel() ) { $cancelflag = true; //don't retry or MasterCard will fine us } if ( $is_orphan && !$cancelflag && !empty( $data ) ) { $action = $this->findCodeAction( 'GET_ORDERSTATUS', 'STATUSID', $data['STATUSID'] ); if ( $action === FinalStatus::PENDING_POKE && !$ran_hooks ){ //only want to do this once - it's not going to change. $this->runAntifraudHooks(); $ran_hooks = true; } $validationAction = $this->getValidationAction(); } //we filtered if ( $validationAction !== 'process' ){ $cancelflag = true; //don't retry: We've fraud-failed them intentionally. } elseif ( $status_result->getCommunicationStatus() === false ) { //can't communicate or internal error $problemflag = true; $problemmessage = "Can't communicate or internal error."; // /me shrugs - I think the orphan slayer is hitting this sometimes. Confusing. } $order_status_results = false; if ( !$cancelflag && !$problemflag ) { // $order_status_results = $this->getFinalStatus(); $txn_data = $this->getTransactionData(); if (isset($txn_data['STATUSID'])){ if( is_null( $original_status_code ) ){ $original_status_code = $txn_data['STATUSID']; } $order_status_results = $this->findCodeAction( 'GET_ORDERSTATUS', 'STATUSID', $txn_data['STATUSID'] ); } if ( $loops === 0 && $is_orphan && !is_null( $original_status_code ) ){ //save stats. if (!isset($this->orphanstats) || !isset( $this->orphanstats[$original_status_code] ) ){ $this->orphanstats[$original_status_code] = 1; } else { $this->orphanstats[$original_status_code] += 1; } } if (!$order_status_results){ $problemflag = true; $problemmessage = "We don't have an order status after doing a GET_ORDERSTATUS."; } switch ( $order_status_results ){ case FinalStatus::FAILED : case FinalStatus::REVISED : $cancelflag = true; //makes sure we don't try to confirm. break 2; case FinalStatus::COMPLETE : $problemflag = true; //nothing to be done. $problemmessage = "GET_ORDERSTATUS reports that the payment is already complete."; $problemseverity = LogLevel::INFO; break 2; case FinalStatus::PENDING_POKE : if ( $is_orphan && !$gotCVV ){ $problemflag = true; $problemmessage = "Unable to retrieve orphan cvv/avs results (Communication problem?)."; } if ( !$ran_hooks ) { $problemflag = true; $problemmessage = 'On the brink of payment confirmation without running antifraud hooks'; $problemseverity = LogLevel::ERROR; break 2; } //none of this should ever execute for a transaction that doesn't use 3d secure... if ( $txn_data['STATUSID'] === '200' && ( $loops < $loopcount-1 ) ){ $this->logger->info( "Running DO_FINISHPAYMENT ($loops)" ); $dopayment_result = $this->do_transaction( 'DO_FINISHPAYMENT' ); $dopayment_data = $dopayment_result->getData(); //Check the txn status and result code to see if we should bother continuing if ( $this->getTransactionStatus() ){ $this->logger->info( "DO_FINISHPAYMENT ($loops) returned with status ID " . $dopayment_data['STATUSID'] ); if ( $this->findCodeAction( 'GET_ORDERSTATUS', 'STATUSID', $dopayment_data['STATUSID'] ) === FinalStatus::FAILED ){ //ack and die. $problemflag = true; //nothing to be done. $problemmessage = "DO_FINISHPAYMENT says the payment failed. Giving up forever."; $this->finalizeInternalStatus( FinalStatus::FAILED ); } } else { $this->logger->error( "DO_FINISHPAYMENT ($loops) returned NOK" ); } break; } if ( $txn_data['STATUSID'] !== '200' ) { break 2; //no need to loop. } case FinalStatus::PENDING : //if it's really pending at this point, we need to... //...leave it alone. If we're orphan slaying, this will stay in the queue. break 2; } } } //if we got here with no problemflag, //confirm or cancel the payment based on $cancelflag if ( !$problemflag ){ if ( is_array( $data ) ){ // FIXME: Refactor as normal unstaging. //if they're set, get CVVRESULT && AVSRESULT $pull_vars['EFFORTID'] = 'effort_id'; $pull_vars['ATTEMPTID'] = 'attempt_id'; $addme = array(); foreach ( $pull_vars as $theirkey => $ourkey) { if ( array_key_exists( $theirkey, $data ) ){ $addme[$ourkey] = $data[$theirkey]; } } if ( count( $addme ) ){ $this->addResponseData( $addme ); } } if ( !$cancelflag ) { $final = $this->do_transaction( 'SET_PAYMENT' ); if ( $final->getCommunicationStatus() === true ) { $this->finalizeInternalStatus( FinalStatus::COMPLETE ); //get the old status from the first txn, and add in the part where we set the payment. $this->transaction_response->setTxnMessage( "Original Response Status (pre-SET_PAYMENT): " . $original_status_code ); $this->runPostProcessHooks(); // Queueing is in here. } else { $this->finalizeInternalStatus( FinalStatus::FAILED ); $problemflag = true; $problemmessage = "SET_PAYMENT couldn't communicate properly!"; } } else { if ($order_status_results === false){ //we didn't do the check, because we're going to fail the thing. /** * No need to send an explicit CANCEL_PAYMENT here, because * the payment has not been set. * In fact, GC will error out if we try to do that, and tell * us there is nothing to cancel. */ $this->finalizeInternalStatus( FinalStatus::FAILED ); } else { //in case we got wiped out, set the final status to what it was before. $this->finalizeInternalStatus( $order_status_results ); } } } if ( $problemflag || $cancelflag ){ if ( $cancelflag ){ //cancel wins $problemmessage = "Cancelling payment"; $problemseverity = LogLevel::INFO; $errors = array( '1000001' => $problemmessage ); } else { $errors = array( '1000000' => 'Transaction could not be processed due to an internal error.' ); } //we have probably had a communication problem that could mean stranded payments. $this->logger->log( $problemseverity, $problemmessage ); //hurm. It would be swell if we had a message that told the user we had some kind of internal error. $ret = new PaymentTransactionResponse(); $ret->setCommunicationStatus( false ); //DO NOT PREPEND $problemmessage WITH ANYTHING! //orphans.php is looking for specific things in position 0. $ret->setMessage( $problemmessage ); foreach( $errors as $code => $error ) { $ret->addError( $code, array( 'message' => $error, 'debugInfo' => 'Failure in transactionConfirm_CreditCard', 'logLevel' => $problemseverity ) ); } // TODO: should we set $this->transaction_response ? return $ret; } // return something better... if we need to! return $status_result; } /** * Process a non-initial effort_id charge. */ protected function transactionRecurring_Charge() { $response = $this->do_transaction( 'DO_PAYMENT' ); $result = PaymentResult::fromResults( $response, $this->getFinalStatus() ); if ( $response->getCommunicationStatus() && !$result->isFailed() && !$result->getErrors() ) { $response = $this->do_transaction( 'GET_ORDERSTATUS' ); $data = $this->getTransactionData(); $orderStatus = $this->findCodeAction( 'GET_ORDERSTATUS', 'STATUSID', $data['STATUSID'] ); if ( $this->getTransactionStatus() && $orderStatus === FinalStatus::PENDING_POKE ) { $this->transactions['SET_PAYMENT']['values']['PAYMENTPRODUCTID'] = $data['PAYMENTPRODUCTID']; $response = $this->do_transaction('SET_PAYMENT'); } } return $response; } protected function transactionDirect_Debit() { $result = $this->do_transaction('DO_BANKVALIDATION'); if ( $result->getCommunicationStatus() ) { $this->transactions['INSERT_ORDERWITHPAYMENT']['values']['HOSTEDINDICATOR'] = 0; $result = $this->do_transaction('INSERT_ORDERWITHPAYMENT'); if ( $result->getCommunicationStatus() === true ) { if ( $this->getFinalStatus() === FinalStatus::PENDING_POKE ) { $txn_data = $this->getTransactionData(); $original_status_code = isset( $txn_data['STATUSID']) ? $txn_data['STATUSID'] : 'NOT SET'; $result = $this->do_transaction( 'SET_PAYMENT' ); if ( $result->getCommunicationStatus() === true ) { $this->finalizeInternalStatus( FinalStatus::COMPLETE ); - // TODO: Stop emitting antimessage. - $this->doLimboStompTransaction( true ); } else { $this->finalizeInternalStatus( FinalStatus::FAILED ); //get the old status from the first txn, and add in the part where we set the payment. $this->transaction_response->setTxnMessage( "Original Response Status (pre-SET_PAYMENT): " . $original_status_code ); } // We won't need the limbo message again, either way, so cancel it. $this->deleteLimboMessage(); } } } return $result; } /** * Parse the response to get the status. Not sure if this should return a bool, or something more... telling. * * @param DomDocument $response The response XML loaded into a DomDocument * @return bool */ public function parseResponseCommunicationStatus( $response ) { $aok = true; foreach ( $response->getElementsByTagName( 'RESULT' ) as $node ) { if ( array_key_exists( $node->nodeValue, $this->return_value_map ) && $this->return_value_map[$node->nodeValue] !== true ) { $aok = false; } } return $aok; } /** * Parse the response to get the errors in a format we can log and otherwise deal with. * return a key/value array of codes (if they exist) and messages. * * If the site has $wgDonationInterfaceDisplayDebug = true, then the real * messages will be sent to the client. Messages will not be translated or * obfuscated. * * @param array $response The response array * @return array */ public function parseResponseErrors( $response ) { $errors = array( ); foreach ( $response->getElementsByTagName( 'ERROR' ) as $node ) { $code = ''; $message = ''; $debugInfo = ''; foreach ( $node->childNodes as $childnode ) { if ( $childnode->nodeName === "CODE" ) { $code = $childnode->nodeValue; } if ( $childnode->nodeName === "MESSAGE" ) { $message = $childnode->nodeValue; $debugInfo = $message; //I am hereby done screwing around with GC field constraint violations. //They vary between ***and within*** payment types, and their docs are a joke. if ( strpos( $message, 'DOES NOT HAVE LENGTH' ) !== false ) { $this->logger->error( $message ); } } } $errors[ $code ] = array( 'logLevel' => LogLevel::ERROR, 'message' => ( $this->getGlobal( 'DisplayDebug' ) ) ? '*** ' . $message : $this->getErrorMapByCodeAndTranslate( $code ), 'debugInfo' => $debugInfo, ); } return $errors; } /** * Harvest the data we need back from the gateway. * return a key/value array * * When we set lookup error code ranges, we use GET_ORDERSTATUS as the key for search * because they are only defined for that transaction type. * * @param DOMDocument $response The response object * @return array */ public function parseResponseData( $response ) { $data = array( ); $transaction = $this->getCurrentTransaction(); switch ( $transaction ) { case 'INSERT_ORDERWITHPAYMENT': $data = $this->xmlChildrenToArray( $response, 'ROW' ); $data['ORDER'] = $this->xmlChildrenToArray( $response, 'ORDER' ); $data['PAYMENT'] = $this->xmlChildrenToArray( $response, 'PAYMENT' ); //if we have no order ID yet (or it's somehow wrong), retrieve it and put it in the usual place. if ( array_key_exists( 'ORDERID', $data ) && ( $data['ORDERID'] != $this->getData_Unstaged_Escaped( 'order_id' ) ) ) { $this->logger->info( "inside " . $data['ORDERID'] ); $this->normalizeOrderID( $data['ORDERID'] ); $this->logger->info( print_r( $this->getOrderIDMeta(), true ) ); $this->addRequestData( array ( 'order_id' => $data['ORDERID'] ) ); $this->logger->info( print_r( $this->getOrderIDMeta(), true ) ); $this->session_addDonorData(); } //if we're of a type that sends donors off never to return, we should record that here. $payment_info = $this->getPaymentMethodMeta(); if ( array_key_exists( 'short_circuit_at', $payment_info ) && $payment_info['short_circuit_at'] === 'first_iop' ){ if ( array_key_exists( 'additional_success_status', $payment_info ) && is_array( $payment_info['additional_success_status'] ) ){ foreach ( $payment_info['additional_success_status'] as $status ){ //mangle the definition of success. $this->addCodeRange( 'GET_ORDERSTATUS', 'STATUSID', FinalStatus::COMPLETE, $status ); } } if ( $this->getTransactionStatus() ) { $this->finalizeInternalStatus( $this->findCodeAction( 'GET_ORDERSTATUS', 'STATUSID', $data['STATUSID'] ) ); } } break; case 'DO_BANKVALIDATION': $data = $this->xmlChildrenToArray( $response, 'RESPONSE' ); unset( $data['META'] ); $data['errors'] = array(); $data['CHECKSPERFORMED'] = $this->xmlGetChecks( $response ); $data['VALIDATIONID'] = $this->xmlChildrenToArray( $response, 'VALIDATIONID' ); // Final Status will already be set if the transaction was unable to communicate properly. if ( $this->getTransactionStatus() ) { $this->finalizeInternalStatus( $this->checkDoBankValidation( $data ) ); } break; case 'GET_ORDERSTATUS': $data = $this->xmlChildrenToArray( $response, 'STATUS' ); $data['ORDER'] = $this->xmlChildrenToArray( $response, 'ORDER' ); break; case 'DO_FINISHPAYMENT': $data = $this->xmlChildrenToArray( $response, 'ROW' ); break; case 'DO_PAYMENT': $data = $this->xmlChildrenToArray( $response, 'ROW' ); if ( isset( $data['STATUSID'] ) ) { $this->finalizeInternalStatus( $this->findCodeAction( 'GET_ORDERSTATUS', 'STATUSID', $data['STATUSID'] ) ); } else { $this->finalizeInternalStatus( FinalStatus::FAILED ); } $data['ORDER'] = $this->xmlChildrenToArray( $response, 'ORDER' ); break; } return $data; } /** * Parse the response object for the checked validations * * @param DOMDocument $response The response object * @return array */ protected function xmlGetChecks( $response ) { $data = array( 'CHECKS' => array(), ); $checks = $response->getElementsByTagName( 'CHECK' ); foreach ( $checks as $check ) { // Get the check code $checkCode = $check->getElementsByTagName('CHECKCODE')->item(0)->nodeValue; // Remove zero paddding $checkCode = ltrim( $checkCode, '0'); // Convert it too an integer settype( $checkCode, 'integer' ); $data['CHECKS'][ $checkCode ] = $check->getElementsByTagName('CHECKRESULT')->item(0)->nodeValue; } // Sort the error codes ksort( $data['CHECKS'] ); return $data; } /** * Interpret DO_BANKVALIDATION checks performed. * * This will use the error map. * * PASSED is a successful validation. * * ERROR is a validation failure. * * WARNING: For now, this will be ignored. * * NOTCHECKED does not need to be worried about in the check results. These * are supposed to appear if a validation failed, rendering the other * validations pointless to check. * * @todo * - There is a problem with the manual for DO_BANKVALIDATION. Failure should return NOK. Is this only on development? * - Messages are not being translated by the provider. * - What do we do about WARNING? For now, it is fail? * - Get the validation id * * @param array $data The data array * * @throws UnexpectedValueException * @return string One of the constants defined in @see FinalStatus */ public function checkDoBankValidation( &$data ) { $checks = &$data['CHECKSPERFORMED']; $isPass = 0; $isError = 0; $isWarning = 0; $isNotChecked = 0; if ( !is_array( $checks['CHECKS'] ) ) { // Should we trigger an error if no checks are performed? // For now, just return failed. return FinalStatus::FAILED; } // We only mark validation as a failure if we have warnings or errors. $return = FinalStatus::COMPLETE; foreach ( $checks['CHECKS'] as $checkCode => $checkResult ) { // Prefix error codes with dbv for DO_BANKVALIDATION $code = 'dbv-' . $checkCode; if ( $checkResult == 'ERROR' ) { $isError++; // Message might need to be put somewhere else. $data['errors'][ $code ] = $this->getErrorMap( $code ); } elseif ( $checkResult == 'NOTCHECKED' ) { $isNotChecked++; } elseif ( $checkResult == 'PASSED' ) { $isPass++; } elseif ( $checkResult == 'WARNING' ) { $isWarning++; // Message might need to be put somewhere else. $data['errors'][ $code ] = $this->getErrorMap( $code ); } else { $message = 'Unknown check result: (' . $checkResult . ')'; throw new UnexpectedValueException( $message ); } } // The return text needs to match something in @see $this->defineGoToThankYouOn() if ( $isPass ) { $return = FinalStatus::COMPLETE; } if ( $isWarning ) { $this->logger->error( 'Got warnings from bank validation: ' . print_r( $data['errors'], TRUE ) ); $return = FinalStatus::COMPLETE; } if ( $isError ) { $return = FinalStatus::FAILED; } return $return; } /** * Gets all the currency codes appropriate for this gateway * @return array of currency codes */ static function getCurrencies() { // If you update this list, also update the list in the exchange_rates drupal module. $currencies = array( 'AED', // UAE dirham 'ARS', // Argentinian peso 'AUD', // Australian dollar 'BBD', // Barbadian dollar 'BDT', // Bagladesh taka 'BGN', // Bulgarian lev 'BHD', // Bahraini dinar 'BMD', // Bermudian dollar 'BND', // Brunei dollar 'BOB', // Bolivia boliviano 'BRL', // Brazilian real // - Removed temporarily for WellsFargo (28/06/13) 'BSD', // Bahamian dollar 'BZD', // Belize dollar 'CAD', // Canadian dollar 'CHF', // Swiss franc 'CLP', // Chilean deso 'CNY', // Chinese yuan renminbi 'COP', // Colombia columb 'CRC', // Costa Rican colon 'CZK', // Czech koruna 'DKK', // Danish krone 'DOP', // Dominican peso 'DZD', // Algerian dinar 'EEK', // Estonian kroon 'EGP', // Egyptian pound 'EUR', // Euro 'GBP', // British pound 'GTQ', // Guatemala quetzal 'HKD', // Hong Kong dollar 'HNL', // Honduras lempira 'HRK', // Croatian kuna 'HUF', // Hungarian forint 'IDR', // Indonesian rupiah 'ILS', // Israeli shekel 'INR', // Indian rupee 'JMD', // Jamaican dollar 'JOD', // Jordanian dinar 'JPY', // Japanese yen 'KES', // Kenyan shilling 'KRW', // South Korean won // - Removed temporarily for WellsFargo (28/06/13) 'KYD', // Cayman Islands dollar 'KZT', // Kazakhstani tenge 'LBP', // Lebanese pound 'LKR', // Sri Lankan rupee 'LTL', // Lithuanian litas 'LVL', // Latvian lats 'MAD', // Moroccan dirham 'MKD', // Macedonia denar 'MUR', // Mauritius rupee 'MVR', // Maldives rufiyaa 'MXN', // Mexican peso 'MYR', // Malaysian ringgit 'NIO', // Nicaragua Cordoba 'NOK', // Norwegian krone 'NZD', // New Zealand dollar 'OMR', // Omani rial 'PAB', // Panamanian balboa 'PEN', // Peru nuevo sol 'PHP', // Philippine peso 'PKR', // Pakistani rupee 'PLN', // Polish złoty // - Removed temporarily for WellsFargo (23/05/13) 'PYG', // Paraguayan guaraní 'QAR', // Qatari rial 'RON', // Romanian leu 'RUB', // Russian ruble 'SAR', // Saudi riyal 'SEK', // Swedish krona 'SGD', // Singapore dollar 'SVC', // Salvadoran colón 'THB', // Thai baht 'TJS', // Tajikistani Somoni 'TND', // Tunisan dinar 'TRY', // Turkish lira 'TTD', // Trinidad and Tobago dollar 'TWD', // New Taiwan dollar 'UAH', // Ukrainian hryvnia 'UYU', // Uruguayan peso 'USD', // U.S. dollar // - Removed temporarily for WellsFargo (28/06/13) 'UZS', // Uzbekistani som // - removed temporarily (Worldpay) 'VND', // Vietnamese dong 'VEF', // Venezuelan bolívar 'XAF', // Central African CFA franc 'XCD', // East Caribbean dollar // - Removed temporarily for WellsFargo (28/06/13) 'XOF', // West African CFA franc 'ZAR', // South African rand ); return $currencies; } /** * Process the response and set transaction_response properties * * @param DomDocument $response Cleaned-up XML from the GlobalCollect API * * @throws ResponseProcessingException with code and potentially retry vars. */ public function processResponse( $response ) { $this->transaction_response->setCommunicationStatus( $this->parseResponseCommunicationStatus( $response ) ); $errors = $this->parseResponseErrors( $response ); $this->transaction_response->setErrors( $errors ); $data = $this->parseResponseData( $response ); $this->transaction_response->setData( $data ); //set the transaction result message $responseStatus = isset( $data['STATUSID'] ) ? $data['STATUSID'] : ''; $this->transaction_response->setTxnMessage( "Response Status: " . $responseStatus ); //TODO: Translate for GC. $this->transaction_response->setGatewayTransactionId( $this->getData_Unstaged_Escaped( 'order_id' ) ); $retErrCode = null; $retErrMsg = ''; $retryVars = array(); // We are also curious to know if there were any recoverable errors foreach ( $errors as $errCode => $errObj ) { $errMsg = $errObj['message']; switch ( $errCode ) { case 300620: // Oh no! We've already used this order # somewhere else! Restart! $this->logger->error( 'Order ID collission! Starting again.' ); $retryVars[] = 'order_id'; $retErrCode = $errCode; $retErrMsg = $errMsg; break; case 430260: //wow: If we were a point of sale, we'd be calling security. case 430357: //lost or stolen card // These two get all the cancel treatment below, plus some extra // IP velocity spanking. if ( $this->getGlobal( 'EnableIPVelocityFilter' ) ) { Gateway_Extras_CustomFilters_IP_Velocity::penalize( $this ); } case 430306: //Expired card. case 430330: //invalid card number case 430354: //issuer unknown // All five these should stop us from retrying at all // Null out the retry vars and throw error immediately $retryVars = null; $this->logger->info( "Got error code $errCode, not retrying to avoid MasterCard fines." ); // TODO: move forceCancel - maybe to the exception? $this->transaction_response->setForceCancel( true ); $this->transaction_response->setErrors( array( 'internal-0003' => array( 'message' => $this->getErrorMapByCodeAndTranslate( 'internal-0003' ), ) ) ); throw new ResponseProcessingException( "Got error code $errCode, not retrying to avoid MasterCard fines.", $errCode ); case 430285: //most common declined cc code. case 430396: //not authorized to cardholder, whatever that means. case 430409: //Declined, because "referred". wth does that even. case 430415: //Declined for "security violation" case 430424: //Declined, because "SYSTEM_MALFUNCTION". I have no words. case 430692: //cvv2 declined break; //don't need to hear about these at all. case 20001000 : //REQUEST {0} NULL VALUE NOT ALLOWED FOR {1} : Validation pain. Need more. //look in the message for more clues. //Yes: That's an 8-digit error code that buckets a silly number of validation issues, some of which are legitimately ours. //The only way to tell is to search the English message. //@TODO: Refactor all 3rd party error handling for GC. This whole switch should definitely be in parseResponseErrors; It is very silly that this is here at all. $not_errors = array( //add more of these stupid things here, if log noise makes you want to '/NULL VALUE NOT ALLOWED FOR EXPIRYDATE/', '/DID NOT PASS THE LUHNCHECK/', ); foreach ( $not_errors as $regex ){ if ( preg_match( $regex, $errObj['debugInfo'] ) ){ //not a system error, but definitely the end of the payment attempt. Log it to info and leave. $this->logger->info( __FUNCTION__ . ": {$errObj['debugInfo']}" ); throw new ResponseProcessingException( $errMsg, $errCode ); } } case 21000050 : //REQUEST {0} VALUE {2} OF FIELD {1} IS NOT A NUMBER WITH MINLENGTH {3}, MAXLENGTH {4} AND PRECISION {5} : More validation pain. //say something painful here. $errMsg = 'Blocking validation problems with this payment. Investigation required! ' . $this->getLogDebugJSON(); case 400120: /* INSERTATTEMPT PAYMENT FOR ORDER ALREADY FINAL FOR COMBINATION. * They already gave us money or failed... * but have probably been forbidden from resultswitcher due to session weirdness. * What should we actually do with these people, other than the default error log? * Special error page saying that they may already have donated? * @TODO: This absolutely happens IRL. Attempt to handle gracefully once we figure out what that means. */ default: $this->logger->error( __FUNCTION__ . " Error $errCode : $errMsg" ); break; } } if ( $retErrCode ) { throw new ResponseProcessingException( $retErrMsg, $retErrCode, $retryVars ); } } /** * The default section of the switch will be hit on first time forms. This * should be okay, because we are only concerned with staged_vars that have * been posted. * * Credit cards staged_vars are set to ensure form failures on validation in * the default case. This should prevent accidental form submission with * unknown transaction types. */ public function defineStagedVars() { //OUR field names. $this->staged_vars = array( 'amount', //'card_num', 'returnto', 'payment_method', 'payment_submethod', 'payment_product', 'issuer_id', 'order_id', //This may or may not oughta-be-here... 'contribution_tracking_id', 'language', 'recurring', 'country', //Street address and zip need to be staged, to provide dummy data in //the event that they are sent blank, which will short-circuit all //AVS checking for accounts that have AVS data tied to them. 'street', 'zip', 'fiscal_number', 'branch_code', //Direct Debit 'account_number', //Direct Debit 'bank_code', //Direct Debit ); } protected function stage_language() { $language = strtolower( $this->getData_Unstaged_Escaped( 'language' ) ); if ( !in_array( $language, $this->getAvailableLanguages() ) ) { $fallbacks = Language::getFallbacksFor( $language ); foreach ( $fallbacks as $fallback ) { if ( in_array( $fallback, $this->getAvailableLanguages() ) ) { $language = $fallback; break; } } } if ( !in_array( $language, $this->getAvailableLanguages() ) ){ $language = 'en'; } if ( $language === 'zh' ) { //Handles GC's mutant Chinese code. $language = 'sc'; } $this->staged_data['language'] = $language; } /** * OUR language codes which are available to use in GlobalCollect. * @return string */ function getAvailableLanguages(){ $languages = array( 'ar', //Arabic 'cs', //Czech 'da', //Danish 'nl', //Dutch 'en', //English 'fa', //Farsi 'fi', //Finish 'fr', //French 'de', //German 'he', //Hebrew 'hi', //Hindi 'hu', //Hungarian 'it', //Italian 'ja', //Japanese 'ko', //Korean 'no', //Norwegian 'pl', //Polish 'pt', //Portuguese 'ro', //Romanian 'ru', //Russian 'sl', //Slovene 'es', //Spanish 'sw', //Swahili 'sv', //Swedish 'th', //Thai 'tr', //Turkish 'ur', //Urdu 'vi', //Vietnamese 'zh', //the REAL chinese code. ); return $languages; } /** * Stage: card_num */ protected function stage_card_num() { if ( array_key_exists( 'card_num', $this->unstaged_data ) ) { $this->staged_data['card_num'] = str_replace( ' ', '', $this->unstaged_data['card_num'] ); } } /** * Stage: payment_product * Stages the payment product ID for GC. * Not what I had in mind to begin with, but this *completely* blew up. */ public function stage_payment_product() { //cc used to look in card_type, but that's been an alias for payment_submethod for a while. DonationData takes care of it. $payment_method = array_key_exists( 'payment_method', $this->staged_data ) ? $this->staged_data['payment_method'] : false; $payment_submethod = array_key_exists( 'payment_submethod', $this->staged_data ) ? $this->staged_data['payment_submethod'] : false; if ( $payment_method === 'cc' ) { //basically do what used to be stage_card_type. $types = array ( 'visa' => '1', 'amex' => '2', 'mc' => '3', 'maestro' => '117', 'solo' => '118', 'laser' => '124', 'jcb' => '125', 'discover' => '128', 'cb' => '130', ); if ( (!is_null( $payment_submethod ) ) && array_key_exists( $payment_submethod, $types ) ) { $this->staged_data['payment_product'] = $types[$payment_submethod]; } else { if ( !empty( $payment_submethod ) ) { $this->logger->error( "Could not find a cc payment product for '$payment_submethod'" ); } } // This array contains all the card types that can use AUTHENTICATIONINDICATOR $authenticationIndicatorTypes = array ( '1', // visa '3', // mc ); $enable3ds = false; $currency = $this->getData_Unstaged_Escaped( 'currency_code' ); $country = strtoupper( $this->getData_Unstaged_Escaped( 'country' ) ); if ( isset( $this->staged_data['payment_product'] ) && in_array( $this->staged_data['payment_product'], $authenticationIndicatorTypes ) ) { $ThreeDSecureRules = $this->getGlobal( '3DSRules' ); //ha if ( array_key_exists( $currency, $ThreeDSecureRules ) ) { if ( !is_array( $ThreeDSecureRules[$currency] ) ) { if ( $ThreeDSecureRules[$currency] === $country ) { $enable3ds = true; } } else { if ( empty( $ThreeDSecureRules[$currency] ) || in_array( $country, $ThreeDSecureRules[$currency] ) ) { $enable3ds = true; } } } } // FIXME: that's one hell of a staging function. Move this to a do_transaction helper. if ( $enable3ds ) { $this->logger->info( "3dSecure enabled for $currency in $country" ); $this->transactions['INSERT_ORDERWITHPAYMENT']['values']['AUTHENTICATIONINDICATOR'] = '1'; } } else { if ( !empty( $payment_submethod ) ) { //everything that isn't cc. if ( array_key_exists( $payment_submethod, $this->payment_submethods ) && isset( $this->payment_submethods[$payment_submethod]['paymentproductid'] ) ) { $this->staged_data['payment_product'] = $this->payment_submethods[$payment_submethod]['paymentproductid']; } else { $this->logger->error( "Could not find a payment product for '$payment_submethod' in payment_submethods array" ); } } else { $this->logger->debug( "payment_submethod found to be empty. Probably okay though." ); } } } /** * Stage branch_code for Direct Debit. * Check the data constraints, and zero-pad out to that number where possible. * Exceptions for the defaults are set in stage_country so we can see them all in the same place */ protected function stage_branch_code() { $this->stageAndZeroPad( 'branch_code' ); } /** * Stage bank_code for Direct Debit. * Check the data constraints, and zero-pad out to that number where possible. * Exceptions for the defaults are set in stage_country so we can see them all in the same place */ protected function stage_bank_code() { $this->stageAndZeroPad( 'bank_code' ); } /** * Stage account_number for Direct Debit. * Check the data constraints, and zero-pad out to that number where possible. * Exceptions for the defaults are set in stage_country so we can see them all in the same place */ protected function stage_account_number() { $this->stageAndZeroPad( 'account_number' ); } /** * Helper to stage a zero-padded number */ protected function stageAndZeroPad( $key ) { if ( isset( $this->unstaged_data[$key] ) ) { $newval = DataValidator::getZeroPaddedValue( $this->unstaged_data[$key], $this->dataConstraints[$key]['length'] ); if ( $newval ) { $this->staged_data[$key] = $newval; } } } /** * Stage: setupStagePaymentMethodForDirectDebit * * @param string $payment_submethod */ protected function setupStagePaymentMethodForDirectDebit( $payment_submethod ) { // DATECOLLECT is required on all Direct Debit $this->addKeyToTransaction('DATECOLLECT'); $this->staged_data['date_collect'] = gmdate('Ymd'); $this->staged_data['direct_debit_text'] = 'Wikimedia Foundation'; $this->var_map['COUNTRYCODEBANK'] = 'country'; $this->dataConstraints['iban']['length'] = 21; // Direct debit has different required fields for each paymentproductid. $this->addKeysToTransactionForSubmethod( $payment_submethod ); } /** * Stage: setupStagePaymentMethodForEWallets * * @param string $payment_submethod */ protected function setupStagePaymentMethodForEWallets( $payment_submethod ) { // DESCRIPTOR is required on WebMoney, assuming it is required for all. $this->addKeyToTransaction('DESCRIPTOR'); $this->staged_data['descriptor'] = 'Wikimedia Foundation/Wikipedia'; $this->var_map['COUNTRYCODEBANK'] = 'country'; // eWallets custom keys $this->addKeysToTransactionForSubmethod( $payment_submethod ); } /** * Stage: payment_method * * @todo * - Need to implement this for credit card if necessary * - ISSUERID will need to provide a dropdown for rtbt_eps and rtbt_ideal. * - COUNTRYCODEBANK will need it's own dropdown for country. Do not map to 'country' * - DATECOLLECT is using gmdate('Ymd') * - DIRECTDEBITTEXT will need to be translated. This is what appears on the bank statement for donations for a client. This is hardcoded to: Wikimedia Foundation */ protected function stage_payment_method() { $payment_method = array_key_exists( 'payment_method', $this->unstaged_data ) ? $this->unstaged_data['payment_method']: false; $payment_submethod = array_key_exists( 'payment_submethod', $this->unstaged_data ) ? $this->unstaged_data['payment_submethod']: false; //Having to front-load the country in the payment submethod is pretty lame. //If we don't have one deliberately set... if (!$payment_submethod){ $trythis = $payment_method . '_' . strtolower( $this->getData_Unstaged_Escaped('country') ); if ( array_key_exists( $trythis, $this->payment_submethods ) ){ $payment_submethod = $trythis; $this->staged_data['payment_submethod'] = $payment_submethod; } } // These will be grouped and ordered by payment product id switch ( $payment_submethod ) { /* Bank transfer */ case 'bt': // Brazil if ( $this->unstaged_data['country'] == 'BR' ) { $this->dataConstraints['direct_debit_text']['city'] = 50; } // Korea - Manual does not specify North or South if ( $this->unstaged_data['country'] == 'KR' ) { $this->dataConstraints['direct_debit_text']['city'] = 50; } break; /* Direct Debit */ case 'dd_de': $this->dataConstraints['account_number']['length'] = 10; $this->dataConstraints['bank_code']['length'] = 8; break; case 'dd_nl': $this->dataConstraints['account_name']['length'] = 30; $this->dataConstraints['account_number']['length'] = 10; $this->dataConstraints['direct_debit_text']['length'] = 32; $this->staged_data['transaction_type'] = '01'; break; case 'dd_gb': $this->staged_data['transaction_type'] = '01'; break; case 'dd_at': $this->dataConstraints['account_name']['length'] = 30; $this->dataConstraints['bank_code']['length'] = 5; $this->dataConstraints['direct_debit_text']['length'] = 28; break; case 'dd_es': $this->dataConstraints['account_name']['length'] = 30; $this->dataConstraints['account_number']['length'] = 10; $this->dataConstraints['bank_code']['length'] = 4; $this->dataConstraints['branch_code']['length'] = 4; $this->dataConstraints['direct_debit_text']['length'] = 40; break; case 'dd_fr': $this->dataConstraints['direct_debit_text']['length'] = 18; break; case 'dd_it': $this->dataConstraints['account_name']['length'] = 30; $this->dataConstraints['account_number']['length'] = 12; $this->dataConstraints['bank_check_digit']['length'] = 1; $this->dataConstraints['bank_code']['length'] = 5; $this->dataConstraints['direct_debit_text']['length'] = 32; break; /* Cash payments */ case 'cash_boleto': $this->addKeyToTransaction('FISCALNUMBER'); break; case 'rtbt_eps': case 'rtbt_ideal': $this->addKeysToTransactionForSubmethod( $payment_submethod ); $this->addKeyToTransaction('ISSUERID'); break; /* Default Case */ default: // Nothing is done in the default case. // It's worth noting that at this point, it might not be an error. break; } switch ($payment_method) { case 'dd': $this->setupStagePaymentMethodForDirectDebit( $payment_submethod ); break; case 'ew': $this->setupStagePaymentMethodForEWallets( $payment_submethod ); break; } } /** * Stage: recurring * Adds the recurring payment pieces to the structure of * INSERT_ORDERWITHPAYMENT if the recurring field is populated. */ protected function stage_recurring(){ if ( !$this->getData_Unstaged_Escaped( 'recurring' ) ) { return; } else { $this->transactions['INSERT_ORDERWITHPAYMENT']['request']['REQUEST']['PARAMS']['ORDER'][] = 'ORDERTYPE'; $this->transactions['INSERT_ORDERWITHPAYMENT']['values']['ORDERTYPE'] = '4'; } } /** * Stage: country * This should be a catch-all for establishing weird country-based rules. * Right now, we only have the one, but there could be more here later. */ protected function stage_country() { switch ( $this->getData_Unstaged_Escaped( 'country' ) ){ case 'AR' : $this->transactions['INSERT_ORDERWITHPAYMENT']['request']['REQUEST']['PARAMS']['ORDER'][] = 'USAGETYPE'; $this->transactions['INSERT_ORDERWITHPAYMENT']['request']['REQUEST']['PARAMS']['ORDER'][] = 'PURCHASETYPE'; $this->transactions['INSERT_ORDERWITHPAYMENT']['values']['USAGETYPE'] = '0'; $this->transactions['INSERT_ORDERWITHPAYMENT']['values']['PURCHASETYPE'] = '1'; break; } } protected function stage_contribution_tracking_id() { $ctid = $this->unstaged_data['contribution_tracking_id']; //append timestamp to ctid $ctid .= '.' . (( microtime( true ) * 1000 ) % 100000); //least significant five $this->staged_data['contribution_tracking_id'] = $ctid; } protected function unstage_contribution_tracking_id() { $ctid = $this->staged_data['contribution_tracking_id']; $ctid = explode( '.', $ctid ); $ctid = $ctid[0]; $this->unstaged_data['contribution_tracking_id'] = $ctid; } protected function stage_fiscal_number() { $this->staged_data['fiscal_number'] = preg_replace( "/[\.\/\-]/", "", $this->getData_Unstaged_Escaped( 'fiscal_number' ) ); } /** * Add keys to transaction for submethod * */ protected function addKeysToTransactionForSubmethod( $payment_submethod ) { // If there are no keys to add, do not proceed. if ( !is_array( $this->payment_submethods[ $payment_submethod ]['keys'] ) ) { return; } foreach ( $this->payment_submethods[ $payment_submethod ]['keys'] as $key ) { $this->addKeyToTransaction( $key ); } } /** * Stage: returnto */ protected function stage_returnto() { // Get the default returnto $returnto = $this->getData_Unstaged_Escaped( 'returnto' ); if ( $this->getData_Unstaged_Escaped( 'payment_method' ) === 'cc' ) { // Add order ID to the returnto URL, only if it's not already there. //TODO: This needs to be more robust (like actually pulling the //qstring keys, resetting the values, and putting it all back) //but for now it'll keep us alive. if ( $this->getOrderIDMeta( 'generate' ) && !is_null( $returnto ) && !strpos( $returnto, 'order_id' ) ) { $queryArray = array( 'order_id' => $this->unstaged_data['order_id'] ); $this->staged_data['returnto'] = wfAppendQuery( $returnto, $queryArray ); } } else { // FIXME: Do we want to set this here? $this->staged_data['returnto'] = $this->getThankYouPage(); } } /** * post-process function for INSERT_ORDERWITHPAYMENT. * This gets called by executeIfFunctionExists, in do_transaction. */ protected function post_process_insert_orderwithpayment(){ //yeah, we absolutely want to do this for every one of these. if ( $this->getTransactionStatus() === true ) { $data = $this->getTransactionData(); $action = $this->findCodeAction( 'GET_ORDERSTATUS', 'STATUSID', $data['STATUSID'] ); if ( $action != FinalStatus::FAILED ){ - // TODO: Stop mirroring to STOMP. - $this->doLimboStompTransaction(); if ( $this->getData_Unstaged_Escaped( 'payment_method' ) === 'cc' ) { $this->setLimboMessage( self::GC_CC_LIMBO_QUEUE ); } else { $this->setLimboMessage(); } } } } protected function pre_process_get_orderstatus(){ static $checked = array(); $oid = $this->getData_Unstaged_Escaped('order_id'); if ( $this->getData_Unstaged_Escaped( 'payment_method' ) === 'cc' && !in_array( $oid, $checked ) ){ $this->runAntifraudHooks(); $checked[] = $oid; } } /** * getCVVResult is intended to be used by the functions filter, to * determine if we want to fail the transaction ourselves or not. */ public function getCVVResult(){ $from_processor = $this->getData_Unstaged_Escaped( 'cvv_result' ); if ( is_null( $from_processor ) ){ return null; } $cvv_map = $this->getGlobal( 'CvvMap' ); if ( !isset( $cvv_map[$from_processor] ) ) { $this->logger->warning( "Unrecognized cvv_result '$from_processor'" ); return false; } $result = $cvv_map[$from_processor]; return $result; } /** * getAVSResult is intended to be used by the functions filter, to * determine if we want to fail the transaction ourselves or not. */ public function getAVSResult(){ if ( is_null( $this->getData_Unstaged_Escaped( 'avs_result' ) ) ){ return null; } //Best guess here: //Scale of 0 - 100, of Problem we think this result is likely to cause. $avs_map = $this->getGlobal( 'AvsMap' ); $result = $avs_map[$this->getData_Unstaged_Escaped( 'avs_result' )]; return $result; } /** * Used by ewallets, rtbt, and cash (boletos) to retrieve the URL we should * be posting the form data to. * * @return string|false Returns FORMACTION if one exists in the transaction response, else false. */ public function getTransactionDataFormAction() { $data = $this->getTransactionData(); if ( is_array( $data ) && array_key_exists( 'FORMACTION', $data ) ) { return $data['FORMACTION']; } else { return false; } } } diff --git a/globalcollect_gateway/scripts/orphan_adapter.php b/globalcollect_gateway/scripts/orphan_adapter.php index 545e23a1..ef11f2a4 100644 --- a/globalcollect_gateway/scripts/orphan_adapter.php +++ b/globalcollect_gateway/scripts/orphan_adapter.php @@ -1,208 +1,175 @@ batch = true; //always batch if we're using this object. parent::__construct( $options = array ( ) ); } // FIXME: Get rid of this. public function unstage_data( $data = array( ), $final = true ) { $unstaged = array( ); foreach ( $data as $key => $val ) { if ( is_array( $val ) ) { $unstaged += $this->unstage_data( $val, false ); } else { if ( array_key_exists( $key, $this->var_map ) ) { //run the unstage data functions. $unstaged[$this->var_map[$key]] = $val; //this would be EXTREMELY bad to put in the regular adapter. $this->staged_data[$this->var_map[$key]] = $val; } else { //$unstaged[$key] = $val; } } } if ( $final ) { // FIXME $this->stageData( 'response' ); } foreach ( $unstaged as $key => $val ) { $unstaged[$key] = $this->staged_data[$key]; } return $unstaged; } // FIXME: This needs some serious code reuse trickery. public function loadDataAndReInit( $data, $useDB = true ) { //re-init all these arrays, because this is a batch thing. $this->session_killAllEverything(); // just to be sure $this->transaction_response = new PaymentTransactionResponse(); $this->hard_data = array( ); $this->unstaged_data = array( ); $this->staged_data = array( ); $this->hard_data['order_id'] = $data['order_id']; $this->dataObj = new DonationData( $this, $data ); $this->unstaged_data = $this->dataObj->getDataEscaped(); if ( $useDB ){ $this->hard_data = array_merge( $this->hard_data, $this->getUTMInfoFromDB() ); } else { $utm_keys = array( 'utm_source', 'utm_campaign', 'utm_medium', 'date' ); foreach($utm_keys as $key){ $this->hard_data[$key] = $data[$key]; } } $this->reAddHardData(); $this->staged_data = $this->unstaged_data; $this->defineTransactions(); $this->defineErrorMap(); $this->defineVarMap(); $this->defineAccountInfo(); $this->defineReturnValueMap(); $this->stageData(); //have to do this again here. $this->reAddHardData(); $this->revalidate(); } public function addRequestData( $dataArray ) { parent::addRequestData( $dataArray ); $this->reAddHardData(); } private function reAddHardData() { //anywhere else, and this would constitute abuse of the system. //so don't do it. $data = $this->hard_data; if ( array_key_exists( 'order_id', $data ) ) { $this->normalizeOrderID( $data['order_id'] ); } foreach ( $data as $key => $val ) { $this->unstaged_data[$key] = $val; $this->staged_data[$key] = $val; } } public function getUTMInfoFromDB() { $db = ContributionTrackingProcessor::contributionTrackingConnection(); if ( !$db ) { die( "There is something terribly wrong with your Contribution Tracking database. fixit." ); } $ctid = $this->getData_Unstaged_Escaped( 'contribution_tracking_id' ); $data = array( ); // if contrib tracking id is not already set, we need to insert the data, otherwise update if ( $ctid ) { $res = $db->select( 'contribution_tracking', array( 'utm_source', 'utm_campaign', 'utm_medium', 'ts' ), array( 'id' => $ctid ) ); foreach ( $res as $thing ) { $data['utm_source'] = $thing->utm_source; $data['utm_campaign'] = $thing->utm_campaign; $data['utm_medium'] = $thing->utm_medium; $data['ts'] = $thing->ts; $msg = ''; foreach ( $data as $key => $val ) { $msg .= "$key = $val "; } $this->logger->info( "$ctid: Found UTM Data. $msg" ); echo "$msg\n"; return $data; } } //if we got here, we can't find anything else... $this->logger->error( "$ctid: FAILED to find UTM Source value. Using default." ); return $data; } /** - * Copying this here because it's the fastest way to bring in an actual timestamp. + * Copy the timestamp rather than using the current time. + * + * FIXME: Carefully move this to the base class and decide when appropriate. */ - protected function doStompTransaction() { - if ( !$this->getGlobal( 'EnableStomp' ) ) { - return; - } - $this->debugarray[] = "Attempting Stomp Transaction!"; - $hook = ''; - - $status = $this->getFinalStatus(); - switch ( $status ) { - case FinalStatus::COMPLETE: - $hook = 'gwStomp'; - break; - case FinalStatus::PENDING: - case FinalStatus::PENDING_POKE: - $hook = 'gwPendingStomp'; - break; - } - if ( $hook === '' ) { - $this->debugarray[] = "No Stomp Hook Found for FINAL_STATUS $status"; - return; - } + protected function getStompTransaction() { + $transaction = parent::getStompTransaction(); + // Overwrite the time field, if historical date is available. if ( !is_null( $this->getData_Unstaged_Escaped( 'date' ) ) ) { - $timestamp = $this->getData_Unstaged_Escaped( 'date' ); - } else { - if ( !is_null( $this->getData_Unstaged_Escaped( 'ts' ) ) ) { - $timestamp = strtotime( $this->getData_Unstaged_Escaped( 'ts' ) ); //I hate that this works. - } else { - $timestamp = time(); - } + $transaction['date'] = $this->getData_Unstaged_Escaped( 'date' ); + } elseif ( !is_null( $this->getData_Unstaged_Escaped( 'ts' ) ) ) { + $transaction['date'] = strtotime( $this->getData_Unstaged_Escaped( 'ts' ) ); //I hate that this works. FIXME: wat. } - // send the thing. - $transaction = array( - 'response' => $this->getTransactionMessage(), - 'date' => $timestamp, - 'gateway_txn_id' => $this->getTransactionGatewayTxnID(), - //'language' => '', - ); - $transaction += $this->getData_Unstaged_Escaped(); - - try { - WmfFramework::runHooks( $hook, array( $transaction ) ); - } catch ( Exception $e ) { - $this->logger->critical( "STOMP ERROR. Could not add message. " . $e->getMessage() ); - } + return $transaction; } /** * Override live adapter with a no-op since orphan doesn't have any new info * before GET_ORDERSTATUS */ protected function pre_process_get_orderstatus() { } } diff --git a/globalcollect_gateway/scripts/orphans.php b/globalcollect_gateway/scripts/orphans.php index 4e85182b..e220b0be 100644 --- a/globalcollect_gateway/scripts/orphans.php +++ b/globalcollect_gateway/scripts/orphans.php @@ -1,405 +1,262 @@ getOrphanGlobal( 'enable' ) ){ echo "\nOrphan cron disabled. Have a nice day."; return; } $this->target_execute_time = $this->getOrphanGlobal( 'target_execute_time' ); $this->max_per_execute = $this->getOrphanGlobal( 'max_per_execute' ); // FIXME: Is this just to trigger batch mode? $data = array( 'wheeee' => 'yes' ); $this->adapter = new GlobalCollectOrphanAdapter(array('external_data' => $data)); $this->logger = DonationLoggerFactory::getLogger( $this->adapter ); //Now, actually do the processing. - $this->orphan_stomp(); + $this->process_orphans(); } - protected function orphan_stomp(){ + protected function process_orphans(){ echo "Slaying orphans...\n"; $this->removed_message_count = 0; $this->start_time = time(); //I want to be clear on the problem I hope to prevent with this. Say, //for instance, we pull a legit orphan, and for whatever reason, can't //completely rectify it. Then, we go back and pull more... and that //same one is in the list again. We should stop after one try per //message per execute. We should also be smart enough to not process //things we believe we just deleted. $this->handled_ids = array(); - //first, we need to... clean up the limbo queue. - - // TODO: Remove STOMP code. - //building in some redundancy here. - $collider_keepGoing = true; - $am_called_count = 0; - while ( $collider_keepGoing ){ - $antimessageCount = $this->handleStompAntiMessages(); - $am_called_count += 1; - if ( $antimessageCount < 10 ){ - $collider_keepGoing = false; - } else { - sleep(2); //two seconds. - } - } - $this->logger->info( 'Removed ' . $this->removed_message_count . ' messages and antimessages.' ); - do { //Pull a batch of CC orphans, keeping in mind that Things May Have Happened in the small slice of time since we handled the antimessages. $orphans = $this->getOrphans(); - echo count( $orphans ) . " orphans left this batch\n"; + echo count( $orphans ) . " orphans left in this batch\n"; //..do stuff. foreach ( $orphans as $correlation_id => $orphan ) { //process if ( $this->keepGoing() ){ // TODO: Maybe we can simplify by checking that modified time < job start time. $this->logger->info( "Attempting to rectify orphan $correlation_id" ); if ( $this->rectifyOrphan( $orphan ) ) { $this->handled_ids[$correlation_id] = 'rectified'; } else { $this->handled_ids[$correlation_id] = 'error'; } } } } while ( count( $orphans ) && $this->keepGoing() ); //TODO: Make stats squirt out all over the place. - $am = 0; $rec = 0; $err = 0; $fe = 0; foreach( $this->handled_ids as $id=>$whathappened ){ switch ( $whathappened ){ - case 'antimessage' : - $am += 1; - break; case 'rectified' : $rec += 1; break; case 'error' : $err += 1; break; case 'false_orphan' : $fe += 1; break; } } $final = "\nDone! Final results: \n"; - $final .= " $am destroyed via antimessage (called $am_called_count times) \n"; $final .= " $rec rectified orphans \n"; $final .= " $err errored out \n"; $final .= " $fe false orphans caught \n"; if ( isset( $this->adapter->orphanstats ) ){ foreach ( $this->adapter->orphanstats as $status => $count ) { $final .= "\n Status $status = $count"; } } $final .= "\n Approximately " . $this->getProcessElapsed() . " seconds to execute.\n"; $this->logger->info( $final ); echo $final; } protected function keepGoing(){ $elapsed = $this->getProcessElapsed(); if ( $elapsed < $this->target_execute_time ) { return true; } else { return false; } } /** * This will both return the elapsed process time, and echo something for * the cronspammer. * @return int elapsed time since start in seconds */ protected function getProcessElapsed(){ $elapsed = time() - $this->start_time; echo "Elapsed Time: $elapsed\n"; return $elapsed; } protected function deleteMessage( $correlation_id, $queue ) { $this->handled_ids[$correlation_id] = 'antimessage'; DonationQueue::instance()->delete( $correlation_id, $queue ); } - function addStompCorrelationIDToAckBucket( $correlation_id, $ackNow = false ){ - static $bucket = array(); - $count = 50; //sure. Why not? - if ( $correlation_id ) { - $bucket[$correlation_id] = "'$correlation_id'"; //avoiding duplicates. - $this->handled_ids[$correlation_id] = 'antimessage'; - } - if ( count( $bucket ) && ( count( $bucket ) >= $count || $ackNow ) ){ - //ack now. - echo 'Acking ' . count( $bucket ) . " bucket messages.\n"; - $selector = 'JMSCorrelationID IN (' . implode( ", ", $bucket ) . ')'; - $ackMe = stompFetchMessages( 'cc-limbo', $selector, $count * 100 ); //This is outrageously high, but I just want to be reasonably sure we get all the matches. - $retrieved_count = count( $ackMe ); - if ( $retrieved_count ){ - stompAckMessages( $ackMe ); - $this->removed_message_count += $retrieved_count; - echo "Done acking $retrieved_count messages. \n"; - } else { - echo "Oh noes! No messages retrieved for $selector...\n"; - } - $bucket = array(); - } - - } - - // TODO: remove STOMP function - function handleStompAntiMessages(){ - $selector = "antimessage = 'true' AND gateway='globalcollect'"; - $antimessages = stompFetchMessages( 'cc-limbo', $selector, 1000 ); - $count = 0; - while ( count( $antimessages ) > 10 && $this->keepGoing() ){ //if there's an antimessage, we can ack 'em all right now. - echo "Colliding " . count( $antimessages ) . " antimessages\n"; - $count += count( $antimessages ); - foreach ( $antimessages as $message ){ - //add the correlation ID to the ack bucket. - if (array_key_exists('correlation-id', $message->headers)) { - $this->addStompCorrelationIDToAckBucket( $message->headers['correlation-id'] ); - - // mirror to new thing - $this->deleteMessage( $message->headers['correlation-id'], GlobalCollectAdapter::GC_CC_LIMBO_QUEUE ); - } else { - echo 'The STOMP message ' . $message->headers['message-id'] . " has no correlation ID!\n"; - } - } - $this->addStompCorrelationIDToAckBucket( false, true ); //ack all outstanding. - $antimessages = stompFetchMessages( 'cc-limbo', $selector, 1000 ); - } - $this->addStompCorrelationIDToAckBucket( false, true ); //this just acks everything that's waiting for it. - $this->logger->info( "Found $count antimessages." ); - return $count; - } - protected function getOrphans() { // TODO: Make this configurable. - $time_buffer = 60*20; //20 minutes? Sure. Why not? + // 20 minutes: this is exactly equal to something on Globalcollect's side. + $time_buffer = 60*20; $orphans = array(); $false_orphans = array(); $queue_pool = new CyclicalArray( $this->getOrphanGlobal( 'gc_cc_limbo_queue_pool' ) ); if ( $queue_pool->isEmpty() ) { // FIXME: cheesy inline default $queue_pool = new CyclicalArray( GlobalCollectAdapter::GC_CC_LIMBO_QUEUE ); } while ( !$queue_pool->isEmpty() ) { $current_queue = $queue_pool->current(); try { $message = DonationQueue::instance()->peek( $current_queue ); if ( !$message ) { $this->logger->info( "Emptied queue [{$current_queue}], removing from pool." ); $queue_pool->dropCurrent(); continue; } $correlation_id = 'globalcollect-' . $message['gateway_txn_id']; if ( array_key_exists( $correlation_id, $this->handled_ids ) ) { // We already did this one, keep going. It's fine to draw // again from the same queue. DonationQueue::instance()->delete( $correlation_id, $current_queue ); continue; } // Check the timestamp to see if it's old enough, and stop when // we're below the threshold. Messages are guaranteed to pop in // chronological order. $elapsed = $this->start_time - $message['date']; if ( $elapsed < $time_buffer ) { $this->logger->info( "Exhausted new messages in [{$current_queue}], removing from pool..." ); $queue_pool->dropCurrent(); continue; } // We got ourselves an orphan! $order_id = explode('-', $correlation_id); $order_id = $order_id[1]; $message['order_id'] = $order_id; $message = unCreateQueueMessage($message); $orphans[$correlation_id] = $message; $this->logger->info( "Found an orphan! $correlation_id" ); $this->deleteMessage( $correlation_id, $current_queue ); // Round-robin the pool before we complete the loop. $queue_pool->rotate(); - - // TODO: stop stomping - $this->addStompCorrelationIDToAckBucket( $correlation_id ); } catch ( ConnectionException $ex ) { // Drop this server, for the duration of this batch. $this->logger->error( "Queue server for [$current_queue] is down! Ignoring for this run..." ); $queue_pool->dropCurrent(); } } - // TODO: stop stomping - $this->addStompCorrelationIDToAckBucket( false, true ); - - return $orphans; - } - - /** - * TODO: Remove this along with other STOMP code. Use getOrphans() instead. - * Returns an array of at most $batch_size decoded orphans that we don't - * think we've rectified yet. - * - * @return array keys are the correlation_id, and the values are the - * decoded stomp message body. - */ - protected function getStompOrphans(){ - $time_buffer = 60*20; //20 minutes? Sure. Why not? - $selector = "payment_method = 'cc' AND gateway='globalcollect'"; - echo "Fetching 300 Orphans\n"; - $messages = stompFetchMessages( 'cc-limbo', $selector, 300 ); - - $batch_size = 300; - echo "Fetching {$batch_size} Orphans\n"; - - $orphans = array(); - $false_orphans = array(); - foreach ( $messages as $message ){ - //this next block will do quite a lot of antimessage collision - //when the queue is not being railed. - if ( array_key_exists('antimessage', $message->headers ) ){ - $correlation_id = $message->headers['correlation-id']; - $false_orphans[] = $correlation_id; - echo "False Orphan! $correlation_id \n"; - } else { - //legit message - if ( !array_key_exists( $message->headers['correlation-id'], $this->handled_ids ) ) { - //check the timestamp to see if it's old enough. - $decoded = json_decode($message->body, true); - if ( array_key_exists( 'date', $decoded ) ){ - $elapsed = $this->start_time - $decoded['date']; - if ( $elapsed > $time_buffer ){ - //we got ourselves an orphan! - $correlation_id = $message->headers['correlation-id']; - $order_id = explode('-', $correlation_id); - $order_id = $order_id[1]; - $decoded['order_id'] = $order_id; - $decoded = unCreateQueueMessage($decoded); - $decoded['card_num'] = ''; - $orphans[$correlation_id] = $decoded; - echo "Found an orphan! $correlation_id \n"; - } - } - } - } - } - - // TODO: Remove STOMP block. - foreach ( $orphans as $cid => $data ){ - if ( in_array( $cid, $false_orphans ) ){ - unset( $orphans[$cid] ); - $this->addStompCorrelationIDToAckBucket( $cid ); - $this->handled_ids[ $cid ] = 'false_orphan'; - - // mirror to new thing - $this->deleteMessage( $cid, GlobalCollectAdapter::GC_CC_LIMBO_QUEUE ); - } - } - return $orphans; } /** * Uses the Orphan Adapter to rectify (complete the charge for) a single orphan. Returns a boolean letting the caller know if * the orphan has been fully rectified or not. * @param array $data Some set of orphan data. * @param boolean $query_contribution_tracking A flag specifying if we should query the contribution_tracking table or not. * @return boolean True if the orphan has been rectified, false if not. */ protected function rectifyOrphan( $data, $query_contribution_tracking = true ){ echo 'Rectifying Orphan ' . $data['order_id'] . "\n"; $rectified = false; $this->adapter->loadDataAndReInit( $data, $query_contribution_tracking ); $results = $this->adapter->do_transaction( 'Confirm_CreditCard' ); $message = $results->getMessage(); if ( $results->getCommunicationStatus() ){ $this->logger->info( $data['contribution_tracking_id'] . ": FINAL: " . $this->adapter->getValidationAction() ); $rectified = true; } else { $this->logger->info( $data['contribution_tracking_id'] . ": ERROR: " . $message ); if ( strpos( $message, "GET_ORDERSTATUS reports that the payment is already complete." ) === 0 ){ $rectified = true; } //handles the transactions we've cancelled ourselves... though if they got this far, that's a problem too. $errors = $results->getErrors(); if ( !empty( $errors ) && array_key_exists( '1000001', $errors ) ){ $rectified = true; } //apparently this is well-formed GlobalCollect for "iono". Get rid of it. if ( strpos( $message, "No processors are available." ) === 0 ){ $rectified = true; } } echo $message . "\n"; return $rectified; } /** * Gets the global setting for the key passed in. * @param type $key * * FIXME: Reuse GatewayAdapter::getGlobal. */ protected function getOrphanGlobal( $key ){ global $wgDonationInterfaceOrphanCron; if ( array_key_exists( $key, $wgDonationInterfaceOrphanCron ) ){ return $wgDonationInterfaceOrphanCron[$key]; } else { return NULL; } } } $maintClass = 'GlobalCollectOrphanRectifier'; require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/tests/Adapter/GlobalCollect/GlobalCollectTest.php b/tests/Adapter/GlobalCollect/GlobalCollectTest.php index d6f087cb..f43f2292 100644 --- a/tests/Adapter/GlobalCollect/GlobalCollectTest.php +++ b/tests/Adapter/GlobalCollect/GlobalCollectTest.php @@ -1,570 +1,567 @@ setMwGlobals( array( 'wgGlobalCollectGatewayEnabled' => true, 'wgDonationInterfaceAllowedHtmlForms' => array( 'cc-vmad' => array( 'file' => $wgGlobalCollectGatewayHtmlFormDir . '/cc/cc-vmad.html', 'gateway' => 'globalcollect', 'payment_methods' => array('cc' => array( 'visa', 'mc', 'amex', 'discover' )), 'countries' => array( '+' => array( 'US', ), ), ), ), ) ); } /** * @param $name string The name of the test case * @param $data array Any parameters read from a dataProvider * @param $dataName string|int The name or index of the data set */ function __construct( $name = null, array $data = array(), $dataName = '' ) { parent::__construct( $name, $data, $dataName ); $this->testAdapterClass = 'TestingGlobalCollectAdapter'; } function tearDown() { TestingGlobalCollectAdapter::clearGlobalsCache(); parent::tearDown(); } /** * testnormalizeOrderID * Non-exhaustive integration tests to verify that order_id * normalization works as expected with different settings and * conditions in theGlobalCollect adapter * @covers GatewayAdapter::normalizeOrderID */ public function testNormalizeOrderID() { $init = self::$initial_vars; unset( $init['order_id'] ); //no order_id from anywhere, explicit no generate $gateway = $this->getFreshGatewayObject( $init, array ( 'order_id_meta' => array ( 'generate' => FALSE ) ) ); $this->assertFalse( $gateway->getOrderIDMeta( 'generate' ), 'The order_id meta generate setting override is not working properly. Deferred order_id generation may be broken.' ); $this->assertNull( $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Failed asserting that an absent order id is left as null, when not generating our own' ); //no order_id from anywhere, explicit generate $gateway = $this->getFreshGatewayObject( $init, array ( 'order_id_meta' => array ( 'generate' => TRUE ) ) ); $this->assertTrue( $gateway->getOrderIDMeta( 'generate' ), 'The order_id meta generate setting override is not working properly. Self order_id generation may be broken.' ); $this->assertInternalType( 'numeric', $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Generated order_id is not numeric, which it should be for GlobalCollect' ); $_GET['order_id'] = '55555'; $_SESSION['Donor']['order_id'] = '44444'; //conflicting order_id in $GET and $SESSION, default GC generation $gateway = $this->getFreshGatewayObject( $init ); $this->assertEquals( '55555', $gateway->getData_Unstaged_Escaped( 'order_id' ), 'GlobalCollect gateway is preferring session data over the $_GET. Session should be secondary.' ); //conflicting order_id in $GET and $SESSION, garbage data in $_GET, default GC generation $_GET['order_id'] = 'nonsense!'; $gateway = $this->getFreshGatewayObject( $init ); $this->assertEquals( '44444', $gateway->getData_Unstaged_Escaped( 'order_id' ), 'GlobalCollect gateway is not ignoring nonsensical order_id candidates' ); unset( $_GET['order_id'] ); //order_id in $SESSION, default GC generation $gateway = $this->getFreshGatewayObject( $init ); $this->assertEquals( '44444', $gateway->getData_Unstaged_Escaped( 'order_id' ), 'GlobalCollect gateway is not recognizing the session order_id' ); $_POST['order_id'] = '33333'; //conflicting order_id in $_POST and $SESSION, default GC generation $gateway = $this->getFreshGatewayObject( $init ); $this->assertEquals( '33333', $gateway->getData_Unstaged_Escaped( 'order_id' ), 'GlobalCollect gateway is preferring session data over the $_POST. Session should be secondary.' ); $init['order_id'] = '22222'; //conflicting order_id in init data, $_POST and $SESSION, explicit GC generation, batch mode $gateway = $this->getFreshGatewayObject( $init, array ( 'order_id_meta' => array ( 'generate' => TRUE ), 'batch_mode' => TRUE, ) ); $this->assertEquals( $init['order_id'], $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Failed asserting that an extrenally provided order id is being honored in batch mode' ); //make sure that decimal numbers are rejected by GC. Should be a toss and regen $init['order_id'] = '2143.0'; unset( $_POST['order_id'] ); unset( $_SESSION['Donor']['order_id'] ); //conflicting order_id in init data, $_POST and $SESSION, explicit GC generation, batch mode $gateway = $this->getFreshGatewayObject( $init, array ( 'order_id_meta' => array ( 'generate' => TRUE, 'disallow_decimals' => TRUE ), 'batch_mode' => TRUE, ) ); $this->assertNotEquals( $init['order_id'], $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Failed assering that a decimal order_id was regenerated, when disallow_decimals is true' ); } /** * Non-exhaustive integration tests to verify that order_id, when in * self-generation mode, won't regenerate until it is told to. * @covers GatewayAdapter::normalizeOrderID * @covers GatewayAdapter::regenerateOrderID */ function testStickyGeneratedOrderID() { $init = self::$initial_vars; unset( $init['order_id'] ); //no order_id from anywhere, explicit generate $gateway = $this->getFreshGatewayObject( $init, array ( 'order_id_meta' => array ( 'generate' => TRUE ) ) ); $this->assertNotNull( $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Generated order_id is null. The rest of this test is broken.' ); $original_order_id = $gateway->getData_Unstaged_Escaped( 'order_id' ); $gateway->normalizeOrderID(); $this->assertEquals( $original_order_id, $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Re-normalized order_id has changed without explicit regeneration.' ); //this might look a bit strange, but we need to be able to generate valid order_ids without making them stick to anything. $gateway->generateOrderID(); $this->assertEquals( $original_order_id, $gateway->getData_Unstaged_Escaped( 'order_id' ), 'function generateOrderID auto-changed the selected order ID. Not cool.' ); $gateway->regenerateOrderID(); $this->assertNotEquals( $original_order_id, $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Re-normalized order_id has not changed, after explicit regeneration.' ); } /** * Integration test to verify that order_id can be retrieved from * performing an INSERT_ORDERWITHPAYMENT. */ function testOrderIDRetrieval() { $init = $this->getDonorTestData(); unset( $init['order_id'] ); $init['payment_method'] = 'cc'; $init['payment_submethod'] = 'visa'; //no order_id from anywhere, explicit generate $gateway = $this->getFreshGatewayObject( $init, array ( 'order_id_meta' => array ( 'generate' => FALSE ) ) ); $this->assertNull( $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Ungenerated order_id is not null. The rest of this test is broken.' ); $gateway->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); $this->assertNotNull( $gateway->getData_Unstaged_Escaped( 'order_id' ), 'No order_id was retrieved from INSERT_ORDERWITHPAYMENT' ); } /** * Just run the GET_ORDERSTATUS transaction and make sure we load the data */ function testGetOrderStatus() { $init = $this->getDonorTestData(); $init['payment_method'] = 'cc'; $init['payment_submethod'] = 'visa'; $init['email'] = 'innocent@safedomain.org'; $gateway = $this->getFreshGatewayObject( $init ); $gateway->do_transaction( 'GET_ORDERSTATUS' ); $data = $gateway->getTransactionData(); $this->assertEquals( 'N', $data['CVVRESULT'], 'CVV Result not loaded from XML response' ); } /** * Don't fraud-fail someone for bad CVV if GET_ORDERSTATUS * comes back with STATUSID 25 and no CVVRESULT * @group CvvResult */ function testConfirmCreditCardStatus25() { $init = $this->getDonorTestData(); $init['payment_method'] = 'cc'; $init['payment_submethod'] = 'visa'; $init['email'] = 'innocent@safedomain.org'; $this->setMwGlobals( 'wgRequest', new FauxRequest( array( 'CVVRESULT' => 'M' ), false ) ); $gateway = $this->getFreshGatewayObject( $init ); $gateway->setDummyGatewayResponseCode( '25' ); $gateway->do_transaction( 'Confirm_CreditCard' ); $action = $gateway->getValidationAction(); $this->assertEquals( 'process', $action, 'Gateway should not fraud fail on STATUSID 25' ); } /** * If CVVRESULT is unrecognized, fraud-fail and warn * @group CvvResult */ function testConfirmCreditCardBadCVVResult() { $init = $this->getDonorTestData(); $init['payment_method'] = 'cc'; $init['payment_submethod'] = 'visa'; $init['email'] = 'innocent@safedomain.org'; $this->setMwGlobals( 'wgRequest', new FauxRequest( array( 'CVVRESULT' => ' ' ), false ) ); $gateway = $this->getFreshGatewayObject( $init ); $gateway->setDummyGatewayResponseCode( '800' ); $gateway->do_transaction( 'Confirm_CreditCard' ); $result = $gateway->getCvvResult(); $this->assertEquals( false, $result, 'Gateway should fraud fail if CVVRESULT is not mapped' ); $matches = $this->getLogMatches( LogLevel::WARNING, "/Unrecognized cvv_result ' '$/" ); $this->assertNotEmpty( $matches, 'Did not log expected warning on unmapped CVVRESULT' ); } /** * We should skip the API call if we're already suspicious */ function testGetOrderStatusSkipsIfFail() { DonationInterface_FraudFiltersTest::setupFraudMaps(); $init = $this->getDonorTestData(); $init['payment_method'] = 'cc'; $init['payment_submethod'] = 'visa'; $init['email'] = 'swhiplash@wikipedia.org'; //configured as a fraudy domain $gateway = $this->getFreshGatewayObject( $init ); $gateway->do_transaction( 'GET_ORDERSTATUS' ); $data = $gateway->getTransactionData(); $this->assertEquals( null, $data['CVVRESULT'], 'preprocess should stop API call if fraud detected' ); } /** * Ensure the Confirm_CreditCard transaction prefers CVVRESULT from the XML * over any value from the querystring */ function testConfirmCreditCardPrefersXmlCvv() { $init = $this->getDonorTestData(); $init['payment_method'] = 'cc'; $init['payment_submethod'] = 'visa'; $init['email'] = 'innocent@safedomain.org'; $this->setMwGlobals( 'wgRequest', new FauxRequest( array( 'CVVRESULT' => 'M' ), false ) ); $gateway = $this->getFreshGatewayObject( $init ); $gateway->do_transaction( 'Confirm_CreditCard' ); $this->assertEquals( 'N', $gateway->getData_Unstaged_Escaped('cvv_result'), 'CVV Result not taken from XML response' ); } /** * If querystring and XML have different CVVRESULT, that's awfully fishy */ function testConfirmCreditCardFailsOnCvvResultConflict() { $init = $this->getDonorTestData(); $init['payment_method'] = 'cc'; $init['payment_submethod'] = 'visa'; $init['email'] = 'innocent@safedomain.org'; $this->setMwGlobals( 'wgRequest', new FauxRequest( array( 'CVVRESULT' => 'M' ), false ) ); $gateway = $this->getFreshGatewayObject( $init ); $result = $gateway->do_transaction( 'Confirm_CreditCard' ); // FIXME: this is not a communication failure, it's a fraud failure $this->assertFalse( $result->getCommunicationStatus(), 'Credit card should fail if querystring and XML have different CVVRESULT' ); } /** * testDefineVarMap * * This is tested with a bank transfer from Spain. * * @covers GlobalCollectAdapter::__construct * @covers GlobalCollectAdapter::defineVarMap */ public function testDefineVarMap() { $gateway = $this->getFreshGatewayObject( self::$initial_vars ); $var_map = array( 'ORDERID' => 'order_id', 'AMOUNT' => 'amount', 'CURRENCYCODE' => 'currency_code', 'LANGUAGECODE' => 'language', 'COUNTRYCODE' => 'country', 'MERCHANTREFERENCE' => 'contribution_tracking_id', 'RETURNURL' => 'returnto', 'IPADDRESS' => 'server_ip', 'ISSUERID' => 'issuer_id', 'PAYMENTPRODUCTID' => 'payment_product', 'CVV' => 'cvv', 'EXPIRYDATE' => 'expiration', 'CREDITCARDNUMBER' => 'card_num', 'FIRSTNAME' => 'fname', 'SURNAME' => 'lname', 'STREET' => 'street', 'CITY' => 'city', 'STATE' => 'state', 'ZIP' => 'zip', 'EMAIL' => 'email', 'ACCOUNTHOLDER' => 'account_holder', 'ACCOUNTNAME' => 'account_name', 'ACCOUNTNUMBER' => 'account_number', 'ADDRESSLINE1E' => 'address_line_1e', 'ADDRESSLINE2' => 'address_line_2', 'ADDRESSLINE3' => 'address_line_3', 'ADDRESSLINE4' => 'address_line_4', 'ATTEMPTID' => 'attempt_id', 'AUTHORISATIONID' => 'authorization_id', 'BANKACCOUNTNUMBER' => 'bank_account_number', 'BANKAGENZIA' => 'bank_agenzia', 'BANKCHECKDIGIT' => 'bank_check_digit', 'BANKCODE' => 'bank_code', 'BANKFILIALE' => 'bank_filiale', 'BANKNAME' => 'bank_name', 'BRANCHCODE' => 'branch_code', 'COUNTRYCODEBANK' => 'country_code_bank', 'COUNTRYDESCRIPTION' => 'country_description', 'CUSTOMERBANKCITY' => 'customer_bank_city', 'CUSTOMERBANKSTREET' => 'customer_bank_street', 'CUSTOMERBANKNUMBER' => 'customer_bank_number', 'CUSTOMERBANKZIP' => 'customer_bank_zip', 'DATECOLLECT' => 'date_collect', 'DESCRIPTOR' => 'descriptor', 'DIRECTDEBITTEXT' => 'direct_debit_text', 'DOMICILIO' => 'domicilio', 'EFFORTID' => 'effort_id', 'IBAN' => 'iban', 'IPADDRESSCUSTOMER' => 'user_ip', 'PAYMENTREFERENCE' => 'payment_reference', 'PROVINCIA' => 'provincia', 'SPECIALID' => 'special_id', 'SWIFTCODE' => 'swift_code', 'TRANSACTIONTYPE' => 'transaction_type', 'FISCALNUMBER' => 'fiscal_number', ); $exposed = TestingAccessWrapper::newFromObject( $gateway ); $this->assertEquals( $var_map, $exposed->var_map ); } public function testLanguageStaging() { $options = $this->getDonorTestData( 'NO' ); $options['payment_method'] = 'cc'; $options['payment_submethod'] = 'visa'; $gateway = $this->getFreshGatewayObject( $options ); $exposed = TestingAccessWrapper::newFromObject( $gateway ); $exposed->stageData(); $this->assertEquals( $exposed->getData_Staged( 'language' ), 'no', "'NO' donor's language was inproperly set. Should be 'no'" ); } public function testLanguageFallbackStaging() { $options = $this->getDonorTestData( 'Catalonia' ); $options['payment_method'] = 'cc'; $options['payment_submethod'] = 'visa'; $gateway = $this->getFreshGatewayObject( $options ); $exposed = TestingAccessWrapper::newFromObject( $gateway ); $exposed->stageData(); // Requesting the fallback language from the gateway. $this->assertEquals( 'en', $exposed->getData_Staged( 'language' ) ); } /** * Make sure unstaging functions don't overwrite core donor data. */ public function testAddResponseData_underzealous() { $options = $this->getDonorTestData( 'Catalonia' ); $options['payment_method'] = 'cc'; $options['payment_submethod'] = 'visa'; $gateway = $this->getFreshGatewayObject( $options ); // This will set staged_data['language'] = 'en'. $exposed = TestingAccessWrapper::newFromObject( $gateway ); $exposed->stageData(); $ctid = mt_rand(); $gateway->addResponseData( array( 'contribution_tracking_id' => $ctid . '.1', ) ); $exposed = TestingAccessWrapper::newFromObject( $gateway ); // Desired vars were written into normalized data. $this->assertEquals( $ctid, $exposed->dataObj->getVal_Escaped( 'contribution_tracking_id' ) ); // Language was not overwritten. $this->assertEquals( 'ca', $exposed->dataObj->getVal_Escaped( 'language' ) ); } /** * Tests to make sure that certain error codes returned from GC will or * will not create payments error loglines. */ function testCCLogsOnGatewayError() { $init = $this->getDonorTestData( 'US' ); unset( $init['order_id'] ); $init['ffname'] = 'cc-vmad'; //this should not throw any payments errors: Just an invalid card. $gateway = $this->getFreshGatewayObject( $init ); $gateway->setDummyGatewayResponseCode( '430285' ); $gateway->do_transaction( 'GET_ORDERSTATUS' ); $this->verifyNoLogErrors(); //Now test one we want to throw a payments error $gateway = $this->getFreshGatewayObject( $init ); $gateway->setDummyGatewayResponseCode( '21000050' ); $gateway->do_transaction( 'GET_ORDERSTATUS' ); $loglines = $this->getLogMatches( LogLevel::ERROR, '/Investigation required!/' ); $this->assertNotEmpty( $loglines, 'GC Error 21000050 is not generating the expected payments log error' ); //Reset logs $this->testLogger->messages = array(); //Most irritating version of 20001000 - They failed to enter an expiration date on GC's form. This should log some specific info, but not an error. $gateway = $this->getFreshGatewayObject( $init ); $gateway->setDummyGatewayResponseCode( '20001000-expiry' ); $gateway->do_transaction( 'GET_ORDERSTATUS' ); $this->verifyNoLogErrors(); $loglines = $this->getLogMatches( LogLevel::INFO, '/processResponse:.*EXPIRYDATE/' ); $this->assertNotEmpty( $loglines, 'GC Error 20001000-expiry is not generating the expected payments log line' ); } /** * Tests to make sure that certain error codes returned from GC will * trigger order cancellation, even if retryable errors also exist. * @dataProvider mcNoRetryCodeProvider */ public function testNoMastercardFinesForRepeatOnBadCodes( $code ) { $init = $this->getDonorTestData( 'US' ); unset( $init['order_id'] ); $init['ffname'] = 'cc-vmad'; //Make it not look like an orphan $this->setMwGlobals( 'wgRequest', new FauxRequest( array( 'CVVRESULT' => 'M', 'AVSRESULT' => '0' ), false ) ); //Toxic card should not retry, even if there's an order id collision $gateway = $this->getFreshGatewayObject( $init ); $gateway->setDummyGatewayResponseCode( $code ); $gateway->do_transaction( 'Confirm_CreditCard' ); $this->assertEquals( 1, count( $gateway->curled ), "Gateway kept trying even with response code $code! MasterCard could fine us a thousand bucks for that!" ); - $this->assertEquals( 1, count( $gateway->limbo_stomps ), "Gateway sent no limbostomps for code $code! Should have sent exactly one antimessage!" ); - $this->assertEquals( true, $gateway->limbo_stomps[0], "Gateway sent wrong stomp message for code $code! Should have sent an antimessage!" ); - - // Test Memcache mirror - $this->assertEquals( 1, count( $gateway->memcache_limbo_stomps ), "Gateway sent no (memcache) limbostomps for code $code! Should have sent exactly one antimessage!" ); - $this->assertEquals( true, $gateway->memcache_limbo_stomps[0], "Gateway sent wrong (memcache) stomp message for code $code! Should have sent an antimessage!" ); + // Test limbo queue contents. + $this->assertEquals( array( true ), $gateway->limbo_messages, + "Gateway did not delete limbo message for code $code!" ); } /** * Tests that two API requests don't send the same order ID and merchant * reference. This was the case when users doubleclicked and we were * using the last 5 digits of time in seconds as a suffix. We want to see * what happens when a 2nd request comes in while the 1st is still waiting * for a CURL response, so here we fake that situation by having CURL throw * an exception during the 1st response. */ public function testNoDupeOrderId( ) { $this->setMwGlobals( 'wgRequest', new FauxRequest( array( 'action'=>'donate', 'amount'=>'3.00', 'card_type'=>'amex', 'city'=>'Hollywood', 'contribution_tracking_id'=>'22901382', 'country'=>'US', 'currency_code'=>'USD', 'emailAdd'=>'FaketyFake@gmail.com', 'fname'=>'Fakety', 'format'=>'json', 'gateway'=>'globalcollect', 'language'=>'en', 'lname'=>'Fake', 'payment_method'=>'cc', 'referrer'=>'http://en.wikipedia.org/wiki/Main_Page', 'state'=>'MA', 'street'=>'99 Fake St', 'utm_campaign'=>'C14_en5C_dec_dsk_FR', 'utm_medium'=>'sitenotice', 'utm_source'=>'B14_120921_5C_lg_fnt_sans.no-LP.cc', 'zip'=>'90210' ), false ) ); $gateway = new TestingGlobalCollectAdapter( array( 'api_request' => 'true' ) ); $gateway->setDummyGatewayResponseCode( 'Exception' ); try { $gateway->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); } catch ( Exception $e ) { // totally expected this } $first = $gateway->curled[0]; //simulate another request coming in before we get anything back from GC $anotherGateway = new TestingGlobalCollectAdapter( array( 'api_request' => 'true' ) ); $anotherGateway->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); $second = $anotherGateway->curled[0]; $this->assertFalse( $first == $second, 'Two calls to the api did the same thing'); } /** * Tests to see that we don't claim we're going to retry when we aren't * going to. For GC, we really only want to retry on code 300620 * @dataProvider benignNoRetryCodeProvider */ public function testNoClaimRetryOnBoringCodes( $code ) { $init = $this->getDonorTestData( 'US' ); unset( $init['order_id'] ); $init['ffname'] = 'cc-vmad'; //Make it not look like an orphan $this->setMwGlobals( 'wgRequest', new FauxRequest( array( 'CVVRESULT' => 'M', 'AVSRESULT' => '0' ), false ) ); $gateway = $this->getFreshGatewayObject( $init ); $gateway->setDummyGatewayResponseCode( $code ); $exposed = TestingAccessWrapper::newFromObject( $gateway ); $start_id = $exposed->getData_Staged( 'order_id' ); $gateway->do_transaction( 'Confirm_CreditCard' ); $finish_id = $exposed->getData_Staged( 'order_id' ); $loglines = $this->getLogMatches( LogLevel::INFO, '/Repeating transaction on request for vars:/' ); $this->assertEmpty( $loglines, "Log says we are going to repeat the transaction for code $code, but that is not true" ); $this->assertEquals( $start_id, $finish_id, "Needlessly regenerated order id for code $code "); } /** * doPayment should return an iframe result with normal data */ function testDoPaymentSuccess() { $init = $this->getDonorTestData(); $init['payment_method'] = 'cc'; $init['payment_submethod'] = 'visa'; $init['email'] = 'innocent@clean.com'; $init['ffname'] = 'cc-vmad'; unset( $init['order_id'] ); $gateway = $this->getFreshGatewayObject( $init ); $result = $gateway->doPayment(); $this->assertEmpty( $result->isFailed(), 'PaymentResult should not be failed' ); $this->assertEmpty( $result->getErrors(), 'PaymentResult should have no errors' ); $this->assertEquals( 'url_placeholder', $result->getIframe(), 'PaymentResult should have iframe set' ); } } diff --git a/tests/includes/test_gateway/TestingGlobalCollectAdapter.php b/tests/includes/test_gateway/TestingGlobalCollectAdapter.php index 10156466..9852a9fd 100644 --- a/tests/includes/test_gateway/TestingGlobalCollectAdapter.php +++ b/tests/includes/test_gateway/TestingGlobalCollectAdapter.php @@ -1,149 +1,141 @@ order_id_meta = $options['order_id_meta']; unset( $options['order_id_meta'] ); } $this->options = $options; parent::__construct( $this->options ); } /** * Clear the static globals cache. */ public static function clearGlobalsCache() { self::$globalsCache = array ( ); } /** * @TODO: Get rid of this and the override mechanism as soon as you * refactor the constructor into something reasonable. * @return type */ public function defineOrderIDMeta() { if ( isset( $this->order_id_meta ) ) { return; } parent::defineOrderIDMeta(); } - /** - * Stub out the limboStomp fn and record the calls - * @param type $antiMessage - */ - public function doLimboStompTransaction( $antiMessage = false ) { - $this->limbo_stomps[] = $antiMessage; - } - + // TODO: Store and test the actual messages. public function setLimboMessage( $queue = 'limbo' ) { - $this->memcache_limbo_stomps[] = false; + $this->limbo_messages[] = false; } /** * Stub out the limboStomp fn and record the calls */ public function deleteLimboMessage( $queue = 'limbo' ) { - $this->memcache_limbo_stomps[] = true; + $this->limbo_messages[] = true; } //@TODO: That minfraud jerk needs its own isolated tests. function runAntifraudHooks() { //now screw around with the batch settings to trick the fraud filters into triggering $is_batch = $this->isBatchProcessor(); $this->batch = true; parent::runAntifraudHooks(); $this->batch = $is_batch; } /** * Set the error code you want the dummy response to return */ public function setDummyGatewayResponseCode( $code ) { $this->dummyGatewayResponseCode = $code; } /** * Set the error code you want the dummy response to return */ public function setDummyCurlResponseCode( $code ) { $this->dummyCurlResponseCode = $code; } protected function curl_transaction( $data ) { $this->curled[] = $data; return parent::curl_transaction( $data ); } /** * Load in some dummy response XML so we can test proper response processing * @throws RuntimeException */ protected function curl_exec( $ch ) { $code = ''; if ( property_exists( $this, 'dummyGatewayResponseCode' ) ) { $code = '_' . $this->dummyGatewayResponseCode; if ( $this->dummyGatewayResponseCode == 'Exception' ) { throw new RuntimeException('blah!'); } } //could start stashing these in a further-down subdir if payment type starts getting in the way, //but frankly I don't want to write tests that test our dummy responses. $file_path = __DIR__ . '/../'; $file_path .= 'Responses' . '/' . self::getIdentifier() . '/'; $file_path .= $this->getCurrentTransaction() . $code . '.testresponse'; //these are all going to be short, so... if ( file_exists( $file_path ) ) { return file_get_contents( $file_path ); } else { echo "File $file_path does not exist.\n"; //<-That will deliberately break the test. return false; } } /** * Load in some dummy curl response info so we can test proper response processing */ protected function curl_getinfo( $ch, $opt = null ) { $code = 200; //response OK if ( property_exists( $this, 'dummyCurlResponseCode' ) ) { $code = ( int ) $this->dummyCurlResponseCode; } //put more here if it ever turns out that we care about it. return array ( 'http_code' => $code, ); } }