| 1 | <?php |
| 2 | /* |
| 3 | +----------------------------------------------------------------------------+ |
| 4 | | Elavon (Nova) Virtual Merchant Core Payment Module for CiviCRM version 4.5 | |
| 5 | +----------------------------------------------------------------------------+ |
| 6 | | Licensed to CiviCRM under the Academic Free License version 3.0 | |
| 7 | | | |
| 8 | | Written & Contributed by Eileen McNaughton - Nov March 2008 | |
| 9 | +----------------------------------------------------------------------------+ |
| 10 | */ |
| 11 | |
| 12 | /** |
| 13 | ----------------------------------------------------------------------------------------------- |
| 14 | The basic functionality of this processor is that variables from the $params object are transformed |
| 15 | into xml. The xml is submitted to the processor's https site |
| 16 | using curl and the response is translated back into an array using the processor's function. |
| 17 | |
| 18 | If an array ($params) is returned to the calling function the values from |
| 19 | the array are merged into the calling functions array. |
| 20 | |
| 21 | If an result of class error is returned it is treated as a failure. No error denotes a success. Be |
| 22 | WARY of this when coding |
| 23 | |
| 24 | ----------------------------------------------------------------------------------------------- |
| 25 | **/ |
| 26 | class CRM_Core_Payment_Elavon extends CRM_Core_Payment { |
| 27 | // (not used, implicit in the API, might need to convert?) |
| 28 | CONST |
| 29 | CHARSET = 'UFT-8'; |
| 30 | |
| 31 | /** |
| 32 | * We only need one instance of this object. So we use the singleton |
| 33 | * pattern and cache the instance in this variable |
| 34 | * |
| 35 | * @var object |
| 36 | * @static |
| 37 | */ |
| 38 | static private $_singleton = NULL; |
| 39 | |
| 40 | /********************************************************** |
| 41 | * Constructor |
| 42 | * |
| 43 | * @param string $mode the mode of operation: live or test |
| 44 | * |
| 45 | * @param $paymentProcessor |
| 46 | * |
| 47 | * @return \CRM_Core_Payment_Elavon ******************************************************* |
| 48 | */ |
| 49 | function __construct($mode, &$paymentProcessor) { |
| 50 | // live or test |
| 51 | $this->_mode = $mode; |
| 52 | $this->_paymentProcessor = $paymentProcessor; |
| 53 | $this->_processorName = ts('Elavon'); |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * Singleton function used to manage this object |
| 58 | * |
| 59 | * @param string $mode the mode of operation: live or test |
| 60 | * |
| 61 | * @param object $paymentProcessor |
| 62 | * |
| 63 | * @return object |
| 64 | * @static |
| 65 | */ |
| 66 | static function &singleton($mode, &$paymentProcessor) { |
| 67 | $processorName = $paymentProcessor['name']; |
| 68 | if (self::$_singleton[$processorName] === NULL) { |
| 69 | self::$_singleton[$processorName] = new CRM_Core_Payment_Elavon($mode, $paymentProcessor); |
| 70 | } |
| 71 | return self::$_singleton[$processorName]; |
| 72 | } |
| 73 | |
| 74 | /********************************************************** |
| 75 | * This function is set up and put here to make the mapping of fields |
| 76 | * from the params object as visually clear as possible for easy editing |
| 77 | * |
| 78 | * Comment out irrelevant fields |
| 79 | **********************************************************/ |
| 80 | function mapProcessorFieldstoParams($params) { |
| 81 | |
| 82 | /********************************************************** |
| 83 | * compile array |
| 84 | * Payment Processor field name fields from $params array |
| 85 | **********************************************************/ |
| 86 | // credit card name |
| 87 | $requestFields['ssl_first_name'] = $params['billing_first_name']; |
| 88 | // credit card name |
| 89 | //$requestFields['ssl_middle_name'] = $params['billing_middle_name']; |
| 90 | // credit card name |
| 91 | $requestFields['ssl_last_name'] = $params['billing_last_name']; |
| 92 | // contact name |
| 93 | $requestFields['ssl_ship_to_first_name'] = $params['first_name']; |
| 94 | // contact name |
| 95 | $requestFields['ssl_ship_to_last_name'] = $params['last_name']; |
| 96 | $requestFields['ssl_card_number'] = $params['credit_card_number']; |
| 97 | $requestFields['ssl_amount'] = trim($params['amount']); |
| 98 | $requestFields['ssl_exp_date'] = sprintf('%02d', (int) $params['month']) . substr($params['year'], 2, 2);; |
| 99 | $requestFields['ssl_cvv2cvc2'] = $params['cvv2']; |
| 100 | // CVV field passed to processor |
| 101 | $requestFields['ssl_cvv2cvc2_indicator'] = "1"; |
| 102 | $requestFields['ssl_avs_address'] = $params['street_address']; |
| 103 | $requestFields['ssl_city'] = $params['city']; |
| 104 | $requestFields['ssl_state'] = $params['state_province']; |
| 105 | $requestFields['ssl_avs_zip'] = $params['postal_code']; |
| 106 | $requestFields['ssl_country'] = $params['country']; |
| 107 | $requestFields['ssl_email'] = $params['email']; |
| 108 | // 32 character string |
| 109 | $requestFields['ssl_invoice_number'] = $params['invoiceID']; |
| 110 | $requestFields['ssl_transaction_type'] = "CCSALE"; |
| 111 | $requestFields['ssl_description'] = empty($params['description']) ? "backoffice payment" : $params['description']; |
| 112 | $requestFields['ssl_customer_number'] = substr($params['credit_card_number'], -4); |
| 113 | // Added two lines below to allow commercial cards to go through as per page 15 of Elavon developer guide |
| 114 | $requestFields['ssl_customer_code'] = '1111'; |
| 115 | $requestFields['ssl_salestax'] = 0.0; |
| 116 | |
| 117 | |
| 118 | /************************************************************************************ |
| 119 | * Fields available from civiCRM not implemented for Elavon |
| 120 | * |
| 121 | * $params['qfKey']; |
| 122 | * $params['amount_other']; |
| 123 | * $params['ip_address']; |
| 124 | * $params['contributionType_name' ]; |
| 125 | * $params['contributionPageID']; |
| 126 | * $params['contributionType_accounting_code']; |
| 127 | * $params['amount_level']; |
| 128 | * $params['credit_card_type']; |
| 129 | ************************************************************************************/ |
| 130 | return $requestFields; |
| 131 | } |
| 132 | |
| 133 | /********************************************************** |
| 134 | * This function sends request and receives response from |
| 135 | * the processor |
| 136 | **********************************************************/ |
| 137 | function doDirectPayment(&$params) { |
| 138 | if (isset($params['is_recur']) && $params['is_recur'] == TRUE) { |
| 139 | CRM_Core_Error::fatal(ts('Elavon - recurring payments not implemented')); |
| 140 | } |
| 141 | |
| 142 | if (!defined('CURLOPT_SSLCERT')) { |
| 143 | CRM_Core_Error::fatal(ts('Elavon / Nova Virtual Merchant Gateway requires curl with SSL support')); |
| 144 | } |
| 145 | |
| 146 | /* |
| 147 | *Create the array of variables to be sent to the processor from the $params array |
| 148 | * passed into this function |
| 149 | */ |
| 150 | |
| 151 | $requestFields = self::mapProcessorFieldstoParams($params); |
| 152 | |
| 153 | /* |
| 154 | * define variables for connecting with the gateway |
| 155 | */ |
| 156 | |
| 157 | $requestFields['ssl_merchant_id'] = $this->_paymentProcessor['user_name']; |
| 158 | $requestFields['ssl_user_id'] = $this->_paymentProcessor['password']; |
| 159 | $requestFields['ssl_pin'] = $this->_paymentProcessor['signature']; |
| 160 | $host = $this->_paymentProcessor['url_site']; |
| 161 | |
| 162 | if ($this->_mode == "test") { |
| 163 | $requestFields['ssl_test_mode'] = "TRUE"; |
| 164 | } |
| 165 | |
| 166 | // Allow further manipulation of the arguments via custom hooks .. |
| 167 | CRM_Utils_Hook::alterPaymentProcessorParams($this, $params, $requestFields); |
| 168 | |
| 169 | /********************************************************** |
| 170 | * Check to see if we have a duplicate before we send |
| 171 | **********************************************************/ |
| 172 | if ($this->_checkDupe($params['invoiceID'])) { |
| 173 | return self::errorExit(9003, '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. 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.'); |
| 174 | } |
| 175 | |
| 176 | /********************************************************** |
| 177 | * Convert to XML using function below |
| 178 | **********************************************************/ |
| 179 | $xml = self::buildXML($requestFields); |
| 180 | |
| 181 | /********************************************************** |
| 182 | * Send to the payment processor using cURL |
| 183 | **********************************************************/ |
| 184 | |
| 185 | $chHost = $host . '?xmldata=' . $xml; |
| 186 | |
| 187 | $ch = curl_init($chHost); |
| 188 | if (!$ch) { |
| 189 | return self::errorExit(9004, 'Could not initiate connection to payment gateway'); |
| 190 | } |
| 191 | |
| 192 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'verifySSL') ? 2 : 0); |
| 193 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'verifySSL')); |
| 194 | // return the result on success, FALSE on failure |
| 195 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); |
| 196 | curl_setopt($ch, CURLOPT_TIMEOUT, 36000); |
| 197 | // set this for debugging -look for output in apache error log |
| 198 | //curl_setopt ($ch,CURLOPT_VERBOSE,1 ); |
| 199 | // ensures any Location headers are followed |
| 200 | if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') { |
| 201 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); |
| 202 | } |
| 203 | |
| 204 | /********************************************************** |
| 205 | * Send the data out over the wire |
| 206 | **********************************************************/ |
| 207 | $responseData = curl_exec($ch); |
| 208 | |
| 209 | /********************************************************** |
| 210 | * See if we had a curl error - if so tell 'em and bail out |
| 211 | * |
| 212 | * NOTE: curl_error does not return a logical value (see its documentation), but |
| 213 | * a string, which is empty when there was no error. |
| 214 | **********************************************************/ |
| 215 | if ((curl_errno($ch) > 0) || (strlen(curl_error($ch)) > 0)) { |
| 216 | curl_close($ch); |
| 217 | $errorNum = curl_errno($ch); |
| 218 | $errorDesc = curl_error($ch); |
| 219 | |
| 220 | // Paranoia - in the unlikley event that 'curl' errno fails |
| 221 | if ($errorNum == 0) |
| 222 | $errorNum = 9005; |
| 223 | |
| 224 | // Paranoia - in the unlikley event that 'curl' error fails |
| 225 | if (strlen($errorDesc) == 0) |
| 226 | $errorDesc = "Connection to payment gateway failed"; |
| 227 | if ($errorNum = 60) { |
| 228 | return self::errorExit($errorNum, "Curl error - " . $errorDesc . " Try this link for more information http://curl.haxx.se/docs/sslcerts.html"); |
| 229 | } |
| 230 | |
| 231 | return self::errorExit($errorNum, "Curl error - " . $errorDesc . " your key is located at " . $key . " the url is " . $host . " xml is " . $requestxml . " processor response = " . $processorResponse); |
| 232 | } |
| 233 | |
| 234 | /********************************************************** |
| 235 | * If null data returned - tell 'em and bail out |
| 236 | * |
| 237 | * NOTE: You will not necessarily get a string back, if the request failed for |
| 238 | * any reason, the return value will be the boolean false. |
| 239 | **********************************************************/ |
| 240 | if (($responseData === FALSE) || (strlen($responseData) == 0)) { |
| 241 | curl_close($ch); |
| 242 | return self::errorExit(9006, "Error: Connection to payment gateway failed - no data returned."); |
| 243 | } |
| 244 | |
| 245 | /********************************************************** |
| 246 | // If gateway returned no data - tell 'em and bail out |
| 247 | **********************************************************/ |
| 248 | if (empty($responseData)) { |
| 249 | curl_close($ch); |
| 250 | return self::errorExit(9007, "Error: No data returned from payment gateway."); |
| 251 | } |
| 252 | |
| 253 | /********************************************************** |
| 254 | // Success so far - close the curl and check the data |
| 255 | **********************************************************/ |
| 256 | curl_close($ch); |
| 257 | |
| 258 | /********************************************************** |
| 259 | * Payment successfully sent to gateway - process the response now |
| 260 | **********************************************************/ |
| 261 | |
| 262 | $processorResponse = self::decodeXMLResponse($responseData); |
| 263 | /*success in test mode returns response "APPROVED" |
| 264 | * test mode always returns trxn_id = 0 |
| 265 | * fix for CRM-2566 |
| 266 | **********************************************************/ |
| 267 | |
| 268 | |
| 269 | if ($processorResponse['errorCode']) { |
| 270 | return self::errorExit(9010, "Error: [" . $processorResponse['errorCode'] . " " . $processorResponse['errorName'] . " " . $processorResponse['errorMessage'] . "] - from payment processor"); |
| 271 | } |
| 272 | if ($processorResponse['ssl_result_message'] == "APPROVED") { |
| 273 | if ($this->_mode == 'test') { |
| 274 | $query = "SELECT MAX(trxn_id) FROM civicrm_contribution WHERE trxn_id LIKE 'test%'"; |
| 275 | $p = array(); |
| 276 | $trxn_id = strval(CRM_Core_Dao::singleValueQuery($query, $p)); |
| 277 | $trxn_id = str_replace('test', '', $trxn_id); |
| 278 | $trxn_id = intval($trxn_id) + 1; |
| 279 | $params['trxn_id'] = sprintf('test%08d', $trxn_id); |
| 280 | return $params; |
| 281 | } |
| 282 | else { |
| 283 | return self::errorExit(9099, "Error: [approval code related to test transaction but mode was " . $this->_mode); |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | // transaction failed, print the reason |
| 288 | if ($processorResponse['ssl_result_message'] != "APPROVAL") { |
| 289 | return self::errorExit(9009, "Error: [" . $processorResponse['ssl_result_message'] . " " . $processorResponse['ssl_result'] . "] - from payment processor"); |
| 290 | } |
| 291 | else { |
| 292 | /* |
| 293 | * Success ! |
| 294 | */ |
| 295 | |
| 296 | if ($this->_mode == 'test') {} |
| 297 | else { |
| 298 | // 'trxn_id' is varchar(255) field. returned value is length 37 |
| 299 | $params['trxn_id'] = $processorResponse['ssl_txn_id']; |
| 300 | } |
| 301 | |
| 302 | $params['trxn_result_code'] = $processorResponse['ssl_approval_code'] . "-Cvv2:" . $processorResponse['ssl_cvv2_response'] . "-avs:" . $processorResponse['ssl_avs_response']; |
| 303 | |
| 304 | return $params; |
| 305 | } |
| 306 | } |
| 307 | // end function doDirectPayment |
| 308 | |
| 309 | /** |
| 310 | * Checks to see if invoice_id already exists in db |
| 311 | * |
| 312 | * @param int $invoiceId The ID to check |
| 313 | * |
| 314 | * @return bool True if ID exists, else false |
| 315 | */ |
| 316 | function _checkDupe($invoiceId) { |
| 317 | $contribution = new CRM_Contribute_DAO_Contribution(); |
| 318 | $contribution->invoice_id = $invoiceId; |
| 319 | return $contribution->find(); |
| 320 | } |
| 321 | |
| 322 | /************************************************** |
| 323 | * Produces error message and returns from class |
| 324 | **************************************************/ |
| 325 | function &errorExit($errorCode = NULL, $errorMessage = NULL) { |
| 326 | $e = CRM_Core_Error::singleton(); |
| 327 | if ($errorCode) { |
| 328 | $e->push($errorCode, 0, NULL, $errorMessage); |
| 329 | } |
| 330 | else { |
| 331 | $e->push(9000, 0, NULL, 'Unknown System Error.'); |
| 332 | } |
| 333 | return $e; |
| 334 | } |
| 335 | |
| 336 | /************************************************** |
| 337 | * NOTE: 'doTransferCheckout' not implemented |
| 338 | **************************************************/ |
| 339 | function doTransferCheckout(&$params, $component) { |
| 340 | CRM_Core_Error::fatal(ts('This function is not implemented')); |
| 341 | } |
| 342 | |
| 343 | /******************************************************************************************** |
| 344 | * This public function checks to see if we have the right processor config values set |
| 345 | * |
| 346 | * NOTE: Called by Events and Contribute to check config params are set prior to trying |
| 347 | * register any credit card details |
| 348 | * |
| 349 | * @return null|string |
| 350 | * @internal param string $mode the mode we are operating in (live or test) - not used |
| 351 | * |
| 352 | * returns string $errorMsg if any errors found - null if OK |
| 353 | * |
| 354 | ****************************************************************************************** |
| 355 | */ |
| 356 | // function checkConfig( $mode ) // CiviCRM V1.9 Declaration |
| 357 | // CiviCRM V2.0 Declaration |
| 358 | function checkConfig() { |
| 359 | $errorMsg = array(); |
| 360 | |
| 361 | if (empty($this->_paymentProcessor['user_name'])) { |
| 362 | $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor'); |
| 363 | } |
| 364 | |
| 365 | if (empty($this->_paymentProcessor['url_site'])) { |
| 366 | $errorMsg[] = ' ' . ts('URL is not set for this payment processor'); |
| 367 | } |
| 368 | |
| 369 | if (!empty($errorMsg)) { |
| 370 | return implode('<p>', $errorMsg); |
| 371 | } |
| 372 | else { |
| 373 | return NULL; |
| 374 | } |
| 375 | } |
| 376 | //end check config |
| 377 | /** |
| 378 | * @param $requestFields |
| 379 | * |
| 380 | * @return string |
| 381 | */ |
| 382 | function buildXML($requestFields) { |
| 383 | $xmlFieldLength['ssl_first_name'] = 15; |
| 384 | // credit card name |
| 385 | $xmlFieldLength['ssl_last_name'] = 15; |
| 386 | // contact name |
| 387 | $xmlFieldLength['ssl_ship_to_first_name'] = 15; |
| 388 | // contact name |
| 389 | $xmlFieldLength['ssl_ship_to_last_name'] = 15; |
| 390 | $xmlFieldLength['ssl_card_number'] = 19; |
| 391 | $xmlFieldLength['ssl_amount'] = 13; |
| 392 | $xmlFieldLength['ssl_exp_date'] = 4; |
| 393 | $xmlFieldLength['ssl_cvv2cvc2'] = 4; |
| 394 | $xmlFieldLength['ssl_cvv2cvc2_indicator'] = 1; |
| 395 | $xmlFieldLength['ssl_avs_address'] = 20; |
| 396 | $xmlFieldLength['ssl_city'] = 20; |
| 397 | $xmlFieldLength['ssl_state'] = 30; |
| 398 | $xmlFieldLength['ssl_avs_zip'] = 9; |
| 399 | $xmlFieldLength['ssl_country'] = 50; |
| 400 | $xmlFieldLength['ssl_email'] = 100; |
| 401 | // 32 character string |
| 402 | $xmlFieldLength['ssl_invoice_number'] = 25; |
| 403 | $xmlFieldLength['ssl_transaction_type'] = 20; |
| 404 | $xmlFieldLength['ssl_description'] = 255; |
| 405 | $xmlFieldLength['ssl_merchant_id'] = 15; |
| 406 | $xmlFieldLength['ssl_user_id'] = 15; |
| 407 | $xmlFieldLength['ssl_pin'] = 128; |
| 408 | $xmlFieldLength['ssl_test_mode'] = 5; |
| 409 | $xmlFieldLength['ssl_salestax'] = 10; |
| 410 | $xmlFieldLength['ssl_customer_code'] = 17; |
| 411 | $xmlFieldLength['ssl_customer_number'] = 25; |
| 412 | |
| 413 | $xml = '<txn>'; |
| 414 | foreach ($requestFields as $key => $value) { |
| 415 | $xml .= '<' . $key . '>' . self::tidyStringforXML($value, $xmlFieldLength[$key]) . '</' . $key . '>'; |
| 416 | } |
| 417 | $xml .= '</txn>'; |
| 418 | return $xml; |
| 419 | } |
| 420 | |
| 421 | /** |
| 422 | * @param $value |
| 423 | * @param $fieldlength |
| 424 | * |
| 425 | * @return string |
| 426 | */ |
| 427 | function tidyStringforXML($value, $fieldlength) { |
| 428 | // the xml is posted to a url so must not contain spaces etc. It also needs to be cut off at a certain |
| 429 | // length to match the processor's field length. The cut needs to be made after spaces etc are |
| 430 | // transformed but must not include a partial transformed character e.g. %20 must be in or out not half-way |
| 431 | $xmlString = substr(rawurlencode($value), 0, $fieldlength); |
| 432 | $lastPercent = strrpos($xmlString, '%'); |
| 433 | if ($lastPercent > $fieldlength - 3) { |
| 434 | $xmlString = substr($xmlString, 0, $lastPercent); |
| 435 | } |
| 436 | return $xmlString; |
| 437 | } |
| 438 | |
| 439 | /************************************************************************ |
| 440 | * Simple function to use in place of the 'simplexml_load_string' call. |
| 441 | * |
| 442 | * It returns the NodeValue for a given NodeName |
| 443 | * or returns an empty string. |
| 444 | ************************************************************************/ |
| 445 | function GetNodeValue($NodeName, &$strXML) { |
| 446 | $OpeningNodeName = "<" . $NodeName . ">"; |
| 447 | $ClosingNodeName = "</" . $NodeName . ">"; |
| 448 | |
| 449 | $pos1 = stripos($strXML, $OpeningNodeName); |
| 450 | $pos2 = stripos($strXML, $ClosingNodeName); |
| 451 | |
| 452 | if (($pos1 === FALSE) || ($pos2 === FALSE)) { |
| 453 | |
| 454 | return ""; |
| 455 | |
| 456 | } |
| 457 | |
| 458 | $pos1 += strlen($OpeningNodeName); |
| 459 | $len = $pos2 - $pos1; |
| 460 | |
| 461 | $return = substr($strXML, $pos1, $len); |
| 462 | // check out rtn values for debug |
| 463 | // echo " $NodeName     $return <br>"; |
| 464 | return ($return); |
| 465 | } |
| 466 | |
| 467 | /** |
| 468 | * @param $Xml |
| 469 | * |
| 470 | * @return mixed |
| 471 | */ |
| 472 | function decodeXMLresponse($Xml) { |
| 473 | |
| 474 | /** |
| 475 | * $xtr = simplexml_load_string($Xml) or die ("Unable to load XML string!"); |
| 476 | **/ |
| 477 | |
| 478 | $processorResponse['ssl_result'] = self::GetNodeValue("ssl_result", $Xml); |
| 479 | $processorResponse['ssl_result_message'] = self::GetNodeValue("ssl_result_message", $Xml); |
| 480 | $processorResponse['ssl_txn_id'] = self::GetNodeValue("ssl_txn_id", $Xml); |
| 481 | $processorResponse['ssl_cvv2_response'] = self::GetNodeValue("ssl_cvv2_response", $Xml); |
| 482 | $processorResponse['ssl_avs_response'] = self::GetNodeValue("ssl_avs_response", $Xml); |
| 483 | $processorResponse['ssl_approval_code'] = self::GetNodeValue("ssl_approval_code", $Xml); |
| 484 | $processorResponse['errorCode'] = self::GetNodeValue("errorCode", $Xml); |
| 485 | $processorResponse['errorName'] = self::GetNodeValue("errorName", $Xml); |
| 486 | $processorResponse['errorMessage'] = self::GetNodeValue("errorMessage", $Xml); |
| 487 | |
| 488 | return $processorResponse; |
| 489 | } |
| 490 | } |
| 491 | |