PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.7.0.14
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.7.0.14
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / gateways / class-charitable-gateway-paypal.php

class-charitable-gateway-paypal.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.7.0.14, at includes/gateways/class-charitable-gateway-paypal.php

925 lines 29.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Paypal Payment Gateway class.
4 *
5 * @package Charitable/Classes/Charitable_Gateway_Paypal
6 * @author David Bisset
7 * @copyright Copyright (c) 2022, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 * @version 1.6.55
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Gateway_Paypal' ) ) :
19
20 /**
21 * Paypal Payment Gateway
22 *
23 * @since 1.0.0
24 */
25 class Charitable_Gateway_Paypal extends Charitable_Gateway {
26
27 /**
28 * Gateway ID.
29 */
30 const ID = 'paypal';
31
32 /**
33 * Instantiate the gateway class, defining its key values.
34 *
35 * @since 1.0.0
36 */
37 public function __construct() {
38 $this->name = apply_filters( 'charitable_gateway_paypal_name', __( 'PayPal', 'charitable' ) );
39
40 $this->defaults = array(
41 'label' => __( 'PayPal', 'charitable' ),
42 );
43
44 $this->supports = array(
45 'recurring',
46 'recurring_cancellation',
47 'refunds',
48 '1.3.0',
49 );
50 }
51
52 /**
53 * Register gateway settings.
54 *
55 * @since 1.0.0
56 *
57 * @param array $settings List of settings. Empty by default.
58 * @return array
59 */
60 public function gateway_settings( $settings ) {
61 return array_merge( $settings, array(
62 'paypal_email' => array(
63 'type' => 'email',
64 'title' => __( 'PayPal Email Address', 'charitable' ),
65 'priority' => 6,
66 'help' => __( 'Enter the email address for the PayPal account that should receive donations.', 'charitable' ),
67 ),
68 'sandbox_paypal_email' => array(
69 'type' => 'email',
70 'title' => __( 'Sandbox PayPal Email Address', 'charitable' ),
71 'priority' => 7,
72 'help' => __( 'Enter the email address for the Sandbox PayPal account that should receive test donations.', 'charitable' ),
73 ),
74 'transaction_mode' => array(
75 'type' => 'radio',
76 'title' => __( 'PayPal Transaction Type', 'charitable' ),
77 'priority' => 8,
78 'options' => array(
79 'donations' => __( 'Donations', 'charitable' ),
80 'standard' => __( 'Standard Transaction', 'charitable' ),
81 ),
82 'default' => 'donations',
83 'help' => sprintf( '%s<br /><a href="%s" target="_blank">%s</a>',
84 __( 'PayPal offers discounted fees to registered non-profit organizations. You must create a PayPal Business account to apply.', 'charitable' ),
85 'https://www.paypal.com/us/webapps/mpp/donations',
86 __( 'Find out more.', 'charitable' )
87 ),
88 ),
89 'disable_ipn_verification' => array(
90 'type' => 'checkbox',
91 'title' => __( 'Disable IPN Verification', 'charitable' ),
92 'priority' => 10,
93 'default' => 0,
94 'help' => __( 'If you are having problems with donations not getting marked as Paid, disabling IPN verification might fix the problem. However, it is important to be aware that this is a <strong>less secure</strong> method for verifying donations.', 'charitable' ),
95 ),
96 'api' => array(
97 'type' => 'heading',
98 'title' => __( 'API Settings', 'charitable' ),
99 'priority' => 20,
100 ),
101 'api_description' => array(
102 'type' => 'content',
103 'content' => '<div class="charitable-settings-notice">'
104 . '<p>' . __( 'API credentials are necessary to process PayPal refunds and subscription cancellations from inside WordPress. To find your credentials:', 'charitable' ) . '</p>'
105 . '<ul>'
106 . '<li>' . sprintf(
107 /* translators: %s: link */
108 __( '<strong>Live</strong>: Go to %s.', 'charitable' ),
109 '<a href="https://www.paypal.com/businessprofile/mytools/apiaccess/firstparty/signature" target="_blank">https://www.paypal.com/businessprofile/mytools/apiaccess/firstparty/signature</a>'
110 ) . '</li>'
111 . '<li>' . sprintf(
112 /* translators: %s: link */
113 __( '<strong>Sandbox</strong>: Go to %s, click on <em>Profile</em> for the Seller account and open the <em>API keys</em> tab.', 'charitable' ),
114 '<a href="https://developer.paypal.com/developer/accounts/" target="_blank">https://developer.paypal.com/developer/accounts/</a>'
115 ) . '</li>'
116 . '</ul>'
117 . '</div>',
118 'priority' => 21,
119 ),
120 'api_username' => array(
121 'title' => __( 'Live API Username', 'charitable' ),
122 'type' => 'text',
123 'priority' => 30,
124 'default' => '',
125 ),
126 'api_password' => array(
127 'title' => __( 'Live API Password', 'charitable' ),
128 'type' => 'password',
129 'priority' => 40,
130 'default' => '',
131 ),
132 'api_signature' => array(
133 'title' => __( 'Live API Signature', 'charitable' ),
134 'type' => 'password',
135 'priority' => 50,
136 'default' => '',
137 ),
138 'sandbox_api_username' => array(
139 'title' => __( 'Sandbox API Username', 'charitable' ),
140 'type' => 'text',
141 'description' => __( 'Create sandbox accounts and obtain API credentials from within your <a href="http://developer.paypal.com">PayPal developer account</a> or read more about it in the <a href="https://developer.paypal.com/docs/classic/api/apiCredentials/#creating-an-api-signature" target="_blank">documentation</a>.', 'charitable' ),
142 'priority' => 60,
143 'default' => '',
144 ),
145 'sandbox_api_password' => array(
146 'title' => __( 'Sandbox API Password', 'charitable' ),
147 'type' => 'password',
148 'priority' => 70,
149 'default' => '',
150 ),
151 'sandbox_api_signature' => array(
152 'title' => __( 'Sandbox API Signature', 'charitable' ),
153 'type' => 'password',
154 'priority' => 80,
155 'default' => '',
156 ),
157 ) );
158 }
159
160 /**
161 * Retrieve PayPal API credentials.
162 *
163 * @since 1.6.0
164 *
165 * @param boolean|null $test_mode Whether to get the test mode credentials.
166 * @return array
167 */
168 public static function get_api_credentials( $test_mode = null ) {
169 if ( is_null( $test_mode ) ) {
170 $test_mode = charitable_get_option( 'test_mode' );
171 }
172
173 $mode = $test_mode ? 'sandbox_' : '';
174
175 /**
176 * Filter the PayPal API Credentials.
177 *
178 * @since 1.6.0
179 *
180 * @param array $creds The API credentials.
181 */
182 return apply_filters(
183 'charitable_paypal_api_credentials',
184 array(
185 'username' => charitable_get_option( array( 'gateways_paypal', $mode . 'api_username' ) ),
186 'password' => charitable_get_option( array( 'gateways_paypal', $mode . 'api_password' ) ),
187 'signature' => charitable_get_option( array( 'gateways_paypal', $mode . 'api_signature' ) ),
188 )
189 );
190 }
191
192 /**
193 * Check whether we have the required PayPal API credentials.
194 *
195 * @since 1.6.0
196 *
197 * @param boolean|null $test_mode Whether to get the test mode credentials.
198 * @return boolean
199 */
200 public static function has_api_credentials( $test_mode = null ) {
201 $creds = self::get_api_credentials( $test_mode );
202 $required = array(
203 'username',
204 'password',
205 'signature',
206 );
207
208 foreach ( $required as $key ) {
209 if ( ! array_key_exists( $key, $creds ) || empty( $creds[ $key ] ) ) {
210 return false;
211 }
212 }
213
214 return true;
215 }
216
217 /**
218 * Return the API endpoint, depending on whether we need the sandbox or the live one.
219 *
220 * @since 1.6.0
221 *
222 * @param boolean|null $test_mode Whether to get the test mode credentials.
223 * @return string
224 */
225 public static function get_api_endpoint( $test_mode = null ) {
226 if ( is_null( $test_mode ) ) {
227 $test_mode = charitable_get_option( 'test_mode' );
228 }
229
230 return $test_mode ? 'https://api-3t.sandbox.paypal.com/nvp' : 'https://api-3t.paypal.com/nvp';
231 }
232
233 /**
234 * Validate the submitted credit card details.
235 *
236 * @since 1.0.0
237 *
238 * @param boolean $valid Boolean value to be returned indicating whether the donation is valid.
239 * @param string $gateway The donation gateway.
240 * @param mixed[] $values Set of donation values.
241 * @return boolean
242 */
243 public static function validate_donation( $valid, $gateway, $values ) {
244 if ( 'paypal' != $gateway ) {
245 return $valid;
246 }
247
248 $email = self::get_paypal_email();
249
250 /* Make sure that the email is set. */
251 if ( ! isset( $email ) || empty( $email ) ) {
252
253 charitable_get_notices()->add_error( __( 'Missing PayPal email address. Unable to proceed with payment.', 'charitable' ) );
254 return false;
255
256 }
257
258 return $valid;
259 }
260
261 /**
262 * Process the donation with PayPal.
263 *
264 * @since 1.0.0
265 *
266 * @param boolean|array $return The value to be returned.
267 * @param int $donation_id The donation ID.
268 * @param Charitable_Donation_Processor $processor The Donation Processor object.
269 * @return array
270 */
271 public static function process_donation( $return, $donation_id, $processor ) {
272 $gateway = new Charitable_Gateway_Paypal();
273 $user_data = $processor->get_donation_data_value( 'user' );
274 $donation = charitable_get_donation( $donation_id );
275 $transaction_mode = $gateway->get_value( 'transaction_mode' );
276 $donation_key = $processor->get_donation_data_value( 'donation_key' );
277
278 /**
279 * Filter the arguments that are passed to PayPal for the payment.
280 *
281 * @since 1.0.0
282 *
283 * @param array $args The arguments.
284 * @param int $donation_id The donation ID.
285 * @param Charitable_Donation_Processor $processor The Donation Processor object.
286 */
287 $paypal_args = apply_filters( 'charitable_paypal_redirect_args', array(
288 'business' => self::get_paypal_email(),
289 'email' => isset( $user_data['email'] ) ? $user_data['email'] : '',
290 'first_name' => isset( $user_data['first_name'] ) ? $user_data['first_name'] : '',
291 'last_name' => isset( $user_data['last_name'] ) ? $user_data['last_name'] : '',
292 'address1' => isset( $user_data['address'] ) ? $user_data['address'] : '',
293 'address2' => isset( $user_data['address_2'] ) ? $user_data['address_2'] : '',
294 'city' => isset( $user_data['city'] ) ? $user_data['city'] : '',
295 'country' => isset( $user_data['country'] ) ? $user_data['country'] : '',
296 'zip' => isset( $user_data['postcode'] ) ? $user_data['postcode'] : '',
297 'invoice' => $donation_key,
298 'amount' => $donation->get_total_donation_amount( true ),
299 'item_name' => html_entity_decode( $donation->get_campaigns_donated_to(), ENT_COMPAT, 'UTF-8' ),
300 'no_shipping' => '1',
301 'shipping' => '0',
302 'currency_code' => charitable_get_currency(),
303 'charset' => get_bloginfo( 'charset' ),
304 'custom' => $donation_id,
305 'rm' => '2',
306 'return' => charitable_get_permalink( 'donation_receipt_page', array( 'donation_id' => $donation_id ) ),
307 'cancel_return' => charitable_get_permalink( 'donation_cancel_page', array( 'donation_id' => $donation_id ) ),
308 'notify_url' => charitable_get_ipn_url( Charitable_Gateway_Paypal::ID ),
309 'bn' => 'Charitable_SP',
310 'cmd' => 'donations' == $transaction_mode ? '_donations' : '_xclick',
311 ), $donation_id, $processor );
312
313 /* Set up the PayPal redirect URL. */
314 $paypal_redirect = trailingslashit( $gateway->get_redirect_url() ) . '?';
315 $paypal_redirect .= http_build_query( $paypal_args );
316 $paypal_redirect = str_replace( '&amp;', '&', $paypal_redirect );
317
318 /* Redirect to PayPal */
319 return array(
320 'redirect' => $paypal_redirect,
321 'safe' => false,
322 );
323
324 }
325
326 /**
327 * Handle a call to our IPN listener.
328 *
329 * @since 1.0.0
330 *
331 * @return void
332 */
333 public static function process_ipn() {
334 /* We only accept POST requests */
335 if ( ! self::is_valid_request() ) {
336 die( __( 'Invalid Request', 'charitable' ) );
337 }
338
339 $gateway = new Charitable_Gateway_Paypal();
340 $data = $gateway->get_encoded_ipn_data();
341
342 if ( defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG ) {
343 error_log( var_export( $data, true ) );
344 }
345
346 if ( empty( $data ) ) {
347 die( __( 'Empty Data', 'charitable' ) );
348 }
349
350 if ( ! $gateway->paypal_ipn_verification( $data ) ) {
351 die( __( 'IPN Verification Failure', 'charitable' ) );
352 }
353
354 $defaults = array(
355 'payment_status' => '',
356 'custom' => 0,
357 'txn_type' => '',
358 );
359
360 $data = wp_parse_args( $data, $defaults );
361 $custom = json_decode( $data['custom'], true );
362 $donation_id = is_array( $custom ) && array_key_exists( 'donation_id', $custom )
363 ? absint( $custom['donation_id'] )
364 : absint( $custom );
365
366 if ( ! $donation_id ) {
367 die( __( 'Missing Donation ID', 'charitable' ) );
368 }
369
370 /**
371 * By default, all transactions are handled by the web_accept handler.
372 * To handle other transaction types in a different way, use the
373 * 'charitable_paypal_{transaction_type}' hook.
374 *
375 * @see Charitable_Gateway_Paypal::process_web_accept()
376 */
377 $txn_type = strlen( $data['txn_type'] ) ? $data['txn_type'] : 'web_accept';
378
379 if ( has_action( 'charitable_paypal_' . $txn_type ) ) {
380 do_action( 'charitable_paypal_' . $txn_type, $data, $donation_id );
381 } else {
382 do_action( 'charitable_paypal_web_accept', $data, $donation_id );
383 }
384
385 exit;
386 }
387
388 /**
389 * Receives verified IPN data from PayPal and processes the donation.
390 *
391 * @since 1.0.0
392 *
393 * @param array $data The data received in the IPN from PayPal.
394 * @param int $donation_id The donation ID received from PayPal.
395 * @return void
396 */
397 public static function process_web_accept( $data, $donation_id ) {
398 $gateway = new Charitable_Gateway_Paypal();
399 $donation = charitable_get_donation( $donation_id );
400
401 if ( 'paypal' != $donation->get_gateway() ) {
402 die( __( 'Incorrect Gateway', 'charitable' ) );
403 }
404
405 $custom = json_decode( $data['custom'], true );
406
407 if ( array_key_exists( 'invoice', $data ) ) {
408 $donation_key = $data['invoice'];
409 } elseif ( is_array( $custom ) && array_key_exists( 'donation_key', $custom ) ) {
410 $donation_key = $custom['donation_key'];
411 } else {
412 die( __( 'Missing Donation Key', 'charitable' ) );
413 }
414
415 $amount = $data['mc_gross'];
416 $payment_status = strtolower( $data['payment_status'] );
417 $currency_code = strtoupper( $data['mc_currency'] );
418 $business_email = isset( $data['business'] ) && is_email( $data['business'] ) ? trim( $data['business'] ) : trim( $data['receiver_email'] );
419
420 /* Verify that the business email matches the PayPal email in the settings */
421 if ( strcasecmp( $business_email, trim( self::get_paypal_email() ) ) != 0 ) {
422
423 $message = sprintf( '%s %s', __( 'Invalid Business email in the IPN response. IPN data:', 'charitable' ), json_encode( $data ) );
424 $donation->log()->add( $message );
425 $donation->update_status( 'charitable-failed' );
426 die( __( 'Incorrect Business Email', 'charitable' ) );
427
428 }
429
430 /* Verify that the currency matches. */
431 if ( $donation->get_currency() !== $currency_code ) {
432
433 $message = sprintf( '%s %s', __( 'The currency in the IPN response does not match the site currency. IPN data:', 'charitable' ), json_encode( $data ) );
434 $donation->log()->add( $message );
435 $donation->update_status( 'charitable-failed' );
436
437 die( __( 'Incorrect Currency', 'charitable' ) );
438
439 }
440
441 /* Process a refunded donation. */
442 if ( in_array( $payment_status, array( 'refunded', 'reversed' ) ) ) {
443
444 /* It's a partial refund. */
445 if ( $amount < $donation->get_total_donation_amount( true ) ) {
446 $message = sprintf( '%s: #%s',
447 __( 'Partial PayPal refund processed', 'charitable' ),
448 isset( $data['parent_txn_id'] ) ? $data['parent_txn_id'] : ''
449 );
450 } else {
451 $message = sprintf( '%s #%s %s: %s',
452 __( 'PayPal Payment', 'charitable' ),
453 isset( $data['parent_txn_id'] ) ? $data['parent_txn_id'] : '',
454 __( 'refunded with reason', 'charitable' ),
455 isset( $data['reason_code'] ) ? $data['reason_code'] : ''
456 );
457 }
458
459 $donation->process_refund( $amount, $message );
460
461 die( __( 'Refund Processed', 'charitable' ) );
462
463 }
464
465 /* Mark a payment as failed. */
466 if ( in_array( $payment_status, array( 'declined', 'failed', 'denied', 'expired', 'voided' ) ) ) {
467
468 $message = sprintf( '%s: %s', __( 'The donation has failed with the following status', 'charitable' ), $payment_status );
469 $donation->log()->add( $message );
470 $donation->update_status( 'charitable-failed' );
471
472 die( __( 'Payment Failed', 'charitable' ) );
473
474 }
475
476 /* If we have already processed this donation, stop here. */
477 if ( 'charitable-completed' == get_post_status( $donation_id ) ) {
478 die( __( 'Donation Processed Already', 'charitable' ) );
479 }
480
481 /* Verify that the donation key matches the one stored for the donation. */
482 if ( $donation_key != $donation->get_donation_key() ) {
483
484 $message = sprintf( '%s %s', __( 'Donation key in the IPN response does not match the donation. IPN data:', 'charitable' ), json_encode( $data ) );
485 $donation->log()->add( $message );
486 $donation->update_status( 'charitable-failed' );
487
488 die( __( 'Invalid Donation Key', 'charitable' ) );
489
490 }
491
492 /* Verify that the amount in the IPN matches the amount we expected. */
493 if ( $amount < $donation->get_total_donation_amount( true ) ) {
494
495 $message = sprintf( '%s %s', __( 'The amount in the IPN response does not match the expected donation amount. IPN data:', 'charitable' ), json_encode( $data ) );
496 $donation->log()->add( $message );
497 $donation->update_status( 'charitable-failed' );
498
499 die( __( 'Incorrect Amount', 'charitable' ) );
500
501 }
502
503 /* Save the transation ID */
504 $donation->set_gateway_transaction_id( $data['txn_id'] );
505
506 /* Process a completed donation. */
507 if ( 'completed' == $payment_status ) {
508
509 $message = sprintf( '%s: %s', __( 'PayPal Transaction ID', 'charitable' ), $data['txn_id'] );
510 $donation->log()->add( $message );
511 $donation->update_status( 'charitable-completed' );
512
513 die( __( 'Donation Completed', 'charitable' ) );
514
515 }
516
517 /* If the donation is set to pending but has a pending_reason provided, save that to the log. */
518 if ( 'pending' == $payment_status ) {
519
520 if ( array_key_exists( 'pending_reason', $data ) ) {
521
522 $message = $gateway->get_pending_reason_note( strtolower( $data['pending_reason'] ) );
523 $donation->log()->add( $message );
524
525 }
526
527 $donation->update_status( 'charitable-pending' );
528
529 die( __( 'Donation Pending', 'charitable' ) );
530
531 }
532
533 die( __( 'Unknown Response', 'charitable' ) );
534 }
535
536 /**
537 * Return the posted IPN data.
538 *
539 * @since 1.0.0
540 *
541 * @return mixed[]
542 */
543 public function get_encoded_ipn_data() {
544 $post_data = '';
545
546 /* Fallback just in case post_max_size is lower than needed. */
547 if ( ini_get( 'allow_url_fopen' ) ) {
548 $post_data = file_get_contents( 'php://input' );
549 } else {
550 ini_set( 'post_max_size', '12M' );
551 }
552
553 if ( strlen( $post_data ) ) {
554 $arg_separator = ini_get( 'arg_separator.output' );
555 $data_string = 'cmd=_notify-validate' . $arg_separator . $post_data;
556
557 /* Convert collected post data to an array */
558 parse_str( $data_string, $data );
559
560 return $data;
561 }
562
563 /* Return an empty array if there are no POST variables. */
564 if ( empty( $_POST ) ) {
565 return array();
566 }
567
568 $data = array(
569 'cmd' => '_notify-validate',
570 );
571
572 return array_merge( $data, $_POST );
573
574 }
575
576 /**
577 * Validates an IPN request with PayPal.
578 *
579 * @since 1.0.0
580 *
581 * @param mixed[] $data Data received from PayPal.
582 * @return boolean
583 */
584 public function paypal_ipn_verification( $data ) {
585 if ( $this->get_value( 'disable_ipn_verification' ) ) {
586 return true;
587 }
588
589 $remote_post_vars = array(
590 'method' => 'POST',
591 'timeout' => 45,
592 'redirection' => 5,
593 'httpversion' => '1.1',
594 'blocking' => true,
595 'headers' => array(
596 'host' => 'www.paypal.com',
597 'connection' => 'close',
598 'content-type' => 'application/x-www-form-urlencoded',
599 'post' => '/cgi-bin/webscr HTTP/1.1',
600
601 ),
602 'sslverify' => false,
603 'body' => $data,
604 );
605
606 /* Get response */
607 $api_response = wp_remote_post( $this->get_redirect_url( true, true ), $remote_post_vars );
608
609 /**
610 * Filter whether the PayPal IPN was verified.
611 *
612 * @since 1.0.0
613 *
614 * @param boolean $valid Whether it has been verified.
615 * @param array|WP_Error $api_response Array in case of successful request. WP_Error otherwise.
616 */
617 return apply_filters( 'charitable_paypal_ipn_verification', $this->is_valid_api_response( $api_response ), $api_response );
618 }
619
620 /**
621 * Return a note to log for a pending payment.
622 *
623 * @since 1.0.0
624 *
625 * @param string $reason_code The reason code received from PayPal.
626 * @return string
627 */
628 public function get_pending_reason_note( $reason_code ) {
629 switch ( $reason_code ) {
630 case 'echeck':
631 $note = __( 'Payment made via eCheck and will clear automatically in 5-8 days', 'charitable' );
632 break;
633
634 case 'address':
635 $note = __( 'Payment requires a confirmed customer address and must be accepted manually through PayPal', 'charitable' );
636 break;
637
638 case 'intl':
639 $note = __( 'Payment must be accepted manually through PayPal due to international account regulations', 'charitable' );
640 break;
641
642 case 'multi-currency':
643 $note = __( 'Payment received in non-shop currency and must be accepted manually through PayPal', 'charitable' );
644 break;
645
646 case 'paymentreview':
647 case 'regulatory_review':
648 $note = __( 'Payment is being reviewed by PayPal staff as high-risk or in possible violation of government regulations', 'charitable' );
649 break;
650
651 case 'unilateral':
652 $note = __( 'Payment was sent to non-confirmed or non-registered email address.', 'charitable' );
653 break;
654
655 case 'upgrade':
656 $note = __( 'PayPal account must be upgraded before this payment can be accepted', 'charitable' );
657 break;
658
659 case 'verify':
660 $note = __( 'PayPal account is not verified. Verify account in order to accept this payment', 'charitable' );
661 break;
662
663 default:
664 /* translators: %s: reason code given by PayPal */
665 $note = sprintf( __( 'Payment is pending for unknown reasons. Contact PayPal support for assistance. Reason code: %s', 'charitable' ), $reason_code );
666 }
667
668 /**
669 * Filter the donation log note added about the reason for the pending donation.
670 *
671 * @since 1.0.0
672 *
673 * @param string $note The note.
674 * @param string $reason_code The reason code received from PayPal.
675 */
676 return apply_filters( 'charitable_paypal_gateway_pending_reason_note', $note, $reason_code );
677 }
678
679 /**
680 * Process a donation's refund.
681 *
682 * @since 1.6.0
683 *
684 * @param int $donation_id The donation ID.
685 * @return boolean
686 */
687 public static function process_refund( $donation_id ) {
688 $donation = charitable_get_donation( $donation_id );
689
690 if ( ! $donation ) {
691 return false;
692 }
693
694 $credentials = self::get_api_credentials( $donation->get_test_mode( false ) );
695
696 $n = 0;
697 $body = array(
698 'METHOD' => 'RefundTransaction',
699 'TRANSACTIONID' => $donation->get_gateway_transaction_id(),
700 'REFUNDTYPE' => 'Full',
701 'USER' => $credentials['username'],
702 'PWD' => $credentials['password'],
703 'SIGNATURE' => $credentials['signature'],
704 'VERSION' => '204',
705 );
706
707 foreach ( $donation->get_campaign_donations() as $campaign_donation ) {
708 $body[ 'L_INVOICEITEMNAME' . $n ] = $campaign_donation->campaign_name;
709 $body[ 'L_DESCRIPTION' . $n ] = __( 'Donation', 'charitable' );
710 $body[ 'L_PRICE' . $n ] = Charitable_Currency::get_instance()->sanitize_monetary_amount( (string) $campaign_donation->amount );
711 $body[ 'L_PRICECURRENCYCODE' . $n ] = charitable_get_currency();
712
713 $n += 1;
714 }
715
716 /**
717 * Filter the PayPal refund args.
718 *
719 * @since 1.6.0
720 *
721 * @param array $body The arguments we're sending to PayPal.
722 * @param Charitable_Donation $donation The donation instance.
723 */
724 $body = apply_filters( 'charitable_paypal_refund_args', $body, $donation );
725 $headers = array(
726 'Content-Type' => 'application/x-www-form-urlencoded',
727 'Cache-Control' => 'no-cache',
728 );
729 $args = array(
730 'body' => $body,
731 'headers' => $headers,
732 'timeout' => 30,
733 'httpversion' => '1.1',
734 );
735
736 if ( defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG ) {
737 error_log( var_export( $args, true ) );
738 }
739
740 /* Post the arguments to PayPal. */
741 $request = wp_remote_post( self::get_api_endpoint( $donation->get_test_mode( false ) ), $args );
742
743 if ( is_wp_error( $request ) ) {
744 $donation->log()->add( sprintf(
745 /* translators: %s: error message. */
746 __( 'PayPal refund failed: %s', 'charitable' ),
747 $request->get_error_message()
748 ) );
749
750 return false;
751 }
752
753 $body = array();
754 $code = wp_remote_retrieve_response_code( $request );
755 $message = wp_remote_retrieve_response_message( $request );
756
757 wp_parse_str( wp_remote_retrieve_body( $request ), $body );
758
759 if ( defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG ) {
760 error_log( var_export( $body, true ) );
761 error_log( 'Response Code: ' . $code );
762 error_log( 'Response Message: ' . $message );
763 }
764
765 if ( 200 === (int) $code
766 && 'OK' === $message
767 && isset( $body['ACK'] )
768 && 'success' === strtolower( $body['ACK'] )
769 ) {
770 update_post_meta( $donation->ID, '_paypal_refunded', true );
771
772 $donation->log()->add( sprintf(
773 /* translators: %s: transaction reference. */
774 __( 'PayPal refund transaction ID: %s', 'charitable' ),
775 $body['REFUNDTRANSACTIONID']
776 ) );
777
778 return true;
779 }
780
781 $error = array_key_exists( 'L_LONGMESSAGE0', $body ) ? $body['L_LONGMESSAGE0'] : __( 'Unknown reason', 'charitable' );
782
783 $donation->log()->add( sprintf(
784 /* translators: %s: error message. */
785 __( 'PayPal refund failed: %s', 'charitable' ),
786 $error
787 ) );
788
789 return false;
790 }
791
792 /**
793 * Check whether a particular donation can have its status changed.
794 *
795 * @since 1.6.0
796 *
797 * @param Charitable_Donation $donation The donation object.
798 * @return boolean
799 */
800 public function is_donation_refundable( Charitable_Donation $donation ) {
801 if ( ! self::has_api_credentials( $donation->get_test_mode( false ) ) ) {
802 return false;
803 }
804
805 return strlen( $donation->get_gateway_transaction_id() )
806 && true != get_post_meta( $donation->ID, '_paypal_refunded', true );
807 }
808
809 /**
810 * Return the base of the PayPal request.
811 *
812 * @since 1.0.0
813 * @since 1.5.4 Added $ipn_check parameter.
814 *
815 * @param boolean $ssl_check Whether to check SSL.
816 * @param boolean $ipn_check Whether this is for an IPN request.
817 * @return string
818 */
819 public function get_redirect_url( $ssl_check = false, $ipn_check = false ) {
820 $paypal_uri = $this->use_ssl( $ssl_check, $ipn_check ) ? 'https://' : 'http://';
821
822 if ( charitable_get_option( 'test_mode' ) ) {
823 $paypal_uri .= 'www.sandbox.';
824 } else {
825 $paypal_uri .= $ipn_check ? 'ipnpb.' : 'www.';
826 }
827
828 $paypal_uri .= 'paypal.com/cgi-bin/webscr';
829
830 /**
831 * Filter the PayPal URI.
832 *
833 * @since 1.0.0
834 * @since 1.5.4 Added $ssl_check and $ipn_check parameters.
835 *
836 * @param string $paypal_uri The URL.
837 * @param boolean $ssl_check Whether to check SSL.
838 * @param boolean $ipn_check Whether this is for an IPN request.
839 */
840 return apply_filters( 'charitable_paypal_uri', $paypal_uri, $ssl_check, $ipn_check );
841 }
842
843 /**
844 * Return the PayPal business email.
845 *
846 * @since 1.6.0
847 *
848 * @return string
849 */
850 public static function get_paypal_email() {
851 $option = charitable_get_option( 'test_mode' ) ? 'sandbox_paypal_email' : 'paypal_email';
852
853 /**
854 * Filter the PayPal Email.
855 *
856 * @since 1.6.0
857 *
858 * @param string $paypal_email The email address.
859 */
860 return apply_filters( 'charitable_paypal_email', charitable_get_option( array( 'gateways_paypal', $option ) ) );
861 }
862
863 /**
864 * Returns the current gateway's ID.
865 *
866 * @since 1.0.3
867 *
868 * @return string
869 */
870 public static function get_gateway_id() {
871 return self::ID;
872 }
873
874 /**
875 * Cancel a subscription in the payment gateway.
876 *
877 * @since 1.5.9
878 *
879 * @param int $subscription_id The ID of the subscription/recurring donation.
880 * @return boolean True if the subscription was successfully cancelled. False otherwise.
881 */
882 public function cancel_subscription( $subscription_id ) {
883 /* @todo Handle subscription cancellation. */
884 return true;
885 }
886
887 /**
888 * Return whether to use SSL.
889 *
890 * @since 1.5.4
891 *
892 * @param boolean $ssl_check Whether to check SSL.
893 * @param boolean $ipn_check Whether this is for an IPN request.
894 * @return boolean
895 */
896 private function use_ssl( $ssl_check = false, $ipn_check = false ) {
897 return $ipn_check || ! $ssl_check || is_ssl();
898 }
899
900 /**
901 * Returns whether the API response we received is valid.
902 *
903 * @since 1.5.4
904 *
905 * @param array|WP_Error $api_response Array in case of successful request. WP_Error otherwise.
906 * @return boolean
907 */
908 private function is_valid_api_response( $api_response ) {
909 return ! is_wp_error( $api_response ) && 'VERIFIED' == $api_response['body'];
910 }
911
912 /**
913 * Returns whether the IPN request is valid.
914 *
915 * @since 1.5.4
916 *
917 * @return boolean
918 */
919 private static function is_valid_request() {
920 return ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' == $_SERVER['REQUEST_METHOD'];
921 }
922 }
923
924 endif;
925