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