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