INFRA-132 - Drupal.WhiteSpace.ScopeIndent.Incorrect
[civicrm-core.git] / CRM / Core / Payment / Realex.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 +--------------------------------------------------------------------+
d25dd0ee 24 */
6a488035
TO
25
26
27/*
d424ffde
CW
28 * Copyright (C) 2009
29 * Licensed to CiviCRM under the Academic Free License version 3.0.
30 *
31 * Written and contributed by Kirkdesigns (http://www.kirkdesigns.co.uk)
32 */
6a488035
TO
33
34/**
35 *
36 * @package CRM
37 * @author Tom Kirkpatrick <tkp@kirkdesigns.co.uk>
38 * $Id$
39 */
40class CRM_Core_Payment_Realex extends CRM_Core_Payment {
7da04cde 41 const AUTH_APPROVED = '00';
6a488035
TO
42
43 protected $_mode = NULL;
44
45 protected $_params = array();
46
47 /**
48 * We only need one instance of this object. So we use the singleton
49 * pattern and cache the instance in this variable
50 *
51 * @var object
6a488035
TO
52 */
53 static private $_singleton = NULL;
54
55 /**
56 * Constructor
57 *
6a0b768e
TO
58 * @param string $mode
59 * The mode of operation: live or test.
6a488035 60 *
77b97be7
EM
61 * @param $paymentProcessor
62 *
63 * @return \CRM_Core_Payment_Realex
6a488035 64 */
00be9182 65 public function __construct($mode, &$paymentProcessor) {
6a488035
TO
66 $this->_mode = $mode;
67 $this->_paymentProcessor = $paymentProcessor;
68 $this->_processorName = ts('Realex');
69
70 $this->_setParam('merchant_ref', $paymentProcessor['user_name']);
71 $this->_setParam('secret', $paymentProcessor['password']);
72 $this->_setParam('account', $paymentProcessor['subject']);
73
74 $this->_setParam('emailCustomer', 'TRUE');
75 srand(time());
76 $this->_setParam('sequence', rand(1, 1000));
77 }
78
6c786a9b 79 /**
c490a46a 80 * @param array $params
6c786a9b
EM
81 *
82 * @throws Exception
83 */
00be9182 84 public function setExpressCheckOut(&$params) {
6a488035
TO
85 CRM_Core_Error::fatal(ts('This function is not implemented'));
86 }
87
6c786a9b
EM
88 /**
89 * @param $token
90 *
91 * @throws Exception
92 */
00be9182 93 public function getExpressCheckoutDetails($token) {
6a488035
TO
94 CRM_Core_Error::fatal(ts('This function is not implemented'));
95 }
96
6c786a9b 97 /**
c490a46a 98 * @param array $params
6c786a9b
EM
99 *
100 * @throws Exception
101 */
00be9182 102 public function doExpressCheckout(&$params) {
6a488035
TO
103 CRM_Core_Error::fatal(ts('This function is not implemented'));
104 }
105
6c786a9b 106 /**
c490a46a 107 * @param array $params
6c786a9b
EM
108 *
109 * @throws Exception
110 */
00be9182 111 public function doTransferCheckout(&$params) {
6a488035
TO
112 CRM_Core_Error::fatal(ts('This function is not implemented'));
113 }
114
115 /**
116 * Submit a payment using Advanced Integration Method
117 *
6a0b768e
TO
118 * @param array $params
119 * Assoc array of input parameters for this transaction.
6a488035 120 *
a6c01b45
CW
121 * @return array
122 * the result in a nice formatted array (or an error object)
6a488035 123 */
00be9182 124 public function doDirectPayment(&$params) {
6a488035
TO
125
126 if (!defined('CURLOPT_SSLCERT')) {
127 return self::error(9001, ts('RealAuth requires curl with SSL support'));
128 }
129
130 $result = $this->setRealexFields($params);
131
132 if ($result !== TRUE) {
133 return $result;
134 }
135
136 /**********************************************************
137 * Check to see if we have a duplicate before we send
138 **********************************************************/
139 if ($this->_checkDupe($this->_getParam('order_id'))) {
140 return self::error(9004, ts('It appears that this transaction is a duplicate. Have you already submitted the form once? If so there may have been a connection problem. Check your email for a receipt from Authorize.net. If you do not receive a receipt within 2 hours you can try your transaction again. If you continue to have problems please contact the site administrator.'));
141 }
142
143 // Create sha1 hash for request
353ffa53 144 $hashme = "{$this->_getParam('timestamp')}.{$this->_getParam('merchant_ref')}.{$this->_getParam('order_id')}.{$this->_getParam('amount')}.{$this->_getParam('currency')}.{$this->_getParam('card_number')}";
6a488035 145 $sha1hash = sha1($hashme);
353ffa53 146 $hashme = "$sha1hash.{$this->_getParam('secret')}";
6a488035
TO
147 $sha1hash = sha1($hashme);
148
6a488035
TO
149 // Generate the request xml that is send to Realex Payments.
150 $request_xml = "<request type='auth' timestamp='{$this->_getParam('timestamp')}'>
151 <merchantid>{$this->_getParam('merchant_ref')}</merchantid>
152 <account>{$this->_getParam('account')}</account>
153 <orderid>{$this->_getParam('order_id')}</orderid>
154 <amount currency='{$this->_getParam('currency')}'>{$this->_getParam('amount')}</amount>
155 <card>
156 <number>{$this->_getParam('card_number')}</number>
157 <expdate>{$this->_getParam('exp_date')}</expdate>
158 <type>{$this->_getParam('card_type')}</type>
159 <chname>{$this->_getParam('card_name')}</chname>
160 <issueno>{$this->_getParam('issue_number')}</issueno>
161 <cvn>
162 <number>{$this->_getParam('cvn')}</number>
163 <presind>1</presind>
164 </cvn>
165 </card>
166 <autosettle flag='1'/>
167 <sha1hash>$sha1hash</sha1hash>
168 <comments>
169 <comment id='1'>{$this->_getParam('comments')}</comment>
170 </comments>
171 <tssinfo>
172 <varref>{$this->_getParam('varref')}</varref>
173 </tssinfo>
174 </request>";
175
176 /**********************************************************
177 * Send to the payment processor using cURL
178 **********************************************************/
179
180 $submit = curl_init($this->_paymentProcessor['url_site']);
181
182 if (!$submit) {
183 return self::error(9002, ts('Could not initiate connection to payment gateway'));
184 }
185
186 curl_setopt($submit, CURLOPT_HTTPHEADER, array('SOAPAction: ""'));
187 curl_setopt($submit, CURLOPT_RETURNTRANSFER, 1);
188 curl_setopt($submit, CURLOPT_TIMEOUT, 60);
189 curl_setopt($submit, CURLOPT_SSL_VERIFYPEER, CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'verifySSL'));
190 curl_setopt($submit, CURLOPT_HEADER, 0);
191
192 // take caching out of the picture
193 curl_setopt($submit, CURLOPT_FORBID_REUSE, 1);
194 curl_setopt($submit, CURLOPT_FRESH_CONNECT, 1);
195
196 // Apply the XML to our curl call
197 curl_setopt($submit, CURLOPT_POST, 1);
198 curl_setopt($submit, CURLOPT_POSTFIELDS, $request_xml);
199
200 $response_xml = curl_exec($submit);
201
202 if (!$response_xml) {
203 return self::error(curl_errno($submit), curl_error($submit));
204 }
205
206 curl_close($submit);
207
208 // Tidy up the responce xml
209 $response_xml = preg_replace("/[\s\t]/", " ", $response_xml);
210 $response_xml = preg_replace("/[\n\r]/", "", $response_xml);
211
212 // Parse the response xml
213 $xml_parser = xml_parser_create();
214 if (!xml_parse($xml_parser, $response_xml)) {
215 return self::error(9003, 'XML Error');
216 }
217
218 $response = $this->xml_parse_into_assoc($response_xml);
219 $response = $response['#return']['RESPONSE'];
220
221 // Log the Realex response for debugging
222 // CRM_Core_Error::debug_var('REALEX --------- Response from Realex: ', $response, TRUE);
223
224 // Return an error if authentication was not successful
225 if ($response['RESULT'] !== self::AUTH_APPROVED) {
226 return self::error($response['RESULT'], ' ' . $response['MESSAGE']);
227 }
228
229 // Check the response hash
353ffa53 230 $hashme = "{$this->_getParam('timestamp')}.{$this->_getParam('merchant_ref')}.{$this->_getParam('order_id')}.{$response['RESULT']}.{$response['MESSAGE']}.{$response['PASREF']}.{$response['AUTHCODE']}";
6a488035 231 $sha1hash = sha1($hashme);
353ffa53 232 $hashme = "$sha1hash.{$this->_getParam('secret')}";
6a488035
TO
233 $sha1hash = sha1($hashme);
234
235 if ($response['SHA1HASH'] != $sha1hash) {
236 // FIXME: Need to actually check this - I couldn't get the
237 // hashes to match so I'm commenting out for now'
238 // return self::error( 9001, "Hash error, please report this to the webmaster" );
239 }
240
241 // FIXME: We are using the trxn_result_code column to store all these extra details since there
242 // seems to be nowhere else to put them. This is THE WRONG THING TO DO!
243 $extras = array(
244 'authcode' => $response['AUTHCODE'],
245 'batch_id' => $response['BATCHID'],
246 'message' => $response['MESSAGE'],
247 'trxn_result_code' => $response['RESULT'],
248 );
249
250 $params['trxn_id'] = $response['PASREF'];
251 $params['trxn_result_code'] = serialize($extras);
252 $params['currencyID'] = $this->_getParam('currency');
253 $params['gross_amount'] = $this->_getParam('amount');
254 $params['fee_amount'] = 0;
255
256 return $params;
257 }
258
259 /**
260 * Helper function to convert XML string to multi-dimension array.
261 *
5a4f6742 262 * @param string $xml
6a488035
TO
263 * an XML string.
264 *
a6c01b45
CW
265 * @return array
266 * An array of the result with following keys:
6a488035 267 */
00be9182 268 public function xml_parse_into_assoc($xml) {
6a488035
TO
269 $input = array();
270 $result = array();
271
272 $result['#error'] = FALSE;
273 $result['#return'] = NULL;
274
275 $xmlparser = xml_parser_create();
276 $ret = xml_parse_into_struct($xmlparser, $xml, $input);
277
278 xml_parser_free($xmlparser);
279
280 if (empty($input)) {
281 $result['#return'] = $xml;
282 }
283 else {
284 if ($ret > 0) {
285 $result['#return'] = $this->_xml_parse($input);
286 }
287 else {
288 $result['#error'] = ts('Error parsing XML result - error code = %1 at line %2 char %3',
289 array(
290 1 => xml_get_error_code($xmlparser),
291 2 => xml_get_current_line_number($xmlparser),
21dfd5f5 292 3 => xml_get_current_column_number($xmlparser),
6a488035
TO
293 )
294 );
295 }
296 }
297 return $result;
298 }
299
6c786a9b 300 /**
5a4f6742 301 * Private helper for xml_parse_into_assoc, to recusively parsing the result
6c786a9b
EM
302 * @param $input
303 * @param int $depth
304 *
305 * @return array
306 */
00be9182 307 public function _xml_parse($input, $depth = 1) {
6a488035
TO
308 $output = array();
309 $children = array();
310
311 foreach ($input as $data) {
312 if ($data['level'] == $depth) {
313 switch ($data['type']) {
314 case 'complete':
315 $output[$data['tag']] = isset($data['value']) ? $data['value'] : '';
316 break;
317
318 case 'open':
319 $children = array();
320 break;
321
322 case 'close':
323 $output[$data['tag']] = $this->_xml_parse($children, $depth + 1);
324 break;
325 }
326 }
327 else {
328 $children[] = $data;
329 }
330 }
331 return $output;
332 }
333
334 /**
335 * Format the params from the form ready for sending to Realex. Also perform some validation
336 */
00be9182 337 public function setRealexFields(&$params) {
2aa397bc 338 if ((int) $params['amount'] <= 0) {
6a488035
TO
339 return self::error(9001, ts('Amount must be positive'));
340 }
341
342 // format amount to be in smallest possible units
343 //list($bills, $pennies) = explode('.', $params['amount']);
344 $this->_setParam('amount', 100 * $params['amount']);
345
346 switch (strtolower($params['credit_card_type'])) {
347 case 'mastercard':
348 $this->_setParam('card_type', 'MC');
349 $this->_setParam('requiresIssueNumber', FALSE);
350 break;
351
352 case 'visa':
353 $this->_setParam('card_type', 'VISA');
354 $this->_setParam('requiresIssueNumber', FALSE);
355 break;
356
357 case 'amex':
358 $this->_setParam('card_type', 'AMEX');
359 $this->_setParam('requiresIssueNumber', FALSE);
360 break;
361
362 case 'laser':
363 $this->_setParam('card_type', 'LASER');
364 $this->_setParam('requiresIssueNumber', FALSE);
365 break;
366
367 case 'maestro':
368 case 'switch':
369 case 'maestro/switch':
370 case 'solo':
371 $this->_setParam('card_type', 'SWITCH');
372 $this->_setParam('requiresIssueNumber', TRUE);
373 break;
374
375 default:
376 return self::error(9001, ts('Credit card type not supported by Realex:') . ' ' . $params['credit_card_type']);
377 }
378
379 // get the card holder name - cater cor customized billing forms
380 if (isset($params['cardholder_name'])) {
381 $credit_card_name = $params['cardholder_name'];
382 }
383 else {
384 $credit_card_name = $params['first_name'] . " ";
385 if (!empty($params['middle_name'])) {
386 $credit_card_name .= $params['middle_name'] . " ";
387 }
388 $credit_card_name .= $params['last_name'];
389 }
390
391 $this->_setParam('card_name', $credit_card_name);
392 $this->_setParam('card_number', str_replace(' ', '', $params['credit_card_number']));
393 $this->_setParam('cvn', $params['cvv2']);
394 $this->_setParam('country', $params['country']);
395 $this->_setParam('post_code', $params['postal_code']);
396 $this->_setParam('order_id', $params['invoiceID']);
397 $params['issue_number'] = (isset($params['issue_number']) ? $params['issue_number'] : '');
398 $this->_setParam('issue_number', $params['issue_number']);
399 $this->_setParam('varref', $params['contributionType_name']);
400 $comment = $params['description'] . ' (page id:' . $params['contributionPageID'] . ')';
401 $this->_setParam('comments', $comment);
402 //$this->_setParam('currency', $params['currencyID']);
403
404 // set the currency to the default which can be overrided.
405 $config = CRM_Core_Config::singleton();
406 $this->_setParam('currency', $config->defaultCurrency);
407
408 // Format the expiry date to MMYY
2aa397bc 409 $expmonth = (string) $params['month'];
6a488035 410 $expmonth = (strlen($expmonth) === 1) ? '0' . $expmonth : $expmonth;
353ffa53 411 $expyear = substr((string) $params['year'], 2, 2);
6a488035
TO
412 $this->_setParam('exp_date', $expmonth . $expyear);
413
414 if (isset($params['credit_card_start_date']) && (strlen($params['credit_card_start_date']['M']) !== 0) &&
415 (strlen($params['credit_card_start_date']['Y']) !== 0)
416 ) {
2aa397bc 417 $startmonth = (string) $params['credit_card_start_date']['M'];
6a488035 418 $startmonth = (strlen($startmonth) === 1) ? '0' . $startmonth : $startmonth;
353ffa53 419 $startyear = substr((string) $params['credit_card_start_date']['Y'], 2, 2);
6a488035
TO
420 $this->_setParam('start_date', $startmonth . $startyear);
421 }
422
423 // Create timestamp
424 $timestamp = strftime("%Y%m%d%H%M%S");
425 $this->_setParam('timestamp', $timestamp);
426
427 return TRUE;
428 }
429
430 /**
431 * Checks to see if invoice_id already exists in db
432 *
6a0b768e
TO
433 * @param int $invoiceId
434 * The ID to check.
6a488035 435 *
a6c01b45
CW
436 * @return bool
437 * True if ID exists, else false
6a488035 438 */
00be9182 439 public function _checkDupe($invoiceId) {
6a488035
TO
440 $contribution = new CRM_Contribute_DAO_Contribution();
441 $contribution->invoice_id = $invoiceId;
442 return $contribution->find();
443 }
444
445 /**
446 * Get the value of a field if set
447 *
6a0b768e
TO
448 * @param string $field
449 * The field.
6a488035 450 *
72b3a70c
CW
451 * @return mixed
452 * value of the field, or empty string if the field is
16b10e64 453 * not set
6a488035 454 */
00be9182 455 public function _getParam($field) {
6a488035
TO
456 if (isset($this->_params[$field])) {
457 return $this->_params[$field];
458 }
459 else {
460 return '';
461 }
462 }
463
464 /**
465 * Set a field to the specified value. Value must be a scalar (int,
466 * float, string, or boolean)
467 *
468 * @param string $field
469 * @param mixed $value
470 *
a6c01b45
CW
471 * @return bool
472 * false if value is not a scalar, true if successful
6a488035 473 */
00be9182 474 public function _setParam($field, $value) {
6a488035
TO
475 if (!is_scalar($value)) {
476 return FALSE;
477 }
478 else {
479 $this->_params[$field] = $value;
480 }
481 }
482
6c786a9b
EM
483 /**
484 * @param null $errorCode
485 * @param null $errorMessage
486 *
487 * @return object
488 */
00be9182 489 public function &error($errorCode = NULL, $errorMessage = NULL) {
6a488035
TO
490 $e = CRM_Core_Error::singleton();
491
492 if ($errorCode) {
493 if ($errorCode == '101' || $errorCode == '102') {
494 $display_error = ts('Card declined by bank. Please try with a different card.');
495 }
496 elseif ($errorCode == '103') {
497 $display_error = ts('Card reported lost or stolen. This incident will be reported.');
498 }
499 elseif ($errorCode == '501') {
500 $display_error = ts("It appears that this transaction is a duplicate. Have you already submitted the form once? If so there may have been a connection problem. Check your email for a receipt for this transaction. If you do not receive a receipt within 2 hours you can try your transaction again. If you continue to have problems please contact the site administrator.");
501 }
502 elseif ($errorCode == '509') {
503 $display_error = $errorMessage;
504 }
505 else {
506 $display_error = ts('We were unable to process your payment at this time. Please try again later.');
507 }
508 $e->push($errorCode, 0, NULL, $display_error);
509 }
510 else {
511 $e->push(9001, 0, NULL, ts('We were unable to process your payment at this time. Please try again later.'));
512 }
513 return $e;
514 }
515
516 /**
517 * This function checks to see if we have the right config values
518 *
a6c01b45
CW
519 * @return string
520 * the error message if any
6a488035 521 */
00be9182 522 public function checkConfig() {
6a488035
TO
523 $error = array();
524 if (empty($this->_paymentProcessor['user_name'])) {
525 $error[] = ts('Merchant ID is not set for this payment processor');
526 }
527
528 if (empty($this->_paymentProcessor['password'])) {
529 $error[] = ts('Secret is not set for this payment processor');
530 }
531
532 if (!empty($error)) {
533 return implode('<p>', $error);
534 }
535 else {
536 return NULL;
537 }
538 }
96025800 539
6a488035 540}