PG-[0-9]{8}-[0-9]+)/state', array( 'methods' => 'GET', 'callback' => static function ( WP_REST_Request $request ) { $order = Paygrab_Orders::get_by_number( (string) $request['number'] ); if ( ! $order || $order->user_id !== get_current_user_id() ) { return new WP_Error( 'paygrab_not_found', __( 'This order could not be found.', 'paygrab' ), array( 'status' => 404 ) ); } return array( 'state' => self::complete_state( $order, Paygrab_Gateways::get( $order->gateway ) ) ); }, 'permission_callback' => 'is_user_logged_in', ) ); } /** * Route the buyer's forms (place order, cancel order). * * @return void */ public static function handle_post() { if ( 'POST' !== paygrab_request_method() || empty( $_POST['paygrab_action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- each action checks its nonce. return; } $action = sanitize_key( wp_unslash( $_POST['paygrab_action'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( ! in_array( $action, array( 'place_order', 'cancel_order' ), true ) ) { return; } if ( ! is_user_logged_in() ) { self::require_login(); } if ( 'place_order' === $action ) { self::place_order(); } self::buyer_cancel(); } /** * Checkout URL for a product. * * @param int $product_id Product. * @param int $source Post the buyer started from (0 for none). * @return string */ public static function url( $product_id, $source = 0 ) { $page = paygrab_page_url( 'checkout' ); if ( ! $page ) { return ''; } $args = array( 'product' => (int) $product_id ); if ( $source ) { $args[ PAYGRAB_SOURCE_ARG ] = (int) $source; } return add_query_arg( $args, $page ); } /** * URL of the completion screen for an order. * * @param Paygrab_Order $order Order. * @return string */ public static function complete_url( Paygrab_Order $order ) { $page = paygrab_page_url( 'checkout' ); return $page ? add_query_arg( 'complete', rawurlencode( $order->order_number ), $page ) : paygrab_mypage_url( $order->order_number ); } /** * Which completion screen fits an order (M8). * * - transfer: waiting for a transfer the buyer makes (bank transfer instructions). * - paid: paid; the files can be downloaded now. * - waiting: back from an online payment that the gateway has not confirmed yet. * - unpaid: the payment was started but did not go through (failed). * - closed: the order was cancelled or refunded. * * @param Paygrab_Order $order Order. * @param Paygrab_Gateway|null $gateway Gateway. * @return string */ public static function complete_state( Paygrab_Order $order, $gateway ) { if ( $order->is_paid() ) { $state = 'paid'; } elseif ( $order->is_pending() && ( $order->pay_deadline || ! $gateway || ! $gateway->confirms_payments() ) ) { $state = 'transfer'; } elseif ( $order->is_pending() ) { $state = 'waiting'; } elseif ( 'failed' === $order->status ) { $state = 'unpaid'; } else { // Cancelled or refunded: say so, without suggesting the payment can still be made. $state = 'closed'; } /** * Filters the completion screen shown for an order (PAYGRAB Pro decides for its gateways). * * @param string $state transfer | paid | waiting | unpaid | closed * @param Paygrab_Order $order Order. * @param Paygrab_Gateway|null $gateway Gateway. */ return (string) apply_filters( 'paygrab_complete_state', $state, $order, $gateway ); } /** * Whether the buyer sees this completion screen for the first time (used to count a sale once, * e.g. by conversion tags, even when the page is reloaded or opened from a bookmark). * * @param Paygrab_Order $order Order. * @param string $state Screen. * @return bool */ private static function first_view( Paygrab_Order $order, $state ) { $type = '_complete_' . $state; foreach ( Paygrab_Orders::events( $order->id ) as $event ) { if ( $type === $event->type ) { return false; } } Paygrab_Orders::add_event( $order->id, $type, '' ); return true; } /** * Messages for error codes passed back in the URL. * * @param string $code Code. * @return string */ public static function error_message( $code ) { $messages = array( 'nonce' => __( 'The page expired. Please try again.', 'paygrab' ), 'unavailable' => __( 'This product is not on sale right now.', 'paygrab' ), 'gateway' => __( 'Please choose a payment method.', 'paygrab' ), 'owned' => __( 'You have already bought this product. You can download it from My page.', 'paygrab' ), 'busy' => __( 'Too many orders in a short time. Please try again later.', 'paygrab' ), 'failed' => __( 'The order could not be completed. Please try again.', 'paygrab' ), 'cancel' => __( 'This order can no longer be cancelled.', 'paygrab' ), ); /** * Filters the error messages shown on the checkout and completion screens (?paygrab_error=code). * Add-ons add their own codes here (PAYGRAB Pro: stripe_unreachable). * * @param array $messages Code => message. */ $messages = (array) apply_filters( 'paygrab_error_messages', $messages ); return isset( $messages[ $code ] ) ? (string) $messages[ $code ] : ''; } /** * [paygrab_checkout] shortcode. * * @return string */ public static function shortcode() { Paygrab_Frontend::enqueue(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only screen selection. $complete = isset( $_GET['complete'] ) ? sanitize_text_field( wp_unslash( $_GET['complete'] ) ) : ''; $product_id = isset( $_GET['product'] ) ? absint( $_GET['product'] ) : 0; $error = isset( $_GET['paygrab_error'] ) ? self::error_message( sanitize_key( wp_unslash( $_GET['paygrab_error'] ) ) ) : ''; // phpcs:enable $user_id = get_current_user_id(); if ( $complete ) { $order = Paygrab_Orders::get_by_number( $complete ); if ( ! $order || ! $user_id || $order->user_id !== $user_id ) { return paygrab_get_template( 'message', array( 'message' => __( 'This order could not be found.', 'paygrab' ), 'link' => paygrab_mypage_url(), 'link_text' => __( 'Go to My page', 'paygrab' ) ) ); } $gateway = Paygrab_Gateways::get( $order->gateway ); $state = self::complete_state( $order, $gateway ); $first = self::first_view( $order, $state ); $html = paygrab_get_template( 'order-complete', array( 'order' => $order, 'gateway' => $gateway, 'state' => $state, 'first' => $first, ) ); // Here rather than in the template: it runs even when an add-on or the theme replaces the template. ob_start(); /** * Fires below the completion screen (PAYGRAB Pro prints conversion tags here). * * It runs on every completion screen, also "waiting", "unpaid" and "closed". Conversion tags * should count a sale only when $first is true and $state is "paid" (or "transfer", when an * order awaiting a bank transfer is counted). * * @param Paygrab_Order $order Order. * @param string $state transfer | paid | waiting | unpaid | closed * @param bool $first First time the buyer sees this screen for this order (count a sale only then). */ do_action( 'paygrab_order_complete', $order, $state, $first ); // An error sent back to the completion screen (e.g. PAYGRAB Pro could not reach Stripe) goes above the panel, // outside the template so it shows even when the template is replaced. $alert = $error ? '
' : ''; return $alert . $html . ob_get_clean(); } $source = paygrab_request_source(); $back = $source ? paygrab_source_url( $source ) : home_url( '/' ); $product = $product_id ? get_post( $product_id ) : null; if ( ! $product || Paygrab_Product::TYPE !== $product->post_type || 'publish' !== $product->post_status ) { return paygrab_get_template( 'message', array( 'message' => __( 'This product could not be found.', 'paygrab' ), 'link' => $back, 'link_text' => $source ? __( 'Back to the article', 'paygrab' ) : __( 'Go to the home page', 'paygrab' ) ) ); } if ( ! $user_id ) { // After logging in or signing up, the buyer goes back to the article they came from. $return = $source ? $back : self::url( $product_id ); return paygrab_get_template( 'checkout-login', array( 'product' => $product, 'login_url' => paygrab_login_url( $return ), 'register_url' => paygrab_register_url( $return ), ) ); } if ( Paygrab_Downloads::active_for( $user_id, $product_id ) ) { return paygrab_get_template( 'message', array( 'message' => self::error_message( 'owned' ), 'link' => paygrab_mypage_url(), 'link_text' => __( 'Go to My page', 'paygrab' ) ) ); } $pending = Paygrab_Orders::pending_for( $user_id, $product_id ); if ( $pending ) { return paygrab_get_template( 'message', array( 'message' => __( 'You already ordered this product and we are waiting for your payment.', 'paygrab' ), 'link' => paygrab_mypage_url( $pending->order_number ), 'link_text' => __( 'See how to pay', 'paygrab' ), ) ); } if ( ! Paygrab_Product::purchasable( $product_id ) ) { return paygrab_get_template( 'message', array( 'message' => self::error_message( 'unavailable' ), 'link' => $back, 'link_text' => $source ? __( 'Back to the article', 'paygrab' ) : __( 'Go to the home page', 'paygrab' ) ) ); } $gateways = Paygrab_Gateways::available(); if ( ! $gateways ) { return paygrab_get_template( 'message', array( 'message' => __( 'Payments are not set up yet. Please contact the seller.', 'paygrab' ), 'link' => $back, 'link_text' => $source ? __( 'Back to the article', 'paygrab' ) : __( 'Go to the home page', 'paygrab' ) ) ); } $data = Paygrab_Product::data( $product_id ); $price = Paygrab_Product::line_price( $product_id ); return paygrab_get_template( 'checkout', array( 'product' => $product, 'data' => $data, 'price' => $price, 'tax' => paygrab_tax_included( $price, $data['tax_rate'] ), 'source' => $source, 'gateways' => $gateways, 'user' => wp_get_current_user(), 'error' => $error, 'law_url' => paygrab_page_url( 'tokushoho' ), ) ); } /** * Send visitors who are not logged in to the login screen. * * @return never */ public static function require_login() { $scheme = is_ssl() ? 'https://' : 'http://'; $here = isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ? $scheme . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) . esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : home_url( '/' ); wp_safe_redirect( wp_login_url( wp_validate_redirect( $here, home_url( '/' ) ) ) ); exit; } /** * Back to the checkout screen with an error. * * @param int $product_id Product. * @param string $code Error code. * @return never */ private static function back( $product_id, $code ) { $url = self::url( $product_id, paygrab_request_source() ); wp_safe_redirect( add_query_arg( 'paygrab_error', $code, $url ? $url : home_url( '/' ) ) ); exit; } /** * Handle the "Place order" form. * * @return never */ public static function place_order() { $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; if ( ! isset( $_POST['_paygrab_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_paygrab_nonce'] ) ), 'paygrab_place_order_' . $product_id ) ) { self::back( $product_id, 'nonce' ); } $user_id = get_current_user_id(); if ( ! Paygrab_Product::purchasable( $product_id ) ) { self::back( $product_id, 'unavailable' ); } if ( Paygrab_Downloads::active_for( $user_id, $product_id ) ) { self::back( $product_id, 'owned' ); } $pending = Paygrab_Orders::pending_for( $user_id, $product_id ); if ( $pending ) { wp_safe_redirect( self::complete_url( $pending ) ); exit; } $choice = isset( $_POST['payment'] ) ? sanitize_text_field( wp_unslash( $_POST['payment'] ) ) : ''; list( $gateway_id, $method ) = array_pad( explode( ':', $choice, 2 ), 2, '' ); $gateway = Paygrab_Gateways::get( $gateway_id ); if ( ! $gateway || ! $gateway->is_available() || ( $method && ! isset( $gateway->methods()[ $method ] ) ) ) { self::back( $product_id, 'gateway' ); } if ( paygrab_rate_limited( 'order_' . $user_id, 10, HOUR_IN_SECONDS ) ) { self::back( $product_id, 'busy' ); } // One order at a time for the same member and product: pressing the button twice, or two tabs, must not make two orders. $lock = 'order_' . $user_id . '_' . $product_id; if ( ! paygrab_lock( $lock ) ) { self::back( $product_id, 'busy' ); } $pending = Paygrab_Orders::pending_for( $user_id, $product_id ); if ( $pending ) { paygrab_unlock( $lock ); wp_safe_redirect( self::complete_url( $pending ) ); exit; } $order = Paygrab_Orders::create( $user_id, $product_id, $gateway, $method, array( 'source_id' => paygrab_request_source() ) ); paygrab_unlock( $lock ); if ( is_wp_error( $order ) ) { self::back( $product_id, 'failed' ); } $result = $gateway->start( $order ); if ( is_wp_error( $result ) || empty( $result['redirect'] ) ) { Paygrab_Orders::set_status( $order, 'failed', array( 'note' => is_wp_error( $result ) ? $result->get_error_message() : '', 'notify' => false, ) ); self::back( $product_id, 'failed' ); } /** * Fires after an order was placed and the payment started. * * @param Paygrab_Order $order Order. * @param Paygrab_Gateway $gateway Gateway. */ do_action( 'paygrab_order_placed', Paygrab_Orders::get( $order->id ), $gateway ); wp_safe_redirect( $result['redirect'] ); exit; } /** * A buyer cancels their own order awaiting payment. * * @return never */ public static function buyer_cancel() { $number = isset( $_POST['order'] ) ? sanitize_text_field( wp_unslash( $_POST['order'] ) ) : ''; $order = Paygrab_Orders::get_by_number( $number ); $back = paygrab_mypage_url( $number ); if ( ! $order || ! isset( $_POST['_paygrab_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_paygrab_nonce'] ) ), 'paygrab_cancel_' . $order->id ) ) { wp_safe_redirect( add_query_arg( 'paygrab_error', 'nonce', $back ) ); exit; } $gateway = Paygrab_Gateways::get( $order->gateway ); if ( $order->user_id !== get_current_user_id() || ! $order->is_pending() || ( $gateway && ! $gateway->buyer_can_cancel() ) ) { wp_safe_redirect( add_query_arg( 'paygrab_error', 'cancel', $back ) ); exit; } Paygrab_Orders::set_status( $order, 'cancelled', array( 'note' => __( '(Cancelled by the customer.)', 'paygrab' ), 'by' => 'buyer', ) ); wp_safe_redirect( add_query_arg( 'paygrab_notice', 'cancelled', $back ) ); exit; } }