Merge pull request #4892 from colemanw/INFRA-132
[civicrm-core.git] / CRM / Core / Payment / PaymentExpressIPN.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
6a488035
TO
5 +--------------------------------------------------------------------+
6 | This file is a part of CiviCRM. |
7 | |
8 | CiviCRM is free software; you can copy, modify, and distribute it |
9 | under the terms of the GNU Affero General Public License |
10 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
11 | |
12 | CiviCRM is distributed in the hope that it will be useful, but |
13 | WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
15 | See the GNU Affero General Public License for more details. |
16 | |
17 | You should have received a copy of the GNU Affero General Public |
18 | License and the CiviCRM Licensing Exception along |
19 | with this program; if not, contact CiviCRM LLC |
20 | at info[AT]civicrm[DOT]org. If you have questions about the |
21 | GNU Affero General Public License or the licensing of CiviCRM, |
22 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
23 +--------------------------------------------------------------------+
24*/
25
26
27/*
28 * PxPay Functionality Copyright (C) 2008 Lucas Baker, Logistic Information Systems Limited (Logis)
29 * PxAccess Functionality Copyright (C) 2008 Eileen McNaughton
30 * Licensed to CiviCRM under the Academic Free License version 3.0.
31 *
32 * Grateful acknowledgements go to Donald Lobo for invaluable assistance
33 * in creating this payment processor module
34 */
4c6ce474
EM
35
36/**
37 * Class CRM_Core_Payment_PaymentExpressIPN
38 */
6a488035
TO
39class CRM_Core_Payment_PaymentExpressIPN extends CRM_Core_Payment_BaseIPN {
40
41 /**
42 * We only need one instance of this object. So we use the singleton
43 * pattern and cache the instance in this variable
44 *
45 * @var object
46 * @static
47 */
48 static private $_singleton = NULL;
49
50 /**
100fef9d 51 * Mode of operation: live or test
6a488035
TO
52 *
53 * @var object
54 */
55 protected $_mode = NULL;
56
6c786a9b 57 /**
100fef9d 58 * @param string $name
6c786a9b
EM
59 * @param $type
60 * @param $object
61 * @param bool $abort
62 *
63 * @return mixed
64 */
00be9182 65 public static function retrieve($name, $type, $object, $abort = TRUE) {
6a488035
TO
66 $value = CRM_Utils_Array::value($name, $object);
67 if ($abort && $value === NULL) {
68 CRM_Core_Error::debug_log_message("Could not find an entry for $name");
69 echo "Failure: Missing Parameter - " . $name . "<p>";
70 exit();
71 }
72
73 if ($value) {
74 if (!CRM_Utils_Type::validate($value, $type)) {
75 CRM_Core_Error::debug_log_message("Could not find a valid entry for $name");
76 echo "Failure: Invalid Parameter<p>";
77 exit();
78 }
79 }
80
81 return $value;
82 }
83
84 /**
85 * Constructor
86 *
6a0b768e
TO
87 * @param string $mode
88 * The mode of operation: live or test.
6a488035 89 *
77b97be7
EM
90 * @param $paymentProcessor
91 *
92 * @return \CRM_Core_Payment_PaymentExpressIPN
6a488035 93 */
00be9182 94 public function __construct($mode, &$paymentProcessor) {
6a488035
TO
95 parent::__construct();
96
97 $this->_mode = $mode;
98 $this->_paymentProcessor = $paymentProcessor;
99 }
100
6a488035
TO
101 /**
102 * The function gets called when a new order takes place.
103 *
dd244018 104 * @param $success
6a0b768e
TO
105 * @param array $privateData
106 * Contains the name value pair of <merchant-private-data>.
6a488035 107 *
dd244018
EM
108 * @param $component
109 * @param $amount
110 * @param $transactionReference
6a488035 111 *
dd244018
EM
112 * @internal param \xml $dataRoot response send by google in xml format
113 * @return void
6a488035 114 */
00be9182 115 public function newOrderNotify($success, $privateData, $component, $amount, $transactionReference) {
6a488035
TO
116 $ids = $input = $params = array();
117
118 $input['component'] = strtolower($component);
119
120 $ids['contact'] = self::retrieve('contactID', 'Integer', $privateData, TRUE);
121 $ids['contribution'] = self::retrieve('contributionID', 'Integer', $privateData, TRUE);
122
123 if ($input['component'] == "event") {
124 $ids['event'] = self::retrieve('eventID', 'Integer', $privateData, TRUE);
125 $ids['participant'] = self::retrieve('participantID', 'Integer', $privateData, TRUE);
126 $ids['membership'] = NULL;
127 }
128 else {
129 $ids['membership'] = self::retrieve('membershipID', 'Integer', $privateData, FALSE);
130 }
131 $ids['contributionRecur'] = $ids['contributionPage'] = NULL;
132
133 $paymentProcessorID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType',
134 'PayPal_Express', 'id', 'name'
135 );
136
137 if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) {
138 return FALSE;
139 }
140
141 // make sure the invoice is valid and matches what we have in the contribution record
142 $input['invoice'] = $privateData['invoiceID'];
143 $input['newInvoice'] = $transactionReference;
144 $contribution = &$objects['contribution'];
145 $input['trxn_id'] = $transactionReference;
146
147 if ($contribution->invoice_id != $input['invoice']) {
148 CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request");
149 echo "Failure: Invoice values dont match between database and IPN request<p>";
150 return;
151 }
152
153 // lets replace invoice-id with Payment Processor -number because thats what is common and unique
154 // in subsequent calls or notifications sent by google.
155 $contribution->invoice_id = $input['newInvoice'];
156
157 $input['amount'] = $amount;
158
159 if ($contribution->total_amount != $input['amount']) {
160 CRM_Core_Error::debug_log_message("Amount values dont match between database and IPN request");
161 echo "Failure: Amount values dont match between database and IPN request. " . $contribution->total_amount . "/" . $input['amount'] . "<p>";
162 return;
163 }
164
165 $transaction = new CRM_Core_Transaction();
166
6a488035
TO
167 // check if contribution is already completed, if so we ignore this ipn
168
169 if ($contribution->contribution_status_id == 1) {
170 CRM_Core_Error::debug_log_message("returning since contribution has already been handled");
171 echo "Success: Contribution has already been handled<p>";
172 return TRUE;
173 }
174 else {
175 /* Since trxn_id hasn't got any use here,
176 * lets make use of it by passing the eventID/membershipTypeID to next level.
177 * And change trxn_id to the payment processor reference before finishing db update */
178
179 if ($ids['event']) {
180 $contribution->trxn_id = $ids['event'] . CRM_Core_DAO::VALUE_SEPARATOR . $ids['participant'];
181 }
182 else {
183 $contribution->trxn_id = $ids['membership'];
184 }
185 }
186 $this->completeTransaction($input, $ids, $objects, $transaction);
187 return TRUE;
188 }
189
190 /**
191
2aa397bc 192 /**
6a488035
TO
193 * The function returns the component(Event/Contribute..)and whether it is Test or not
194 *
6a0b768e
TO
195 * @param array $privateData
196 * Contains the name-value pairs of transaction related data.
197 * @param int $orderNo
198 * <order-total> send by google.
6a488035
TO
199 *
200 * @return array context of this call (test, component, payment processor id)
201 * @static
202 */
00be9182 203 public static function getContext($privateData, $orderNo) {
6a488035
TO
204
205 $component = NULL;
206 $isTest = NULL;
207
208 $contributionID = $privateData['contributionID'];
209 $contribution = new CRM_Contribute_DAO_Contribution();
210 $contribution->id = $contributionID;
211
212 if (!$contribution->find(TRUE)) {
213 CRM_Core_Error::debug_log_message("Could not find contribution record: $contributionID");
214 echo "Failure: Could not find contribution record for $contributionID<p>";
215 exit();
216 }
217
218 if (stristr($contribution->source, 'Online Contribution')) {
219 $component = 'contribute';
220 }
221 elseif (stristr($contribution->source, 'Online Event Registration')) {
222 $component = 'event';
223 }
224 $isTest = $contribution->is_test;
225
226 $duplicateTransaction = 0;
227 if ($contribution->contribution_status_id == 1) {
228 //contribution already handled. (some processors do two notifications so this could be valid)
229 $duplicateTransaction = 1;
230 }
231
232 if ($component == 'contribute') {
233 if (!$contribution->contribution_page_id) {
234 CRM_Core_Error::debug_log_message("Could not find contribution page for contribution record: $contributionID");
235 echo "Failure: Could not find contribution page for contribution record: $contributionID<p>";
236 exit();
237 }
238 }
239 else {
240
241 $eventID = $privateData['eventID'];
242
243 if (!$eventID) {
244 CRM_Core_Error::debug_log_message("Could not find event ID");
245 echo "Failure: Could not find eventID<p>";
246 exit();
247 }
248
249 // we are in event mode
250 // make sure event exists and is valid
251 $event = new CRM_Event_DAO_Event();
252 $event->id = $eventID;
253 if (!$event->find(TRUE)) {
254 CRM_Core_Error::debug_log_message("Could not find event: $eventID");
255 echo "Failure: Could not find event: $eventID<p>";
256 exit();
257 }
258 }
259
260 return array($isTest, $component, $duplicateTransaction);
2aa397bc 261 }
6a488035
TO
262
263 /**
264 * This method is handles the response that will be invoked by the
265 * notification or request sent by the payment processor.
2aa397bc 266 * hex string from paymentexpress is passed to this function as hex string. Code based on googleIPN
6a488035
TO
267 * mac_key is only passed if the processor is pxaccess as it is used for decryption
268 * $dps_method is either pxaccess or pxpay
269 */
00be9182 270 public static function main($dps_method, $rawPostData, $dps_url, $dps_user, $dps_key, $mac_key) {
6a488035
TO
271
272 $config = CRM_Core_Config::singleton();
273 define('RESPONSE_HANDLER_LOG_FILE', $config->uploadDir . 'CiviCRM.PaymentExpress.log');
274
275 //Setup the log file
276 if (!$message_log = fopen(RESPONSE_HANDLER_LOG_FILE, "a")) {
277 error_func("Cannot open " . RESPONSE_HANDLER_LOG_FILE . " file.\n", 0);
278 exit(1);
279 }
280
281 if ($dps_method == "pxpay") {
282 $processResponse = CRM_Core_Payment_PaymentExpressUtils::_valueXml(array(
283 'PxPayUserId' => $dps_user,
284 'PxPayKey' => $dps_key,
285 'Response' => $_GET['result'],
286 ));
287 $processResponse = CRM_Core_Payment_PaymentExpressUtils::_valueXml('ProcessResponse', $processResponse);
288
289 fwrite($message_log, sprintf("\n\r%s:- %s\n", date("D M j G:i:s T Y"),
290 $processResponse
291 ));
292
293 // Send the XML-formatted validation request to DPS so that we can receive a decrypted XML response which contains the transaction results
294 $curl = CRM_Core_Payment_PaymentExpressUtils::_initCURL($processResponse, $dps_url);
295
296 fwrite($message_log, sprintf("\n\r%s:- %s\n", date("D M j G:i:s T Y"),
297 $curl
298 ));
299 $success = FALSE;
300 if ($response = curl_exec($curl)) {
301 fwrite($message_log, sprintf("\n\r%s:- %s\n", date("D M j G:i:s T Y"),
302 $response
303 ));
304 curl_close($curl);
305
306 // Assign the returned XML values to variables
307 $valid = CRM_Core_Payment_PaymentExpressUtils::_xmlAttribute($response, 'valid');
308 $success = CRM_Core_Payment_PaymentExpressUtils::_xmlElement($response, 'Success');
309 $txnId = CRM_Core_Payment_PaymentExpressUtils::_xmlElement($response, 'TxnId');
310 $responseText = CRM_Core_Payment_PaymentExpressUtils::_xmlElement($response, 'ResponseText');
311 $authCode = CRM_Core_Payment_PaymentExpressUtils::_xmlElement($response, 'AuthCode');
312 $DPStxnRef = CRM_Core_Payment_PaymentExpressUtils::_xmlElement($response, 'DpsTxnRef');
313 $qfKey = CRM_Core_Payment_PaymentExpressUtils::_xmlElement($response, "TxnData1");
314 $privateData = CRM_Core_Payment_PaymentExpressUtils::_xmlElement($response, "TxnData2");
2aa397bc 315 list($component, $paymentProcessorID,) = explode(',', CRM_Core_Payment_PaymentExpressUtils::_xmlElement($response, "TxnData3"));
6a488035
TO
316 $amount = CRM_Core_Payment_PaymentExpressUtils::_xmlElement($response, "AmountSettlement");
317 $merchantReference = CRM_Core_Payment_PaymentExpressUtils::_xmlElement($response, "MerchantReference");
318 }
319 else {
320 // calling DPS failed
321 CRM_Core_Error::fatal(ts('Unable to establish connection to the payment gateway to verify transaction response.'));
322 exit;
323 }
324 }
325 elseif ($dps_method == "pxaccess") {
326
2aa397bc 327 require_once 'PaymentExpress/pxaccess.inc.php';
6a488035
TO
328 global $pxaccess;
329 $pxaccess = new PxAccess($dps_url, $dps_user, $dps_key, $mac_key);
330 #getResponse method in PxAccess object returns PxPayResponse object
331 #which encapsulates all the response data
332 $rsp = $pxaccess->getResponse($rawPostData);
333
334 $qfKey = $rsp->getTxnData1();
335 $privateData = $rsp->getTxnData2();
2aa397bc 336 list($component, $paymentProcessorID) = explode(',', $rsp->getTxnData3());
6a488035
TO
337 $success = $rsp->getSuccess();
338 $authCode = $rsp->getAuthCode();
339 $DPStxnRef = $rsp->getDpsTxnRef();
340 $amount = $rsp->getAmountSettlement();
341 $MerchantReference = $rsp->getMerchantReference();
342 }
343
344 $privateData = $privateData ? self::stringToArray($privateData) : '';
345
346 // Record the current count in array, before we start adding things (for later checks)
347 $countPrivateData = count($privateData);
348
349 // Private Data consists of : a=contactID, b=contributionID,c=contributionTypeID,d=invoiceID,e=membershipID,f=participantID,g=eventID
350 $privateData['contactID'] = $privateData['a'];
351 $privateData['contributionID'] = $privateData['b'];
352 $privateData['contributionTypeID'] = $privateData['c'];
353 $privateData['invoiceID'] = $privateData['d'];
354
355 if ($component == "event") {
356 $privateData['participantID'] = $privateData['f'];
357 $privateData['eventID'] = $privateData['g'];
358 }
359 elseif ($component == "contribute") {
360
361 if ($countPrivateData == 5) {
362 $privateData["membershipID"] = $privateData['e'];
363 }
364 }
365
366 $transactionReference = $authCode . "-" . $DPStxnRef;
367
368 list($mode, $component, $duplicateTransaction) = self::getContext($privateData, $transactionReference);
369 $mode = $mode ? 'test' : 'live';
370
6a488035
TO
371 $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($paymentProcessorID,
372 $mode
373 );
374
375 $ipn = self::singleton($mode, $component, $paymentProcessor);
376
6a488035
TO
377 //Check status and take appropriate action
378
379 if ($success == 1) {
380 if ($duplicateTransaction == 0) {
381 $ipn->newOrderNotify($success, $privateData, $component, $amount, $transactionReference);
382 }
383
384 if ($component == "event") {
385 $finalURL = CRM_Utils_System::url('civicrm/event/register',
386 "_qf_ThankYou_display=1&qfKey=$qfKey",
387 FALSE, NULL, FALSE
388 );
389 }
390 elseif ($component == "contribute") {
391 $finalURL = CRM_Utils_System::url('civicrm/contribute/transact',
392 "_qf_ThankYou_display=1&qfKey=$qfKey",
393 FALSE, NULL, FALSE
394 );
395 }
396
397 CRM_Utils_System::redirect($finalURL);
398 }
399 else {
400
401 if ($component == "event") {
402 $finalURL = CRM_Utils_System::url('civicrm/event/confirm',
403 "reset=1&cc=fail&participantId=$privateData[participantID]",
404 FALSE, NULL, FALSE
405 );
406 }
407 elseif ($component == "contribute") {
408 $finalURL = CRM_Utils_System::url('civicrm/contribute/transact',
409 "_qf_Main_display=1&cancel=1&qfKey=$qfKey",
410 FALSE, NULL, FALSE
411 );
412 }
413
414 CRM_Utils_System::redirect($finalURL);
415 }
416 }
417
418 /**
419 * Converts the comma separated name-value pairs in <TxnData2>
420 * to an array of values.
421 */
00be9182 422 public static function stringToArray($str) {
6a488035
TO
423 $vars = $labels = array();
424 $labels = explode(',', $str);
425 foreach ($labels as $label) {
426 $terms = explode('=', $label);
427 $vars[$terms[0]] = $terms[1];
428 }
429 return $vars;
430 }
431}