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