WC_Gateway_Paypal_IPN_Handler

Handles responses from PayPal IPN.

Defined (1)

The class is defined in the following location(s).

/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php  
  1. class WC_Gateway_Paypal_IPN_Handler extends WC_Gateway_Paypal_Response { 
  2.  
  3. /** @var string Receiver email address to validate */ 
  4. protected $receiver_email; 
  5.  
  6. /** 
  7. * Constructor. 
  8. * @param bool $sandbox 
  9. * @param string $receiver_email 
  10. */ 
  11. public function __construct( $sandbox = false, $receiver_email = '' ) { 
  12. add_action( 'woocommerce_api_wc_gateway_paypal', array( $this, 'check_response' ) ); 
  13. add_action( 'valid-paypal-standard-ipn-request', array( $this, 'valid_response' ) ); 
  14.  
  15. $this->receiver_email = $receiver_email; 
  16. $this->sandbox = $sandbox; 
  17.  
  18. /** 
  19. * Check for PayPal IPN Response. 
  20. */ 
  21. public function check_response() { 
  22. if ( ! empty( $_POST ) && $this->validate_ipn() ) { 
  23. $posted = wp_unslash( $_POST ); 
  24.  
  25. // @codingStandardsIgnoreStart 
  26. do_action( 'valid-paypal-standard-ipn-request', $posted ); 
  27. // @codingStandardsIgnoreEnd 
  28. exit; 
  29.  
  30. wp_die( 'PayPal IPN Request Failure', 'PayPal IPN', array( 'response' => 500 ) ); 
  31.  
  32. /** 
  33. * There was a valid response. 
  34. * @param array $posted Post data after wp_unslash 
  35. */ 
  36. public function valid_response( $posted ) { 
  37. if ( ! empty( $posted['custom'] ) && ( $order = $this->get_paypal_order( $posted['custom'] ) ) ) { 
  38.  
  39. // Lowercase returned variables. 
  40. $posted['payment_status'] = strtolower( $posted['payment_status'] ); 
  41.  
  42. // Sandbox fix. 
  43. if ( isset( $posted['test_ipn'] ) && 1 == $posted['test_ipn'] && 'pending' == $posted['payment_status'] ) { 
  44. $posted['payment_status'] = 'completed'; 
  45.  
  46. WC_Gateway_Paypal::log( 'Found order #' . $order->get_id() ); 
  47. WC_Gateway_Paypal::log( 'Payment status: ' . $posted['payment_status'] ); 
  48.  
  49. if ( method_exists( $this, 'payment_status_' . $posted['payment_status'] ) ) { 
  50. call_user_func( array( $this, 'payment_status_' . $posted['payment_status'] ), $order, $posted ); 
  51.  
  52. /** 
  53. * Check PayPal IPN validity. 
  54. */ 
  55. public function validate_ipn() { 
  56. WC_Gateway_Paypal::log( 'Checking IPN response is valid' ); 
  57.  
  58. // Get received values from post data 
  59. $validate_ipn = wp_unslash( $_POST ); 
  60. $validate_ipn['cmd'] = '_notify-validate'; 
  61.  
  62. // Send back post vars to paypal 
  63. $params = array( 
  64. 'body' => $validate_ipn,  
  65. 'timeout' => 60,  
  66. 'httpversion' => '1.1',  
  67. 'compress' => false,  
  68. 'decompress' => false,  
  69. 'user-agent' => 'WooCommerce/' . WC()->version,  
  70. ); 
  71.  
  72. // Post back to get a response. 
  73. $response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $params ); 
  74.  
  75. WC_Gateway_Paypal::log( 'IPN Request: ' . wc_print_r( $params, true ) ); 
  76. WC_Gateway_Paypal::log( 'IPN Response: ' . wc_print_r( $response, true ) ); 
  77.  
  78. // Check to see if the request was valid. 
  79. if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 && strstr( $response['body'], 'VERIFIED' ) ) { 
  80. WC_Gateway_Paypal::log( 'Received valid response from PayPal' ); 
  81. return true; 
  82.  
  83. WC_Gateway_Paypal::log( 'Received invalid response from PayPal' ); 
  84.  
  85. if ( is_wp_error( $response ) ) { 
  86. WC_Gateway_Paypal::log( 'Error response: ' . $response->get_error_message() ); 
  87.  
  88. return false; 
  89.  
  90. /** 
  91. * Check for a valid transaction type. 
  92. * @param string $txn_type 
  93. */ 
  94. protected function validate_transaction_type( $txn_type ) { 
  95. $accepted_types = array( 'cart', 'instant', 'express_checkout', 'web_accept', 'masspay', 'send_money', 'paypal_here' ); 
  96.  
  97. if ( ! in_array( strtolower( $txn_type ), $accepted_types ) ) { 
  98. WC_Gateway_Paypal::log( 'Aborting, Invalid type:' . $txn_type ); 
  99. exit; 
  100.  
  101. /** 
  102. * Check currency from IPN matches the order. 
  103. * @param WC_Order $order 
  104. * @param string $currency 
  105. */ 
  106. protected function validate_currency( $order, $currency ) { 
  107. if ( $order->get_currency() != $currency ) { 
  108. WC_Gateway_Paypal::log( 'Payment error: Currencies do not match (sent "' . $order->get_currency() . '" | returned "' . $currency . '")' ); 
  109.  
  110. // Put this order on-hold for manual checking. 
  111. $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal currencies do not match (code %s).', 'woocommerce' ), $currency ) ); 
  112. exit; 
  113.  
  114. /** 
  115. * Check payment amount from IPN matches the order. 
  116. * @param WC_Order $order 
  117. * @param int $amount 
  118. */ 
  119. protected function validate_amount( $order, $amount ) { 
  120. if ( number_format( $order->get_total(), 2, '.', '' ) != number_format( $amount, 2, '.', '' ) ) { 
  121. WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (gross ' . $amount . ')' ); 
  122.  
  123. // Put this order on-hold for manual checking. 
  124. $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal amounts do not match (gross %s).', 'woocommerce' ), $amount ) ); 
  125. exit; 
  126.  
  127. /** 
  128. * Check receiver email from PayPal. If the receiver email in the IPN is different than what is stored in. 
  129. * WooCommerce -> Settings -> Checkout -> PayPal, it will log an error about it. 
  130. * @param WC_Order $order 
  131. * @param string $receiver_email 
  132. */ 
  133. protected function validate_receiver_email( $order, $receiver_email ) { 
  134. if ( strcasecmp( trim( $receiver_email ), trim( $this->receiver_email ) ) != 0 ) { 
  135. WC_Gateway_Paypal::log( "IPN Response is for another account: {$receiver_email}. Your email is {$this->receiver_email}" ); 
  136.  
  137. // Put this order on-hold for manual checking. 
  138. $order->update_status( 'on-hold', sprintf( __( 'Validation error: PayPal IPN response from a different email address (%s).', 'woocommerce' ), $receiver_email ) ); 
  139. exit; 
  140.  
  141. /** 
  142. * Handle a completed payment. 
  143. * @param WC_Order $order 
  144. * @param array $posted 
  145. */ 
  146. protected function payment_status_completed( $order, $posted ) { 
  147. if ( $order->has_status( wc_get_is_paid_statuses() ) ) { 
  148. WC_Gateway_Paypal::log( 'Aborting, Order #' . $order->get_id() . ' is already complete.' ); 
  149. exit; 
  150.  
  151. $this->validate_transaction_type( $posted['txn_type'] ); 
  152. $this->validate_currency( $order, $posted['mc_currency'] ); 
  153. $this->validate_amount( $order, $posted['mc_gross'] ); 
  154. $this->validate_receiver_email( $order, $posted['receiver_email'] ); 
  155. $this->save_paypal_meta_data( $order, $posted ); 
  156.  
  157. if ( 'completed' === $posted['payment_status'] ) { 
  158. if ( $order->has_status( 'cancelled' ) ) { 
  159. $this->payment_status_paid_cancelled_order( $order, $posted ); 
  160.  
  161. $this->payment_complete( $order, ( ! empty( $posted['txn_id'] ) ? wc_clean( $posted['txn_id'] ) : '' ), __( 'IPN payment completed', 'woocommerce' ) ); 
  162.  
  163. if ( ! empty( $posted['mc_fee'] ) ) { 
  164. // Log paypal transaction fee. 
  165. update_post_meta( $order->get_id(), 'PayPal Transaction Fee', wc_clean( $posted['mc_fee'] ) ); 
  166. } else { 
  167. if ( 'authorization' === $posted['pending_reason'] ) { 
  168. $this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) ); 
  169. } else { 
  170. $this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $posted['pending_reason'] ) ); 
  171.  
  172. /** 
  173. * Handle a pending payment. 
  174. * @param WC_Order $order 
  175. * @param array $posted 
  176. */ 
  177. protected function payment_status_pending( $order, $posted ) { 
  178. $this->payment_status_completed( $order, $posted ); 
  179.  
  180. /** 
  181. * Handle a failed payment. 
  182. * @param WC_Order $order 
  183. * @param array $posted 
  184. */ 
  185. protected function payment_status_failed( $order, $posted ) { 
  186. $order->update_status( 'failed', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) ); 
  187.  
  188. /** 
  189. * Handle a denied payment. 
  190. * @param WC_Order $order 
  191. * @param array $posted 
  192. */ 
  193. protected function payment_status_denied( $order, $posted ) { 
  194. $this->payment_status_failed( $order, $posted ); 
  195.  
  196. /** 
  197. * Handle an expired payment. 
  198. * @param WC_Order $order 
  199. * @param array $posted 
  200. */ 
  201. protected function payment_status_expired( $order, $posted ) { 
  202. $this->payment_status_failed( $order, $posted ); 
  203.  
  204. /** 
  205. * Handle a voided payment. 
  206. * @param WC_Order $order 
  207. * @param array $posted 
  208. */ 
  209. protected function payment_status_voided( $order, $posted ) { 
  210. $this->payment_status_failed( $order, $posted ); 
  211.  
  212. /** 
  213. * When a user cancelled order is marked paid. 
  214. * @param WC_Order $order 
  215. * @param array $posted 
  216. */ 
  217. protected function payment_status_paid_cancelled_order( $order, $posted ) { 
  218. $this->send_ipn_email_notification( 
  219. sprintf( __( 'Payment for cancelled order %s received', 'woocommerce' ), '<a class="link" href="' . esc_url( admin_url( 'post.php?post=' . $order->get_id() . '&action=edit' ) ) . '">' . $order->get_order_number() . '</a>' ),  
  220. sprintf( __( 'Order #%1$s has been marked paid by PayPal IPN, but was previously cancelled. Admin handling required.', 'woocommerce' ), $order->get_order_number() ) 
  221. ); 
  222.  
  223. /** 
  224. * Handle a refunded order. 
  225. * @param WC_Order $order 
  226. * @param array $posted 
  227. */ 
  228. protected function payment_status_refunded( $order, $posted ) { 
  229. // Only handle full refunds, not partial. 
  230. if ( $order->get_total() == ( $posted['mc_gross'] * -1 ) ) { 
  231.  
  232. // Mark order as refunded. 
  233. $order->update_status( 'refunded', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) ); 
  234.  
  235. $this->send_ipn_email_notification( 
  236. sprintf( __( 'Payment for order %s refunded', 'woocommerce' ), '<a class="link" href="' . esc_url( admin_url( 'post.php?post=' . $order->get_id() . '&action=edit' ) ) . '">' . $order->get_order_number() . '</a>' ),  
  237. sprintf( __( 'Order #%1$s has been marked as refunded - PayPal reason code: %2$s', 'woocommerce' ), $order->get_order_number(), $posted['reason_code'] ) 
  238. ); 
  239.  
  240. /** 
  241. * Handle a reversal. 
  242. * @param WC_Order $order 
  243. * @param array $posted 
  244. */ 
  245. protected function payment_status_reversed( $order, $posted ) { 
  246. $order->update_status( 'on-hold', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) ); 
  247.  
  248. $this->send_ipn_email_notification( 
  249. sprintf( __( 'Payment for order %s reversed', 'woocommerce' ), '<a class="link" href="' . esc_url( admin_url( 'post.php?post=' . $order->get_id() . '&action=edit' ) ) . '">' . $order->get_order_number() . '</a>' ),  
  250. 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'] ) ) 
  251. ); 
  252.  
  253. /** 
  254. * Handle a cancelled reversal. 
  255. * @param WC_Order $order 
  256. * @param array $posted 
  257. */ 
  258. protected function payment_status_canceled_reversal( $order, $posted ) { 
  259. $this->send_ipn_email_notification( 
  260. sprintf( __( 'Reversal cancelled for order #%s', 'woocommerce' ), $order->get_order_number() ),  
  261. 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( admin_url( 'post.php?post=' . $order->get_id() . '&action=edit' ) ) ) 
  262. ); 
  263.  
  264. /** 
  265. * Save important data from the IPN to the order. 
  266. * @param WC_Order $order 
  267. * @param array $posted 
  268. */ 
  269. protected function save_paypal_meta_data( $order, $posted ) { 
  270. if ( ! empty( $posted['payer_email'] ) ) { 
  271. update_post_meta( $order->get_id(), 'Payer PayPal address', wc_clean( $posted['payer_email'] ) ); 
  272. if ( ! empty( $posted['first_name'] ) ) { 
  273. update_post_meta( $order->get_id(), 'Payer first name', wc_clean( $posted['first_name'] ) ); 
  274. if ( ! empty( $posted['last_name'] ) ) { 
  275. update_post_meta( $order->get_id(), 'Payer last name', wc_clean( $posted['last_name'] ) ); 
  276. if ( ! empty( $posted['payment_type'] ) ) { 
  277. update_post_meta( $order->get_id(), 'Payment type', wc_clean( $posted['payment_type'] ) ); 
  278. if ( ! empty( $posted['txn_id'] ) ) { 
  279. update_post_meta( $order->get_id(), '_transaction_id', wc_clean( $posted['txn_id'] ) ); 
  280. if ( ! empty( $posted['payment_status'] ) ) { 
  281. update_post_meta( $order->get_id(), '_paypal_status', wc_clean( $posted['payment_status'] ) ); 
  282.  
  283. /** 
  284. * Send a notification to the user handling orders. 
  285. * @param string $subject 
  286. * @param string $message 
  287. */ 
  288. protected function send_ipn_email_notification( $subject, $message ) { 
  289. $new_order_settings = get_option( 'woocommerce_new_order_settings', array() ); 
  290. $mailer = WC()->mailer(); 
  291. $message = $mailer->wrap_message( $subject, $message ); 
  292.  
  293. $mailer->send( ! empty( $new_order_settings['recipient'] ) ? $new_order_settings['recipient'] : get_option( 'admin_email' ), strip_tags( $subject ), $message );