r69650 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r69649‎ | r69650 | r69651 >
Date:23:50, 20 July 2010
Author:awjrichards
Status:reverted (Comments)
Tags:
Comment:
* fixed example install path for payflow pro gateway
* added a maintenance script that checks a queue (via Stomp) for messages that need to be verified against Payflow Pro, pulls those messages and verifies them
Modified paths:
  • /trunk/extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.php (modified) (history)
  • /trunk/extensions/DonationInterface/payflowpro_gateway/stompPFPPendingProcessor.php (added) (history)

Diff [purge]

Index: trunk/extensions/DonationInterface/payflowpro_gateway/stompPFPPendingProcessor.php
@@ -0,0 +1,401 @@
 2+<?php
 3+/**
 4+ * Processes pending PayflowPro transactions in a queueing service using Stomp
 5+ *
 6+ * Uses the MediaWiki maintenance system for command line execution. Requires MW > 1.16
 7+ *
 8+ * This was built to verify pending PayflowPro transactions in an ActiveMQ queue system.
 9+ * It pulls a transaction out of a 'pending' queue and submits the message to PayflowPro
 10+ * for verification. If PayflowPro verifies the transaction, it is then passed off to a
 11+ * 'confirmed' queue for processing elsewhere. If PayflowPro rejects for a small variety
 12+ * of reasons (ones that require some user intervention), the message is reloaded to the
 13+ * queue. If PayflowPro completely rejects the message, it is pruned from the queue
 14+ * altogether.
 15+ *
 16+ * This performs some logging (depending on the log_level setting), which if not set to 0
 17+ * just gets output to the screen.
 18+ *
 19+ * Config options (key = var name for localSettings.php, value = command line arg name):
 20+ * $options = array (
 21+ * 'wgPayflowProURL' => 'pfp-url',
 22+ * 'wgPayflowProPartnerID' => 'pfp-partner-id',
 23+ * 'wgPayflowProVendorID' => 'pfp-vendor-id',
 24+ * 'wgPayflowProUserID' => 'pfp-user-id',
 25+ * 'wgPayflowProPassword' => 'pfp-password',
 26+ * 'wgActiveMQStompURI' => 'activemq-stomp-uri',
 27+ * 'wgActiveMQPendingPath' => 'activemq-pending-queue',
 28+ * 'wgActiveMQConfirmedPath' => 'activemq-confirmed-queue',
 29+ * 'wgActiveMQPendingProcessingBatchSize' => 'batch-size',
 30+ * 'wgActiveMQPendingProcessLogLevel' => 'log-level' );
 31+ * Each of these config options gets set as a class property with the name of the command line arg,
 32+ * with the '-' replaced with a '_' (eg 'pfp-url' becomes $this->pfp_url).
 33+ *
 34+ * @author: Arthur Richards <arichards@wikimedia.org>
 35+ */
 36+require_once( dirname(__FILE__) . "/../../../maintenance/Maintenance.php" );
 37+
 38+// load necessary stomp files from DonationInterface/active_mq
 39+//require_once( dirname( __FILE__ ) . "/../extensions/DonationInterface/activemq_stomp/Stomp.php" );
 40+require_once('/var/www/sites/all/modules/queue2civicrm/Stomp.php'); // why are these Stomps different?!
 41+//require_once( dirname( __FILE__ ) . "/../extensions/DonationInterface/activemq_stomp/Stomp/Exception.php" );
 42+require_once('/var/www/sites/all/modules/queue2civicrm/Stomp/Exception.php');
 43+
 44+define( 'LOG_LEVEL_QUIET', 0 ); // disables all logging
 45+define( 'LOG_LEVEL_INFO', 1 ); // some useful logging information
 46+define( 'LOG_LEVEL_DEBUG', 2 ); // verbose logging for debug
 47+
 48+class StompPFPPendingProcessor extends Maintenance {
 49+
 50+ /** If TRUE, output extra information for debug purposes **/
 51+ protected $log_level = LOG_LEVEL_INFO;
 52+
 53+ /** Holds our Stomp connection instance **/
 54+ protected $stomp;
 55+
 56+ /** The number of items to process **/
 57+ protected $batch_size = 50;
 58+
 59+ public function __construct() {
 60+ parent::__construct();
 61+
 62+ // register command line params with the parent class
 63+ $this->register_params();
 64+ }
 65+
 66+ public function execute() {
 67+ // load configuration options
 68+ $this->load_config_options();
 69+ $this->log( "Pending queue processor bootstrapped and ready to go!" );
 70+
 71+ // estamplish a connection to the stomp listener
 72+ $this->set_stomp_connection();
 73+
 74+ $this->log( "Preparing to process up to {$this->batch_size} pending transactions.", LOG_LEVEL_DEBUG );
 75+
 76+ // batch process pending transactions
 77+ for ( $i = 0; $i < $this->batch_size; $i++ ) {
 78+ // empty pending_transaction
 79+ if ( isset( $message )) unset( $message );
 80+
 81+ // fetch the latest pending transaction from the queue (Stomp_Frame object)
 82+ $message = $this->fetch_message( $this->activemq_pending_queue );
 83+ // if we do not get a pending transaction back...
 84+ if ( !$message ) {
 85+ $this->log( "There are no more pending transactions to process.", LOG_LEVEL_DEBUG );
 86+ break;
 87+ }
 88+
 89+ // the message is in it's raw format, we need to decode just it's body
 90+ $pending_transaction = json_decode( $message->body, TRUE );
 91+ $this->log( "Pending transaction: " . print_r( $pending_transaction, TRUE ), LOG_LEVEL_DEBUG );
 92+
 93+ // fetch the payflow pro status of this transaction
 94+ $status = $this->fetch_payflow_transaction_status( $pending_transaction['gateway_txn_id'] );
 95+
 96+ // determine the result code from the payflow pro status message
 97+ $result_code = $this->parse_payflow_transaction_status( $status );
 98+
 99+ // handle the pending transaction based on the payflow pro result code
 100+ $this->handle_pending_transaction( $result_code, json_encode( $pending_transaction ));
 101+
 102+ $ack_response = $this->stomp->ack( $message );
 103+ $this->log( "Ack response: $ack_response", LOG_LEVEL_DEBUG );
 104+ }
 105+ $this->log( "Processed $i messages." );
 106+ }
 107+
 108+ /**
 109+ * Fetch latest raw message from a queue
 110+ *
 111+ * @param $destination string of the destination path from where to fetch a message
 112+ * @return mixed raw message (Stomp_Frame object) from Stomp client or False if no msg present
 113+ */
 114+ protected function fetch_message( $destination ) {
 115+ $this->log( "Attempting to connect to queue at: $destination", LOG_LEVEL_DEBUG );
 116+
 117+ $this->stomp->subscribe( $destination );
 118+
 119+ $this->log( "Attempting to pull queued item", LOG_LEVEL_DEBUG );
 120+ $message = $this->stomp->readFrame();
 121+ return $message;
 122+ }
 123+
 124+ /**
 125+ * Send a message to the queue
 126+ *
 127+ * @param $destination string of the destination path for where to send a message
 128+ * @param $message string the (formatted) message to send to the queue
 129+ * @param $options array of additional Stomp options
 130+ * @return bool result from send, FALSE on failure
 131+ */
 132+ protected function queue_message( $destination, $message, $options = array( 'persistent' => TRUE )) {
 133+ $this->log( "Attempting to queue message...", LOG_LEVEL_DEBUG );
 134+ $sent = $this->stomp->send( $destination, $message, $options );
 135+ $this->log( "Result of queuing message: $sent", LOG_LEVEL_DEBUG );
 136+ return $sent;
 137+ }
 138+
 139+ /**
 140+ * Fetch the PayflowPro status of a transaction.
 141+ *
 142+ * @param $transaction_id string of the original ID of the transaction to status check
 143+ * @return string containing the raw status message returned by PayflowPro
 144+ */
 145+ protected function fetch_payflow_transaction_status( $transaction_id ) {
 146+ $this->log( "Transaction ID: $transaction_id", LOG_LEVEL_DEBUG );
 147+ // create payflow query string, include string lengths
 148+ $queryArray = array(
 149+ 'TRXTYPE' => 'I',
 150+ 'TENDER' => 'C',
 151+ 'USER' => $this->pfp_user_id, //$payflow_data['user'],
 152+ 'VENDOR' => $this->pfp_vendor_id, //$payflow_data['vendor'],
 153+ 'PARTNER' => $this->pfp_partner_id, //$payflow_data['partner'],
 154+ 'PWD' => $this->pfp_password, //$payflow_data['password'],
 155+ 'ORIGID' => $transaction_id,
 156+ );
 157+ $this->log( "PayflowPro query array: " . print_r( $queryArray, TRUE ), LOG_LEVEL_DEBUG );
 158+
 159+ // format the query string for PayflowPro
 160+ foreach ( $queryArray as $name => $value ) {
 161+ $query[] = $name . '[' . strlen( $value ) . ']=' . $value;
 162+ }
 163+ $payflow_query = implode( '&', $query );
 164+ $this->log( "PayflowPro query array (formatted): " . print_r( $payflow_query, TRUE ), LOG_LEVEL_DEBUG );
 165+
 166+ // assign header data necessary for the curl_setopt() function
 167+ $order_id = date( 'ymdH' ) . rand( 1000, 9999 ); //why?
 168+ $user_agent = $_SERVER['HTTP_USER_AGENT'];
 169+ $headers[] = 'Content-Type: text/namevalue';
 170+ $headers[] = 'Content-Length : ' . strlen( $payflow_query );
 171+ $headers[] = 'X-VPS-Client-Timeout: 45';
 172+ $headers[] = 'X-VPS-Request-ID:' . $order_id;
 173+ $ch = curl_init();
 174+
 175+ curl_setopt( $ch, CURLOPT_URL, $this->pfp_url );
 176+ curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
 177+ curl_setopt( $ch, CURLOPT_USERAGENT, $user_agent );
 178+ curl_setopt( $ch, CURLOPT_HEADER, 1 );
 179+ curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
 180+ curl_setopt( $ch, CURLOPT_TIMEOUT, 90 );
 181+ curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 0 );
 182+ curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 0 );
 183+ curl_setopt( $ch, CURLOPT_POSTFIELDS, $payflow_query );
 184+ curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 2 );
 185+ curl_setopt( $ch, CURLOPT_FORBID_REUSE, true );
 186+ curl_setopt( $ch, CURLOPT_POST, 1 );
 187+
 188+ // As suggested in the PayPal developer forum sample code, try more than once to get a response
 189+ // in case there is a general network issue
 190+ for ( $i=1; $i <=3; $i++ ) {
 191+ $this->log( "Attempt #$i to connect to PayflowPro...", LOG_LEVEL_DEBUG );
 192+ $status = curl_exec( $ch );
 193+ $headers = curl_getinfo( $ch );
 194+
 195+ if ( $headers['http_code'] != 200 && $headers['http_code'] != 403 ) {
 196+ sleep( 5 );
 197+ } elseif ( $headers['http_code'] == 200 || $headers['http_code'] == 403 ) {
 198+ $this->log( "Succesfully connected to PayflowPro", LOG_LEVEL_DEBUG );
 199+ break;
 200+ }
 201+ }
 202+
 203+ if ( $headers['http_code'] != 200 ) {
 204+ $this->log( "No response from PayflowPro after $i attempts." );
 205+ curl_close( $ch );
 206+ exit(1);
 207+ }
 208+
 209+ curl_close( $ch );
 210+
 211+ $this->log( "PayflowPro reported status: $status", LOG_LEVEL_DEBUG );
 212+ return $status;
 213+ }
 214+
 215+ /**
 216+ * Parse the result code out of PayflowPro's status message.
 217+ *
 218+ * This is modified from queue2civicrm_payflow_result() in the Drupal queue2civicrm module.
 219+ * That code, however, seemed to be cataloging all of the key/value pairs in the status
 220+ * message. Since we only care about the result code, that's all I'm looking for.
 221+ * Perhaps it is more favorable to return an aray of the key/value pairs in the status
 222+ * message...
 223+ *
 224+ * @status string The full status message returned by a PayflowPro queyry
 225+ * @return int PayflowPro result code, FALSE on failure
 226+ */
 227+ protected function parse_payflow_transaction_status( $status ) {
 228+ // we only really care about the 'RESULT' portion of the status message
 229+ $result = strstr( $status, 'RESULT' );
 230+
 231+ // log the result string?
 232+ $this->log( "PayflowPro RESULT string: $result", LOG_LEVEL_DEBUG );
 233+
 234+ // establish our key/value positions in the string to facilitate extracting the value
 235+ $key_position = strpos( $result, '=' );
 236+ $value_position = strpos( $result, '&' ) ? strpos( $result, '&' ) : strlen( $result) ;
 237+
 238+ $result_code = substr( $result, $key_position + 1, $value_position - $key_position - 1 );
 239+ $this->log( "PayflowPro result code: $result_code", LOG_LEVEL_DEBUG );
 240+ return $result_code;
 241+ }
 242+
 243+ /**
 244+ * Apropriately handles pending transactions based on the PayflowPro result code
 245+ *
 246+ * @param int PayflowPro result code
 247+ * @param string Formatted message to send to a queue
 248+ */
 249+ protected function handle_pending_transaction( $result_code, $message ) {
 250+ switch ( $result_code ) {
 251+ case "0": // push to confirmed queue
 252+ $this->log( "Attempting to push message to confirmed queue: " . print_r( $message, TRUE ), LOG_LEVEL_DEBUG );
 253+ if ( $this->queue_message( $this->activemq_confirmed_queue, $message )) {
 254+ $this->log( "Succesfully pushed message to confirmed queue.", LOG_LEVEL_DEBUG );
 255+ }
 256+ break;
 257+ case "126": // push back to pending queue
 258+ case "26": //push back to pending queue
 259+ $this->log( "Attempting to push message back to pending queue: " . print_r( $message, TRUE ), LOG_LEVEL_DEBUG );
 260+ if ( $this->queue_message( $this->activemq_pending_queue, $message )) {
 261+ $this->log( "Succesfully pushed message back to pending queue", LOG_LEVEL_DEBUG );
 262+ }
 263+ break;
 264+ default:
 265+ $this->log( "Message ignored: " . print_r( $message, TRUE ), LOG_LEVEL_DEBUG );
 266+ break;
 267+ }
 268+ }
 269+
 270+ /**
 271+ * Loads configuration options
 272+ *
 273+ * Config options can be set in localSettings.php or with arguments passed in via the command
 274+ * line. Command line arguments will override localSettings.php defined options.
 275+ */
 276+ protected function load_config_options() {
 277+ // Associative array of mediawiki option => maintenance arg name
 278+ $options = array (
 279+ 'wgPayflowProURL' => 'pfp-url',
 280+ 'wgPayflowProPartnerID' => 'pfp-partner-id',
 281+ 'wgPayflowProVendorID' => 'pfp-vendor-id',
 282+ 'wgPayflowProUserID' => 'pfp-user-id',
 283+ 'wgPayflowProPassword' => 'pfp-password',
 284+ 'wgActiveMQStompURI' => 'activemq-stomp-uri',
 285+ 'wgActiveMQPendingPath' => 'activemq-pending-queue',
 286+ 'wgActiveMQConfirmedPath' => 'activemq-confirmed-queue',
 287+ 'wgActiveMQPendingProcessingBatchSize' => 'batch-size',
 288+ 'wgActiveMQPendingProcessLogLevel' => 'log-level' );
 289+
 290+ // loop through our options and set their values,
 291+ // overrides local settings with command line options
 292+ foreach ( $options as $mw_option => $m_arg_name ) {
 293+ global ${$mw_option};
 294+
 295+ // replace - with _ from CL args to map to class properties
 296+ $property = str_replace( "-", "_", $m_arg_name );
 297+
 298+ // set class property with the config option
 299+ $this->$property = parent::getOption( $m_arg_name, ${$mw_option} );
 300+
 301+ $this->log( "$property = $mw_option, $m_arg_name, ${$mw_option}", LOG_LEVEL_DEBUG );
 302+ $this->log( "$property = {$this->$property}", LOG_LEVEL_DEBUG );
 303+ }
 304+ }
 305+
 306+ /**
 307+ * Registers parameters with the maintenance system
 308+ */
 309+ protected function register_params() {
 310+ //parent::addOption( $name, $description, $required=false, $withArg=false )
 311+ parent::addOption( 'pfp-url', 'The PayflowPro URL', FALSE, TRUE );
 312+ parent::addOption(
 313+ 'pfp-partner-id',
 314+ 'Authorized reseller ID for PayflowPro (eg "PayPal")',
 315+ FALSE,
 316+ TRUE );
 317+ parent::addOption( 'pfp-vendor-id', 'The PayflowPro merchant login ID', FALSE, TRUE );
 318+ parent::addOption(
 319+ 'pfp-user-id',
 320+ "If one or more users are set up, this should be the authorized PayflowPro user ID, otherwise this should be the same as pfp-vendor-id",
 321+ FALSE,
 322+ TRUE );
 323+ parent::addOption(
 324+ 'pfp-password',
 325+ 'The PayflowPro merchant login password. ** Declaring this here could be a security risk.',
 326+ FALSE,
 327+ TRUE );
 328+ parent::addOption(
 329+ 'activemq-stomp-uri',
 330+ 'The URI to the ActiveMQ Stomp listener',
 331+ FALSE,
 332+ TRUE );
 333+ parent::addOption(
 334+ 'activemq-pending-queue',
 335+ 'The path to the ActiveMQ pending queue',
 336+ FALSE,
 337+ TRUE );
 338+ parent::addOption(
 339+ 'activemq-confirmed-queue',
 340+ 'The path to the ActiveMQ confirmed queue',
 341+ FALSE,
 342+ TRUE );
 343+ // I know there is a method to handle batch size this in the parent class, but it makes me
 344+ // do things I don't want to do.
 345+ parent::addOption(
 346+ 'batch-size',
 347+ 'The number of queue items to process. Default: ' . $this->batch_size,
 348+ FALSE,
 349+ TRUE );
 350+ parent::addOption(
 351+ 'log-level',
 352+ "The level of logging you would like to enable. Options:\n\t0 - No output\n\t1 - Normal, minimal output\n\t2 - Debug, verbose output",
 353+ FALSE,
 354+ TRUE );
 355+ }
 356+
 357+ /**
 358+ * Establishes a connection to the stomp listener
 359+ *
 360+ * Stomp listner URI set in config options (via command line or localSettings.php).
 361+ * If a connection cannot be established, will exit with non-0 status.
 362+ */
 363+ protected function set_stomp_connection() {
 364+ //attempt to connect, otherwise throw exception and exit
 365+ $this->log( "Attempting to connect to Stomp listener: {$this->activemq_stomp_uri}", LOG_LEVEL_DEBUG );
 366+ try {
 367+ //establish stomp connection
 368+ $this->stomp = new Stomp( $this->activemq_stomp_uri );
 369+ $this->stomp->connect();
 370+ $this->log( "Successfully connected to Stomp listener", LOG_LEVEL_DEBUG );
 371+ } catch (Stomp_Exception $e) {
 372+ $this->log( "Stomp connection failed: " . $e->getMessage() );
 373+ exit(1);
 374+ }
 375+ }
 376+
 377+ /**
 378+ * Logs messages of less than or equal value to the defined log level.
 379+ *
 380+ * Log levels available are defined by the constants LOG_LEVEL_*. The log level for the script
 381+ * defaults to LOG_LEVEL_INFO but can be overridden in LocalSettings.php or via a command line
 382+ * argument passed in at run time.
 383+ *
 384+ * @param $message string containing the message you wish to log
 385+ * @param $level int of the highest log level you wish to output messages for
 386+ */
 387+ protected function log( $message, $level=LOG_LEVEL_INFO ) {
 388+ if ( $this->log_level >= $level ) echo date('c') . ": " . $level . " : " . $message . "\n";
 389+ }
 390+
 391+ public function __destruct() {
 392+ // clean up our stomp connection
 393+ $this->log( "Cleaning up stomp connection...", LOG_LEVEL_DEBUG );
 394+ if ( isset( $this->stomp )) $this->stomp->disconnect();
 395+ $this->log( "Stomp connection cleaned up", LOG_LEVEL_DEBUG );
 396+ $this->log( "Exiting gracefully" );
 397+
 398+ }
 399+}
 400+
 401+$maintClass = "StompPFPPendingProcessor";
 402+require_once( DO_MAINTENANCE );
Index: trunk/extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.php
@@ -4,7 +4,7 @@
55 if( !defined( 'MEDIAWIKI' ) ) {
66 echo <<<EOT
77 To install PayflowPro Gateway extension, put the following line in LocalSettings.php:
8 -require_once( "\$IP/extensions/payflowpro_gateway/payflowpro_gateway.php" );
 8+require_once( "\$IP/extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.php" );
99 EOT;
1010 exit( 1 );
1111 }

Comments

#Comment by Tim Starling (talk | contribs)   07:31, 13 August 2010
require_once('/var/www/sites/all/modules/queue2civicrm/Stomp.php'); // why are these Stomps different?!

Don't hard-code absolute paths. Defer the load of Stomp.php until setup is complete, and then use a regular configuration variable to find it. For instance, you could put the require_once() in StompPFPPendingProcessor::__construct(). Unlike in compiled languages, as long as the class is defined before the code that uses it is actually run, it won't cause an error.

define( 'LOG_LEVEL_QUIET', 0 ); // disables all logging
define( 'LOG_LEVEL_INFO', 1 ); // some useful logging information
define( 'LOG_LEVEL_DEBUG', 2 ); // verbose logging for debug

Please use class constants instead of global constants, or prefix the names with the name of the extension, to avoid conflicts with other extensions that have logging.

			// empty pending_transaction
			if ( isset( $message )) unset( $message );

This code is unnecessary and should be removed.

			// the message is in it's raw format, we need to decode just it's body

Possessive "its" has no apostrophe.

		// we only really care about the 'RESULT' portion of the status message
		$result = strstr( $status, 'RESULT' );

Indeed, that is all we really care about. So parse the response properly so that if the word RESULT appears in some value part before the actual result key, your code will still return that data about which we care so much. For example, if I gave this function the string:

XRESULT=x&YRESULT=y&BATCHID=NORESULT&RESULT=3

The result code should be 3, not x, y or an empty string. You should test the function separately using maintenance/eval.php.

		// loop through our options and set their values, 
		// overrides local settings with command line options
		foreach ( $options as $mw_option => $m_arg_name ) {
			global ${$mw_option};

			// replace - with _ from CL args to map to class properties
			$property = str_replace( "-", "_", $m_arg_name );

I think you're trying to be too clever here, to the detriment of readability. This style of code makes it difficult to search the code for a given member variable (e.g. $this->activemq_stomp_uri), and to find out where that member variable came from. If you added var declarations to the class, then you'd have somewhere to document what they all mean. And if you made load_config_options() look more like this:

		$this->pfp_url = $this->getOption( 'pfp-url', $wgPayflowProURL );
		$this->pfp_partner_id = $this->getOption( 'pfp-partner-id', $wgPayflowProPartnerID );
		$this->pfp_vendor_id = $this->getOption( 'pfp-vendor-id', $wgPayflowProVendorID );
		...

then the data flow would be immediately obvious, the code would be searchable, and static analysis would work on it.

			"The level of logging you would like to enable.  Options:\n\t0 - No output\n\t1 - Normal, minimal output\n\t2 - Debug, verbose output", 

So you have readable names for these options in the code, but not for the users? Don't users need readable options even more than developers? It's not hard to write something like:

		$logLevels = array( 
			'none' => self::NONE, 
			'info' => self::INFO, 
			'debug' => self::DEBUG );
		if ( isset( $logLevels[$level] ) ) {
			$this->log_level = $logLevels[$level];
		}
#Comment by Awjrichards (talk | contribs)   19:20, 25 August 2010

The files in question have been removed as of r71653. This provided rather custom functionality, which is now handled by scripts outside of MW.

Status & tagging log