Merged in feature/from-pantheon (pull request #16)
code from pantheon * code from pantheon
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Gateway_Paypal_API_Handler file.
|
||||
*
|
||||
* @package WooCommerce\Gateways
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Refunds and other API requests such as capture.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class WC_Gateway_Paypal_API_Handler {
|
||||
|
||||
/**
|
||||
* API Username
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $api_username;
|
||||
|
||||
/**
|
||||
* API Password
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $api_password;
|
||||
|
||||
/**
|
||||
* API Signature
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $api_signature;
|
||||
|
||||
/**
|
||||
* Sandbox
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $sandbox = false;
|
||||
|
||||
/**
|
||||
* Get capture request args.
|
||||
* See https://developer.paypal.com/docs/classic/api/merchant/DoCapture_API_Operation_NVP/.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param float $amount Amount.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_capture_request( $order, $amount = null ) {
|
||||
$request = array(
|
||||
'VERSION' => '84.0',
|
||||
'SIGNATURE' => self::$api_signature,
|
||||
'USER' => self::$api_username,
|
||||
'PWD' => self::$api_password,
|
||||
'METHOD' => 'DoCapture',
|
||||
'AUTHORIZATIONID' => $order->get_transaction_id(),
|
||||
'AMT' => number_format( is_null( $amount ) ? $order->get_total() : $amount, 2, '.', '' ),
|
||||
'CURRENCYCODE' => $order->get_currency(),
|
||||
'COMPLETETYPE' => 'Complete',
|
||||
);
|
||||
return apply_filters( 'woocommerce_paypal_capture_request', $request, $order, $amount );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get refund request args.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param float $amount Refund amount.
|
||||
* @param string $reason Refund reason.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_refund_request( $order, $amount = null, $reason = '' ) {
|
||||
$request = array(
|
||||
'VERSION' => '84.0',
|
||||
'SIGNATURE' => self::$api_signature,
|
||||
'USER' => self::$api_username,
|
||||
'PWD' => self::$api_password,
|
||||
'METHOD' => 'RefundTransaction',
|
||||
'TRANSACTIONID' => $order->get_transaction_id(),
|
||||
'NOTE' => html_entity_decode( wc_trim_string( $reason, 255 ), ENT_NOQUOTES, 'UTF-8' ),
|
||||
'REFUNDTYPE' => 'Full',
|
||||
);
|
||||
if ( ! is_null( $amount ) ) {
|
||||
$request['AMT'] = number_format( $amount, 2, '.', '' );
|
||||
$request['CURRENCYCODE'] = $order->get_currency();
|
||||
$request['REFUNDTYPE'] = 'Partial';
|
||||
}
|
||||
return apply_filters( 'woocommerce_paypal_refund_request', $request, $order, $amount, $reason );
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture an authorization.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param float $amount Amount.
|
||||
* @return object Either an object of name value pairs for a success, or a WP_ERROR object.
|
||||
*/
|
||||
public static function do_capture( $order, $amount = null ) {
|
||||
$raw_response = wp_safe_remote_post(
|
||||
self::$sandbox ? 'https://api-3t.sandbox.paypal.com/nvp' : 'https://api-3t.paypal.com/nvp',
|
||||
array(
|
||||
'method' => 'POST',
|
||||
'body' => self::get_capture_request( $order, $amount ),
|
||||
'timeout' => 70,
|
||||
'user-agent' => 'WooCommerce/' . WC()->version,
|
||||
'httpversion' => '1.1',
|
||||
)
|
||||
);
|
||||
|
||||
WC_Gateway_Paypal::log( 'DoCapture Response: ' . wc_print_r( $raw_response, true ) );
|
||||
|
||||
if ( is_wp_error( $raw_response ) ) {
|
||||
return $raw_response;
|
||||
} elseif ( empty( $raw_response['body'] ) ) {
|
||||
return new WP_Error( 'paypal-api', 'Empty Response' );
|
||||
}
|
||||
|
||||
parse_str( $raw_response['body'], $response );
|
||||
|
||||
return (object) $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refund an order via PayPal.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param float $amount Refund amount.
|
||||
* @param string $reason Refund reason.
|
||||
* @return object Either an object of name value pairs for a success, or a WP_ERROR object.
|
||||
*/
|
||||
public static function refund_transaction( $order, $amount = null, $reason = '' ) {
|
||||
$raw_response = wp_safe_remote_post(
|
||||
self::$sandbox ? 'https://api-3t.sandbox.paypal.com/nvp' : 'https://api-3t.paypal.com/nvp',
|
||||
array(
|
||||
'method' => 'POST',
|
||||
'body' => self::get_refund_request( $order, $amount, $reason ),
|
||||
'timeout' => 70,
|
||||
'user-agent' => 'WooCommerce/' . WC()->version,
|
||||
'httpversion' => '1.1',
|
||||
)
|
||||
);
|
||||
|
||||
WC_Gateway_Paypal::log( 'Refund Response: ' . wc_print_r( $raw_response, true ) );
|
||||
|
||||
if ( is_wp_error( $raw_response ) ) {
|
||||
return $raw_response;
|
||||
} elseif ( empty( $raw_response['body'] ) ) {
|
||||
return new WP_Error( 'paypal-api', 'Empty Response' );
|
||||
}
|
||||
|
||||
parse_str( $raw_response['body'], $response );
|
||||
|
||||
return (object) $response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Here for backwards compatibility.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class WC_Gateway_Paypal_Refund extends WC_Gateway_Paypal_API_Handler {
|
||||
/**
|
||||
* Get refund request args. Proxy to WC_Gateway_Paypal_API_Handler::get_refund_request().
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param float $amount Refund amount.
|
||||
* @param string $reason Refund reason.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_request( $order, $amount = null, $reason = '' ) {
|
||||
return self::get_refund_request( $order, $amount, $reason );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an order refund.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param float $amount Refund amount.
|
||||
* @param string $reason Refund reason.
|
||||
* @param bool $sandbox Whether to use sandbox mode or not.
|
||||
* @return object Either an object of name value pairs for a success, or a WP_ERROR object.
|
||||
*/
|
||||
public static function refund_order( $order, $amount = null, $reason = '', $sandbox = false ) {
|
||||
if ( $sandbox ) {
|
||||
self::$sandbox = $sandbox;
|
||||
}
|
||||
$result = self::refund_transaction( $order, $amount, $reason );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
} else {
|
||||
return (array) $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
/**
|
||||
* Handles responses from PayPal IPN.
|
||||
*
|
||||
* @package WooCommerce\PayPal
|
||||
* @version 3.3.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once dirname( __FILE__ ) . '/class-wc-gateway-paypal-response.php';
|
||||
|
||||
/**
|
||||
* WC_Gateway_Paypal_IPN_Handler class.
|
||||
*/
|
||||
class WC_Gateway_Paypal_IPN_Handler extends WC_Gateway_Paypal_Response {
|
||||
|
||||
/**
|
||||
* Receiver email address to validate.
|
||||
*
|
||||
* @var string Receiver email address.
|
||||
*/
|
||||
protected $receiver_email;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param bool $sandbox Use sandbox or not.
|
||||
* @param string $receiver_email Email to receive IPN from.
|
||||
*/
|
||||
public function __construct( $sandbox = false, $receiver_email = '' ) {
|
||||
add_action( 'woocommerce_api_wc_gateway_paypal', array( $this, 'check_response' ) );
|
||||
add_action( 'valid-paypal-standard-ipn-request', array( $this, 'valid_response' ) );
|
||||
|
||||
$this->receiver_email = $receiver_email;
|
||||
$this->sandbox = $sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for PayPal IPN Response.
|
||||
*/
|
||||
public function check_response() {
|
||||
if ( ! empty( $_POST ) && $this->validate_ipn() ) { // WPCS: CSRF ok.
|
||||
$posted = wp_unslash( $_POST ); // WPCS: CSRF ok, input var ok.
|
||||
|
||||
// phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
|
||||
do_action( 'valid-paypal-standard-ipn-request', $posted );
|
||||
exit;
|
||||
}
|
||||
|
||||
wp_die( 'PayPal IPN Request Failure', 'PayPal IPN', array( 'response' => 500 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* There was a valid response.
|
||||
*
|
||||
* @param array $posted Post data after wp_unslash.
|
||||
*/
|
||||
public function valid_response( $posted ) {
|
||||
$order = ! empty( $posted['custom'] ) ? $this->get_paypal_order( $posted['custom'] ) : false;
|
||||
|
||||
if ( $order ) {
|
||||
|
||||
// Lowercase returned variables.
|
||||
$posted['payment_status'] = strtolower( $posted['payment_status'] );
|
||||
|
||||
WC_Gateway_Paypal::log( 'Found order #' . $order->get_id() );
|
||||
WC_Gateway_Paypal::log( 'Payment status: ' . $posted['payment_status'] );
|
||||
|
||||
if ( method_exists( $this, 'payment_status_' . $posted['payment_status'] ) ) {
|
||||
call_user_func( array( $this, 'payment_status_' . $posted['payment_status'] ), $order, $posted );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check PayPal IPN validity.
|
||||
*/
|
||||
public function validate_ipn() {
|
||||
WC_Gateway_Paypal::log( 'Checking IPN response is valid' );
|
||||
|
||||
// Get received values from post data.
|
||||
$validate_ipn = wp_unslash( $_POST ); // WPCS: CSRF ok, input var ok.
|
||||
$validate_ipn['cmd'] = '_notify-validate';
|
||||
|
||||
// Send back post vars to paypal.
|
||||
$params = array(
|
||||
'body' => $validate_ipn,
|
||||
'timeout' => 60,
|
||||
'httpversion' => '1.1',
|
||||
'compress' => false,
|
||||
'decompress' => false,
|
||||
'user-agent' => 'WooCommerce/' . WC()->version,
|
||||
);
|
||||
|
||||
// Post back to get a response.
|
||||
$response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $params );
|
||||
|
||||
WC_Gateway_Paypal::log( 'IPN Response: ' . wc_print_r( $response, true ) );
|
||||
|
||||
// Check to see if the request was valid.
|
||||
if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 && strstr( $response['body'], 'VERIFIED' ) ) {
|
||||
WC_Gateway_Paypal::log( 'Received valid response from PayPal IPN' );
|
||||
return true;
|
||||
}
|
||||
|
||||
WC_Gateway_Paypal::log( 'Received invalid response from PayPal IPN' );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
WC_Gateway_Paypal::log( 'Error response: ' . $response->get_error_message() );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a valid transaction type.
|
||||
*
|
||||
* @param string $txn_type Transaction type.
|
||||
*/
|
||||
protected function validate_transaction_type( $txn_type ) {
|
||||
$accepted_types = array( 'cart', 'instant', 'express_checkout', 'web_accept', 'masspay', 'send_money', 'paypal_here' );
|
||||
|
||||
if ( ! in_array( strtolower( $txn_type ), $accepted_types, true ) ) {
|
||||
WC_Gateway_Paypal::log( 'Aborting, Invalid type:' . $txn_type );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check currency from IPN matches the order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param string $currency Currency code.
|
||||
*/
|
||||
protected function validate_currency( $order, $currency ) {
|
||||
if ( $order->get_currency() !== $currency ) {
|
||||
WC_Gateway_Paypal::log( 'Payment error: Currencies do not match (sent "' . $order->get_currency() . '" | returned "' . $currency . '")' );
|
||||
|
||||
/* translators: %s: currency code. */
|
||||
$order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal currencies do not match (code %s).', 'woocommerce' ), $currency ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check payment amount from IPN matches the order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param int $amount Amount to validate.
|
||||
*/
|
||||
protected function validate_amount( $order, $amount ) {
|
||||
if ( number_format( $order->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) {
|
||||
WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (gross ' . $amount . ')' );
|
||||
|
||||
/* translators: %s: Amount. */
|
||||
$order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal amounts do not match (gross %s).', 'woocommerce' ), $amount ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check receiver email from PayPal. If the receiver email in the IPN is different than what is stored in.
|
||||
* WooCommerce -> Settings -> Checkout -> PayPal, it will log an error about it.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param string $receiver_email Email to validate.
|
||||
*/
|
||||
protected function validate_receiver_email( $order, $receiver_email ) {
|
||||
if ( strcasecmp( trim( $receiver_email ), trim( $this->receiver_email ) ) !== 0 ) {
|
||||
WC_Gateway_Paypal::log( "IPN Response is for another account: {$receiver_email}. Your email is {$this->receiver_email}" );
|
||||
|
||||
/* translators: %s: email address . */
|
||||
$order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal IPN response from a different email address (%s).', 'woocommerce' ), $receiver_email ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a completed payment.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function payment_status_completed( $order, $posted ) {
|
||||
if ( $order->has_status( wc_get_is_paid_statuses() ) ) {
|
||||
WC_Gateway_Paypal::log( 'Aborting, Order #' . $order->get_id() . ' is already complete.' );
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->validate_transaction_type( $posted['txn_type'] );
|
||||
$this->validate_currency( $order, $posted['mc_currency'] );
|
||||
$this->validate_amount( $order, $posted['mc_gross'] );
|
||||
$this->validate_receiver_email( $order, $posted['receiver_email'] );
|
||||
$this->save_paypal_meta_data( $order, $posted );
|
||||
|
||||
if ( 'completed' === $posted['payment_status'] ) {
|
||||
if ( $order->has_status( 'cancelled' ) ) {
|
||||
$this->payment_status_paid_cancelled_order( $order, $posted );
|
||||
}
|
||||
|
||||
if ( ! empty( $posted['mc_fee'] ) ) {
|
||||
$order->add_meta_data( 'PayPal Transaction Fee', wc_clean( $posted['mc_fee'] ) );
|
||||
}
|
||||
|
||||
$this->payment_complete( $order, ( ! empty( $posted['txn_id'] ) ? wc_clean( $posted['txn_id'] ) : '' ), __( 'IPN payment completed', 'woocommerce' ) );
|
||||
} else {
|
||||
if ( 'authorization' === $posted['pending_reason'] ) {
|
||||
$this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) );
|
||||
} else {
|
||||
/* translators: %s: pending reason. */
|
||||
$this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $posted['pending_reason'] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a pending payment.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function payment_status_pending( $order, $posted ) {
|
||||
$this->payment_status_completed( $order, $posted );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a failed payment.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function payment_status_failed( $order, $posted ) {
|
||||
/* translators: %s: payment status. */
|
||||
$order->update_status( 'failed', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a denied payment.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function payment_status_denied( $order, $posted ) {
|
||||
$this->payment_status_failed( $order, $posted );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an expired payment.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function payment_status_expired( $order, $posted ) {
|
||||
$this->payment_status_failed( $order, $posted );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a voided payment.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function payment_status_voided( $order, $posted ) {
|
||||
$this->payment_status_failed( $order, $posted );
|
||||
}
|
||||
|
||||
/**
|
||||
* When a user cancelled order is marked paid.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function payment_status_paid_cancelled_order( $order, $posted ) {
|
||||
$this->send_ipn_email_notification(
|
||||
/* translators: %s: order link. */
|
||||
sprintf( __( 'Payment for cancelled order %s received', 'woocommerce' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ),
|
||||
/* translators: %s: order ID. */
|
||||
sprintf( __( 'Order #%s has been marked paid by PayPal IPN, but was previously cancelled. Admin handling required.', 'woocommerce' ), $order->get_order_number() )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a refunded order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function payment_status_refunded( $order, $posted ) {
|
||||
// Only handle full refunds, not partial.
|
||||
if ( $order->get_total() === wc_format_decimal( $posted['mc_gross'] * -1, wc_get_price_decimals() ) ) {
|
||||
|
||||
/* translators: %s: payment status. */
|
||||
$order->update_status( 'refunded', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) );
|
||||
|
||||
$this->send_ipn_email_notification(
|
||||
/* translators: %s: order link. */
|
||||
sprintf( __( 'Payment for order %s refunded', 'woocommerce' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ),
|
||||
/* translators: %1$s: order ID, %2$s: reason code. */
|
||||
sprintf( __( 'Order #%1$s has been marked as refunded - PayPal reason code: %2$s', 'woocommerce' ), $order->get_order_number(), $posted['reason_code'] )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a reversal.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function payment_status_reversed( $order, $posted ) {
|
||||
/* translators: %s: payment status. */
|
||||
$order->update_status( 'on-hold', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) );
|
||||
|
||||
$this->send_ipn_email_notification(
|
||||
/* translators: %s: order link. */
|
||||
sprintf( __( 'Payment for order %s reversed', 'woocommerce' ), '<a class="link" href="' . esc_url( $order->get_edit_order_url() ) . '">' . $order->get_order_number() . '</a>' ),
|
||||
/* translators: %1$s: order ID, %2$s: reason code. */
|
||||
sprintf( __( 'Order #%1$s has been marked on-hold due to a reversal - PayPal reason code: %2$s', 'woocommerce' ), $order->get_order_number(), wc_clean( $posted['reason_code'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a cancelled reversal.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function payment_status_canceled_reversal( $order, $posted ) {
|
||||
$this->send_ipn_email_notification(
|
||||
/* translators: %s: order link. */
|
||||
sprintf( __( 'Reversal cancelled for order #%s', 'woocommerce' ), $order->get_order_number() ),
|
||||
/* translators: %1$s: order ID, %2$s: order link. */
|
||||
sprintf( __( 'Order #%1$s has had a reversal cancelled. Please check the status of payment and update the order status accordingly here: %2$s', 'woocommerce' ), $order->get_order_number(), esc_url( $order->get_edit_order_url() ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save important data from the IPN to the order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param array $posted Posted data.
|
||||
*/
|
||||
protected function save_paypal_meta_data( $order, $posted ) {
|
||||
if ( ! empty( $posted['payment_type'] ) ) {
|
||||
$order->update_meta_data( 'Payment type', wc_clean( $posted['payment_type'] ) );
|
||||
}
|
||||
if ( ! empty( $posted['txn_id'] ) ) {
|
||||
$order->set_transaction_id( wc_clean( $posted['txn_id'] ) );
|
||||
}
|
||||
if ( ! empty( $posted['payment_status'] ) ) {
|
||||
$order->update_meta_data( '_paypal_status', wc_clean( $posted['payment_status'] ) );
|
||||
}
|
||||
$order->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification to the user handling orders.
|
||||
*
|
||||
* @param string $subject Email subject.
|
||||
* @param string $message Email message.
|
||||
*/
|
||||
protected function send_ipn_email_notification( $subject, $message ) {
|
||||
$new_order_settings = get_option( 'woocommerce_new_order_settings', array() );
|
||||
$mailer = WC()->mailer();
|
||||
$message = $mailer->wrap_message( $subject, $message );
|
||||
|
||||
$woocommerce_paypal_settings = get_option( 'woocommerce_paypal_settings' );
|
||||
if ( ! empty( $woocommerce_paypal_settings['ipn_notification'] ) && 'no' === $woocommerce_paypal_settings['ipn_notification'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$mailer->send( ! empty( $new_order_settings['recipient'] ) ? $new_order_settings['recipient'] : get_option( 'admin_email' ), strip_tags( $subject ), $message );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Gateway_Paypal_PDT_Handler file.
|
||||
*
|
||||
* @package WooCommerce\Gateways
|
||||
*/
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once dirname( __FILE__ ) . '/class-wc-gateway-paypal-response.php';
|
||||
|
||||
/**
|
||||
* Handle PDT Responses from PayPal.
|
||||
*/
|
||||
class WC_Gateway_Paypal_PDT_Handler extends WC_Gateway_Paypal_Response {
|
||||
|
||||
/**
|
||||
* Identity token for PDT support
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $identity_token;
|
||||
|
||||
/**
|
||||
* Receiver email address to validate.
|
||||
*
|
||||
* @var string Receiver email address.
|
||||
*/
|
||||
protected $receiver_email;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param bool $sandbox Whether to use sandbox mode or not.
|
||||
* @param string $identity_token Identity token for PDT support.
|
||||
*/
|
||||
public function __construct( $sandbox = false, $identity_token = '' ) {
|
||||
add_action( 'woocommerce_thankyou_paypal', array( $this, 'check_response_for_order' ) );
|
||||
$this->identity_token = $identity_token;
|
||||
$this->sandbox = $sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set receiver email to enable more strict validation.
|
||||
*
|
||||
* @param string $receiver_email Email to receive PDT notification from.
|
||||
*/
|
||||
public function set_receiver_email( $receiver_email = '' ) {
|
||||
$this->receiver_email = $receiver_email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a PDT transaction to ensure its authentic.
|
||||
*
|
||||
* @param string $transaction TX ID.
|
||||
* @return bool|array False or result array if successful and valid.
|
||||
*/
|
||||
protected function validate_transaction( $transaction ) {
|
||||
$pdt = array(
|
||||
'body' => array(
|
||||
'cmd' => '_notify-synch',
|
||||
'tx' => $transaction,
|
||||
'at' => $this->identity_token,
|
||||
),
|
||||
'timeout' => 60,
|
||||
'httpversion' => '1.1',
|
||||
'user-agent' => 'WooCommerce/' . Constants::get_constant( 'WC_VERSION' ),
|
||||
);
|
||||
|
||||
// Post back to get a response.
|
||||
$response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $pdt );
|
||||
|
||||
if ( is_wp_error( $response ) || strpos( $response['body'], 'SUCCESS' ) !== 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse transaction result data.
|
||||
$transaction_result = array_map( 'wc_clean', array_map( 'urldecode', explode( "\n", $response['body'] ) ) );
|
||||
$transaction_results = array();
|
||||
|
||||
foreach ( $transaction_result as $line ) {
|
||||
$line = explode( '=', $line );
|
||||
$transaction_results[ $line[0] ] = isset( $line[1] ) ? $line[1] : '';
|
||||
}
|
||||
|
||||
if ( ! empty( $transaction_results['charset'] ) && function_exists( 'iconv' ) ) {
|
||||
foreach ( $transaction_results as $key => $value ) {
|
||||
$transaction_results[ $key ] = iconv( $transaction_results['charset'], 'utf-8', $value );
|
||||
}
|
||||
}
|
||||
|
||||
return $transaction_results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Response for PDT, taking the order id from the request.
|
||||
*
|
||||
* @deprecated 6.4 Use check_response_for_order instead.
|
||||
*/
|
||||
public function check_response() {
|
||||
global $wp;
|
||||
$order_id = apply_filters( 'woocommerce_thankyou_order_id', absint( $wp->query_vars['order-received'] ) );
|
||||
|
||||
$this->check_response_for_order( $order_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Response for PDT.
|
||||
*
|
||||
* @since 6.4
|
||||
*
|
||||
* @param mixed $wc_order_id The order id to check the response against.
|
||||
*/
|
||||
public function check_response_for_order( $wc_order_id ) {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( empty( $_REQUEST['tx'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wc_order = wc_get_order( $wc_order_id );
|
||||
if ( ! $wc_order->needs_payment() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$transaction = wc_clean( wp_unslash( $_REQUEST['tx'] ) );
|
||||
$transaction_result = $this->validate_transaction( $transaction );
|
||||
|
||||
if ( $transaction_result ) {
|
||||
$status = strtolower( $transaction_result['payment_status'] );
|
||||
$amount = isset( $transaction_result['mc_gross'] ) ? $transaction_result['mc_gross'] : 0;
|
||||
$order = $this->get_paypal_order( $transaction_result['custom'] );
|
||||
|
||||
if ( ! $order ) {
|
||||
// No valid WC order found on tx data.
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $wc_order->get_id() !== $order->get_id() ) {
|
||||
/* translators: 1: order ID, 2: order ID. */
|
||||
WC_Gateway_Paypal::log( sprintf( __( 'Received PDT notification for order %1$d on endpoint for order %2$d.', 'woocommerce' ), $order->get_id(), $wc_order_id ), 'error' );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 0 !== strcasecmp( trim( $transaction_result['receiver_email'] ), trim( $this->receiver_email ) ) ) {
|
||||
/* translators: 1: email address, 2: order ID . */
|
||||
WC_Gateway_Paypal::log( sprintf( __( 'Received PDT notification for another account: %1$s. Order ID: %2$d.', 'woocommerce' ), $transaction_result['receiver_email'], $order->get_id() ), 'error' );
|
||||
return;
|
||||
}
|
||||
|
||||
// We have a valid response from PayPal.
|
||||
WC_Gateway_Paypal::log( 'PDT Transaction Status: ' . wc_print_r( $status, true ) );
|
||||
|
||||
$order->add_meta_data( '_paypal_status', $status );
|
||||
$order->set_transaction_id( $transaction );
|
||||
|
||||
if ( 'completed' === $status ) {
|
||||
if ( number_format( $order->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) {
|
||||
WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (amt ' . $amount . ')', 'error' );
|
||||
/* translators: 1: Payment amount */
|
||||
$this->payment_on_hold( $order, sprintf( __( 'Validation error: PayPal amounts do not match (amt %s).', 'woocommerce' ), $amount ) );
|
||||
} else {
|
||||
// Log paypal transaction fee and payment type.
|
||||
if ( ! empty( $transaction_result['mc_fee'] ) ) {
|
||||
$order->add_meta_data( 'PayPal Transaction Fee', wc_clean( $transaction_result['mc_fee'] ) );
|
||||
}
|
||||
if ( ! empty( $transaction_result['payment_type'] ) ) {
|
||||
$order->add_meta_data( 'Payment type', wc_clean( $transaction_result['payment_type'] ) );
|
||||
}
|
||||
|
||||
$this->payment_complete( $order, $transaction, __( 'PDT payment completed', 'woocommerce' ) );
|
||||
}
|
||||
} else {
|
||||
if ( 'authorization' === $transaction_result['pending_reason'] ) {
|
||||
$this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) );
|
||||
} else {
|
||||
/* translators: 1: Pending reason */
|
||||
$this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $transaction_result['pending_reason'] ) );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
WC_Gateway_Paypal::log( 'Received invalid response from PayPal PDT' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Gateway_Paypal_Request file.
|
||||
*
|
||||
* @package WooCommerce\Gateways
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\Utilities\NumberUtil;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates requests to send to PayPal.
|
||||
*/
|
||||
class WC_Gateway_Paypal_Request {
|
||||
|
||||
/**
|
||||
* Stores line items to send to PayPal.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $line_items = array();
|
||||
|
||||
/**
|
||||
* Pointer to gateway making the request.
|
||||
*
|
||||
* @var WC_Gateway_Paypal
|
||||
*/
|
||||
protected $gateway;
|
||||
|
||||
/**
|
||||
* Endpoint for requests from PayPal.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $notify_url;
|
||||
|
||||
/**
|
||||
* Endpoint for requests to PayPal.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $endpoint;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param WC_Gateway_Paypal $gateway Paypal gateway object.
|
||||
*/
|
||||
public function __construct( $gateway ) {
|
||||
$this->gateway = $gateway;
|
||||
$this->notify_url = WC()->api_request_url( 'WC_Gateway_Paypal' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PayPal request URL for an order.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param bool $sandbox Whether to use sandbox mode or not.
|
||||
* @return string
|
||||
*/
|
||||
public function get_request_url( $order, $sandbox = false ) {
|
||||
$this->endpoint = $sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr?test_ipn=1&' : 'https://www.paypal.com/cgi-bin/webscr?';
|
||||
$paypal_args = $this->get_paypal_args( $order );
|
||||
$paypal_args['bn'] = 'WooThemes_Cart'; // Append WooCommerce PayPal Partner Attribution ID. This should not be overridden for this gateway.
|
||||
|
||||
// Mask (remove) PII from the logs.
|
||||
$mask = array(
|
||||
'first_name' => '***',
|
||||
'last_name' => '***',
|
||||
'address1' => '***',
|
||||
'address2' => '***',
|
||||
'city' => '***',
|
||||
'state' => '***',
|
||||
'zip' => '***',
|
||||
'country' => '***',
|
||||
'email' => '***@***',
|
||||
'night_phone_a' => '***',
|
||||
'night_phone_b' => '***',
|
||||
'night_phone_c' => '***',
|
||||
);
|
||||
|
||||
WC_Gateway_Paypal::log( 'PayPal Request Args for order ' . $order->get_order_number() . ': ' . wc_print_r( array_merge( $paypal_args, array_intersect_key( $mask, $paypal_args ) ), true ) );
|
||||
|
||||
return $this->endpoint . http_build_query( $paypal_args, '', '&' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit length of an arg.
|
||||
*
|
||||
* @param string $string Argument to limit.
|
||||
* @param integer $limit Limit size in characters.
|
||||
* @return string
|
||||
*/
|
||||
protected function limit_length( $string, $limit = 127 ) {
|
||||
$str_limit = $limit - 3;
|
||||
if ( function_exists( 'mb_strimwidth' ) ) {
|
||||
if ( mb_strlen( $string ) > $limit ) {
|
||||
$string = mb_strimwidth( $string, 0, $str_limit ) . '...';
|
||||
}
|
||||
} else {
|
||||
if ( strlen( $string ) > $limit ) {
|
||||
$string = substr( $string, 0, $str_limit ) . '...';
|
||||
}
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction args for paypal request, except for line item args.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_transaction_args( $order ) {
|
||||
return array_merge(
|
||||
array(
|
||||
'cmd' => '_cart',
|
||||
'business' => $this->gateway->get_option( 'email' ),
|
||||
'no_note' => 1,
|
||||
'currency_code' => get_woocommerce_currency(),
|
||||
'charset' => 'utf-8',
|
||||
'rm' => is_ssl() ? 2 : 1,
|
||||
'upload' => 1,
|
||||
'return' => esc_url_raw( add_query_arg( 'utm_nooverride', '1', $this->gateway->get_return_url( $order ) ) ),
|
||||
'cancel_return' => esc_url_raw( $order->get_cancel_order_url_raw() ),
|
||||
'image_url' => esc_url_raw( $this->gateway->get_option( 'image_url' ) ),
|
||||
'paymentaction' => $this->gateway->get_option( 'paymentaction' ),
|
||||
'invoice' => $this->limit_length( $this->gateway->get_option( 'invoice_prefix' ) . $order->get_order_number(), 127 ),
|
||||
'custom' => wp_json_encode(
|
||||
array(
|
||||
'order_id' => $order->get_id(),
|
||||
'order_key' => $order->get_order_key(),
|
||||
)
|
||||
),
|
||||
'notify_url' => $this->limit_length( $this->notify_url, 255 ),
|
||||
'first_name' => $this->limit_length( $order->get_billing_first_name(), 32 ),
|
||||
'last_name' => $this->limit_length( $order->get_billing_last_name(), 64 ),
|
||||
'address1' => $this->limit_length( $order->get_billing_address_1(), 100 ),
|
||||
'address2' => $this->limit_length( $order->get_billing_address_2(), 100 ),
|
||||
'city' => $this->limit_length( $order->get_billing_city(), 40 ),
|
||||
'state' => $this->get_paypal_state( $order->get_billing_country(), $order->get_billing_state() ),
|
||||
'zip' => $this->limit_length( wc_format_postcode( $order->get_billing_postcode(), $order->get_billing_country() ), 32 ),
|
||||
'country' => $this->limit_length( $order->get_billing_country(), 2 ),
|
||||
'email' => $this->limit_length( $order->get_billing_email() ),
|
||||
),
|
||||
$this->get_phone_number_args( $order ),
|
||||
$this->get_shipping_args( $order )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the default request with line items is too long, generate a new one with only one line item.
|
||||
*
|
||||
* If URL is longer than 2,083 chars, ignore line items and send cart to Paypal as a single item.
|
||||
* One item's name can only be 127 characters long, so the URL should not be longer than limit.
|
||||
* URL character limit via:
|
||||
* https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer.
|
||||
*
|
||||
* @param WC_Order $order Order to be sent to Paypal.
|
||||
* @param array $paypal_args Arguments sent to Paypal in the request.
|
||||
* @return array
|
||||
*/
|
||||
protected function fix_request_length( $order, $paypal_args ) {
|
||||
$max_paypal_length = 2083;
|
||||
$query_candidate = http_build_query( $paypal_args, '', '&' );
|
||||
|
||||
if ( strlen( $this->endpoint . $query_candidate ) <= $max_paypal_length ) {
|
||||
return $paypal_args;
|
||||
}
|
||||
|
||||
return apply_filters(
|
||||
'woocommerce_paypal_args',
|
||||
array_merge(
|
||||
$this->get_transaction_args( $order ),
|
||||
$this->get_line_item_args( $order, true )
|
||||
),
|
||||
$order
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PayPal Args for passing to PP.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_paypal_args( $order ) {
|
||||
WC_Gateway_Paypal::log( 'Generating payment form for order ' . $order->get_order_number() . '. Notify URL: ' . $this->notify_url );
|
||||
|
||||
$force_one_line_item = apply_filters( 'woocommerce_paypal_force_one_line_item', false, $order );
|
||||
|
||||
if ( ( wc_tax_enabled() && wc_prices_include_tax() ) || ! $this->line_items_valid( $order ) ) {
|
||||
$force_one_line_item = true;
|
||||
}
|
||||
|
||||
$paypal_args = apply_filters(
|
||||
'woocommerce_paypal_args',
|
||||
array_merge(
|
||||
$this->get_transaction_args( $order ),
|
||||
$this->get_line_item_args( $order, $force_one_line_item )
|
||||
),
|
||||
$order
|
||||
);
|
||||
|
||||
return $this->fix_request_length( $order, $paypal_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get phone number args for paypal request.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_phone_number_args( $order ) {
|
||||
$phone_number = wc_sanitize_phone_number( $order->get_billing_phone() );
|
||||
|
||||
if ( in_array( $order->get_billing_country(), array( 'US', 'CA' ), true ) ) {
|
||||
$phone_number = ltrim( $phone_number, '+1' );
|
||||
$phone_args = array(
|
||||
'night_phone_a' => substr( $phone_number, 0, 3 ),
|
||||
'night_phone_b' => substr( $phone_number, 3, 3 ),
|
||||
'night_phone_c' => substr( $phone_number, 6, 4 ),
|
||||
);
|
||||
} else {
|
||||
$calling_code = WC()->countries->get_country_calling_code( $order->get_billing_country() );
|
||||
$calling_code = is_array( $calling_code ) ? $calling_code[0] : $calling_code;
|
||||
|
||||
if ( $calling_code ) {
|
||||
$phone_number = str_replace( $calling_code, '', preg_replace( '/^0/', '', $order->get_billing_phone() ) );
|
||||
}
|
||||
|
||||
$phone_args = array(
|
||||
'night_phone_a' => $calling_code,
|
||||
'night_phone_b' => $phone_number,
|
||||
);
|
||||
}
|
||||
return $phone_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shipping args for paypal request.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_shipping_args( $order ) {
|
||||
$shipping_args = array();
|
||||
if ( $order->needs_shipping_address() ) {
|
||||
$shipping_args['address_override'] = $this->gateway->get_option( 'address_override' ) === 'yes' ? 1 : 0;
|
||||
$shipping_args['no_shipping'] = 0;
|
||||
if ( 'yes' === $this->gateway->get_option( 'send_shipping' ) ) {
|
||||
// If we are sending shipping, send shipping address instead of billing.
|
||||
$shipping_args['first_name'] = $this->limit_length( $order->get_shipping_first_name(), 32 );
|
||||
$shipping_args['last_name'] = $this->limit_length( $order->get_shipping_last_name(), 64 );
|
||||
$shipping_args['address1'] = $this->limit_length( $order->get_shipping_address_1(), 100 );
|
||||
$shipping_args['address2'] = $this->limit_length( $order->get_shipping_address_2(), 100 );
|
||||
$shipping_args['city'] = $this->limit_length( $order->get_shipping_city(), 40 );
|
||||
$shipping_args['state'] = $this->get_paypal_state( $order->get_shipping_country(), $order->get_shipping_state() );
|
||||
$shipping_args['country'] = $this->limit_length( $order->get_shipping_country(), 2 );
|
||||
$shipping_args['zip'] = $this->limit_length( wc_format_postcode( $order->get_shipping_postcode(), $order->get_shipping_country() ), 32 );
|
||||
}
|
||||
} else {
|
||||
$shipping_args['no_shipping'] = 1;
|
||||
}
|
||||
return $shipping_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shipping cost line item args for paypal request.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param bool $force_one_line_item Whether one line item was forced by validation or URL length.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_shipping_cost_line_item( $order, $force_one_line_item ) {
|
||||
$line_item_args = array();
|
||||
$shipping_total = $order->get_shipping_total();
|
||||
if ( $force_one_line_item ) {
|
||||
$shipping_total += $order->get_shipping_tax();
|
||||
}
|
||||
|
||||
// Add shipping costs. Paypal ignores anything over 5 digits (999.99 is the max).
|
||||
// We also check that shipping is not the **only** cost as PayPal won't allow payment
|
||||
// if the items have no cost.
|
||||
if ( $order->get_shipping_total() > 0 && $order->get_shipping_total() < 999.99 && $this->number_format( $order->get_shipping_total() + $order->get_shipping_tax(), $order ) !== $this->number_format( $order->get_total(), $order ) ) {
|
||||
$line_item_args['shipping_1'] = $this->number_format( $shipping_total, $order );
|
||||
} elseif ( $order->get_shipping_total() > 0 ) {
|
||||
/* translators: %s: Order shipping method */
|
||||
$this->add_line_item( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ), 1, $this->number_format( $shipping_total, $order ) );
|
||||
}
|
||||
|
||||
return $line_item_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line item args for paypal request as a single line item.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_line_item_args_single_item( $order ) {
|
||||
$this->delete_line_items();
|
||||
|
||||
$all_items_name = $this->get_order_item_names( $order );
|
||||
$this->add_line_item( $all_items_name ? $all_items_name : __( 'Order', 'woocommerce' ), 1, $this->number_format( $order->get_total() - $this->round( $order->get_shipping_total() + $order->get_shipping_tax(), $order ), $order ), $order->get_order_number() );
|
||||
$line_item_args = $this->get_shipping_cost_line_item( $order, true );
|
||||
|
||||
return array_merge( $line_item_args, $this->get_line_items() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line item args for paypal request.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param bool $force_one_line_item Create only one item for this order.
|
||||
* @return array
|
||||
*/
|
||||
protected function get_line_item_args( $order, $force_one_line_item = false ) {
|
||||
$line_item_args = array();
|
||||
|
||||
if ( $force_one_line_item ) {
|
||||
/**
|
||||
* Send order as a single item.
|
||||
*
|
||||
* For shipping, we longer use shipping_1 because paypal ignores it if *any* shipping rules are within paypal, and paypal ignores anything over 5 digits (999.99 is the max).
|
||||
*/
|
||||
$line_item_args = $this->get_line_item_args_single_item( $order );
|
||||
} else {
|
||||
/**
|
||||
* Passing a line item per product if supported.
|
||||
*/
|
||||
$this->prepare_line_items( $order );
|
||||
$line_item_args['tax_cart'] = $this->number_format( $order->get_total_tax(), $order );
|
||||
|
||||
if ( $order->get_total_discount() > 0 ) {
|
||||
$line_item_args['discount_amount_cart'] = $this->number_format( $this->round( $order->get_total_discount(), $order ), $order );
|
||||
}
|
||||
|
||||
$line_item_args = array_merge( $line_item_args, $this->get_shipping_cost_line_item( $order, false ) );
|
||||
$line_item_args = array_merge( $line_item_args, $this->get_line_items() );
|
||||
|
||||
}
|
||||
|
||||
return $line_item_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order item names as a string.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @return string
|
||||
*/
|
||||
protected function get_order_item_names( $order ) {
|
||||
$item_names = array();
|
||||
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$item_name = $item->get_name();
|
||||
$item_meta = wp_strip_all_tags(
|
||||
wc_display_item_meta(
|
||||
$item,
|
||||
array(
|
||||
'before' => '',
|
||||
'separator' => ', ',
|
||||
'after' => '',
|
||||
'echo' => false,
|
||||
'autop' => false,
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ( $item_meta ) {
|
||||
$item_name .= ' (' . $item_meta . ')';
|
||||
}
|
||||
|
||||
$item_names[] = $item_name . ' x ' . $item->get_quantity();
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_paypal_get_order_item_names', implode( ', ', $item_names ), $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order item names as a string.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param WC_Order_Item $item Order item object.
|
||||
* @return string
|
||||
*/
|
||||
protected function get_order_item_name( $order, $item ) {
|
||||
$item_name = $item->get_name();
|
||||
$item_meta = wp_strip_all_tags(
|
||||
wc_display_item_meta(
|
||||
$item,
|
||||
array(
|
||||
'before' => '',
|
||||
'separator' => ', ',
|
||||
'after' => '',
|
||||
'echo' => false,
|
||||
'autop' => false,
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ( $item_meta ) {
|
||||
$item_name .= ' (' . $item_meta . ')';
|
||||
}
|
||||
|
||||
return apply_filters( 'woocommerce_paypal_get_order_item_name', $item_name, $order, $item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all line items.
|
||||
*/
|
||||
protected function get_line_items() {
|
||||
return $this->line_items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all line items.
|
||||
*/
|
||||
protected function delete_line_items() {
|
||||
$this->line_items = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the order has valid line items to use for PayPal request.
|
||||
*
|
||||
* The line items are invalid in case of mismatch in totals or if any amount < 0.
|
||||
*
|
||||
* @param WC_Order $order Order to be examined.
|
||||
* @return bool
|
||||
*/
|
||||
protected function line_items_valid( $order ) {
|
||||
$negative_item_amount = false;
|
||||
$calculated_total = 0;
|
||||
|
||||
// Products.
|
||||
foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) {
|
||||
if ( 'fee' === $item['type'] ) {
|
||||
$item_line_total = $this->number_format( $item['line_total'], $order );
|
||||
$calculated_total += $item_line_total;
|
||||
} else {
|
||||
$item_line_total = $this->number_format( $order->get_item_subtotal( $item, false ), $order );
|
||||
$calculated_total += $item_line_total * $item->get_quantity();
|
||||
}
|
||||
|
||||
if ( $item_line_total < 0 ) {
|
||||
$negative_item_amount = true;
|
||||
}
|
||||
}
|
||||
$mismatched_totals = $this->number_format( $calculated_total + $order->get_total_tax() + $this->round( $order->get_shipping_total(), $order ) - $this->round( $order->get_total_discount(), $order ), $order ) !== $this->number_format( $order->get_total(), $order );
|
||||
return ! $negative_item_amount && ! $mismatched_totals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get line items to send to paypal.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
*/
|
||||
protected function prepare_line_items( $order ) {
|
||||
$this->delete_line_items();
|
||||
|
||||
// Products.
|
||||
foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $item ) {
|
||||
if ( 'fee' === $item['type'] ) {
|
||||
$item_line_total = $this->number_format( $item['line_total'], $order );
|
||||
$this->add_line_item( $item->get_name(), 1, $item_line_total );
|
||||
} else {
|
||||
$product = $item->get_product();
|
||||
$sku = $product ? $product->get_sku() : '';
|
||||
$item_line_total = $this->number_format( $order->get_item_subtotal( $item, false ), $order );
|
||||
$this->add_line_item( $this->get_order_item_name( $order, $item ), $item->get_quantity(), $item_line_total, $sku );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add PayPal Line Item.
|
||||
*
|
||||
* @param string $item_name Item name.
|
||||
* @param int $quantity Item quantity.
|
||||
* @param float $amount Amount.
|
||||
* @param string $item_number Item number.
|
||||
*/
|
||||
protected function add_line_item( $item_name, $quantity = 1, $amount = 0.0, $item_number = '' ) {
|
||||
$index = ( count( $this->line_items ) / 4 ) + 1;
|
||||
|
||||
$item = apply_filters(
|
||||
'woocommerce_paypal_line_item',
|
||||
array(
|
||||
'item_name' => html_entity_decode( wc_trim_string( $item_name ? wp_strip_all_tags( $item_name ) : __( 'Item', 'woocommerce' ), 127 ), ENT_NOQUOTES, 'UTF-8' ),
|
||||
'quantity' => (int) $quantity,
|
||||
'amount' => wc_float_to_string( (float) $amount ),
|
||||
'item_number' => $item_number,
|
||||
),
|
||||
$item_name,
|
||||
$quantity,
|
||||
$amount,
|
||||
$item_number
|
||||
);
|
||||
|
||||
$this->line_items[ 'item_name_' . $index ] = $this->limit_length( $item['item_name'], 127 );
|
||||
$this->line_items[ 'quantity_' . $index ] = $item['quantity'];
|
||||
$this->line_items[ 'amount_' . $index ] = $item['amount'];
|
||||
$this->line_items[ 'item_number_' . $index ] = $this->limit_length( $item['item_number'], 127 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state to send to paypal.
|
||||
*
|
||||
* @param string $cc Country two letter code.
|
||||
* @param string $state State code.
|
||||
* @return string
|
||||
*/
|
||||
protected function get_paypal_state( $cc, $state ) {
|
||||
if ( 'US' === $cc ) {
|
||||
return $state;
|
||||
}
|
||||
|
||||
$states = WC()->countries->get_states( $cc );
|
||||
|
||||
if ( isset( $states[ $state ] ) ) {
|
||||
return $states[ $state ];
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currency has decimals.
|
||||
*
|
||||
* @param string $currency Currency to check.
|
||||
* @return bool
|
||||
*/
|
||||
protected function currency_has_decimals( $currency ) {
|
||||
if ( in_array( $currency, array( 'HUF', 'JPY', 'TWD' ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Round prices.
|
||||
*
|
||||
* @param double $price Price to round.
|
||||
* @param WC_Order $order Order object.
|
||||
* @return double
|
||||
*/
|
||||
protected function round( $price, $order ) {
|
||||
$precision = 2;
|
||||
|
||||
if ( ! $this->currency_has_decimals( $order->get_currency() ) ) {
|
||||
$precision = 0;
|
||||
}
|
||||
|
||||
return NumberUtil::round( $price, $precision );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format prices.
|
||||
*
|
||||
* @param float|int $price Price to format.
|
||||
* @param WC_Order $order Order object.
|
||||
* @return string
|
||||
*/
|
||||
protected function number_format( $price, $order ) {
|
||||
$decimals = 2;
|
||||
|
||||
if ( ! $this->currency_has_decimals( $order->get_currency() ) ) {
|
||||
$decimals = 0;
|
||||
}
|
||||
|
||||
return number_format( $price, $decimals, '.', '' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WC_Gateway_Paypal_Response file.
|
||||
*
|
||||
* @package WooCommerce\Gateways
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Responses.
|
||||
*/
|
||||
abstract class WC_Gateway_Paypal_Response {
|
||||
|
||||
/**
|
||||
* Sandbox mode
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $sandbox = false;
|
||||
|
||||
/**
|
||||
* Get the order from the PayPal 'Custom' variable.
|
||||
*
|
||||
* @param string $raw_custom JSON Data passed back by PayPal.
|
||||
* @return bool|WC_Order object
|
||||
*/
|
||||
protected function get_paypal_order( $raw_custom ) {
|
||||
// We have the data in the correct format, so get the order.
|
||||
$custom = json_decode( $raw_custom );
|
||||
if ( $custom && is_object( $custom ) ) {
|
||||
$order_id = $custom->order_id;
|
||||
$order_key = $custom->order_key;
|
||||
} else {
|
||||
// Nothing was found.
|
||||
WC_Gateway_Paypal::log( 'Order ID and key were not found in "custom".', 'error' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
// We have an invalid $order_id, probably because invoice_prefix has changed.
|
||||
$order_id = wc_get_order_id_by_order_key( $order_key );
|
||||
$order = wc_get_order( $order_id );
|
||||
}
|
||||
|
||||
if ( ! $order || ! hash_equals( $order->get_order_key(), $order_key ) ) {
|
||||
WC_Gateway_Paypal::log( 'Order Keys do not match.', 'error' );
|
||||
return false;
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete order, add transaction ID and note.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param string $txn_id Transaction ID.
|
||||
* @param string $note Payment note.
|
||||
*/
|
||||
protected function payment_complete( $order, $txn_id = '', $note = '' ) {
|
||||
if ( ! $order->has_status( array( 'processing', 'completed' ) ) ) {
|
||||
$order->add_order_note( $note );
|
||||
$order->payment_complete( $txn_id );
|
||||
|
||||
if ( isset( WC()->cart ) ) {
|
||||
WC()->cart->empty_cart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hold order and add note.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
* @param string $reason Reason why the payment is on hold.
|
||||
*/
|
||||
protected function payment_on_hold( $order, $reason = '' ) {
|
||||
$order->update_status( 'on-hold', $reason );
|
||||
|
||||
if ( isset( WC()->cart ) ) {
|
||||
WC()->cart->empty_cart();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
/**
|
||||
* Settings for PayPal Standard Gateway.
|
||||
*
|
||||
* @package WooCommerce\Classes\Payment
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
return array(
|
||||
'enabled' => array(
|
||||
'title' => __( 'Enable/Disable', 'woocommerce' ),
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Enable PayPal Standard', 'woocommerce' ),
|
||||
'default' => 'no',
|
||||
),
|
||||
'title' => array(
|
||||
'title' => __( 'Title', 'woocommerce' ),
|
||||
'type' => 'safe_text',
|
||||
'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
|
||||
'default' => __( 'PayPal', 'woocommerce' ),
|
||||
'desc_tip' => true,
|
||||
),
|
||||
'description' => array(
|
||||
'title' => __( 'Description', 'woocommerce' ),
|
||||
'type' => 'text',
|
||||
'desc_tip' => true,
|
||||
'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ),
|
||||
'default' => __( "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account.", 'woocommerce' ),
|
||||
),
|
||||
'email' => array(
|
||||
'title' => __( 'PayPal email', 'woocommerce' ),
|
||||
'type' => 'email',
|
||||
'description' => __( 'Please enter your PayPal email address; this is needed in order to take payment.', 'woocommerce' ),
|
||||
'default' => get_option( 'admin_email' ),
|
||||
'desc_tip' => true,
|
||||
'placeholder' => 'you@youremail.com',
|
||||
),
|
||||
'advanced' => array(
|
||||
'title' => __( 'Advanced options', 'woocommerce' ),
|
||||
'type' => 'title',
|
||||
'description' => '',
|
||||
),
|
||||
'testmode' => array(
|
||||
'title' => __( 'PayPal sandbox', 'woocommerce' ),
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Enable PayPal sandbox', 'woocommerce' ),
|
||||
'default' => 'no',
|
||||
/* translators: %s: URL */
|
||||
'description' => sprintf( __( 'PayPal sandbox can be used to test payments. Sign up for a <a href="%s">developer account</a>.', 'woocommerce' ), 'https://developer.paypal.com/' ),
|
||||
),
|
||||
'debug' => array(
|
||||
'title' => __( 'Debug log', 'woocommerce' ),
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Enable logging', 'woocommerce' ),
|
||||
'default' => 'no',
|
||||
/* translators: %s: URL */
|
||||
'description' => sprintf( __( 'Log PayPal events, such as IPN requests, inside %s Note: this may log personal information. We recommend using this for debugging purposes only and deleting the logs when finished.', 'woocommerce' ), '<code>' . WC_Log_Handler_File::get_log_file_path( 'paypal' ) . '</code>' ),
|
||||
),
|
||||
'ipn_notification' => array(
|
||||
'title' => __( 'IPN email notifications', 'woocommerce' ),
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Enable IPN email notifications', 'woocommerce' ),
|
||||
'default' => 'yes',
|
||||
'description' => __( 'Send notifications when an IPN is received from PayPal indicating refunds, chargebacks and cancellations.', 'woocommerce' ),
|
||||
),
|
||||
'receiver_email' => array(
|
||||
'title' => __( 'Receiver email', 'woocommerce' ),
|
||||
'type' => 'email',
|
||||
'description' => __( 'If your main PayPal email differs from the PayPal email entered above, input your main receiver email for your PayPal account here. This is used to validate IPN requests.', 'woocommerce' ),
|
||||
'default' => '',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => 'you@youremail.com',
|
||||
),
|
||||
'identity_token' => array(
|
||||
'title' => __( 'PayPal identity token', 'woocommerce' ),
|
||||
'type' => 'text',
|
||||
'description' => __( 'Optionally enable "Payment Data Transfer" (Profile > Profile and Settings > My Selling Tools > Website Preferences) and then copy your identity token here. This will allow payments to be verified without the need for PayPal IPN.', 'woocommerce' ),
|
||||
'default' => '',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => '',
|
||||
),
|
||||
'invoice_prefix' => array(
|
||||
'title' => __( 'Invoice prefix', 'woocommerce' ),
|
||||
'type' => 'text',
|
||||
'description' => __( 'Please enter a prefix for your invoice numbers. If you use your PayPal account for multiple stores ensure this prefix is unique as PayPal will not allow orders with the same invoice number.', 'woocommerce' ),
|
||||
'default' => 'WC-',
|
||||
'desc_tip' => true,
|
||||
),
|
||||
'send_shipping' => array(
|
||||
'title' => __( 'Shipping details', 'woocommerce' ),
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Send shipping details to PayPal instead of billing.', 'woocommerce' ),
|
||||
'description' => __( 'PayPal allows us to send one address. If you are using PayPal for shipping labels you may prefer to send the shipping address rather than billing. Turning this option off may prevent PayPal Seller protection from applying.', 'woocommerce' ),
|
||||
'default' => 'yes',
|
||||
),
|
||||
'address_override' => array(
|
||||
'title' => __( 'Address override', 'woocommerce' ),
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Enable "address_override" to prevent address information from being changed.', 'woocommerce' ),
|
||||
'description' => __( 'PayPal verifies addresses therefore this setting can cause errors (we recommend keeping it disabled).', 'woocommerce' ),
|
||||
'default' => 'no',
|
||||
),
|
||||
'paymentaction' => array(
|
||||
'title' => __( 'Payment action', 'woocommerce' ),
|
||||
'type' => 'select',
|
||||
'class' => 'wc-enhanced-select',
|
||||
'description' => __( 'Choose whether you wish to capture funds immediately or authorize payment only.', 'woocommerce' ),
|
||||
'default' => 'sale',
|
||||
'desc_tip' => true,
|
||||
'options' => array(
|
||||
'sale' => __( 'Capture', 'woocommerce' ),
|
||||
'authorization' => __( 'Authorize', 'woocommerce' ),
|
||||
),
|
||||
),
|
||||
'image_url' => array(
|
||||
'title' => __( 'Image url', 'woocommerce' ),
|
||||
'type' => 'text',
|
||||
'description' => __( 'Optionally enter the URL to a 150x50px image displayed as your logo in the upper left corner of the PayPal checkout pages.', 'woocommerce' ),
|
||||
'default' => '',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'Optional', 'woocommerce' ),
|
||||
),
|
||||
'api_details' => array(
|
||||
'title' => __( 'API credentials', 'woocommerce' ),
|
||||
'type' => 'title',
|
||||
/* translators: %s: URL */
|
||||
'description' => sprintf( __( 'Enter your PayPal API credentials to process refunds via PayPal. Learn how to access your <a href="%s">PayPal API Credentials</a>.', 'woocommerce' ), 'https://developer.paypal.com/webapps/developer/docs/classic/api/apiCredentials/#create-an-api-signature' ),
|
||||
),
|
||||
'api_username' => array(
|
||||
'title' => __( 'Live API username', 'woocommerce' ),
|
||||
'type' => 'text',
|
||||
'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
|
||||
'default' => '',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'Optional', 'woocommerce' ),
|
||||
),
|
||||
'api_password' => array(
|
||||
'title' => __( 'Live API password', 'woocommerce' ),
|
||||
'type' => 'password',
|
||||
'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
|
||||
'default' => '',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'Optional', 'woocommerce' ),
|
||||
),
|
||||
'api_signature' => array(
|
||||
'title' => __( 'Live API signature', 'woocommerce' ),
|
||||
'type' => 'password',
|
||||
'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
|
||||
'default' => '',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'Optional', 'woocommerce' ),
|
||||
),
|
||||
'sandbox_api_username' => array(
|
||||
'title' => __( 'Sandbox API username', 'woocommerce' ),
|
||||
'type' => 'text',
|
||||
'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
|
||||
'default' => '',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'Optional', 'woocommerce' ),
|
||||
),
|
||||
'sandbox_api_password' => array(
|
||||
'title' => __( 'Sandbox API password', 'woocommerce' ),
|
||||
'type' => 'password',
|
||||
'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
|
||||
'default' => '',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'Optional', 'woocommerce' ),
|
||||
),
|
||||
'sandbox_api_signature' => array(
|
||||
'title' => __( 'Sandbox API signature', 'woocommerce' ),
|
||||
'type' => 'password',
|
||||
'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ),
|
||||
'default' => '',
|
||||
'desc_tip' => true,
|
||||
'placeholder' => __( 'Optional', 'woocommerce' ),
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user