woocommerce /
woocommerce-gateway-stripe
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | if ( ! defined( 'ABSPATH' ) ) { |
||
| 3 | exit; |
||
| 4 | } |
||
| 5 | |||
| 6 | /** |
||
| 7 | * Class WC_Stripe_Webhook_Handler. |
||
| 8 | * |
||
| 9 | * Handles webhooks from Stripe on sources that are not immediately chargeable. |
||
| 10 | * @since 4.0.0 |
||
| 11 | */ |
||
| 12 | class WC_Stripe_Webhook_Handler extends WC_Stripe_Payment_Gateway { |
||
| 13 | /** |
||
| 14 | * Delay of retries. |
||
| 15 | * |
||
| 16 | * @var int |
||
| 17 | */ |
||
| 18 | public $retry_interval; |
||
| 19 | |||
| 20 | /** |
||
| 21 | * Is test mode active? |
||
| 22 | * |
||
| 23 | * @var bool |
||
| 24 | */ |
||
| 25 | public $testmode; |
||
| 26 | |||
| 27 | /** |
||
| 28 | * The secret to use when verifying webhooks. |
||
| 29 | * |
||
| 30 | * @var string |
||
| 31 | */ |
||
| 32 | protected $secret; |
||
| 33 | |||
| 34 | /** |
||
| 35 | * Constructor. |
||
| 36 | * |
||
| 37 | * @since 4.0.0 |
||
| 38 | * @version 4.0.0 |
||
| 39 | */ |
||
| 40 | public function __construct() { |
||
| 41 | $this->retry_interval = 2; |
||
| 42 | $stripe_settings = get_option( 'woocommerce_stripe_settings', array() ); |
||
| 43 | $this->testmode = ( ! empty( $stripe_settings['testmode'] ) && 'yes' === $stripe_settings['testmode'] ) ? true : false; |
||
| 44 | $secret_key = ( $this->testmode ? 'test_' : '' ) . 'webhook_secret'; |
||
| 45 | $this->secret = ! empty( $stripe_settings[ $secret_key ] ) ? $stripe_settings[ $secret_key ] : false; |
||
| 46 | |||
| 47 | add_action( 'woocommerce_api_wc_stripe', array( $this, 'check_for_webhook' ) ); |
||
| 48 | } |
||
| 49 | |||
| 50 | /** |
||
| 51 | * Check incoming requests for Stripe Webhook data and process them. |
||
| 52 | * |
||
| 53 | * @since 4.0.0 |
||
| 54 | * @version 4.0.0 |
||
| 55 | */ |
||
| 56 | public function check_for_webhook() { |
||
| 57 | if ( ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) |
||
| 58 | || ! isset( $_GET['wc-api'] ) |
||
| 59 | || ( 'wc_stripe' !== $_GET['wc-api'] ) |
||
| 60 | ) { |
||
| 61 | return; |
||
| 62 | } |
||
| 63 | |||
| 64 | $request_body = file_get_contents( 'php://input' ); |
||
| 65 | $request_headers = array_change_key_case( $this->get_request_headers(), CASE_UPPER ); |
||
| 66 | |||
| 67 | // Validate it to make sure it is legit. |
||
| 68 | if ( $this->is_valid_request( $request_headers, $request_body ) ) { |
||
| 69 | $this->process_webhook( $request_body ); |
||
| 70 | status_header( 200 ); |
||
| 71 | exit; |
||
| 72 | } else { |
||
| 73 | WC_Stripe_Logger::log( 'Incoming webhook failed validation: ' . print_r( $request_body, true ) ); |
||
| 74 | // A webhook endpoint must return a 2xx HTTP status code. |
||
| 75 | // @see https://stripe.com/docs/webhooks/build#return-a-2xx-status-code-quickly |
||
| 76 | status_header( 204 ); |
||
| 77 | exit; |
||
| 78 | } |
||
| 79 | } |
||
| 80 | |||
| 81 | /** |
||
| 82 | * Verify the incoming webhook notification to make sure it is legit. |
||
| 83 | * |
||
| 84 | * @since 4.0.0 |
||
| 85 | * @version 4.0.0 |
||
| 86 | * @param string $request_headers The request headers from Stripe. |
||
| 87 | * @param string $request_body The request body from Stripe. |
||
| 88 | * @return bool |
||
| 89 | */ |
||
| 90 | public function is_valid_request( $request_headers = null, $request_body = null ) { |
||
| 91 | if ( null === $request_headers || null === $request_body ) { |
||
| 92 | return false; |
||
| 93 | } |
||
| 94 | |||
| 95 | if ( ! empty( $request_headers['USER-AGENT'] ) && ! preg_match( '/Stripe/', $request_headers['USER-AGENT'] ) ) { |
||
| 96 | return false; |
||
| 97 | } |
||
| 98 | |||
| 99 | if ( ! empty( $this->secret ) ) { |
||
| 100 | // Check for a valid signature. |
||
| 101 | $signature_format = '/^t=(?P<timestamp>\d+)(?P<signatures>(,v\d+=[a-z0-9]+){1,2})$/'; |
||
| 102 | if ( empty( $request_headers['STRIPE-SIGNATURE'] ) || ! preg_match( $signature_format, $request_headers['STRIPE-SIGNATURE'], $matches ) ) { |
||
| 103 | return false; |
||
| 104 | } |
||
| 105 | |||
| 106 | // Verify the timestamp. |
||
| 107 | $timestamp = intval( $matches['timestamp'] ); |
||
| 108 | if ( abs( $timestamp - time() ) > 5 * MINUTE_IN_SECONDS ) { |
||
| 109 | return; |
||
| 110 | } |
||
| 111 | |||
| 112 | // Generate the expected signature. |
||
| 113 | $signed_payload = $timestamp . '.' . $request_body; |
||
| 114 | $expected_signature = hash_hmac( 'sha256', $signed_payload, $this->secret ); |
||
| 115 | |||
| 116 | // Check if the expected signature is present. |
||
| 117 | if ( ! preg_match( '/,v\d+=' . preg_quote( $expected_signature, '/' ) . '/', $matches['signatures'] ) ) { |
||
| 118 | return false; |
||
| 119 | } |
||
| 120 | } |
||
| 121 | |||
| 122 | return true; |
||
| 123 | } |
||
| 124 | |||
| 125 | /** |
||
| 126 | * Gets the incoming request headers. Some servers are not using |
||
| 127 | * Apache and "getallheaders()" will not work so we may need to |
||
| 128 | * build our own headers. |
||
| 129 | * |
||
| 130 | * @since 4.0.0 |
||
| 131 | * @version 4.0.0 |
||
| 132 | */ |
||
| 133 | public function get_request_headers() { |
||
| 134 | if ( ! function_exists( 'getallheaders' ) ) { |
||
| 135 | $headers = array(); |
||
| 136 | |||
| 137 | foreach ( $_SERVER as $name => $value ) { |
||
| 138 | if ( 'HTTP_' === substr( $name, 0, 5 ) ) { |
||
| 139 | $headers[ str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', substr( $name, 5 ) ) ) ) ) ] = $value; |
||
| 140 | } |
||
| 141 | } |
||
| 142 | |||
| 143 | return $headers; |
||
| 144 | } else { |
||
| 145 | return getallheaders(); |
||
| 146 | } |
||
| 147 | } |
||
| 148 | |||
| 149 | /** |
||
| 150 | * Process webhook payments. |
||
| 151 | * This is where we charge the source. |
||
| 152 | * |
||
| 153 | * @since 4.0.0 |
||
| 154 | * @version 4.0.0 |
||
| 155 | * @param object $notification |
||
| 156 | * @param bool $retry |
||
| 157 | */ |
||
| 158 | public function process_webhook_payment( $notification, $retry = true ) { |
||
| 159 | // The following 3 payment methods are synchronous so does not need to be handle via webhook. |
||
| 160 | if ( 'card' === $notification->data->object->type || 'sepa_debit' === $notification->data->object->type || 'three_d_secure' === $notification->data->object->type ) { |
||
| 161 | return; |
||
| 162 | } |
||
| 163 | |||
| 164 | $order = WC_Stripe_Helper::get_order_by_source_id( $notification->data->object->id ); |
||
| 165 | |||
| 166 | if ( ! $order ) { |
||
| 167 | WC_Stripe_Logger::log( 'Could not find order via source ID: ' . $notification->data->object->id ); |
||
| 168 | return; |
||
| 169 | } |
||
| 170 | |||
| 171 | $order_id = $order->get_id(); |
||
| 172 | $source_id = $notification->data->object->id; |
||
| 173 | |||
| 174 | $is_pending_receiver = ( 'receiver' === $notification->data->object->flow ); |
||
| 175 | |||
| 176 | try { |
||
| 177 | if ( $order->has_status( array( 'processing', 'completed' ) ) ) { |
||
| 178 | return; |
||
| 179 | } |
||
| 180 | |||
| 181 | if ( $order->has_status( 'on-hold' ) && ! $is_pending_receiver ) { |
||
| 182 | return; |
||
| 183 | } |
||
| 184 | |||
| 185 | // Result from Stripe API request. |
||
| 186 | $response = null; |
||
| 187 | |||
| 188 | // This will throw exception if not valid. |
||
| 189 | $this->validate_minimum_order_amount( $order ); |
||
| 190 | |||
| 191 | WC_Stripe_Logger::log( "Info: (Webhook) Begin processing payment for order $order_id for the amount of {$order->get_total()}" ); |
||
| 192 | |||
| 193 | // Prep source object. |
||
| 194 | $source_object = new stdClass(); |
||
| 195 | $source_object->token_id = ''; |
||
| 196 | $source_object->customer = $this->get_stripe_customer_id( $order ); |
||
| 197 | $source_object->source = $source_id; |
||
| 198 | |||
| 199 | // Make the request. |
||
| 200 | $response = WC_Stripe_API::request( $this->generate_payment_request( $order, $source_object ), 'charges', 'POST', true ); |
||
| 201 | $headers = $response['headers']; |
||
| 202 | $response = $response['body']; |
||
| 203 | |||
| 204 | View Code Duplication | if ( ! empty( $response->error ) ) { |
|
| 205 | // Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without. |
||
| 206 | if ( $this->is_no_such_customer_error( $response->error ) ) { |
||
| 207 | delete_user_option( $order->get_customer_id(), '_stripe_customer_id' ); |
||
| 208 | $order->delete_meta_data( '_stripe_customer_id' ); |
||
| 209 | $order->save(); |
||
| 210 | } |
||
| 211 | |||
| 212 | if ( $this->is_no_such_token_error( $response->error ) && $prepared_source->token_id ) { |
||
| 213 | // Source param wrong? The CARD may have been deleted on stripe's end. Remove token and show message. |
||
| 214 | $wc_token = WC_Payment_Tokens::get( $prepared_source->token_id ); |
||
|
0 ignored issues
–
show
|
|||
| 215 | $wc_token->delete(); |
||
| 216 | $localized_message = __( 'This card is no longer available and has been removed.', 'woocommerce-gateway-stripe' ); |
||
| 217 | $order->add_order_note( $localized_message ); |
||
| 218 | throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message ); |
||
| 219 | } |
||
| 220 | |||
| 221 | // We want to retry. |
||
| 222 | if ( $this->is_retryable_error( $response->error ) ) { |
||
| 223 | if ( $retry ) { |
||
| 224 | // Don't do anymore retries after this. |
||
| 225 | if ( 5 <= $this->retry_interval ) { |
||
| 226 | |||
| 227 | return $this->process_webhook_payment( $notification, false ); |
||
| 228 | } |
||
| 229 | |||
| 230 | sleep( $this->retry_interval ); |
||
| 231 | |||
| 232 | $this->retry_interval++; |
||
| 233 | return $this->process_webhook_payment( $notification, true ); |
||
| 234 | } else { |
||
| 235 | $localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' ); |
||
| 236 | $order->add_order_note( $localized_message ); |
||
| 237 | throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message ); |
||
| 238 | } |
||
| 239 | } |
||
| 240 | |||
| 241 | $localized_messages = WC_Stripe_Helper::get_localized_messages(); |
||
| 242 | |||
| 243 | if ( 'card_error' === $response->error->type ) { |
||
| 244 | $localized_message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message; |
||
| 245 | } else { |
||
| 246 | $localized_message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message; |
||
| 247 | } |
||
| 248 | |||
| 249 | $order->add_order_note( $localized_message ); |
||
| 250 | |||
| 251 | throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message ); |
||
| 252 | } |
||
| 253 | |||
| 254 | // To prevent double processing the order on WC side. |
||
| 255 | if ( ! $this->is_original_request( $headers ) ) { |
||
| 256 | return; |
||
| 257 | } |
||
| 258 | |||
| 259 | do_action( 'wc_gateway_stripe_process_webhook_payment', $response, $order ); |
||
| 260 | |||
| 261 | $this->process_response( $response, $order ); |
||
| 262 | |||
| 263 | } catch ( WC_Stripe_Exception $e ) { |
||
| 264 | WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() ); |
||
| 265 | |||
| 266 | do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification, $e ); |
||
| 267 | |||
| 268 | $statuses = array( 'pending', 'failed' ); |
||
| 269 | |||
| 270 | if ( $order->has_status( $statuses ) ) { |
||
| 271 | $this->send_failed_order_email( $order_id ); |
||
| 272 | } |
||
| 273 | } |
||
| 274 | } |
||
| 275 | |||
| 276 | /** |
||
| 277 | * Process webhook disputes that is created. |
||
| 278 | * This is trigger when a fraud is detected or customer processes chargeback. |
||
| 279 | * We want to put the order into on-hold and add an order note. |
||
| 280 | * |
||
| 281 | * @since 4.0.0 |
||
| 282 | * @param object $notification |
||
| 283 | */ |
||
| 284 | public function process_webhook_dispute( $notification ) { |
||
| 285 | $order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge ); |
||
| 286 | |||
| 287 | if ( ! $order ) { |
||
| 288 | WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->charge ); |
||
| 289 | return; |
||
| 290 | } |
||
| 291 | |||
| 292 | /* translators: 1) The URL to the order. */ |
||
| 293 | $order->update_status( 'on-hold', sprintf( __( 'A dispute was created for this order. Response is needed. Please go to your <a href="%s" title="Stripe Dashboard" target="_blank">Stripe Dashboard</a> to review this dispute.', 'woocommerce-gateway-stripe' ), $this->get_transaction_url( $order ) ) ); |
||
| 294 | |||
| 295 | do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification ); |
||
| 296 | |||
| 297 | $order_id = $order->get_id(); |
||
| 298 | $this->send_failed_order_email( $order_id ); |
||
| 299 | } |
||
| 300 | |||
| 301 | /** |
||
| 302 | * Process webhook capture. This is used for an authorized only |
||
| 303 | * transaction that is later captured via Stripe not WC. |
||
| 304 | * |
||
| 305 | * @since 4.0.0 |
||
| 306 | * @version 4.0.0 |
||
| 307 | * @param object $notification |
||
| 308 | */ |
||
| 309 | public function process_webhook_capture( $notification ) { |
||
| 310 | $order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id ); |
||
| 311 | |||
| 312 | if ( ! $order ) { |
||
| 313 | WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id ); |
||
| 314 | return; |
||
| 315 | } |
||
| 316 | |||
| 317 | if ( 'stripe' === $order->get_payment_method() ) { |
||
| 318 | $charge = $order->get_transaction_id(); |
||
| 319 | $captured = $order->get_meta( '_stripe_charge_captured', true ); |
||
| 320 | |||
| 321 | if ( $charge && 'no' === $captured ) { |
||
| 322 | $order->update_meta_data( '_stripe_charge_captured', 'yes' ); |
||
| 323 | |||
| 324 | // Store other data such as fees |
||
| 325 | $order->set_transaction_id( $notification->data->object->id ); |
||
| 326 | |||
| 327 | if ( isset( $notification->data->object->balance_transaction ) ) { |
||
| 328 | $this->update_fees( $order, $notification->data->object->balance_transaction ); |
||
| 329 | } |
||
| 330 | |||
| 331 | // Check and see if capture is partial. |
||
| 332 | if ( $this->is_partial_capture( $notification ) ) { |
||
| 333 | $partial_amount = $this->get_partial_amount_to_charge( $notification ); |
||
| 334 | $order->set_total( $partial_amount ); |
||
| 335 | $this->update_fees( $order, $notification->data->object->refunds->data[0]->balance_transaction ); |
||
| 336 | /* translators: partial captured amount */ |
||
| 337 | $order->add_order_note( sprintf( __( 'This charge was partially captured via Stripe Dashboard in the amount of: %s', 'woocommerce-gateway-stripe' ), $partial_amount ) ); |
||
| 338 | } else { |
||
| 339 | $order->payment_complete( $notification->data->object->id ); |
||
| 340 | |||
| 341 | /* translators: transaction id */ |
||
| 342 | $order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $notification->data->object->id ) ); |
||
| 343 | } |
||
| 344 | |||
| 345 | if ( is_callable( array( $order, 'save' ) ) ) { |
||
| 346 | $order->save(); |
||
| 347 | } |
||
| 348 | } |
||
| 349 | } |
||
| 350 | } |
||
| 351 | |||
| 352 | /** |
||
| 353 | * Process webhook charge succeeded. This is used for payment methods |
||
| 354 | * that takes time to clear which is asynchronous. e.g. SEPA, SOFORT. |
||
| 355 | * |
||
| 356 | * @since 4.0.0 |
||
| 357 | * @version 4.0.0 |
||
| 358 | * @param object $notification |
||
| 359 | */ |
||
| 360 | public function process_webhook_charge_succeeded( $notification ) { |
||
| 361 | // Ignore the notification for charges, created through PaymentIntents. |
||
| 362 | if ( isset( $notification->data->object->payment_intent ) && $notification->data->object->payment_intent ) { |
||
| 363 | return; |
||
| 364 | } |
||
| 365 | |||
| 366 | // The following payment methods are synchronous so does not need to be handle via webhook. |
||
| 367 | if ( ( isset( $notification->data->object->source->type ) && 'card' === $notification->data->object->source->type ) || ( isset( $notification->data->object->source->type ) && 'three_d_secure' === $notification->data->object->source->type ) ) { |
||
| 368 | return; |
||
| 369 | } |
||
| 370 | |||
| 371 | $order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id ); |
||
| 372 | |||
| 373 | if ( ! $order ) { |
||
| 374 | WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id ); |
||
| 375 | return; |
||
| 376 | } |
||
| 377 | |||
| 378 | if ( ! $order->has_status( 'on-hold' ) ) { |
||
| 379 | return; |
||
| 380 | } |
||
| 381 | |||
| 382 | // Store other data such as fees |
||
| 383 | $order->set_transaction_id( $notification->data->object->id ); |
||
| 384 | |||
| 385 | if ( isset( $notification->data->object->balance_transaction ) ) { |
||
| 386 | $this->update_fees( $order, $notification->data->object->balance_transaction ); |
||
| 387 | } |
||
| 388 | |||
| 389 | $order->payment_complete( $notification->data->object->id ); |
||
| 390 | |||
| 391 | /* translators: transaction id */ |
||
| 392 | $order->add_order_note( sprintf( __( 'Stripe charge complete (Charge ID: %s)', 'woocommerce-gateway-stripe' ), $notification->data->object->id ) ); |
||
| 393 | |||
| 394 | if ( is_callable( array( $order, 'save' ) ) ) { |
||
| 395 | $order->save(); |
||
| 396 | } |
||
| 397 | } |
||
| 398 | |||
| 399 | /** |
||
| 400 | * Process webhook charge failed. |
||
| 401 | * |
||
| 402 | * @since 4.0.0 |
||
| 403 | * @since 4.1.5 Can handle any fail payments from any methods. |
||
| 404 | * @param object $notification |
||
| 405 | */ |
||
| 406 | public function process_webhook_charge_failed( $notification ) { |
||
| 407 | $order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id ); |
||
| 408 | |||
| 409 | if ( ! $order ) { |
||
| 410 | WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id ); |
||
| 411 | return; |
||
| 412 | } |
||
| 413 | |||
| 414 | // If order status is already in failed status don't continue. |
||
| 415 | if ( $order->has_status( 'failed' ) ) { |
||
| 416 | return; |
||
| 417 | } |
||
| 418 | |||
| 419 | $order->update_status( 'failed', __( 'This payment failed to clear.', 'woocommerce-gateway-stripe' ) ); |
||
| 420 | |||
| 421 | do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification ); |
||
| 422 | } |
||
| 423 | |||
| 424 | /** |
||
| 425 | * Process webhook source canceled. This is used for payment methods |
||
| 426 | * that redirects and awaits payments from customer. |
||
| 427 | * |
||
| 428 | * @since 4.0.0 |
||
| 429 | * @since 4.1.15 Add check to make sure order is processed by Stripe. |
||
| 430 | * @param object $notification |
||
| 431 | */ |
||
| 432 | public function process_webhook_source_canceled( $notification ) { |
||
| 433 | $order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id ); |
||
| 434 | |||
| 435 | // If can't find order by charge ID, try source ID. |
||
| 436 | if ( ! $order ) { |
||
| 437 | $order = WC_Stripe_Helper::get_order_by_source_id( $notification->data->object->id ); |
||
| 438 | |||
| 439 | if ( ! $order ) { |
||
| 440 | WC_Stripe_Logger::log( 'Could not find order via charge/source ID: ' . $notification->data->object->id ); |
||
| 441 | return; |
||
| 442 | } |
||
| 443 | } |
||
| 444 | |||
| 445 | // Don't proceed if payment method isn't Stripe. |
||
| 446 | if ( 'stripe' !== $order->get_payment_method() ) { |
||
| 447 | WC_Stripe_Logger::log( 'Canceled webhook abort: Order was not processed by Stripe: ' . $order->get_id() ); |
||
| 448 | return; |
||
| 449 | } |
||
| 450 | |||
| 451 | if ( ! $order->has_status( 'cancelled' ) ) { |
||
| 452 | $order->update_status( 'cancelled', __( 'This payment has cancelled.', 'woocommerce-gateway-stripe' ) ); |
||
| 453 | } |
||
| 454 | |||
| 455 | do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification ); |
||
| 456 | } |
||
| 457 | |||
| 458 | /** |
||
| 459 | * Process webhook refund. |
||
| 460 | * |
||
| 461 | * @since 4.0.0 |
||
| 462 | * @version 4.0.0 |
||
| 463 | * @param object $notification |
||
| 464 | */ |
||
| 465 | public function process_webhook_refund( $notification ) { |
||
| 466 | $order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->id ); |
||
| 467 | |||
| 468 | if ( ! $order ) { |
||
| 469 | WC_Stripe_Logger::log( 'Could not find order via charge ID: ' . $notification->data->object->id ); |
||
| 470 | return; |
||
| 471 | } |
||
| 472 | |||
| 473 | $order_id = $order->get_id(); |
||
| 474 | |||
| 475 | if ( 'stripe' === $order->get_payment_method() ) { |
||
| 476 | $charge = $order->get_transaction_id(); |
||
| 477 | $captured = $order->get_meta( '_stripe_charge_captured', true ); |
||
| 478 | $refund_id = $order->get_meta( '_stripe_refund_id', true ); |
||
| 479 | |||
| 480 | // If the refund ID matches, don't continue to prevent double refunding. |
||
| 481 | if ( $notification->data->object->refunds->data[0]->id === $refund_id ) { |
||
| 482 | return; |
||
| 483 | } |
||
| 484 | |||
| 485 | // Only refund captured charge. |
||
| 486 | if ( $charge ) { |
||
| 487 | $reason = ( isset( $captured ) && 'yes' === $captured ) ? __( 'Refunded via Stripe Dashboard', 'woocommerce-gateway-stripe' ) : __( 'Pre-Authorization Released via Stripe Dashboard', 'woocommerce-gateway-stripe' ); |
||
| 488 | |||
| 489 | // Create the refund. |
||
| 490 | $refund = wc_create_refund( |
||
| 491 | array( |
||
| 492 | 'order_id' => $order_id, |
||
| 493 | 'amount' => $this->get_refund_amount( $notification ), |
||
| 494 | 'reason' => $reason, |
||
| 495 | ) |
||
| 496 | ); |
||
| 497 | |||
| 498 | if ( is_wp_error( $refund ) ) { |
||
| 499 | WC_Stripe_Logger::log( $refund->get_error_message() ); |
||
| 500 | } |
||
| 501 | |||
| 502 | $order->update_meta_data( '_stripe_refund_id', $notification->data->object->refunds->data[0]->id ); |
||
| 503 | |||
| 504 | $amount = wc_price( $notification->data->object->refunds->data[0]->amount / 100 ); |
||
| 505 | |||
| 506 | if ( in_array( strtolower( $order->get_currency() ), WC_Stripe_Helper::no_decimal_currencies() ) ) { |
||
| 507 | $amount = wc_price( $notification->data->object->refunds->data[0]->amount ); |
||
| 508 | } |
||
| 509 | |||
| 510 | if ( isset( $notification->data->object->refunds->data[0]->balance_transaction ) ) { |
||
| 511 | $this->update_fees( $order, $notification->data->object->refunds->data[0]->balance_transaction ); |
||
| 512 | } |
||
| 513 | |||
| 514 | /* translators: 1) dollar amount 2) transaction id 3) refund message */ |
||
| 515 | $refund_message = ( isset( $captured ) && 'yes' === $captured ) ? sprintf( __( 'Refunded %1$s - Refund ID: %2$s - %3$s', 'woocommerce-gateway-stripe' ), $amount, $notification->data->object->refunds->data[0]->id, $reason ) : __( 'Pre-Authorization Released via Stripe Dashboard', 'woocommerce-gateway-stripe' ); |
||
| 516 | |||
| 517 | $order->add_order_note( $refund_message ); |
||
| 518 | } |
||
| 519 | } |
||
| 520 | } |
||
| 521 | |||
| 522 | /** |
||
| 523 | * Process webhook reviews that are opened. i.e Radar. |
||
| 524 | * |
||
| 525 | * @since 4.0.6 |
||
| 526 | * @param object $notification |
||
| 527 | */ |
||
| 528 | View Code Duplication | public function process_review_opened( $notification ) { |
|
| 529 | if ( isset( $notification->data->object->payment_intent ) ) { |
||
| 530 | $order = WC_Stripe_Helper::get_order_by_intent_id( $notification->data->object->payment_intent ); |
||
| 531 | |||
| 532 | if ( ! $order ) { |
||
| 533 | WC_Stripe_Logger::log( '[Review Opened] Could not find order via intent ID: ' . $notification->data->object->payment_intent ); |
||
| 534 | return; |
||
| 535 | } |
||
| 536 | } else { |
||
| 537 | $order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge ); |
||
| 538 | |||
| 539 | if ( ! $order ) { |
||
| 540 | WC_Stripe_Logger::log( '[Review Opened] Could not find order via charge ID: ' . $notification->data->object->charge ); |
||
| 541 | return; |
||
| 542 | } |
||
| 543 | } |
||
| 544 | |||
| 545 | /* translators: 1) The URL to the order. 2) The reason type. */ |
||
| 546 | $message = sprintf( __( 'A review has been opened for this order. Action is needed. Please go to your <a href="%1$s" title="Stripe Dashboard" target="_blank">Stripe Dashboard</a> to review the issue. Reason: (%2$s)', 'woocommerce-gateway-stripe' ), $this->get_transaction_url( $order ), $notification->data->object->reason ); |
||
| 547 | |||
| 548 | if ( apply_filters( 'wc_stripe_webhook_review_change_order_status', true, $order, $notification ) ) { |
||
| 549 | $order->update_status( 'on-hold', $message ); |
||
| 550 | } else { |
||
| 551 | $order->add_order_note( $message ); |
||
| 552 | } |
||
| 553 | } |
||
| 554 | |||
| 555 | /** |
||
| 556 | * Process webhook reviews that are closed. i.e Radar. |
||
| 557 | * |
||
| 558 | * @since 4.0.6 |
||
| 559 | * @param object $notification |
||
| 560 | */ |
||
| 561 | View Code Duplication | public function process_review_closed( $notification ) { |
|
| 562 | if ( isset( $notification->data->object->payment_intent ) ) { |
||
| 563 | $order = WC_Stripe_Helper::get_order_by_intent_id( $notification->data->object->payment_intent ); |
||
| 564 | |||
| 565 | if ( ! $order ) { |
||
| 566 | WC_Stripe_Logger::log( '[Review Closed] Could not find order via intent ID: ' . $notification->data->object->payment_intent ); |
||
| 567 | return; |
||
| 568 | } |
||
| 569 | } else { |
||
| 570 | $order = WC_Stripe_Helper::get_order_by_charge_id( $notification->data->object->charge ); |
||
| 571 | |||
| 572 | if ( ! $order ) { |
||
| 573 | WC_Stripe_Logger::log( '[Review Closed] Could not find order via charge ID: ' . $notification->data->object->charge ); |
||
| 574 | return; |
||
| 575 | } |
||
| 576 | } |
||
| 577 | |||
| 578 | /* translators: 1) The reason type. */ |
||
| 579 | $message = sprintf( __( 'The opened review for this order is now closed. Reason: (%s)', 'woocommerce-gateway-stripe' ), $notification->data->object->reason ); |
||
| 580 | |||
| 581 | if ( $order->has_status( 'on-hold' ) ) { |
||
| 582 | if ( apply_filters( 'wc_stripe_webhook_review_change_order_status', true, $order, $notification ) ) { |
||
| 583 | $order->update_status( 'processing', $message ); |
||
| 584 | } else { |
||
| 585 | $order->add_order_note( $message ); |
||
| 586 | } |
||
| 587 | } else { |
||
| 588 | $order->add_order_note( $message ); |
||
| 589 | } |
||
| 590 | } |
||
| 591 | |||
| 592 | /** |
||
| 593 | * Checks if capture is partial. |
||
| 594 | * |
||
| 595 | * @since 4.0.0 |
||
| 596 | * @version 4.0.0 |
||
| 597 | * @param object $notification |
||
| 598 | */ |
||
| 599 | public function is_partial_capture( $notification ) { |
||
| 600 | return 0 < $notification->data->object->amount_refunded; |
||
| 601 | } |
||
| 602 | |||
| 603 | /** |
||
| 604 | * Gets the amount refunded. |
||
| 605 | * |
||
| 606 | * @since 4.0.0 |
||
| 607 | * @version 4.0.0 |
||
| 608 | * @param object $notification |
||
| 609 | */ |
||
| 610 | public function get_refund_amount( $notification ) { |
||
| 611 | if ( $this->is_partial_capture( $notification ) ) { |
||
| 612 | $amount = $notification->data->object->refunds->data[0]->amount / 100; |
||
| 613 | |||
| 614 | View Code Duplication | if ( in_array( strtolower( $notification->data->object->currency ), WC_Stripe_Helper::no_decimal_currencies() ) ) { |
|
| 615 | $amount = $notification->data->object->refunds->data[0]->amount; |
||
| 616 | } |
||
| 617 | |||
| 618 | return $amount; |
||
| 619 | } |
||
| 620 | |||
| 621 | return false; |
||
| 622 | } |
||
| 623 | |||
| 624 | /** |
||
| 625 | * Gets the amount we actually charge. |
||
| 626 | * |
||
| 627 | * @since 4.0.0 |
||
| 628 | * @version 4.0.0 |
||
| 629 | * @param object $notification |
||
| 630 | */ |
||
| 631 | public function get_partial_amount_to_charge( $notification ) { |
||
| 632 | if ( $this->is_partial_capture( $notification ) ) { |
||
| 633 | $amount = ( $notification->data->object->amount - $notification->data->object->amount_refunded ) / 100; |
||
| 634 | |||
| 635 | View Code Duplication | if ( in_array( strtolower( $notification->data->object->currency ), WC_Stripe_Helper::no_decimal_currencies() ) ) { |
|
| 636 | $amount = ( $notification->data->object->amount - $notification->data->object->amount_refunded ); |
||
| 637 | } |
||
| 638 | |||
| 639 | return $amount; |
||
| 640 | } |
||
| 641 | |||
| 642 | return false; |
||
| 643 | } |
||
| 644 | |||
| 645 | public function process_payment_intent_success( $notification ) { |
||
| 646 | $intent = $notification->data->object; |
||
| 647 | $order = WC_Stripe_Helper::get_order_by_intent_id( $intent->id ); |
||
| 648 | |||
| 649 | if ( ! $order ) { |
||
| 650 | WC_Stripe_Logger::log( 'Could not find order via intent ID: ' . $intent->id ); |
||
| 651 | return; |
||
| 652 | } |
||
| 653 | |||
| 654 | if ( ! $order->has_status( array( 'pending', 'failed' ) ) ) { |
||
| 655 | return; |
||
| 656 | } |
||
| 657 | |||
| 658 | if ( $this->lock_order_payment( $order, $intent ) ) { |
||
| 659 | return; |
||
| 660 | } |
||
| 661 | |||
| 662 | $order_id = $order->get_id(); |
||
| 663 | if ( 'payment_intent.succeeded' === $notification->type || 'payment_intent.amount_capturable_updated' === $notification->type ) { |
||
| 664 | $charge = end( $intent->charges->data ); |
||
| 665 | WC_Stripe_Logger::log( "Stripe PaymentIntent $intent->id succeeded for order $order_id" ); |
||
| 666 | |||
| 667 | do_action( 'wc_gateway_stripe_process_payment', $charge, $order ); |
||
| 668 | |||
| 669 | // Process valid response. |
||
| 670 | $this->process_response( $charge, $order ); |
||
| 671 | |||
| 672 | View Code Duplication | } else { |
|
| 673 | $error_message = $intent->last_payment_error ? $intent->last_payment_error->message : ""; |
||
| 674 | |||
| 675 | /* translators: 1) The error message that was received from Stripe. */ |
||
| 676 | $order->update_status( 'failed', sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $error_message ) ); |
||
| 677 | |||
| 678 | do_action( 'wc_gateway_stripe_process_webhook_payment_error', $order, $notification ); |
||
| 679 | |||
| 680 | $this->send_failed_order_email( $order_id ); |
||
| 681 | } |
||
| 682 | |||
| 683 | $this->unlock_order_payment( $order ); |
||
| 684 | } |
||
| 685 | |||
| 686 | public function process_setup_intent( $notification ) { |
||
| 687 | $intent = $notification->data->object; |
||
| 688 | $order = WC_Stripe_Helper::get_order_by_setup_intent_id( $intent->id ); |
||
| 689 | |||
| 690 | if ( ! $order ) { |
||
| 691 | WC_Stripe_Logger::log( 'Could not find order via setup intent ID: ' . $intent->id ); |
||
| 692 | return; |
||
| 693 | } |
||
| 694 | |||
| 695 | if ( ! $order->has_status( array( 'pending', 'failed' ) ) ) { |
||
| 696 | return; |
||
| 697 | } |
||
| 698 | |||
| 699 | if ( $this->lock_order_payment( $order, $intent ) ) { |
||
| 700 | return; |
||
| 701 | } |
||
| 702 | |||
| 703 | $order_id = $order->get_id(); |
||
| 704 | if ( 'setup_intent.succeeded' === $notification->type ) { |
||
| 705 | WC_Stripe_Logger::log( "Stripe SetupIntent $intent->id succeeded for order $order_id" ); |
||
| 706 | if ( WC_Stripe_Helper::is_pre_orders_exists() && WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) { |
||
| 707 | WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order ); |
||
| 708 | } else { |
||
| 709 | $order->payment_complete(); |
||
| 710 | } |
||
| 711 | View Code Duplication | } else { |
|
| 712 | $error_message = $intent->last_setup_error ? $intent->last_setup_error->message : ""; |
||
| 713 | |||
| 714 | /* translators: 1) The error message that was received from Stripe. */ |
||
| 715 | $order->update_status( 'failed', sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $error_message ) ); |
||
| 716 | |||
| 717 | $this->send_failed_order_email( $order_id ); |
||
| 718 | } |
||
| 719 | |||
| 720 | $this->unlock_order_payment( $order ); |
||
| 721 | } |
||
| 722 | |||
| 723 | /** |
||
| 724 | * Processes the incoming webhook. |
||
| 725 | * |
||
| 726 | * @since 4.0.0 |
||
| 727 | * @version 4.0.0 |
||
| 728 | * @param string $request_body |
||
| 729 | */ |
||
| 730 | public function process_webhook( $request_body ) { |
||
| 731 | $notification = json_decode( $request_body ); |
||
| 732 | |||
| 733 | switch ( $notification->type ) { |
||
| 734 | case 'source.chargeable': |
||
| 735 | $this->process_webhook_payment( $notification ); |
||
| 736 | break; |
||
| 737 | |||
| 738 | case 'source.canceled': |
||
| 739 | $this->process_webhook_source_canceled( $notification ); |
||
| 740 | break; |
||
| 741 | |||
| 742 | case 'charge.succeeded': |
||
| 743 | $this->process_webhook_charge_succeeded( $notification ); |
||
| 744 | break; |
||
| 745 | |||
| 746 | case 'charge.failed': |
||
| 747 | $this->process_webhook_charge_failed( $notification ); |
||
| 748 | break; |
||
| 749 | |||
| 750 | case 'charge.captured': |
||
| 751 | $this->process_webhook_capture( $notification ); |
||
| 752 | break; |
||
| 753 | |||
| 754 | case 'charge.dispute.created': |
||
| 755 | $this->process_webhook_dispute( $notification ); |
||
| 756 | break; |
||
| 757 | |||
| 758 | case 'charge.refunded': |
||
| 759 | $this->process_webhook_refund( $notification ); |
||
| 760 | break; |
||
| 761 | |||
| 762 | case 'review.opened': |
||
| 763 | $this->process_review_opened( $notification ); |
||
| 764 | break; |
||
| 765 | |||
| 766 | case 'review.closed': |
||
| 767 | $this->process_review_closed( $notification ); |
||
| 768 | break; |
||
| 769 | |||
| 770 | case 'payment_intent.succeeded': |
||
| 771 | case 'payment_intent.payment_failed': |
||
| 772 | case 'payment_intent.amount_capturable_updated': |
||
| 773 | $this->process_payment_intent_success( $notification ); |
||
| 774 | break; |
||
| 775 | |||
| 776 | case 'setup_intent.succeeded': |
||
| 777 | case 'setup_intent.setup_failed': |
||
| 778 | $this->process_setup_intent( $notification ); |
||
| 779 | |||
| 780 | } |
||
| 781 | } |
||
| 782 | } |
||
| 783 | |||
| 784 | new WC_Stripe_Webhook_Handler(); |
||
| 785 |
This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.