From bdb13b88d1b1ad7b0c606ee46a1556987339a296 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 15 May 2023 16:35:46 +0100 Subject: [PATCH] Permalink solution for the checkout endpoint/template (#9406) * Checkout endpoint work * Move setting field to util * Include link to edit the template * Remove todo * Refactor checkout templates to share logic (#9411) * Sync endpoints with pages (#9426) * Switch to page syncing * Update settings descriptions --------- Co-authored-by: Paulo Arromba <17236129+wavvves@users.noreply.github.com> --- src/BlockTemplatesController.php | 115 +++++++++++++++++++- src/BlockTypes/ClassicTemplate.php | 2 +- src/Templates/AbstractPageTemplate.php | 138 ++++++++++++++++++++++++ src/Templates/CartTemplate.php | 68 ++++-------- src/Templates/CheckoutTemplate.php | 68 ++++-------- src/Templates/OrderReceivedTemplate.php | 47 ++++---- src/Utils/BlockTemplateUtils.php | 6 +- src/Utils/SettingsUtils.php | 46 ++++++++ 8 files changed, 362 insertions(+), 128 deletions(-) create mode 100644 src/Templates/AbstractPageTemplate.php create mode 100644 src/Utils/SettingsUtils.php diff --git a/src/BlockTemplatesController.php b/src/BlockTemplatesController.php index ced5cb74f8b..02abc9831d1 100644 --- a/src/BlockTemplatesController.php +++ b/src/BlockTemplatesController.php @@ -1,7 +1,6 @@ block_template_is_available( CartTemplate::SLUG ) + ! BlockTemplateUtils::theme_has_template( CartTemplate::get_slug() ) && $this->block_template_is_available( CartTemplate::get_slug() ) ) { add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 ); } elseif ( is_checkout() && - ! BlockTemplateUtils::theme_has_template( CheckoutTemplate::SLUG ) && $this->block_template_is_available( CheckoutTemplate::SLUG ) + ! BlockTemplateUtils::theme_has_template( CheckoutTemplate::get_slug() ) && $this->block_template_is_available( CheckoutTemplate::get_slug() ) ) { add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 ); } elseif ( is_wc_endpoint_url( 'order-received' ) - && ! BlockTemplateUtils::theme_has_template( OrderReceivedTemplate::SLUG ) - && $this->block_template_is_available( OrderReceivedTemplate::SLUG ) + && ! BlockTemplateUtils::theme_has_template( OrderReceivedTemplate::get_slug() ) + && $this->block_template_is_available( OrderReceivedTemplate::get_slug() ) ) { add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 ); } else { @@ -658,4 +665,102 @@ function_exists( 'is_shop' ) && return $post_type_name; } + + /** + * Replaces page settings in WooCommerce with text based permalinks which point to a template. + * + * @param array $settings Settings pages. + * @return array + */ + public function template_permalink_settings( $settings ) { + foreach ( $settings as $key => $setting ) { + if ( 'woocommerce_checkout_page_id' === $setting['id'] ) { + $checkout_page = CheckoutTemplate::get_placeholder_page(); + $settings[ $key ] = [ + 'title' => __( 'Checkout page', 'woo-gutenberg-products-block' ), + 'desc' => sprintf( + // translators: %1$s: opening anchor tag, %2$s: closing anchor tag. + __( 'The checkout template can be %1$s edited here%2$s.', 'woo-gutenberg-products-block' ), + '', + '' + ), + 'desc_tip' => __( 'This is the URL to the checkout page.', 'woo-gutenberg-products-block' ), + 'id' => 'woocommerce_checkout_page_endpoint', + 'type' => 'permalink', + 'default' => $checkout_page ? $checkout_page->post_name : CheckoutTemplate::get_slug(), + 'autoload' => false, + ]; + } + if ( 'woocommerce_cart_page_id' === $setting['id'] ) { + $cart_page = CartTemplate::get_placeholder_page(); + $settings[ $key ] = [ + 'title' => __( 'Cart page', 'woo-gutenberg-products-block' ), + 'desc' => sprintf( + // translators: %1$s: opening anchor tag, %2$s: closing anchor tag. + __( 'The cart template can be %1$s edited here%2$s.', 'woo-gutenberg-products-block' ), + '', + '' + ), + 'desc_tip' => __( 'This is the URL to the cart page.', 'woo-gutenberg-products-block' ), + 'id' => 'woocommerce_cart_page_endpoint', + 'type' => 'permalink', + 'default' => $cart_page ? $cart_page->post_name : CartTemplate::get_slug(), + 'autoload' => false, + ]; + } + } + + return $settings; + } + + /** + * Syncs entered permalink with the pages and returns the correct value. + * + * @param string $value Value of the option. + * @param string $option Name of the option. + * @return string + */ + public function update_template_permalink( $value, $option ) { + if ( 'woocommerce_checkout_page_endpoint' === $option ) { + return $this->sync_endpoint_with_page( CheckoutTemplate::get_placeholder_page(), 'checkout', $value ); + } + if ( 'woocommerce_cart_page_endpoint' === $option ) { + return $this->sync_endpoint_with_page( CartTemplate::get_placeholder_page(), 'cart', $value ); + } + return $value; + } + + /** + * Syncs the provided permalink with the actual WP page. + * + * @param WP_Post|null $page The page object, or null if it does not exist. + * @param string $page_slug The identifier for the page e.g. cart, checkout. + * @param string $permalink The new permalink to use. + * @return string THe actual permalink assigned to the page. May differ from $permalink if it was already taken. + */ + protected function sync_endpoint_with_page( $page, $page_slug, $permalink ) { + if ( ! $page ) { + $updated_page_id = wc_create_page( + esc_sql( $permalink ), + 'woocommerce_' . $page_slug . '_page_id', + $page_slug, + '', + '', + 'publish' + ); + } else { + $updated_page_id = wp_update_post( + [ + 'ID' => $page->ID, + 'post_name' => esc_sql( $permalink ), + ] + ); + } + + // Get post again in case slug was updated with a suffix. + if ( $updated_page_id && ! is_wp_error( $updated_page_id ) ) { + return get_post( $updated_page_id )->post_name; + } + return $permalink; + } } diff --git a/src/BlockTypes/ClassicTemplate.php b/src/BlockTypes/ClassicTemplate.php index 5720eb51cf7..090f15b8e8d 100644 --- a/src/BlockTypes/ClassicTemplate.php +++ b/src/BlockTypes/ClassicTemplate.php @@ -63,7 +63,7 @@ protected function render( $attributes, $content, $block ) { $frontend_scripts::load_scripts(); } - if ( OrderReceivedTemplate::SLUG === $attributes['template'] ) { + if ( OrderReceivedTemplate::get_slug() === $attributes['template'] ) { return $this->render_order_received(); } diff --git a/src/Templates/AbstractPageTemplate.php b/src/Templates/AbstractPageTemplate.php new file mode 100644 index 00000000000..df267c9525d --- /dev/null +++ b/src/Templates/AbstractPageTemplate.php @@ -0,0 +1,138 @@ +init(); + } + } + + /** + * Initialization method. + */ + protected function init() { + add_filter( 'page_template_hierarchy', array( $this, 'page_template_hierarchy' ), 1 ); + add_filter( 'frontpage_template_hierarchy', array( $this, 'page_template_hierarchy' ), 1 ); + add_filter( 'woocommerce_blocks_template_content', array( $this, 'page_template_content' ), 10, 2 ); + add_action( 'current_screen', array( $this, 'page_template_editor_redirect' ) ); + add_filter( 'pre_get_document_title', array( $this, 'page_template_title' ) ); + } + + /** + * Returns the template slug. + * + * @return string + */ + abstract public static function get_slug(); + + /** + * Returns the page object assigned to this template/page. + * + * @return \WP_Post|null Post object or null. + */ + abstract public static function get_placeholder_page(); + + /** + * Should return the title of the page. + * + * @return string + */ + abstract public static function get_template_title(); + + /** + * Should return true on pages/endpoints/routes where the template should be shown. + * + * @return boolean + */ + abstract protected function is_active_template(); + + /** + * Returns the URL to edit the template. + * + * @return string + */ + protected function get_edit_template_url() { + return admin_url( 'site-editor.php?postType=wp_template&postId=woocommerce%2Fwoocommerce%2F%2F' . $this->get_slug() ); + } + + /** + * Get the default content for a template. + * + * Overridden by child class to include their own logic. + * + * @param string $template_content The original content of the template. + * @return string + */ + protected function get_default_template_content( $template_content ) { + return $template_content; + } + + /** + * When the page should be displaying the template, add it to the hierarchy. + * + * This places the template name e.g. `cart`, at the beginning of the template hierarchy array. The hook priority + * is 1 to ensure it runs first; other consumers e.g. extensions, could therefore inject their own template instead + * of this one when using the default priority of 10. + * + * @param array $templates Templates that match the pages_template_hierarchy. + */ + public function page_template_hierarchy( $templates ) { + if ( $this->is_active_template() ) { + array_unshift( $templates, $this->get_slug() ); + } + return $templates; + } + + /** + * Returns the default template content. + * + * @param string $template_content The content of the template. + * @param object $template_file The template file object. + * @return string + */ + public function page_template_content( $template_content, $template_file ) { + if ( $this->get_slug() !== $template_file->slug ) { + return $template_content; + } + return $this->get_default_template_content( $template_content ); + } + + /** + * Redirect the edit page screen to the template editor. + * + * @param \WP_Screen $current_screen Current screen information. + */ + public function page_template_editor_redirect( \WP_Screen $current_screen ) { + $page = $this->get_placeholder_page(); + $edit_page_id = 'page' === $current_screen->id && ! empty( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + if ( $page && $edit_page_id === $page->id ) { + wp_safe_redirect( $this->get_edit_template_url() ); + exit; + } + } + + /** + * Filter the page title when the template is active. + * + * @param string $title Page title. + * @return string + */ + public function page_template_title( $title ) { + if ( $this->is_active_template() ) { + return $this->get_template_title(); + } + return $title; + } +} diff --git a/src/Templates/CartTemplate.php b/src/Templates/CartTemplate.php index 4064b53aa9b..f5c916cd3ff 100644 --- a/src/Templates/CartTemplate.php +++ b/src/Templates/CartTemplate.php @@ -6,78 +6,52 @@ * * @internal */ -class CartTemplate { - - const SLUG = 'cart'; - +class CartTemplate extends AbstractPageTemplate { /** - * Constructor. + * Template slug. * - * Templates require FSE theme support, so this will only init if a FSE theme is active. + * @return string */ - public function __construct() { - // Templates require FSE theme support. - if ( ! wc_current_theme_is_fse_theme() ) { - return; - } - $this->init(); + public static function get_slug() { + return 'cart'; } /** - * Initialization method. + * Returns the page object assigned to this template/page. + * + * @return \WP_Post|null Post object or null. */ - protected function init() { - add_filter( 'page_template_hierarchy', array( $this, 'update_page_template_hierarchy' ), 1 ); - add_action( 'current_screen', array( $this, 'template_editor_redirect' ) ); - add_filter( 'woocommerce_blocks_template_content', array( $this, 'default_template_content' ), 10, 3 ); + public static function get_placeholder_page() { + $page_id = wc_get_page_id( 'cart' ); + return $page_id ? get_post( $page_id ) : null; } /** - * When the page is displaying the cart and a block theme is active, render the Cart Template. - * - * This places the template name e.g. `cart`, at the beginning of the template hierarchy array. The hook priority - * is 1 to ensure it runs first; other consumers e.g. extensions, could therefore inject their own template instead - * of this one when using the default priority of 10. + * True when viewing the cart page or cart endpoint. * - * @param array $templates Templates that match the pages_template_hierarchy. + * @return boolean */ - public function update_page_template_hierarchy( $templates ) { - if ( is_cart() ) { - array_unshift( $templates, self::SLUG ); - } - return $templates; + protected function is_active_template() { + return is_cart(); } /** - * Redirect a page to the template editor if it's the checkout page and a block theme is active. + * Should return the title of the page. * - * @param \WP_Screen $current_screen Current screen information. + * @return string */ - public function template_editor_redirect( \WP_Screen $current_screen ) { - $page_id = wc_get_page_id( 'cart' ) ?: false; - $edit_page_id = 'page' === $current_screen->id && ! empty( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - if ( $edit_page_id === $page_id ) { - wp_safe_redirect( admin_url( 'site-editor.php?postType=wp_template&postId=woocommerce%2Fwoocommerce%2F%2Fcart' ) ); - exit; - } + public static function get_template_title() { + return __( 'Cart', 'woo-gutenberg-products-block' ); } /** * Migrates an existing page using blocks to the block templates. * * @param string $template_content The content of the template. - * @param object $template_file The template file object. - * @param string $template_type The type of template. * @return string */ - public function default_template_content( $template_content, $template_file, $template_type ) { - if ( self::SLUG !== $template_file->slug ) { - return $template_content; - } - - $page_id = wc_get_page_id( 'cart' ); - $page = $page_id ? get_post( $page_id ) : false; + public function get_default_template_content( $template_content ) { + $page = $this->get_placeholder_page(); if ( $page && ! empty( $page->post_content ) ) { $template_content = ' diff --git a/src/Templates/CheckoutTemplate.php b/src/Templates/CheckoutTemplate.php index 8a32e3e79bf..6c991922b81 100644 --- a/src/Templates/CheckoutTemplate.php +++ b/src/Templates/CheckoutTemplate.php @@ -6,78 +6,52 @@ * * @internal */ -class CheckoutTemplate { - - const SLUG = 'checkout'; - +class CheckoutTemplate extends AbstractPageTemplate { /** - * Constructor. + * Template slug. * - * Templates require FSE theme support, so this will only init if a FSE theme is active. + * @return string */ - public function __construct() { - // Templates require FSE theme support. - if ( ! wc_current_theme_is_fse_theme() ) { - return; - } - $this->init(); + public static function get_slug() { + return 'checkout'; } /** - * Initialization method. + * Returns the page object assigned to this template/page. + * + * @return \WP_Post|null Post object or null. */ - protected function init() { - add_filter( 'page_template_hierarchy', array( $this, 'update_page_template_hierarchy' ), 1 ); - add_action( 'current_screen', array( $this, 'template_editor_redirect' ) ); - add_filter( 'woocommerce_blocks_template_content', array( $this, 'default_template_content' ), 10, 3 ); + public static function get_placeholder_page() { + $page_id = wc_get_page_id( 'checkout' ); + return $page_id ? get_post( $page_id ) : null; } /** - * When the page is displaying the checkout and a block theme is active, render the Checkout Template. - * - * This places the template name e.g. `checkout`, at the beginning of the template hierarchy array. The hook priority - * is 1 to ensure it runs first; other consumers e.g. extensions, could therefore inject their own template instead - * of this one when using the default priority of 10. + * Should return the title of the page. * - * @param array $templates Templates that match the pages_template_hierarchy. + * @return string */ - public function update_page_template_hierarchy( $templates ) { - if ( is_checkout() ) { - array_unshift( $templates, self::SLUG ); - } - return $templates; + public static function get_template_title() { + return __( 'Checkout', 'woo-gutenberg-products-block' ); } /** - * Redirect a page to the template editor if it's the checkout page and a block theme is active. + * True when viewing the cart page or cart endpoint. * - * @param \WP_Screen $current_screen Current screen information. + * @return boolean */ - public function template_editor_redirect( \WP_Screen $current_screen ) { - $page_id = wc_get_page_id( 'checkout' ); - $edit_page_id = 'page' === $current_screen->id && ! empty( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - if ( $edit_page_id === $page_id ) { - wp_safe_redirect( admin_url( 'site-editor.php?postType=wp_template&postId=woocommerce%2Fwoocommerce%2F%2Fcheckout' ) ); - exit; - } + public function is_active_template() { + return is_checkout(); } /** * Migrates an existing page using blocks to the block templates. * * @param string $template_content The content of the template. - * @param object $template_file The template file object. - * @param string $template_type The type of template. * @return string */ - public function default_template_content( $template_content, $template_file, $template_type ) { - if ( self::SLUG !== $template_file->slug ) { - return $template_content; - } - - $page_id = wc_get_page_id( 'checkout' ); - $page = $page_id ? get_post( $page_id ) : false; + public function get_default_template_content( $template_content ) { + $page = $this->get_placeholder_page(); if ( $page && ! empty( $page->post_content ) ) { $template_content = ' diff --git a/src/Templates/OrderReceivedTemplate.php b/src/Templates/OrderReceivedTemplate.php index e6707f06b3a..3fd14e9d22d 100644 --- a/src/Templates/OrderReceivedTemplate.php +++ b/src/Templates/OrderReceivedTemplate.php @@ -6,43 +6,40 @@ * * @internal */ -class OrderReceivedTemplate { - - const SLUG = 'order-received'; - +class OrderReceivedTemplate extends AbstractPageTemplate { /** - * Constructor. + * Template slug. * - * Templates require FSE theme support, so this will only init if a FSE theme is active. + * @return string */ - public function __construct() { - // Templates require FSE theme support. - if ( ! wc_current_theme_is_fse_theme() ) { - return; - } - $this->init(); + public static function get_slug() { + return 'order-received'; } /** - * Initialization method. + * Returns the page object assigned to this template/page. + * + * @return \WP_Post|null Post object or null. */ - protected function init() { - add_filter( 'page_template_hierarchy', array( $this, 'update_page_template_hierarchy' ), 1 ); + public static function get_placeholder_page() { + return null; } /** - * When it's the Order Received page and a block theme is active, render the Order Received Template. + * True when viewing the cart page or cart endpoint. * - * This places the template name e.g. `order-received`, at the beginning of the template hierarchy array. The hook priority - * is 1 to ensure it runs first; other consumers e.g. extensions, could therefore inject their own template instead - * of this one when using the default priority of 10. + * @return boolean + */ + protected function is_active_template() { + return is_wc_endpoint_url( 'order-received' ); + } + + /** + * Should return the title of the page. * - * @param array $templates Templates that match the pages_template_hierarchy. + * @return string */ - public function update_page_template_hierarchy( $templates ) { - if ( is_wc_endpoint_url( 'order-received' ) ) { - array_unshift( $templates, self::SLUG ); - } - return $templates; + public static function get_template_title() { + return __( 'Order Received', 'woo-gutenberg-products-block' ); } } diff --git a/src/Utils/BlockTemplateUtils.php b/src/Utils/BlockTemplateUtils.php index d83ef525fad..963d4a5b97b 100644 --- a/src/Utils/BlockTemplateUtils.php +++ b/src/Utils/BlockTemplateUtils.php @@ -350,15 +350,15 @@ public static function get_plugin_block_template_types() { 'title' => _x( 'Mini-Cart', 'Template name', 'woo-gutenberg-products-block' ), 'description' => __( 'Template used to display the Mini-Cart drawer.', 'woo-gutenberg-products-block' ), ), - CartTemplate::SLUG => array( + CartTemplate::get_slug() => array( 'title' => _x( 'Cart', 'Template name', 'woo-gutenberg-products-block' ), 'description' => __( 'Template used to display the Cart.', 'woo-gutenberg-products-block' ), ), - CheckoutTemplate::SLUG => array( + CheckoutTemplate::get_slug() => array( 'title' => _x( 'Checkout', 'Template name', 'woo-gutenberg-products-block' ), 'description' => __( 'Template used to display the Checkout.', 'woo-gutenberg-products-block' ), ), - OrderReceivedTemplate::SLUG => array( + OrderReceivedTemplate::get_slug() => array( 'title' => _x( 'Order Received', 'Template name', 'woo-gutenberg-products-block' ), 'description' => __( 'Displays the order confirmation page.', 'woo-gutenberg-products-block' ), ), diff --git a/src/Utils/SettingsUtils.php b/src/Utils/SettingsUtils.php new file mode 100644 index 00000000000..892af1c1c2f --- /dev/null +++ b/src/Utils/SettingsUtils.php @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + +