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