PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.11.3
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.11.3
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.8.11.3, at includes/gateways/class-charitable-gateway-paypal.php

927 lines 30.0 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) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 * @version 1.8.9.1
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( esc_html( __( 'Invalid Request', 'charitable' ) ) );
337 }
338
339 $gateway = new Charitable_Gateway_Paypal();
340 $data = $gateway->get_encoded_ipn_data();
341
342 if ( charitable_is_debug() ) {
343 error_log( var_export( $data, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_var_export
344 }
345
346 if ( empty( $data ) ) {
347 die( esc_html( __( 'Empty Data', 'charitable' ) ) );
348 }
349
350 if ( ! $gateway->paypal_ipn_verification( $data ) ) {
351 die( esc_html( __( '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( esc_html( __( '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( esc_html( __( '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( esc_html( __( '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' ), wp_json_encode( $data ) );
424 $donation->log()->add( $message );
425 $donation->update_status( 'charitable-failed' );
426 die( esc_html( __( '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' ), wp_json_encode( $data ) );
434 $donation->log()->add( $message );
435 $donation->update_status( 'charitable-failed' );
436
437 die( esc_html( __( '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( esc_html( __( '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( esc_html( __( '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( esc_html( __( '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' ), wp_json_encode( $data ) );
485 $donation->log()->add( $message );
486 $donation->update_status( 'charitable-failed' );
487
488 die( esc_html( __( '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' ), wp_json_encode( $data ) );
496 $donation->log()->add( $message );
497 $donation->update_status( 'charitable-failed' );
498
499 die( esc_html( __( '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( esc_html__( '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( esc_html( __( 'Donation Pending', 'charitable' ) ) );
530
531 }
532
533 die( esc_html( __( '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' ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
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 // phpcs:disable WordPress.Security.NonceVerification.Missing
565 if ( empty( $_POST ) ) {
566 return array();
567 }
568
569 $data = array(
570 'cmd' => '_notify-validate',
571 );
572
573 return array_merge( $data, $_POST );
574 // phpcs:enable WordPress.Security.NonceVerification.Missing
575
576 }
577
578 /**
579 * Validates an IPN request with PayPal.
580 *
581 * @since 1.0.0
582 *
583 * @param mixed[] $data Data received from PayPal.
584 * @return boolean
585 */
586 public function paypal_ipn_verification( $data ) {
587 if ( $this->get_value( 'disable_ipn_verification' ) ) {
588 return true;
589 }
590
591 $remote_post_vars = array(
592 'method' => 'POST',
593 'timeout' => 45,
594 'redirection' => 5,
595 'httpversion' => '1.1',
596 'blocking' => true,
597 'headers' => array(
598 'host' => 'www.paypal.com',
599 'connection' => 'close',
600 'content-type' => 'application/x-www-form-urlencoded',
601 'post' => '/cgi-bin/webscr HTTP/1.1',
602
603 ),
604 'sslverify' => false,
605 'body' => $data,
606 );
607
608 /* Get response */
609 $api_response = wp_remote_post( $this->get_redirect_url( true, true ), $remote_post_vars );
610
611 /**
612 * Filter whether the PayPal IPN was verified.
613 *
614 * @since 1.0.0
615 *
616 * @param boolean $valid Whether it has been verified.
617 * @param array|WP_Error $api_response Array in case of successful request. WP_Error otherwise.
618 */
619 return apply_filters( 'charitable_paypal_ipn_verification', $this->is_valid_api_response( $api_response ), $api_response );
620 }
621
622 /**
623 * Return a note to log for a pending payment.
624 *
625 * @since 1.0.0
626 *
627 * @param string $reason_code The reason code received from PayPal.
628 * @return string
629 */
630 public function get_pending_reason_note( $reason_code ) {
631 switch ( $reason_code ) {
632 case 'echeck':
633 $note = __( 'Payment made via eCheck and will clear automatically in 5-8 days', 'charitable' );
634 break;
635
636 case 'address':
637 $note = __( 'Payment requires a confirmed customer address and must be accepted manually through PayPal', 'charitable' );
638 break;
639
640 case 'intl':
641 $note = __( 'Payment must be accepted manually through PayPal due to international account regulations', 'charitable' );
642 break;
643
644 case 'multi-currency':
645 $note = __( 'Payment received in non-shop currency and must be accepted manually through PayPal', 'charitable' );
646 break;
647
648 case 'paymentreview':
649 case 'regulatory_review':
650 $note = __( 'Payment is being reviewed by PayPal staff as high-risk or in possible violation of government regulations', 'charitable' );
651 break;
652
653 case 'unilateral':
654 $note = __( 'Payment was sent to non-confirmed or non-registered email address.', 'charitable' );
655 break;
656
657 case 'upgrade':
658 $note = __( 'PayPal account must be upgraded before this payment can be accepted', 'charitable' );
659 break;
660
661 case 'verify':
662 $note = __( 'PayPal account is not verified. Verify account in order to accept this payment', 'charitable' );
663 break;
664
665 default:
666 /* translators: %s: reason code given by PayPal */
667 $note = sprintf( __( 'Payment is pending for unknown reasons. Contact PayPal support for assistance. Reason code: %s', 'charitable' ), $reason_code );
668 }
669
670 /**
671 * Filter the donation log note added about the reason for the pending donation.
672 *
673 * @since 1.0.0
674 *
675 * @param string $note The note.
676 * @param string $reason_code The reason code received from PayPal.
677 */
678 return apply_filters( 'charitable_paypal_gateway_pending_reason_note', $note, $reason_code );
679 }
680
681 /**
682 * Process a donation's refund.
683 *
684 * @since 1.6.0
685 *
686 * @param int $donation_id The donation ID.
687 * @return boolean
688 */
689 public static function process_refund( $donation_id ) {
690 $donation = charitable_get_donation( $donation_id );
691
692 if ( ! $donation ) {
693 return false;
694 }
695
696 $credentials = self::get_api_credentials( $donation->get_test_mode( false ) );
697
698 $n = 0;
699 $body = array(
700 'METHOD' => 'RefundTransaction',
701 'TRANSACTIONID' => $donation->get_gateway_transaction_id(),
702 'REFUNDTYPE' => 'Full',
703 'USER' => $credentials['username'],
704 'PWD' => $credentials['password'],
705 'SIGNATURE' => $credentials['signature'],
706 'VERSION' => '204',
707 );
708
709 foreach ( $donation->get_campaign_donations() as $campaign_donation ) {
710 $body[ 'L_INVOICEITEMNAME' . $n ] = $campaign_donation->campaign_name;
711 $body[ 'L_DESCRIPTION' . $n ] = __( 'Donation', 'charitable' );
712 $body[ 'L_PRICE' . $n ] = Charitable_Currency::get_instance()->sanitize_monetary_amount( (string) $campaign_donation->amount );
713 $body[ 'L_PRICECURRENCYCODE' . $n ] = charitable_get_currency();
714
715 $n += 1;
716 }
717
718 /**
719 * Filter the PayPal refund args.
720 *
721 * @since 1.6.0
722 *
723 * @param array $body The arguments we're sending to PayPal.
724 * @param Charitable_Donation $donation The donation instance.
725 */
726 $body = apply_filters( 'charitable_paypal_refund_args', $body, $donation );
727 $headers = array(
728 'Content-Type' => 'application/x-www-form-urlencoded',
729 'Cache-Control' => 'no-cache',
730 );
731 $args = array(
732 'body' => $body,
733 'headers' => $headers,
734 'timeout' => 30,
735 'httpversion' => '1.1',
736 );
737
738 if ( charitable_is_debug() ) {
739 error_log( var_export( $args, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_var_export
740 }
741
742 /* Post the arguments to PayPal. */
743 $request = wp_remote_post( self::get_api_endpoint( $donation->get_test_mode( false ) ), $args );
744
745 if ( is_wp_error( $request ) ) {
746 $donation->log()->add( sprintf(
747 /* translators: %s: error message. */
748 __( 'PayPal refund failed: %s', 'charitable' ),
749 $request->get_error_message()
750 ) );
751
752 return false;
753 }
754
755 $body = array();
756 $code = wp_remote_retrieve_response_code( $request );
757 $message = wp_remote_retrieve_response_message( $request );
758
759 wp_parse_str( wp_remote_retrieve_body( $request ), $body );
760
761 if ( charitable_is_debug() ) {
762 error_log( var_export( $body, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_var_export
763 error_log( 'Response Code: ' . $code ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
764 error_log( 'Response Message: ' . $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
765 }
766
767 if ( 200 === (int) $code
768 && 'OK' === $message
769 && isset( $body['ACK'] )
770 && 'success' === strtolower( $body['ACK'] )
771 ) {
772 update_post_meta( $donation->ID, '_paypal_refunded', true );
773
774 $donation->log()->add( sprintf(
775 /* translators: %s: transaction reference. */
776 __( 'PayPal refund transaction ID: %s', 'charitable' ),
777 $body['REFUNDTRANSACTIONID']
778 ) );
779
780 return true;
781 }
782
783 $error = array_key_exists( 'L_LONGMESSAGE0', $body ) ? $body['L_LONGMESSAGE0'] : __( 'Unknown reason', 'charitable' );
784
785 $donation->log()->add( sprintf(
786 /* translators: %s: error message. */
787 __( 'PayPal refund failed: %s', 'charitable' ),
788 $error
789 ) );
790
791 return false;
792 }
793
794 /**
795 * Check whether a particular donation can have its status changed.
796 *
797 * @since 1.6.0
798 *
799 * @param Charitable_Donation $donation The donation object.
800 * @return boolean
801 */
802 public function is_donation_refundable( Charitable_Donation $donation ) {
803 if ( ! self::has_api_credentials( $donation->get_test_mode( false ) ) ) {
804 return false;
805 }
806
807 return strlen( $donation->get_gateway_transaction_id() )
808 && true != get_post_meta( $donation->ID, '_paypal_refunded', true );
809 }
810
811 /**
812 * Return the base of the PayPal request.
813 *
814 * @since 1.0.0
815 * @since 1.5.4 Added $ipn_check parameter.
816 *
817 * @param boolean $ssl_check Whether to check SSL.
818 * @param boolean $ipn_check Whether this is for an IPN request.
819 * @return string
820 */
821 public function get_redirect_url( $ssl_check = false, $ipn_check = false ) {
822 $paypal_uri = $this->use_ssl( $ssl_check, $ipn_check ) ? 'https://' : 'http://';
823
824 if ( charitable_get_option( 'test_mode' ) ) {
825 $paypal_uri .= 'www.sandbox.';
826 } else {
827 $paypal_uri .= $ipn_check ? 'ipnpb.' : 'www.';
828 }
829
830 $paypal_uri .= 'paypal.com/cgi-bin/webscr';
831
832 /**
833 * Filter the PayPal URI.
834 *
835 * @since 1.0.0
836 * @since 1.5.4 Added $ssl_check and $ipn_check parameters.
837 *
838 * @param string $paypal_uri The URL.
839 * @param boolean $ssl_check Whether to check SSL.
840 * @param boolean $ipn_check Whether this is for an IPN request.
841 */
842 return apply_filters( 'charitable_paypal_uri', $paypal_uri, $ssl_check, $ipn_check );
843 }
844
845 /**
846 * Return the PayPal business email.
847 *
848 * @since 1.6.0
849 *
850 * @return string
851 */
852 public static function get_paypal_email() {
853 $option = charitable_get_option( 'test_mode' ) ? 'sandbox_paypal_email' : 'paypal_email';
854
855 /**
856 * Filter the PayPal Email.
857 *
858 * @since 1.6.0
859 *
860 * @param string $paypal_email The email address.
861 */
862 return apply_filters( 'charitable_paypal_email', charitable_get_option( array( 'gateways_paypal', $option ) ) );
863 }
864
865 /**
866 * Returns the current gateway's ID.
867 *
868 * @since 1.0.3
869 *
870 * @return string
871 */
872 public static function get_gateway_id() {
873 return self::ID;
874 }
875
876 /**
877 * Cancel a subscription in the payment gateway.
878 *
879 * @since 1.5.9
880 *
881 * @param int $subscription_id The ID of the subscription/recurring donation.
882 * @return boolean True if the subscription was successfully cancelled. False otherwise.
883 */
884 public function cancel_subscription( $subscription_id ) {
885 /* @todo Handle subscription cancellation. */
886 return true;
887 }
888
889 /**
890 * Return whether to use SSL.
891 *
892 * @since 1.5.4
893 *
894 * @param boolean $ssl_check Whether to check SSL.
895 * @param boolean $ipn_check Whether this is for an IPN request.
896 * @return boolean
897 */
898 private function use_ssl( $ssl_check = false, $ipn_check = false ) {
899 return $ipn_check || ! $ssl_check || is_ssl();
900 }
901
902 /**
903 * Returns whether the API response we received is valid.
904 *
905 * @since 1.5.4
906 *
907 * @param array|WP_Error $api_response Array in case of successful request. WP_Error otherwise.
908 * @return boolean
909 */
910 private function is_valid_api_response( $api_response ) {
911 return ! is_wp_error( $api_response ) && 'VERIFIED' == $api_response['body'];
912 }
913
914 /**
915 * Returns whether the IPN request is valid.
916 *
917 * @since 1.5.4
918 *
919 * @return boolean
920 */
921 private static function is_valid_request() {
922 return ! isset( $_SERVER['REQUEST_METHOD'] ) || 'POST' == $_SERVER['REQUEST_METHOD'];
923 }
924 }
925
926 endif;
927