Merge pull request #19087 from civicrm/5.32
[civicrm-core.git] / CRM / Core / Payment / Elavon.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +----------------------------------------------------------------------------+
fee14197 4 | Elavon (Nova) Virtual Merchant Core Payment Module for CiviCRM version 5 |
6a488035
TO
5 +----------------------------------------------------------------------------+
6 | Licensed to CiviCRM under the Academic Free License version 3.0 |
7 | |
8 | Written & Contributed by Eileen McNaughton - Nov March 2008 |
9 +----------------------------------------------------------------------------+
e70a7fc0 10 */
6a488035 11
cfaee245 12use Civi\Payment\Exception\PaymentProcessorException;
13
6a488035 14/**
353ffa53
TO
15 * -----------------------------------------------------------------------------------------------
16 * The basic functionality of this processor is that variables from the $params object are transformed
17 * into xml. The xml is submitted to the processor's https site
18 * using curl and the response is translated back into an array using the processor's function.
19 *
20 * If an array ($params) is returned to the calling function the values from
21 * the array are merged into the calling functions array.
22 *
23 * If an result of class error is returned it is treated as a failure. No error denotes a success. Be
24 * WARY of this when coding
25 *
26 * -----------------------------------------------------------------------------------------------
d5cc0fc2 27 */
6a488035 28class CRM_Core_Payment_Elavon extends CRM_Core_Payment {
6a488035 29
46043255 30 /**
fe482240 31 * Constructor.
6a488035 32 *
6a0b768e
TO
33 * @param string $mode
34 * The mode of operation: live or test.
6a488035 35 *
cfaee245 36 * @param array $paymentProcessor
77b97be7 37 */
00be9182 38 public function __construct($mode, &$paymentProcessor) {
6a488035
TO
39 // live or test
40 $this->_mode = $mode;
41 $this->_paymentProcessor = $paymentProcessor;
6a488035
TO
42 }
43
46043255 44 /**
cfaee245 45 * Map fields to parameters.
46 *
6a488035
TO
47 * This function is set up and put here to make the mapping of fields
48 * from the params object as visually clear as possible for easy editing
49 *
cfaee245 50 * @param array $params
51 *
46043255
CW
52 * @return array
53 */
00be9182 54 public function mapProcessorFieldstoParams($params) {
6a488035 55 $requestFields['ssl_first_name'] = $params['billing_first_name'];
6a488035
TO
56 $requestFields['ssl_last_name'] = $params['billing_last_name'];
57 // contact name
58 $requestFields['ssl_ship_to_first_name'] = $params['first_name'];
59 // contact name
60 $requestFields['ssl_ship_to_last_name'] = $params['last_name'];
61 $requestFields['ssl_card_number'] = $params['credit_card_number'];
b3dd98a5 62 $requestFields['ssl_amount'] = trim($params['amount']);
fe7f4414 63 $requestFields['ssl_exp_date'] = sprintf('%02d', (int) $params['month']) . substr($params['year'], 2, 2);
6a488035
TO
64 $requestFields['ssl_cvv2cvc2'] = $params['cvv2'];
65 // CVV field passed to processor
66 $requestFields['ssl_cvv2cvc2_indicator'] = "1";
67 $requestFields['ssl_avs_address'] = $params['street_address'];
68 $requestFields['ssl_city'] = $params['city'];
69 $requestFields['ssl_state'] = $params['state_province'];
70 $requestFields['ssl_avs_zip'] = $params['postal_code'];
71 $requestFields['ssl_country'] = $params['country'];
72 $requestFields['ssl_email'] = $params['email'];
73 // 32 character string
74 $requestFields['ssl_invoice_number'] = $params['invoiceID'];
75 $requestFields['ssl_transaction_type'] = "CCSALE";
ceb10dc7 76 $requestFields['ssl_description'] = empty($params['description']) ? "backoffice payment" : $params['description'];
6a488035 77 $requestFields['ssl_customer_number'] = substr($params['credit_card_number'], -4);
b3dd98a5
DL
78 // Added two lines below to allow commercial cards to go through as per page 15 of Elavon developer guide
79 $requestFields['ssl_customer_code'] = '1111';
80 $requestFields['ssl_salestax'] = 0.0;
6a488035
TO
81 return $requestFields;
82 }
83
46043255 84 /**
cfaee245 85 * This function sends request and receives response from the processor.
86 *
46043255 87 * @param array $params
cfaee245 88 *
89 * @return array
90 *
91 * @throws \CRM_Core_Exception
46043255 92 */
00be9182 93 public function doDirectPayment(&$params) {
b3dd98a5 94 if (isset($params['is_recur']) && $params['is_recur'] == TRUE) {
2d296f18 95 throw new CRM_Core_Exception(ts('Elavon - recurring payments not implemented'));
6a488035
TO
96 }
97
98 if (!defined('CURLOPT_SSLCERT')) {
2d296f18 99 throw new CRM_Core_Exception(ts('Elavon / Nova Virtual Merchant Gateway requires curl with SSL support'));
6a488035
TO
100 }
101
46043255
CW
102 //Create the array of variables to be sent to the processor from the $params array
103 // passed into this function
cfaee245 104 $requestFields = $this->mapProcessorFieldstoParams($params);
6a488035 105
46043255 106 // define variables for connecting with the gateway
6a488035 107 $requestFields['ssl_merchant_id'] = $this->_paymentProcessor['user_name'];
9c1bc317
CW
108 $requestFields['ssl_user_id'] = $this->_paymentProcessor['password'] ?? NULL;
109 $requestFields['ssl_pin'] = $this->_paymentProcessor['signature'] ?? NULL;
6a488035
TO
110 $host = $this->_paymentProcessor['url_site'];
111
cfaee245 112 if ($this->_mode === 'test') {
6a488035
TO
113 $requestFields['ssl_test_mode'] = "TRUE";
114 }
115
116 // Allow further manipulation of the arguments via custom hooks ..
117 CRM_Utils_Hook::alterPaymentProcessorParams($this, $params, $requestFields);
118
46043255 119 // Check to see if we have a duplicate before we send
d253aeb8 120 if ($this->checkDupe($params['invoiceID'], CRM_Utils_Array::value('contributionID', $params))) {
cfaee245 121 throw new PaymentProcessorException('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.', 9003);
6a488035
TO
122 }
123
46043255 124 // Convert to XML using function below
cfaee245 125 $xml = $this->buildXML($requestFields);
6a488035 126
46043255 127 // Send to the payment processor using cURL
6a488035
TO
128
129 $chHost = $host . '?xmldata=' . $xml;
130
131 $ch = curl_init($chHost);
132 if (!$ch) {
cfaee245 133 throw new PaymentProcessorException('Could not initiate connection to payment gateway', 9004);
6a488035
TO
134 }
135
aaffa79f 136 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, Civi::settings()->get('verifySSL') ? 2 : 0);
137 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, Civi::settings()->get('verifySSL'));
6a488035
TO
138 // return the result on success, FALSE on failure
139 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
140 curl_setopt($ch, CURLOPT_TIMEOUT, 36000);
141 // set this for debugging -look for output in apache error log
142 //curl_setopt ($ch,CURLOPT_VERBOSE,1 );
143 // ensures any Location headers are followed
439b9688
LS
144 if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') {
145 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
146 }
6a488035 147
46043255 148 // Send the data out over the wire
6a488035
TO
149 $responseData = curl_exec($ch);
150
46043255
CW
151 // See if we had a curl error - if so tell 'em and bail out
152 // NOTE: curl_error does not return a logical value (see its documentation), but
153 // a string, which is empty when there was no error.
6a488035
TO
154 if ((curl_errno($ch) > 0) || (strlen(curl_error($ch)) > 0)) {
155 curl_close($ch);
156 $errorNum = curl_errno($ch);
157 $errorDesc = curl_error($ch);
158
159 // Paranoia - in the unlikley event that 'curl' errno fails
2aa397bc
TO
160 if ($errorNum == 0) {
161 $errorNum = 9005;
162 }
6a488035
TO
163
164 // Paranoia - in the unlikley event that 'curl' error fails
2aa397bc 165 if (strlen($errorDesc) == 0) {
cfaee245 166 $errorDesc = 'Connection to payment gateway failed';
2aa397bc 167 }
cfaee245 168 throw new PaymentProcessorException('Curl error - ' . $errorDesc . ' Try this link for more information http://curl.haxx.se/docs/sslcerts.html', $errorNum);
6a488035
TO
169 }
170
46043255
CW
171 // If null data returned - tell 'em and bail out
172 // NOTE: You will not necessarily get a string back, if the request failed for
173 // any reason, the return value will be the boolean false.
6a488035
TO
174 if (($responseData === FALSE) || (strlen($responseData) == 0)) {
175 curl_close($ch);
cfaee245 176 throw new PaymentProcessorException('Error: Connection to payment gateway failed - no data returned.', 9006);
6a488035
TO
177 }
178
46043255 179 // If gateway returned no data - tell 'em and bail out
6a488035
TO
180 if (empty($responseData)) {
181 curl_close($ch);
cfaee245 182 throw new PaymentProcessorException('Error: No data returned from payment gateway.', 9007);
6a488035
TO
183 }
184
46043255 185 // Success so far - close the curl and check the data
6a488035
TO
186 curl_close($ch);
187
46043255 188 // Payment successfully sent to gateway - process the response now
6a488035 189
cfaee245 190 $processorResponse = $this->decodeXMLresponse($responseData);
46043255
CW
191 // success in test mode returns response "APPROVED"
192 // test mode always returns trxn_id = 0
193 // fix for CRM-2566
6a488035 194
6a488035 195 if ($processorResponse['errorCode']) {
cfaee245 196 throw new PaymentProcessorException("Error: [" . $processorResponse['errorCode'] . " " . $processorResponse['errorName'] . " " . $processorResponse['errorMessage'] . '] - from payment processor', 9010);
6a488035 197 }
cfaee245 198 if ($processorResponse['ssl_result_message'] === "APPROVED") {
199 if ($this->_mode === 'test') {
353ffa53 200 $query = "SELECT MAX(trxn_id) FROM civicrm_contribution WHERE trxn_id LIKE 'test%'";
cfaee245 201 $trxn_id = (string) CRM_Core_DAO::singleValueQuery($query);
202 $trxn_id = (int) str_replace('test', '', $trxn_id);
203 ++$trxn_id;
6a488035
TO
204 $params['trxn_id'] = sprintf('test%08d', $trxn_id);
205 return $params;
206 }
cfaee245 207 throw new PaymentProcessorException('Error: [approval code related to test transaction but mode was ' . $this->_mode, 9099);
6a488035
TO
208 }
209
210 // transaction failed, print the reason
cfaee245 211 if ($processorResponse['ssl_result_message'] !== "APPROVAL") {
212 throw new PaymentProcessorException('Error: [' . $processorResponse['ssl_result_message'] . ' ' . $processorResponse['ssl_result'] . '] - from payment processor', 9009);
6a488035
TO
213 }
214 else {
46043255 215 // Success !
cfaee245 216 if ($this->_mode !== 'test') {
6a488035
TO
217 // 'trxn_id' is varchar(255) field. returned value is length 37
218 $params['trxn_id'] = $processorResponse['ssl_txn_id'];
219 }
220
221 $params['trxn_result_code'] = $processorResponse['ssl_approval_code'] . "-Cvv2:" . $processorResponse['ssl_cvv2_response'] . "-avs:" . $processorResponse['ssl_avs_response'];
222
223 return $params;
224 }
225 }
6a488035 226
46043255 227 /**
fe482240 228 * This public function checks to see if we have the right processor config values set.
6a488035
TO
229 *
230 * NOTE: Called by Events and Contribute to check config params are set prior to trying
231 * register any credit card details
232 *
46043255
CW
233 * @return string|null
234 * $errorMsg if any errors found - null if OK
6a488035 235 *
77b97be7 236 */
00be9182 237 public function checkConfig() {
be2fb01f 238 $errorMsg = [];
6a488035
TO
239
240 if (empty($this->_paymentProcessor['user_name'])) {
241 $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor');
242 }
243
244 if (empty($this->_paymentProcessor['url_site'])) {
245 $errorMsg[] = ' ' . ts('URL is not set for this payment processor');
246 }
247
248 if (!empty($errorMsg)) {
249 return implode('<p>', $errorMsg);
250 }
cfaee245 251 return NULL;
6a488035 252 }
46043255 253
6c786a9b
EM
254 /**
255 * @param $requestFields
256 *
257 * @return string
258 */
00be9182 259 public function buildXML($requestFields) {
6a488035
TO
260 $xmlFieldLength['ssl_first_name'] = 15;
261 // credit card name
262 $xmlFieldLength['ssl_last_name'] = 15;
263 // contact name
264 $xmlFieldLength['ssl_ship_to_first_name'] = 15;
265 // contact name
266 $xmlFieldLength['ssl_ship_to_last_name'] = 15;
267 $xmlFieldLength['ssl_card_number'] = 19;
268 $xmlFieldLength['ssl_amount'] = 13;
269 $xmlFieldLength['ssl_exp_date'] = 4;
270 $xmlFieldLength['ssl_cvv2cvc2'] = 4;
271 $xmlFieldLength['ssl_cvv2cvc2_indicator'] = 1;
272 $xmlFieldLength['ssl_avs_address'] = 20;
273 $xmlFieldLength['ssl_city'] = 20;
274 $xmlFieldLength['ssl_state'] = 30;
275 $xmlFieldLength['ssl_avs_zip'] = 9;
276 $xmlFieldLength['ssl_country'] = 50;
277 $xmlFieldLength['ssl_email'] = 100;
278 // 32 character string
279 $xmlFieldLength['ssl_invoice_number'] = 25;
280 $xmlFieldLength['ssl_transaction_type'] = 20;
281 $xmlFieldLength['ssl_description'] = 255;
282 $xmlFieldLength['ssl_merchant_id'] = 15;
b3dd98a5
DL
283 $xmlFieldLength['ssl_user_id'] = 15;
284 $xmlFieldLength['ssl_pin'] = 128;
6a488035 285 $xmlFieldLength['ssl_test_mode'] = 5;
b3dd98a5
DL
286 $xmlFieldLength['ssl_salestax'] = 10;
287 $xmlFieldLength['ssl_customer_code'] = 17;
288 $xmlFieldLength['ssl_customer_number'] = 25;
6a488035
TO
289
290 $xml = '<txn>';
291 foreach ($requestFields as $key => $value) {
a8ed7f4d
EL
292 //dev/core/966 Don't send email through the urlencode.
293 if ($key == 'ssl_email') {
294 $xml .= '<' . $key . '>' . substr($value, 0, $xmlFieldLength[$key]) . '</' . $key . '>';
295 }
296 else {
297 $xml .= '<' . $key . '>' . self::tidyStringforXML($value, $xmlFieldLength[$key]) . '</' . $key . '>';
298 }
6a488035 299 }
b3dd98a5 300 $xml .= '</txn>';
6a488035
TO
301 return $xml;
302 }
303
6c786a9b
EM
304 /**
305 * @param $value
306 * @param $fieldlength
307 *
308 * @return string
309 */
00be9182 310 public function tidyStringforXML($value, $fieldlength) {
6a488035
TO
311 // the xml is posted to a url so must not contain spaces etc. It also needs to be cut off at a certain
312 // length to match the processor's field length. The cut needs to be made after spaces etc are
313 // transformed but must not include a partial transformed character e.g. %20 must be in or out not half-way
314 $xmlString = substr(rawurlencode($value), 0, $fieldlength);
315 $lastPercent = strrpos($xmlString, '%');
316 if ($lastPercent > $fieldlength - 3) {
317 $xmlString = substr($xmlString, 0, $lastPercent);
318 }
319 return $xmlString;
320 }
321
46043255 322 /**
6a488035
TO
323 * Simple function to use in place of the 'simplexml_load_string' call.
324 *
325 * It returns the NodeValue for a given NodeName
326 * or returns an empty string.
46043255
CW
327 *
328 * @param string $NodeName
329 * @param string $strXML
330 * @return string
331 */
00be9182 332 public function GetNodeValue($NodeName, &$strXML) {
6a488035
TO
333 $OpeningNodeName = "<" . $NodeName . ">";
334 $ClosingNodeName = "</" . $NodeName . ">";
335
336 $pos1 = stripos($strXML, $OpeningNodeName);
337 $pos2 = stripos($strXML, $ClosingNodeName);
338
339 if (($pos1 === FALSE) || ($pos2 === FALSE)) {
340
cfaee245 341 return '';
6a488035
TO
342
343 }
344
345 $pos1 += strlen($OpeningNodeName);
346 $len = $pos2 - $pos1;
347
348 $return = substr($strXML, $pos1, $len);
349 // check out rtn values for debug
350 // echo " $NodeName &nbsp &nbsp $return <br>";
351 return ($return);
352 }
353
6c786a9b 354 /**
46043255 355 * @param string $Xml
6c786a9b
EM
356 *
357 * @return mixed
358 */
00be9182 359 public function decodeXMLresponse($Xml) {
be2fb01f 360 $processorResponse = [];
6a488035
TO
361
362 $processorResponse['ssl_result'] = self::GetNodeValue("ssl_result", $Xml);
363 $processorResponse['ssl_result_message'] = self::GetNodeValue("ssl_result_message", $Xml);
364 $processorResponse['ssl_txn_id'] = self::GetNodeValue("ssl_txn_id", $Xml);
365 $processorResponse['ssl_cvv2_response'] = self::GetNodeValue("ssl_cvv2_response", $Xml);
366 $processorResponse['ssl_avs_response'] = self::GetNodeValue("ssl_avs_response", $Xml);
367 $processorResponse['ssl_approval_code'] = self::GetNodeValue("ssl_approval_code", $Xml);
368 $processorResponse['errorCode'] = self::GetNodeValue("errorCode", $Xml);
369 $processorResponse['errorName'] = self::GetNodeValue("errorName", $Xml);
370 $processorResponse['errorMessage'] = self::GetNodeValue("errorMessage", $Xml);
371
372 return $processorResponse;
373 }
96025800 374
6a488035 375}