| @@ -16,9 +16,8 @@ | ||
| 16 | 16 | use WC_Order; |
| 17 | 17 | use WC_Order_Item; |
| 18 | 18 | use WC_Order_Item_Product; |
| 19 | 19 | use WC_Order_Item_Shipping; |
| 20 | -use WC_Payment_Gateway; | |
| 21 | 20 | use WC_Discounts; |
| 22 | 21 | use WC_Product; |
| 23 | 22 | use WC_Product_Simple; |
| 24 | 23 | use WC_Tax; |
| @@ -62,13 +61,9 @@ | ||
| 62 | 61 | add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'payment_complete_order_status' ), 10, 3 ); |
| 63 | 62 | add_filter( 'woocommerce_bacs_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 ); |
| 64 | 63 | add_filter( 'woocommerce_cheque_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 ); |
| 65 | 64 | add_filter( 'woocommerce_cod_process_payment_order_status', array( $this, 'offline_process_payment_order_status' ), 10, 2 ); |
| 66 | - // PHP_INT_MAX: the redirect is the decision input and gateways rewrite it on | |
| 67 | - // this same filter — Stripe swaps get_return_url() for a #confirm-pi hash at | |
| 68 | - // 99999 when the intent needs 3DS. Reading it earlier closes a sale the | |
| 69 | - // customer is still confirming. | |
| 70 | - add_filter( 'woocommerce_payment_successful_result', array( $this, 'apply_unpaid_gateway_order_status' ), PHP_INT_MAX, 2 ); | |
| 65 | + add_filter( 'woocommerce_payment_successful_result', array( $this, 'apply_unpaid_gateway_order_status' ), 10, 2 ); | |
| 71 | 66 | add_filter( 'woocommerce_hidden_order_itemmeta', array( $this, 'hidden_order_itemmeta' ) ); |
| 72 | 67 | add_filter( 'woocommerce_order_item_product', array( $this, 'order_item_product' ), 10, 2 ); |
| 73 | 68 | add_filter( 'woocommerce_order_get_tax_location', array( $this, 'get_tax_location' ), 10, 2 ); |
| 74 | 69 | add_action( 'woocommerce_order_item_after_calculate_taxes', array( $this, 'order_item_after_calculate_taxes' ) ); |
| @@ -245,17 +240,8 @@ | ||
| 245 | 240 | * - the gateway must be enabled for POS *and* carry an explicitly stored |
| 246 | 241 | * status. The settings view synthesizes `wc-completed` for every installed |
| 247 | 242 | * gateway it has never seen, so trusting the computed value would mark an |
| 248 | 243 | * unconfigured third-party gateway Completed with no money taken. |
| 249 | - * - the gateway's redirect must be the order's own received page. That is | |
| 250 | - * the gateway saying it is finished; any other target means it is still | |
| 251 | - * collecting the money — see redirect_targets_order_received(). | |
| 252 | - * - the gateway must not be one that moves money, read from the capability | |
| 253 | - * WooCommerce has every gateway declare: refunds. A gateway that can give | |
| 254 | - * money back takes money; if it hands the customer to the received page | |
| 255 | - * without having taken it, the payment is pending (a bank transfer, an | |
| 256 | - * async method) and its webhook owns the status. A quote, invoice or | |
| 257 | - * purchase-order gateway cannot refund, because nothing was ever paid. | |
| 258 | 244 | * |
| 259 | 245 | * @param array $result Gateway result, passed through untouched. |
| 260 | 246 | * @param int $order_id Order ID. |
| 261 | 247 | * |
| @@ -275,17 +261,9 @@ | ||
| 275 | 261 | if ( ! $order->has_status( 'pos-open' ) || $order->get_date_paid( 'edit' ) ) { |
| 276 | 262 | return $result; |
| 277 | 263 | } |
| 278 | 264 | |
| 279 | - if ( ! \is_array( $result ) || ! $this->redirect_targets_order_received( $result, $order ) ) { | |
| 280 | - return $result; | |
| 281 | - } | |
| 282 | - | |
| 283 | 265 | $gateway_id = $order->get_payment_method(); |
| 284 | - | |
| 285 | - if ( $this->gateway_moves_money( $gateway_id ) ) { | |
| 286 | - return $result; | |
| 287 | - } | |
| 288 | 266 | $configured = $this->get_stored_gateway_order_status( $gateway_id ); |
| 289 | 267 | |
| 290 | 268 | if ( '' === $configured ) { |
| 291 | 269 | return $result; |
| @@ -341,178 +319,8 @@ | ||
| 341 | 319 | remove_filter( 'woocommerce_payment_complete_order_status', $suppress_paid_date, PHP_INT_MAX ); |
| 342 | 320 | } |
| 343 | 321 | |
| 344 | 322 | return $result; |
| 345 | - } | |
| 346 | - | |
| 347 | - /** | |
| 348 | - * Whether a successful gateway result sends the customer to the order's received page. | |
| 349 | - * | |
| 350 | - * The redirect is the one thing every gateway declares about what happens | |
| 351 | - * next, and it separates the two shapes that both "return success and leave | |
| 352 | - * the order open": | |
| 353 | - * | |
| 354 | - * - A gateway that is finished sends the customer to the received page, | |
| 355 | - * `get_return_url( $order )`. Quotes, invoices, purchase orders, BACS, | |
| 356 | - * cheque and COD all do. No money will ever move through it, so the | |
| 357 | - * configured POS status is the only thing that closes the sale. | |
| 358 | - * - A gateway that still has to collect the money sends them somewhere else: | |
| 359 | - * a hosted checkout off-site (Dintero, Mollie, PayPal, Klarna), or an | |
| 360 | - * on-site pay or receipt page that posts a form to one. It settles the | |
| 361 | - * order later, from its callback, through payment_complete(). Acting on | |
| 362 | - * this shape marked the order Completed while the cashier was still | |
| 363 | - * looking at the hosted checkout (1.10.20–1.10.22): Dintero's capture | |
| 364 | - * handler then found no transaction and bounced the order to on-hold, and | |
| 365 | - * the till — which reads any status outside its open/unpaid set as a | |
| 366 | - * finished sale — opened the receipt before a payment method was chosen. | |
| 367 | - * | |
| 368 | - * Compared without scheme or trailing slash: `get_return_url()` may upgrade to | |
| 369 | - * https, and a gateway may append its own arguments. Both the order's received | |
| 370 | - * URL and its `woocommerce_get_return_url`-filtered form are accepted, so a | |
| 371 | - * plugin that moves the thank-you page still matches — see is_same_page() for | |
| 372 | - * what "same page" means on plain permalinks, where the page is in the query. | |
| 373 | - * | |
| 374 | - * A redirect carrying a fragment never matches. A fragment on a thank-you URL | |
| 375 | - * is a gateway's instruction to its own script to do something before the sale | |
| 376 | - * is done (Stripe's and WooPayments' `#confirm-pi…` 3DS hand-off); a gateway | |
| 377 | - * that is finished has no reason to add one. Nor does a missing or relative | |
| 378 | - * redirect match: WooCommerce's pay handler would redirect to it unchanged, | |
| 379 | - * and nothing here can tell what it is. | |
| 380 | - * | |
| 381 | - * @param array $result Gateway result from process_payment(). | |
| 382 | - * @param WC_Order $order The order being paid. | |
| 383 | - * | |
| 384 | - * @return bool | |
| 385 | - */ | |
| 386 | - private function redirect_targets_order_received( array $result, WC_Order $order ): bool { | |
| 387 | - $redirect = isset( $result['redirect'] ) && \is_string( $result['redirect'] ) ? trim( $result['redirect'] ) : ''; | |
| 388 | - | |
| 389 | - if ( '' === $redirect || false !== strpos( $redirect, '#' ) ) { | |
| 390 | - return false; | |
| 391 | - } | |
| 392 | - | |
| 393 | - $received_url = $order->get_checkout_order_received_url(); | |
| 394 | - | |
| 395 | - /** This filter is documented in woocommerce/includes/abstracts/abstract-wc-payment-gateway.php */ | |
| 396 | - $return_url = apply_filters( 'woocommerce_get_return_url', $received_url, $order ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce core hook. | |
| 397 | - | |
| 398 | - foreach ( array_unique( array( $received_url, $return_url ) ) as $candidate ) { | |
| 399 | - if ( \is_string( $candidate ) && '' !== $candidate && $this->is_same_page( $redirect, $candidate ) ) { | |
| 400 | - return true; | |
| 401 | - } | |
| 402 | - } | |
| 403 | - | |
| 404 | - return false; | |
| 405 | - } | |
| 406 | - | |
| 407 | - /** | |
| 408 | - * Whether a gateway's redirect lands on the given page. | |
| 409 | - * | |
| 410 | - * Host (case-insensitive), port and path (without a trailing slash) must | |
| 411 | - * match; the scheme is ignored, and a port is compared only when it is not | |
| 412 | - * the scheme's default, so http and https forms of one origin agree while | |
| 413 | - * two services on one host do not. | |
| 414 | - * | |
| 415 | - * The query is compared one way: every argument the page carries must be on | |
| 416 | - * the redirect with the same value, except `key`, which a gateway may drop. | |
| 417 | - * Extra arguments on the redirect are fine (`utm_nooverride`, a gateway's own | |
| 418 | - * tracking). This is what makes plain permalinks safe, where every | |
| 419 | - * WooCommerce page shares the path `/` and the page is its query: the | |
| 420 | - * received page carries `page_id` and the received endpoint (`order-received`, | |
| 421 | - * or whatever the store renamed it to, read from the URL itself rather than | |
| 422 | - * assumed), so the checkout page, the pay page and any other page of the same | |
| 423 | - * site fail to carry one of them. | |
| 424 | - * | |
| 425 | - * @param string $redirect The gateway's redirect. | |
| 426 | - * @param string $page The page it must land on. | |
| 427 | - * | |
| 428 | - * @return bool | |
| 429 | - */ | |
| 430 | - private function is_same_page( string $redirect, string $page ): bool { | |
| 431 | - $parts_r = wp_parse_url( $redirect ); | |
| 432 | - $parts_p = wp_parse_url( $page ); | |
| 433 | - | |
| 434 | - if ( ! \is_array( $parts_r ) || ! \is_array( $parts_p ) ) { | |
| 435 | - return false; | |
| 436 | - } | |
| 437 | - | |
| 438 | - $host_r = strtolower( (string) ( $parts_r['host'] ?? '' ) ); | |
| 439 | - $host_p = strtolower( (string) ( $parts_p['host'] ?? '' ) ); | |
| 440 | - | |
| 441 | - if ( '' === $host_r || $host_r !== $host_p ) { | |
| 442 | - return false; | |
| 443 | - } | |
| 444 | - | |
| 445 | - if ( $this->explicit_port( $parts_r ) !== $this->explicit_port( $parts_p ) ) { | |
| 446 | - return false; | |
| 447 | - } | |
| 448 | - | |
| 449 | - $path_r = untrailingslashit( (string) ( $parts_r['path'] ?? '/' ) ); | |
| 450 | - $path_p = untrailingslashit( (string) ( $parts_p['path'] ?? '/' ) ); | |
| 451 | - | |
| 452 | - if ( $path_r !== $path_p ) { | |
| 453 | - return false; | |
| 454 | - } | |
| 455 | - | |
| 456 | - parse_str( (string) ( $parts_r['query'] ?? '' ), $query_r ); | |
| 457 | - parse_str( (string) ( $parts_p['query'] ?? '' ), $query_p ); | |
| 458 | - | |
| 459 | - foreach ( $query_p as $name => $value ) { | |
| 460 | - if ( 'key' === $name ) { | |
| 461 | - continue; | |
| 462 | - } | |
| 463 | - | |
| 464 | - if ( ! isset( $query_r[ $name ] ) || (string) $query_r[ $name ] !== (string) $value ) { | |
| 465 | - return false; | |
| 466 | - } | |
| 467 | - } | |
| 468 | - | |
| 469 | - return true; | |
| 470 | - } | |
| 471 | - | |
| 472 | - /** | |
| 473 | - * The port of a parsed URL, or '' when it is the scheme's default. | |
| 474 | - * | |
| 475 | - * @param array $parts Output of wp_parse_url(). | |
| 476 | - * | |
| 477 | - * @return string | |
| 478 | - */ | |
| 479 | - private function explicit_port( array $parts ): string { | |
| 480 | - if ( ! isset( $parts['port'] ) ) { | |
| 481 | - return ''; | |
| 482 | - } | |
| 483 | - | |
| 484 | - $port = (int) $parts['port']; | |
| 485 | - $scheme = strtolower( (string) ( $parts['scheme'] ?? '' ) ); | |
| 486 | - | |
| 487 | - if ( ( 'http' === $scheme && 80 === $port ) || ( 'https' === $scheme && 443 === $port ) ) { | |
| 488 | - return ''; | |
| 489 | - } | |
| 490 | - | |
| 491 | - return (string) $port; | |
| 492 | - } | |
| 493 | - | |
| 494 | - /** | |
| 495 | - * Whether a gateway declares WooCommerce's refund capability. | |
| 496 | - * | |
| 497 | - * The declaration is the gateway's own (`$supports`, through | |
| 498 | - * `WC_Payment_Gateway::supports()` and its filter). A gateway WooCommerce | |
| 499 | - * does not know is taken as not moving money: the merchant stored a POS status | |
| 500 | - * for it, and that choice is the only thing left to act on. | |
| 501 | - * | |
| 502 | - * @param string $gateway_id The payment gateway ID. | |
| 503 | - * | |
| 504 | - * @return bool | |
| 505 | - */ | |
| 506 | - private function gateway_moves_money( string $gateway_id ): bool { | |
| 507 | - if ( '' === $gateway_id ) { | |
| 508 | - return false; | |
| 509 | - } | |
| 510 | - | |
| 511 | - $gateways = WC()->payment_gateways()->payment_gateways(); | |
| 512 | - $gateway = $gateways[ $gateway_id ] ?? null; | |
| 513 | - | |
| 514 | - return $gateway instanceof WC_Payment_Gateway && $gateway->supports( 'refunds' ); | |
| 515 | 323 | } |
| 516 | 324 | |
| 517 | 325 | /** |
| 518 | 326 | * Read the explicitly stored per-gateway order status. |