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 @@ |
4 | 4 | if( !defined( 'MEDIAWIKI' ) ) { |
5 | 5 | echo <<<EOT |
6 | 6 | 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" ); |
8 | 8 | EOT; |
9 | 9 | exit( 1 ); |
10 | 10 | } |
— | — | @@ -21,6 +21,10 @@ |
22 | 22 | $dir = dirname( __FILE__ ) . '/'; |
23 | 23 | $wgExtensionMessagesFiles['PaypalGateway'] = $dir . 'paypal_gateway.i18n.php'; |
24 | 24 | |
| 25 | +// Set up special page for IPN/ActiveMQ handling (experimental) |
| 26 | +$wgAutoloadClasses['PaypalIPNProcessing'] = $dir . 'SpecialPaypalIPNProcessing.php'; |
| 27 | +$wgSpecialPages['PaypalIPNProcessing'] = 'PaypalIPNProcessing'; |
| 28 | + |
25 | 29 | // default variables that should be set in LocalSettings.php |
26 | 30 | $wgPaypalEmail = ''; |
27 | 31 | |