Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add generic function to determine if URL is a store page #45299

Merged
merged 9 commits into from Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugins/woocommerce/changelog/add-is-store-page
@@ -0,0 +1,4 @@
Significance: minor
Type: add

Add is_store_page helper function
111 changes: 111 additions & 0 deletions plugins/woocommerce/src/Admin/WCAdminHelper.php
Expand Up @@ -122,4 +122,115 @@ 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can add a filter here for extensibility, what do you think?

Copy link
Member Author

@chihsuan chihsuan Mar 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Added a filter here 095c963 🙂

'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(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure how big of an impact it is on performance but there's a get_option() call in wc_privacy_policy_page_id()

'terms' => wc_terms_and_conditions_page_id(),
);

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure how big of an impact it is on performance but there's a get_option() call in wc_get_permalink_structure()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I think it's a minor impact. If it becomes a problem, we can optimize it later.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From ChatGPT:

The impact of calling wc_get_permalink_structure() on performance depends on various factors such as server configuration, database size, and the context in which the function is being called.

  1. Caching: If the result of wc_get_permalink_structure() is cached, subsequent calls to the function within the same request will have minimal impact on performance. WordPress itself caches the permalink structure, so in most cases, calling this function won't cause a significant performance hit.

  2. Database: The function involves querying the database to retrieve the permalink structure. The impact may be noticeable on large or busy websites with heavy database loads. However, for most websites, especially those with caching mechanisms in place, the impact should be minimal.

  3. Context: If the function is called frequently or in a critical performance path, such as within a loop that executes many times, the cumulative impact may become more significant. In such cases, it's advisable to call the function sparingly or optimize the code structure to minimize redundant calls.

  4. Server Resources: The impact also depends on the server resources available. On high-traffic websites or under heavy server load, the impact may be more noticeable compared to low-traffic websites or under light server load.

Overall, calling wc_get_permalink_structure() typically has a minor impact on performance in most WordPress environments, especially when cached properly. However, it's essential to consider the specific context and optimize code accordingly if performance issues arise.

$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 ) {
$quey = wp_parse_url( $url, PHP_URL_QUERY );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$quey = wp_parse_url( $url, PHP_URL_QUERY );
$query = wp_parse_url( $url, PHP_URL_QUERY );

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d84d9f2

$path = wp_parse_url( $url, PHP_URL_PATH ) . ( $quey ? '?' . $quey : '' );
$home_path = wp_parse_url( site_url(), PHP_URL_PATH );
$normalized_path = trim( substr( $path, strlen( $home_path ) ), '/' );
return $normalized_path;
}
}
Expand Up @@ -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.
Expand Down Expand Up @@ -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 );
}
}
}