r69651 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r69650‎ | r69651 | r69652 >
Date:23:57, 20 July 2010
Author:awjrichards
Status:deferred
Tags:
Comment:
* Added experimental/incomplete IPN listener script for PayPal's IPN service
* Fixed example install path for paypal_gateway
Modified paths:
  • /trunk/extensions/DonationInterface/SpecialPaypalIPNProcessing.php (added) (history)
  • /trunk/extensions/DonationInterface/paypal_gateway/paypal_gateway.php (modified) (history)

Diff [purge]

Index: trunk/extensions/DonationInterface/SpecialPaypalIPNProcessing.php
@@ -0,0 +1,261 @@
 2+<?php
 3+/**
 4+ * Special class to act as IPN listener and handler. Also pushes messages into the ActiveMQ
 5+ * queueing system.
 6+ *
 7+ * NOTE: THIS IS EXPERIMENTAL AND INCOMPLETE
 8+ *
 9+ * Requires ContributionTracking extension.
 10+ *
 11+ * Configurable variables:
 12+ * $wgPayPalIPNProcessingLogLevel - can be one of the defined LOG_LEVEL_* constants.
 13+ *
 14+ * PayPal IPN docs: https://cms.paypal.com/us/cgi-bin/?&cmd=_render-content&content_ID=developer/e_howto_admin_IPNIntro
 15+ *
 16+ * @author Arthur Richards <arichards@wikimedia.org>
 17+ * @TODO: add a better mechanism for changing log level
 18+ */
 19+
 20+/** Set available log levels **/
 21+DEFINE( 'LOG_LEVEL_QUIET', 0 ); // output nothing
 22+DEFINE( 'LOG_LEVEL_INFO', 1 ); // output minimal info
 23+DEFINE( 'LOG_LEVEL_DEBUG', 2 ); // output lots of info
 24+
 25+class PaypalIPNProcessing extends UnlistedSpecialPage {
 26+
 27+ // set the apropriate logging level
 28+ protected $log_level = LOG_LEVEL_INFO;
 29+
 30+ // path to Stomp
 31+ protected $stomp_path = dirname( __FILE__ ) . "/../../activemq_stomp/Stomp.php";
 32+
 33+ // path to pending queue
 34+ protected $pending_queue = '/queue/pending_paypal';
 35+
 36+ function __construct() {
 37+ parent::__construct( 'PaypalIPNProcessing' );
 38+ wfLoadExtensionMessages( 'PaypalIPNProcessing' );
 39+ $this->out( "Loading Paypal IPN processor" );
 40+
 41+ if ( isset( $wgPayPalIPNProcessingLogLevel )) {
 42+ $this->log_level = $wgPayPalIPNProcessingLogLevel;
 43+ }
 44+
 45+ if ( isset( $wgPayPalIPNProcessingStompPath )) {
 46+ $this->stomp_path = $wgPayPalIPNProcessingStompPath;
 47+ }
 48+
 49+ if ( isset( $wgPayPalIPNProcessingPendingQueue )) {
 50+ $this->pending_queue = $wgPayPalIPNProcessingPendingQueue;
 51+ }
 52+ }
 53+
 54+ /**
 55+ * Output in plain text?
 56+ */
 57+ function execute( $par ) {
 58+ global $wgRequest, $wgOut;
 59+ $wgOut->disable();
 60+ header( "Content-type: text/plain; charset=utf-8" );
 61+
 62+ //make sure we're actually getting something posted to the page.
 63+ if ( empty( $_POST )) {
 64+ $this->out( "Received an empty post object." );
 65+ return;
 66+ }
 67+
 68+ // connect to stomp
 69+ $this->set_stomp_connection();
 70+
 71+ //push message to pending queue
 72+ $contribution = $this->ipn_parse( $_POST );
 73+ // do the queueing - perhaps move out the tracking checking to its own func?
 74+ if ( !$this->queue_message( $this->pending_queue, $contribution )){
 75+ $this->out( "There was a problem queueing the message to the queue: " . $this->pending_queue );
 76+ $this->out( "Message: " . print_r( $contribution, TRUE ), LOG_LEVEL_DEBUG );
 77+ }
 78+
 79+
 80+ //verify the message with PayPal
 81+ if ( !$this->ipn_verify( $_POST )) {
 82+ $this->out( "Message did not pass PayPal verification." );
 83+ $this->out( "\$_POST contents: " . print_r( $_POST, TRUE ), LOG_LEVEL_DEBUG );
 84+ return;
 85+ }
 86+
 87+ //push to donations queue, remove from pending
 88+ }
 89+
 90+ /**
 91+ * Verify IPN's message validitiy
 92+ *
 93+ * Yoinked from fundcore_paypal_verify() in fundcore/gateways/fundcore_paypal.module Drupal module
 94+ * @param $post_data array of post data - the message received from PayPal
 95+ * @return bool
 96+ */
 97+ protected function ipn_verify( $post_data ) {
 98+ if ( $post_data[ 'payment_status' ] != 'Completed' ) {
 99+ // order not completed
 100+ $this->out( "Message not marked as complete." );
 101+ return FALSE;
 102+ }
 103+
 104+ if ( $post_data[ 'mc_gross' ] <= 0 ) {
 105+ $this->out( "Message has 0 or less in the mc_gross field." );
 106+ return FALSE;
 107+ }
 108+
 109+ // url to respond to paypal with verification response
 110+ $postback_url = 'https://www.paypal.com/cgi-bin/webscr';
 111+ if (isset($post_data['test_ipn'])) {
 112+ $postback_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr';
 113+ }
 114+
 115+ // respond with exact same data/structure + cmd=_notify-validate
 116+ $attr = $post_data;
 117+ $attr['cmd'] = '_notify-validate';
 118+
 119+ // send the message back to PayPal for verification
 120+ $status = $this->curl_download( $postback_url, $attr );
 121+ if ($status != 'VERIFIED') {
 122+ $this->out( "The message could not be verified." );
 123+ $this->out( "Returned with status: $status", LOG_LEVEL_DEBUG );
 124+ return FALSE;
 125+ }
 126+
 127+ return TRUE;
 128+ }
 129+
 130+ /**
 131+ * Parse the PayPal message/post data into the format we need for ActiveMQ
 132+ *
 133+ * @param $post_data array containing the $_POST data from PayPal
 134+ * @return array containing the parsed/formatted message for stuffing into ActiveMQ
 135+ */
 136+ protected function ipn_parse( $post_data ) {
 137+ $contribution = array();
 138+
 139+ $timestamp = strtotime($post_data['payment_date']);
 140+
 141+ // Detect if we're using the new-style
 142+ if (is_numeric($post_data['option_selection1'])) {
 143+ // get the database connection to the tracking table
 144+ $tracking_db = contributionTrackingConnection();
 145+
 146+ // Query from Drupal: $tracking_data = db_fetch_array(db_query('SELECT * FROM {contribution_tracking} WHERE id = %d', $post_data['option_selection1']));
 147+ $tracking_query = $tracking_db->select(
 148+ 'contribution_tracking',
 149+ array( 'optout', 'anonymous', 'note' ),
 150+ array( 'id' => $post_data[ 'option_selection1' ]);
 151+ $tracking_data = $tracking_query->fetchRow();
 152+
 153+ $contribution['contribution_tracking_id'] = $post_data['option_selection1'];
 154+ $contribution['optout'] = $tracking_data['optout'];
 155+ $contribution['anonymous'] = $tracking_data['anonymous'];
 156+ $contribution['comment'] = $tracking_data['note'];
 157+ } else {
 158+ $split = explode(';', $post_data['option_selection1']);
 159+ $contribution['anonymous'] = ($split[0] != 'public' && $split[0] != 'Mention my name');
 160+ $contribution['comment'] = $post_data['option_selection2'];
 161+ }
 162+
 163+ $contribution['email'] = $post_data['payer_email'];
 164+ $contribution['first_name'] = $post_data['first_name'];
 165+ $contribution['last_name'] = $post_data['last_name'];
 166+
 167+ $split = split("\n", str_replace("\r", '', $post_data['address_street']));
 168+
 169+ $contribution['street_address'] = $split[0];
 170+ $contribution['supplemental_address_1'] = $split[1];
 171+ $contribution['city'] = $post_data['address_city'];
 172+ $contribution['original_currency'] = $post_data['mc_currency'];
 173+ $contribution['original_gross'] = $post_data['mc_gross'];
 174+ $contribution['fee'] = $post_data['mc_fee'],
 175+ $contribution['gross'] = $post_data['mc_gross'],
 176+ $contribution['net'] = $contribution['gross'] - $contribution['fee'];
 177+ $contribution['date'] = $timestamp;
 178+
 179+ //print_r the contribution?
 180+
 181+ return $contribution;
 182+ }
 183+
 184+ /**
 185+ * Connect to a URL, send optional post variables, return data
 186+ *
 187+ * Yoinked from _fundcore_paypal_download in fundcore/gateways/fundcore_paypal.module Drupal module
 188+ * @param $url String of the URL to connect to
 189+ * @param $vars Array of POST variables
 190+ * @return String containing the output returned from Server
 191+ */
 192+ protected function curl_download( $url, $vars = NULL ) {
 193+ $ch = curl_init();
 194+ curl_setopt($ch, CURLOPT_URL, $url);
 195+ curl_setopt($ch, CURLOPT_HEADER, 0);
 196+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
 197+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 198+
 199+ if ($vars !== NULL) {
 200+ curl_setopt($ch, CURLOPT_POST, 1);
 201+ curl_setopt($ch, CURLOPT_POSTFIELDS, $vars);
 202+ }
 203+ $data = curl_exec($ch);
 204+ if (!$data) {
 205+ $data = curl_error($ch);
 206+ }
 207+ curl_close($ch);
 208+ return $data;
 209+ }
 210+
 211+ /**
 212+ * Establishes a connection to the stomp listener
 213+ *
 214+ * Stomp listner URI set in config options (via command line or localSettings.php).
 215+ * If a connection cannot be established, will exit with non-0 status.
 216+ */
 217+ protected function set_stomp_connection() {
 218+ require_once( $this->stomp_path );
 219+ //attempt to connect, otherwise throw exception and exit
 220+ $this->out( "Attempting to connect to Stomp listener: {$this->activemq_stomp_uri}", LOG_LEVEL_DEBUG );
 221+ try {
 222+ //establish stomp connection
 223+ $this->stomp = new Stomp( $this->activemq_stomp_uri );
 224+ $this->stomp->connect();
 225+ $this->out( "Successfully connected to Stomp listener", LOG_LEVEL_DEBUG );
 226+ } catch (Stomp_Exception $e) {
 227+ $this->out( "Stomp connection failed: " . $e->getMessage() );
 228+ exit(1);
 229+ }
 230+ }
 231+
 232+ /**
 233+ * Send a message to the queue
 234+ *
 235+ * @param $destination string of the destination path for where to send a message
 236+ * @param $message string the (formatted) message to send to the queue
 237+ * @param $options array of additional Stomp options
 238+ * @return bool result from send, FALSE on failure
 239+ */
 240+ protected function queue_message( $destination, $message, $options = array( 'persistent' => TRUE )) {
 241+ $this->out( "Attempting to queue message...", LOG_LEVEL_DEBUG );
 242+ $sent = $this->stomp->send( $destination, $message, $options );
 243+ $this->out( "Result of queuing message: $sent", LOG_LEVEL_DEBUG );
 244+ return $sent;
 245+ }
 246+
 247+
 248+
 249+ /**
 250+ * Formats text for output.
 251+ *
 252+ * @param $msg String a message to output.
 253+ * @param $level the Level at which the message should be output.
 254+ */
 255+ protected function out( $msg, $level=LOG_LEVEL_INFO ) {
 256+ if ( $this->log_level >= $level ) echo date( 'c' ) . ": " . $msg . "\n";
 257+ }
 258+
 259+ public function __destruct() {
 260+ $this->out( "Exiting gracefully." );
 261+ }
 262+}
Index: trunk/extensions/DonationInterface/paypal_gateway/paypal_gateway.php
@@ -3,7 +3,7 @@
44 if( !defined( 'MEDIAWIKI' ) ) {
55 echo <<<EOT
66 To install my extension, put the following line in LocalSettings.php:
7 -require_once( "\$IP/extensions/paypal_gateway/paypal_gateway.php" );
 7+require_once( "\$IP/extensions/DonationInterface/paypal_gateway/paypal_gateway.php" );
88 EOT;
99 exit( 1 );
1010 }
@@ -21,6 +21,10 @@
2222 $dir = dirname( __FILE__ ) . '/';
2323 $wgExtensionMessagesFiles['PaypalGateway'] = $dir . 'paypal_gateway.i18n.php';
2424
 25+// Set up special page for IPN/ActiveMQ handling (experimental)
 26+$wgAutoloadClasses['PaypalIPNProcessing'] = $dir . 'SpecialPaypalIPNProcessing.php';
 27+$wgSpecialPages['PaypalIPNProcessing'] = 'PaypalIPNProcessing';
 28+
2529 // default variables that should be set in LocalSettings.php
2630 $wgPaypalEmail = '';
2731

Status & tagging log