| 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( '&', '&', $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 |
|