diff --git a/plugins/woocommerce/changelog/add-is-store-page b/plugins/woocommerce/changelog/add-is-store-page new file mode 100644 index 000000000000..4794367bf70b --- /dev/null +++ b/plugins/woocommerce/changelog/add-is-store-page @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add is_store_page helper function diff --git a/plugins/woocommerce/src/Admin/WCAdminHelper.php b/plugins/woocommerce/src/Admin/WCAdminHelper.php index 4e4ad1796c4b..4295b69e80d6 100644 --- a/plugins/woocommerce/src/Admin/WCAdminHelper.php +++ b/plugins/woocommerce/src/Admin/WCAdminHelper.php @@ -122,4 +122,123 @@ public static function is_site_fresh() { return $date > $month_ago; } + + /** + * Test if a URL is a store page. This function ignores the domain and protocol of the URL and only checks the path and query string. + * + * Store pages are defined as: + * + * - My Account + * - Shop + * - Cart + * - Checkout + * - Privacy Policy + * - Terms and Conditions + * + * Additionally, the following autogenerated pages should be included: + * - Product pages + * - Product Category pages + * - Product Tag pages + * + * @param string $url URL to check. If not provided, the current URL will be used. + * @return bool Whether or not the URL is a store page. + */ + public static function is_store_page( $url = '' ) { + $url = $url ? $url : esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) ); + + if ( ! $url ) { + return false; + } + $normalized_path = self::get_normalized_url_path( $url ); + + // WC store pages. + $store_pages = array( + 'myaccount' => wc_get_page_id( 'myaccount' ), + 'shop' => wc_get_page_id( 'shop' ), + 'cart' => wc_get_page_id( 'cart' ), + 'checkout' => wc_get_page_id( 'checkout' ), + 'privacy' => wc_privacy_policy_page_id(), + 'terms' => wc_terms_and_conditions_page_id(), + ); + + /** + * Filter the store pages array to check if a URL is a store page. + * + * @since 8.8.0 + * @param array $store_pages The store pages array. The keys are the page slugs and the values are the page IDs. + */ + $store_pages = apply_filters( 'woocommerce_store_pages', $store_pages ); + + foreach ( $store_pages as $page => $page_id ) { + if ( 0 >= $page_id ) { + continue; + } + + $permalink = get_permalink( $page_id ); + if ( ! $permalink ) { + continue; + } + + if ( 0 === strpos( $normalized_path, self::get_normalized_url_path( $permalink ) ) ) { + return true; + } + } + + // Check product, category and tag pages. + $permalink_structure = wc_get_permalink_structure(); + $permalink_keys = array( + 'category_base', + 'tag_base', + 'product_base', + ); + + foreach ( $permalink_keys as $key ) { + if ( ! isset( $permalink_structure[ $key ] ) || ! is_string( $permalink_structure[ $key ] ) ) { + continue; + } + + // Check if the URL path starts with the matching base. + if ( 0 === strpos( $normalized_path, trim( $permalink_structure[ $key ], '/' ) ) ) { + return true; + } + + // If the permalink structure contains placeholders, we need to check if the URL matches the structure using regex. + if ( strpos( $permalink_structure[ $key ], '%' ) !== false ) { + global $wp_rewrite; + $rules = $wp_rewrite->generate_rewrite_rule( $permalink_structure[ $key ] ); + + if ( is_array( $rules ) && ! empty( $rules ) ) { + // rule key is the regex pattern. + $rule = array_keys( $rules )[0]; + $rule = '#^' . str_replace( '?$', '', $rule ) . '#'; + + if ( preg_match( $rule, $normalized_path ) ) { + return true; + } + } + } + } + + return false; + } + + /** + * Get normalized URL path. + * 1. Only keep the path and query string (if any). + * 2. Remove wp home path from the URL path if WP is installed in a subdirectory. + * 3. Remove leading and trailing slashes. + * + * For example: + * + * - https://example.com/wordpress/shop/uncategorized/test/?add-to-cart=123 => shop/uncategorized/test/?add-to-cart=123 + * + * @param string $url URL to normalize. + */ + private static function get_normalized_url_path( $url ) { + $query = wp_parse_url( $url, PHP_URL_QUERY ); + $path = wp_parse_url( $url, PHP_URL_PATH ) . ( $query ? '?' . $query : '' ); + $home_path = wp_parse_url( site_url(), PHP_URL_PATH ); + $normalized_path = trim( substr( $path, strlen( $home_path ) ), '/' ); + return $normalized_path; + } } diff --git a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/wc-admin-helper.php b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/wc-admin-helper.php index 03d23a3ac326..f132325adbd2 100644 --- a/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/wc-admin-helper.php +++ b/plugins/woocommerce/tests/legacy/unit-tests/woocommerce-admin/wc-admin-helper.php @@ -12,7 +12,41 @@ * * @package WooCommerce\Admin\Tests\WCAdminHelper */ -class WC_Admin_Tests_Admin_Helper extends WP_UnitTestCase { +class WC_Admin_Tests_Admin_Helper extends WC_Unit_Test_Case { + /** + * Set up before class. + */ + public static function setUpBeforeClass(): void { + parent::setUpBeforeClass(); + + // Ensure pages exist. + WC_Install::create_pages(); + + // Set up permalinks. + update_option( + 'woocommerce_permalinks', + array( + 'product_base' => '/shop/%product_cat%', + 'category_base' => 'product-category', + 'tag_base' => 'product-tag', + 'attribute_base' => 'test', + 'use_verbose_page_rules' => true, + ) + ); + } + + /** + * Tear down after class. + */ + public static function tearDownAfterClass(): void { + // Delete pages. + wp_delete_post( get_option( 'woocommerce_shop_page_id' ), true ); + wp_delete_post( get_option( 'woocommerce_cart_page_id' ), true ); + wp_delete_post( get_option( 'woocommerce_checkout_page_id' ), true ); + wp_delete_post( get_option( 'woocommerce_myaccount_page_id' ), true ); + wp_delete_post( wc_privacy_policy_page_id(), true ); + wp_delete_post( wc_terms_and_conditions_page_id(), true ); + } /** * Test get_wcadmin_active_for_in_seconds_with with invalid timestamp option. @@ -176,4 +210,52 @@ public function test_is_fresh_site_fresh_site_option_must_be_1() { update_option( 'fresh_site', '1' ); $this->assertTrue( WCAdminHelper::is_site_fresh() ); } + + /** + * Get store page test data. This data is used to test is_store_page function. + * + * We don't use the data provider in this test because data provider are executed before setUpBeforeClass and cause other tests to fail since we need to create pages to generate the test data. + * + * @return array[] list of store page test data. + */ + public function get_store_page_test_data() { + return array( + array( get_permalink( wc_get_page_id( 'cart' ) ), true ), // Test case 1: URL matches cart page. + array( get_permalink( wc_get_page_id( 'myaccount' ) ) . '/orders/', true ), // Test case 2: URL matches my account > orders page. + array( 'https://example.com/product-category/sample-category/', true ), // Test case 3: URL matches product category page. + array( 'https://example.com/product-tag/sample-tag/', true ), // Test case 4: URL matches product tag page. + array( 'https://example.com/shop/uncategorized/test/', true ), // Test case 5: URL matches product page. + array( '/shop/t-shirt/test/', true ), // Test case 6: URL path matches product page. + array( 'https://example.com/about-us/', false ), // Test case 7: URL does not match any store page. + ); + } + + /** + * + * Test is_store_page function with different URLs. + * + */ + public function test_is_store_page() { + global $wp_rewrite; + + $wp_rewrite = $this->getMockBuilder( 'WP_Rewrite' )->getMock(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + + $permalink_structure = array( + 'category_base' => 'product-category', + 'tag_base' => 'product-tag', + 'product_base' => 'product', + ); + + $wp_rewrite->expects( $this->any() ) + ->method( 'generate_rewrite_rule' ) + ->willReturn( array( 'shop/(.+?)/?$' => 'index.php?product_cat=$matches[1]&year=$matches[2]' ) ); + + $test_data = $this->get_store_page_test_data(); + + foreach ( $test_data as $data ) { + list( $url, $expected_result ) = $data; + $result = WCAdminHelper::is_store_page( $url ); + $this->assertEquals( $expected_result, $result ); + } + } }