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