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