customer_service = new CustomerService(); $this->shopping_cart_service = new ShoppingCartService(); $this->payment_method_service = new PaymentMethodService(); $this->blocks_payment_data_service = new BlocksPaymentDataService(); } /** * Wallet payloads (Google Pay token, etc.) are JSON strings. * Treat them as opaque: do not run sanitize_text_field() to avoid corrupting JSON. * * @param string $value * @return string */ private function normalize_wallet_payload( string $value ): string { $value = trim( $value ); if ( '' === $value ) { return ''; } // Remove null bytes to avoid storage / transport issues. $value = str_replace( "\0", '', $value ); // Defensive limit: Google Pay tokens are ~1-3KB; allow plenty. if ( strlen( $value ) > 20000 ) { $value = substr( $value, 0, 20000 ); } return $value; } /** * Get a wallet payment token from the current request or from Blocks order meta. * * Wallet Direct (Apple Pay / Google Pay) can send the token either as: * - $_POST['payment_token'] (legacy) * - $_POST['_payment_token'] (Blocks/JS) * - Order meta '_multisafepay_blocks_payment_data[_payment_token]' (Blocks persisted) * * @param WC_Order $order * @return string */ public function get_wallet_payment_token( WC_Order $order ): string { $payment_method_id = (string) $order->get_payment_method(); $is_store_api_request = ( new BlocksContextService() )->is_store_api_request(); $read_request_value = static function ( string $key ) use ( $is_store_api_request ): string { if ( ! isset( $_POST[ $key ] ) ) { return ''; } // Store API requests are already unslashed by the REST layer. // Do not modify opaque wallet payloads (tokens/JSON). // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $value = ''; if ( $is_store_api_request ) { $value = (string) $_POST[ $key ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash } if ( ! $is_store_api_request ) { $value = (string) wp_unslash( $_POST[ $key ] ); } // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized return $value; }; $payment_token = $this->normalize_wallet_payload( $read_request_value( 'payment_token' ) ); if ( ! empty( $payment_token ) ) { return $payment_token; } $payment_method_wallet_key = $payment_method_id . '_payment_token'; $payment_token_from_method_key = $this->normalize_wallet_payload( $read_request_value( $payment_method_wallet_key ) ); if ( ! empty( $payment_token_from_method_key ) ) { return $payment_token_from_method_key; } $token_from_blocks_method_key = $this->normalize_wallet_payload( (string) $this->blocks_payment_data_service->get_blocks_payment_data_value( $order, $payment_method_wallet_key ) ); if ( ! empty( $token_from_blocks_method_key ) ) { return $token_from_blocks_method_key; } // Legacy-compatible fallback for Blocks: some flows can send/store just `payment_token`. return $this->normalize_wallet_payload( (string) $this->blocks_payment_data_service->get_blocks_payment_data_value( $order, 'payment_token' ) ); } /** * @param WC_Order $order * @param string $gateway_code * @param string $type * @return OrderRequest * @throws InvalidArgumentException */ public function create_order_request( WC_Order $order, string $gateway_code, string $type ): OrderRequest { $order_request = new OrderRequest(); $order_request ->addOrderId( $order->get_order_number() ) ->addMoney( MoneyUtil::create_money( (float) ( $order->get_total() ), $order->get_currency() ) ) ->addGatewayCode( $gateway_code ) ->addType( $type ) ->addPluginDetails( $this->create_plugin_details() ) ->addDescriptionText( $this->get_order_description_text( $order->get_order_number() ) ) ->addCustomer( $this->customer_service->create_customer_details( $order ) ) ->addPaymentOptions( $this->create_payment_options( $order ) ) ->addSecondsActive( $this->get_seconds_active() ) ->addSecondChance( ( new SecondChance() )->addSendEmail( (bool) get_option( 'multisafepay_second_chance', false ) ) ) ->addData( array( 'var2' => $order->get_id() ) ); if ( $order->needs_shipping_address() && $order->has_shipping_address() ) { $order_request->addDelivery( $this->customer_service->create_delivery_details( $order ) ); } if ( ! get_option( 'multisafepay_disable_shopping_cart', false ) || in_array( $gateway_code, GatewaysSdk::SHOPPING_CART_REQUIRED_GATEWAYS, true ) ) { $order_request->addShoppingCart( $this->shopping_cart_service->create_shopping_cart( $order, $order->get_currency(), $gateway_code ) ); } $payment_method_id = $order->get_payment_method(); $payment_component_payload_key = $payment_method_id . '_payment_component_payload'; $payment_component_payload = sanitize_text_field( wp_unslash( $_POST[ $payment_component_payload_key ] ?? '' ) ); if ( empty( $payment_component_payload ) ) { $payment_component_payload = $this->blocks_payment_data_service->get_blocks_payment_data_value( $order, $payment_component_payload_key ); } if ( ! empty( $payment_component_payload ) ) { $order_request->addType( 'direct' ); $order_request->addData( array( 'payment_data' => array( 'payload' => $payment_component_payload, ), ) ); } $payment_token = $this->get_wallet_payment_token( $order ); if ( ! empty( $payment_token ) && ( ( 'APPLEPAY' === $gateway_code ) || ( 'GOOGLEPAY' === $gateway_code ) ) ) { $order_request->addType( 'direct' ); $order_request->addGatewayInfo( ( new Wallet() )->addPaymentToken( $payment_token ) ); } // Force BILLINK B2B and B2C transactions to be direct if ( 'BILLINK' === $gateway_code ) { $payment_method_id = $order->get_payment_method(); $payment_method = $this->payment_method_service->get_woocommerce_payment_gateway_by_id( $payment_method_id ); if ( $payment_method ) { $payment_component_option = $payment_method->get_option( 'payment_component', 'no' ); if ( 'P' === $payment_component_option || 'B' === $payment_component_option ) { $order_request->addType( 'direct' ); } } } $order_request = $this->add_none_tax_rate( $order_request ); return apply_filters( 'multisafepay_order_request', $order_request ); } /** * @return PluginDetails */ protected function create_plugin_details(): PluginDetails { $plugin_details = new PluginDetails(); global $wp_version; return $plugin_details ->addApplicationName( 'Wordpress-WooCommerce' ) ->addApplicationVersion( 'WordPress version: ' . $wp_version . '. WooCommerce version: ' . WC_VERSION ) ->addPluginVersion( MULTISAFEPAY_PLUGIN_VERSION ) ->addShopRootUrl( get_bloginfo( 'url' ) ); } /** * @param WC_Order $order * @return PaymentOptions * @throws InvalidArgumentException */ private function create_payment_options( WC_Order $order ): PaymentOptions { $payment_options = new PaymentOptions(); $payment_options->addNotificationUrl( get_rest_url( get_current_blog_id(), 'multisafepay/v1/notification' ) ); $cancel_endpoint = ( get_option( 'multisafepay_redirect_after_cancel', 'cart' ) === 'cart' ? '' : wc_get_checkout_url() ); $cancel_url = wp_specialchars_decode( $order->get_cancel_order_url( $cancel_endpoint ) ); if ( is_wc_endpoint_url( 'order-pay' ) ) { $cancel_url = wp_specialchars_decode( $order->get_checkout_payment_url() ); } $payment_options->addCancelUrl( $cancel_url ); $payment_options->addRedirectUrl( $order->get_checkout_order_received_url() ); if ( ! apply_filters( 'multisafepay_post_notification', true ) ) { $payment_options->addNotificationUrl( add_query_arg( 'wc-api', 'multisafepay', home_url( '/' ) ) ); $payment_options->addNotificationMethod( 'GET' ); } // Add BILLINK specific settings $payment_method_id = $order->get_payment_method(); $payment_method = $this->payment_method_service->get_woocommerce_payment_gateway_by_id( $payment_method_id ); if ( $payment_method && 'BILLINK' === $payment_method->get_payment_method_gateway_code() ) { $payment_component_option = $payment_method->get_option( 'payment_component', 'no' ); if ( 'P' === $payment_component_option || 'B' === $payment_component_option ) { $payment_options->addSettings( array( 'gateways' => array( 'BILLINK' => array( 'type' => $payment_component_option ) ) ) ); } } return $payment_options; } /** * Return the order description. * * @param string $order_number * @return string $order_description */ protected function get_order_description_text( string $order_number ): string { /* translators: %s: order id */ $order_description = sprintf( __( 'Payment for order: %s', 'multisafepay' ), $order_number ); if ( get_option( 'multisafepay_order_request_description', false ) ) { $order_description = str_replace( '{order_number}', $order_number, get_option( 'multisafepay_order_request_description', false ) ); } return $order_description; } /** * Return the time active in seconds defined in the plugin settings page * * @return int */ protected function get_seconds_active(): int { $time_active = get_option( 'multisafepay_time_active', '30' ); $time_active_unit = get_option( 'multisafepay_time_unit', 'days' ); if ( 'days' === $time_active_unit ) { $time_active = $time_active * 24 * 60 * 60; } if ( 'hours' === $time_active_unit ) { $time_active = $time_active * 60 * 60; } return $time_active; } /** * This method adds a tax rate of 0, in case is not being created automatically by the shopping cart. * This is required to process refunds, based on shopping cart items * * @param OrderRequest $order_request * @return OrderRequest * @throws InvalidArgumentException */ public function add_none_tax_rate( OrderRequest $order_request ): OrderRequest { if ( $order_request->getShoppingCart() === null ) { return $order_request; } if ( $order_request->getCheckoutOptions()->getTaxTable() === null ) { return $order_request; } $shopping_cart = $order_request->getShoppingCart()->getData(); if ( isset( $shopping_cart['items'] ) ) { foreach ( $shopping_cart['items'] as $item ) { if ( '0' === $item['tax_table_selector'] ) { return $order_request; } } } $tax_rate = ( new TaxRate() )->addRate( 0 ); $tax_rule = ( new TaxRule() )->addTaxRate( $tax_rate )->addName( '0' ); $order_request->getCheckoutOptions()->getTaxTable()->addTaxRule( $tax_rule ); return $order_request; } }