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