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