From 8215c94aaad947d0ef0aad4df32c1f1633f9aa02 Mon Sep 17 00:00:00 2001 From: Vansh Kapoor Date: Tue, 20 Oct 2026 11:07:49 +0530 Subject: [PATCH] Normalize the activation fingerprint so URL variants of one site match get_site_url() was sent as-is, so https://www.example.com, https://example.com and http://example.com created separate activations for the same site. The scheme also follows the current request via is_ssl(), so one site could send both http and https. Activation::get_fingerprint() now forces https, lowercases the host, drops a leading www. label and a trailing slash, and keeps the port and path. A canonical https non-www URL keeps its exact previous fingerprint. Exposed via the surecart_licensing_fingerprint filter. --- src/Activation.php | 42 +++++++- tests/unit/ActivationFingerprintTest.php | 122 +++++++++++++++++++++++ 2 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 tests/unit/ActivationFingerprintTest.php diff --git a/src/Activation.php b/src/Activation.php index c482621..47b84c9 100644 --- a/src/Activation.php +++ b/src/Activation.php @@ -37,6 +37,44 @@ public function __construct( Client $client ) { $this->option_key = 'surecart_' . md5( $this->client->slug ) . '_license_activation_id'; } + /** + * Get the fingerprint that identifies this site's activation. + * + * URL variants of one site map to one value: the scheme becomes https, the + * host is lowercased, a leading "www." label is dropped and a trailing slash + * is removed. The port and path are kept, since a subdirectory install or a + * different port can be a separate site. A canonical https, non-www URL keeps + * the exact fingerprint it had before normalization. + * + * @return string + */ + public function get_fingerprint() { + $site_url = get_site_url(); + $parts = wp_parse_url( $site_url ); + $fingerprint = esc_url_raw( $site_url ); + + if ( is_array( $parts ) && ! empty( $parts['host'] ) ) { + $host = strtolower( $parts['host'] ); + if ( 0 === strpos( $host, 'www.' ) ) { + $host = substr( $host, 4 ); + } + + $fingerprint = esc_url_raw( + 'https://' . $host + . ( isset( $parts['port'] ) ? ':' . $parts['port'] : '' ) + . ( isset( $parts['path'] ) ? untrailingslashit( $parts['path'] ) : '' ) + ); + } + + /** + * Filter the activation fingerprint. + * + * @param string $fingerprint The normalized fingerprint. + * @param string $site_url The raw site URL. + */ + return apply_filters( 'surecart_licensing_fingerprint', $fingerprint, $site_url ); + } + /** * Create an activation for the license. * @@ -55,7 +93,7 @@ public function create( $license_id ) { trailingslashit( $this->endpoint ), array( 'activation' => array( - 'fingerprint' => esc_url_raw( get_site_url() ), + 'fingerprint' => $this->get_fingerprint(), 'name' => get_bloginfo(), 'license' => $license_id, ), @@ -107,7 +145,7 @@ public function update( $id = '' ) { 'PATCH', trailingslashit( $this->endpoint ) . $id, array( - 'fingerprint' => esc_url_raw( get_site_url() ), + 'fingerprint' => $this->get_fingerprint(), 'name' => get_bloginfo(), 'license' => $license_key, ) diff --git a/tests/unit/ActivationFingerprintTest.php b/tests/unit/ActivationFingerprintTest.php new file mode 100644 index 0000000..790850c --- /dev/null +++ b/tests/unit/ActivationFingerprintTest.php @@ -0,0 +1,122 @@ +client = new \SureCart\Licensing\Client( 'SureCart', __FILE__ ); + $this->bodies = array(); + add_filter( 'pre_http_request', array( $this, 'stub' ), 10, 3 ); + } + + public function tear_down() { + remove_filter( 'pre_http_request', array( $this, 'stub' ), 10 ); + remove_all_filters( 'surecart_licensing_fingerprint' ); + parent::tear_down(); + } + + /** + * Record the request body and answer with a created activation. + * + * @param false|array|\WP_Error $pre Unused, required by the filter. + * @param array $args Request arguments. + * @param string $url Request URL. + * + * @return array + */ + public function stub( $pre, $args, $url ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + $this->bodies[] = is_array( $args['body'] ) ? $args['body'] : json_decode( $args['body'], true ); + return array( + 'headers' => array(), + 'cookies' => array(), + 'response' => array( + 'code' => 201, + 'message' => '', + ), + 'body' => wp_json_encode( array( 'id' => 'act_new' ) ), + ); + } + + /** + * Fingerprint for a given site URL. + * + * @param string $site_url The site URL. + * + * @return string + */ + private function fingerprint_for( $site_url ) { + update_option( 'siteurl', $site_url ); + return $this->client->activation()->get_fingerprint(); + } + + public function test_get_fingerprint_is_unchanged_for_a_canonical_https_url() { + // The common case must keep its existing fingerprint, so its activation is still matched. + $this->assertSame( 'https://example.com', $this->fingerprint_for( 'https://example.com' ) ); + } + + public function test_get_fingerprint_maps_www_scheme_case_and_trailing_slash_variants_to_one_value() { + foreach ( array( 'https://www.example.com', 'http://example.com', 'http://www.example.com', 'https://WWW.Example.COM', 'https://example.com/' ) as $variant ) { + $this->assertSame( 'https://example.com', $this->fingerprint_for( $variant ), $variant ); + } + } + + public function test_get_fingerprint_keeps_the_path_and_port() { + // Subdirectory installs and ports can be different sites. + $this->assertSame( 'https://example.com/shop', $this->fingerprint_for( 'https://www.example.com/shop/' ) ); + $this->assertSame( 'https://example.com/Shop', $this->fingerprint_for( 'https://example.com/Shop' ) ); + $this->assertSame( 'https://localhost:8888', $this->fingerprint_for( 'http://localhost:8888' ) ); + } + + public function test_get_fingerprint_only_strips_a_leading_www_label() { + $this->assertSame( 'https://wwwexample.com', $this->fingerprint_for( 'https://wwwexample.com' ) ); + $this->assertSame( 'https://shop.www.example.com', $this->fingerprint_for( 'https://shop.www.example.com' ) ); + } + + public function test_get_fingerprint_falls_back_to_the_raw_url_without_a_host() { + add_filter( + 'site_url', + function () { + return 'not a url'; + } + ); + $this->assertSame( esc_url_raw( 'not a url' ), $this->client->activation()->get_fingerprint() ); + remove_all_filters( 'site_url' ); + } + + public function test_get_fingerprint_is_filterable() { + update_option( 'siteurl', 'https://www.example.com' ); + add_filter( + 'surecart_licensing_fingerprint', + function ( $fingerprint, $site_url ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + return $site_url; + }, + 10, + 2 + ); + $this->assertSame( get_site_url(), $this->client->activation()->get_fingerprint() ); + } + + public function test_create_sends_the_normalized_fingerprint() { + update_option( 'siteurl', 'https://www.example.com/' ); + + $this->client->activation()->create( 'lic_1' ); + + $this->assertSame( 'https://example.com', $this->bodies[0]['activation']['fingerprint'] ); + } +}