_mode = $mode; $this->_paymentProcessor = $paymentProcessor; $this->_processorName = ts('Elavon'); } /** * This function is set up and put here to make the mapping of fields * from the params object as visually clear as possible for easy editing * * Comment out irrelevant fields * @param $params * @return array */ public function mapProcessorFieldstoParams($params) { // compile array // Payment Processor field name fields from $params array // credit card name $requestFields['ssl_first_name'] = $params['billing_first_name']; // credit card name //$requestFields['ssl_middle_name'] = $params['billing_middle_name']; // credit card name $requestFields['ssl_last_name'] = $params['billing_last_name']; // contact name $requestFields['ssl_ship_to_first_name'] = $params['first_name']; // contact name $requestFields['ssl_ship_to_last_name'] = $params['last_name']; $requestFields['ssl_card_number'] = $params['credit_card_number']; $requestFields['ssl_amount'] = trim($params['amount']); $requestFields['ssl_exp_date'] = sprintf('%02d', (int) $params['month']) . substr($params['year'], 2, 2);; $requestFields['ssl_cvv2cvc2'] = $params['cvv2']; // CVV field passed to processor $requestFields['ssl_cvv2cvc2_indicator'] = "1"; $requestFields['ssl_avs_address'] = $params['street_address']; $requestFields['ssl_city'] = $params['city']; $requestFields['ssl_state'] = $params['state_province']; $requestFields['ssl_avs_zip'] = $params['postal_code']; $requestFields['ssl_country'] = $params['country']; $requestFields['ssl_email'] = $params['email']; // 32 character string $requestFields['ssl_invoice_number'] = $params['invoiceID']; $requestFields['ssl_transaction_type'] = "CCSALE"; $requestFields['ssl_description'] = empty($params['description']) ? "backoffice payment" : $params['description']; $requestFields['ssl_customer_number'] = substr($params['credit_card_number'], -4); // Added two lines below to allow commercial cards to go through as per page 15 of Elavon developer guide $requestFields['ssl_customer_code'] = '1111'; $requestFields['ssl_salestax'] = 0.0; /************************************************************************************ * Fields available from civiCRM not implemented for Elavon * * $params['qfKey']; * $params['amount_other']; * $params['ip_address']; * $params['contributionType_name' ]; * $params['contributionPageID']; * $params['contributionType_accounting_code']; * $params['amount_level']; * $params['credit_card_type']; ************************************************************************************/ return $requestFields; } /** * This function sends request and receives response from * the processor * @param array $params * @return array|object * @throws Exception */ public function doDirectPayment(&$params) { if (isset($params['is_recur']) && $params['is_recur'] == TRUE) { CRM_Core_Error::fatal(ts('Elavon - recurring payments not implemented')); } if (!defined('CURLOPT_SSLCERT')) { CRM_Core_Error::fatal(ts('Elavon / Nova Virtual Merchant Gateway requires curl with SSL support')); } //Create the array of variables to be sent to the processor from the $params array // passed into this function $requestFields = self::mapProcessorFieldstoParams($params); // define variables for connecting with the gateway $requestFields['ssl_merchant_id'] = $this->_paymentProcessor['user_name']; $requestFields['ssl_user_id'] = CRM_Utils_Array::value('password', $this->_paymentProcessor); $requestFields['ssl_pin'] = CRM_Utils_Array::value('signature', $this->_paymentProcessor); $host = $this->_paymentProcessor['url_site']; if ($this->_mode == "test") { $requestFields['ssl_test_mode'] = "TRUE"; } // Allow further manipulation of the arguments via custom hooks .. CRM_Utils_Hook::alterPaymentProcessorParams($this, $params, $requestFields); // Check to see if we have a duplicate before we send if ($this->checkDupe($params['invoiceID'], CRM_Utils_Array::value('contributionID', $params))) { 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.'); } // Convert to XML using function below $xml = self::buildXML($requestFields); // Send to the payment processor using cURL $chHost = $host . '?xmldata=' . $xml; $ch = curl_init($chHost); if (!$ch) { return self::errorExit(9004, 'Could not initiate connection to payment gateway'); } curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, Civi::settings()->get('verifySSL') ? 2 : 0); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, Civi::settings()->get('verifySSL')); // return the result on success, FALSE on failure curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 36000); // set this for debugging -look for output in apache error log //curl_setopt ($ch,CURLOPT_VERBOSE,1 ); // ensures any Location headers are followed if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') { curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); } // Send the data out over the wire $responseData = curl_exec($ch); // See if we had a curl error - if so tell 'em and bail out // NOTE: curl_error does not return a logical value (see its documentation), but // a string, which is empty when there was no error. if ((curl_errno($ch) > 0) || (strlen(curl_error($ch)) > 0)) { curl_close($ch); $errorNum = curl_errno($ch); $errorDesc = curl_error($ch); // Paranoia - in the unlikley event that 'curl' errno fails if ($errorNum == 0) { $errorNum = 9005; } // Paranoia - in the unlikley event that 'curl' error fails if (strlen($errorDesc) == 0) { $errorDesc = "Connection to payment gateway failed"; } if ($errorNum = 60) { return self::errorExit($errorNum, "Curl error - " . $errorDesc . " Try this link for more information http://curl.haxx.se/docs/sslcerts.html"); } return self::errorExit($errorNum, "Curl error - " . $errorDesc . " your key is located at " . $key . " the url is " . $host . " xml is " . $requestxml . " processor response = " . $processorResponse); } // If null data returned - tell 'em and bail out // NOTE: You will not necessarily get a string back, if the request failed for // any reason, the return value will be the boolean false. if (($responseData === FALSE) || (strlen($responseData) == 0)) { curl_close($ch); return self::errorExit(9006, "Error: Connection to payment gateway failed - no data returned."); } // If gateway returned no data - tell 'em and bail out if (empty($responseData)) { curl_close($ch); return self::errorExit(9007, "Error: No data returned from payment gateway."); } // Success so far - close the curl and check the data curl_close($ch); // Payment successfully sent to gateway - process the response now $processorResponse = self::decodeXMLResponse($responseData); // success in test mode returns response "APPROVED" // test mode always returns trxn_id = 0 // fix for CRM-2566 if ($processorResponse['errorCode']) { return self::errorExit(9010, "Error: [" . $processorResponse['errorCode'] . " " . $processorResponse['errorName'] . " " . $processorResponse['errorMessage'] . "] - from payment processor"); } if ($processorResponse['ssl_result_message'] == "APPROVED") { if ($this->_mode == 'test') { $query = "SELECT MAX(trxn_id) FROM civicrm_contribution WHERE trxn_id LIKE 'test%'"; $p = []; $trxn_id = strval(CRM_Core_Dao::singleValueQuery($query, $p)); $trxn_id = str_replace('test', '', $trxn_id); $trxn_id = intval($trxn_id) + 1; $params['trxn_id'] = sprintf('test%08d', $trxn_id); return $params; } else { return self::errorExit(9099, "Error: [approval code related to test transaction but mode was " . $this->_mode); } } // transaction failed, print the reason if ($processorResponse['ssl_result_message'] != "APPROVAL") { return self::errorExit(9009, "Error: [" . $processorResponse['ssl_result_message'] . " " . $processorResponse['ssl_result'] . "] - from payment processor"); } else { // Success ! if ($this->_mode != 'test') { // 'trxn_id' is varchar(255) field. returned value is length 37 $params['trxn_id'] = $processorResponse['ssl_txn_id']; } $params['trxn_result_code'] = $processorResponse['ssl_approval_code'] . "-Cvv2:" . $processorResponse['ssl_cvv2_response'] . "-avs:" . $processorResponse['ssl_avs_response']; return $params; } } /** * Produces error message and returns from class. * @param string $errorCode * @param string $errorMessage * @return CRM_Core_Error */ public function &errorExit($errorCode = NULL, $errorMessage = NULL) { $e = CRM_Core_Error::singleton(); if ($errorCode) { $e->push($errorCode, 0, NULL, $errorMessage); } else { $e->push(9000, 0, NULL, 'Unknown System Error.'); } return $e; } /** * This public function checks to see if we have the right processor config values set. * * NOTE: Called by Events and Contribute to check config params are set prior to trying * register any credit card details * * @return string|null * $errorMsg if any errors found - null if OK * */ public function checkConfig() { $errorMsg = []; if (empty($this->_paymentProcessor['user_name'])) { $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor'); } if (empty($this->_paymentProcessor['url_site'])) { $errorMsg[] = ' ' . ts('URL is not set for this payment processor'); } if (!empty($errorMsg)) { return implode('

', $errorMsg); } else { return NULL; } } /** * @param $requestFields * * @return string */ public function buildXML($requestFields) { $xmlFieldLength['ssl_first_name'] = 15; // credit card name $xmlFieldLength['ssl_last_name'] = 15; // contact name $xmlFieldLength['ssl_ship_to_first_name'] = 15; // contact name $xmlFieldLength['ssl_ship_to_last_name'] = 15; $xmlFieldLength['ssl_card_number'] = 19; $xmlFieldLength['ssl_amount'] = 13; $xmlFieldLength['ssl_exp_date'] = 4; $xmlFieldLength['ssl_cvv2cvc2'] = 4; $xmlFieldLength['ssl_cvv2cvc2_indicator'] = 1; $xmlFieldLength['ssl_avs_address'] = 20; $xmlFieldLength['ssl_city'] = 20; $xmlFieldLength['ssl_state'] = 30; $xmlFieldLength['ssl_avs_zip'] = 9; $xmlFieldLength['ssl_country'] = 50; $xmlFieldLength['ssl_email'] = 100; // 32 character string $xmlFieldLength['ssl_invoice_number'] = 25; $xmlFieldLength['ssl_transaction_type'] = 20; $xmlFieldLength['ssl_description'] = 255; $xmlFieldLength['ssl_merchant_id'] = 15; $xmlFieldLength['ssl_user_id'] = 15; $xmlFieldLength['ssl_pin'] = 128; $xmlFieldLength['ssl_test_mode'] = 5; $xmlFieldLength['ssl_salestax'] = 10; $xmlFieldLength['ssl_customer_code'] = 17; $xmlFieldLength['ssl_customer_number'] = 25; $xml = ''; foreach ($requestFields as $key => $value) { //dev/core/966 Don't send email through the urlencode. if ($key == 'ssl_email') { $xml .= '<' . $key . '>' . substr($value, 0, $xmlFieldLength[$key]) . ''; } else { $xml .= '<' . $key . '>' . self::tidyStringforXML($value, $xmlFieldLength[$key]) . ''; } } $xml .= ''; return $xml; } /** * @param $value * @param $fieldlength * * @return string */ public function tidyStringforXML($value, $fieldlength) { // the xml is posted to a url so must not contain spaces etc. It also needs to be cut off at a certain // length to match the processor's field length. The cut needs to be made after spaces etc are // transformed but must not include a partial transformed character e.g. %20 must be in or out not half-way $xmlString = substr(rawurlencode($value), 0, $fieldlength); $lastPercent = strrpos($xmlString, '%'); if ($lastPercent > $fieldlength - 3) { $xmlString = substr($xmlString, 0, $lastPercent); } return $xmlString; } /** * Simple function to use in place of the 'simplexml_load_string' call. * * It returns the NodeValue for a given NodeName * or returns an empty string. * * @param string $NodeName * @param string $strXML * @return string */ public function GetNodeValue($NodeName, &$strXML) { $OpeningNodeName = "<" . $NodeName . ">"; $ClosingNodeName = ""; $pos1 = stripos($strXML, $OpeningNodeName); $pos2 = stripos($strXML, $ClosingNodeName); if (($pos1 === FALSE) || ($pos2 === FALSE)) { return ""; } $pos1 += strlen($OpeningNodeName); $len = $pos2 - $pos1; $return = substr($strXML, $pos1, $len); // check out rtn values for debug // echo " $NodeName     $return
"; return ($return); } /** * @param string $Xml * * @return mixed */ public function decodeXMLresponse($Xml) { $processorResponse = []; $processorResponse['ssl_result'] = self::GetNodeValue("ssl_result", $Xml); $processorResponse['ssl_result_message'] = self::GetNodeValue("ssl_result_message", $Xml); $processorResponse['ssl_txn_id'] = self::GetNodeValue("ssl_txn_id", $Xml); $processorResponse['ssl_cvv2_response'] = self::GetNodeValue("ssl_cvv2_response", $Xml); $processorResponse['ssl_avs_response'] = self::GetNodeValue("ssl_avs_response", $Xml); $processorResponse['ssl_approval_code'] = self::GetNodeValue("ssl_approval_code", $Xml); $processorResponse['errorCode'] = self::GetNodeValue("errorCode", $Xml); $processorResponse['errorName'] = self::GetNodeValue("errorName", $Xml); $processorResponse['errorMessage'] = self::GetNodeValue("errorMessage", $Xml); return $processorResponse; } }