Merge pull request #17484 from civicrm/5.26
[civicrm-core.git] / CRM / Core / Payment / PayflowPro.php
1 <?php
2 /*
3 +----------------------------------------------------------------------------+
4 | Payflow Pro Core Payment Module for CiviCRM version 5 |
5 +----------------------------------------------------------------------------+
6 | Licensed to CiviCRM under the Academic Free License version 3.0 |
7 | |
8 | Written & Contributed by Eileen McNaughton - 2009 |
9 +---------------------------------------------------------------------------+
10 */
11
12 /**
13 * Class CRM_Core_Payment_PayflowPro.
14 */
15 class CRM_Core_Payment_PayflowPro extends CRM_Core_Payment {
16 // (not used, implicit in the API, might need to convert?)
17 const
18 CHARSET = 'UFT-8';
19
20 /**
21 * Constructor
22 *
23 * @param string $mode
24 * The mode of operation: live or test.
25 * @param $paymentProcessor
26 */
27 public function __construct($mode, &$paymentProcessor) {
28 // live or test
29 $this->_mode = $mode;
30 $this->_paymentProcessor = $paymentProcessor;
31 }
32
33 /*
34 * This function sends request and receives response from
35 * the processor. It is the main function for processing on-server
36 * credit card transactions
37 */
38
39 /**
40 * This function collects all the information from a web/api form and invokes
41 * the relevant payment processor specific functions to perform the transaction
42 *
43 * @param array $params
44 * Assoc array of input parameters for this transaction.
45 *
46 * @return array
47 * the result in an nice formatted array (or an error object)
48 * @abstract
49 */
50 public function doDirectPayment(&$params) {
51 if (!defined('CURLOPT_SSLCERT')) {
52 throw new CRM_Core_Exception(ts('Payflow Pro requires curl with SSL support'));
53 }
54
55 /*
56 * define variables for connecting with the gateway
57 */
58
59 // Are you using the Payflow Fraud Protection Service?
60 // Default is YES, change to NO or blank if not.
61 //This has not been investigated as part of writing this payment processor
62 $fraud = 'NO';
63 //if you have not set up a separate user account the vendor name is used as the username
64 if (!$this->_paymentProcessor['subject']) {
65 $user = $this->_paymentProcessor['user_name'];
66 }
67 else {
68 $user = $this->_paymentProcessor['subject'];
69 }
70
71 // ideally this id would be passed through into this class as
72 // part of the paymentProcessor
73 //object with the other variables. It seems inefficient to re-query to get it.
74 //$params['processor_id'] = CRM_Core_DAO::getFieldValue(
75 // 'CRM_Contribute_DAO_ContributionP
76 //age',$params['contributionPageID'], 'payment_processor_id' );
77
78 /*
79 *Create the array of variables to be sent to the processor from the $params array
80 * passed into this function
81 *
82 */
83
84 $payflow_query_array = [
85 'USER' => $user,
86 'VENDOR' => $this->_paymentProcessor['user_name'],
87 'PARTNER' => $this->_paymentProcessor['signature'],
88 'PWD' => $this->_paymentProcessor['password'],
89 // C - Direct Payment using credit card
90 'TENDER' => 'C',
91 // A - Authorization, S - Sale
92 'TRXTYPE' => 'S',
93 'ACCT' => urlencode($params['credit_card_number']),
94 'CVV2' => $params['cvv2'],
95 'EXPDATE' => urlencode(sprintf('%02d', (int) $params['month']) . substr($params['year'], 2, 2)),
96 'ACCTTYPE' => urlencode($params['credit_card_type']),
97 'AMT' => urlencode($this->getAmount($params)),
98 'CURRENCY' => urlencode($params['currency']),
99 'FIRSTNAME' => $params['billing_first_name'],
100 //credit card name
101 'LASTNAME' => $params['billing_last_name'],
102 //credit card name
103 'STREET' => $params['street_address'],
104 'CITY' => urlencode($params['city']),
105 'STATE' => urlencode($params['state_province']),
106 'ZIP' => urlencode($params['postal_code']),
107 'COUNTRY' => urlencode($params['country']),
108 'EMAIL' => $params['email'],
109 'CUSTIP' => urlencode($params['ip_address']),
110 'COMMENT1' => urlencode($params['contributionType_accounting_code']),
111 'COMMENT2' => $mode,
112 'INVNUM' => urlencode($params['invoiceID']),
113 'ORDERDESC' => urlencode($params['description']),
114 'VERBOSITY' => 'MEDIUM',
115 'BILLTOCOUNTRY' => urlencode($params['country']),
116 ];
117
118 if ($params['installments'] == 1) {
119 $params['is_recur'] = FALSE;
120 }
121
122 if ($params['is_recur'] == TRUE) {
123
124 $payflow_query_array['TRXTYPE'] = 'R';
125 $payflow_query_array['OPTIONALTRX'] = 'S';
126 $payflow_query_array['OPTIONALTRXAMT'] = $this->getAmount($params);
127 //Amount of the initial Transaction. Required
128 $payflow_query_array['ACTION'] = 'A';
129 //A for add recurring (M-modify,C-cancel,R-reactivate,I-inquiry,P-payment
130 $payflow_query_array['PROFILENAME'] = urlencode('RegularContribution');
131 //A for add recurring (M-modify,C-cancel,R-reactivate,I-inquiry,P-payment
132 if ($params['installments'] > 0) {
133 $payflow_query_array['TERM'] = $params['installments'] - 1;
134 //ie. in addition to the one happening with this transaction
135 }
136 // $payflow_query_array['COMPANYNAME']
137 // $payflow_query_array['DESC'] = not set yet Optional
138 // description of the goods or
139 //services being purchased.
140 //This parameter applies only for ACH_CCD accounts.
141 // The
142 // $payflow_query_array['MAXFAILPAYMENTS'] = 0;
143 // number of payment periods (as s
144 //pecified by PAYPERIOD) for which the transaction is allowed
145 //to fail before PayPal cancels a profile. the default
146 // value of 0 (zero) specifies no
147 //limit. Retry
148 //attempts occur until the term is complete.
149 // $payflow_query_array['RETRYNUMDAYS'] = (not set as can't assume business rule
150
151 switch ($params['frequency_unit']) {
152 case '1 week':
153 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") + 7,
154 date("Y")
155 );
156 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") + (7 * $payflow_query_array['TERM']),
157 date("Y")
158 );
159 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
160 $payflow_query_array['PAYPERIOD'] = "WEEK";
161 $params['frequency_unit'] = "week";
162 $params['frequency_interval'] = 1;
163 break;
164
165 case '2 weeks':
166 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") + 14, date("Y"));
167 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") + (14 * $payflow_query_array['TERM']), date("Y ")
168 );
169 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
170 $payflow_query_array['PAYPERIOD'] = "BIWK";
171 $params['frequency_unit'] = "week";
172 $params['frequency_interval'] = 2;
173 break;
174
175 case '4 weeks':
176 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d") + 28, date("Y")
177 );
178 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d") + (28 * $payflow_query_array['TERM']), date("Y")
179 );
180 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
181 $payflow_query_array['PAYPERIOD'] = "FRWK";
182 $params['frequency_unit'] = "week";
183 $params['frequency_interval'] = 4;
184 break;
185
186 case '1 month':
187 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") + 1,
188 date("d"), date("Y")
189 );
190 $params['end_date'] = mktime(0, 0, 0, date("m") +
191 (1 * $payflow_query_array['TERM']),
192 date("d"), date("Y")
193 );
194 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
195 $payflow_query_array['PAYPERIOD'] = "MONT";
196 $params['frequency_unit'] = "month";
197 $params['frequency_interval'] = 1;
198 break;
199
200 case '3 months':
201 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") + 3, date("d"), date("Y")
202 );
203 $params['end_date'] = mktime(0, 0, 0, date("m") +
204 (3 * $payflow_query_array['TERM']),
205 date("d"), date("Y")
206 );
207 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
208 $payflow_query_array['PAYPERIOD'] = "QTER";
209 $params['frequency_unit'] = "month";
210 $params['frequency_interval'] = 3;
211 break;
212
213 case '6 months':
214 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m") + 6, date("d"),
215 date("Y")
216 );
217 $params['end_date'] = mktime(0, 0, 0, date("m") +
218 (6 * $payflow_query_array['TERM']),
219 date("d"), date("Y")
220 );
221 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']
222 );
223 $payflow_query_array['PAYPERIOD'] = "SMYR";
224 $params['frequency_unit'] = "month";
225 $params['frequency_interval'] = 6;
226 break;
227
228 case '1 year':
229 $params['next_sched_contribution_date'] = mktime(0, 0, 0, date("m"), date("d"),
230 date("Y") + 1
231 );
232 $params['end_date'] = mktime(0, 0, 0, date("m"), date("d"),
233 date("Y") +
234 (1 * $payflow_query_array['TEM'])
235 );
236 $payflow_query_array['START'] = date('mdY', $params['next_sched_contribution_date']);
237 $payflow_query_array['PAYPERIOD'] = "YEAR";
238 $params['frequency_unit'] = "year";
239 $params['frequency_interval'] = 1;
240 break;
241 }
242 }
243
244 CRM_Utils_Hook::alterPaymentProcessorParams($this, $params, $payflow_query_array);
245 $payflow_query = $this->convert_to_nvp($payflow_query_array);
246
247 /*
248 * Check to see if we have a duplicate before we send
249 */
250 if ($this->checkDupe($params['invoiceID'], CRM_Utils_Array::value('contributionID', $params))) {
251 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.');
252 }
253
254 // ie. url at payment processor to submit to.
255 $submiturl = $this->_paymentProcessor['url_site'];
256
257 $responseData = self::submit_transaction($submiturl, $payflow_query);
258
259 /*
260 * Payment successfully sent to gateway - process the response now
261 */
262 $result = strstr($responseData, "RESULT");
263 if (empty($result)) {
264 return self::errorExit(9016, "No RESULT code from PayPal.");
265 }
266
267 $nvpArray = [];
268 while (strlen($result)) {
269 // name
270 $keypos = strpos($result, '=');
271 $keyval = substr($result, 0, $keypos);
272 // value
273 $valuepos = strpos($result, '&') ? strpos($result, '&') : strlen($result);
274 $valval = substr($result, $keypos + 1, $valuepos - $keypos - 1);
275 // decoding the respose
276 $nvpArray[$keyval] = $valval;
277 $result = substr($result, $valuepos + 1, strlen($result));
278 }
279 // get the result code to validate.
280 $result_code = $nvpArray['RESULT'];
281 /*debug
282 echo "<p>Params array</p><br>";
283 print_r($params);
284 echo "<p></p><br>";
285 echo "<p>Values to Payment Processor</p><br>";
286 print_r($payflow_query_array);
287 echo "<p></p><br>";
288 echo "<p>Results from Payment Processor</p><br>";
289 print_r($nvpArray);
290 echo "<p></p><br>";
291 */
292
293 switch ($result_code) {
294 case 0:
295
296 /*******************************************************
297 * Success !
298 * This is a successful transaction. Payflow Pro does return further information
299 * about transactions to help you identify fraud including whether they pass
300 * the cvv check, the avs check. This is stored in
301 * CiviCRM as part of the transact
302 * but not further processing is done. Business rules would need to be defined
303 *******************************************************/
304 $params['trxn_id'] = $nvpArray['PNREF'] . $nvpArray['TRXPNREF'];
305 //'trxn_id' is varchar(255) field. returned value is length 12
306 $params['trxn_result_code'] = $nvpArray['AUTHCODE'] . "-Cvv2:" . $nvpArray['CVV2MATCH'] . "-avs:" . $nvpArray['AVSADDR'];
307
308 if ($params['is_recur'] == TRUE) {
309 $params['recur_trxn_id'] = $nvpArray['PROFILEID'];
310 //'trxn_id' is varchar(255) field. returned value is length 12
311 }
312 return $params;
313
314 case 1:
315 return self::errorExit(9008, "There is a payment processor configuration problem. This is usually due to invalid account information or ip restrictions on the account. You can verify ip restriction by logging // into Manager. See Service Settings >> Allowed IP Addresses. ");
316
317 case 12:
318 // Hard decline from bank.
319 return self::errorExit(9009, "Your transaction was declined ");
320
321 case 13:
322 // Voice authorization required.
323 return self::errorExit(9010, "Your Transaction is pending. Contact Customer Service to complete your order.");
324
325 case 23:
326 // Issue with credit card number or expiration date.
327 return self::errorExit(9011, "Invalid credit card information. Please re-enter.");
328
329 case 26:
330 return self::errorExit(9012, "You have not configured your payment processor with the correct credentials. Make sure you have provided both the <vendor> and the <user> variables ");
331
332 default:
333 return self::errorExit(9013, "Error - from payment processor: [" . $result_code . " " . $nvpArray['RESPMSG'] . "] ");
334 }
335
336 return self::errorExit(9014, "Check the code - all transactions should have been headed off before they got here. Something slipped through the net");
337 }
338
339 /**
340 * Produces error message and returns from class
341 *
342 * @param null $errorCode
343 * @param null $errorMessage
344 *
345 * @return object
346 */
347 public function &errorExit($errorCode = NULL, $errorMessage = NULL) {
348 $e = CRM_Core_Error::singleton();
349 if ($errorCode) {
350 $e->push($errorCode, 0, NULL, $errorMessage);
351 }
352 else {
353 $e->push(9000, 0, NULL, 'Unknown System Error.');
354 }
355 return $e;
356 }
357
358 /**
359 * NOTE: 'doTransferCheckout' not implemented
360 *
361 * @param array $params
362 * @param $component
363 *
364 * @throws Exception
365 */
366 public function doTransferCheckout(&$params, $component) {
367 throw new CRM_Core_Exception(ts('This function is not implemented'));
368 }
369
370 /**
371 * This public function checks to see if we have the right processor config values set
372 *
373 * NOTE: Called by Events and Contribute to check config params are set prior to trying
374 * register any credit card details
375 *
376 * @return string|null
377 * the error message if any, null if OK
378 */
379 public function checkConfig() {
380 $errorMsg = [];
381 if (empty($this->_paymentProcessor['user_name'])) {
382 $errorMsg[] = ' ' . ts('ssl_merchant_id is not set for this payment processor');
383 }
384
385 if (empty($this->_paymentProcessor['url_site'])) {
386 $errorMsg[] = ' ' . ts('URL is not set for %1', [1 => $this->_paymentProcessor['name']]);
387 }
388
389 if (!empty($errorMsg)) {
390 return implode('<p>', $errorMsg);
391 }
392 else {
393 return NULL;
394 }
395 }
396
397 /**
398 * convert to a name/value pair (nvp) string
399 *
400 * @param $payflow_query_array
401 *
402 * @return array|string
403 */
404 public function convert_to_nvp($payflow_query_array) {
405 foreach ($payflow_query_array as $key => $value) {
406 $payflow_query[] = $key . '[' . strlen($value) . ']=' . $value;
407 }
408 $payflow_query = implode('&', $payflow_query);
409
410 return $payflow_query;
411 }
412
413 /**
414 * Submit transaction using cURL
415 *
416 * @param string $submiturl Url to direct HTTPS GET to
417 * @param $payflow_query value string to be posted
418 *
419 * @return mixed|object
420 */
421 public function submit_transaction($submiturl, $payflow_query) {
422 // get data ready for API
423 $user_agent = $_SERVER['HTTP_USER_AGENT'];
424 // Here's your custom headers; adjust appropriately for your setup:
425 $headers[] = "Content-Type: text/namevalue";
426 //or text/xml if using XMLPay.
427 $headers[] = "Content-Length : " . strlen($data);
428 // Length of data to be passed
429 // Here the server timeout value is set to 45, but notice
430 // below in the cURL section, the timeout
431 // for cURL is 90 seconds. You want to make sure the server
432 // timeout is less, then the connection.
433 $headers[] = "X-VPS-Timeout: 45";
434 //random unique number - the transaction is retried using this transaction ID
435 // in this function but if that doesn't work and it is re- submitted
436 // it is treated as a new attempt. Payflow Pro doesn't allow
437 // you to change details (e.g. card no) when you re-submit
438 // you can only try the same details
439 $headers[] = "X-VPS-Request-ID: " . rand(1, 1000000000);
440 // optional header field
441 $headers[] = "X-VPS-VIT-Integration-Product: CiviCRM";
442 // other Optional Headers. If used adjust as necessary.
443 // Name of your OS
444 //$headers[] = "X-VPS-VIT-OS-Name: Linux";
445 // OS Version
446 //$headers[] = "X-VPS-VIT-OS-Version: RHEL 4";
447 // What you are using
448 //$headers[] = "X-VPS-VIT-Client-Type: PHP/cURL";
449 // For your info
450 //$headers[] = "X-VPS-VIT-Client-Version: 0.01";
451 // For your info
452 //$headers[] = "X-VPS-VIT-Client-Architecture: x86";
453 // Application version
454 //$headers[] = "X-VPS-VIT-Integration-Version: 0.01";
455 $ch = curl_init();
456 curl_setopt($ch, CURLOPT_URL, $submiturl);
457 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
458 curl_setopt($ch, CURLOPT_USERAGENT, $user_agent);
459 curl_setopt($ch, CURLOPT_HEADER, 1);
460 // tells curl to include headers in response
461 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
462 // return into a variable
463 curl_setopt($ch, CURLOPT_TIMEOUT, 90);
464 // times out after 90 secs
465 if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') {
466 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
467 }
468 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, Civi::settings()->get('verifySSL'));
469 // this line makes it work under https
470 curl_setopt($ch, CURLOPT_POSTFIELDS, $payflow_query);
471 //adding POST data
472 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, Civi::settings()->get('verifySSL') ? 2 : 0);
473 //verifies ssl certificate
474 curl_setopt($ch, CURLOPT_FORBID_REUSE, TRUE);
475 //forces closure of connection when done
476 curl_setopt($ch, CURLOPT_POST, 1);
477 //data sent as POST
478
479 // Try to submit the transaction up to 3 times with 5 second delay. This can be used
480 // in case of network issues. The idea here is since you are posting via HTTPS there
481 // could be general network issues, so try a few times before you tell customer there
482 // is an issue.
483
484 $i = 1;
485 while ($i++ <= 3) {
486 $responseData = curl_exec($ch);
487 $responseHeaders = curl_getinfo($ch);
488 if ($responseHeaders['http_code'] != 200) {
489 // Let's wait 5 seconds to see if its a temporary network issue.
490 sleep(5);
491 }
492 elseif ($responseHeaders['http_code'] == 200) {
493 // we got a good response, drop out of loop.
494 break;
495 }
496 }
497 if ($responseHeaders['http_code'] != 200) {
498 return self::errorExit(9015, "Error connecting to the Payflow Pro API server.");
499 }
500
501 /*
502 * Transaction submitted -
503 * See if we had a curl error - if so tell 'em and bail out
504 *
505 * NOTE: curl_error does not return a logical value (see its documentation), but
506 * a string, which is empty when there was no error.
507 */
508 if ((curl_errno($ch) > 0) || (strlen(curl_error($ch)) > 0)) {
509 curl_close($ch);
510 $errorNum = curl_errno($ch);
511 $errorDesc = curl_error($ch);
512
513 //Paranoia - in the unlikley event that 'curl' errno fails
514 if ($errorNum == 0) {
515 $errorNum = 9005;
516 }
517
518 // Paranoia - in the unlikley event that 'curl' error fails
519 if (strlen($errorDesc) == 0) {
520 $errorDesc = "Connection to payment gateway failed";
521 }
522 if ($errorNum = 60) {
523 return self::errorExit($errorNum, "Curl error - " . $errorDesc .
524 " Try this link for more information http://curl.haxx.se/d
525 ocs/sslcerts.html"
526 );
527 }
528
529 return self::errorExit($errorNum, "Curl error - " . $errorDesc .
530 " processor response = " . $processorResponse
531 );
532 }
533
534 /*
535 * If null data returned - tell 'em and bail out
536 *
537 * NOTE: You will not necessarily get a string back, if the request failed for
538 * any reason, the return value will be the boolean false.
539 */
540 if (($responseData === FALSE) || (strlen($responseData) == 0)) {
541 curl_close($ch);
542 return self::errorExit(9006, "Error: Connection to payment gateway failed - no data
543 returned. Gateway url set to $submiturl");
544 }
545
546 /*
547 * If gateway returned no data - tell 'em and bail out
548 */
549 if (empty($responseData)) {
550 curl_close($ch);
551 return self::errorExit(9007, "Error: No data returned from payment gateway.");
552 }
553
554 /*
555 * Success so far - close the curl and check the data
556 */
557 curl_close($ch);
558 return $responseData;
559 }
560
561 }