PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.22
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.22
1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 All 168 releases
← All changes | includes/Orders.php +1 -193 1.10.24 → 1.10.22 View file →
@@ -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.