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