init(); } /** * Initialize the Protected_Router class * * @return void */ private function init() { /** * Create the rewrite rule for the route */ add_action( 'init', [ $this, 'add_rewrite_rule' ], 10 ); /** * Add the query var for the route */ add_filter( 'query_vars', [ $this, 'add_query_var' ], 1, 1 ); /** * Redirects the route to the graphql processor */ add_action( 'pre_get_posts', [ $this, 'resolve_request' ], 1 ); } /** * Returns the Protected_Router singleton instance. * * @return \WPGraphQL\WooCommerce\Utils\Protected_Router */ public static function instance() { if ( is_null( self::$instance ) ) { self::$instance = new self(); } // Return the Protected_Router Instance. return self::$instance; } /** * Initializes the Protected_Router singleton. * * @return void */ public static function initialize() { self::instance(); } /** * Throw error on object clone. * The whole idea of the singleton design pattern is that there is a single object * therefore, we don't want the object to be cloned. * * @return void */ public function __clone() { // Cloning instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'Protected_Router class should not be cloned.', 'graphql-for-ecommerce' ), esc_html( WPGRAPHQL_WOOCOMMERCE_VERSION ) ); } /** * Disable unserializing of the class. * * @return void */ public function __wakeup() { // De-serializing instances of the class is forbidden. _doing_it_wrong( __FUNCTION__, esc_html__( 'De-serializing instances of the Protected_Router class is not allowed', 'graphql-for-ecommerce' ), esc_html( WPGRAPHQL_WOOCOMMERCE_VERSION ) ); } /** * Adds rewrite rule for the route endpoint * * @return void */ public function add_rewrite_rule() { add_rewrite_rule( self::$route . '/?$', 'index.php?' . self::$route . '=true', 'top' ); } /** * Adds the query_var for the route * * @param array $query_vars The array of whitelisted query variables. * * @return array */ public function add_query_var( $query_vars ) { $query_vars[] = self::$route; return $query_vars; } /** * Returns true when the current request is a request to download the plugin. * * @return boolean */ public static function is_auth_request() { $is_auth_request = false; if ( isset( $_GET[ self::$route ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $is_auth_request = true; } elseif ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) { // Check the server to determine if the auth endpoint is being requested. $host = wp_unslash( $_SERVER['HTTP_HOST'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $uri = wp_unslash( $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! is_string( $host ) ) { return false; } if ( ! is_string( $uri ) ) { return false; } $parsed_site_url = wp_parse_url( site_url( self::$route ), PHP_URL_PATH ); $auth_url = ! empty( $parsed_site_url ) ? wp_unslash( $parsed_site_url ) : self::$route; $parsed_request_url = wp_parse_url( $uri, PHP_URL_PATH ); $request_url = ! empty( $parsed_request_url ) ? wp_unslash( $parsed_request_url ) : ''; // Determine if the route is indeed a download request. $is_auth_request = false !== strpos( $request_url, $auth_url ); }//end if /** * Filter whether the request is a download request. Default is false. * * @param boolean $is_download_request Whether the request is a request to download the plugin. Default false. */ return apply_filters( 'woographql_is_auth_request', $is_auth_request ); } /** * This resolves the http request and ensures that WordPress can respond with the appropriate * response instead of responding with a template from the standard WordPress Template * Loading process * * @param \WP_Query $query The WP_Query instance (passed by pre_get_posts). * * @return void */ public function resolve_request( $query ) { // Only handle the main front-end query. Other WP_Query instances // (e.g. from Elementor during init) must be ignored to avoid // calling WC session methods before WooCommerce is ready. if ( ! $query->is_main_query() || is_admin() ) { return; } /** * Remove the resolve_request function from the pre_get_posts action * to prevent an infinite loop */ remove_action( 'pre_get_posts', [ $this, 'resolve_request' ], 1 ); /** * Ensure we're on the registered route for the transfer */ if ( ! $this->is_auth_request() ) { return; } // Bail if WooCommerce session is not yet initialized. if ( ! function_exists( 'WC' ) || is_null( WC()->session ) ) { return; } /** * Set is_home to false */ $query->is_home = false; /** * Process the GraphQL query Request */ $this->process_auth_request(); } /** * Returns the name of all the valid nonce names. * * @return array */ public static function get_nonce_names() { $enabled_authorizing_url_fields = wc_graphql_enabled_authorizing_url_fields(); $nonce_names = []; if ( ! empty( $enabled_authorizing_url_fields ) ) { foreach ( array_keys( $enabled_authorizing_url_fields ) as $field ) { $nonce_names[ $field ] = wc_graphql_get_authorizing_url_nonce_param_name( $field ); } } // Download URL nonce is always registered. $nonce_names['download_url'] = woographql_setting( 'download_url_nonce_param', '_wc_download' ); return array_filter( $nonce_names ); } /** * Returns the nonce action prefix for the provided field. * * @param string $field Field. * @return string|null */ public function get_nonce_prefix( $field ) { switch ( $field ) { case 'cart_url': return 'load-cart_'; case 'checkout_url': return 'load-checkout_'; case 'account_url': return 'load-account_'; case 'add_payment_method_url': return 'add-payment-method_'; case 'download_url': return 'download_'; default: return apply_filters( 'woographql_auth_nonce_prefix', null, $field, $this ); } } /** * Returns the target endpoint url for the provided field. * * @todo Add error logging here when WC Page needs to be created. * * @param string $field Field. * @return string|null */ public function get_target_endpoint( $field ) { switch ( $field ) { case 'cart_url': $cart_page_id = wc_get_page_id( 'cart' ); $cart_page_url = get_permalink( $cart_page_id ); return $cart_page_url ? $cart_page_url : null; case 'checkout_url': return wc_get_endpoint_url( 'checkout' ); case 'account_url': $account_page_id = get_option( 'woocommerce_myaccount_page_id' ); $account_page_url = get_permalink( $account_page_id ); return $account_page_url ? $account_page_url : null; case 'add_payment_method_url': return wc_get_account_endpoint_url( 'add-payment-method' ); case 'download_url': return $this->get_download_target_url(); default: return apply_filters( 'woographql_auth_target_endpoint', null, $field, $this ); } } /** * Resolves the download URL for the current request using the download_id and session_id. * * @return string|null */ private function get_download_target_url() { $download_id = isset( $_REQUEST['download_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['download_id'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $session_id = isset( $_REQUEST['session_id'] ) ? absint( $_REQUEST['session_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $download_id ) || empty( $session_id ) ) { return null; } $downloads = wc_get_customer_available_downloads( $session_id ); foreach ( $downloads as $download ) { if ( $download['download_id'] === $download_id ) { return $download['download_url']; } } return null; } /** * Redirects to homepage. * * @return void */ private function redirect_to_home() { status_header( 404 ); wp_safe_redirect( home_url() ); exit; } /** * Send stable version of plugin to download. * * @throws \Exception Session not found. * * @return void */ private function process_auth_request() { // Bail early if session ID or nonce not found. $nonce_names = $this->get_nonce_names(); if ( empty( $nonce_names ) ) { $this->redirect_to_home(); return; } /** * Nonce prefix * * @var string $nonce_prefix */ $nonce_prefix = null; /** * Session ID * * @var string $session_id */ $session_id = null; /** * Nonce * * @var string $nonce */ $nonce = null; /** * Field * * @var string $field */ $field = null; foreach ( $nonce_names as $possible_field => $nonce_param ) { if ( in_array( $nonce_param, array_keys( $_REQUEST ), true ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $field = $possible_field; $nonce_prefix = $this->get_nonce_prefix( $field ); $session_id = isset( $_REQUEST['session_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['session_id'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $nonce = isset( $_REQUEST[ $nonce_param ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ $nonce_param ] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended break; } } if ( empty( $field ) || empty( $nonce_prefix ) || empty( $session_id ) || empty( $nonce ) ) { $this->redirect_to_home(); return; } // Bail early if session user already authenticated. if ( 0 !== get_current_user_id() && get_current_user_id() === absint( $session_id ) ) { $redirect_url = $this->get_target_endpoint( (string) $field ); if ( empty( $redirect_url ) ) { $this->redirect_to_home(); return; } wp_safe_redirect( $redirect_url ); exit; } // Unauthenticate if current user not session user. if ( 0 !== get_current_user_id() ) { wp_clear_auth_cookie(); wp_set_current_user( 0 ); } // Verify nonce. if ( null !== $nonce && ! woographql_verify_nonce( $nonce, $nonce_prefix . $session_id ) ) { $this->redirect_to_home(); } do_action( 'woographql_process_auth_request_nonce_verified' ); // If Session ID is a user ID authenticate as session user. if ( 0 !== absint( $session_id ) ) { $user_id = absint( $session_id ); wp_clear_auth_cookie(); wp_set_current_user( $user_id ); wp_set_auth_cookie( $user_id ); } /** * Session object * * @var \WPGraphQL\WooCommerce\Utils\Transfer_Session_Handler $session */ $session = \WC()->session; // Read session data connected to session ID. $session_data = $session->get_session( $session_id ); // We were passed a session ID, yet no session was found. Let's log this and bail. if ( ! is_array( $session_data ) || empty( $session_data ) ) { // TODO: Switch to WC Notices. throw new \Exception( 'Could not locate WooCommerce session on checkout' ); } // Reinitialize session and save session cookie before redirect. $session->init_session_cookie(); // Set the session variable. foreach ( $session_data as $key => $value ) { $session->set( $key, maybe_unserialize( $value ) ); } $session->set_customer_session_cookie( true ); // After session has been restored on redirect to destination. $redirect_url = $this->get_target_endpoint( (string) $field ); if ( empty( $redirect_url ) ) { $this->redirect_to_home(); return; } wp_safe_redirect( $redirect_url ); exit; } }